UNPKG

node-red-contrib-deconz

Version:
478 lines (449 loc) 14.8 kB
const Utils = require("./Utils"); const HomeKitFormatter = require("./HomeKitFormatter"); const dotProp = require("dot-prop"); class CommandParser { constructor(command, message_in, node) { this.type = command.type; this.domain = command.domain; this.valid_domain = []; this.arg = command.arg; this.message_in = message_in; this.node = node; this.result = { config: {}, state: {}, }; } async build() { switch (this.type) { case "deconz_state": switch (this.domain) { case "lights": this.valid_domain.push("lights"); await this.parseDeconzStateLightArgs(); break; case "covers": this.valid_domain.push("covers"); await this.parseDeconzStateCoverArgs(); break; case "groups": this.valid_domain.push("groups"); await this.parseDeconzStateLightArgs(); break; case "scene_call": await this.parseDeconzStateSceneCallArgs(); break; } break; case "homekit": if ( this.message_in.hap !== undefined && this.message_in.hap.session === undefined ) { this.node.error( "deCONZ outptut node received a message that was not initiated by a HomeKit node. " + "Make sure you disable the 'Allow Message Passthrough' in homekit-bridge node or ensure " + "appropriate filtering of the messages." ); return null; } this.valid_domain.push("lights"); this.valid_domain.push("group"); break; case "custom": this.valid_domain.push("any"); await this.parseCustomArgs(); break; } } async parseDeconzStateLightArgs() { // On command this.result.state.on = await this.getNodeProperty( this.arg.on, ["toggle"], [ ["keep", undefined], ["set.true", true], ["set.false", false], ] ); if (["on", "true"].includes(this.result.state.on)) this.result.state.on = true; if (["off", "false"].includes(this.result.state.on)) this.result.state.on = false; // Colors commands for (const k of ["bri", "sat", "hue", "ct", "xy"]) { if ( this.arg[k] === undefined || this.arg[k].value === undefined || this.arg[k].value.length === 0 ) continue; switch (this.arg[k].direction) { case "set": if (k === "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( await this.getNodeProperty(this.arg[k]) ); } break; case "inc": this.result.state[`${k}_inc`] = Number( await this.getNodeProperty(this.arg[k]) ); break; case "dec": this.result.state[`${k}_inc`] = -Number( await this.getNodeProperty(this.arg[k]) ); break; case "detect_from_value": let value = await this.getNodeProperty(this.arg[k]); switch (typeof value) { case "string": switch (value.substr(0, 1)) { case "+": this.result.state[`${k}_inc`] = Number(value.substr(1)); break; case "-": this.result.state[`${k}_inc`] = -Number(value.substr(1)); break; default: this.result.state[k] = Number(value); break; } break; default: this.result.state[k] = Number(value); break; } break; } } for (const k of ["alert", "effect", "colorloopspeed", "transitiontime"]) { if (this.arg[k] === undefined || this.arg[k].value === undefined) continue; if (this.arg[k].value.length > 0) this.result.state[k] = await this.getNodeProperty(this.arg[k]); } } async parseDeconzStateCoverArgs() { this.result.state.open = await this.getNodeProperty( this.arg.open, ["toggle"], [ ["keep", undefined], ["set.true", true], ["set.false", false], ] ); this.result.state.stop = await this.getNodeProperty( this.arg.stop, [], [ ["keep", undefined], ["set.true", true], ["set.false", false], ] ); this.result.state.lift = await this.getNodeProperty(this.arg.lift, [ "stop", ]); this.result.state.tilt = await this.getNodeProperty(this.arg.tilt); } async parseDeconzStateSceneCallArgs() { switch ( await this.getNodeProperty(this.arg.scene_mode, ["single", "dynamic"]) ) { case "single": case undefined: this.result.scene_call = { mode: "single", groupId: await this.getNodeProperty(this.arg.group), sceneId: await this.getNodeProperty(this.arg.scene), }; break; case "dynamic": this.result.scene_call = { mode: "dynamic", sceneName: await this.getNodeProperty(this.arg.scene_name), }; break; } } 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"); } if ( deviceMeta.hascolor === true && Array.isArray(deviceMeta.device_colorcapabilities) && !deviceMeta.device_colorcapabilities.includes("unknown") ) { let checkColorModesCompatibility = (charsName, mode) => { if ( dotProp.has(values, charsName) && !Utils.supportColorCapability(deviceMeta, mode) ) { this.node.warn( `The light '${deviceMeta.name}' don't support '${charsName}' values. ` + `You can use only '${deviceMeta.device_colorcapabilities.toString()}' modes.` ); } }; checkColorModesCompatibility("Hue", "hs"); checkColorModesCompatibility("Saturation", "hs"); checkColorModesCompatibility("ColorTemperature", "ct"); } new HomeKitFormatter.toDeconz().parse( values, allValues, this.result, deviceMeta ); dotProp.set( this.result, "state.transitiontime", await this.getNodeProperty(this.arg.transitiontime) ); } async parseCustomArgs() { let target = await this.getNodeProperty(this.arg.target, [ "attribute", "state", "config", "scene_call", ]); let command = await this.getNodeProperty(this.arg.command, ["object"]); let value = await this.getNodeProperty(this.arg.payload); switch (target) { case "attribute": if (command === "object") { this.result = value; } else { this.result[command] = value; } break; case "state": case "config": if (command === "object") { this.result[target] = value; } else { this.result[target][command] = value; } break; case "scene_call": if (typeof value !== "object") return; if (value.group !== undefined && value.scene !== undefined) { this.result.scene_call = { mode: "single", groupId: value.group, sceneId: value.scene, }; } else if (value.scene_name !== undefined) { this.result.scene_call = { mode: "dynamic", sceneName: value.scene_name, }; } else if (value.scene_regexp !== undefined) { this.result.scene_call = { mode: "dynamic", sceneName: RegExp(value.scene_regexp), }; } else if (this.node.error) { this.node.error( "deCONZ outptut node received a message with scene call target but " + "no scene name or scene regex or group/scene id." ); } break; } } /** * * @param node Node * @param devices Device[] * @returns {*[]} */ async getRequests(node, devices) { let deconzApi = node.server.api; let requests = []; if ( (this.type === "deconz_state" && this.domain === "scene_call") || (this.type === "custom" && this.arg.target.type === "scene_call") ) { switch (this.result.scene_call.mode) { case "single": let request = {}; request.endpoint = deconzApi.url.groups.scenes.recall( this.result.scene_call.groupId, this.result.scene_call.sceneId ); request.meta = node.server.device_list.getDeviceByDomainID( "groups", this.result.scene_call.groupId ); if (request.meta && Array.isArray(request.meta.scenes)) { request.scene_meta = request.meta.scenes .filter( (scene) => Number(scene.id) === this.result.scene_call.sceneId ) .shift(); } request.params = Utils.clone(this.result); requests.push(request); break; case "dynamic": // For each device that is light group for (let device of devices) { if (device.data.type === "LightGroup") { // Filter scene by name let sceneMeta = device.data.scenes .filter((scene) => this.result.scene_call.sceneName instanceof RegExp ? this.result.scene_call.sceneName.test(scene.name) : scene.name === this.result.scene_call.sceneName ) .shift(); if (sceneMeta) { let request = {}; request.endpoint = deconzApi.url.groups.scenes.recall( device.data.id, sceneMeta.id ); request.meta = device; request.scene_meta = sceneMeta; request.params = Utils.clone(this.result); requests.push(request); } } } break; } } else { if (this.valid_domain.length === 0) return requests; for (let device of devices) { // Skip if device is invalid, should never happen. if (device === undefined || device.data === undefined) continue; // If the device type do not match the command type skip the device if ( !( this.valid_domain.includes("any") || this.valid_domain.includes(device.data.device_type) || (Utils.isDeviceCover(device.data) === true && this.valid_domain.includes("covers")) ) ) continue; // Parse HomeKit values with device Meta if (this.type === "homekit") { this.result = { config: {}, state: {}, }; await this.parseHomekitArgs(device.data); } // Make sure that the endpoint exist let deviceTypeEndpoint = deconzApi.url[device.data.device_type]; if (deviceTypeEndpoint === undefined) throw new Error( "Invalid device endpoint, got " + device.data.device_type ); // Attribute request if (Object.keys(this.result).length > 0) { let request = {}; request.endpoint = deviceTypeEndpoint.main(device.data.device_id); request.meta = device.data; request.params = Utils.clone(this.result); delete request.params.state; delete request.params.config; requests.push(request); } // State request if (Object.keys(this.result.state).length > 0) { let request = {}; request.endpoint = deviceTypeEndpoint.action(device.data.device_id); request.meta = device.data; request.params = Utils.clone(this.result.state); if (request.params.on === "toggle") { switch (device.data.device_type) { case "lights": if (typeof device.data.state.on === "boolean") { request.params.on = !device.data.state.on; } else { if (node.error) { node.error( `[deconz] The light ${device.data.device_path} don't have a 'on' state value.` ); } delete request.params.on; } break; case "groups": delete request.params.on; request.params.toggle = true; break; } } if (request.params.open === "toggle") { if (typeof device.data.state.open === "boolean") { request.params.open = !device.data.state.open; } else { if (node.error) { node.error( `The cover ${device.data.device_path} don't have a 'open' state value.` ); } delete request.params.open; } } requests.push(request); } // Config request if (Object.keys(this.result.config).length > 0) { let request = {}; request.endpoint = deviceTypeEndpoint.config(device.data.device_id); request.meta = device.data; request.params = Utils.clone(this.result.config); requests.push(request); } } } // Remove undefined params in requests requests = requests .map((request) => { for (const [k, v] of Object.entries(request.params)) { if (v === undefined) delete request.params[k]; } return request; }) .filter((request) => Object.keys(request.params).length > 0); return requests; } async getNodeProperty(property, noValueTypes, valueMaps) { if (typeof property === "undefined") return undefined; if (Array.isArray(valueMaps)) for (const map of valueMaps) if ( Array.isArray(map) && map.length === 2 && (property.type === map[0] || `${property.type}.${property.value}` === map[0]) ) return map[1]; return await Utils.getNodeProperty( property, this.node, this.message_in, noValueTypes ); } } module.exports = CommandParser;