node-red-contrib-deconz
Version:
deCONZ connectivity nodes for node-red
400 lines (368 loc) • 14 kB
JavaScript
const CommandParser = require("../src/runtime/CommandParser");
const Utils = require("../src/runtime/Utils");
const got = require("got");
const ConfigMigration = require("../src/migration/ConfigMigration");
const dotProp = require("dot-prop");
const NodeType = "deconz-output";
module.exports = function (RED) {
const defaultCommand = {
type: "deconz_state",
domain: "lights",
arg: {
on: { type: "keep", value: "" },
alert: { type: "str", value: "" },
effect: { type: "str", value: "" },
colorloopspeed: { type: "num", value: "" },
open: { type: "keep", value: "" },
stop: { type: "keep", value: "" },
lift: { type: "num", value: "" },
tilt: { type: "num", value: "" },
group: { type: "num", value: "" },
scene: { type: "num", value: "" },
target: { type: "state", value: "" },
command: { type: "str", value: "on" },
payload: { type: "msg", value: "payload" },
delay: { type: "num", value: "2000" },
transitiontime: { type: "num", value: "" },
retryonerror: { type: "num", value: "0" },
aftererror: { type: "continue", value: "" },
bri: { direction: "set", type: "num", value: "" },
sat: { direction: "set", type: "num", value: "" },
hue: { direction: "set", type: "num", value: "" },
ct: { direction: "set", type: "num", value: "" },
xy: { direction: "set", type: "json", value: "[]" },
},
};
const defaultConfig = {
name: "",
statustext: "",
statustext_type: "auto",
search_type: "device",
device_list: [],
device_name: "",
query: "{}",
commands: [defaultCommand],
specific: {
delay: { type: "num", value: "50" },
result: { type: "at_end", value: "" },
},
};
class deConzOut {
constructor(config) {
RED.nodes.createNode(this, config);
let node = this;
node.config = config;
node.ready = false;
node.cleanStatusTimer = null;
node.status({});
//get server node
node.server = RED.nodes.getNode(node.config.server);
if (!node.server) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.server_node_error",
});
return;
}
let initNode = function () {
node.server.off("onStart", initNode);
if (node.server.migrateNodeConfiguration(node)) {
// Make sure that all expected config are defined
node.config = Object.assign({}, defaultConfig, node.config);
node.ready = true;
}
};
if (node.server.state.pooling.isValid === true) {
(async () => {
await Utils.sleep(1500);
initNode();
})()
.then()
.catch((error) => {
console.error(error);
});
} else {
node.server.on("onStart", initNode);
}
node.on("input", (message_in, send, done) => {
// For maximum backwards compatibility, check that send and done exists.
send =
send ||
function () {
node.send.apply(node, arguments);
};
done =
done ||
function (err) {
if (err) node.error(err, message_in);
};
(async () => {
if (node.config.statustext_type === "auto")
clearTimeout(node.cleanStatusTimer);
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) {
done(RED._(waitResult));
return;
}
let delay = await Utils.getNodeProperty(
node.config.specific.delay,
this,
message_in
);
if (typeof delay !== "number") delay = 50;
let devices = [];
switch (node.config.search_type) {
case "device":
for (let path of node.config.device_list) {
let device = node.server.device_list.getDeviceByPath(path);
if (device) {
devices.push({ data: device });
} else {
done(`Error : Device not found : '${path}'`);
}
}
break;
case "json":
case "jsonata":
const querySrc = await new Promise((resolve, reject) => {
RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(node.config.query, node),
message_in,
(err, value) => {
if (err) reject(err);
else resolve(value);
}
);
});
try {
for (let r of node.server.device_list.getDevicesByQuery(
querySrc
).matched) {
devices.push({ data: r });
}
} catch (e) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.query_error",
});
done(e.toString());
return;
}
break;
}
let resultMsgs = [];
let errorMsgs = [];
let resultTimings = ["never", "after_command", "at_end"];
let resultTiming = await Utils.getNodeProperty(
node.config.specific.result,
this,
message_in,
resultTimings
);
if (!resultTimings.includes(resultTiming)) resultTiming = "never";
let command_count = node.config.commands.length;
for (const [
command_id,
saved_command,
] of node.config.commands.entries()) {
// Make sure that all expected config are defined
const command = Object.assign({}, defaultCommand, saved_command);
if (command.type === "pause") {
let sleep_delay = await Utils.getNodeProperty(
command.arg.delay,
this,
message_in
);
node.status({
fill: "blue",
shape: "dot",
text: RED._(
"node-red-contrib-deconz/server:status.out_commands.main"
)
.replace("{{index}}", (command_id + 1).toString())
.replace("{{count}}", command_count)
.replace(
"{{status}}",
RED._(
"node-red-contrib-deconz/server:status.out_commands.pause"
).replace("{{delay}}", sleep_delay)
),
});
await Utils.sleep(sleep_delay, 2000);
continue;
}
try {
let cp = new CommandParser(command, message_in, node);
await cp.build();
let requests = await cp.getRequests(node, devices);
let request_count = requests.length;
for (const [request_id, request] of requests.entries()) {
try {
node.status({
fill: "blue",
shape: "dot",
text: RED._(
"node-red-contrib-deconz/server:status.out_commands.main"
)
.replace("{{index}}", (command_id + 1).toString())
.replace("{{count}}", command_count)
.replace(
"{{status}}",
RED._(
"node-red-contrib-deconz/server:status.out_commands.request"
)
.replace("{{index}}", (request_id + 1).toString())
.replace("{{count}}", request_count)
),
});
const response = await got(
node.server.api.url.main() + request.endpoint,
{
method: "PUT",
retry:
(await Utils.getNodeProperty(
command.arg.retryonerror,
this,
message_in
)) || 0,
json: request.params,
responseType: "json",
timeout: 2000, // TODO make configurable ?
}
);
if (resultTiming !== "never") {
let result = {};
let errors = [];
for (const r of response.body) {
if (r.success !== undefined)
for (const [enpointKey, value] of Object.entries(
r.success
))
result[
enpointKey.replace(request.endpoint + "/", "")
] = value;
if (r.error !== undefined) errors.push(r.error);
}
let resultMsg = {};
if (resultTiming === "after_command") {
resultMsg = Utils.cloneMessage(message_in, [
"request",
"meta",
"payload",
"errors",
]);
resultMsg.payload = result;
} else if (resultTiming === "at_end") {
resultMsg.result = result;
}
resultMsg.request = request.params;
resultMsg.meta = request.meta;
if (request.scene_meta !== undefined)
resultMsg.scene_meta = request.scene_meta;
if (errors.length > 0) resultMsg.errors = errors;
if (resultTiming === "after_command") {
send(resultMsg);
} else if (resultTiming === "at_end") {
resultMsgs.push(resultMsg);
}
}
let sleep_delay =
delay - dotProp.get(response, "timings.phases.total", 0);
if (sleep_delay >= 200)
node.status({
fill: "blue",
shape: "dot",
text: RED._(
"node-red-contrib-deconz/server:status.out_commands.main"
)
.replace("{{index}}", (command_id + 1).toString())
.replace("{{count}}", command_count)
.replace(
"{{status}}",
RED._(
"node-red-contrib-deconz/server:status.out_commands.delay"
).replace("{{delay}}", sleep_delay)
),
});
await Utils.sleep(sleep_delay);
} catch (error) {
// Clean up status
node.status({});
if (resultTiming !== "never") {
let errorMsg = {};
if (resultTiming === "after_command") {
errorMsg = Utils.cloneMessage(message_in, [
"request",
"meta",
"payload",
"errors",
]);
}
errorMsg.request = request.params;
errorMsg.meta = request.meta;
errorMsg.errors = [
{
type: 0,
code: dotProp.get(error, "response.statusCode"),
message: dotProp.get(error, "response.statusMessage"),
description: `${error.name}: ${error.message}`,
apiEndpoint: request.endpoint,
},
];
if (resultTiming === "after_command") {
send(errorMsg);
} else if (resultTiming === "at_end") {
resultMsgs.push(errorMsg);
}
}
if (
(await Utils.getNodeProperty(
command.arg.aftererror,
this,
message_in,
["continue", "stop"]
)) === "stop"
)
return;
if (error.timings !== undefined) {
await Utils.sleep(
delay - dotProp.get(error, "timings.phases.total", 0)
);
} else {
await Utils.sleep(delay);
}
}
}
} catch (error) {
node.status({});
node.error(
`Error while processing command #${command_id + 1}, ${error}`,
message_in
);
console.warn(error);
}
}
if (resultTiming === "at_end") {
let endMsg = Utils.cloneMessage(message_in, ["payload", "errors"]);
endMsg.payload = resultMsgs;
if (errorMsgs.length > 0) endMsg.errors = errorMsgs;
send(endMsg);
}
node.server.updateNodeStatus(node, null);
if (node.config.statustext_type === "auto")
node.cleanStatusTimer = setTimeout(function () {
node.status({}); //clean
}, 3000);
done();
})()
.then()
.catch((error) => {
console.error(error);
});
});
}
}
RED.nodes.registerType(NodeType, deConzOut);
};