node-red-contrib-deconz
Version:
deCONZ connectivity nodes for node-red
478 lines (449 loc) • 14.8 kB
JavaScript
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;