diff --git a/CHANGELOG.md b/CHANGELOG.md index 311068a..fba9f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] :construction: +## [2.3.11] - 2023-12-27 ![Relative date](https://img.shields.io/date/1703697133?label=) + +### Fixed + +- Fix more JSONata Expression handling for Node-Red 4.0. @Zehir +- Fix default value for node status of deconz-battery. @Zehir +- Fix deconz-battery node filter not showing devices with `state.battery`. (#228) @Zehir +- Fix HomeKit attribute BatteryLevel and StatusLowBattery for devices with `state.battery`. @Zehir + ## [2.3.10] - 2023-12-14 ![Relative date](https://img.shields.io/date/1702556236?label=) ### Fixed diff --git a/deconz.js b/deconz.js index fa20186..dbdb93d 100644 --- a/deconz.js +++ b/deconz.js @@ -49,7 +49,7 @@ module.exports = function (RED) { /** * Enable http route to JSON itemlist for each controller (controller id passed as GET query parameter) */ - RED.httpAdmin.get(NODE_PATH + "itemlist", function (req, res) { + RED.httpAdmin.get(NODE_PATH + "itemlist", async function (req, res) { try { let config = req.query; let controller = RED.nodes.getNode(config.controllerID); @@ -64,13 +64,18 @@ module.exports = function (RED) { req.query.query !== undefined && ["json", "jsonata"].includes(queryType) ) { - query = RED.util.evaluateNodeProperty( - req.query.query, - queryType, - RED.nodes.getNode(req.query.nodeID), - {}, - undefined - ); + query = await new Promise((resolve, reject) => { + RED.util.evaluateNodeProperty( + req.query.query, + queryType, + RED.nodes.getNode(req.query.nodeID), + {}, + (err, value) => { + if (err) reject(err); + else resolve(value); + } + ); + }); } } catch (e) { return res.json({ @@ -290,6 +295,7 @@ module.exports = function (RED) { if (controller && controller.constructor.name === "ServerNode") { let fakeNode = { server: controller }; let cp = new CommandParser(config.command, {}, fakeNode); + await cp.build(); let devices = []; for (let path of config.device_list) { let device = controller.device_list.getDeviceByPath(path); @@ -299,25 +305,25 @@ module.exports = function (RED) { console.warn(`Error : Device not found : '${path}'`); } } - let requests = cp.getRequests(fakeNode, devices); + let requests = await cp.getRequests(fakeNode, devices); for (const [request_id, request] of requests.entries()) { const response = await got( controller.api.url.main() + request.endpoint, { method: "PUT", retry: - Utils.getNodeProperty( + (await Utils.getNodeProperty( config.command.arg.retryonerror, this, {} - ) || 0, + )) || 0, json: request.params, responseType: "json", timeout: 2000, // TODO make configurable ? } ); await Utils.sleep( - Utils.getNodeProperty(config.delay, this, {}) || 50 + (await Utils.getNodeProperty(config.delay, this, {})) || 50 ); } res.status(200).end(); diff --git a/nodes/api.js b/nodes/api.js index c85b14d..1ece027 100644 --- a/nodes/api.js +++ b/nodes/api.js @@ -80,7 +80,7 @@ module.exports = function (RED) { // Load the config let config = node.config; let methods = ["GET", "POST", "PUT", "DELETE"]; - let method = Utils.getNodeProperty( + let method = await Utils.getNodeProperty( config.specific.method, node, message_in, @@ -88,12 +88,12 @@ module.exports = function (RED) { ); // Make sure the method is valid if (!methods.includes(method)) method = "GET"; - let endpoint = Utils.getNodeProperty( + let endpoint = await Utils.getNodeProperty( config.specific.endpoint, node, message_in ); - let payload = Utils.getNodeProperty( + let payload = await Utils.getNodeProperty( config.specific.payload, node, message_in diff --git a/nodes/battery.html b/nodes/battery.html index 41165c2..131fe58 100644 --- a/nodes/battery.html +++ b/nodes/battery.html @@ -143,7 +143,7 @@ required: false }, statustext_type: { - value: "default", + value: "auto", required: true }, search_type: { diff --git a/nodes/out.js b/nodes/out.js index 59ab339..25e2011 100644 --- a/nodes/out.js +++ b/nodes/out.js @@ -117,7 +117,7 @@ module.exports = function (RED) { return; } - let delay = Utils.getNodeProperty( + let delay = await Utils.getNodeProperty( node.config.specific.delay, this, message_in @@ -169,7 +169,7 @@ module.exports = function (RED) { let resultMsgs = []; let errorMsgs = []; let resultTimings = ["never", "after_command", "at_end"]; - let resultTiming = Utils.getNodeProperty( + let resultTiming = await Utils.getNodeProperty( node.config.specific.result, this, message_in, @@ -185,7 +185,7 @@ module.exports = function (RED) { // Make sure that all expected config are defined const command = Object.assign({}, defaultCommand, saved_command); if (command.type === "pause") { - let sleep_delay = Utils.getNodeProperty( + let sleep_delay = await Utils.getNodeProperty( command.arg.delay, this, message_in @@ -211,7 +211,8 @@ module.exports = function (RED) { try { let cp = new CommandParser(command, message_in, node); - let requests = cp.getRequests(node, devices); + await cp.build(); + let requests = await cp.getRequests(node, devices); let request_count = requests.length; for (const [request_id, request] of requests.entries()) { try { @@ -238,11 +239,11 @@ module.exports = function (RED) { { method: "PUT", retry: - Utils.getNodeProperty( + (await Utils.getNodeProperty( command.arg.retryonerror, this, message_in - ) || 0, + )) || 0, json: request.params, responseType: "json", timeout: 2000, // TODO make configurable ? @@ -343,12 +344,12 @@ module.exports = function (RED) { } if ( - Utils.getNodeProperty( + (await Utils.getNodeProperty( command.arg.aftererror, this, message_in, ["continue", "stop"] - ) === "stop" + )) === "stop" ) return; diff --git a/nodes/server.js b/nodes/server.js index 5fa9621..b3a63a4 100644 --- a/nodes/server.js +++ b/nodes/server.js @@ -823,7 +823,7 @@ module.exports = function (RED) { } } - updateNodeStatus(node, msgToSend) { + async updateNodeStatus(node, msgToSend) { if (node.server.ready === false) { node.status({ fill: "red", @@ -885,7 +885,7 @@ module.exports = function (RED) { node.status({ fill: "green", shape: "dot", - text: Utils.getNodeProperty( + text: await Utils.getNodeProperty( { type: node.config.statustext_type, value: node.config.statustext, @@ -924,6 +924,8 @@ module.exports = function (RED) { break; case "deconz-battery": let battery = dotProp.get(firstmsg, "meta.config.battery"); + if (battery === undefined) + battery = dotProp.get(firstmsg, "meta.state.battery"); if (battery === undefined) return; node.status({ fill: diff --git a/package.json b/package.json index df8b477..87f370c 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red-contrib-deconz", - "version": "2.3.10", + "version": "2.3.11", "description": "deCONZ connectivity nodes for node-red", "keywords": [ "deconz", diff --git a/src/editor/DeconzDeviceListEditor.js b/src/editor/DeconzDeviceListEditor.js index f37cf3b..24cf9a8 100644 --- a/src/editor/DeconzDeviceListEditor.js +++ b/src/editor/DeconzDeviceListEditor.js @@ -70,12 +70,18 @@ class DeconzDeviceListEditor extends DeconzEditor { options.keepOnlyMatched = true; params.query = JSON.stringify({ type: "match", + method: "OR", match: { "config.battery": { type: "complex", operator: "!==", value: undefined, }, + "state.battery": { + type: "complex", + operator: "!==", + value: undefined, + }, }, }); } diff --git a/src/runtime/CommandParser.js b/src/runtime/CommandParser.js index 8dd2cae..80d888a 100644 --- a/src/runtime/CommandParser.js +++ b/src/runtime/CommandParser.js @@ -14,24 +14,26 @@ class CommandParser { config: {}, state: {}, }; + } + async build() { switch (this.type) { case "deconz_state": switch (this.domain) { case "lights": this.valid_domain.push("lights"); - this.parseDeconzStateLightArgs(); + await this.parseDeconzStateLightArgs(); break; case "covers": this.valid_domain.push("covers"); - this.parseDeconzStateCoverArgs(); + await this.parseDeconzStateCoverArgs(); break; case "groups": this.valid_domain.push("groups"); - this.parseDeconzStateLightArgs(); + await this.parseDeconzStateLightArgs(); break; case "scene_call": - this.parseDeconzStateSceneCallArgs(); + await this.parseDeconzStateSceneCallArgs(); break; } break; @@ -52,14 +54,14 @@ class CommandParser { break; case "custom": this.valid_domain.push("any"); - this.parseCustomArgs(); + await this.parseCustomArgs(); break; } } - parseDeconzStateLightArgs() { + async parseDeconzStateLightArgs() { // On command - this.result.state.on = this.getNodeProperty( + this.result.state.on = await this.getNodeProperty( this.arg.on, ["toggle"], [ @@ -84,26 +86,28 @@ class CommandParser { switch (this.arg[k].direction) { case "set": if (k === "xy") { - let xy = this.getNodeProperty(this.arg.xy); + let xy = await this.getNodeProperty(this.arg.xy); if (Array.isArray(xy) && xy.length === 2) { this.result.state[k] = xy.map(Number); } } else { - this.result.state[k] = Number(this.getNodeProperty(this.arg[k])); + this.result.state[k] = Number( + await this.getNodeProperty(this.arg[k]) + ); } break; case "inc": this.result.state[`${k}_inc`] = Number( - this.getNodeProperty(this.arg[k]) + await this.getNodeProperty(this.arg[k]) ); break; case "dec": this.result.state[`${k}_inc`] = -Number( - this.getNodeProperty(this.arg[k]) + await this.getNodeProperty(this.arg[k]) ); break; case "detect_from_value": - let value = this.getNodeProperty(this.arg[k]); + let value = await this.getNodeProperty(this.arg[k]); switch (typeof value) { case "string": switch (value.substr(0, 1)) { @@ -130,12 +134,12 @@ class CommandParser { if (this.arg[k] === undefined || this.arg[k].value === undefined) continue; if (this.arg[k].value.length > 0) - this.result.state[k] = this.getNodeProperty(this.arg[k]); + this.result.state[k] = await this.getNodeProperty(this.arg[k]); } } - parseDeconzStateCoverArgs() { - this.result.state.open = this.getNodeProperty( + async parseDeconzStateCoverArgs() { + this.result.state.open = await this.getNodeProperty( this.arg.open, ["toggle"], [ @@ -145,7 +149,7 @@ class CommandParser { ] ); - this.result.state.stop = this.getNodeProperty( + this.result.state.stop = await this.getNodeProperty( this.arg.stop, [], [ @@ -155,31 +159,35 @@ class CommandParser { ] ); - this.result.state.lift = this.getNodeProperty(this.arg.lift, ["stop"]); - this.result.state.tilt = this.getNodeProperty(this.arg.tilt); + this.result.state.lift = await this.getNodeProperty(this.arg.lift, [ + "stop", + ]); + this.result.state.tilt = await this.getNodeProperty(this.arg.tilt); } - parseDeconzStateSceneCallArgs() { - switch (this.getNodeProperty(this.arg.scene_mode, ["single", "dynamic"])) { + async parseDeconzStateSceneCallArgs() { + switch ( + await this.getNodeProperty(this.arg.scene_mode, ["single", "dynamic"]) + ) { case "single": case undefined: this.result.scene_call = { mode: "single", - groupId: this.getNodeProperty(this.arg.group), - sceneId: this.getNodeProperty(this.arg.scene), + groupId: await this.getNodeProperty(this.arg.group), + sceneId: await this.getNodeProperty(this.arg.scene), }; break; case "dynamic": this.result.scene_call = { mode: "dynamic", - sceneName: this.getNodeProperty(this.arg.scene_name), + sceneName: await this.getNodeProperty(this.arg.scene_name), }; break; } } - parseHomekitArgs(deviceMeta) { - let values = this.getNodeProperty(this.arg.payload); + async parseHomekitArgs(deviceMeta) { + let values = await this.getNodeProperty(this.arg.payload); let allValues = values; if (dotProp.has(this.message_in, "hap.allChars")) { allValues = dotProp.get(this.message_in, "hap.allChars"); @@ -216,19 +224,19 @@ class CommandParser { dotProp.set( this.result, "state.transitiontime", - this.getNodeProperty(this.arg.transitiontime) + await this.getNodeProperty(this.arg.transitiontime) ); } - parseCustomArgs() { - let target = this.getNodeProperty(this.arg.target, [ + async parseCustomArgs() { + let target = await this.getNodeProperty(this.arg.target, [ "attribute", "state", "config", "scene_call", ]); - let command = this.getNodeProperty(this.arg.command, ["object"]); - let value = this.getNodeProperty(this.arg.payload); + let command = await this.getNodeProperty(this.arg.command, ["object"]); + let value = await this.getNodeProperty(this.arg.payload); switch (target) { case "attribute": if (command === "object") { @@ -279,7 +287,7 @@ class CommandParser { * @param devices Device[] * @returns {*[]} */ - getRequests(node, devices) { + async getRequests(node, devices) { let deconzApi = node.server.api; let requests = []; @@ -359,7 +367,7 @@ class CommandParser { config: {}, state: {}, }; - this.parseHomekitArgs(device.data); + await this.parseHomekitArgs(device.data); } // Make sure that the endpoint exist @@ -446,7 +454,7 @@ class CommandParser { return requests; } - getNodeProperty(property, noValueTypes, valueMaps) { + async getNodeProperty(property, noValueTypes, valueMaps) { if (typeof property === "undefined") return undefined; if (Array.isArray(valueMaps)) for (const map of valueMaps) @@ -457,7 +465,7 @@ class CommandParser { `${property.type}.${property.value}` === map[0]) ) return map[1]; - return Utils.getNodeProperty( + return await Utils.getNodeProperty( property, this.node, this.message_in, diff --git a/src/runtime/HomeKitFormatter.js b/src/runtime/HomeKitFormatter.js index a126449..991ec5d 100644 --- a/src/runtime/HomeKitFormatter.js +++ b/src/runtime/HomeKitFormatter.js @@ -494,15 +494,31 @@ const HomeKitFormat = (() => { .to((rawEvent, deviceMeta) => 2); // Stopped //#endregion //#region Battery - HKF.BatteryLevel = directMap(["to"], "config.battery") + HKF.BatteryLevel = new Attribute() .services("Battery") + .to((rawEvent, deviceMeta) => { + let battery = dotProp.get(rawEvent, "config.battery"); + if (battery === undefined) { + battery = dotProp.get(rawEvent, "state.battery"); + } + return battery; + }) .limit(0, 100); HKF.StatusLowBattery = new Attribute() .services("Battery") - .needEventMeta("config.battery") - .to((rawEvent, deviceMeta) => - dotProp.get(rawEvent, "config.battery") <= 15 ? 1 : 0 - ); + .needEventMeta( + (rawEvent, deviceMeta) => + dotProp.has(rawEvent, "config.battery") || + dotProp.has(rawEvent, "state.battery") + ) + .to((rawEvent, deviceMeta) => { + let battery = dotProp.get(rawEvent, "config.battery"); + if (battery === undefined) { + battery = dotProp.get(rawEvent, "state.battery"); + } + return battery <= 15 ? 1 : 0; + }); + //#endregion //#region Lock Mechanism HKF.LockTargetState = new Attribute() diff --git a/src/runtime/Utils.js b/src/runtime/Utils.js index 92af1d8..2430517 100644 --- a/src/runtime/Utils.js +++ b/src/runtime/Utils.js @@ -21,18 +21,24 @@ class Utils { return msg; } - static getNodeProperty(property, node, message_in, noValueTypes) { + static async getNodeProperty(property, node, message_in, noValueTypes) { if (typeof property !== "object") return; if (property.type === "num" && property.value === "") return; + return Array.isArray(noValueTypes) && noValueTypes.includes(property.type) ? property.type - : REDUtil.evaluateNodeProperty( - property.value, - property.type, - node, - message_in, - undefined - ); + : await new Promise((resolve, reject) => { + RED.util.evaluateNodeProperty( + property.value, + property.type, + node, + message_in, + (err, value) => { + if (err) reject(err); + else resolve(value); + } + ); + }); } static convertRange(value, r1, r2, roundValue = false, limitValue = false) {