node-red-contrib-deconz
Version:
deCONZ connectivity nodes for node-red
346 lines (319 loc) • 10.6 kB
JavaScript
const NODE_PATH = "/node-red-contrib-deconz/";
const path = require("path");
const ConfigMigration = require("./src/migration/ConfigMigration");
const DeconzAPI = require("./src/runtime/DeconzAPI");
const CommandParser = require("./src/runtime/CommandParser");
const got = require("got");
const Utils = require("./src/runtime/Utils");
const CompareVersion = require("compare-versions");
const HomeKitFormatter = require("./src/runtime/HomeKitFormatter");
module.exports = function (RED) {
function backendNode(config) {
RED.nodes.createNode(this, config);
}
RED.nodes.registerType("deconz-ui-backend", backendNode);
/**
* Static files route because some users are using Node-Red 1.3.0 or lower
*/
if (
RED.version === undefined ||
CompareVersion.compare(RED.version(), "1.3.0", "<")
) {
RED.httpAdmin.get("/resources" + NODE_PATH + "*", function (req, res) {
try {
let options = {
root: __dirname + "/resources",
dotfiles: "deny",
};
res.sendFile(req.params[0], options);
} catch (e) {
console.error(e);
res.status(500).end();
}
});
}
/**
* Enable http route to multiple-select static files
*/
RED.httpAdmin.get(NODE_PATH + "multiple-select/*", function (req, res) {
try {
let options = {
root: path.dirname(require.resolve("multiple-select")),
dotfiles: "deny",
};
res.sendFile(req.params[0], options);
} catch (e) {
console.error(e);
res.status(500).end();
}
});
/**
* Enable http route to JSON itemlist for each controller (controller id passed as GET query parameter)
*/
RED.httpAdmin.get(NODE_PATH + "itemlist", async function (req, res) {
try {
let config = req.query;
let controller = RED.nodes.getNode(config.controllerID);
let forceRefresh = config.forceRefresh
? ["1", "yes", "true"].includes(config.forceRefresh.toLowerCase())
: false;
let query;
let queryType = req.query.queryType || "json";
try {
if (
req.query.query !== undefined &&
["json", "jsonata"].includes(queryType)
) {
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({
error_message: e.message,
error_stack: e.stack,
});
}
if (controller && controller.constructor.name === "ServerNode") {
(async () => {
try {
if (forceRefresh)
await controller.discoverDevices({
forceRefresh: true,
});
if (query === undefined) {
res.json({
items: controller.device_list.getAllDevices(),
});
} else {
res.json({
items: controller.device_list.getDevicesByQuery(query),
});
}
} catch (e) {
return res.json({
error_message: e.message,
error_stack: e.stack,
});
}
})();
} else {
return res.json({
error_message: "Can't find the server node. Did you press deploy ?",
});
}
} catch (e) {
console.error(e);
res.status(500).end();
}
});
["attribute", "state", "config"].forEach(function (type) {
RED.httpAdmin.get(NODE_PATH + type + "list", function (req, res) {
try {
let config = req.query;
let controller = RED.nodes.getNode(config.controllerID);
let devicesIDs = JSON.parse(config.devices);
const isAttribute = type === "attribute";
if (
controller &&
controller.constructor.name === "ServerNode" &&
devicesIDs
) {
let type_list = isAttribute ? ["state", "config"] : [type];
let sample = {};
let count = {};
for (const _type of type_list) {
sample[_type] = {};
count[_type] = {};
}
if (isAttribute) {
sample[type] = {};
count[type] = {};
}
for (const deviceID of devicesIDs) {
let device = controller.device_list.getDeviceByPath(deviceID);
if (!device) continue;
if (isAttribute) {
for (const value of Object.keys(device)) {
if (type_list.includes(value)) continue;
count[type][value] = (count[type][value] || 0) + 1;
sample[type][value] = device[value];
}
}
for (const _type of type_list) {
if (!device[_type]) continue;
for (const value of Object.keys(device[_type])) {
count[_type][value] = (count[_type][value] || 0) + 1;
sample[_type][value] = device[_type][value];
}
}
}
res.json({ count: count, sample: sample });
} else {
res.status(404).end();
}
} catch (e) {
console.error(e);
res.status(500).end();
}
});
});
RED.httpAdmin.get(NODE_PATH + "homekitlist", function (req, res) {
try {
let config = req.query;
let controller = RED.nodes.getNode(config.controllerID);
let devicesIDs = JSON.parse(config.devices);
if (
controller &&
controller.constructor.name === "ServerNode" &&
devicesIDs
) {
let sample = { homekit: {} };
let count = { homekit: {} };
const formatter = new HomeKitFormatter.fromDeconz({});
for (const deviceID of devicesIDs) {
let device = controller.device_list.getDeviceByPath(deviceID);
if (!device) continue;
let propertiesList = formatter.getValidPropertiesList(device);
let characteristics = formatter.parse(device, device);
for (const property of propertiesList) {
count.homekit[property] = (count.homekit[property] || 0) + 1;
const propertyName =
formatter.format[property]._name !== undefined
? formatter.format[property]._name
: property;
if (characteristics[propertyName] !== undefined) {
sample.homekit[property] = characteristics[propertyName];
}
}
}
res.json({ count, sample });
} else {
res.status(404).end();
}
} catch (e) {
console.error(e);
res.status(500).end();
}
});
/**
* @deprecated getScenesByDevice
*/
RED.httpAdmin.get(NODE_PATH + "getScenesByDevice", function (req, res) {
try {
let config = req.query;
let controller = RED.nodes.getNode(config.controllerID);
if (controller && controller.constructor.name === "ServerNode") {
if (
"scenes" in controller.items[config.device] &&
config.device in controller.items
) {
res.json(controller.items[config.device].scenes);
} else {
res.json({});
}
} else {
res.status(404).end();
}
} catch (e) {
console.error(e);
res.status(500).end();
}
});
RED.httpAdmin.get(NODE_PATH + "configurationMigration", function (req, res) {
try {
let data = req.query;
let config = JSON.parse(data.config);
let server = RED.nodes.getNode(
data.type === "deconz-server" ? data.id : config.server
);
if (server === undefined) {
res.json({ errors: [`Could not find the server node.`] });
return;
}
if (server.state.ready === true || data.type === "deconz-server") {
let configMigration = new ConfigMigration(data.type, config, server);
let result = configMigration.migrate(config);
res.json(result);
} else {
res.json({
errors: [
`The server node is not ready. Please check the server configuration.`,
],
});
}
} catch (e) {
console.error(e);
res.status(500).end();
}
});
RED.httpAdmin.get(NODE_PATH + "serverAutoconfig", async function (req, res) {
try {
let data = req.query;
let config = JSON.parse(data.config);
let api = new DeconzAPI(config);
let result = await api.discoverSettings(config.discoverParam || {});
res.json(result);
} catch (e) {
console.error(e);
res.status(500).end();
}
});
RED.httpAdmin.post(NODE_PATH + "testCommand", async function (req, res) {
try {
let config = req.body;
if (!Array.isArray(config.device_list)) config.device_list = [];
let controller = RED.nodes.getNode(config.controllerID);
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);
if (device) {
devices.push({ data: device });
} else {
console.warn(`Error : Device not found : '${path}'`);
}
}
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:
(await Utils.getNodeProperty(
config.command.arg.retryonerror,
this,
{}
)) || 0,
json: request.params,
responseType: "json",
timeout: 2000, // TODO make configurable ?
}
);
await Utils.sleep(
(await Utils.getNodeProperty(config.delay, this, {})) || 50
);
}
res.status(200).end();
} else {
res.status(404).end();
}
} catch (e) {
console.warn("Error when running command : " + e.toString());
res.status(500).end();
}
});
};