Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

node-red-contrib-deconz

Package Overview
Dependencies
Maintainers
2
Versions
146
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

node-red-contrib-deconz - npm Package Compare versions

Comparing version 2.3.5 to 2.3.6

8

CHANGELOG.md

@@ -10,2 +10,8 @@ # Changelog

## [2.3.6] - 2022-10-05 ![Relative date](https://img.shields.io/date/1665001644?label=)
### Added
- Added HomeKit attributes min/max value limits. @Zehir
## [2.3.5] - 2022-09-12 ![Relative date](https://img.shields.io/date/1662979773?label=)

@@ -292,2 +298,2 @@

- Lastest version from @andreypopov.
- Lastest legacy version. @andreypopov

548

deconz.js

@@ -1,3 +0,3 @@

const NODE_PATH = '/node-red-contrib-deconz/';
const path = require('path');
const NODE_PATH = "/node-red-contrib-deconz/";
const path = require("path");
const ConfigMigration = require("./src/migration/ConfigMigration");

@@ -8,288 +8,326 @@ const DeconzAPI = require("./src/runtime/DeconzAPI");

const Utils = require("./src/runtime/Utils");
const CompareVersion = require('compare-versions');
const CompareVersion = require("compare-versions");
const HomeKitFormatter = require("./src/runtime/HomeKitFormatter");
module.exports = function (RED) {
/**
* 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();
}
});
}
/**
* 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 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", 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 = RED.util.evaluateNodeProperty(
req.query.query,
queryType,
RED.nodes.getNode(req.query.nodeID),
{},
undefined
);
}
});
} catch (e) {
return res.json({
error_message: e.message,
error_stack: e.stack,
});
}
/**
* 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) {
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 = RED.util.evaluateNodeProperty(
req.query.query,
queryType,
RED.nodes.getNode(req.query.nodeID),
{}, undefined
);
}
} 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
});
}
})();
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 {
return res.json({
error_message: "Can't find the server node. Did you press deploy ?"
});
res.json({
items: controller.device_list.getDevicesByQuery(query),
});
}
} catch (e) {
console.error(e);
res.status(500).end();
}
});
} 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) {
["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 type_list = (isAttribute) ? ['state', 'config'] : [type];
let sample = {};
let count = {};
let sample = {};
let count = {};
for (const _type of type_list) {
sample[_type] = {};
count[_type] = {};
}
for (const _type of type_list) {
sample[_type] = {};
count[_type] = {};
}
if (isAttribute) {
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;
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];
}
}
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];
}
}
}
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();
}
});
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) {
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: {} };
let sample = { homekit: {} };
let count = { homekit: {} };
const formatter = new HomeKitFormatter.fromDeconz({});
const formatter = (new HomeKitFormatter.fromDeconz({}));
for (const deviceID of devicesIDs) {
let device = controller.device_list.getDeviceByPath(deviceID);
if (!device) continue;
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);
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();
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];
}
} 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();
}
});
res.json({ count, sample });
} 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();
/**
* @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 + '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.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);
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}'`);
}
}
});
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);
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 = 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(config.command.arg.retryonerror, this, {}) || 0,
json: request.params,
responseType: 'json',
timeout: 2000 // TODO make configurable ?
}
);
await Utils.sleep(Utils.getNodeProperty(config.delay, this, {}) || 50);
}
res.status(200).end();
} else {
res.status(404).end();
let requests = 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(
config.command.arg.retryonerror,
this,
{}
) || 0,
json: request.params,
responseType: "json",
timeout: 2000, // TODO make configurable ?
}
} catch (e) {
console.warn("Error when running command : " + e.toString());
res.status(500).end();
);
await Utils.sleep(
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();
}
});
};

@@ -5,139 +5,162 @@ const OutputMsgFormatter = require("../src/runtime/OutputMsgFormatter");

const NodeType = 'deconz-api';
const NodeType = "deconz-api";
module.exports = function (RED) {
const defaultConfig = {
name: "",
topic: "",
specific: {
method: { type: "GET" },
endpoint: { type: "str", value: "/" },
payload: { type: "json", value: "{}" },
},
};
const defaultConfig = {
name: "",
topic: "",
specific: {
method: { type: 'GET' },
endpoint: { type: 'str', value: '/' },
payload: { type: 'json', value: '{}' }
}
};
class deConzItemApi {
constructor(config) {
RED.nodes.createNode(this, config);
class deConzItemApi {
constructor(config) {
RED.nodes.createNode(this, config);
let node = this;
node.config = config;
node.ready = false;
let node = this;
node.config = config;
node.ready = false;
node.cleanStatusTimer = null;
node.status({});
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/battery:status.server_node_error",
});
return;
}
//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/battery: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;
}
};
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);
}
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 () => {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) {
done(RED._(waitResult));
return;
}
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 () => {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) {
done(RED._(waitResult));
return;
}
// Load the config
let config = node.config;
let methods = ["GET", "POST", "PUT", "DELETE"];
let method = Utils.getNodeProperty(
config.specific.method,
node,
message_in,
methods
);
// Make sure the method is valid
if (!methods.includes(method)) method = "GET";
let endpoint = Utils.getNodeProperty(
config.specific.endpoint,
node,
message_in
);
let payload = Utils.getNodeProperty(
config.specific.payload,
node,
message_in
);
// Load the config
let config = node.config;
let methods = ['GET', 'POST', 'PUT', 'DELETE'];
let method = Utils.getNodeProperty(config.specific.method, node, message_in, methods);
// Make sure the method is valid
if (!methods.includes(method)) method = 'GET';
let endpoint = Utils.getNodeProperty(config.specific.endpoint, node, message_in);
let payload = Utils.getNodeProperty(config.specific.payload, node, message_in);
// Add pending status
node.status({
fill: "blue",
shape: "dot",
text: "node-red-contrib-deconz/get:status.pending",
});
// Add pending status
node.status({
fill: "blue",
shape: "dot",
text: "node-red-contrib-deconz/get:status.pending"
});
// Do request
const response = await node.server.api.doRequest(endpoint, {
method,
body: payload,
});
// Do request
const response = await node.server.api.doRequest(endpoint, { method, body: payload });
// Add output properties
let outputProperties = {
payload: response.body,
status: {
code: response.statusCode,
message: response.statusMessage,
},
};
let outputMsg = Utils.cloneMessage(
message_in,
Object.keys(outputProperties)
);
Object.assign(outputMsg, outputProperties);
outputMsg.topic = config.topic;
// Add output properties
let outputProperties = {
payload: response.body,
status: {
code: response.statusCode,
message: response.statusMessage,
}
};
let outputMsg = Utils.cloneMessage(message_in, Object.keys(outputProperties));
Object.assign(outputMsg, outputProperties);
outputMsg.topic = config.topic;
// Clear status timer
if (node.cleanStatusTimer) {
clearTimeout(node.cleanStatusTimer);
node.cleanStatusTimer = null;
}
// Clear status timer
if (node.cleanStatusTimer) {
clearTimeout(node.cleanStatusTimer);
node.cleanStatusTimer = null;
}
// Set status
node.status({
fill: response.statusCode === 200 ? "green" : "red",
shape: "dot",
text: `${response.statusCode}: ${response.statusMessage}`,
});
// Set status
node.status({
fill: response.statusCode === 200 ? "green" : "red",
shape: "dot",
text: `${response.statusCode}: ${response.statusMessage}`
});
// Add status cleanup timer
node.cleanStatusTimer = setTimeout(function () {
node.status({}); //clean
}, 3000);
// Add status cleanup timer
node.cleanStatusTimer = setTimeout(function () {
node.status({}); //clean
}, 3000);
// Send the message
send(outputMsg);
done();
})()
.then()
.catch((error) => {
console.error(error);
});
});
// Send the message
send(outputMsg);
done();
})().then().catch((error) => {
console.error(error);
});
});
node.on('close', (removed, done) => {
done();
});
}
node.on("close", (removed, done) => {
done();
});
}
}
RED.nodes.registerType(NodeType, deConzItemApi);
RED.nodes.registerType(NodeType, deConzItemApi);
};
const OutputMsgFormatter = require("../src/runtime/OutputMsgFormatter");
const Utils = require("../src/runtime/Utils");
const NodeType = 'deconz-battery';
const NodeType = "deconz-battery";
module.exports = function (RED) {
const defaultRule = {
type: "config",
format: "single",
onstart: true,
};
const defaultRule = {
type: 'config',
format: 'single',
onstart: true
};
const defaultConfig = {
name: "",
topic: "",
statustext: "",
statustext_type: "auto",
search_type: "device",
device_list: [],
device_name: "",
query: "{}",
outputs: 1,
output_rules: [defaultRule],
};
const defaultConfig = {
name: "",
topic: "",
statustext: "",
statustext_type: 'auto',
search_type: 'device',
device_list: [],
device_name: "",
query: "{}",
outputs: 1,
output_rules: [defaultRule]
};
class deConzItemBattery {
constructor(config) {
RED.nodes.createNode(this, config);
let node = this;
node.config = config;
node.ready = false;
class deConzItemBattery {
constructor(config) {
RED.nodes.createNode(this, config);
node.status({});
let node = this;
node.config = config;
node.ready = false;
//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/battery:status.server_node_error",
});
return;
}
node.status({});
node.status({
fill: "blue",
shape: "dot",
text: "node-red-contrib-deconz/server:status.starting",
});
//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/battery:status.server_node_error"
});
return;
}
node.status({
fill: "blue",
shape: "dot",
text: "node-red-contrib-deconz/server:status.starting"
});
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.registerNode();
node.server.updateNodeStatus(node, null);
node.ready = true;
}
};
if (node.server.state.pooling.isValid === true) {
(async () => {
await Utils.sleep(1500);
initNode();
node.server.propagateStartNews([node.id]);
})().then().catch((error) => {
console.error(error);
});
} else {
node.server.on('onStart', initNode);
}
node.on('close', (removed, done) => {
this.unregisterNode();
done();
});
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.registerNode();
node.server.updateNodeStatus(node, null);
node.ready = true;
}
};
registerNode() {
let node = this;
if (node.config.search_type === 'device') {
node.config.device_list.forEach(function (item) {
node.server.registerNodeByDevicePath(node.config.id, item);
});
} else {
node.server.registerNodeWithQuery(node.config.id);
}
}
if (node.server.state.pooling.isValid === true) {
(async () => {
await Utils.sleep(1500);
initNode();
node.server.propagateStartNews([node.id]);
})()
.then()
.catch((error) => {
console.error(error);
});
} else {
node.server.on("onStart", initNode);
}
unregisterNode() {
let node = this;
if (node.config.search_type === "device") {
node.config.device_list.forEach(function (item) {
node.server.unregisterNodeByDevicePath(node.config.id, item);
});
} else {
node.server.unregisterNodeWithQuery(node.config.id);
}
}
node.on("close", (removed, done) => {
this.unregisterNode();
done();
});
}
handleDeconzEvent(device, changed, rawEvent, opt) {
let node = this;
registerNode() {
let node = this;
if (node.config.search_type === "device") {
node.config.device_list.forEach(function (item) {
node.server.registerNodeByDevicePath(node.config.id, item);
});
} else {
node.server.registerNodeWithQuery(node.config.id);
}
}
(async () => {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) return;
unregisterNode() {
let node = this;
if (node.config.search_type === "device") {
node.config.device_list.forEach(function (item) {
node.server.unregisterNodeByDevicePath(node.config.id, item);
});
} else {
node.server.unregisterNodeWithQuery(node.config.id);
}
}
let options = Object.assign({
initialEvent: false,
errorEvent: false
}, opt);
handleDeconzEvent(device, changed, rawEvent, opt) {
let node = this;
if (options.errorEvent === true) {
node.status({
fill: "red",
shape: "dot",
text: options.errorCode || "Unknown Error"
});
if (options.isGlobalError === false)
node.error(options.errorMsg || "Unknown Error");
return;
}
(async () => {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) return;
let msgs = new Array(this.config.output_rules.length);
this.config.output_rules.forEach((saved_rule, index) => {
// Make sure that all expected config are defined
const rule = Object.assign({}, defaultRule, saved_rule);
// Only if it's not on start and the start msg are blocked
if (!(options.initialEvent === true && rule.onstart !== true)) {
// Clean up old msgs
msgs.fill(undefined);
let options = Object.assign(
{
initialEvent: false,
errorEvent: false,
},
opt
);
// Format msgs, can get one or many msgs.
let formatter = new OutputMsgFormatter(rule, NodeType, this.config);
let msgToSend = formatter.getMsgs({ data: device, changed }, rawEvent, options);
if (options.errorEvent === true) {
node.status({
fill: "red",
shape: "dot",
text: options.errorCode || "Unknown Error",
});
if (options.isGlobalError === false)
node.error(options.errorMsg || "Unknown Error");
return;
}
// Make sure that the result is an array
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend];
let msgs = new Array(this.config.output_rules.length);
this.config.output_rules.forEach((saved_rule, index) => {
// Make sure that all expected config are defined
const rule = Object.assign({}, defaultRule, saved_rule);
// Only if it's not on start and the start msg are blocked
if (!(options.initialEvent === true && rule.onstart !== true)) {
// Clean up old msgs
msgs.fill(undefined);
// Send msgs
for (let msg of msgToSend) {
msg.topic = this.config.topic;
msg = Object.assign(msg, msg.payload); // For retro-compatibility
msgs[index] = msg;
node.send(msgs);
}
// Format msgs, can get one or many msgs.
let formatter = new OutputMsgFormatter(rule, NodeType, this.config);
let msgToSend = formatter.getMsgs(
{ data: device, changed },
rawEvent,
options
);
// Update node status
if (index === 0)
node.server.updateNodeStatus(node, msgToSend);
}
// Make sure that the result is an array
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend];
});
})().then().catch((error) => {
console.error(error);
});
}
// Send msgs
for (let msg of msgToSend) {
msg.topic = this.config.topic;
msg = Object.assign(msg, msg.payload); // For retro-compatibility
msgs[index] = msg;
node.send(msgs);
}
// Update node status
if (index === 0) node.server.updateNodeStatus(node, msgToSend);
}
});
})()
.then()
.catch((error) => {
console.error(error);
});
}
}
RED.nodes.registerType(NodeType, deConzItemBattery);
RED.nodes.registerType(NodeType, deConzItemBattery);
};
module.exports = function (RED) {
class deConzItemEvent {
constructor(config) {
RED.nodes.createNode(this, config);
class deConzItemEvent {
constructor(config) {
RED.nodes.createNode(this, config);
let node = this;
node.config = config;
let node = this;
node.config = config;
node.status({
fill: "blue",
shape: "dot",
text: "node-red-contrib-deconz/server:status.starting"
});
node.status({
fill: "blue",
shape: "dot",
text: "node-red-contrib-deconz/server:status.starting",
});
//get server node
node.server = RED.nodes.getNode(node.config.server);
if (node.server) {
node.server.registerEventNode(node.id);
}
//get server node
node.server = RED.nodes.getNode(node.config.server);
if (node.server) {
node.server.registerEventNode(node.id);
}
let initCounter = () => {
node.server.off('onStart', initCounter);
node.status({
fill: "green",
shape: "dot",
text: RED._('node-red-contrib-deconz/server:status.event_count')
.replace('{{event_count}}', 0)
});
};
node.server.on('onStart', initCounter);
let initCounter = () => {
node.server.off("onStart", initCounter);
node.status({
fill: "green",
shape: "dot",
text: RED._(
"node-red-contrib-deconz/server:status.event_count"
).replace("{{event_count}}", 0),
});
};
node.server.on("onStart", initCounter);
node.on('close', (removed, done) => {
node.server.unregisterEventNode(node.id);
done();
});
node.on("close", (removed, done) => {
node.server.unregisterEventNode(node.id);
done();
});
}
}
handleDeconzEvent(device, changed, rawEvent, opt) {
let node = this;
let options = Object.assign(
{
initialEvent: false,
errorEvent: false,
},
opt
);
handleDeconzEvent(device, changed, rawEvent, opt) {
let node = this;
let options = Object.assign({
initialEvent: false,
errorEvent: false
}, opt);
if (options.errorEvent === true) {
node.status({
fill: "red",
shape: "dot",
text: options.errorCode || "Unknown Error",
});
if (options.isGlobalError === false)
node.error(options.errorMsg || "Unknown Error");
return;
}
if (options.errorEvent === true) {
node.status({
fill: "red",
shape: "dot",
text: options.errorCode || "Unknown Error"
});
if (options.isGlobalError === false)
node.error(options.errorMsg || "Unknown Error");
return;
}
node.send({
payload: rawEvent,
meta: device
});
}
node.send({
payload: rawEvent,
meta: device,
});
}
}
RED.nodes.registerType('deconz-event', deConzItemEvent);
RED.nodes.registerType("deconz-event", deConzItemEvent);
};

@@ -6,165 +6,180 @@ const ConfigMigration = require("../src/migration/ConfigMigration");

const NodeType = 'deconz-get';
const NodeType = "deconz-get";
module.exports = function (RED) {
const defaultRule = {
type: "state",
format: "single",
};
const defaultRule = {
type: 'state',
format: 'single'
};
const defaultConfig = {
name: "",
statustext: "",
statustext_type: "auto",
search_type: "device",
device_list: [],
device_name: "",
query: "{}",
outputs: 1,
output_rules: [defaultRule],
};
const defaultConfig = {
name: "",
statustext: "",
statustext_type: 'auto',
search_type: 'device',
device_list: [],
device_name: "",
query: "{}",
outputs: 1,
output_rules: [defaultRule],
};
class deConzItemGet {
constructor(config) {
RED.nodes.createNode(this, config);
class deConzItemGet {
constructor(config) {
RED.nodes.createNode(this, config);
let node = this;
node.config = config;
node.ready = false;
let node = this;
node.config = config;
node.ready = false;
node.cleanStatusTimer = null;
node.status({});
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/get:status.server_node_error",
});
return;
}
//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/get: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.server.updateNodeStatus(node, null);
node.ready = true;
}
};
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.server.updateNodeStatus(node, null);
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);
}
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 () => {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) {
done(RED._(waitResult));
return;
}
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 () => {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) {
done(RED._(waitResult));
return;
}
let unreachableDevices = [];
let msgs = new Array(this.config.output_rules.length);
let devices = [];
let unreachableDevices = [];
let msgs = new Array(this.config.output_rules.length);
let devices = [];
switch (node.config.search_type) {
case "device":
for (let path of node.config.device_list) {
devices.push({
data: node.server.device_list.getDeviceByPath(path),
});
}
break;
case "json":
case "jsonata":
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(node.config.query, node),
message_in,
undefined
);
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;
}
switch (node.config.search_type) {
case 'device':
for (let path of node.config.device_list) {
devices.push({ data: node.server.device_list.getDeviceByPath(path) });
}
break;
case 'json':
case 'jsonata':
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(node.config.query, node),
message_in,
undefined
);
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;
}
node.config.output_rules.forEach((saved_rule, index) => {
// Make sure that all expected config are defined
const rule = Object.assign({}, defaultRule, saved_rule);
node.config.output_rules.forEach((saved_rule, index) => {
// Make sure that all expected config are defined
const rule = Object.assign({}, defaultRule, saved_rule);
// Only if it's not on start and the start msg are blocked
// Only if it's not on start and the start msg are blocked
// Clean up old msgs
msgs.fill(undefined);
// Clean up old msgs
msgs.fill(undefined);
// Format msgs, can get one or many msgs.
let formatter = new OutputMsgFormatter(rule, NodeType, node.config);
let msgToSend = formatter.getMsgs(devices, undefined, {
src_msg: RED.util.cloneMessage(message_in),
});
// Format msgs, can get one or many msgs.
let formatter = new OutputMsgFormatter(rule, NodeType, node.config);
let msgToSend = formatter.getMsgs(devices, undefined, {
src_msg: RED.util.cloneMessage(message_in)
});
// Make sure that the result is an array
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend];
// Make sure that the result is an array
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend];
// Send msgs
for (let msg of msgToSend) {
msgs[index] = msg;
send(msgs);
if (
dotProp.get(msg, "meta.state.reachable") === false ||
dotProp.get(msg, "meta.config.reachable") === false
) {
let device_path = dotProp.get(msg, "meta.device_path");
if (device_path && !unreachableDevices.includes(device_path)) {
done(
`Device "${dotProp.get(
msg,
"meta.name"
)}" is not reachable.`
);
unreachableDevices.push(device_path);
}
}
}
// Send msgs
for (let msg of msgToSend) {
msgs[index] = msg;
send(msgs);
if (dotProp.get(msg, 'meta.state.reachable') === false ||
dotProp.get(msg, 'meta.config.reachable') === false
) {
let device_path = dotProp.get(msg, 'meta.device_path');
if (device_path && !unreachableDevices.includes(device_path)) {
done(`Device "${dotProp.get(msg, 'meta.name')}" is not reachable.`);
unreachableDevices.push(device_path);
}
}
}
// Update node status
if (index === 0) node.server.updateNodeStatus(node, msgToSend);
});
if (node.config.statustext_type === "auto")
node.cleanStatusTimer = setTimeout(function () {
node.status({}); //clean
}, 3000);
// Update node status
if (index === 0)
node.server.updateNodeStatus(node, msgToSend);
});
if (node.config.statustext_type === 'auto')
node.cleanStatusTimer = setTimeout(function () {
node.status({}); //clean
}, 3000);
if (unreachableDevices.length === 0) done();
})().then().catch((error) => {
console.error(error);
});
});
}
if (unreachableDevices.length === 0) done();
})()
.then()
.catch((error) => {
console.error(error);
});
});
}
}
RED.nodes.registerType(NodeType, deConzItemGet);
RED.nodes.registerType(NodeType, deConzItemGet);
};
const OutputMsgFormatter = require("../src/runtime/OutputMsgFormatter");
const Utils = require("../src/runtime/Utils");
const NodeType = 'deconz-input';
const NodeType = "deconz-input";
module.exports = function (RED) {
const defaultRule = {
type: "state",
format: "single",
output: "always",
onstart: true,
payload: ["__complete__"],
};
const defaultRule = {
type: 'state',
format: 'single',
output: 'always',
onstart: true,
payload: ['__complete__']
};
const defaultConfig = {
name: "",
topic: "",
statustext: "",
statustext_type: "auto",
search_type: "device",
device_list: [],
device_name: "",
query: "{}",
outputs: 1,
output_rules: [defaultRule],
};
const defaultConfig = {
name: "",
topic: "",
statustext: "",
statustext_type: 'auto',
search_type: 'device',
device_list: [],
device_name: "",
query: "{}",
outputs: 1,
output_rules: [defaultRule]
};
class deConzItemIn {
constructor(config) {
RED.nodes.createNode(this, config);
class deConzItemIn {
constructor(config) {
RED.nodes.createNode(this, config);
let node = this;
node.config = config;
node.ready = false;
let node = this;
node.config = config;
node.ready = false;
node.status({});
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;
}
//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;
}
node.status({
fill: "blue",
shape: "dot",
text: "node-red-contrib-deconz/server:status.starting",
});
node.status({
fill: "blue",
shape: "dot",
text: "node-red-contrib-deconz/server:status.starting"
});
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.registerNode();
node.server.updateNodeStatus(node, null);
node.ready = true;
}
};
if (node.server.state.pooling.isValid === true) {
(async () => {
await Utils.sleep(1500);
initNode();
node.server.propagateStartNews([node.id]);
})().then().catch((error) => {
console.error(error);
});
} else {
node.server.on('onStart', initNode);
}
node.on('close', (removed, done) => {
this.unregisterNode();
done();
});
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.registerNode();
node.server.updateNodeStatus(node, null);
node.ready = true;
}
};
registerNode() {
let node = this;
if (node.config.search_type === 'device') {
node.config.device_list.forEach(function (item) {
node.server.registerNodeByDevicePath(node.config.id, item);
});
} else {
node.server.registerNodeWithQuery(node.config.id);
}
}
if (node.server.state.pooling.isValid === true) {
(async () => {
await Utils.sleep(1500);
initNode();
node.server.propagateStartNews([node.id]);
})()
.then()
.catch((error) => {
console.error(error);
});
} else {
node.server.on("onStart", initNode);
}
unregisterNode() {
let node = this;
if (node.config.search_type === "device") {
node.config.device_list.forEach(function (item) {
node.server.unregisterNodeByDevicePath(node.config.id, item);
});
} else {
node.server.unregisterNodeWithQuery(node.config.id);
}
}
node.on("close", (removed, done) => {
this.unregisterNode();
done();
});
}
handleDeconzEvent(device, changed, rawEvent, opt) {
let node = this;
registerNode() {
let node = this;
if (node.config.search_type === "device") {
node.config.device_list.forEach(function (item) {
node.server.registerNodeByDevicePath(node.config.id, item);
});
} else {
node.server.registerNodeWithQuery(node.config.id);
}
}
(async () => {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) return;
unregisterNode() {
let node = this;
if (node.config.search_type === "device") {
node.config.device_list.forEach(function (item) {
node.server.unregisterNodeByDevicePath(node.config.id, item);
});
} else {
node.server.unregisterNodeWithQuery(node.config.id);
}
}
let options = Object.assign({
initialEvent: false,
errorEvent: false
}, opt);
handleDeconzEvent(device, changed, rawEvent, opt) {
let node = this;
if (options.errorEvent === true) {
node.status({
fill: "red",
shape: "dot",
text: options.errorCode || "Unknown Error"
});
if (options.isGlobalError === false)
node.error(options.errorMsg || "Unknown Error");
return;
}
(async () => {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) return;
let msgs = new Array(this.config.output_rules.length);
this.config.output_rules.forEach((saved_rule, index) => {
// Make sure that all expected config are defined
const rule = Object.assign({}, defaultRule, saved_rule);
// Only if it's not on start and the start msg are blocked
if (!(options.initialEvent === true && rule.onstart !== true)) {
// Clean up old msgs
msgs.fill(undefined);
let options = Object.assign(
{
initialEvent: false,
errorEvent: false,
},
opt
);
// Format msgs, can get one or many msgs.
let formatter = new OutputMsgFormatter(rule, NodeType, this.config);
let msgToSend = formatter.getMsgs({ data: device, changed }, rawEvent, options);
if (options.errorEvent === true) {
node.status({
fill: "red",
shape: "dot",
text: options.errorCode || "Unknown Error",
});
if (options.isGlobalError === false)
node.error(options.errorMsg || "Unknown Error");
return;
}
// Make sure that the result is an array
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend];
let msgs = new Array(this.config.output_rules.length);
this.config.output_rules.forEach((saved_rule, index) => {
// Make sure that all expected config are defined
const rule = Object.assign({}, defaultRule, saved_rule);
// Only if it's not on start and the start msg are blocked
if (!(options.initialEvent === true && rule.onstart !== true)) {
// Clean up old msgs
msgs.fill(undefined);
// Send msgs
for (let msg of msgToSend) {
msg.topic = this.config.topic;
msgs[index] = msg;
node.send(msgs);
}
// Format msgs, can get one or many msgs.
let formatter = new OutputMsgFormatter(rule, NodeType, this.config);
let msgToSend = formatter.getMsgs(
{ data: device, changed },
rawEvent,
options
);
// Update node status
if (index === 0)
node.server.updateNodeStatus(node, msgToSend);
}
// Make sure that the result is an array
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend];
});
})().then().catch((error) => {
console.error(error);
});
}
// Send msgs
for (let msg of msgToSend) {
msg.topic = this.config.topic;
msgs[index] = msg;
node.send(msgs);
}
// Update node status
if (index === 0) node.server.updateNodeStatus(node, msgToSend);
}
});
})()
.then()
.catch((error) => {
console.error(error);
});
}
}
RED.nodes.registerType(NodeType, deConzItemIn);
RED.nodes.registerType(NodeType, deConzItemIn);
};
const CommandParser = require("../src/runtime/CommandParser");
const Utils = require("../src/runtime/Utils");
const got = require('got');
const got = require("got");
const ConfigMigration = require("../src/migration/ConfigMigration");
const dotProp = require("dot-prop");
const NodeType = 'deconz-output';
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 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: "" },
},
};
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;
}
};
};
class deConzOut {
constructor(config) {
RED.nodes.createNode(this, config);
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);
}
let node = this;
node.config = config;
node.ready = false;
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);
};
node.cleanStatusTimer = null;
node.status({});
(async () => {
if (node.config.statustext_type === "auto")
clearTimeout(node.cleanStatusTimer);
//get server node
node.server = RED.nodes.getNode(node.config.server);
if (!node.server) {
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) {
done(RED._(waitResult));
return;
}
let delay = 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":
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(node.config.query, node),
message_in,
undefined
);
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.server_node_error"
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.query_error",
});
done(e.toString());
return;
}
}
break;
}
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;
}
};
let resultMsgs = [];
let errorMsgs = [];
let resultTimings = ["never", "after_command", "at_end"];
let resultTiming = Utils.getNodeProperty(
node.config.specific.result,
this,
message_in,
resultTimings
);
if (!resultTimings.includes(resultTiming)) resultTiming = "never";
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);
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 = 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;
}
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);
};
try {
let cp = new CommandParser(command, message_in, node);
let requests = 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)
),
});
(async () => {
if (node.config.statustext_type === 'auto')
clearTimeout(node.cleanStatusTimer);
const response = await got(
node.server.api.url.main() + request.endpoint,
{
method: "PUT",
retry:
Utils.getNodeProperty(
command.arg.retryonerror,
this,
message_in
) || 0,
json: request.params,
responseType: "json",
timeout: 2000, // TODO make configurable ?
}
);
let waitResult = await Utils.waitForEverythingReady(node);
if (waitResult) {
done(RED._(waitResult));
return;
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 delay = 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':
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(node.config.query, node),
message_in,
undefined
);
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 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;
}
let resultMsgs = [];
let errorMsgs = [];
let resultTimings = ['never', 'after_command', 'at_end'];
let resultTiming = Utils.getNodeProperty(node.config.specific.result, this, message_in, resultTimings);
if (!resultTimings.includes(resultTiming)) resultTiming = 'never';
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;
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 = 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;
}
if (resultTiming === "after_command") {
send(resultMsg);
} else if (resultTiming === "at_end") {
resultMsgs.push(resultMsg);
}
}
try {
let cp = new CommandParser(command, message_in, node);
let requests = 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)
)
});
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({});
const response = await got(
node.server.api.url.main() + request.endpoint,
{
method: 'PUT',
retry: Utils.getNodeProperty(command.arg.retryonerror, this, message_in) || 0,
json: request.params,
responseType: 'json',
timeout: 2000 // TODO make configurable ?
}
);
if (resultTiming !== "never") {
let errorMsg = {};
if (resultTiming === "after_command") {
errorMsg = Utils.cloneMessage(message_in, [
"request",
"meta",
"payload",
"errors",
]);
}
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);
}
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,
},
];
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 (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 === "after_command") {
send(errorMsg);
} else if (resultTiming === "at_end") {
resultMsgs.push(errorMsg);
}
}
if (resultTiming === 'at_end') {
let endMsg = Utils.cloneMessage(message_in, ['payload', 'errors']);
endMsg.payload = resultMsgs;
if (errorMsgs.length > 0)
endMsg.errors = errorMsgs;
send(endMsg);
}
if (
Utils.getNodeProperty(
command.arg.aftererror,
this,
message_in,
["continue", "stop"]
) === "stop"
)
return;
node.server.updateNodeStatus(node, null);
if (node.config.statustext_type === 'auto')
node.cleanStatusTimer = setTimeout(function () {
node.status({}); //clean
}, 3000);
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);
}
}
done();
if (resultTiming === "at_end") {
let endMsg = Utils.cloneMessage(message_in, ["payload", "errors"]);
endMsg.payload = resultMsgs;
if (errorMsgs.length > 0) endMsg.errors = errorMsgs;
send(endMsg);
}
})().then().catch((error) => {
console.error(error);
});
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);
RED.nodes.registerType(NodeType, deConzOut);
};

@@ -1,807 +0,956 @@

const got = require('got');
const got = require("got");
const dotProp = require('dot-prop');
const DeviceList = require('../src/runtime/DeviceList');
const dotProp = require("dot-prop");
const DeviceList = require("../src/runtime/DeviceList");
const DeconzAPI = require("../src/runtime/DeconzAPI");
const DeconzSocket = require("../src/runtime/DeconzSocket");
const ConfigMigration = require("../src/migration/ConfigMigration");
const Query = require('../src/runtime/Query');
const Query = require("../src/runtime/Query");
const Utils = require("../src/runtime/Utils");
const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async/fixed');
const {
setIntervalAsync,
clearIntervalAsync,
} = require("set-interval-async/fixed");
module.exports = function (RED) {
class ServerNode {
constructor(config) {
RED.nodes.createNode(this, config);
let node = this;
node.config = config;
node.state = {
ready: false,
startFailed: false,
isStopping: false,
pooling: {
isValid: false,
reachable: false,
discoverProcessRunning: false,
lastPooling: undefined,
failCount: 0,
errorTriggered: false
},
websocket: {
isValid: false,
reachable: false,
lastConnected: undefined,
lastEvent: undefined,
lastDisconnected: undefined,
eventCount: 0
}
};
class ServerNode {
constructor(config) {
RED.nodes.createNode(this, config);
let node = this;
node.config = config;
node.state = {
ready: false,
startFailed: false,
isStopping: false,
pooling: {
isValid: false,
reachable: false,
discoverProcessRunning: false,
lastPooling: undefined,
failCount: 0,
errorTriggered: false,
},
websocket: {
isValid: false,
reachable: false,
lastConnected: undefined,
lastEvent: undefined,
lastDisconnected: undefined,
eventCount: 0,
},
};
// Config migration
let configMigration = new ConfigMigration('deconz-server', node.config, this);
let migrationResult = configMigration.applyMigration(node.config, node);
if (Array.isArray(migrationResult.errors) && migrationResult.errors.length > 0) {
migrationResult.errors.forEach(
error => node.error(`Error with migration of node ${node.type} with id ${node.id}`, error)
);
}
// Config migration
let configMigration = new ConfigMigration(
"deconz-server",
node.config,
this
);
let migrationResult = configMigration.applyMigration(node.config, node);
if (
Array.isArray(migrationResult.errors) &&
migrationResult.errors.length > 0
) {
migrationResult.errors.forEach((error) =>
node.error(
`Error with migration of node ${node.type} with id ${node.id}`,
error
)
);
}
node.device_list = new DeviceList();
node.api = new DeconzAPI({
ip: node.config.ip,
port: node.config.port,
apikey: node.credentials.secured_apikey
});
node.device_list = new DeviceList();
node.api = new DeconzAPI({
ip: node.config.ip,
port: node.config.port,
apikey: node.credentials.secured_apikey,
});
// Example : ["ea9cd132.08f36"]
node.nodesWithQuery = [];
node.nodesEvent = [];
node.nodesByDevicePath = {};
// Example : ["ea9cd132.08f36"]
node.nodesWithQuery = [];
node.nodesEvent = [];
node.nodesByDevicePath = {};
node.setMaxListeners(255);
node.refreshDiscoverTimer = null;
node.refreshDiscoverInterval = node.config.polling >= 3 ? node.config.polling * 1000 : 15000;
node.setMaxListeners(255);
node.refreshDiscoverTimer = null;
node.refreshDiscoverInterval =
node.config.polling >= 3 ? node.config.polling * 1000 : 15000;
node.on('close', () => this.onClose());
node.on("close", () => this.onClose());
(async () => {
//TODO make the delay configurable
await Utils.sleep(1500);
(async () => {
//TODO make the delay configurable
await Utils.sleep(1500);
let pooling = async () => {
let result = await node.discoverDevices({ forceRefresh: true });
if (result === true) {
if (node.state.pooling.isValid === false) {
node.state.pooling.isValid = true;
node.state.ready = true;
this.setupDeconzSocket(node);
node.emit('onStart');
}
node.state.pooling.reachable = true;
node.state.pooling.lastPooling = Date.now();
node.state.pooling.failCount = 0;
if (node.state.pooling.errorTriggered === true) {
node.log(`discoverDevices: Connected to deconz API.`);
}
node.state.pooling.errorTriggered = false;
} else if (node.state.pooling.isValid === false) {
if (node.state.startFailed) return;
node.state.pooling.failCount++;
let code = RED._('node-red-contrib-deconz/server:status.deconz_not_reachable');
let reason = "discoverDevices: Can't connect to deconz API since starting. " +
"Please check server configuration.";
if (node.state.pooling.errorTriggered === false) {
node.state.pooling.errorTriggered = true;
node.propagateErrorNews(code, reason, true);
}
if (node.state.pooling.failCount % 4 === 2) {
node.error(reason);
}
} else {
node.state.pooling.failCount++;
let code = RED._('node-red-contrib-deconz/server:status.deconz_not_reachable');
let reason = "discoverDevices: Can't connect to deconz API.";
let pooling = async () => {
let result = await node.discoverDevices({ forceRefresh: true });
if (result === true) {
if (node.state.pooling.isValid === false) {
node.state.pooling.isValid = true;
node.state.ready = true;
this.setupDeconzSocket(node);
node.emit("onStart");
}
node.state.pooling.reachable = true;
node.state.pooling.lastPooling = Date.now();
node.state.pooling.failCount = 0;
if (node.state.pooling.errorTriggered === true) {
node.log(`discoverDevices: Connected to deconz API.`);
}
node.state.pooling.errorTriggered = false;
} else if (node.state.pooling.isValid === false) {
if (node.state.startFailed) return;
node.state.pooling.failCount++;
let code = RED._(
"node-red-contrib-deconz/server:status.deconz_not_reachable"
);
let reason =
"discoverDevices: Can't connect to deconz API since starting. " +
"Please check server configuration.";
if (node.state.pooling.errorTriggered === false) {
node.state.pooling.errorTriggered = true;
node.propagateErrorNews(code, reason, true);
}
if (node.state.pooling.failCount % 4 === 2) {
node.error(reason);
}
} else {
node.state.pooling.failCount++;
let code = RED._(
"node-red-contrib-deconz/server:status.deconz_not_reachable"
);
let reason = "discoverDevices: Can't connect to deconz API.";
if (node.state.pooling.errorTriggered === false) {
node.state.pooling.errorTriggered = true;
node.propagateErrorNews(code, reason, true);
}
if (node.state.pooling.failCount % 4 === 2) {
node.error(reason);
}
}
};
if (node.state.pooling.errorTriggered === false) {
node.state.pooling.errorTriggered = true;
node.propagateErrorNews(code, reason, true);
}
if (node.state.pooling.failCount % 4 === 2) {
node.error(reason);
}
}
};
await pooling();
if (node.state.startFailed !== true) {
this.refreshDiscoverTimer = setIntervalAsync(pooling, node.refreshDiscoverInterval);
}
})().then().catch((error) => {
node.state.ready = false;
node.error("Deconz Server node error " + error.toString());
console.log("Error from server node #1", error);
});
await pooling();
if (node.state.startFailed !== true) {
this.refreshDiscoverTimer = setIntervalAsync(
pooling,
node.refreshDiscoverInterval
);
}
})()
.then()
.catch((error) => {
node.state.ready = false;
node.error("Deconz Server node error " + error.toString());
console.log("Error from server node #1", error);
});
}
setupDeconzSocket(node) {
node.socket = new DeconzSocket({
hostname: node.config.ip,
port: node.config.ws_port,
secure: node.config.secure || false
});
node.socket.on('open', () => {
node.log(`WebSocket opened`);
node.state.websocket.isValid = true;
node.state.websocket.reachable = true;
node.state.websocket.lastConnected = Date.now();
// This is used only on websocket reconnect, not the initial connection.
if (node.state.ready) node.propagateStartNews();
});
node.socket.on('message', (payload) => this.onSocketMessage(payload));
node.socket.on('error', (err) => {
let node = this;
node.state.websocket.reachable = false;
node.state.websocket.lastDisconnected = Date.now();
// don't bother the user unless there's a reason or if the server is stopping.
if (err && node.state.isStopping === false) {
node.error(`WebSocket error: ${err}`);
}
});
node.socket.on('close', (code, reason) => {
node.state.websocket.reachable = false;
node.state.websocket.lastDisconnected = Date.now();
// don't bother the user unless there's a reason or if the server is stopping.
if (reason && node.state.isStopping === false) {
node.warn(`WebSocket disconnected: ${code} - ${reason}`);
}
if (node.state.ready) node.propagateErrorNews(code, reason);
});
node.socket.on('pong-timeout', () => {
let node = this;
node.state.websocket.reachable = false;
node.state.websocket.lastDisconnected = Date.now();
node.warn('WebSocket connection timeout, reconnecting');
});
node.socket.on('unauthorized', () => () => {
let node = this;
node.state.websocket.isValid = false;
node.state.websocket.lastDisconnected = Date.now();
node.warn('WebSocket authentication failed');
});
setupDeconzSocket(node) {
node.socket = new DeconzSocket({
hostname: node.config.ip,
port: node.config.ws_port,
secure: node.config.secure || false,
});
node.socket.on("open", () => {
node.log(`WebSocket opened`);
node.state.websocket.isValid = true;
node.state.websocket.reachable = true;
node.state.websocket.lastConnected = Date.now();
// This is used only on websocket reconnect, not the initial connection.
if (node.state.ready) node.propagateStartNews();
});
node.socket.on("message", (payload) => this.onSocketMessage(payload));
node.socket.on("error", (err) => {
let node = this;
node.state.websocket.reachable = false;
node.state.websocket.lastDisconnected = Date.now();
// don't bother the user unless there's a reason or if the server is stopping.
if (err && node.state.isStopping === false) {
node.error(`WebSocket error: ${err}`);
}
});
node.socket.on("close", (code, reason) => {
node.state.websocket.reachable = false;
node.state.websocket.lastDisconnected = Date.now();
// don't bother the user unless there's a reason or if the server is stopping.
if (reason && node.state.isStopping === false) {
node.warn(`WebSocket disconnected: ${code} - ${reason}`);
}
if (node.state.ready) node.propagateErrorNews(code, reason);
});
node.socket.on("pong-timeout", () => {
let node = this;
node.state.websocket.reachable = false;
node.state.websocket.lastDisconnected = Date.now();
node.warn("WebSocket connection timeout, reconnecting");
});
node.socket.on("unauthorized", () => () => {
let node = this;
node.state.websocket.isValid = false;
node.state.websocket.lastDisconnected = Date.now();
node.warn("WebSocket authentication failed");
});
}
async discoverDevices(opt) {
let node = this;
let options = Object.assign({
forceRefresh: false,
callback: () => {
}
}, opt);
async discoverDevices(opt) {
let node = this;
let options = Object.assign(
{
forceRefresh: false,
callback: () => {},
},
opt
);
if (options.forceRefresh === false || node.state.pooling.discoverProcessRunning === true) {
//node.log('discoverDevices: Using cached devices');
return;
}
if (
options.forceRefresh === false ||
node.state.pooling.discoverProcessRunning === true
) {
//node.log('discoverDevices: Using cached devices');
return;
}
node.state.pooling.discoverProcessRunning = true;
try {
let mainConfig = await got(node.api.url.main(), { retry: 1, timeout: 2000 }).json();
try {
let group0 = await got(node.api.url.main() + node.api.url.groups.main(0), {
retry: 1,
timeout: 2000
}).json();
node.device_list.all_group_real_id = group0.id;
mainConfig.groups['0'] = group0;
} catch (e) {
node.log(
`discoverDevices: Could not get group 0 ${e.toString()}. This should not happen, ` +
`please open an issue on https://github.com/dresden-elektronik/deconz-rest-plugin`
);
}
node.device_list.parse(mainConfig);
//node.log(`discoverDevices: Updated ${node.device_list.count}`);
node.state.pooling.discoverProcessRunning = false;
return true;
} catch (e) {
if (e.response !== undefined && e.response.statusCode === 403) {
node.state.startFailed = true;
let code = RED._('node-red-contrib-deconz/server:status.invalid_api_key');
let reason = "discoverDevices: Can't use to deconz API, invalid api key. " +
"Please check server configuration.";
node.error(reason);
node.propagateErrorNews(code, reason, true);
node.onClose();
}
//node.error(`discoverDevices: Can't connect to deconz API.`);
node.state.pooling.discoverProcessRunning = false;
return false;
node.state.pooling.discoverProcessRunning = true;
try {
let mainConfig = await got(node.api.url.main(), {
retry: 1,
timeout: 2000,
}).json();
try {
let group0 = await got(
node.api.url.main() + node.api.url.groups.main(0),
{
retry: 1,
timeout: 2000,
}
).json();
node.device_list.all_group_real_id = group0.id;
mainConfig.groups["0"] = group0;
} catch (e) {
node.log(
`discoverDevices: Could not get group 0 ${e.toString()}. This should not happen, ` +
`please open an issue on https://github.com/dresden-elektronik/deconz-rest-plugin`
);
}
node.device_list.parse(mainConfig);
//node.log(`discoverDevices: Updated ${node.device_list.count}`);
node.state.pooling.discoverProcessRunning = false;
return true;
} catch (e) {
if (e.response !== undefined && e.response.statusCode === 403) {
node.state.startFailed = true;
let code = RED._(
"node-red-contrib-deconz/server:status.invalid_api_key"
);
let reason =
"discoverDevices: Can't use to deconz API, invalid api key. " +
"Please check server configuration.";
node.error(reason);
node.propagateErrorNews(code, reason, true);
node.onClose();
}
//node.error(`discoverDevices: Can't connect to deconz API.`);
node.state.pooling.discoverProcessRunning = false;
return false;
}
}
propagateStartNews(whitelistNodes) {
let node = this;
// Node with device selected
propagateStartNews(whitelistNodes) {
let node = this;
// Node with device selected
let filterMethod;
if (Array.isArray(whitelistNodes)) {
filterMethod = (id) => whitelistNodes.includes(id);
}
let filterMethod;
if (Array.isArray(whitelistNodes)) {
filterMethod = (id) => whitelistNodes.includes(id);
}
for (let [device_path, nodeIDs] of Object.entries(node.nodesByDevicePath)) {
if (filterMethod) nodeIDs = nodeIDs.filter(filterMethod);
node.propagateNews(nodeIDs, {
type: 'start',
node_type: 'device_path',
device: node.device_list.getDeviceByPath(device_path)
});
}
for (let [device_path, nodeIDs] of Object.entries(
node.nodesByDevicePath
)) {
if (filterMethod) nodeIDs = nodeIDs.filter(filterMethod);
node.propagateNews(nodeIDs, {
type: "start",
node_type: "device_path",
device: node.device_list.getDeviceByPath(device_path),
});
}
// Node with quety
for (let nodeID of node.nodesWithQuery) {
if (filterMethod && filterMethod(nodeID) === false) continue;
let target = RED.nodes.getNode(nodeID);
// Node with quety
for (let nodeID of node.nodesWithQuery) {
if (filterMethod && filterMethod(nodeID) === false) continue;
let target = RED.nodes.getNode(nodeID);
if (!target) {
node.warn('ERROR: cant get ' + nodeID + ' node for start news, removed from list NodeWithQuery');
node.unregisterNodeWithQuery(nodeID);
continue;
}
if (!target) {
node.warn(
"ERROR: cant get " +
nodeID +
" node for start news, removed from list NodeWithQuery"
);
node.unregisterNodeWithQuery(nodeID);
continue;
}
// TODO Cache JSONata expresssions ?
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(target.config.query, target),
{},
undefined
);
try {
let devices = node.device_list.getDevicesByQuery(querySrc);
if (devices.matched.length === 0) continue;
for (let device of devices.matched) {
node.propagateNews(nodeID, {
type: 'start',
node_type: 'query',
device: device,
});
}
} catch (e) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.query_error"
});
node.error(e.toString() + '\nNode ID : ' + nodeID + '\nQuery: ' + JSON.stringify(querySrc));
}
}
// TODO Cache JSONata expresssions ?
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(target.config.query, target),
{},
undefined
);
try {
let devices = node.device_list.getDevicesByQuery(querySrc);
if (devices.matched.length === 0) continue;
for (let device of devices.matched) {
node.propagateNews(nodeID, {
type: "start",
node_type: "query",
device: device,
});
}
} catch (e) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.query_error",
});
node.error(
e.toString() +
"\nNode ID : " +
nodeID +
"\nQuery: " +
JSON.stringify(querySrc)
);
}
}
}
propagateErrorNews(code, reason, isGlobalError = false) {
let node = this;
if (!reason) return;
propagateErrorNews(code, reason, isGlobalError = false) {
let node = this;
if (!reason) return;
if (node.state.ready === false) {
RED.nodes.eachNode((target) => {
if (['deconz-input', 'deconz-battery', 'deconz-get', 'deconz-out', 'deconz-event'].includes(target.type)) {
let targetNode = RED.nodes.getNode(target.id);
if (targetNode) targetNode.status({
fill: "red",
shape: "dot",
text: code
});
}
});
return;
}
if (node.state.ready === false) {
RED.nodes.eachNode((target) => {
if (
[
"deconz-input",
"deconz-battery",
"deconz-get",
"deconz-out",
"deconz-event",
].includes(target.type)
) {
let targetNode = RED.nodes.getNode(target.id);
if (targetNode)
targetNode.status({
fill: "red",
shape: "dot",
text: code,
});
}
});
return;
}
// Node with device selected
for (let [device_path, nodeIDs] of Object.entries(node.nodesByDevicePath)) {
node.propagateNews(nodeIDs, {
type: 'error',
node_type: 'device_path',
device: node.device_list.getDeviceByPath(device_path),
errorCode: code,
errorMsg: reason || "Unknown error",
isGlobalError
});
}
// Node with device selected
for (let [device_path, nodeIDs] of Object.entries(
node.nodesByDevicePath
)) {
node.propagateNews(nodeIDs, {
type: "error",
node_type: "device_path",
device: node.device_list.getDeviceByPath(device_path),
errorCode: code,
errorMsg: reason || "Unknown error",
isGlobalError,
});
}
// Node with quety
for (let nodeID of node.nodesWithQuery) {
let target = RED.nodes.getNode(nodeID);
// Node with quety
for (let nodeID of node.nodesWithQuery) {
let target = RED.nodes.getNode(nodeID);
if (!target) {
node.warn('ERROR: cant get ' + nodeID + ' node for error news, removed from list NodeWithQuery');
node.unregisterNodeWithQuery(nodeID);
continue;
}
if (!target) {
node.warn(
"ERROR: cant get " +
nodeID +
" node for error news, removed from list NodeWithQuery"
);
node.unregisterNodeWithQuery(nodeID);
continue;
}
// TODO Cache JSONata expresssions ?
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(target.config.query, target),
{},
undefined
);
try {
let devices = node.device_list.getDevicesByQuery(querySrc);
if (devices.matched.length === 0) continue;
for (let device of devices.matched) {
node.propagateNews(nodeID, {
type: 'error',
node_type: 'query',
device: device,
errorCode: code,
errorMsg: reason || "Unknown error",
isGlobalError
});
}
} catch (e) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.query_error"
});
node.error(e.toString() + '\nNode ID : ' + nodeID + '\nQuery: ' + JSON.stringify(querySrc));
}
}
// TODO Cache JSONata expresssions ?
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(target.config.query, target),
{},
undefined
);
try {
let devices = node.device_list.getDevicesByQuery(querySrc);
if (devices.matched.length === 0) continue;
for (let device of devices.matched) {
node.propagateNews(nodeID, {
type: "error",
node_type: "query",
device: device,
errorCode: code,
errorMsg: reason || "Unknown error",
isGlobalError,
});
}
} catch (e) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.query_error",
});
node.error(
e.toString() +
"\nNode ID : " +
nodeID +
"\nQuery: " +
JSON.stringify(querySrc)
);
}
}
}
/**
*
* @param nodeIDs List of nodes [nodeID1, nodeID2]
* @param news Object what kind of news need to be sent
* {type: 'start|event|error', eventData:{}, errorCode: "", errorMsg: "", device: {}, changed: {}}
*/
propagateNews(nodeIDs, news) {
//TODO add the event type in the msg
let node = this;
/**
*
* @param nodeIDs List of nodes [nodeID1, nodeID2]
* @param news Object what kind of news need to be sent
* {type: 'start|event|error', eventData:{}, errorCode: "", errorMsg: "", device: {}, changed: {}}
*/
propagateNews(nodeIDs, news) {
//TODO add the event type in the msg
let node = this;
// Make sure that we have node to send the message to
if (nodeIDs === undefined || Array.isArray(nodeIDs) && nodeIDs.length === 0) return;
if (!Array.isArray(nodeIDs)) nodeIDs = [nodeIDs];
// Make sure that we have node to send the message to
if (
nodeIDs === undefined ||
(Array.isArray(nodeIDs) && nodeIDs.length === 0)
)
return;
if (!Array.isArray(nodeIDs)) nodeIDs = [nodeIDs];
for (const nodeID of nodeIDs) {
let target = RED.nodes.getNode(nodeID);
for (const nodeID of nodeIDs) {
let target = RED.nodes.getNode(nodeID);
// Check if device exist
if (news.device === undefined) {
target.handleDeconzEvent(
news.device,
[],
{},
{
errorEvent: true,
errorCode: "DEVICE_NOT_FOUND",
errorMsg: "Device not found, please check server configuration"
}
);
continue;
}
// Check if device exist
if (news.device === undefined) {
target.handleDeconzEvent(
news.device,
[],
{},
{
errorEvent: true,
errorCode: "DEVICE_NOT_FOUND",
errorMsg: "Device not found, please check server configuration",
}
);
continue;
}
// If the target does not exist we remove it from the node list
if (!target) {
switch (news.node_type) {
case 'device_path':
node.warn('ERROR: cant get ' + nodeID + ' node, removed from list nodesByDevicePath');
node.unregisterNodeByDevicePath(nodeID, news.device.device_path);
break;
case 'query':
node.warn('ERROR: cant get ' + nodeID + ' node, removed from list nodesWithQuery');
node.unregisterNodeWithQuery(nodeID);
break;
case 'event_node':
node.warn('ERROR: cant get ' + nodeID + ' node, removed from list nodesEvent');
node.unregisterEventNode(nodeID);
break;
}
return;
}
// If the target does not exist we remove it from the node list
if (!target) {
switch (news.node_type) {
case "device_path":
node.warn(
"ERROR: cant get " +
nodeID +
" node, removed from list nodesByDevicePath"
);
node.unregisterNodeByDevicePath(nodeID, news.device.device_path);
break;
case "query":
node.warn(
"ERROR: cant get " +
nodeID +
" node, removed from list nodesWithQuery"
);
node.unregisterNodeWithQuery(nodeID);
break;
case "event_node":
node.warn(
"ERROR: cant get " +
nodeID +
" node, removed from list nodesEvent"
);
node.unregisterEventNode(nodeID);
break;
}
return;
}
switch (news.type) {
case 'start':
switch (target.type) {
case 'deconz-input':
case 'deconz-battery':
target.handleDeconzEvent(
news.device,
[],
news.device,
{ initialEvent: true }
);
break;
}
switch (news.type) {
case "start":
switch (target.type) {
case "deconz-input":
case "deconz-battery":
target.handleDeconzEvent(news.device, [], news.device, {
initialEvent: true,
});
break;
}
break;
case 'event':
let dataParsed = news.eventData;
switch (dataParsed.t) {
case "event":
if (target.type === "deconz-event") {
target.handleDeconzEvent(
news.device,
news.changed,
dataParsed
);
target.status({
fill: "green",
shape: "dot",
text: RED._('node-red-contrib-deconz/server:status.event_count')
.replace('{{event_count}}', node.state.websocket.eventCount)
});
} else {
switch (dataParsed.e) {
case "added":
case "deleted":
node.discoverDevices({
forceRefresh: true
}).then();
break;
case "changed":
if (['deconz-input', 'deconz-battery'].includes(target.type)) {
target.handleDeconzEvent(
news.device,
news.changed,
dataParsed
);
} else {
node.warn("WTF this is used : We tried to send a msg to a non input node.");
continue;
}
break;
case "scene-called":
if (target.type === 'deconz-input') {
target.handleDeconzEvent(
news.device,
news.changed,
dataParsed
);
}
break;
default:
node.warn("Unknown event of type '" + dataParsed.e + "'. " + JSON.stringify(dataParsed));
break;
}
}
break;
default:
node.warn("Unknown message of type '" + dataParsed.t + "'. " + JSON.stringify(dataParsed));
break;
}
break;
case 'error':
switch (target.type) {
case 'deconz-input':
case 'deconz-battery':
target.handleDeconzEvent(
news.device,
[],
{},
{
errorEvent: true,
errorCode: news.errorCode || "Unknown Error",
errorMsg: news.errorMsg || "Unknown Error"
}
);
break;
//TODO Implement other node types
}
break;
break;
case "event":
let dataParsed = news.eventData;
switch (dataParsed.t) {
case "event":
if (target.type === "deconz-event") {
target.handleDeconzEvent(
news.device,
news.changed,
dataParsed
);
target.status({
fill: "green",
shape: "dot",
text: RED._(
"node-red-contrib-deconz/server:status.event_count"
).replace(
"{{event_count}}",
node.state.websocket.eventCount
),
});
} else {
switch (dataParsed.e) {
case "added":
case "deleted":
node
.discoverDevices({
forceRefresh: true,
})
.then();
break;
case "changed":
if (
["deconz-input", "deconz-battery"].includes(target.type)
) {
target.handleDeconzEvent(
news.device,
news.changed,
dataParsed
);
} else {
node.warn(
"WTF this is used : We tried to send a msg to a non input node."
);
continue;
}
break;
case "scene-called":
if (target.type === "deconz-input") {
target.handleDeconzEvent(
news.device,
news.changed,
dataParsed
);
}
break;
default:
node.warn(
"Unknown event of type '" +
dataParsed.e +
"'. " +
JSON.stringify(dataParsed)
);
break;
}
}
break;
default:
node.warn(
"Unknown message of type '" +
dataParsed.t +
"'. " +
JSON.stringify(dataParsed)
);
break;
}
}
break;
case "error":
switch (target.type) {
case "deconz-input":
case "deconz-battery":
target.handleDeconzEvent(
news.device,
[],
{},
{
errorEvent: true,
errorCode: news.errorCode || "Unknown Error",
errorMsg: news.errorMsg || "Unknown Error",
}
);
break;
registerEventNode(nodeID) {
let node = this;
if (!node.nodesEvent.includes(nodeID)) node.nodesEvent.push(nodeID);
//TODO Implement other node types
}
break;
}
}
}
unregisterEventNode(nodeID) {
let node = this;
let index = node.nodesEvent.indexOf(nodeID);
if (index !== -1) node.nodesEvent.splice(index, 1);
}
registerEventNode(nodeID) {
let node = this;
if (!node.nodesEvent.includes(nodeID)) node.nodesEvent.push(nodeID);
}
registerNodeByDevicePath(nodeID, device_path) {
let node = this;
if (!(device_path in node.nodesByDevicePath)) node.nodesByDevicePath[device_path] = [];
if (!node.nodesByDevicePath[device_path].includes(nodeID)) node.nodesByDevicePath[device_path].push(nodeID);
}
unregisterEventNode(nodeID) {
let node = this;
let index = node.nodesEvent.indexOf(nodeID);
if (index !== -1) node.nodesEvent.splice(index, 1);
}
unregisterNodeByDevicePath(nodeID, device_path) {
let node = this;
let index = node.nodesByDevicePath[device_path].indexOf(nodeID);
if (index !== -1) node.nodesByDevicePath[device_path].splice(index, 1);
}
registerNodeByDevicePath(nodeID, device_path) {
let node = this;
if (!(device_path in node.nodesByDevicePath))
node.nodesByDevicePath[device_path] = [];
if (!node.nodesByDevicePath[device_path].includes(nodeID))
node.nodesByDevicePath[device_path].push(nodeID);
}
registerNodeWithQuery(nodeID) {
let node = this;
if (!node.nodesWithQuery.includes(nodeID)) node.nodesWithQuery.push(nodeID);
}
unregisterNodeByDevicePath(nodeID, device_path) {
let node = this;
let index = node.nodesByDevicePath[device_path].indexOf(nodeID);
if (index !== -1) node.nodesByDevicePath[device_path].splice(index, 1);
}
unregisterNodeWithQuery(nodeID) {
let node = this;
let index = node.nodesWithQuery.indexOf(nodeID);
if (index !== -1) node.nodesWithQuery.splice(index, 1);
}
registerNodeWithQuery(nodeID) {
let node = this;
if (!node.nodesWithQuery.includes(nodeID))
node.nodesWithQuery.push(nodeID);
}
onClose() {
let node = this;
node.state.isStopping = true;
node.log('Shutting down deconz server node.');
(async () => {
if (node.refreshDiscoverTimer)
await clearIntervalAsync(node.refreshDiscoverTimer);
})().then(() => {
node.state.ready = false;
if (node.socket !== undefined) {
node.socket.close();
node.socket = undefined;
}
node.log('Deconz server stopped!');
node.emit('onClose');
}).catch((error) => {
console.error("Error on Close", error);
});
}
unregisterNodeWithQuery(nodeID) {
let node = this;
let index = node.nodesWithQuery.indexOf(nodeID);
if (index !== -1) node.nodesWithQuery.splice(index, 1);
}
updateDevice(device, dataParsed) {
let node = this;
let changed = [];
onClose() {
let node = this;
node.state.isStopping = true;
node.log("Shutting down deconz server node.");
(async () => {
if (node.refreshDiscoverTimer)
await clearIntervalAsync(node.refreshDiscoverTimer);
})()
.then(() => {
node.state.ready = false;
if (node.socket !== undefined) {
node.socket.close();
node.socket = undefined;
}
node.log("Deconz server stopped!");
node.emit("onClose");
})
.catch((error) => {
console.error("Error on Close", error);
});
}
if (dotProp.has(dataParsed, 'name')) {
device.name = dotProp.get(dataParsed, 'name');
changed.push('name');
updateDevice(device, dataParsed) {
let node = this;
let changed = [];
if (dotProp.has(dataParsed, "name")) {
device.name = dotProp.get(dataParsed, "name");
changed.push("name");
}
["config", "state"].forEach(function (key) {
if (dotProp.has(dataParsed, key)) {
Object.keys(dotProp.get(dataParsed, key)).forEach(function (
state_name
) {
let valuePath = key + "." + state_name;
let newValue = dotProp.get(dataParsed, valuePath);
let oldValue = dotProp.get(device, valuePath);
if (newValue !== oldValue) {
changed.push(`${key}.${state_name}`);
dotProp.set(device, valuePath, newValue);
}
['config', 'state'].forEach(function (key) {
if (dotProp.has(dataParsed, key)) {
Object.keys(dotProp.get(dataParsed, key)).forEach(function (state_name) {
let valuePath = key + '.' + state_name;
let newValue = dotProp.get(dataParsed, valuePath);
let oldValue = dotProp.get(device, valuePath);
if (newValue !== oldValue) {
changed.push(`${key}.${state_name}`);
dotProp.set(device, valuePath, newValue);
}
});
}
});
return changed;
});
}
});
return changed;
}
onSocketMessage(dataParsed) {
let node = this;
node.state.websocket.lastEvent = Date.now();
node.state.websocket.isValid = true;
node.state.websocket.reachable = true;
if (node.state.websocket.eventCount >= Number.MAX_SAFE_INTEGER) node.state.websocket.eventCount = 0;
node.state.websocket.eventCount++;
onSocketMessage(dataParsed) {
let node = this;
node.state.websocket.lastEvent = Date.now();
node.state.websocket.isValid = true;
node.state.websocket.reachable = true;
if (node.state.websocket.eventCount >= Number.MAX_SAFE_INTEGER)
node.state.websocket.eventCount = 0;
node.state.websocket.eventCount++;
// Drop websocket msgs if the pooling don't work
if (node.state.pooling.isValid === false) return node.error('Got websocket msg but the pooling is invalid. This should not happen.');
// Drop websocket msgs if the pooling don't work
if (node.state.pooling.isValid === false)
return node.error(
"Got websocket msg but the pooling is invalid. This should not happen."
);
// There is an issue with the id of all lights magic group. The valid ID is 0.
if (
dataParsed.r === 'groups' &&
node.device_list.all_group_real_id !== undefined &&
dataParsed.id === node.device_list.all_group_real_id
) dataParsed.id = '0';
// There is an issue with the id of all lights magic group. The valid ID is 0.
if (
dataParsed.r === "groups" &&
node.device_list.all_group_real_id !== undefined &&
dataParsed.id === node.device_list.all_group_real_id
)
dataParsed.id = "0";
node.emit('onSocketMessage', dataParsed); //Used by event node, TODO Really used ?
node.emit("onSocketMessage", dataParsed); //Used by event node, TODO Really used ?
let device;
if (dataParsed.e === 'scene-called') {
device = node.device_list.getDeviceByDomainID('groups', dataParsed.gid);
} else {
device = node.device_list.getDeviceByDomainID(dataParsed.r, dataParsed.id);
}
let device;
if (dataParsed.e === "scene-called") {
device = node.device_list.getDeviceByDomainID("groups", dataParsed.gid);
} else {
device = node.device_list.getDeviceByDomainID(
dataParsed.r,
dataParsed.id
);
}
// TODO handle case if device is not found
if (device === undefined) return node.error('Got websocket msg but the device does not exist. ' + JSON.stringify(dataParsed));
let changed = node.updateDevice(device, dataParsed);
// TODO handle case if device is not found
if (device === undefined)
return node.error(
"Got websocket msg but the device does not exist. " +
JSON.stringify(dataParsed)
);
let changed = node.updateDevice(device, dataParsed);
// Node with device selected
node.propagateNews(node.nodesByDevicePath[device.device_path], {
type: 'event',
node_type: 'device_path',
eventData: dataParsed,
device: device,
changed: changed
});
// Node with device selected
node.propagateNews(node.nodesByDevicePath[device.device_path], {
type: "event",
node_type: "device_path",
eventData: dataParsed,
device: device,
changed: changed,
});
// Node with quety
let matched = [];
for (let nodeID of node.nodesWithQuery) {
let target = RED.nodes.getNode(nodeID);
// Node with quety
let matched = [];
for (let nodeID of node.nodesWithQuery) {
let target = RED.nodes.getNode(nodeID);
if (!target) {
node.warn('ERROR: cant get ' + nodeID + ' node for socket message news, removed from list NodeWithQuery');
node.unregisterNodeWithQuery(nodeID);
continue;
}
if (!target) {
node.warn(
"ERROR: cant get " +
nodeID +
" node for socket message news, removed from list NodeWithQuery"
);
node.unregisterNodeWithQuery(nodeID);
continue;
}
// TODO Cache JSONata expresssions ?
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(target.config.query, target),
{},
undefined
);
try {
let query = new Query(querySrc);
if (query.match(device)) {
matched.push(nodeID);
}
} catch (e) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.query_error"
});
node.error(e.toString() + '\nNode ID : ' + nodeID + '\nQuery: ' + JSON.stringify(querySrc));
}
}
// TODO Cache JSONata expresssions ?
let querySrc = RED.util.evaluateJSONataExpression(
RED.util.prepareJSONataExpression(target.config.query, target),
{},
undefined
);
try {
let query = new Query(querySrc);
if (query.match(device)) {
matched.push(nodeID);
}
} catch (e) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.query_error",
});
node.error(
e.toString() +
"\nNode ID : " +
nodeID +
"\nQuery: " +
JSON.stringify(querySrc)
);
}
}
if (matched.length > 0) node.propagateNews(matched, {
type: 'event',
node_type: 'query',
eventData: dataParsed,
device: device,
changed: changed
});
if (matched.length > 0)
node.propagateNews(matched, {
type: "event",
node_type: "query",
eventData: dataParsed,
device: device,
changed: changed,
});
// Event Nodes
node.propagateNews(node.nodesEvent, {
type: 'event',
node_type: 'event_node',
eventData: dataParsed,
device: device,
changed: changed
});
// Event Nodes
node.propagateNews(node.nodesEvent, {
type: "event",
node_type: "event_node",
eventData: dataParsed,
device: device,
changed: changed,
});
}
}
getDefaultMsg(nodeType) {
switch (nodeType) {
case "deconz-input":
return "node-red-contrib-deconz/server:status.connected";
case "deconz-get":
return "node-red-contrib-deconz/server:status.received";
case "deconz-output":
return "node-red-contrib-deconz/server:status.done";
}
}
getDefaultMsg(nodeType) {
switch (nodeType) {
case 'deconz-input':
return 'node-red-contrib-deconz/server:status.connected';
case 'deconz-get':
return 'node-red-contrib-deconz/server:status.received';
case 'deconz-output':
return 'node-red-contrib-deconz/server:status.done';
}
}
updateNodeStatus(node, msgToSend) {
if (node.server.ready === false) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.server_node_error",
});
return;
}
updateNodeStatus(node, msgToSend) {
if (node.server.ready === false) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.server_node_error"
});
return;
}
if (
node.config.search_type === "device" &&
node.config.device_list.length === 0 &&
// Check if the commands do not contain only scene call
!(
Array.isArray(node.config.commands) &&
node.config.commands.every(
(c) =>
(c.type === "deconz_state" && c.domain === "scene_call") ||
(c.type === "custom" &&
!["attribute", "state", "config"].includes(c.arg.target.type))
)
)
) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.device_not_set",
});
return;
}
if (node.config.search_type === "device" && (node.config.device_list.length === 0) &&
// Check if the commands do not contain only scene call
!(
Array.isArray(node.config.commands) &&
node.config.commands.every(
c => (c.type === 'deconz_state' && c.domain === 'scene_call') ||
(c.type === 'custom' && !['attribute', 'state', 'config'].includes(c.arg.target.type))
)
)
) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.device_not_set"
});
return;
}
if (msgToSend === null) {
node.status({
fill: "green",
shape: "dot",
text: this.getDefaultMsg(node.type),
});
return;
}
if (msgToSend === null) {
node.status({
fill: "green",
shape: "dot",
text: this.getDefaultMsg(node.type)
});
return;
}
let firstmsg = msgToSend[0];
if (firstmsg === undefined) return;
let firstmsg = msgToSend[0];
if (firstmsg === undefined) return;
if (
dotProp.get(firstmsg, "meta.state.reachable") === false ||
dotProp.get(firstmsg, "meta.config.reachable") === false
) {
node.status({
fill: "red",
shape: "ring",
text: "node-red-contrib-deconz/server:status.device_not_reachable",
});
return;
}
if (dotProp.get(firstmsg, 'meta.state.reachable') === false ||
dotProp.get(firstmsg, 'meta.config.reachable') === false
) {
switch (node.config.statustext_type) {
case "msg":
case "jsonata":
node.status({
fill: "green",
shape: "dot",
text: Utils.getNodeProperty(
{
type: node.config.statustext_type,
value: node.config.statustext,
},
node,
firstmsg
),
});
break;
case "auto":
switch (node.type) {
case "deconz-input":
case "deconz-get":
let firstOutputRule = node.config.output_rules[0];
if (firstOutputRule === undefined) return;
if (
Array.isArray(firstOutputRule.payload) &&
firstOutputRule.payload.length === 1 &&
!["__complete__", "__each__", "__auto__"].includes(
firstOutputRule.payload[0]
) &&
typeof firstmsg.payload !== "object"
) {
node.status({
fill: "red",
shape: "ring",
text: "node-red-contrib-deconz/server:status.device_not_reachable"
fill: "green",
shape: "dot",
text: firstmsg.payload,
});
return;
}
switch (node.config.statustext_type) {
case 'msg':
case 'jsonata':
node.status({
fill: "green",
shape: "dot",
text: Utils.getNodeProperty({
type: node.config.statustext_type,
value: node.config.statustext
}, node, firstmsg)
});
break;
case 'auto':
switch (node.type) {
case 'deconz-input':
case 'deconz-get':
let firstOutputRule = node.config.output_rules[0];
if (firstOutputRule === undefined) return;
if (Array.isArray(firstOutputRule.payload) && firstOutputRule.payload.length === 1 &&
!['__complete__', '__each__', '__auto__'].includes(firstOutputRule.payload[0]) &&
typeof firstmsg.payload !== 'object'
) {
node.status({
fill: "green",
shape: "dot",
text: firstmsg.payload
});
} else {
node.status({
fill: "green",
shape: "dot",
text: this.getDefaultMsg(node.type)
});
}
break;
case 'deconz-battery':
let battery = dotProp.get(firstmsg, 'meta.config.battery');
if (battery === undefined) return;
node.status({
fill: (battery >= 20) ? ((battery >= 50) ? "green" : "yellow") : "red",
shape: "dot",
text: battery + '%'
});
break;
}
break;
}
}
migrateNodeConfiguration(node) {
let configMigration = new ConfigMigration(node.type, node.config, this);
let migrationResult = configMigration.applyMigration(node.config, node);
if (Array.isArray(migrationResult.errors) && migrationResult.errors.length > 0) {
migrationResult.errors.forEach(
error => console.error(`Error with migration of node ${node.type} with id ${node.id}`, error)
);
node.error(
`Error with migration of node ${node.type} with id ${node.id}\n` +
migrationResult.errors.join('\n') +
'\nPlease open the node settings and update the configuration'
);
} else {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.migration_error"
fill: "green",
shape: "dot",
text: this.getDefaultMsg(node.type),
});
return false;
}
return true;
}
}
break;
case "deconz-battery":
let battery = dotProp.get(firstmsg, "meta.config.battery");
if (battery === undefined) return;
node.status({
fill:
battery >= 20 ? (battery >= 50 ? "green" : "yellow") : "red",
shape: "dot",
text: battery + "%",
});
break;
}
break;
}
}
RED.nodes.registerType('deconz-server', ServerNode, {
credentials: {
secured_apikey: { type: "text" }
}
});
migrateNodeConfiguration(node) {
let configMigration = new ConfigMigration(node.type, node.config, this);
let migrationResult = configMigration.applyMigration(node.config, node);
if (
Array.isArray(migrationResult.errors) &&
migrationResult.errors.length > 0
) {
migrationResult.errors.forEach((error) =>
console.error(
`Error with migration of node ${node.type} with id ${node.id}`,
error
)
);
node.error(
`Error with migration of node ${node.type} with id ${node.id}\n` +
migrationResult.errors.join("\n") +
"\nPlease open the node settings and update the configuration"
);
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.migration_error",
});
return false;
}
return true;
}
}
RED.nodes.registerType("deconz-server", ServerNode, {
credentials: {
secured_apikey: { type: "text" },
},
});
};
{
"name": "node-red-contrib-deconz",
"version": "2.3.5",
"version": "2.3.6",
"description": "deCONZ connectivity nodes for node-red",

@@ -62,3 +62,2 @@ "keywords": [

"grunt": "^1.3.0",
"grunt-contrib-jshint": "^3.0.0",
"grunt-contrib-uglify": "^5.0.1",

@@ -65,0 +64,0 @@ "grunt-contrib-watch": "^1.1.0",

@@ -1,2 +0,2 @@

class DeconzEditor{constructor(node,options={}){this.node=node,this.options=options}get elements(){return{}}get NRCD(){return"node-red-contrib-deconz"}findElements(){this.$elements={},Object.keys(this.elements).forEach(k=>{this.$elements[k]=this.findElement(this.elements[k])})}findElement(identifier){return"#"!==identifier.charAt(0)&&"."!==identifier.charAt(0)&&(identifier="#"+identifier),$(identifier)}async init(){this.findElements()}async connect(){}sendError(msg,timeout=1e4){let myNotification=RED.notify(msg,{timeout:timeout,type:"error",buttons:[{text:"Ok",class:"primary",click:()=>myNotification.close()}]})}getIcon(icon,includeClass=!1){return"deconz"===icon?"icons/node-red-contrib-deconz/icon-color.png":"homekit"===icon?"icons/node-red-contrib-deconz/homekit-logo.png":RED.nodes.fontAwesome.getIconList().includes("fa-"+icon)?`${includeClass?"fa ":""}fa-`+icon:icon}createIconElement(icon,container,isLarge=!1){if("fa-"===icon.substr(0,3)){if(RED.nodes.fontAwesome.getIconUnicode(icon)){let faIconElement=$("<i/>").appendTo(container);return void faIconElement.addClass("fa "+icon+(isLarge?" fa-lg":""))}icon=RED.settings.apiRootUrl+"icons/node-red/arrow-in.svg"}let imageIconElement=$("<div/>").appendTo(container);imageIconElement.css("backgroundImage","url("+icon+")")}getI18n(prefix,suffix,data={}){let _path=prefix;suffix&&(_path+="."+suffix),data.defaultValue="_deconz_undefined_";prefix=RED._(_path,data);if("_deconz_undefined_"!==prefix)return prefix}async generateSimpleListField(container,options){let input=$("<select/>",{id:options.id});if(options.choices)for(var[key,value]of options.choices)input.append($("<option/>").attr("value",key).html(RED._(value)));var row=await this.generateInputWithLabel(input,options);return container.append(row),void 0!==options.currentValue&&input.val(options.currentValue),input}async generateTypedInput(container,options){let input=$("<input/>",{id:options.id,placeholder:RED._(options.placeholder)});options=$("<input/>",{id:options.id+"_type",type:"hidden"});return input.append(options),input}async initTypedInput(input,options){options=$.extend({addDefaultTypes:!0,displayOnlyIcon:!1,value:{},width:"200px"},options);let typedInputOptions=$.extend({types:["msg","flow","global"]},options.typedInput);if(typedInputOptions.typeField=options.typeId,options.addDefaultTypes)for(var type of["msg","flow","global","jsonata"])typedInputOptions.types.includes(type)||typedInputOptions.types.push(type);if(options.displayOnlyIcon){let that=this;var valueLabel=function(a,b){let typeDefinition;for(const type of this.typeList)"object"==typeof type&&type.value===this.propertyType&&(typeDefinition=type);void 0!==typeDefinition&&void 0!==typeDefinition.icon&&(this.oldValue=this.input.val(),this.input.val(""),this.valueLabelContainer.hide(),that.createIconElement(typeDefinition.icon,this.selectLabel),this.selectTrigger.addClass("red-ui-typedInput-full-width"),this.selectLabel.show())};for(let type of typedInputOptions.types)"string"!=typeof type&&(type.hasValue=!0,type.valueLabel=valueLabel)}input.typedInput(typedInputOptions),void 0!==options.width&&input.typedInput("width",options.width),options.value&&(void 0!==options.value.type&&input.typedInput("type",options.value.type),void 0!==options.value.value&&input.typedInput("value",options.value.value))}async generateTypedInputField(container,options){var input=await this.generateTypedInput(container,{id:options.id,placeholder:this.getI18n(options.i18n,"placeholder")}),row=await this.generateInputWithLabel(input,options);return container.append(row),await this.initTypedInput(input,options),input}async generateDoubleTypedInputField(container,optionsFirst,optionsSecond){var inputFirst=await this.generateTypedInput(container,optionsFirst);let row=await this.generateInputWithLabel(inputFirst,optionsFirst);var inputSecond=await this.generateTypedInput(container,optionsSecond);row.append(inputSecond),container.append(row),optionsFirst.displayOnlyIcon=!0,optionsFirst.width="50px",optionsSecond.width="150px",await this.initTypedInput(inputFirst,optionsFirst),await this.initTypedInput(inputSecond,optionsSecond)}generateTypedInputType(i18n,name,data={}){if(data.value=name,void 0===data.label&&(data.label=this.getI18n(i18n,`options.${name}.label`,{})||name),!1!==data.icon&&void 0===data.icon&&(data.icon=this.getIcon(this.getI18n(i18n,`options.${name}.icon`))),data.icon&&"fa-"===data.icon.substr(0,3)&&(data.icon="fa "+data.icon),Array.isArray(data.subOptions)){Array.isArray(data.options)||(data.options=[]);for(const opt of data.subOptions)data.options.push(this.generateTypedInputType(i18n+".options."+name,"string"==typeof opt?opt:opt.name,{icon:!1}))}return data}async generateCheckboxField(container,options){var input=$("<input/>",{id:options.id,type:"checkbox",style:"display: table-cell; width: 14px;vertical-align: top;margin-right: 5px",checked:options.currentValue});let row=await this.generateInputWithLabel(input,options);row.append($("<span/>").html(RED._(options.descText)).css("display","table-cell")),container.append(row)}async generateInputWithLabel(input,options={}){let row=$("<div/>",{class:"form-row",style:"padding:5px;margin:0;display:table;min-width:420px;"});var inputID=input.attr("id");if(inputID){let labelElement=$("<label/>");labelElement.attr("for",inputID),labelElement.attr("class","l-width"),labelElement.attr("style","display:table-cell;"),void 0===options.title&&(options.title=this.getI18n(options.i18n,"title")),options.title&&labelElement.attr("title",this.getI18n(options.i18n,"title")),void 0===options.icon&&(options.icon=this.getI18n(options.i18n,"icon")),options.icon&&(this.createIconElement(this.getIcon(options.icon),labelElement),labelElement.append("&nbsp;")),void 0===options.label&&(options.label=this.getI18n(options.i18n,"label")),options.label&&labelElement.append(`<span>${options.label}</span>`),row.append(labelElement)}return input.css("display","table-cell"),row.append(input),row}async generateHR(container,topBottom="5px",leftRight="50px"){container.append(`<hr style="margin: ${topBottom} ${leftRight};">`)}async generateSeparator(container,label){container.append(`<div class="separator">${RED._(label)}</div>`)}static versionCompare(v1,v2,options={}){const lexicographical=options&&options.lexicographical;options=options&&options.zeroExtend;let v1parts=v1.split("."),v2parts=v2.split(".");function isValidPart(x){return(lexicographical?/^\d+[A-Za-z]*$/:/^\d+$/).test(x)}if(!v1parts.every(isValidPart)||!v2parts.every(isValidPart))return NaN;if(options){for(;v1parts.length<v2parts.length;)v1parts.push("0");for(;v2parts.length<v1parts.length;)v2parts.push("0")}lexicographical||(v1parts=v1parts.map(Number),v2parts=v2parts.map(Number));for(let i=0;i<v1parts.length;++i){if(v2parts.length===i)return 1;if(v1parts[i]!==v2parts[i])return v1parts[i]>v2parts[i]?1:-1}return v1parts.length!==v2parts.length?-1:0}}class DeconzMainEditor extends DeconzEditor{constructor(node,options={}){if(super(node,$.extend(!0,{have:{statustext:!0,query:!0,device:!0,output_rules:!1,commands:!1,specific:!1},device:{batteryFilter:!1},output_rules:{format:{single:!0,array:!1,sum:!1,average:!1,min:!1,max:!1},type:{attribute:!0,state:!0,config:!0,homekit:!1,scene_call:!1}},commands:{type:{deconz_state:!0,homekit:!0,custom:!0,pause:!0}},specific:{api:{},output:{},server:{}}},options)),this.subEditor={},this.initDone=!1,this.options.have.statustext&&(this.subEditor.statustext=new DeconzStatusTextEditor(this.node,this.options.statustext)),this.options.have.device&&(this.subEditor.device=new DeconzDeviceEditor(this.node,this.options.device)),this.options.have.query&&(this.subEditor.query=new DeconzQueryEditor(this.node,this.options.query)),this.options.have.specific)switch(this.node.type){case"deconz-api":this.subEditor.specific=new DeconzSpecificApiEditor(this.node,this.options.specific.api);break;case"deconz-output":this.subEditor.specific=new DeconzSpecificOutputEditor(this.node,this.options.specific.output);break;case"deconz-server":this.subEditor.specific=new DeconzSpecificServerEditor(this.node,this.options.specific.server)}this.options.have.output_rules&&(this.subEditor.output_rules=new DeconzOutputRuleListEditor(this.node,this.options.output_rules)),this.options.have.commands&&(this.subEditor.commands=new DeconzCommandListEditor(this.node,this.options.commands))}get elements(){return{tipBox:"node-input-tip-box",server:"node-input-server"}}async configurationMigration(){if(!((this.node.config_version||0)>=this.node._def.defaults.config_version.value)){let config={};for(const key of Object.keys(this.node._def.defaults))config[key]=this.node[key];var data={id:this.node.id,type:this.node.type,config:JSON.stringify(config)};let errorMsg="Error while migrating the configuration of the node from version "+(this.node.config_version||0)+" to version "+this.node._def.defaults.config_version.value+".",result=await $.getJSON(this.NRCD+"/configurationMigration",data).catch((t,u)=>{this.$elements.tipBox.append(`<div class="form-tips form-warning"><p>Migration errors:</p><p>${errorMsg}</p></div>`)});if(result&&!result.notNeeded){if(result.new)for(var[key,value]of Object.entries(result.new))this.node[key]=value;if(result.delete&&Array.isArray(result.delete))for(const key of result.delete)delete this.node[key];data=msg=>"node-red-contrib-deconz"===msg.substr(0,23)?RED._(msg):msg;result.errors&&Array.isArray(result.errors)&&0<result.errors.length&&this.$elements.tipBox.append('<div class="form-tips form-warning"><p>Migration errors:</p><ul>'+`<li>${result.errors.map(data).join("</li><li>")}</li>`+"</ul></div>"),result.info&&Array.isArray(result.info)&&0<result.info.length&&this.$elements.tipBox.append('<div class="form-tips"><p>Migration info:</p><ul>'+`<li>${result.info.map(data).join("</li><li>")}</li>`+"</ul></div>")}}}async init(){await new Promise(resolve=>setTimeout(resolve,100)),await super.init(),await this.configurationMigration(),this.initPromises=[];for(const editor of Object.values(this.subEditor))this.initPromises.push(editor.init(this));await Promise.all(this.initPromises),this.initDone=!0,delete this.initPromises;let connectPromises=[];for(const editor of Object.values(this.subEditor))connectPromises.push(editor.connect());await Promise.all(connectPromises)}get serverNode(){return"deconz-server"===this.node.type?this.node:RED.nodes.node(this.$elements.server.val())}async isInitialized(){this.initDone||await Promise.all(this.initPromises)}async updateQueryDeviceDisplay(options){var type=this.subEditor.query.$elements.select.typedInput("type");switch(type){case"device":await this.subEditor.device.updateList(options);break;case"json":case"jsonata":this.subEditor.query.$elements.select.typedInput("validate")&&await this.subEditor.query.updateList(options)}await this.subEditor.device.display("device"===type),await this.subEditor.query.display("device"!==type)}oneditsave(){var newRules;this.options.have.output_rules&&(newRules=this.subEditor.output_rules.value,this.node.outputs=newRules.length,this.node.output_rules=newRules),this.options.have.commands&&(this.node.commands=this.subEditor.commands.value),this.options.have.specific&&(this.node.specific=this.subEditor.specific.value)}}class DeconzStatusTextEditor extends DeconzEditor{constructor(node,options={}){super(node,$.extend({allowedTypes:["msg","jsonata"]},options))}get elements(){return{statustext:"node-input-statustext"}}async init(mainEditor){await super.init(),this.mainEditor=mainEditor,this.initTypedInput()}initTypedInput(){let options=[];this.mainEditor.options.have.statustext&&options.push({value:"auto",label:RED._(this.NRCD+"/server:editor.inputs.statustext.options.auto"),icon:`icons/${this.NRCD}/icon-color.png`,hasValue:!1}),this.$elements.statustext.typedInput({type:"auto",types:options.concat(this.options.allowedTypes),typeField:`#${this.elements.statustext}_type`})}}class DeconzDeviceListEditor extends DeconzEditor{constructor(node,options={}){super(node,options)}get xhrURL(){return this.NRCD+"/itemlist"}get xhrParams(){return{controllerID:this.mainEditor.serverNode.id,forceRefresh:this.options.refresh}}async display(display=!0){if(this.$elements.showHide)return display?this.$elements.showHide.show():this.$elements.showHide.hide(),this.$elements.showHide.promise()}async getItems(options,xhrParams){xhrParams.forceRefresh=options.refresh;xhrParams=await $.getJSON(this.xhrURL,xhrParams).catch((t,u)=>{this.sendError(400===t.status&&t.responseText?t.responseText:u.toString())});if(!xhrParams||!xhrParams.error_message)return this.formatItemList(xhrParams.items,options.keepOnlyMatched);console.warn(xhrParams.error_message),RED.notify("Warning : "+xhrParams.error_message,{type:"warning",timeout:1e4})}async updateList(options){if(options=$.extend({refresh:!0},options),this.mainEditor.serverNode){let list=this.$elements.list,params=this.xhrParams;!0===this.options.batteryFilter&&(options.keepOnlyMatched=!0,params.query=JSON.stringify({type:"match",match:{"config.battery":{type:"complex",operator:"!==",value:void 0}}}));options=await this.getItems(options,params);list.children().remove(),options&&this.generateHtmlItemList(options,this.$elements.list),list.multipleSelect("refresh"),options&&list.multipleSelect("enable")}}formatItemList(items,keepOnlyMatched=!1){let itemList={};var injectItems=(part,matched)=>{part.forEach(item=>{var device_type=item.type;void 0===itemList[device_type]&&(itemList[device_type]=[]),item.query_match=matched,itemList[device_type].push(item)})};return injectItems(items.matched,!0),!1===keepOnlyMatched&&injectItems(items.rejected,!1),itemList}generateHtmlItemList(items,htmlContainer){var group_key,item_list,queryMode=this.constructor===DeconzQueryEditor;for([group_key,item_list]of Object.entries(items).sort((a,b)=>{a=a[0].toLowerCase(),b=b[0].toLowerCase();return a<b?-1:b<a?1:0})){let groupHtml=$("<optgroup/>").attr("label",group_key);for(const item of item_list.sort((a,b)=>{a=a.name.toLowerCase(),b=b.name.toLowerCase();return a<b?-1:b<a?1:0})){let label=item.name,opt=("groups"===item.device_type&&(label+=" (lights: "+item.lights.length,item.scenes.length&&(label+=", scenes: "+item.scenes.length),label+=")"),$("<option>"+label+"</option>").attr("value",item.device_path));queryMode&&item.query_match&&opt.attr("selected",""),opt.appendTo(groupHtml)}groupHtml.appendTo(htmlContainer)}}}class DeconzQueryEditor extends DeconzDeviceListEditor{constructor(node,options={}){super(node,$.extend({allowedTypes:["json","jsonata"]},options))}get elements(){return{select:"node-input-query",list:"node-input-query_result",showHide:".deconz-query-selector",refreshButton:"#force-refresh-query-result"}}get type(){return this.$elements.select.typedInput("type")}set type(val){this.$elements.list.typedInput("type",val)}get value(){return this.$elements.select.typedInput("value")}set value(val){this.$elements.list.typedInput("value",val)}get xhrParams(){let params=super.xhrParams;return params.query=this.value,params.queryType=this.type,params.nodeID=this.node.id,params}async init(mainEditor){await super.init(),this.mainEditor=mainEditor,this.initTypedInput(),this.$elements.list.multipleSelect({maxHeight:300,dropWidth:320,width:320,single:!1,selectAll:!1,filter:!0,filterPlaceholder:RED._(this.NRCD+"/server:editor.inputs.device.device.filter"),placeholder:RED._(this.NRCD+"/server:editor.multiselect.none_selected"),numberDisplayed:1,disableIfEmpty:!0,showClear:!1,hideOptgroupCheckboxes:!0,filterGroup:!0,onClick:view=>{this.$elements.list.multipleSelect(view.selected?"uncheck":"check",view.value)}}),await this.mainEditor.updateQueryDeviceDisplay({useSavedData:!0})}initTypedInput(){let options=[];this.mainEditor.options.have.device&&options.push({value:"device",label:RED._(this.NRCD+"/server:editor.inputs.device.query.options.device"),icon:`icons/${this.NRCD}/icon-color.png`,hasValue:!1}),this.$elements.select.typedInput({type:"text",types:options.concat(this.options.allowedTypes),typeField:"#node-input-search_type"})}async connect(){await super.connect(),this.$elements.select.on("change",()=>{this.mainEditor.updateQueryDeviceDisplay({useSavedData:!0}),this.mainEditor.options.have.output_rules&&this.mainEditor.subEditor.output_rules.refresh()}),this.$elements.refreshButton.on("click",()=>{this.updateList(),this.mainEditor.options.have.output_rules&&this.mainEditor.subEditor.output_rules.refresh()})}}class DeconzDeviceEditor extends DeconzDeviceListEditor{constructor(node,options={}){super(node,$.extend({batteryFilter:!1},options))}get elements(){return{list:"node-input-device_list",showHide:".deconz-device-selector",refreshButton:"#force-refresh"}}get value(){return this.$elements.list.multipleSelect("getSelects")}set value(val){this.$elements.list.multipleSelect("setSelects",val)}async init(mainEditor){await super.init(),this.mainEditor=mainEditor,this.$elements.list.multipleSelect({maxHeight:300,dropWidth:320,width:320,single:"multiple"!==this.$elements.list.attr("multiple"),filter:!0,filterPlaceholder:RED._(this.NRCD+"/server:editor.inputs.device.device.filter"),placeholder:RED._(this.NRCD+"/server:editor.multiselect.none_selected"),showClear:!0})}async connect(){await super.connect();var refreshDeviceList=()=>{this.updateList($.extend(this.options,{useSelectedData:!0})),this.mainEditor.options.have.output_rules&&this.mainEditor.subEditor.output_rules.refresh()};this.mainEditor.$elements.server.on("change",refreshDeviceList),this.$elements.refreshButton.on("click",refreshDeviceList),this.mainEditor.options.have.output_rules&&this.$elements.list.on("change",()=>{this.mainEditor.subEditor.output_rules.refresh()})}async updateList(options){let itemsSelected;(options=$.extend({useSavedData:!1,useSelectedData:!1},options)).useSelectedData&&(itemsSelected=this.$elements.list.multipleSelect("getSelects")),await super.updateList(options),options.useSavedData&&Array.isArray(this.node.device_list)?this.$elements.list.multipleSelect("setSelects",this.node.device_list):options.useSelectedData&&Array.isArray(itemsSelected)&&this.$elements.list.multipleSelect("setSelects",itemsSelected)}}class DeconzSpecificApiEditor extends DeconzEditor{constructor(node,options={}){super(node,$.extend({},options))}get elements(){return{container:"deconz-api-form",name:"node-config-input-name",method:"node-config-input-method",endpoint:"node-config-input-endpoint",payload:"node-config-input-payload"}}get default(){return{name:"",method:{type:"GET"},endpoint:{type:"str",value:"/"},payload:{type:"json",value:"{}"}}}async init(){this.node.specific=$.extend(!0,this.default.specific,this.node.specific);var container=this.findElement(this.elements.container);await this.generateMethodField(container,this.node.specific.method),await this.generateEndpointField(container,this.node.specific.endpoint),await this.generatePayloadField(container,this.node.specific.payload),await super.init()}async connect(){await super.connect()}get value(){return{method:{type:this.$elements.method.typedInput("type"),value:this.$elements.method.typedInput("value")},endpoint:{type:this.$elements.endpoint.typedInput("type"),value:this.$elements.endpoint.typedInput("value")},payload:{type:this.$elements.payload.typedInput("type"),value:this.$elements.payload.typedInput("value")}}}set value(newValues){this.$elements.name.val(newValues.name);for(var field of["method","endpoint","payload"])this.$elements[field].typedInput("type",newValues[field].type),this.$elements[field].typedInput("value",newValues[field].value);for(const element of Object.values(this.$elements))element.trigger("change")}async generateMethodField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.api.method";await this.generateTypedInputField(container,{id:this.elements.method,i18n:i18n,value:value,width:"250px",typedInput:{types:[this.generateTypedInputType(i18n,"GET",{hasValue:!1}),this.generateTypedInputType(i18n,"POST",{hasValue:!1}),this.generateTypedInputType(i18n,"PUT",{hasValue:!1}),this.generateTypedInputType(i18n,"DELETE",{hasValue:!1})]}})}async generateEndpointField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.api.endpoint";await this.generateTypedInputField(container,{id:this.elements.endpoint,i18n:i18n,value:value,width:"250px",typedInput:{types:["str"]}})}async generatePayloadField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.api.payload";await this.generateTypedInputField(container,{id:this.elements.payload,i18n:i18n,value:value,width:"250px",typedInput:{types:["json","jsonata"]}})}}class DeconzSpecificOutputEditor extends DeconzEditor{constructor(node,options={}){super(node,$.extend({},options))}get elements(){return{container:"specific-container",delay:"node-input-delay",result:"node-input-result"}}get default(){return{delay:{type:"num",value:50},result:{type:"at_end"}}}async init(){this.node.specific=$.extend(!0,this.default,this.node.specific);var container=this.findElement(this.elements.container);await this.generateSeparator(container,this.NRCD+"/server:editor.inputs.separator.specific"),await this.generateDelayField(container,this.node.specific.delay),await this.generateResultField(container,this.node.specific.result),await super.init()}async generateDelayField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.output.delay";await this.generateTypedInputField(container,{id:this.elements.delay,i18n:i18n,value:value,width:"250px",typedInput:{types:["num"]}})}async generateResultField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.output.result";await this.generateTypedInputField(container,{id:this.elements.result,i18n:i18n,value:value,width:"250px",typedInput:{types:[this.generateTypedInputType(i18n,"never",{hasValue:!1}),this.generateTypedInputType(i18n,"after_command",{hasValue:!1}),this.generateTypedInputType(i18n,"at_end",{hasValue:!1})]}})}async connect(){await super.connect()}get value(){return{delay:{type:this.$elements.delay.typedInput("type"),value:this.$elements.delay.typedInput("value")},result:{type:this.$elements.result.typedInput("type"),value:this.$elements.result.typedInput("value")}}}}class DeconzSpecificServerEditor extends DeconzEditor{constructor(node,options={}){super(node,$.extend({},options))}get elements(){return{name:"node-config-input-name",ip:"node-config-input-ip",port:"node-config-input-port",apikey:"node-config-input-secured_apikey",ws_port:"node-config-input-ws_port",secure:"node-config-input-secure",polling:"node-config-input-polling",getSettingsButton:"node-contrib-deconz-get-settings"}}get default(){return{name:"",ip:"",port:"",apikey:"",ws_port:"",secure:!1,polling:15}}get xhrURL(){return this.NRCD+"/serverAutoconfig"}async init(){this.node.specific=$.extend(!0,this.default,this.node.specific),await super.init(),this.node.migration_secured_apikey&&(this.$elements.apikey.val(this.node.migration_secured_apikey),this.$elements.apikey.trigger("change"))}async connect(){await super.connect(),this.$elements.getSettingsButton.on("click",()=>this.discoverParams())}async discoverParams(overrideSettings){void 0===overrideSettings&&(overrideSettings={});let myNotification,stop=!1,closeNotification=()=>{myNotification&&"function"==typeof myNotification.close&&myNotification.close(),stop=!0};myNotification=RED.notify("<p>Trying to find the server settings, please wait...<br>This can take up to 15 seconds.</p>",{modal:!0,fixed:!0,type:"info",buttons:[{text:"Cancel",class:"primary",click:closeNotification}]});try{let params=Object.assign({},this.value,overrideSettings),request=(void 0===params.discoverParam&&(params.discoverParam={}),params.discoverParam.devicetype="Node-Red Deconz Plugin"+(this.node?" id:"+this.node.id:""),await $.getJSON(this.xhrURL,{config:JSON.stringify(params)}).catch((t,u)=>{this.sendError(400===t.status&&t.responseText?t.responseText:u.toString())}));if(!stop)if(request.error){let html=`<p>Error ${request.error.code}: ${request.error.description}</p>`,buttons=[{text:"Cancel",click:closeNotification}];switch(request.error.code){case"GATEWAY_CHOICE":html+="<p>There is multiple Deconz device in you network, please select the one you want to configure.</p>";let node=this;for(const[index,gateway]of request.error.gateway_list.entries())buttons.push({text:`#${index+1}: `+gateway.name,id:"node-red-contrib-deconz-gateway-id-"+index,class:"primary",click:()=>{var gateway_id=gateway.bridge_id;closeNotification(),gateway_id&&(request.currentSettings.discoverParam.targetGatewayID=gateway_id),node.discoverParams(request.currentSettings)}});buttons.push(buttons.shift());break;case"DECONZ_ERROR":101===request.error.type&&(buttons.unshift({text:"I pressed the link button",class:"primary",click:()=>{closeNotification(),this.discoverParams(request.currentSettings)}}),html=(html+="<p>The reason why the request failed is that the gateway was not unlocked. This mechanism is needed to prevent anybody from access to the gateway without being permitted to do so.</p><ul><li>In a new browser tab open the <a href='http://phoscon.de/pwa/' target='_blank'>Phoscon App</a></li><li>Click on Menu -> Settings -> Gateway</li><li>Click on \"Advanced\" button</li><li>Click on the \"Authenticate app\" button</li></ul>")+`<p>Within 60 seconds after unlocking the gateway, click on the button "${buttons[0].text}".</p>`);break;default:buttons[buttons.length-1].text="Cancel"}html+=`<p>Logs:</p><pre>${request.log.join("\n")}</pre>`,closeNotification(),myNotification=RED.notify(html,{modal:!0,fixed:!0,type:"error",buttons:buttons})}else request.success?(closeNotification(),myNotification=RED.notify("<p>Settings fetched successfully !</p>",{modal:!1,fixed:!1,type:"success"}),this.value=request.currentSettings):(closeNotification(),myNotification=RED.notify(`<p>Unknown error : ${JSON.stringify(request)}</p>`,{modal:!0,fixed:!0,type:"error",buttons:[{text:"Ok",class:"primary",click:closeNotification}]}))}catch(error){closeNotification(),myNotification=RED.notify(`<p>Error while processing request: ${error.toString()}</p>`,{type:"error"})}}get value(){return{name:this.$elements.name.val(),ip:this.$elements.ip.val(),port:this.$elements.port.val(),apikey:this.$elements.apikey.val(),ws_port:this.$elements.ws_port.val(),secure:this.$elements.secure.prop("checked"),polling:this.$elements.polling.val()}}set value(newValues){this.$elements.name.val(newValues.name),this.$elements.ip.val(newValues.ip),this.$elements.port.val(newValues.port),this.$elements.apikey.val(newValues.apikey),this.$elements.ws_port.val(newValues.ws_port),this.$elements.secure.prop("checked",newValues.secure),this.$elements.polling.val(newValues.polling);for(const element of Object.values(this.$elements))element.trigger("change")}}class DeconzListItemEditor extends DeconzEditor{constructor(node,listEditor,container,options={}){super(node,options),this.listEditor=listEditor,container.uniqueId(),this.uniqueId=container.attr("id"),this.container=container}set index(value){void 0!==value&&this.$elements&&this.$elements.outputButton&&this.$elements.outputButton.find(".node-input-rule-index").html(value+1),this._index=value}get index(){return this._index}async init(){await this.generateOutputButton(this.container.children().first()),await super.init()}async generateOutputButton(container){$("<a/>",{id:this.elements.outputButton,class:"red-ui-button top-right-badge",title:RED._(this.options.button_title)}).append(`&nbsp;&#8594;&nbsp;<span class="node-input-rule-index">${this.index+1}</span>&nbsp;`).appendTo(container)}}class DeconzListItemListEditor extends DeconzEditor{constructor(node,options={}){super(node,options),this.items={}}get listType(){return"item"}get buttons(){return[]}async init(mainEditor){await super.init(),this.mainEditor=mainEditor}async initList(itemEditorClass,items=[]){var buttons=this.buttons,addButton=0===buttons.length||DeconzEditor.versionCompare(RED.settings.version,"1.3.0")<0;this.$elements.list.editableList({sortable:!0,removable:!0,height:"auto",addButton:addButton,buttons:buttons,addItem:(row,index,item)=>{let itemEditor=new itemEditorClass(this.node,this,row,this.options);item.uniqueId=itemEditor.uniqueId,(this.items[item.uniqueId]=itemEditor).init(item,index)},removeItem:item=>{if(!item.uniqueId||!this.items[item.uniqueId])throw new Error(`Error while removing the ${this.listType}, the ${this.listType} ${item.uniqueId} does not exist.`);var deletedIndex=this.items[item.uniqueId].index;delete this.items[item.uniqueId];for(const item of Object.values(this.items))item.index>deletedIndex&&item.index--},sortItems:items=>{items.each((index,item)=>{if(!this.items[item.attr("id")])throw new Error(`Error while moving the ${this.listType}, the ${this.listType} ${index+1} does not exist.`);this.items[item.attr("id")].index=index})}}),0<items.length&&this.$elements.list.editableList("addItems",items)}get value(){let result=[];for(const rule of Object.values(this.items).sort((a,b)=>a.index-b.index))result.push(rule.value);return result}refresh(){}}class DeconzOutputRuleEditor extends DeconzListItemEditor{constructor(node,listEditor,container,options={}){super(node,listEditor,container,options=$.extend({enableEachState:!0},options))}get elements(){let elements={};for(const key of["format","type","payload","output","onstart","onerror","outputButton"])elements[key]=`node-input-output-rule-${this.uniqueId}-`+key;return elements}get value(){let value={};switch(value.type=this.$elements.type.val(),value.format=this.$elements.format.val(),value.type){case"attribute":case"state":case"config":"deconz-input"===this.node.type&&(value.output=this.$elements.output.val()),["deconz-input","deconz-battery"].includes(this.node.type)&&(value.onstart=this.$elements.onstart.is(":checked")),["deconz-input","deconz-get"].includes(this.node.type)&&(value.payload=this.$elements.payload.multipleSelect("getSelects"));break;case"homekit":["deconz-input","deconz-battery"].includes(this.node.type)&&(value.onstart=this.$elements.onstart.is(":checked")),"deconz-input"===this.node.type&&(value.onerror=this.$elements.onerror.is(":checked")),["deconz-input","deconz-get"].includes(this.node.type)&&(value.payload=this.$elements.payload.multipleSelect("getSelects"))}return value}get defaultRule(){let rule={type:"state",payload:["__complete__"],format:"single"};return"deconz-input"===this.node.type&&(rule.output="always",rule.onstart=!0,rule.onerror=!0),"deconz-battery"===this.node.type&&(rule.onstart=!0),rule}async init(rule,index){this._index=index,rule=$.extend(!0,this.defaultRule,rule),await this.generatePayloadTypeField(this.container,rule.type),["deconz-input","deconz-get"].includes(this.node.type)&&await this.generatePayloadField(this.container),await this.generatePayloadFormatField(this.container,rule.format),"deconz-input"===this.node.type&&await this.generateOutputField(this.container,(void 0!==rule.output?rule:this.defaultRule).output),["deconz-input","deconz-battery"].includes(this.node.type)&&await this.generateOnStartField(this.container,(void 0!==rule.onstart?rule:this.defaultRule).onstart),"deconz-input"===this.node.type&&await this.generateOnErrorField(this.container,(void 0!==rule.onerror?rule:this.defaultRule).onerror),await super.init(),await this.listEditor.mainEditor.isInitialized(),await this.initPayloadList(rule.payload),await this.updateShowHide(rule.type),await this.connect()}async connect(){await super.connect(),this.$elements.type.on("change",()=>{var type=this.$elements.type.val();["attribute","state","config","homekit"].includes(type)&&this.updatePayloadList(),this.updateShowHide(type)}),this.$elements.outputButton.on("click",()=>{try{let nodes=RED.nodes.filterLinks({source:this.node,sourcePort:this.index}).map(l=>{var result=l.target.type;return""!==l.target.name?result+":"+l.target.name:void 0!==l.target._def.label?result+":"+l.target._def.label():result}),myNotification=RED.notify(`The output ${this.index+1} is sending message to ${nodes.length} nodes :<br>`+nodes.join("<br>"),{modal:!0,timeout:5e3,buttons:[{text:"okay",class:"primary",click:()=>myNotification.close()}]})}catch(e){this.sendError("This is using not documented API so can be broken at anytime.<br>Error while getting connected nodes: "+e.toString())}})}async updateShowHide(type){switch(type){case"attribute":case"state":case"config":this.$elements.payload.closest(".form-row").show(),this.$elements.output.closest(".form-row").show(),this.$elements.onstart.closest(".form-row").show(),this.$elements.onerror.closest(".form-row").hide();break;case"homekit":this.$elements.payload.closest(".form-row").show(),this.$elements.output.closest(".form-row").hide(),this.$elements.onstart.closest(".form-row").show(),this.$elements.onerror.closest(".form-row").show();break;case"scene_call":this.$elements.payload.closest(".form-row").hide(),this.$elements.output.closest(".form-row").hide(),this.$elements.onstart.closest(".form-row").hide(),this.$elements.onerror.closest(".form-row").hide()}}async updatePayloadList(){if(this.listEditor.mainEditor.serverNode){this.$elements.payload.multipleSelect("disable"),this.$elements.payload.children().remove();var queryType=this.listEditor.mainEditor.subEditor.query.type,devices=this.listEditor.mainEditor.subEditor.device.value,type=this.$elements.type.val();if(["attribute","state","config","homekit"].includes(type)){var i18n=this.NRCD+"/server:editor.inputs.outputs.payload";let html="";if("homekit"===type?html+='<option value="__auto__">'+RED._(i18n+".options.auto")+"</option>":(html+='<option value="__complete__">'+RED._(i18n+".options.complete")+"</option>",!0===this.options.enableEachState&&(html+='<option value="__each__">'+RED._(i18n+".options.each")+"</option>")),this.$elements.payload.html(html),"device"===queryType){var data=await $.getJSON(this.NRCD+`/${type}list`,{controllerID:this.listEditor.mainEditor.serverNode.id,devices:JSON.stringify(this.listEditor.mainEditor.subEditor.device.value)});for(const _type of"attribute"===type?["attribute","state","config"]:[type]){let label=this.getI18n(i18n+".group_label."+_type),groupHtml=(void 0===label&&(label=_type),$("<optgroup/>",{label:label}));for(const item of Object.keys(data.count[_type]).sort()){let sample=data.sample[_type][item];sample="string"==typeof sample?`"${sample}"`:Array.isArray(sample)?`[${sample.toString()}]`:null===sample||void 0===sample?"NULL":sample.toString();let label;var count=data.count[_type][item];label=count===devices.length?RED._(i18n+".item_list",{name:item,sample:sample}):RED._(i18n+".item_list_mix",{name:item,sample:sample,item_count:count,device_count:devices.length}),$("<option>"+label+"</option>").attr("value","attribute"===type&&"attribute"!==_type?_type+"."+item:item).appendTo(groupHtml)}$.isEmptyObject(data.count[_type])||groupHtml.appendTo(this.$elements.payload)}}this.$elements.payload.multipleSelect("refresh").multipleSelect("enable")}}}async initPayloadList(value){let list=this.$elements.payload;list.addClass("multiple-select"),list.multipleSelect({maxHeight:300,dropWidth:300,width:200,numberDisplayed:1,single:!1,selectAll:!1,container:".node-input-output-container-row",filter:!0,filterPlaceholder:RED._(this.NRCD+"/server:editor.inputs.device.device.filter"),placeholder:RED._(this.NRCD+"/server:editor.multiselect.none_selected"),onClick:view=>{if(view.selected)switch(view.value){case"__complete__":case"__each__":case"__auto__":list.multipleSelect("setSelects",[view.value]);break;default:list.multipleSelect("uncheck","__complete__"),list.multipleSelect("uncheck","__each__"),list.multipleSelect("uncheck","__auto__")}},onUncheckAll:()=>{list.multipleSelect("setSelects",["__complete__","__auto__"])},onOptgroupClick:view=>{view.selected&&(list.multipleSelect("uncheck","__complete__"),list.multipleSelect("uncheck","__each__"),list.multipleSelect("uncheck","__auto__"))}}),await this.updatePayloadList(),value&&list.multipleSelect("setSelects",value)}async generatePayloadTypeField(container,value){var type,enabled,i18n=this.NRCD+"/server:editor.inputs.outputs.type";let choices=[];for([type,enabled]of Object.entries(this.listEditor.options.type))enabled&&choices.push([type,i18n+".options."+type]);await this.generateSimpleListField(container,{id:this.elements.type,i18n:i18n,choices:choices,currentValue:value})}async generatePayloadField(container){var i18n=this.NRCD+"/server:editor.inputs.outputs.payload";await this.generateSimpleListField(container,{id:this.elements.payload,i18n:i18n})}async generatePayloadFormatField(container,value){var format,enabled,i18n=this.NRCD+"/server:editor.inputs.outputs.format";let choices=[];for([format,enabled]of Object.entries(this.listEditor.options.format))enabled&&choices.push([format,i18n+".options."+format]);await this.generateSimpleListField(container,{id:this.elements.format,i18n:i18n,choices:choices,currentValue:value})}async generateOutputField(container,value){var i18n=this.NRCD+"/server:editor.inputs.outputs.output";await this.generateSimpleListField(container,{id:this.elements.output,i18n:i18n,choices:[["always",i18n+".options.always"],["onchange",i18n+".options.onchange"],["onupdate",i18n+".options.onupdate"]],currentValue:value})}async generateOnStartField(container,value){var i18n=this.NRCD+"/server:editor.inputs.outputs.on_start";await this.generateCheckboxField(container,{id:this.elements.onstart,i18n:i18n,currentValue:value})}async generateOnErrorField(container,value){var i18n=this.NRCD+"/server:editor.inputs.outputs.on_error";await this.generateCheckboxField(container,{id:this.elements.onerror,i18n:i18n,currentValue:value})}}class DeconzOutputRuleListEditor extends DeconzListItemListEditor{get elements(){return{list:"node-input-output-container"}}get listType(){return"rule"}get buttons(){let buttons=[];var type_name,i18n=this.NRCD+"/server:editor.inputs.outputs.type";for(const[type,enabled]of Object.entries(this.options.type))enabled&&(type_name=this.getI18n(i18n+".options."+type),buttons.push({label:this.getI18n(i18n+".add_button","label",{type:type_name}),icon:this.getIcon(this.getI18n(i18n+".add_button","icon"),!0),title:this.getI18n(i18n+".add_button","title",{type:type_name}),click:()=>this.$elements.list.editableList("addItem",{type:type})}));return buttons}async init(mainEditor){await super.init(mainEditor),await this.initList(DeconzOutputRuleEditor,this.node.output_rules)}refresh(){for(const rule of Object.values(this.items))rule.updatePayloadList()}}class DeconzCommandEditor extends DeconzListItemEditor{constructor(node,listEditor,container,options={}){super(node,listEditor,container,options=$.extend({},options)),this.containers={}}get lightKeys(){return["bri","sat","hue","ct","xy"]}get argKeys(){return["on","alert","effect","colorloopspeed","open","stop","lift","tilt","scene_mode","group","scene","scene_name","target","command","payload","delay","transitiontime","retryonerror","aftererror"]}get elements(){let keys=this.argKeys;keys.push("typedomain"),keys.push("outputButton"),keys.push("scene_picker"),keys.push("scene_picker_refresh");for(const lightKey of this.lightKeys)keys.push(lightKey),keys.push(lightKey+"_direction");let elements={};for(const key of keys)elements[key]=`node-input-output-rule-${this.uniqueId}-`+key;return elements}set value(command){}get value(){let value={arg:{}};value.type=this.$elements.typedomain.typedInput("type"),value.domain=this.$elements.typedomain.typedInput("value");for(const key of this.argKeys)this.$elements[key].parent(".form-row").is(":visible")&&(value.arg[key]={type:this.$elements[key].typedInput("type"),value:this.$elements[key].typedInput("value")});for(const key of this.lightKeys)this.$elements[key].parent(".form-row").is(":visible")&&(value.arg[key]={direction:this.$elements[key+"_direction"].typedInput("type"),type:this.$elements[key].typedInput("type"),value:this.$elements[key].typedInput("value")});return value}get defaultCommand(){return{type:"deconz_state",domain:"lights",target:"state",arg:{on:{type:"keep"},bri:{direction:"set",type:"num"},sat:{direction:"set",type:"num"},hue:{direction:"set",type:"num"},ct:{direction:"set",type:"num"},xy:{direction:"set",type:"num"},alert:{type:"str"},effect:{type:"str"},colorloopspeed:{type:"num"},transitiontime:{type:"num"},command:{type:"str",value:"on"},payload:{type:"msg",value:"payload"},delay:{type:"num",value:2e3},target:{type:"state"},group:{type:"num"},scene_mode:{type:"single"},scene_call:{type:"num"},scene_name:{type:"str"},retryonerror:{type:"num",value:0},aftererror:{type:"continue"}}}}async init(command,index){this._index=index,command=$.extend(!0,this.defaultCommand,command),await this.generateTypeDomainField(this.container,{type:command.type,value:command.domain}),this.containers.light=$("<div>").appendTo(this.container),await this.generateLightOnField(this.containers.light,command.arg.on);for(const lightType of["bri","sat","hue","ct","xy"])await this.generateLightColorField(this.containers.light,lightType,command.arg[lightType]),"bri"===lightType&&await this.generateHR(this.containers.light);await this.generateHR(this.containers.light),await this.generateLightAlertField(this.containers.light,command.arg.alert),await this.generateLightEffectField(this.containers.light,command.arg.effect),await this.generateLightColorLoopSpeedField(this.containers.light,command.arg.colorloopspeed),this.containers.windows_cover=$("<div>").appendTo(this.container),await this.generateCoverOpenField(this.containers.windows_cover,command.arg.open),await this.generateCoverStopField(this.containers.windows_cover,command.arg.stop),await this.generateCoverLiftField(this.containers.windows_cover,command.arg.lift),await this.generateCoverTiltField(this.containers.windows_cover,command.arg.tilt),this.containers.scene_call=$("<div>").appendTo(this.container),await this.generateSceneModeField(this.containers.scene_call,command.arg.scene_mode),this.containers.scene_call_single=$("<div>").appendTo(this.containers.scene_call),await this.generateScenePickerField(this.containers.scene_call_single,command.arg.group+"."+command.arg.scene),await this.generateSceneGroupField(this.containers.scene_call_single,command.arg.group),await this.generateSceneSceneField(this.containers.scene_call_single,command.arg.scene),this.containers.scene_call_dynamic=$("<div>").appendTo(this.containers.scene_call),await this.generateSceneNameField(this.containers.scene_call_dynamic,command.arg.scene_name),this.containers.target=$("<div>").appendTo(this.container),await this.generateTargetField(this.containers.target,command.arg.target),this.containers.command=$("<div>").appendTo(this.container),await this.generateCommandField(this.containers.command,command.arg.command),this.containers.payload=$("<div>").appendTo(this.container),await this.generatePayloadField(this.containers.payload,command.arg.payload),this.containers.pause=$("<div>").appendTo(this.container),await this.generatePauseDelayField(this.containers.pause,command.arg.delay),this.containers.transition=$("<div>").appendTo(this.container),await this.generateHR(this.containers.transition),await this.generateCommonTransitionTimeField(this.containers.transition,command.arg.transitiontime),this.containers.common=$("<div>").appendTo(this.container),await this.generateHR(this.containers.common),await this.generateCommonOnErrorRetryField(this.containers.common,command.arg.retryonerror),await this.generateCommonOnErrorAfterField(this.containers.common,command.arg.aftererror),await super.init(),await this.listEditor.mainEditor.isInitialized(),await this.updateShowHide(command.type,command.domain),await this.connect()}async connect(){await super.connect(),this.$elements.typedomain.on("change",(event,type,value)=>{this.updateShowHide(type,value)}),this.$elements.outputButton.on("click",async()=>{try{if("device"!==this.listEditor.mainEditor.subEditor.query.type)this.sendError("Error : The run command can only work with device list.",5e3);else{var command=this.value,devices=this.listEditor.mainEditor.subEditor.device.value;if(0!==devices.length||("deconz_state"===command.type&&"scene_call"===command.domain||"custom"===command.type&&"scene_call"===command.arg.target.type))if("pause"===command.type)this.sendError("Error : Can't test pause command.",5e3);else{for(var[name,value]of Object.entries(command.arg))if(["msg","flow","global","jsonata"].includes(value.type))return void this.sendError(`Error : Cant run this command because the value "${name}" is type "${value.type}".`,5e3);let myNotification=RED.notify("Sending request...",{type:"info"});await $.post(this.NRCD+"/testCommand",{controllerID:this.listEditor.mainEditor.serverNode.id,device_list:devices,command:command,delay:this.listEditor.mainEditor.subEditor.specific.value.delay}).catch((t,u)=>{this.sendError(400===t.status&&t.responseText?t.responseText:u.toString())});myNotification.close(),myNotification=RED.notify("Ok",{timeout:1e3,type:"success"})}else this.sendError("Error : No device selected.",5e3)}}catch(e){let myNotification=RED.notify(e.toString(),{type:"error",buttons:[{class:"error",click:()=>myNotification.close()}]})}});const updateSceneGroupSelection=()=>{let value=this.$elements.scene_picker.multipleSelect("getSelects");var parts;1===value.length&&(this.$elements.group.off("change",updateScenePickerSelection),this.$elements.scene.off("change",updateScenePickerSelection),parts=value[0].split("."),this.$elements.group.typedInput("type","num"),this.$elements.group.typedInput("value",parts[0]),this.$elements.scene.typedInput("type","num"),this.$elements.scene.typedInput("value",parts[1]),this.$elements.group.on("change",updateScenePickerSelection),this.$elements.scene.on("change",updateScenePickerSelection))},updateScenePickerSelection=()=>{this.$elements.scene_picker.off("change",updateSceneGroupSelection),this.$elements.scene_picker.multipleSelect("setSelects","num"!==this.$elements.group.typedInput("type")||"num"!==this.$elements.group.typedInput("type")?[]:[this.$elements.group.typedInput("value")+"."+this.$elements.scene.typedInput("value")]),this.$elements.scene_picker.on("change",updateSceneGroupSelection)};this.$elements.scene_mode.on("change",()=>{var isSingle="single"===this.$elements.scene_mode.typedInput("type");this.containers.scene_call_single.toggle(isSingle),this.containers.scene_call_dynamic.toggle(!isSingle)}),this.$elements.scene_picker.on("change",updateSceneGroupSelection),this.$elements.group.on("change",updateScenePickerSelection),this.$elements.scene.on("change",updateScenePickerSelection),this.$elements.scene_picker_refresh.on("click",()=>this.updateSceneList()),this.$elements.target.on("change",(event,type,value)=>{this.containers.command.toggle("scene_call"!==type)})}async updateShowHide(type,domain){let containers=[];switch(type){case"deconz_state":switch(domain){case"lights":case"groups":containers.push("light"),containers.push("transition");break;case"covers":containers.push("windows_cover");break;case"scene_call":containers.push("scene_call"),await this.updateSceneList(),containers.push("scene_call_"+("single"===this.$elements.scene_mode.typedInput("type")?"single":"dynamic"))}containers.push("common");break;case"homekit":this.$elements.payload.typedInput("types",["msg","flow","global","json","jsonata"]),containers.push("payload"),containers.push("transition"),containers.push("common");break;case"custom":containers.push("target"),"scene_call"!==this.$elements.target.typedInput("type")&&containers.push("command"),this.$elements.payload.typedInput("types",["msg","flow","global","str","num","bool","json","jsonata","date"]),containers.push("payload"),containers.push("transition"),containers.push("common");break;case"pause":containers.push("pause")}for(var[key,value]of Object.entries(this.containers))value.toggle(containers.includes(key))}async updateSceneList(){this.$elements.scene_picker.multipleSelect("disable"),this.$elements.scene_picker.children().remove();let queryEditor=this.listEditor.mainEditor.subEditor.query;if(void 0!==queryEditor){let params=queryEditor.xhrParams;params.queryType="json",params.query=JSON.stringify({match:{device_type:"groups"}});var groups=await queryEditor.getItems({refresh:!0,keepOnlyMatched:!0},params);if(void 0!==groups&&void 0!==groups.LightGroup){for(const group of groups.LightGroup){let groupHtml=$("<optgroup/>",{label:group.id+" - "+group.name});if(group.scenes&&0<group.scenes.length){for(const scene of group.scenes)$(`<option>${scene.id} - ${scene.name}</option>`).attr("value",group.id+"."+scene.id).appendTo(groupHtml);groupHtml.appendTo(this.$elements.scene_picker)}}this.$elements.scene_picker.multipleSelect("refresh").multipleSelect("enable"),this.$elements.scene_picker.multipleSelect("setSelects","num"!==this.$elements.group.typedInput("type")||"num"!==this.$elements.group.typedInput("type")?[]:[this.$elements.group.typedInput("value")+"."+this.$elements.scene.typedInput("value")])}}}async generateTypeDomainField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type";await this.generateTypedInputField(container,{id:this.elements.typedomain,i18n:i18n,value:value,addDefaultTypes:!1,typedInput:{default:"deconz_state",types:[this.generateTypedInputType(i18n,"deconz_state",{subOptions:["lights","covers","groups","scene_call"]}),this.generateTypedInputType(i18n,"homekit",{hasValue:!1}),this.generateTypedInputType(i18n,"custom",{hasValue:!1}),this.generateTypedInputType(i18n,"pause",{hasValue:!1})]}})}async generateLightOnField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields.on";await this.generateTypedInputField(container,{id:this.elements.on,i18n:i18n,value:value,typedInput:{default:"keep",types:[this.generateTypedInputType(i18n,"keep",{hasValue:!1}),this.generateTypedInputType(i18n,"set",{subOptions:["true","false"]}),this.generateTypedInputType(i18n,"toggle",{hasValue:!1})]}})}async generateLightColorField(container,fieldName,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields";let fieldFormat=["num"],directionsFormat=[this.generateTypedInputType(i18n+".lightFields","set",{hasValue:!1})];switch(fieldName){case"bri":fieldFormat.push("str"),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","inc",{hasValue:!1})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","dec",{hasValue:!1})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","detect_from_value",{hasValue:!1}));break;case"ct":fieldFormat.push("str"),fieldFormat.push(this.generateTypedInputType(i18n+".ct","deconz",{subOptions:["cold","white","warm"]})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","inc",{hasValue:!1})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","dec",{hasValue:!1})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","detect_from_value",{hasValue:!1}));break;case"xy":fieldFormat=["json"]}await this.generateDoubleTypedInputField(container,{id:this.elements[fieldName+"_direction"],i18n:i18n+"."+fieldName,addDefaultTypes:!1,value:{type:value.direction},typedInput:{types:directionsFormat}},{id:this.elements[fieldName],value:{type:value.type,value:["xy"===fieldName&&void 0===value.value?"[]":value.value]},typedInput:{types:fieldFormat}})}async generateLightAlertField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields.alert";await this.generateTypedInputField(container,{id:this.elements.alert,i18n:i18n,value:value,typedInput:{types:["str",this.generateTypedInputType(i18n,"deconz",{subOptions:["none","select","lselect"]})]}})}async generateLightEffectField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields.effect";await this.generateTypedInputField(container,{id:this.elements.effect,i18n:i18n,value:value,typedInput:{types:["str",this.generateTypedInputType(i18n,"deconz",{subOptions:["none","colorloop"]})]}})}async generateLightColorLoopSpeedField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields.colorloopspeed";await this.generateTypedInputField(container,{id:this.elements.colorloopspeed,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateCoverOpenField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.covers.fields.open";await this.generateTypedInputField(container,{id:this.elements.open,i18n:i18n,value:value,typedInput:{types:[this.generateTypedInputType(i18n,"keep",{hasValue:!1}),this.generateTypedInputType(i18n,"set",{subOptions:["true","false"]}),this.generateTypedInputType(i18n,"toggle",{hasValue:!1})]}})}async generateCoverStopField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.covers.fields.stop";await this.generateTypedInputField(container,{id:this.elements.stop,i18n:i18n,value:value,typedInput:{types:[this.generateTypedInputType(i18n,"keep",{hasValue:!1}),this.generateTypedInputType(i18n,"set",{subOptions:["true","false"]})]}})}async generateCoverLiftField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.covers.fields.lift";await this.generateTypedInputField(container,{id:this.elements.lift,i18n:i18n,value:value,typedInput:{types:["num","str",this.generateTypedInputType(i18n,"stop",{hasValue:!1})]}})}async generateCoverTiltField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.covers.fields.tilt";await this.generateTypedInputField(container,{id:this.elements.tilt,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateSceneModeField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.mode";await this.generateTypedInputField(container,{id:this.elements.scene_mode,i18n:i18n,value:value,addDefaultTypes:!1,typedInput:{types:[this.generateTypedInputType(i18n,"single",{hasValue:!1}),this.generateTypedInputType(i18n,"dynamic",{hasValue:!1})]}})}async generateScenePickerField(container,value=0){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.picker";let list=await this.generateSimpleListField(container,{id:this.elements.scene_picker,i18n:i18n});list.addClass("multiple-select"),list.multipleSelect({maxHeight:300,dropWidth:300,width:200,numberDisplayed:1,single:!0,singleRadio:!0,hideOptgroupCheckboxes:!0,showClear:!0,selectAll:!1,filter:!0,filterPlaceholder:this.getI18n(i18n,"filter_place_holder"),placeholder:RED._(this.NRCD+"/server:editor.multiselect.none_selected"),container:".node-input-output-container-row"});container=$("<a/>",{id:this.elements.scene_picker_refresh,class:"red-ui-button",style:"margin-left:10px;"});this.createIconElement(this.getIcon("refresh"),container),list.closest(".form-row").append(container)}async generateSceneGroupField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.group";await this.generateTypedInputField(container,{id:this.elements.group,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateSceneSceneField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.scene";await this.generateTypedInputField(container,{id:this.elements.scene,i18n:i18n,value:value,typedInput:{types:["num","str",this.generateTypedInputType(i18n,"deconz",{subOptions:["next","prev"]})]}})}async generateSceneNameField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.scene_name";await this.generateTypedInputField(container,{id:this.elements.scene_name,i18n:i18n,value:value,typedInput:{types:["str","re"]}})}async generateTargetField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.common.fields.target";await this.generateTypedInputField(container,{id:this.elements.target,i18n:i18n,value:value,typedInput:{types:[this.generateTypedInputType(i18n,"attribute",{hasValue:!1}),this.generateTypedInputType(i18n,"state",{hasValue:!1}),this.generateTypedInputType(i18n,"config",{hasValue:!1}),this.generateTypedInputType(i18n,"scene_call",{hasValue:!1})]}})}async generateCommandField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.common.fields.command";await this.generateTypedInputField(container,{id:this.elements.command,i18n:i18n,value:value,typedInput:{types:["str",this.generateTypedInputType(i18n,"object",{hasValue:!1})]}})}async generatePayloadField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.common.fields.payload";await this.generateTypedInputField(container,{id:this.elements.payload,i18n:i18n,value:value,addDefaultTypes:!1,typedInput:{types:["msg","flow","global","str","num","bool","json","jsonata","date"]}})}async generatePauseDelayField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.pause.fields.delay";await this.generateTypedInputField(container,{id:this.elements.delay,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateCommonTransitionTimeField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.common.fields.transitiontime";await this.generateTypedInputField(container,{id:this.elements.transitiontime,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateCommonOnErrorRetryField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.common.fields.retryonerror";await this.generateTypedInputField(container,{id:this.elements.retryonerror,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateCommonOnErrorAfterField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.common.fields.aftererror";await this.generateTypedInputField(container,{id:this.elements.aftererror,i18n:i18n,value:value,typedInput:{types:[this.generateTypedInputType(i18n,"continue",{hasValue:!1}),this.generateTypedInputType(i18n,"stop",{hasValue:!1})]}})}}class DeconzCommandListEditor extends DeconzListItemListEditor{get elements(){return{list:"node-input-output-container"}}get listType(){return"command"}get buttons(){let buttons=[];var type_name,i18n=this.NRCD+"/server:editor.inputs.commands.type";for(const[type,enabled]of Object.entries(this.options.type))enabled&&(type_name=this.getI18n(i18n+".options."+type,"label"),buttons.push({label:this.getI18n(i18n+".add_button","label",{type:type_name}),icon:this.getIcon(this.getI18n(i18n+".add_button","icon"),!0),title:this.getI18n(i18n+".add_button","title",{type:type_name}),click:()=>this.$elements.list.editableList("addItem",{type:type})}));return buttons}async init(mainEditor){await super.init(mainEditor),await this.initList(DeconzCommandEditor,this.node.commands)}}
class DeconzEditor{constructor(node,options={}){this.node=node,this.options=options}get elements(){return{}}get NRCD(){return"node-red-contrib-deconz"}findElements(){this.$elements={},Object.keys(this.elements).forEach(k=>{this.$elements[k]=this.findElement(this.elements[k])})}findElement(identifier){return"#"!==identifier.charAt(0)&&"."!==identifier.charAt(0)&&(identifier="#"+identifier),$(identifier)}async init(){this.findElements()}async connect(){}sendError(msg,timeout=1e4){let myNotification=RED.notify(msg,{timeout:timeout,type:"error",buttons:[{text:"Ok",class:"primary",click:()=>myNotification.close()}]})}getIcon(icon,includeClass=!1){return"deconz"===icon?"icons/node-red-contrib-deconz/icon-color.png":"homekit"===icon?"icons/node-red-contrib-deconz/homekit-logo.png":RED.nodes.fontAwesome.getIconList().includes("fa-"+icon)?`${includeClass?"fa ":""}fa-`+icon:icon}createIconElement(icon,container,isLarge=!1){if("fa-"===icon.substr(0,3)){if(RED.nodes.fontAwesome.getIconUnicode(icon))return void $("<i/>").appendTo(container).addClass("fa "+icon+(isLarge?" fa-lg":""));icon=RED.settings.apiRootUrl+"icons/node-red/arrow-in.svg"}$("<div/>").appendTo(container).css("backgroundImage","url("+icon+")")}getI18n(prefix,suffix,data={}){let _path=prefix;suffix&&(_path+="."+suffix),data.defaultValue="_deconz_undefined_";prefix=RED._(_path,data);if("_deconz_undefined_"!==prefix)return prefix}async generateSimpleListField(container,options){var input=$("<select/>",{id:options.id});if(options.choices)for(var[key,value]of options.choices)input.append($("<option/>").attr("value",key).html(RED._(value)));var row=await this.generateInputWithLabel(input,options);return container.append(row),void 0!==options.currentValue&&input.val(options.currentValue),input}async generateTypedInput(container,options){var input=$("<input/>",{id:options.id,placeholder:RED._(options.placeholder)}),options=$("<input/>",{id:options.id+"_type",type:"hidden"});return input.append(options),input}async initTypedInput(input,options){options=$.extend({addDefaultTypes:!0,displayOnlyIcon:!1,value:{},width:"200px"},options);var typedInputOptions=$.extend({types:["msg","flow","global"]},options.typedInput);if(typedInputOptions.typeField=options.typeId,options.addDefaultTypes)for(var type of["msg","flow","global","jsonata"])typedInputOptions.types.includes(type)||typedInputOptions.types.push(type);if(options.displayOnlyIcon){let that=this;var valueLabel=function(a,b){let typeDefinition;for(const type of this.typeList)"object"==typeof type&&type.value===this.propertyType&&(typeDefinition=type);void 0!==typeDefinition&&void 0!==typeDefinition.icon&&(this.oldValue=this.input.val(),this.input.val(""),this.valueLabelContainer.hide(),that.createIconElement(typeDefinition.icon,this.selectLabel),this.selectTrigger.addClass("red-ui-typedInput-full-width"),this.selectLabel.show())};for(let type of typedInputOptions.types)"string"!=typeof type&&(type.hasValue=!0,type.valueLabel=valueLabel)}input.typedInput(typedInputOptions),void 0!==options.width&&input.typedInput("width",options.width),options.value&&(void 0!==options.value.type&&input.typedInput("type",options.value.type),void 0!==options.value.value&&input.typedInput("value",options.value.value))}async generateTypedInputField(container,options){var input=await this.generateTypedInput(container,{id:options.id,placeholder:this.getI18n(options.i18n,"placeholder")}),row=await this.generateInputWithLabel(input,options);return container.append(row),await this.initTypedInput(input,options),input}async generateDoubleTypedInputField(container,optionsFirst,optionsSecond){var inputFirst=await this.generateTypedInput(container,optionsFirst),row=await this.generateInputWithLabel(inputFirst,optionsFirst),inputSecond=await this.generateTypedInput(container,optionsSecond);row.append(inputSecond),container.append(row),optionsFirst.displayOnlyIcon=!0,optionsFirst.width="50px",optionsSecond.width="150px",await this.initTypedInput(inputFirst,optionsFirst),await this.initTypedInput(inputSecond,optionsSecond)}generateTypedInputType(i18n,name,data={}){if(data.value=name,void 0===data.label&&(data.label=this.getI18n(i18n,`options.${name}.label`,{})||name),!1!==data.icon&&void 0===data.icon&&(data.icon=this.getIcon(this.getI18n(i18n,`options.${name}.icon`))),data.icon&&"fa-"===data.icon.substr(0,3)&&(data.icon="fa "+data.icon),Array.isArray(data.subOptions)){Array.isArray(data.options)||(data.options=[]);for(const opt of data.subOptions)data.options.push(this.generateTypedInputType(i18n+".options."+name,"string"==typeof opt?opt:opt.name,{icon:!1}))}return data}async generateCheckboxField(container,options){var input=$("<input/>",{id:options.id,type:"checkbox",style:"display: table-cell; width: 14px;vertical-align: top;margin-right: 5px",checked:options.currentValue}),input=await this.generateInputWithLabel(input,options);input.append($("<span/>").html(RED._(options.descText)).css("display","table-cell")),container.append(input)}async generateInputWithLabel(input,options={}){var labelElement,row=$("<div/>",{class:"form-row",style:"padding:5px;margin:0;display:table;min-width:420px;"}),inputID=input.attr("id");return inputID&&((labelElement=$("<label/>")).attr("for",inputID),labelElement.attr("class","l-width"),labelElement.attr("style","display:table-cell;"),void 0===options.title&&(options.title=this.getI18n(options.i18n,"title")),options.title&&labelElement.attr("title",this.getI18n(options.i18n,"title")),void 0===options.icon&&(options.icon=this.getI18n(options.i18n,"icon")),options.icon&&(this.createIconElement(this.getIcon(options.icon),labelElement),labelElement.append("&nbsp;")),void 0===options.label&&(options.label=this.getI18n(options.i18n,"label")),options.label&&labelElement.append(`<span>${options.label}</span>`),row.append(labelElement)),input.css("display","table-cell"),row.append(input),row}async generateHR(container,topBottom="5px",leftRight="50px"){container.append(`<hr style="margin: ${topBottom} ${leftRight};">`)}async generateSeparator(container,label){container.append(`<div class="separator">${RED._(label)}</div>`)}static versionCompare(v1,v2,options={}){const lexicographical=options&&options.lexicographical;options=options&&options.zeroExtend;let v1parts=v1.split("."),v2parts=v2.split(".");function isValidPart(x){return(lexicographical?/^\d+[A-Za-z]*$/:/^\d+$/).test(x)}if(!v1parts.every(isValidPart)||!v2parts.every(isValidPart))return NaN;if(options){for(;v1parts.length<v2parts.length;)v1parts.push("0");for(;v2parts.length<v1parts.length;)v2parts.push("0")}lexicographical||(v1parts=v1parts.map(Number),v2parts=v2parts.map(Number));for(let i=0;i<v1parts.length;++i){if(v2parts.length===i)return 1;if(v1parts[i]!==v2parts[i])return v1parts[i]>v2parts[i]?1:-1}return v1parts.length!==v2parts.length?-1:0}}class DeconzMainEditor extends DeconzEditor{constructor(node,options={}){if(super(node,$.extend(!0,{have:{statustext:!0,query:!0,device:!0,output_rules:!1,commands:!1,specific:!1},device:{batteryFilter:!1},output_rules:{format:{single:!0,array:!1,sum:!1,average:!1,min:!1,max:!1},type:{attribute:!0,state:!0,config:!0,homekit:!1,scene_call:!1}},commands:{type:{deconz_state:!0,homekit:!0,custom:!0,pause:!0}},specific:{api:{},output:{},server:{}}},options)),this.subEditor={},this.initDone=!1,this.options.have.statustext&&(this.subEditor.statustext=new DeconzStatusTextEditor(this.node,this.options.statustext)),this.options.have.device&&(this.subEditor.device=new DeconzDeviceEditor(this.node,this.options.device)),this.options.have.query&&(this.subEditor.query=new DeconzQueryEditor(this.node,this.options.query)),this.options.have.specific)switch(this.node.type){case"deconz-api":this.subEditor.specific=new DeconzSpecificApiEditor(this.node,this.options.specific.api);break;case"deconz-output":this.subEditor.specific=new DeconzSpecificOutputEditor(this.node,this.options.specific.output);break;case"deconz-server":this.subEditor.specific=new DeconzSpecificServerEditor(this.node,this.options.specific.server)}this.options.have.output_rules&&(this.subEditor.output_rules=new DeconzOutputRuleListEditor(this.node,this.options.output_rules)),this.options.have.commands&&(this.subEditor.commands=new DeconzCommandListEditor(this.node,this.options.commands))}get elements(){return{tipBox:"node-input-tip-box",server:"node-input-server"}}async configurationMigration(){if(!((this.node.config_version||0)>=this.node._def.defaults.config_version.value)){var config={};for(const key of Object.keys(this.node._def.defaults))config[key]=this.node[key];var data={id:this.node.id,type:this.node.type,config:JSON.stringify(config)};let errorMsg="Error while migrating the configuration of the node from version "+(this.node.config_version||0)+" to version "+this.node._def.defaults.config_version.value+".";data=await $.getJSON(this.NRCD+"/configurationMigration",data).catch((t,u)=>{this.$elements.tipBox.append(`<div class="form-tips form-warning"><p>Migration errors:</p><p>${errorMsg}</p></div>`)});if(data&&!data.notNeeded){if(data.new)for(var[key,value]of Object.entries(data.new))this.node[key]=value;if(data.delete&&Array.isArray(data.delete))for(const key of data.delete)delete this.node[key];var mapI18N=msg=>"node-red-contrib-deconz"===msg.substr(0,23)?RED._(msg):msg;data.errors&&Array.isArray(data.errors)&&0<data.errors.length&&this.$elements.tipBox.append('<div class="form-tips form-warning"><p>Migration errors:</p><ul>'+`<li>${data.errors.map(mapI18N).join("</li><li>")}</li>`+"</ul></div>"),data.info&&Array.isArray(data.info)&&0<data.info.length&&this.$elements.tipBox.append('<div class="form-tips"><p>Migration info:</p><ul>'+`<li>${data.info.map(mapI18N).join("</li><li>")}</li>`+"</ul></div>")}}}async init(){await new Promise(resolve=>setTimeout(resolve,100)),await super.init(),await this.configurationMigration(),this.initPromises=[];for(const editor of Object.values(this.subEditor))this.initPromises.push(editor.init(this));await Promise.all(this.initPromises),this.initDone=!0,delete this.initPromises;var connectPromises=[];for(const editor of Object.values(this.subEditor))connectPromises.push(editor.connect());await Promise.all(connectPromises)}get serverNode(){return"deconz-server"===this.node.type?this.node:RED.nodes.node(this.$elements.server.val())}async isInitialized(){this.initDone||await Promise.all(this.initPromises)}async updateQueryDeviceDisplay(options){var type=this.subEditor.query.$elements.select.typedInput("type");switch(type){case"device":await this.subEditor.device.updateList(options);break;case"json":case"jsonata":this.subEditor.query.$elements.select.typedInput("validate")&&await this.subEditor.query.updateList(options)}await this.subEditor.device.display("device"===type),await this.subEditor.query.display("device"!==type)}oneditsave(){var newRules;this.options.have.output_rules&&(newRules=this.subEditor.output_rules.value,this.node.outputs=newRules.length,this.node.output_rules=newRules),this.options.have.commands&&(this.node.commands=this.subEditor.commands.value),this.options.have.specific&&(this.node.specific=this.subEditor.specific.value)}}class DeconzStatusTextEditor extends DeconzEditor{constructor(node,options={}){super(node,$.extend({allowedTypes:["msg","jsonata"]},options))}get elements(){return{statustext:"node-input-statustext"}}async init(mainEditor){await super.init(),this.mainEditor=mainEditor,this.initTypedInput()}initTypedInput(){var options=[];this.mainEditor.options.have.statustext&&options.push({value:"auto",label:RED._(this.NRCD+"/server:editor.inputs.statustext.options.auto"),icon:`icons/${this.NRCD}/icon-color.png`,hasValue:!1}),this.$elements.statustext.typedInput({type:"auto",types:options.concat(this.options.allowedTypes),typeField:`#${this.elements.statustext}_type`})}}class DeconzDeviceListEditor extends DeconzEditor{constructor(node,options={}){super(node,options)}get xhrURL(){return this.NRCD+"/itemlist"}get xhrParams(){return{controllerID:this.mainEditor.serverNode.id,forceRefresh:this.options.refresh}}async display(display=!0){if(this.$elements.showHide)return display?this.$elements.showHide.show():this.$elements.showHide.hide(),this.$elements.showHide.promise()}async getItems(options,xhrParams){xhrParams.forceRefresh=options.refresh;xhrParams=await $.getJSON(this.xhrURL,xhrParams).catch((t,u)=>{this.sendError(400===t.status&&t.responseText?t.responseText:u.toString())});if(!xhrParams||!xhrParams.error_message)return this.formatItemList(xhrParams.items,options.keepOnlyMatched);console.warn(xhrParams.error_message),RED.notify("Warning : "+xhrParams.error_message,{type:"warning",timeout:1e4})}async updateList(options){var list,params;options=$.extend({refresh:!0},options),this.mainEditor.serverNode&&(list=this.$elements.list,params=this.xhrParams,!0===this.options.batteryFilter&&(options.keepOnlyMatched=!0,params.query=JSON.stringify({type:"match",match:{"config.battery":{type:"complex",operator:"!==",value:void 0}}})),options=await this.getItems(options,params),list.children().remove(),options&&this.generateHtmlItemList(options,this.$elements.list),list.multipleSelect("refresh"),options&&list.multipleSelect("enable"))}formatItemList(items,keepOnlyMatched=!1){let itemList={};var injectItems=(part,matched)=>{part.forEach(item=>{var device_type=item.type;void 0===itemList[device_type]&&(itemList[device_type]=[]),item.query_match=matched,itemList[device_type].push(item)})};return injectItems(items.matched,!0),!1===keepOnlyMatched&&injectItems(items.rejected,!1),itemList}generateHtmlItemList(items,htmlContainer){var group_key,item_list,queryMode=this.constructor===DeconzQueryEditor;for([group_key,item_list]of Object.entries(items).sort((a,b)=>{a=a[0].toLowerCase(),b=b[0].toLowerCase();return a<b?-1:b<a?1:0})){var groupHtml=$("<optgroup/>").attr("label",group_key);for(const item of item_list.sort((a,b)=>{a=a.name.toLowerCase(),b=b.name.toLowerCase();return a<b?-1:b<a?1:0})){let label=item.name;"groups"===item.device_type&&(label+=" (lights: "+item.lights.length,item.scenes.length&&(label+=", scenes: "+item.scenes.length),label+=")");var opt=$("<option>"+label+"</option>").attr("value",item.device_path);queryMode&&item.query_match&&opt.attr("selected",""),opt.appendTo(groupHtml)}groupHtml.appendTo(htmlContainer)}}}class DeconzQueryEditor extends DeconzDeviceListEditor{constructor(node,options={}){super(node,$.extend({allowedTypes:["json","jsonata"]},options))}get elements(){return{select:"node-input-query",list:"node-input-query_result",showHide:".deconz-query-selector",refreshButton:"#force-refresh-query-result"}}get type(){return this.$elements.select.typedInput("type")}set type(val){this.$elements.list.typedInput("type",val)}get value(){return this.$elements.select.typedInput("value")}set value(val){this.$elements.list.typedInput("value",val)}get xhrParams(){var params=super.xhrParams;return params.query=this.value,params.queryType=this.type,params.nodeID=this.node.id,params}async init(mainEditor){await super.init(),this.mainEditor=mainEditor,this.initTypedInput(),this.$elements.list.multipleSelect({maxHeight:300,dropWidth:320,width:320,single:!1,selectAll:!1,filter:!0,filterPlaceholder:RED._(this.NRCD+"/server:editor.inputs.device.device.filter"),placeholder:RED._(this.NRCD+"/server:editor.multiselect.none_selected"),numberDisplayed:1,disableIfEmpty:!0,showClear:!1,hideOptgroupCheckboxes:!0,filterGroup:!0,onClick:view=>{this.$elements.list.multipleSelect(view.selected?"uncheck":"check",view.value)}}),await this.mainEditor.updateQueryDeviceDisplay({useSavedData:!0})}initTypedInput(){var options=[];this.mainEditor.options.have.device&&options.push({value:"device",label:RED._(this.NRCD+"/server:editor.inputs.device.query.options.device"),icon:`icons/${this.NRCD}/icon-color.png`,hasValue:!1}),this.$elements.select.typedInput({type:"text",types:options.concat(this.options.allowedTypes),typeField:"#node-input-search_type"})}async connect(){await super.connect(),this.$elements.select.on("change",()=>{this.mainEditor.updateQueryDeviceDisplay({useSavedData:!0}),this.mainEditor.options.have.output_rules&&this.mainEditor.subEditor.output_rules.refresh()}),this.$elements.refreshButton.on("click",()=>{this.updateList(),this.mainEditor.options.have.output_rules&&this.mainEditor.subEditor.output_rules.refresh()})}}class DeconzDeviceEditor extends DeconzDeviceListEditor{constructor(node,options={}){super(node,$.extend({batteryFilter:!1},options))}get elements(){return{list:"node-input-device_list",showHide:".deconz-device-selector",refreshButton:"#force-refresh"}}get value(){return this.$elements.list.multipleSelect("getSelects")}set value(val){this.$elements.list.multipleSelect("setSelects",val)}async init(mainEditor){await super.init(),this.mainEditor=mainEditor,this.$elements.list.multipleSelect({maxHeight:300,dropWidth:320,width:320,single:"multiple"!==this.$elements.list.attr("multiple"),filter:!0,filterPlaceholder:RED._(this.NRCD+"/server:editor.inputs.device.device.filter"),placeholder:RED._(this.NRCD+"/server:editor.multiselect.none_selected"),showClear:!0})}async connect(){await super.connect();var refreshDeviceList=()=>{this.updateList($.extend(this.options,{useSelectedData:!0})),this.mainEditor.options.have.output_rules&&this.mainEditor.subEditor.output_rules.refresh()};this.mainEditor.$elements.server.on("change",refreshDeviceList),this.$elements.refreshButton.on("click",refreshDeviceList),this.mainEditor.options.have.output_rules&&this.$elements.list.on("change",()=>{this.mainEditor.subEditor.output_rules.refresh()})}async updateList(options){let itemsSelected;(options=$.extend({useSavedData:!1,useSelectedData:!1},options)).useSelectedData&&(itemsSelected=this.$elements.list.multipleSelect("getSelects")),await super.updateList(options),options.useSavedData&&Array.isArray(this.node.device_list)?this.$elements.list.multipleSelect("setSelects",this.node.device_list):options.useSelectedData&&Array.isArray(itemsSelected)&&this.$elements.list.multipleSelect("setSelects",itemsSelected)}}class DeconzSpecificApiEditor extends DeconzEditor{constructor(node,options={}){super(node,$.extend({},options))}get elements(){return{container:"deconz-api-form",name:"node-config-input-name",method:"node-config-input-method",endpoint:"node-config-input-endpoint",payload:"node-config-input-payload"}}get default(){return{name:"",method:{type:"GET"},endpoint:{type:"str",value:"/"},payload:{type:"json",value:"{}"}}}async init(){this.node.specific=$.extend(!0,this.default.specific,this.node.specific);var container=this.findElement(this.elements.container);await this.generateMethodField(container,this.node.specific.method),await this.generateEndpointField(container,this.node.specific.endpoint),await this.generatePayloadField(container,this.node.specific.payload),await super.init()}async connect(){await super.connect()}get value(){return{method:{type:this.$elements.method.typedInput("type"),value:this.$elements.method.typedInput("value")},endpoint:{type:this.$elements.endpoint.typedInput("type"),value:this.$elements.endpoint.typedInput("value")},payload:{type:this.$elements.payload.typedInput("type"),value:this.$elements.payload.typedInput("value")}}}set value(newValues){this.$elements.name.val(newValues.name);for(var field of["method","endpoint","payload"])this.$elements[field].typedInput("type",newValues[field].type),this.$elements[field].typedInput("value",newValues[field].value);for(const element of Object.values(this.$elements))element.trigger("change")}async generateMethodField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.api.method";await this.generateTypedInputField(container,{id:this.elements.method,i18n:i18n,value:value,width:"250px",typedInput:{types:[this.generateTypedInputType(i18n,"GET",{hasValue:!1}),this.generateTypedInputType(i18n,"POST",{hasValue:!1}),this.generateTypedInputType(i18n,"PUT",{hasValue:!1}),this.generateTypedInputType(i18n,"DELETE",{hasValue:!1})]}})}async generateEndpointField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.api.endpoint";await this.generateTypedInputField(container,{id:this.elements.endpoint,i18n:i18n,value:value,width:"250px",typedInput:{types:["str"]}})}async generatePayloadField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.api.payload";await this.generateTypedInputField(container,{id:this.elements.payload,i18n:i18n,value:value,width:"250px",typedInput:{types:["json","jsonata"]}})}}class DeconzSpecificOutputEditor extends DeconzEditor{constructor(node,options={}){super(node,$.extend({},options))}get elements(){return{container:"specific-container",delay:"node-input-delay",result:"node-input-result"}}get default(){return{delay:{type:"num",value:50},result:{type:"at_end"}}}async init(){this.node.specific=$.extend(!0,this.default,this.node.specific);var container=this.findElement(this.elements.container);await this.generateSeparator(container,this.NRCD+"/server:editor.inputs.separator.specific"),await this.generateDelayField(container,this.node.specific.delay),await this.generateResultField(container,this.node.specific.result),await super.init()}async generateDelayField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.output.delay";await this.generateTypedInputField(container,{id:this.elements.delay,i18n:i18n,value:value,width:"250px",typedInput:{types:["num"]}})}async generateResultField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.specific.output.result";await this.generateTypedInputField(container,{id:this.elements.result,i18n:i18n,value:value,width:"250px",typedInput:{types:[this.generateTypedInputType(i18n,"never",{hasValue:!1}),this.generateTypedInputType(i18n,"after_command",{hasValue:!1}),this.generateTypedInputType(i18n,"at_end",{hasValue:!1})]}})}async connect(){await super.connect()}get value(){return{delay:{type:this.$elements.delay.typedInput("type"),value:this.$elements.delay.typedInput("value")},result:{type:this.$elements.result.typedInput("type"),value:this.$elements.result.typedInput("value")}}}}class DeconzSpecificServerEditor extends DeconzEditor{constructor(node,options={}){super(node,$.extend({},options))}get elements(){return{name:"node-config-input-name",ip:"node-config-input-ip",port:"node-config-input-port",apikey:"node-config-input-secured_apikey",ws_port:"node-config-input-ws_port",secure:"node-config-input-secure",polling:"node-config-input-polling",getSettingsButton:"node-contrib-deconz-get-settings"}}get default(){return{name:"",ip:"",port:"",apikey:"",ws_port:"",secure:!1,polling:15}}get xhrURL(){return this.NRCD+"/serverAutoconfig"}async init(){this.node.specific=$.extend(!0,this.default,this.node.specific),await super.init(),this.node.migration_secured_apikey&&(this.$elements.apikey.val(this.node.migration_secured_apikey),this.$elements.apikey.trigger("change"))}async connect(){await super.connect(),this.$elements.getSettingsButton.on("click",()=>this.discoverParams())}async discoverParams(overrideSettings){void 0===overrideSettings&&(overrideSettings={});let myNotification,stop=!1,closeNotification=()=>{myNotification&&"function"==typeof myNotification.close&&myNotification.close(),stop=!0};myNotification=RED.notify("<p>Trying to find the server settings, please wait...<br>This can take up to 15 seconds.</p>",{modal:!0,fixed:!0,type:"info",buttons:[{text:"Cancel",class:"primary",click:closeNotification}]});try{var params=Object.assign({},this.value,overrideSettings);void 0===params.discoverParam&&(params.discoverParam={}),params.discoverParam.devicetype="Node-Red Deconz Plugin"+(this.node?" id:"+this.node.id:"");let request=await $.getJSON(this.xhrURL,{config:JSON.stringify(params)}).catch((t,u)=>{this.sendError(400===t.status&&t.responseText?t.responseText:u.toString())});if(!stop)if(request.error){let html=`<p>Error ${request.error.code}: ${request.error.description}</p>`;var buttons=[{text:"Cancel",click:closeNotification}];switch(request.error.code){case"GATEWAY_CHOICE":html+="<p>There is multiple Deconz device in you network, please select the one you want to configure.</p>";let node=this;for(const[index,gateway]of request.error.gateway_list.entries())buttons.push({text:`#${index+1}: `+gateway.name,id:"node-red-contrib-deconz-gateway-id-"+index,class:"primary",click:()=>{return gateway_id=gateway.bridge_id,closeNotification(),gateway_id&&(request.currentSettings.discoverParam.targetGatewayID=gateway_id),void node.discoverParams(request.currentSettings);var gateway_id}});buttons.push(buttons.shift());break;case"DECONZ_ERROR":101===request.error.type&&(buttons.unshift({text:"I pressed the link button",class:"primary",click:()=>{closeNotification(),this.discoverParams(request.currentSettings)}}),html=(html+="<p>The reason why the request failed is that the gateway was not unlocked. This mechanism is needed to prevent anybody from access to the gateway without being permitted to do so.</p><ul><li>In a new browser tab open the <a href='http://phoscon.de/pwa/' target='_blank'>Phoscon App</a></li><li>Click on Menu -> Settings -> Gateway</li><li>Click on \"Advanced\" button</li><li>Click on the \"Authenticate app\" button</li></ul>")+`<p>Within 60 seconds after unlocking the gateway, click on the button "${buttons[0].text}".</p>`);break;default:buttons[buttons.length-1].text="Cancel"}html+=`<p>Logs:</p><pre>${request.log.join("\n")}</pre>`,closeNotification(),myNotification=RED.notify(html,{modal:!0,fixed:!0,type:"error",buttons:buttons})}else request.success?(closeNotification(),myNotification=RED.notify("<p>Settings fetched successfully !</p>",{modal:!1,fixed:!1,type:"success"}),this.value=request.currentSettings):(closeNotification(),myNotification=RED.notify(`<p>Unknown error : ${JSON.stringify(request)}</p>`,{modal:!0,fixed:!0,type:"error",buttons:[{text:"Ok",class:"primary",click:closeNotification}]}))}catch(error){closeNotification(),myNotification=RED.notify(`<p>Error while processing request: ${error.toString()}</p>`,{type:"error"})}}get value(){return{name:this.$elements.name.val(),ip:this.$elements.ip.val(),port:this.$elements.port.val(),apikey:this.$elements.apikey.val(),ws_port:this.$elements.ws_port.val(),secure:this.$elements.secure.prop("checked"),polling:this.$elements.polling.val()}}set value(newValues){this.$elements.name.val(newValues.name),this.$elements.ip.val(newValues.ip),this.$elements.port.val(newValues.port),this.$elements.apikey.val(newValues.apikey),this.$elements.ws_port.val(newValues.ws_port),this.$elements.secure.prop("checked",newValues.secure),this.$elements.polling.val(newValues.polling);for(const element of Object.values(this.$elements))element.trigger("change")}}class DeconzListItemEditor extends DeconzEditor{constructor(node,listEditor,container,options={}){super(node,options),this.listEditor=listEditor,container.uniqueId(),this.uniqueId=container.attr("id"),this.container=container}set index(value){void 0!==value&&this.$elements&&this.$elements.outputButton&&this.$elements.outputButton.find(".node-input-rule-index").html(value+1),this._index=value}get index(){return this._index}async init(){await this.generateOutputButton(this.container.children().first()),await super.init()}async generateOutputButton(container){$("<a/>",{id:this.elements.outputButton,class:"red-ui-button top-right-badge",title:RED._(this.options.button_title)}).append(`&nbsp;&#8594;&nbsp;<span class="node-input-rule-index">${this.index+1}</span>&nbsp;`).appendTo(container)}}class DeconzListItemListEditor extends DeconzEditor{constructor(node,options={}){super(node,options),this.items={}}get listType(){return"item"}get buttons(){return[]}async init(mainEditor){await super.init(),this.mainEditor=mainEditor}async initList(itemEditorClass,items=[]){var buttons=this.buttons,addButton=0===buttons.length||DeconzEditor.versionCompare(RED.settings.version,"1.3.0")<0;this.$elements.list.editableList({sortable:!0,removable:!0,height:"auto",addButton:addButton,buttons:buttons,addItem:(row,index,item)=>{row=new itemEditorClass(this.node,this,row,this.options);item.uniqueId=row.uniqueId,(this.items[item.uniqueId]=row).init(item,index)},removeItem:item=>{if(!item.uniqueId||!this.items[item.uniqueId])throw new Error(`Error while removing the ${this.listType}, the ${this.listType} ${item.uniqueId} does not exist.`);var deletedIndex=this.items[item.uniqueId].index;delete this.items[item.uniqueId];for(const item of Object.values(this.items))item.index>deletedIndex&&item.index--},sortItems:items=>{items.each((index,item)=>{if(!this.items[item.attr("id")])throw new Error(`Error while moving the ${this.listType}, the ${this.listType} ${index+1} does not exist.`);this.items[item.attr("id")].index=index})}}),0<items.length&&this.$elements.list.editableList("addItems",items)}get value(){var result=[];for(const rule of Object.values(this.items).sort((a,b)=>a.index-b.index))result.push(rule.value);return result}refresh(){}}class DeconzOutputRuleEditor extends DeconzListItemEditor{constructor(node,listEditor,container,options={}){super(node,listEditor,container,options=$.extend({enableEachState:!0},options))}get elements(){var elements={};for(const key of["format","type","payload","output","onstart","onerror","outputButton"])elements[key]=`node-input-output-rule-${this.uniqueId}-`+key;return elements}get value(){var value={};switch(value.type=this.$elements.type.val(),value.format=this.$elements.format.val(),value.type){case"attribute":case"state":case"config":"deconz-input"===this.node.type&&(value.output=this.$elements.output.val()),["deconz-input","deconz-battery"].includes(this.node.type)&&(value.onstart=this.$elements.onstart.is(":checked")),["deconz-input","deconz-get"].includes(this.node.type)&&(value.payload=this.$elements.payload.multipleSelect("getSelects"));break;case"homekit":["deconz-input","deconz-battery"].includes(this.node.type)&&(value.onstart=this.$elements.onstart.is(":checked")),"deconz-input"===this.node.type&&(value.onerror=this.$elements.onerror.is(":checked")),["deconz-input","deconz-get"].includes(this.node.type)&&(value.payload=this.$elements.payload.multipleSelect("getSelects"))}return value}get defaultRule(){var rule={type:"state",payload:["__complete__"],format:"single"};return"deconz-input"===this.node.type&&(rule.output="always",rule.onstart=!0,rule.onerror=!0),"deconz-battery"===this.node.type&&(rule.onstart=!0),rule}async init(rule,index){this._index=index,rule=$.extend(!0,this.defaultRule,rule),await this.generatePayloadTypeField(this.container,rule.type),["deconz-input","deconz-get"].includes(this.node.type)&&await this.generatePayloadField(this.container),await this.generatePayloadFormatField(this.container,rule.format),"deconz-input"===this.node.type&&await this.generateOutputField(this.container,(void 0!==rule.output?rule:this.defaultRule).output),["deconz-input","deconz-battery"].includes(this.node.type)&&await this.generateOnStartField(this.container,(void 0!==rule.onstart?rule:this.defaultRule).onstart),"deconz-input"===this.node.type&&await this.generateOnErrorField(this.container,(void 0!==rule.onerror?rule:this.defaultRule).onerror),await super.init(),await this.listEditor.mainEditor.isInitialized(),await this.initPayloadList(rule.payload),await this.updateShowHide(rule.type),await this.connect()}async connect(){await super.connect(),this.$elements.type.on("change",()=>{var type=this.$elements.type.val();["attribute","state","config","homekit"].includes(type)&&this.updatePayloadList(),this.updateShowHide(type)}),this.$elements.outputButton.on("click",()=>{try{var nodes=RED.nodes.filterLinks({source:this.node,sourcePort:this.index}).map(l=>{var result=l.target.type;return""!==l.target.name?result+":"+l.target.name:void 0!==l.target._def.label?result+":"+l.target._def.label():result});let myNotification=RED.notify(`The output ${this.index+1} is sending message to ${nodes.length} nodes :<br>`+nodes.join("<br>"),{modal:!0,timeout:5e3,buttons:[{text:"okay",class:"primary",click:()=>myNotification.close()}]})}catch(e){this.sendError("This is using not documented API so can be broken at anytime.<br>Error while getting connected nodes: "+e.toString())}})}async updateShowHide(type){switch(type){case"attribute":case"state":case"config":this.$elements.payload.closest(".form-row").show(),this.$elements.output.closest(".form-row").show(),this.$elements.onstart.closest(".form-row").show(),this.$elements.onerror.closest(".form-row").hide();break;case"homekit":this.$elements.payload.closest(".form-row").show(),this.$elements.output.closest(".form-row").hide(),this.$elements.onstart.closest(".form-row").show(),this.$elements.onerror.closest(".form-row").show();break;case"scene_call":this.$elements.payload.closest(".form-row").hide(),this.$elements.output.closest(".form-row").hide(),this.$elements.onstart.closest(".form-row").hide(),this.$elements.onerror.closest(".form-row").hide()}}async updatePayloadList(){if(this.listEditor.mainEditor.serverNode){this.$elements.payload.multipleSelect("disable"),this.$elements.payload.children().remove();var queryType=this.listEditor.mainEditor.subEditor.query.type,devices=this.listEditor.mainEditor.subEditor.device.value,type=this.$elements.type.val();if(["attribute","state","config","homekit"].includes(type)){var i18n=this.NRCD+"/server:editor.inputs.outputs.payload";let html="";if("homekit"===type?html+='<option value="__auto__">'+RED._(i18n+".options.auto")+"</option>":(html+='<option value="__complete__">'+RED._(i18n+".options.complete")+"</option>",!0===this.options.enableEachState&&(html+='<option value="__each__">'+RED._(i18n+".options.each")+"</option>")),this.$elements.payload.html(html),"device"===queryType){var data=await $.getJSON(this.NRCD+`/${type}list`,{controllerID:this.listEditor.mainEditor.serverNode.id,devices:JSON.stringify(this.listEditor.mainEditor.subEditor.device.value)});for(const _type of"attribute"===type?["attribute","state","config"]:[type]){let label=this.getI18n(i18n+".group_label."+_type);void 0===label&&(label=_type);var groupHtml=$("<optgroup/>",{label:label});for(const item of Object.keys(data.count[_type]).sort()){let sample=data.sample[_type][item];sample="string"==typeof sample?`"${sample}"`:Array.isArray(sample)?`[${sample.toString()}]`:null===sample||void 0===sample?"NULL":sample.toString();let label;var count=data.count[_type][item];label=count===devices.length?RED._(i18n+".item_list",{name:item,sample:sample}):RED._(i18n+".item_list_mix",{name:item,sample:sample,item_count:count,device_count:devices.length}),$("<option>"+label+"</option>").attr("value","attribute"===type&&"attribute"!==_type?_type+"."+item:item).appendTo(groupHtml)}$.isEmptyObject(data.count[_type])||groupHtml.appendTo(this.$elements.payload)}}this.$elements.payload.multipleSelect("refresh").multipleSelect("enable")}}}async initPayloadList(value){let list=this.$elements.payload;list.addClass("multiple-select"),list.multipleSelect({maxHeight:300,dropWidth:300,width:200,numberDisplayed:1,single:!1,selectAll:!1,container:".node-input-output-container-row",filter:!0,filterPlaceholder:RED._(this.NRCD+"/server:editor.inputs.device.device.filter"),placeholder:RED._(this.NRCD+"/server:editor.multiselect.none_selected"),onClick:view=>{if(view.selected)switch(view.value){case"__complete__":case"__each__":case"__auto__":list.multipleSelect("setSelects",[view.value]);break;default:list.multipleSelect("uncheck","__complete__"),list.multipleSelect("uncheck","__each__"),list.multipleSelect("uncheck","__auto__")}},onUncheckAll:()=>{list.multipleSelect("setSelects",["__complete__","__auto__"])},onOptgroupClick:view=>{view.selected&&(list.multipleSelect("uncheck","__complete__"),list.multipleSelect("uncheck","__each__"),list.multipleSelect("uncheck","__auto__"))}}),await this.updatePayloadList(),value&&list.multipleSelect("setSelects",value)}async generatePayloadTypeField(container,value){var type,enabled,i18n=this.NRCD+"/server:editor.inputs.outputs.type",choices=[];for([type,enabled]of Object.entries(this.listEditor.options.type))enabled&&choices.push([type,i18n+".options."+type]);await this.generateSimpleListField(container,{id:this.elements.type,i18n:i18n,choices:choices,currentValue:value})}async generatePayloadField(container){var i18n=this.NRCD+"/server:editor.inputs.outputs.payload";await this.generateSimpleListField(container,{id:this.elements.payload,i18n:i18n})}async generatePayloadFormatField(container,value){var format,enabled,i18n=this.NRCD+"/server:editor.inputs.outputs.format",choices=[];for([format,enabled]of Object.entries(this.listEditor.options.format))enabled&&choices.push([format,i18n+".options."+format]);await this.generateSimpleListField(container,{id:this.elements.format,i18n:i18n,choices:choices,currentValue:value})}async generateOutputField(container,value){var i18n=this.NRCD+"/server:editor.inputs.outputs.output";await this.generateSimpleListField(container,{id:this.elements.output,i18n:i18n,choices:[["always",i18n+".options.always"],["onchange",i18n+".options.onchange"],["onupdate",i18n+".options.onupdate"]],currentValue:value})}async generateOnStartField(container,value){var i18n=this.NRCD+"/server:editor.inputs.outputs.on_start";await this.generateCheckboxField(container,{id:this.elements.onstart,i18n:i18n,currentValue:value})}async generateOnErrorField(container,value){var i18n=this.NRCD+"/server:editor.inputs.outputs.on_error";await this.generateCheckboxField(container,{id:this.elements.onerror,i18n:i18n,currentValue:value})}}class DeconzOutputRuleListEditor extends DeconzListItemListEditor{get elements(){return{list:"node-input-output-container"}}get listType(){return"rule"}get buttons(){var type_name,buttons=[],i18n=this.NRCD+"/server:editor.inputs.outputs.type";for(const[type,enabled]of Object.entries(this.options.type))enabled&&(type_name=this.getI18n(i18n+".options."+type),buttons.push({label:this.getI18n(i18n+".add_button","label",{type:type_name}),icon:this.getIcon(this.getI18n(i18n+".add_button","icon"),!0),title:this.getI18n(i18n+".add_button","title",{type:type_name}),click:()=>this.$elements.list.editableList("addItem",{type:type})}));return buttons}async init(mainEditor){await super.init(mainEditor),await this.initList(DeconzOutputRuleEditor,this.node.output_rules)}refresh(){for(const rule of Object.values(this.items))rule.updatePayloadList()}}class DeconzCommandEditor extends DeconzListItemEditor{constructor(node,listEditor,container,options={}){super(node,listEditor,container,options=$.extend({},options)),this.containers={}}get lightKeys(){return["bri","sat","hue","ct","xy"]}get argKeys(){return["on","alert","effect","colorloopspeed","open","stop","lift","tilt","scene_mode","group","scene","scene_name","target","command","payload","delay","transitiontime","retryonerror","aftererror"]}get elements(){var keys=this.argKeys;keys.push("typedomain"),keys.push("outputButton"),keys.push("scene_picker"),keys.push("scene_picker_refresh");for(const lightKey of this.lightKeys)keys.push(lightKey),keys.push(lightKey+"_direction");var elements={};for(const key of keys)elements[key]=`node-input-output-rule-${this.uniqueId}-`+key;return elements}set value(command){}get value(){var value={arg:{}};value.type=this.$elements.typedomain.typedInput("type"),value.domain=this.$elements.typedomain.typedInput("value");for(const key of this.argKeys)this.$elements[key].parent(".form-row").is(":visible")&&(value.arg[key]={type:this.$elements[key].typedInput("type"),value:this.$elements[key].typedInput("value")});for(const key of this.lightKeys)this.$elements[key].parent(".form-row").is(":visible")&&(value.arg[key]={direction:this.$elements[key+"_direction"].typedInput("type"),type:this.$elements[key].typedInput("type"),value:this.$elements[key].typedInput("value")});return value}get defaultCommand(){return{type:"deconz_state",domain:"lights",target:"state",arg:{on:{type:"keep"},bri:{direction:"set",type:"num"},sat:{direction:"set",type:"num"},hue:{direction:"set",type:"num"},ct:{direction:"set",type:"num"},xy:{direction:"set",type:"num"},alert:{type:"str"},effect:{type:"str"},colorloopspeed:{type:"num"},transitiontime:{type:"num"},command:{type:"str",value:"on"},payload:{type:"msg",value:"payload"},delay:{type:"num",value:2e3},target:{type:"state"},group:{type:"num"},scene_mode:{type:"single"},scene_call:{type:"num"},scene_name:{type:"str"},retryonerror:{type:"num",value:0},aftererror:{type:"continue"}}}}async init(command,index){this._index=index,command=$.extend(!0,this.defaultCommand,command),await this.generateTypeDomainField(this.container,{type:command.type,value:command.domain}),this.containers.light=$("<div>").appendTo(this.container),await this.generateLightOnField(this.containers.light,command.arg.on);for(const lightType of["bri","sat","hue","ct","xy"])await this.generateLightColorField(this.containers.light,lightType,command.arg[lightType]),"bri"===lightType&&await this.generateHR(this.containers.light);await this.generateHR(this.containers.light),await this.generateLightAlertField(this.containers.light,command.arg.alert),await this.generateLightEffectField(this.containers.light,command.arg.effect),await this.generateLightColorLoopSpeedField(this.containers.light,command.arg.colorloopspeed),this.containers.windows_cover=$("<div>").appendTo(this.container),await this.generateCoverOpenField(this.containers.windows_cover,command.arg.open),await this.generateCoverStopField(this.containers.windows_cover,command.arg.stop),await this.generateCoverLiftField(this.containers.windows_cover,command.arg.lift),await this.generateCoverTiltField(this.containers.windows_cover,command.arg.tilt),this.containers.scene_call=$("<div>").appendTo(this.container),await this.generateSceneModeField(this.containers.scene_call,command.arg.scene_mode),this.containers.scene_call_single=$("<div>").appendTo(this.containers.scene_call),await this.generateScenePickerField(this.containers.scene_call_single,command.arg.group+"."+command.arg.scene),await this.generateSceneGroupField(this.containers.scene_call_single,command.arg.group),await this.generateSceneSceneField(this.containers.scene_call_single,command.arg.scene),this.containers.scene_call_dynamic=$("<div>").appendTo(this.containers.scene_call),await this.generateSceneNameField(this.containers.scene_call_dynamic,command.arg.scene_name),this.containers.target=$("<div>").appendTo(this.container),await this.generateTargetField(this.containers.target,command.arg.target),this.containers.command=$("<div>").appendTo(this.container),await this.generateCommandField(this.containers.command,command.arg.command),this.containers.payload=$("<div>").appendTo(this.container),await this.generatePayloadField(this.containers.payload,command.arg.payload),this.containers.pause=$("<div>").appendTo(this.container),await this.generatePauseDelayField(this.containers.pause,command.arg.delay),this.containers.transition=$("<div>").appendTo(this.container),await this.generateHR(this.containers.transition),await this.generateCommonTransitionTimeField(this.containers.transition,command.arg.transitiontime),this.containers.common=$("<div>").appendTo(this.container),await this.generateHR(this.containers.common),await this.generateCommonOnErrorRetryField(this.containers.common,command.arg.retryonerror),await this.generateCommonOnErrorAfterField(this.containers.common,command.arg.aftererror),await super.init(),await this.listEditor.mainEditor.isInitialized(),await this.updateShowHide(command.type,command.domain),await this.connect()}async connect(){await super.connect(),this.$elements.typedomain.on("change",(event,type,value)=>{this.updateShowHide(type,value)}),this.$elements.outputButton.on("click",async()=>{try{if("device"!==this.listEditor.mainEditor.subEditor.query.type)this.sendError("Error : The run command can only work with device list.",5e3);else{var command=this.value,devices=this.listEditor.mainEditor.subEditor.device.value;if(0!==devices.length||("deconz_state"===command.type&&"scene_call"===command.domain||"custom"===command.type&&"scene_call"===command.arg.target.type))if("pause"===command.type)this.sendError("Error : Can't test pause command.",5e3);else{for(var[name,value]of Object.entries(command.arg))if(["msg","flow","global","jsonata"].includes(value.type))return void this.sendError(`Error : Cant run this command because the value "${name}" is type "${value.type}".`,5e3);let myNotification=RED.notify("Sending request...",{type:"info"});await $.post(this.NRCD+"/testCommand",{controllerID:this.listEditor.mainEditor.serverNode.id,device_list:devices,command:command,delay:this.listEditor.mainEditor.subEditor.specific.value.delay}).catch((t,u)=>{this.sendError(400===t.status&&t.responseText?t.responseText:u.toString())});myNotification.close(),myNotification=RED.notify("Ok",{timeout:1e3,type:"success"})}else this.sendError("Error : No device selected.",5e3)}}catch(e){let myNotification=RED.notify(e.toString(),{type:"error",buttons:[{class:"error",click:()=>myNotification.close()}]})}});const updateSceneGroupSelection=()=>{var value=this.$elements.scene_picker.multipleSelect("getSelects");1===value.length&&(this.$elements.group.off("change",updateScenePickerSelection),this.$elements.scene.off("change",updateScenePickerSelection),value=value[0].split("."),this.$elements.group.typedInput("type","num"),this.$elements.group.typedInput("value",value[0]),this.$elements.scene.typedInput("type","num"),this.$elements.scene.typedInput("value",value[1]),this.$elements.group.on("change",updateScenePickerSelection),this.$elements.scene.on("change",updateScenePickerSelection))},updateScenePickerSelection=()=>{this.$elements.scene_picker.off("change",updateSceneGroupSelection),this.$elements.scene_picker.multipleSelect("setSelects","num"!==this.$elements.group.typedInput("type")||"num"!==this.$elements.group.typedInput("type")?[]:[this.$elements.group.typedInput("value")+"."+this.$elements.scene.typedInput("value")]),this.$elements.scene_picker.on("change",updateSceneGroupSelection)};this.$elements.scene_mode.on("change",()=>{var isSingle="single"===this.$elements.scene_mode.typedInput("type");this.containers.scene_call_single.toggle(isSingle),this.containers.scene_call_dynamic.toggle(!isSingle)}),this.$elements.scene_picker.on("change",updateSceneGroupSelection),this.$elements.group.on("change",updateScenePickerSelection),this.$elements.scene.on("change",updateScenePickerSelection),this.$elements.scene_picker_refresh.on("click",()=>this.updateSceneList()),this.$elements.target.on("change",(event,type,value)=>{this.containers.command.toggle("scene_call"!==type)})}async updateShowHide(type,domain){var key,value,containers=[];switch(type){case"deconz_state":switch(domain){case"lights":case"groups":containers.push("light"),containers.push("transition");break;case"covers":containers.push("windows_cover");break;case"scene_call":containers.push("scene_call"),await this.updateSceneList(),containers.push("scene_call_"+("single"===this.$elements.scene_mode.typedInput("type")?"single":"dynamic"))}containers.push("common");break;case"homekit":this.$elements.payload.typedInput("types",["msg","flow","global","json","jsonata"]),containers.push("payload"),containers.push("transition"),containers.push("common");break;case"custom":containers.push("target"),"scene_call"!==this.$elements.target.typedInput("type")&&containers.push("command"),this.$elements.payload.typedInput("types",["msg","flow","global","str","num","bool","json","jsonata","date"]),containers.push("payload"),containers.push("transition"),containers.push("common");break;case"pause":containers.push("pause")}for([key,value]of Object.entries(this.containers))value.toggle(containers.includes(key))}async updateSceneList(){this.$elements.scene_picker.multipleSelect("disable"),this.$elements.scene_picker.children().remove();var queryEditor=this.listEditor.mainEditor.subEditor.query;if(void 0!==queryEditor){var params=queryEditor.xhrParams,queryEditor=(params.queryType="json",params.query=JSON.stringify({match:{device_type:"groups"}}),await queryEditor.getItems({refresh:!0,keepOnlyMatched:!0},params));if(void 0!==queryEditor&&void 0!==queryEditor.LightGroup){for(const group of queryEditor.LightGroup){var groupHtml=$("<optgroup/>",{label:group.id+" - "+group.name});if(group.scenes&&0<group.scenes.length){for(const scene of group.scenes)$(`<option>${scene.id} - ${scene.name}</option>`).attr("value",group.id+"."+scene.id).appendTo(groupHtml);groupHtml.appendTo(this.$elements.scene_picker)}}this.$elements.scene_picker.multipleSelect("refresh").multipleSelect("enable"),this.$elements.scene_picker.multipleSelect("setSelects","num"!==this.$elements.group.typedInput("type")||"num"!==this.$elements.group.typedInput("type")?[]:[this.$elements.group.typedInput("value")+"."+this.$elements.scene.typedInput("value")])}}}async generateTypeDomainField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type";await this.generateTypedInputField(container,{id:this.elements.typedomain,i18n:i18n,value:value,addDefaultTypes:!1,typedInput:{default:"deconz_state",types:[this.generateTypedInputType(i18n,"deconz_state",{subOptions:["lights","covers","groups","scene_call"]}),this.generateTypedInputType(i18n,"homekit",{hasValue:!1}),this.generateTypedInputType(i18n,"custom",{hasValue:!1}),this.generateTypedInputType(i18n,"pause",{hasValue:!1})]}})}async generateLightOnField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields.on";await this.generateTypedInputField(container,{id:this.elements.on,i18n:i18n,value:value,typedInput:{default:"keep",types:[this.generateTypedInputType(i18n,"keep",{hasValue:!1}),this.generateTypedInputType(i18n,"set",{subOptions:["true","false"]}),this.generateTypedInputType(i18n,"toggle",{hasValue:!1})]}})}async generateLightColorField(container,fieldName,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields";let fieldFormat=["num"];var directionsFormat=[this.generateTypedInputType(i18n+".lightFields","set",{hasValue:!1})];switch(fieldName){case"bri":fieldFormat.push("str"),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","inc",{hasValue:!1})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","dec",{hasValue:!1})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","detect_from_value",{hasValue:!1}));break;case"ct":fieldFormat.push("str"),fieldFormat.push(this.generateTypedInputType(i18n+".ct","deconz",{subOptions:["cold","white","warm"]})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","inc",{hasValue:!1})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","dec",{hasValue:!1})),directionsFormat.push(this.generateTypedInputType(i18n+".lightFields","detect_from_value",{hasValue:!1}));break;case"xy":fieldFormat=["json"]}await this.generateDoubleTypedInputField(container,{id:this.elements[fieldName+"_direction"],i18n:i18n+"."+fieldName,addDefaultTypes:!1,value:{type:value.direction},typedInput:{types:directionsFormat}},{id:this.elements[fieldName],value:{type:value.type,value:["xy"===fieldName&&void 0===value.value?"[]":value.value]},typedInput:{types:fieldFormat}})}async generateLightAlertField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields.alert";await this.generateTypedInputField(container,{id:this.elements.alert,i18n:i18n,value:value,typedInput:{types:["str",this.generateTypedInputType(i18n,"deconz",{subOptions:["none","select","lselect"]})]}})}async generateLightEffectField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields.effect";await this.generateTypedInputField(container,{id:this.elements.effect,i18n:i18n,value:value,typedInput:{types:["str",this.generateTypedInputType(i18n,"deconz",{subOptions:["none","colorloop"]})]}})}async generateLightColorLoopSpeedField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.lights.fields.colorloopspeed";await this.generateTypedInputField(container,{id:this.elements.colorloopspeed,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateCoverOpenField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.covers.fields.open";await this.generateTypedInputField(container,{id:this.elements.open,i18n:i18n,value:value,typedInput:{types:[this.generateTypedInputType(i18n,"keep",{hasValue:!1}),this.generateTypedInputType(i18n,"set",{subOptions:["true","false"]}),this.generateTypedInputType(i18n,"toggle",{hasValue:!1})]}})}async generateCoverStopField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.covers.fields.stop";await this.generateTypedInputField(container,{id:this.elements.stop,i18n:i18n,value:value,typedInput:{types:[this.generateTypedInputType(i18n,"keep",{hasValue:!1}),this.generateTypedInputType(i18n,"set",{subOptions:["true","false"]})]}})}async generateCoverLiftField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.covers.fields.lift";await this.generateTypedInputField(container,{id:this.elements.lift,i18n:i18n,value:value,typedInput:{types:["num","str",this.generateTypedInputType(i18n,"stop",{hasValue:!1})]}})}async generateCoverTiltField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.covers.fields.tilt";await this.generateTypedInputField(container,{id:this.elements.tilt,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateSceneModeField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.mode";await this.generateTypedInputField(container,{id:this.elements.scene_mode,i18n:i18n,value:value,addDefaultTypes:!1,typedInput:{types:[this.generateTypedInputType(i18n,"single",{hasValue:!1}),this.generateTypedInputType(i18n,"dynamic",{hasValue:!1})]}})}async generateScenePickerField(container,value=0){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.picker",container=await this.generateSimpleListField(container,{id:this.elements.scene_picker,i18n:i18n}),i18n=(container.addClass("multiple-select"),container.multipleSelect({maxHeight:300,dropWidth:300,width:200,numberDisplayed:1,single:!0,singleRadio:!0,hideOptgroupCheckboxes:!0,showClear:!0,selectAll:!1,filter:!0,filterPlaceholder:this.getI18n(i18n,"filter_place_holder"),placeholder:RED._(this.NRCD+"/server:editor.multiselect.none_selected"),container:".node-input-output-container-row"}),$("<a/>",{id:this.elements.scene_picker_refresh,class:"red-ui-button",style:"margin-left:10px;"}));this.createIconElement(this.getIcon("refresh"),i18n),container.closest(".form-row").append(i18n)}async generateSceneGroupField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.group";await this.generateTypedInputField(container,{id:this.elements.group,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateSceneSceneField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.scene";await this.generateTypedInputField(container,{id:this.elements.scene,i18n:i18n,value:value,typedInput:{types:["num","str",this.generateTypedInputType(i18n,"deconz",{subOptions:["next","prev"]})]}})}async generateSceneNameField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.scene_call.fields.scene_name";await this.generateTypedInputField(container,{id:this.elements.scene_name,i18n:i18n,value:value,typedInput:{types:["str","re"]}})}async generateTargetField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.common.fields.target";await this.generateTypedInputField(container,{id:this.elements.target,i18n:i18n,value:value,typedInput:{types:[this.generateTypedInputType(i18n,"attribute",{hasValue:!1}),this.generateTypedInputType(i18n,"state",{hasValue:!1}),this.generateTypedInputType(i18n,"config",{hasValue:!1}),this.generateTypedInputType(i18n,"scene_call",{hasValue:!1})]}})}async generateCommandField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.common.fields.command";await this.generateTypedInputField(container,{id:this.elements.command,i18n:i18n,value:value,typedInput:{types:["str",this.generateTypedInputType(i18n,"object",{hasValue:!1})]}})}async generatePayloadField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.common.fields.payload";await this.generateTypedInputField(container,{id:this.elements.payload,i18n:i18n,value:value,addDefaultTypes:!1,typedInput:{types:["msg","flow","global","str","num","bool","json","jsonata","date"]}})}async generatePauseDelayField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.pause.fields.delay";await this.generateTypedInputField(container,{id:this.elements.delay,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateCommonTransitionTimeField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.common.fields.transitiontime";await this.generateTypedInputField(container,{id:this.elements.transitiontime,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateCommonOnErrorRetryField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.common.fields.retryonerror";await this.generateTypedInputField(container,{id:this.elements.retryonerror,i18n:i18n,value:value,typedInput:{types:["num"]}})}async generateCommonOnErrorAfterField(container,value={}){var i18n=this.NRCD+"/server:editor.inputs.commands.type.options.deconz_state.options.common.fields.aftererror";await this.generateTypedInputField(container,{id:this.elements.aftererror,i18n:i18n,value:value,typedInput:{types:[this.generateTypedInputType(i18n,"continue",{hasValue:!1}),this.generateTypedInputType(i18n,"stop",{hasValue:!1})]}})}}class DeconzCommandListEditor extends DeconzListItemListEditor{get elements(){return{list:"node-input-output-container"}}get listType(){return"command"}get buttons(){var type_name,buttons=[],i18n=this.NRCD+"/server:editor.inputs.commands.type";for(const[type,enabled]of Object.entries(this.options.type))enabled&&(type_name=this.getI18n(i18n+".options."+type,"label"),buttons.push({label:this.getI18n(i18n+".add_button","label",{type:type_name}),icon:this.getIcon(this.getI18n(i18n+".add_button","icon"),!0),title:this.getI18n(i18n+".add_button","title",{type:type_name}),click:()=>this.$elements.list.editableList("addItem",{type:type})}));return buttons}async init(mainEditor){await super.init(mainEditor),await this.initList(DeconzCommandEditor,this.node.commands)}}
//# sourceMappingURL=deconz-editor.js.map

@@ -1,81 +0,86 @@

const dotProp = require('dot-prop');
const dotProp = require("dot-prop");
const ConfigMigrationHandlerApi = require("./ConfigMigrationHandlerApi");
const ConfigMigrationHandlerInput = require('./ConfigMigrationHandlerInput');
const ConfigMigrationHandlerGet = require('./ConfigMigrationHandlerGet');
const ConfigMigrationHandlerOutput = require('./ConfigMigrationHandlerOutput');
const ConfigMigrationHandlerInput = require("./ConfigMigrationHandlerInput");
const ConfigMigrationHandlerGet = require("./ConfigMigrationHandlerGet");
const ConfigMigrationHandlerOutput = require("./ConfigMigrationHandlerOutput");
const ConfigMigrationHandlerBattery = require("./ConfigMigrationHandlerBattery");
const ConfigMigrationHandlerServer = require('./ConfigMigrationHandlerServer');
const ConfigMigrationHandlerServer = require("./ConfigMigrationHandlerServer");
class ConfigMigration {
constructor(type, config, server) {
this.type = type;
switch (this.type) {
case "deconz-api":
this.handler = new ConfigMigrationHandlerApi(config, server);
break;
case "deconz-input":
this.handler = new ConfigMigrationHandlerInput(config, server);
break;
case "deconz-get":
this.handler = new ConfigMigrationHandlerGet(config, server);
break;
case "deconz-output":
this.handler = new ConfigMigrationHandlerOutput(config, server);
break;
case "deconz-battery":
this.handler = new ConfigMigrationHandlerBattery(config, server);
break;
case "deconz-server":
this.handler = new ConfigMigrationHandlerServer(config, server);
break;
}
}
constructor(type, config, server) {
this.type = type;
switch (this.type) {
case 'deconz-api':
this.handler = new ConfigMigrationHandlerApi(config, server);
break;
case 'deconz-input':
this.handler = new ConfigMigrationHandlerInput(config, server);
break;
case 'deconz-get':
this.handler = new ConfigMigrationHandlerGet(config, server);
break;
case 'deconz-output':
this.handler = new ConfigMigrationHandlerOutput(config, server);
break;
case 'deconz-battery':
this.handler = new ConfigMigrationHandlerBattery(config, server);
break;
case 'deconz-server':
this.handler = new ConfigMigrationHandlerServer(config, server);
break;
}
migrate(config) {
if (this.handler === undefined || !this.handler.migrate) {
return {
errors: [
`Configuration migration handler not found for node type '${this.type}'.`,
],
};
}
migrate(config) {
if (this.handler === undefined || !this.handler.migrate) {
return { errors: [`Configuration migration handler not found for node type '${this.type}'.`] };
}
if (!this.handler.isLastestVersion) {
this.handler.migrate(config);
if (!this.handler.isLastestVersion) {
this.handler.migrate(config);
if (
Array.isArray(this.handler.result.errors) &&
this.handler.result.errors.length === 0
) {
this.handler.result.info.push("Configuration migration OK.");
}
if (Array.isArray(this.handler.result.errors) && this.handler.result.errors.length === 0) {
this.handler.result.info.push(
'Configuration migration OK.'
);
}
this.handler.result.info.push(
'Update the node configuration to hide this message.'
);
return this.handler.result;
} else {
return { notNeeded: true };
}
this.handler.result.info.push(
"Update the node configuration to hide this message."
);
return this.handler.result;
} else {
return { notNeeded: true };
}
}
applyMigration(config, node) {
let result = this.migrate(config);
if (
(Array.isArray(result.errors) && result.errors.length > 0) ||
result.notNeeded === true
) return result;
applyMigration(config, node) {
let result = this.migrate(config);
if (
(Array.isArray(result.errors) && result.errors.length > 0) ||
result.notNeeded === true
)
return result;
// Apply new configuration
for (const [k, v] of Object.entries(result.new)) {
dotProp.set(config, k, v);
}
result.delete.forEach(k => dotProp.delete(config, k));
// Apply new configuration
for (const [k, v] of Object.entries(result.new)) {
dotProp.set(config, k, v);
}
result.delete.forEach((k) => dotProp.delete(config, k));
// Apply new data on controller
for (const [k, v] of Object.entries(result.controller.new)) {
dotProp.set(node, k, v);
}
result.controller.delete.forEach(k => dotProp.delete(node, k));
// Apply new data on controller
for (const [k, v] of Object.entries(result.controller.new)) {
dotProp.set(node, k, v);
}
result.controller.delete.forEach((k) => dotProp.delete(node, k));
return result;
}
return result;
}
}
module.exports = ConfigMigration;
module.exports = ConfigMigration;
class ConfigMigrationHandler {
constructor(config, server) {
this.config = config;
this.server = server;
this.config_version = this.config.config_version;
this.result = {
new: {},
delete: [],
controller: {
new: {},
delete: []
},
errors: [],
info: []
};
}
constructor(config, server) {
this.config = config;
this.server = server;
this.config_version = this.config.config_version;
this.result = {
new: {},
delete: [],
controller: {
new: {},
delete: [],
},
errors: [],
info: [],
};
}
get currentVersion() {
return this.config_version;
}
get currentVersion() {
return this.config_version;
}
get isLastestVersion() {
return this.currentVersion === this.lastVersion;
}
get isLastestVersion() {
return this.currentVersion === this.lastVersion;
}
migrateFromLegacy() {
this.result.info.push('node-red-contrib-deconz/server:tip.update_2_0_0_or_later');
this.result.info.push('node-red-contrib-deconz/server:tip.help_discord_github');
}
migrateFromLegacy() {
this.result.info.push(
"node-red-contrib-deconz/server:tip.update_2_0_0_or_later"
);
this.result.info.push(
"node-red-contrib-deconz/server:tip.help_discord_github"
);
}
migrateDeviceFromLegacy() {
// Migrate device
this.result.new.search_type = 'device';
this.result.new.query = '{}';
this.result.new.device_list = [];
migrateDeviceFromLegacy() {
// Migrate device
this.result.new.search_type = "device";
this.result.new.query = "{}";
this.result.new.device_list = [];
let device;
if (typeof this.config.device === 'string' && this.config.device !== 'undefined' && this.config.device.length > 0) {
if (this.config.device.substr(0, 6) === 'group_') {
device = this.server.device_list.getDeviceByDomainID(
'groups',
Number(this.config.device.substr(6))
);
} else {
device = this.server.device_list.getDeviceByUniqueID(this.config.device);
}
}
if (device) {
this.result.new.device_list.push(this.server.device_list.getPathByDevice(device));
} else {
this.result.errors.push(`Could not find the device '${this.config.device_name}' with uniqueID '${this.config.device}'.`);
}
this.result.delete.push('device');
return device;
let device;
if (
typeof this.config.device === "string" &&
this.config.device !== "undefined" &&
this.config.device.length > 0
) {
if (this.config.device.substr(0, 6) === "group_") {
device = this.server.device_list.getDeviceByDomainID(
"groups",
Number(this.config.device.substr(6))
);
} else {
device = this.server.device_list.getDeviceByUniqueID(
this.config.device
);
}
}
if (device) {
this.result.new.device_list.push(
this.server.device_list.getPathByDevice(device)
);
} else {
this.result.errors.push(
`Could not find the device '${this.config.device_name}' with uniqueID '${this.config.device}'.`
);
}
this.result.delete.push("device");
migrateHomeKitPayload() {
let currentRules = this.config.output_rules;
if (currentRules === undefined) currentRules = this.result.new.output_rules;
if (Array.isArray(currentRules)) {
this.result.new.output_rules = currentRules.map((rule) => {
if (rule.type === 'homekit') {
this.result.info.push('node-red-contrib-deconz/server:tip.homekit_payload');
rule.payload = ['__auto__'];
}
return rule;
});
return device;
}
migrateHomeKitPayload() {
let currentRules = this.config.output_rules;
if (currentRules === undefined) currentRules = this.result.new.output_rules;
if (Array.isArray(currentRules)) {
this.result.new.output_rules = currentRules.map((rule) => {
if (rule.type === "homekit") {
this.result.info.push(
"node-red-contrib-deconz/server:tip.homekit_payload"
);
rule.payload = ["__auto__"];
}
return rule;
});
}
}
}
module.exports = ConfigMigrationHandler;
module.exports = ConfigMigrationHandler;

@@ -1,15 +0,14 @@

const ConfigMigrationHandler = require('./ConfigMigrationHandler');
const ConfigMigrationHandler = require("./ConfigMigrationHandler");
class ConfigMigrationHandlerApi extends ConfigMigrationHandler {
get lastVersion() {
return 1; // Don't forget to update node declaration too
}
get lastVersion() {
return 1; // Don't forget to update node declaration too
}
migrate(controller) {
this.controller = controller;
this.result.new.config_version = this.config_version;
}
migrate(controller) {
this.controller = controller;
this.result.new.config_version = this.config_version;
}
}
module.exports = ConfigMigrationHandlerApi;
module.exports = ConfigMigrationHandlerApi;

@@ -1,48 +0,53 @@

const ConfigMigrationHandler = require('./ConfigMigrationHandler');
const ConfigMigrationHandler = require("./ConfigMigrationHandler");
class ConfigMigrationHandlerBattery extends ConfigMigrationHandler {
get lastVersion() {
return 2; // Don't forget to update node declaration too
}
get lastVersion() {
return 2; // Don't forget to update node declaration too
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
if (this.currentVersion === 1) this.migrateHomeKitPayload();
this.result.new.config_version = this.config_version;
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
if (this.currentVersion === 1) this.migrateHomeKitPayload();
this.result.new.config_version = this.config_version;
}
migrateFromLegacy() {
super.migrateFromLegacy();
// Migrate device
let device = super.migrateDeviceFromLegacy();
migrateFromLegacy() {
super.migrateFromLegacy();
// Migrate device
let device = super.migrateDeviceFromLegacy();
// Migrate output
this.result.new.outputs = 2;
this.result.new.output_rules = [
{
type: 'config',
format: 'single',
onstart: this.config.outputAtStartup !== undefined ? this.config.outputAtStartup : true
},
{
type: 'homekit',
format: 'single',
onstart: this.config.outputAtStartup !== undefined ? this.config.outputAtStartup : true,
}
];
this.result.delete.push('state');
this.result.delete.push('output');
this.result.delete.push('outputAtStartup');
// Migrate output
this.result.new.outputs = 2;
this.result.new.output_rules = [
{
type: "config",
format: "single",
onstart:
this.config.outputAtStartup !== undefined
? this.config.outputAtStartup
: true,
},
{
type: "homekit",
format: "single",
onstart:
this.config.outputAtStartup !== undefined
? this.config.outputAtStartup
: true,
},
];
this.result.delete.push("state");
this.result.delete.push("output");
this.result.delete.push("outputAtStartup");
this.config_version = 1;
}
this.config_version = 1;
}
migrateHomeKitPayload() {
super.migrateHomeKitPayload();
this.config_version = 2;
}
migrateHomeKitPayload() {
super.migrateHomeKitPayload();
this.config_version = 2;
}
}
module.exports = ConfigMigrationHandlerBattery;
module.exports = ConfigMigrationHandlerBattery;

@@ -1,39 +0,38 @@

const ConfigMigrationHandler = require('./ConfigMigrationHandler');
const ConfigMigrationHandler = require("./ConfigMigrationHandler");
class ConfigMigrationHandlerGet extends ConfigMigrationHandler {
get lastVersion() {
return 1; // Don't forget to update node declaration too
}
get lastVersion() {
return 1; // Don't forget to update node declaration too
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
this.result.new.config_version = this.config_version;
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
this.result.new.config_version = this.config_version;
}
migrateFromLegacy() {
super.migrateFromLegacy();
// Migrate device
let device = super.migrateDeviceFromLegacy();
migrateFromLegacy() {
super.migrateFromLegacy();
// Migrate device
let device = super.migrateDeviceFromLegacy();
// Migrate output
this.result.new.outputs = 1;
this.result.new.output_rules = [
{
type: 'state',
format: 'single',
payload: [
(this.config.state === undefined || this.config.state === '0') ?
'__complete__' :
this.config.state
]
}
];
this.result.delete.push('state');
// Migrate output
this.result.new.outputs = 1;
this.result.new.output_rules = [
{
type: "state",
format: "single",
payload: [
this.config.state === undefined || this.config.state === "0"
? "__complete__"
: this.config.state,
],
},
];
this.result.delete.push("state");
this.config_version = 1;
}
this.config_version = 1;
}
}
module.exports = ConfigMigrationHandlerGet;
module.exports = ConfigMigrationHandlerGet;

@@ -1,49 +0,53 @@

const ConfigMigrationHandler = require('./ConfigMigrationHandler');
const ConfigMigrationHandler = require("./ConfigMigrationHandler");
class ConfigMigrationHandlerInput extends ConfigMigrationHandler {
get lastVersion() {
return 2; // Don't forget to update node declaration too
}
get lastVersion() {
return 2; // Don't forget to update node declaration too
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
if (this.currentVersion === 1) this.migrateHomeKitPayload();
this.result.new.config_version = this.config_version;
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
if (this.currentVersion === 1) this.migrateHomeKitPayload();
this.result.new.config_version = this.config_version;
}
migrateFromLegacy() {
super.migrateFromLegacy();
// Migrate device
let device = this.migrateDeviceFromLegacy();
migrateFromLegacy() {
super.migrateFromLegacy();
// Migrate device
let device = this.migrateDeviceFromLegacy();
// Migrate output
this.result.new.outputs = 2;
this.result.new.output_rules = [
{
format: 'single',
type: 'state',
payload: [
(this.config.state === undefined || this.config.state === '0') ?
'__complete__' :
this.config.state
],
output: this.config.output !== undefined ? this.config.output : 'always',
onstart: this.config.outputAtStartup !== undefined ? this.config.outputAtStartup : true
},
{ type: 'homekit', onstart: true, onerror: true }
];
this.result.delete.push('state');
this.result.delete.push('output');
this.result.delete.push('outputAtStartup');
// Migrate output
this.result.new.outputs = 2;
this.result.new.output_rules = [
{
format: "single",
type: "state",
payload: [
this.config.state === undefined || this.config.state === "0"
? "__complete__"
: this.config.state,
],
output:
this.config.output !== undefined ? this.config.output : "always",
onstart:
this.config.outputAtStartup !== undefined
? this.config.outputAtStartup
: true,
},
{ type: "homekit", onstart: true, onerror: true },
];
this.result.delete.push("state");
this.result.delete.push("output");
this.result.delete.push("outputAtStartup");
this.config_version = 1;
}
this.config_version = 1;
}
migrateHomeKitPayload() {
super.migrateHomeKitPayload();
this.config_version = 2;
}
migrateHomeKitPayload() {
super.migrateHomeKitPayload();
this.config_version = 2;
}
}
module.exports = ConfigMigrationHandlerInput;
module.exports = ConfigMigrationHandlerInput;

@@ -1,574 +0,659 @@

const ConfigMigrationHandler = require('./ConfigMigrationHandler');
const ConfigMigrationHandler = require("./ConfigMigrationHandler");
const Utils = require("../runtime/Utils");
class ConfigMigrationHandlerOutput extends ConfigMigrationHandler {
get lastVersion() {
return 2; // Don't forget to update node declaration too
}
get lastVersion() {
return 2; // Don't forget to update node declaration too
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
if (this.currentVersion === 1) this.migrateSceneCallMode();
this.result.new.config_version = this.config_version;
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
if (this.currentVersion === 1) this.migrateSceneCallMode();
this.result.new.config_version = this.config_version;
}
migrateFromLegacy() {
super.migrateFromLegacy();
// Migrate device
let device = this.migrateDeviceFromLegacy();
migrateFromLegacy() {
super.migrateFromLegacy();
// Migrate device
let device = this.migrateDeviceFromLegacy();
let command = {
arg: {}
};
let command = {
arg: {},
};
// Change custom string command that have valid deconz_cmd
if (this.config.commandType === 'str' && [
'on',
'toggle',
'bri', 'hue', 'sat',
'ct', 'xy',
'scene', 'alert', 'effect',
'colorloopspeed'
].includes(this.config.command)) {
this.config.commandType = 'deconz_cmd';
}
// Change custom string command that have valid deconz_cmd
if (
this.config.commandType === "str" &&
[
"on",
"toggle",
"bri",
"hue",
"sat",
"ct",
"xy",
"scene",
"alert",
"effect",
"colorloopspeed",
].includes(this.config.command)
) {
this.config.commandType = "deconz_cmd";
}
if (Utils.isDeviceCover(device) && this.config.commandType === 'str' && [
'open',
'stop',
'lift',
'tilt'
].includes(this.config.command)) {
this.config.commandType = 'deconz_cmd';
if (
Utils.isDeviceCover(device) &&
this.config.commandType === "str" &&
["open", "stop", "lift", "tilt"].includes(this.config.command)
) {
this.config.commandType = "deconz_cmd";
}
// TODO Migrate commands
switch (this.config.commandType) {
case "deconz_cmd":
command.type = "deconz_state";
if (
typeof this.config.device === "string" &&
this.config.device !== "undefined" &&
this.config.device.length > 0 &&
this.config.device.substr(0, 6) === "group_"
) {
command.domain = "groups";
} else if (Utils.isDeviceCover(device)) {
command.domain = "covers";
} else {
command.domain = "lights";
}
// TODO Migrate commands
switch (this.config.commandType) {
case 'deconz_cmd':
command.type = 'deconz_state';
if (typeof this.config.device === 'string' &&
this.config.device !== 'undefined' &&
this.config.device.length > 0 &&
this.config.device.substr(0, 6) === 'group_'
) {
command.domain = 'groups';
} else if (Utils.isDeviceCover(device)) {
command.domain = 'covers';
command.target = "state";
switch (this.config.command) {
case "on":
switch (this.config.payloadType) {
case "deconz_payload":
command.arg.on = {
type: "set",
value: (this.config.payload === "1").toString(),
};
break;
case "msg":
case "flow":
case "global":
command.arg.on = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
case "str":
if (this.config.payload === "true") {
command.arg.on = {
type: "set",
value: "true",
};
} else if (this.config.payload === "false") {
command.arg.on = {
type: "set",
value: "false",
};
} else {
command.domain = 'lights';
this.result.errors.push(
`Invalid value '${this.config.payload}' for option Switch (true/false)`
);
}
command.target = 'state';
switch (this.config.command) {
case 'on':
switch (this.config.payloadType) {
case 'deconz_payload':
command.arg.on = {
type: 'set',
value: (this.config.payload === '1').toString()
};
break;
case 'msg':
case 'flow':
case 'global':
command.arg.on = {
type: this.config.payloadType,
value: this.config.payload
};
break;
case 'str':
if (this.config.payload === 'true') {
command.arg.on = {
type: 'set',
value: 'true'
};
} else if (this.config.payload === 'false') {
command.arg.on = {
type: 'set',
value: 'false'
};
} else {
this.result.errors.push(`Invalid value '${this.config.payload}' for option Switch (true/false)`);
}
break;
case 'num':
if (this.config.payload === '1') {
command.arg.on = {
type: 'set',
value: 'true'
};
} else if (this.config.payload === '0') {
command.arg.on = {
type: 'set',
value: 'false'
};
} else {
this.result.errors.push(`Invalid value '${this.config.payload}' for option Switch (true/false)`);
}
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option Switch (true/false)`);
break;
}
break;
break;
case "num":
if (this.config.payload === "1") {
command.arg.on = {
type: "set",
value: "true",
};
} else if (this.config.payload === "0") {
command.arg.on = {
type: "set",
value: "false",
};
} else {
this.result.errors.push(
`Invalid value '${this.config.payload}' for option Switch (true/false)`
);
}
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option Switch (true/false)`
);
break;
}
break;
case 'toggle':
command.arg.on = {
type: 'toggle',
value: ''
};
break;
case "toggle":
command.arg.on = {
type: "toggle",
value: "",
};
break;
case 'bri':
case 'hue':
case 'sat':
command.arg[this.config.command] = {
direction: 'set'
};
switch (this.config.payloadType) {
case 'msg':
case 'flow':
case 'global':
command.arg[this.config.command].type = this.config.payloadType;
command.arg[this.config.command].value = this.config.payload;
break;
case 'str':
case 'num':
command.arg[this.config.command].type = 'num';
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(`Invalid value '${this.config.payload}' for option '${this.config.command}'`);
} else {
command.arg[this.config.command].value = this.config.payload;
}
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option '${this.config.command}'`);
break;
}
break;
case 'ct':
command.arg.ct = {
direction: 'set'
};
switch (this.config.payloadType) {
case 'deconz_payload':
command.arg.ct.type = 'deconz';
switch (this.config.payload) {
case '153':
command.arg.ct.value = 'cold';
break;
case '320':
command.arg.ct.value = 'white';
break;
case '500':
command.arg.ct.value = 'warm';
break;
default:
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(`Invalid value '${this.config.payload}' for option 'ct'`);
} else {
command.arg.ct.type = 'num';
command.arg.ct.value = this.config.payload;
}
break;
}
break;
case 'msg':
case 'flow':
case 'global':
command.arg.ct.type = this.config.payloadType;
command.arg.ct.value = this.config.payload;
break;
case 'str':
case 'num':
command.arg.ct.type = 'num';
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(`Invalid value '${this.config.payload}' for option 'ct'`);
} else {
command.arg.ct.value = this.config.payload;
}
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'ct'`);
break;
}
break;
case 'xy':
command.arg.xy = {
direction: 'set'
};
switch (this.config.payloadType) {
case 'msg':
case 'flow':
case 'global':
command.arg.xy.type = this.config.payloadType;
command.arg.xy.value = this.config.payload;
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'xy'`);
break;
}
break;
case "bri":
case "hue":
case "sat":
command.arg[this.config.command] = {
direction: "set",
};
switch (this.config.payloadType) {
case "msg":
case "flow":
case "global":
command.arg[this.config.command].type = this.config.payloadType;
command.arg[this.config.command].value = this.config.payload;
break;
case "str":
case "num":
command.arg[this.config.command].type = "num";
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(
`Invalid value '${this.config.payload}' for option '${this.config.command}'`
);
} else {
command.arg[this.config.command].value = this.config.payload;
}
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option '${this.config.command}'`
);
break;
}
break;
case "ct":
command.arg.ct = {
direction: "set",
};
switch (this.config.payloadType) {
case "deconz_payload":
command.arg.ct.type = "deconz";
switch (this.config.payload) {
case "153":
command.arg.ct.value = "cold";
break;
case "320":
command.arg.ct.value = "white";
break;
case "500":
command.arg.ct.value = "warm";
break;
default:
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(
`Invalid value '${this.config.payload}' for option 'ct'`
);
} else {
command.arg.ct.type = "num";
command.arg.ct.value = this.config.payload;
}
break;
}
break;
case "msg":
case "flow":
case "global":
command.arg.ct.type = this.config.payloadType;
command.arg.ct.value = this.config.payload;
break;
case "str":
case "num":
command.arg.ct.type = "num";
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(
`Invalid value '${this.config.payload}' for option 'ct'`
);
} else {
command.arg.ct.value = this.config.payload;
}
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'ct'`
);
break;
}
break;
case "xy":
command.arg.xy = {
direction: "set",
};
switch (this.config.payloadType) {
case "msg":
case "flow":
case "global":
command.arg.xy.type = this.config.payloadType;
command.arg.xy.value = this.config.payload;
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'xy'`
);
break;
}
break;
case 'scene':
command.domain = 'scene_call';
case "scene":
command.domain = "scene_call";
// Strip 'group_' from device name
if (typeof this.config.device === 'string' && this.config.device !== 'undefined' && this.config.device.length > 0) {
let part = this.config.device.substring(6);
if (part.length === 0 || isNaN(parseInt(part))) {
this.result.errors.push(`Invalid group ID '${this.config.device}' for calling scene`);
} else {
command.arg.group = {
type: 'num',
value: String(part)
};
}
}
// Strip 'group_' from device name
if (
typeof this.config.device === "string" &&
this.config.device !== "undefined" &&
this.config.device.length > 0
) {
let part = this.config.device.substring(6);
if (part.length === 0 || isNaN(parseInt(part))) {
this.result.errors.push(
`Invalid group ID '${this.config.device}' for calling scene`
);
} else {
command.arg.group = {
type: "num",
value: String(part),
};
}
}
switch (this.config.payloadType) {
case 'deconz_payload':
case 'str':
case 'num':
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(`Invalid scene ID '${this.config.payload}' for calling scene`);
} else {
command.arg.scene = {
type: 'num',
value: this.config.payload
};
}
break;
case 'msg':
case 'flow':
case 'global':
command.arg.scene = {
type: this.config.payloadType,
value: this.config.payload
};
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for calling scene`);
break;
}
break;
switch (this.config.payloadType) {
case "deconz_payload":
case "str":
case "num":
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(
`Invalid scene ID '${this.config.payload}' for calling scene`
);
} else {
command.arg.scene = {
type: "num",
value: this.config.payload,
};
}
break;
case "msg":
case "flow":
case "global":
command.arg.scene = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for calling scene`
);
break;
}
break;
case 'alert':
switch (this.config.payloadType) {
case 'deconz_payload':
switch (this.config.payload) {
case 'none':
case 'select':
case 'lselect':
command.arg.alert = {
type: 'deconz',
value: this.config.payload
};
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'alert'`);
break;
}
break;
case 'msg':
case 'flow':
case 'global':
command.arg.alert = {
type: this.config.payloadType,
value: this.config.payload
};
break;
case 'str':
case 'num':
command.arg.alert = {
type: 'str',
value: this.config.payload
};
if (['none', 'select', 'lselect'].includes(command.arg.alert.value))
command.arg.alert.type = 'deconz';
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'alert'`);
break;
}
break;
case 'effect':
switch (this.config.payloadType) {
case 'deconz_payload':
switch (this.config.payload) {
case 'none':
case 'colorloop':
command.arg.effect = {
type: 'deconz',
value: this.config.payload
};
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'effect'`);
break;
}
break;
case 'msg':
case 'flow':
case 'global':
command.arg.effect = {
type: this.config.payloadType,
value: this.config.payload
};
break;
case 'str':
case 'num':
command.arg.effect = {
type: 'str',
value: this.config.payload
};
if (['none', 'colorloop'].includes(command.arg.effect.value))
command.arg.effect.type = 'deconz';
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'effect'`);
break;
}
break;
case 'colorloopspeed':
switch (this.config.payloadType) {
case 'msg':
case 'flow':
case 'global':
command.arg.colorloopspeed = {
type: this.config.payloadType,
value: this.config.payload
};
break;
case 'str':
case 'num':
command.arg.colorloopspeed = { type: 'num' };
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(`Invalid value '${this.config.payload}' for option 'colorloopspeed'`);
} else {
command.arg.colorloopspeed.value = this.config.payload;
}
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'colorloopspeed'`);
break;
}
break;
case 'open':
case 'stop':
switch (this.config.payloadType) {
case 'msg':
case 'flow':
case 'global':
command.arg[this.config.command] = {
type: this.config.payloadType,
value: this.config.payload
};
break;
case 'str':
if (['true', 'false'].includes(this.config.payload)) {
command.arg[this.config.command] = {
type: 'set',
value: this.config.payload
};
} else {
this.result.errors.push(`Invalid value '${this.config.payload}' for option '${this.config.command}'`);
}
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option '${this.config.command}'`);
break;
}
break;
case 'lift':
switch (this.config.payloadType) {
case 'msg':
case 'flow':
case 'global':
case 'str':
case 'num':
if (this.config.payload === 'stop') {
command.arg.lift = { type: 'stop' };
} else if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(`Invalid value '${this.config.payload}' for option 'lift'`);
} else {
command.arg.lift = {
type: this.config.payloadType,
value: this.config.payload
};
}
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'lift'`);
break;
}
break;
case 'tilt':
switch (this.config.payloadType) {
case 'msg':
case 'flow':
case 'global':
case 'str':
case 'num':
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(`Invalid value '${this.config.payload}' for option 'tilt'`);
} else {
command.arg.tilt = {
type: this.config.payloadType,
value: this.config.payload
};
if (command.arg.tilt.type === 'str')
command.arg.tilt.type = 'num';
}
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for option 'tilt'`);
break;
}
break;
case "alert":
switch (this.config.payloadType) {
case "deconz_payload":
switch (this.config.payload) {
case "none":
case "select":
case "lselect":
command.arg.alert = {
type: "deconz",
value: this.config.payload,
};
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'alert'`
);
break;
}
if (this.config.command !== 'on' &&
this.config.command !== 'toggle' &&
![
'scene', 'alert', 'effect', 'colorloopspeed',
'open', 'stop', 'lift', 'tilt'
].includes(this.config.command)
break;
case "msg":
case "flow":
case "global":
command.arg.alert = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
case "str":
case "num":
command.arg.alert = {
type: "str",
value: this.config.payload,
};
if (
["none", "select", "lselect"].includes(
command.arg.alert.value
)
)
command.arg.on = { type: 'set', value: 'true' };
if (this.config.command === 'bri' && !isNaN(this.config.payload))
command.arg.on = { type: 'set', value: Number(this.config.payload) > 0 ? 'true' : 'false' };
command.arg.alert.type = "deconz";
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'alert'`
);
break;
}
break;
case 'homekit':
command.type = 'homekit';
switch (this.config.payloadType) {
case 'msg':
command.arg.payload = {
type: this.config.payloadType,
value: this.config.payload
};
break;
case 'flow':
case 'global':
case 'str':
case 'num':
this.result.errors.push(`The type '${this.config.payloadType}' was not valid in legacy version, he has been converted to 'msg'.`);
command.arg.payload = {
type: 'msg',
value: this.config.payload
};
break;
default:
this.result.errors.push(`Invalid value type '${this.config.payloadType}' for homekit command`);
break;
case "effect":
switch (this.config.payloadType) {
case "deconz_payload":
switch (this.config.payload) {
case "none":
case "colorloop":
command.arg.effect = {
type: "deconz",
value: this.config.payload,
};
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'effect'`
);
break;
}
break;
case 'str':
command.type = 'custom';
command.arg.target = { type: 'state' };
command.arg.command = {
type: 'str',
value: this.config.command
case "msg":
case "flow":
case "global":
command.arg.effect = {
type: this.config.payloadType,
value: this.config.payload,
};
command.arg.payload = {
type: this.config.payloadType,
value: this.config.payload
};
break;
case 'msg':
command.type = 'custom';
command.arg.target = { type: 'state' };
command.arg.command = {
type: 'msg',
value: this.config.command
case "str":
case "num":
command.arg.effect = {
type: "str",
value: this.config.payload,
};
command.arg.payload = {
type: this.config.payloadType,
value: this.config.payload
};
if (["none", "colorloop"].includes(command.arg.effect.value))
command.arg.effect.type = "deconz";
break;
case 'object':
command.type = 'custom';
command.arg.target = { type: 'state' };
command.arg.command = { type: 'object' };
command.arg.payload = {
type: this.config.payloadType,
value: this.config.payload
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'effect'`
);
break;
}
break;
case "colorloopspeed":
switch (this.config.payloadType) {
case "msg":
case "flow":
case "global":
command.arg.colorloopspeed = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
default:
this.result.errors.push(`Invalid command type '${this.config.commandType}' for migration`);
}
case "str":
case "num":
command.arg.colorloopspeed = { type: "num" };
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(
`Invalid value '${this.config.payload}' for option 'colorloopspeed'`
);
} else {
command.arg.colorloopspeed.value = this.config.payload;
}
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'colorloopspeed'`
);
break;
}
break;
switch (this.config.transitionTimeType) {
case 'msg':
case 'flow':
case 'global':
command.arg.transition = {
type: this.config.transitionTimeType,
value: this.config.transitionTime
case "open":
case "stop":
switch (this.config.payloadType) {
case "msg":
case "flow":
case "global":
command.arg[this.config.command] = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
case 'str':
case 'num':
command.arg.transition = { type: 'num' };
if (this.config.transitionTime === '') {
command.arg.transition.value = '';
} else if (isNaN(parseInt(this.config.transitionTime))) {
this.result.errors.push(`Invalid value '${this.config.transitionTime}' for option 'transition'`);
case "str":
if (["true", "false"].includes(this.config.payload)) {
command.arg[this.config.command] = {
type: "set",
value: this.config.payload,
};
} else {
command.arg.transition.value = this.config.transitionTime;
this.result.errors.push(
`Invalid value '${this.config.payload}' for option '${this.config.command}'`
);
}
break;
default:
if (typeof this.config.transitionTimeType === 'undefined') {
command.arg.transition = { type: 'num' };
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option '${this.config.command}'`
);
break;
}
break;
case "lift":
switch (this.config.payloadType) {
case "msg":
case "flow":
case "global":
case "str":
case "num":
if (this.config.payload === "stop") {
command.arg.lift = { type: "stop" };
} else if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(
`Invalid value '${this.config.payload}' for option 'lift'`
);
} else {
this.result.errors.push(`Invalid value type '${this.config.transitionTimeType}' for option 'transition'`);
command.arg.lift = {
type: this.config.payloadType,
value: this.config.payload,
};
}
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'lift'`
);
break;
}
break;
case "tilt":
switch (this.config.payloadType) {
case "msg":
case "flow":
case "global":
case "str":
case "num":
if (isNaN(parseInt(this.config.payload))) {
this.result.errors.push(
`Invalid value '${this.config.payload}' for option 'tilt'`
);
} else {
command.arg.tilt = {
type: this.config.payloadType,
value: this.config.payload,
};
if (command.arg.tilt.type === "str")
command.arg.tilt.type = "num";
}
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for option 'tilt'`
);
break;
}
break;
}
this.result.delete.push('command');
this.result.delete.push('commandType');
this.result.delete.push('payload');
this.result.delete.push('payloadType');
this.result.delete.push('transitionTime');
this.result.delete.push('transitionTimeType');
if (
this.config.command !== "on" &&
this.config.command !== "toggle" &&
![
"scene",
"alert",
"effect",
"colorloopspeed",
"open",
"stop",
"lift",
"tilt",
].includes(this.config.command)
)
command.arg.on = { type: "set", value: "true" };
if (this.config.command === "bri" && !isNaN(this.config.payload))
command.arg.on = {
type: "set",
value: Number(this.config.payload) > 0 ? "true" : "false",
};
break;
command.arg.aftererror = { type: 'continue' };
this.result.new.commands = [command];
this.result.new.specific = {
delay: { type: 'num', value: 50 },
result: { type: 'at_end' },
case "homekit":
command.type = "homekit";
switch (this.config.payloadType) {
case "msg":
command.arg.payload = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
case "flow":
case "global":
case "str":
case "num":
this.result.errors.push(
`The type '${this.config.payloadType}' was not valid in legacy version, he has been converted to 'msg'.`
);
command.arg.payload = {
type: "msg",
value: this.config.payload,
};
break;
default:
this.result.errors.push(
`Invalid value type '${this.config.payloadType}' for homekit command`
);
break;
}
break;
case "str":
command.type = "custom";
command.arg.target = { type: "state" };
command.arg.command = {
type: "str",
value: this.config.command,
};
this.config_version = 1;
command.arg.payload = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
case "msg":
command.type = "custom";
command.arg.target = { type: "state" };
command.arg.command = {
type: "msg",
value: this.config.command,
};
command.arg.payload = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
case "object":
command.type = "custom";
command.arg.target = { type: "state" };
command.arg.command = { type: "object" };
command.arg.payload = {
type: this.config.payloadType,
value: this.config.payload,
};
break;
default:
this.result.errors.push(
`Invalid command type '${this.config.commandType}' for migration`
);
}
migrateSceneCallMode() {
// For each commands
let newCommands = [];
if (Array.isArray(this.config.commands)) {
for (let i = 0; i < this.config.commands.length; i++) {
let command = this.config.commands[i];
if (command.type === 'deconz_state' && command.domain === 'scene_call') {
command.arg.scene_mode = { type: 'single', value: '' };
}
newCommands[i] = command;
}
this.result.new.commands = newCommands;
this.config_version = 2;
switch (this.config.transitionTimeType) {
case "msg":
case "flow":
case "global":
command.arg.transition = {
type: this.config.transitionTimeType,
value: this.config.transitionTime,
};
break;
case "str":
case "num":
command.arg.transition = { type: "num" };
if (this.config.transitionTime === "") {
command.arg.transition.value = "";
} else if (isNaN(parseInt(this.config.transitionTime))) {
this.result.errors.push(
`Invalid value '${this.config.transitionTime}' for option 'transition'`
);
} else {
command.arg.transition.value = this.config.transitionTime;
}
break;
default:
if (typeof this.config.transitionTimeType === "undefined") {
command.arg.transition = { type: "num" };
} else {
this.result.errors.push(
`Invalid value type '${this.config.transitionTimeType}' for option 'transition'`
);
}
break;
}
this.result.delete.push("command");
this.result.delete.push("commandType");
this.result.delete.push("payload");
this.result.delete.push("payloadType");
this.result.delete.push("transitionTime");
this.result.delete.push("transitionTimeType");
command.arg.aftererror = { type: "continue" };
this.result.new.commands = [command];
this.result.new.specific = {
delay: { type: "num", value: 50 },
result: { type: "at_end" },
};
this.config_version = 1;
}
migrateSceneCallMode() {
// For each commands
let newCommands = [];
if (Array.isArray(this.config.commands)) {
for (let i = 0; i < this.config.commands.length; i++) {
let command = this.config.commands[i];
if (
command.type === "deconz_state" &&
command.domain === "scene_call"
) {
command.arg.scene_mode = { type: "single", value: "" };
}
newCommands[i] = command;
}
this.result.new.commands = newCommands;
this.config_version = 2;
}
}
}
module.exports = ConfigMigrationHandlerOutput;

@@ -1,30 +0,32 @@

const ConfigMigrationHandler = require('./ConfigMigrationHandler');
const ConfigMigrationHandler = require("./ConfigMigrationHandler");
class ConfigMigrationHandlerServer extends ConfigMigrationHandler {
get lastVersion() {
return 1; // Don't forget to update node declaration too
}
get lastVersion() {
return 1; // Don't forget to update node declaration too
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
this.result.new.config_version = this.config_version;
}
migrate(controller) {
this.controller = controller;
if (this.currentVersion === undefined) this.migrateFromLegacy();
this.result.new.config_version = this.config_version;
}
migrateFromLegacy() {
super.migrateFromLegacy();
migrateFromLegacy() {
super.migrateFromLegacy();
// Prior 1.2.0 the apikey was not stored in credentials
if (this.config.apikey !== undefined) {
this.result.controller.new['credentials.secured_apikey'] = this.config.apikey; // For backend migration
this.result.new.migration_secured_apikey = this.config.apikey; // For frontend migration
this.result.delete.push('apikey');
this.result.info.push('node-red-contrib-deconz/server:tip.secured_apikey_warning_message_update');
}
this.config_version = 1;
// Prior 1.2.0 the apikey was not stored in credentials
if (this.config.apikey !== undefined) {
this.result.controller.new["credentials.secured_apikey"] =
this.config.apikey; // For backend migration
this.result.new.migration_secured_apikey = this.config.apikey; // For frontend migration
this.result.delete.push("apikey");
this.result.info.push(
"node-red-contrib-deconz/server:tip.secured_apikey_warning_message_update"
);
}
this.config_version = 1;
}
}
module.exports = ConfigMigrationHandlerServer;
module.exports = ConfigMigrationHandlerServer;

@@ -62,3 +62,2 @@ /**

const Colorspace = {};

@@ -72,16 +71,26 @@

/** @brief u'v' coordinates of the white point for CIE Lu*v* */
Colorspace.WHITEPOINT_U = ((4 * Colorspace.WHITEPOINT_X) / (Colorspace.WHITEPOINT_X + 15 * Colorspace.WHITEPOINT_Y + 3 * Colorspace.WHITEPOINT_Z));
Colorspace.WHITEPOINT_V = ((9 * Colorspace.WHITEPOINT_Y) / (Colorspace.WHITEPOINT_X + 15 * Colorspace.WHITEPOINT_Y + 3 * Colorspace.WHITEPOINT_Z));
Colorspace.WHITEPOINT_U =
(4 * Colorspace.WHITEPOINT_X) /
(Colorspace.WHITEPOINT_X +
15 * Colorspace.WHITEPOINT_Y +
3 * Colorspace.WHITEPOINT_Z);
Colorspace.WHITEPOINT_V =
(9 * Colorspace.WHITEPOINT_Y) /
(Colorspace.WHITEPOINT_X +
15 * Colorspace.WHITEPOINT_Y +
3 * Colorspace.WHITEPOINT_Z);
/** @brief Min of A and B */
Colorspace.MIN = (A, B) => (((A) <= (B)) ? (A) : (B));
Colorspace.MIN = (A, B) => (A <= B ? A : B);
/** @brief Max of A and B */
Colorspace.MAX = (A, B) => (((A) >= (B)) ? (A) : (B));
Colorspace.MAX = (A, B) => (A >= B ? A : B);
/** @brief Min of A, B, and C */
Colorspace.MIN3 = (A, B, C) => (((A) <= (B)) ? Colorspace.MIN(A, C) : Colorspace.MIN(B, C));
Colorspace.MIN3 = (A, B, C) =>
A <= B ? Colorspace.MIN(A, C) : Colorspace.MIN(B, C);
/** @brief Max of A, B, and C */
Colorspace.MAX3 = (A, B, C) => (((A) >= (B)) ? Colorspace.MAX(A, C) : Colorspace.MAX(B, C));
Colorspace.MAX3 = (A, B, C) =>
A >= B ? Colorspace.MAX(A, C) : Colorspace.MAX(B, C);

@@ -95,7 +104,6 @@ /** @brief The constant pi */

*/
Colorspace.GAMMACORRECTION = (t) => (
((t) <= 0.0031306684425005883) ?
(12.92 * (t)) :
(1.055 * Math.pow((t), 0.416666666666666667) - 0.055)
);
Colorspace.GAMMACORRECTION = (t) =>
t <= 0.0031306684425005883
? 12.92 * t
: 1.055 * Math.pow(t, 0.416666666666666667) - 0.055;

@@ -105,7 +113,4 @@ /**

*/
Colorspace.INVGAMMACORRECTION = (t) => (
((t) <= 0.0404482362771076) ?
((t) / 12.92) :
Math.pow(((t) + 0.055) / 1.055, 2.4)
);
Colorspace.INVGAMMACORRECTION = (t) =>
t <= 0.0404482362771076 ? t / 12.92 : Math.pow((t + 0.055) / 1.055, 2.4);

@@ -116,7 +121,6 @@ /**

*/
Colorspace.LABF = (t) => (
(t >= 8.85645167903563082e-3) ?
Math.pow(t, 0.333333333333333) :
(841.0 / 108.0) * (t) + (4.0 / 29.0)
);
Colorspace.LABF = (t) =>
t >= 8.85645167903563082e-3
? Math.pow(t, 0.333333333333333)
: (841.0 / 108.0) * t + 4.0 / 29.0;

@@ -127,7 +131,4 @@ /**

*/
Colorspace.LABINVF = (t) => (
(t >= 0.206896551724137931) ?
((t) * (t) * (t)) :
(108.0 / 841.0) * ((t) - (4.0 / 29.0))
);
Colorspace.LABINVF = (t) =>
t >= 0.206896551724137931 ? t * t * t : (108.0 / 841.0) * (t - 4.0 / 29.0);

@@ -147,7 +148,7 @@ /*

Colorspace.Rgb2Yuv = (R, G, B) => {
return {
Y: (0.299 * R + 0.587 * G + 0.114 * B),
U: (-0.147 * R - 0.289 * G + 0.436 * B),
V: (0.615 * R - 0.515 * G - 0.100 * B)
};
return {
Y: 0.299 * R + 0.587 * G + 0.114 * B,
U: -0.147 * R - 0.289 * G + 0.436 * B,
V: 0.615 * R - 0.515 * G - 0.1 * B,
};
};

@@ -157,7 +158,7 @@

Colorspace.Yuv2Rgb = (Y, U, V) => {
return {
R: (Y - 3.945707070708279e-05 * U + 1.1398279671717170825 * V),
G: (Y - 0.3946101641414141437 * U - 0.5805003156565656797 * V),
B: (Y + 2.0319996843434342537 * U - 4.813762626262513e-04 * V)
};
return {
R: Y - 3.945707070708279e-5 * U + 1.1398279671717170825 * V,
G: Y - 0.3946101641414141437 * U - 0.5805003156565656797 * V,
B: Y + 2.0319996843434342537 * U - 4.813762626262513e-4 * V,
};
};

@@ -167,7 +168,7 @@

Colorspace.Rgb2Ycbcr = (R, G, B) => {
return {
Y: (65.481 * R + 128.553 * G + 24.966 * B + 16),
Cb: (-37.797 * R - 74.203 * G + 112.0 * B + 128),
Cr: (112.0 * R - 93.786 * G - 18.214 * B + 128)
};
return {
Y: 65.481 * R + 128.553 * G + 24.966 * B + 16,
Cb: -37.797 * R - 74.203 * G + 112.0 * B + 128,
Cr: 112.0 * R - 93.786 * G - 18.214 * B + 128,
};
};

@@ -177,10 +178,19 @@

Colorspace.Ycbcr2Rgb = (Y, Cr, Cb) => {
Y -= 16;
Cb -= 128;
Cr -= 128;
return {
R: (0.00456621004566210107 * Y + 1.1808799897946415e-09 * Cr + 0.00625892896994393634 * Cb),
G: (0.00456621004566210107 * Y - 0.00153632368604490212 * Cr - 0.00318811094965570701 * Cb),
B: (0.00456621004566210107 * Y + 0.00791071623355474145 * Cr + 1.1977497040190077e-08 * Cb)
};
Y -= 16;
Cb -= 128;
Cr -= 128;
return {
R:
0.00456621004566210107 * Y +
1.1808799897946415e-9 * Cr +
0.00625892896994393634 * Cb,
G:
0.00456621004566210107 * Y -
0.00153632368604490212 * Cr -
0.00318811094965570701 * Cb,
B:
0.00456621004566210107 * Y +
0.00791071623355474145 * Cr +
1.1977497040190077e-8 * Cb,
};
};

@@ -190,8 +200,8 @@

Colorspace.Rgb2Jpegycbcr = (R, G, B) => {
let C1 = Colorspace.Rgb2Ypbpr(R, G, B);
return {
Y: C1.Y,
Cb: C1.Pb + 0.5,
Cr: C1.Pr + 0.5
};
let C1 = Colorspace.Rgb2Ypbpr(R, G, B);
return {
Y: C1.Y,
Cb: C1.Pb + 0.5,
Cr: C1.Pr + 0.5,
};
};

@@ -201,5 +211,5 @@

Colorspace.Jpegycbcr2Rgb = (Y, Cb, Cr) => {
Cb -= 0.5;
Cr -= 0.5;
return Colorspace.Ypbpr2Rgb(Y, Cb, Cr);
Cb -= 0.5;
Cr -= 0.5;
return Colorspace.Ypbpr2Rgb(Y, Cb, Cr);
};

@@ -209,7 +219,7 @@

Colorspace.Rgb2Ypbpr = (R, G, B) => {
return {
Y: (0.299 * R + 0.587 * G + 0.114 * B),
Pb: (-0.1687367 * R - 0.331264 * G + 0.5 * B),
Pr: (0.5 * R - 0.418688 * G - 0.081312 * B)
};
return {
Y: 0.299 * R + 0.587 * G + 0.114 * B,
Pb: -0.1687367 * R - 0.331264 * G + 0.5 * B,
Pr: 0.5 * R - 0.418688 * G - 0.081312 * B,
};
};

@@ -219,7 +229,16 @@

Colorspace.Ypbpr2Rgb = (Y, Pb, Pr) => {
return {
R: (0.99999999999914679361 * Y - 1.2188941887145875e-06 * Pb + 1.4019995886561440468 * Pr),
G: (0.99999975910502514331 * Y - 0.34413567816504303521 * Pb - 0.71413649331646789076 * Pr),
B: (1.00000124040004623180 * Y + 1.77200006607230409200 * Pb + 2.1453384174593273e-06 * Pr)
};
return {
R:
0.99999999999914679361 * Y -
1.2188941887145875e-6 * Pb +
1.4019995886561440468 * Pr,
G:
0.99999975910502514331 * Y -
0.34413567816504303521 * Pb -
0.71413649331646789076 * Pr,
B:
1.0000012404000462318 * Y +
1.772000066072304092 * Pb +
2.1453384174593273e-6 * Pr,
};
};

@@ -229,7 +248,7 @@

Colorspace.Rgb2Ydbdr = (R, G, B) => {
return {
Y: (0.299 * R + 0.587 * G + 0.114 * B),
Db: (-0.450 * R - 0.883 * G + 1.333 * B),
Dr: (-1.333 * R + 1.116 * G + 0.217 * B)
};
return {
Y: 0.299 * R + 0.587 * G + 0.114 * B,
Db: -0.45 * R - 0.883 * G + 1.333 * B,
Dr: -1.333 * R + 1.116 * G + 0.217 * B,
};
};

@@ -239,7 +258,7 @@

Colorspace.Ydbdr2Rgb = (Y, Db, Dr) => {
return {
R: (Y + 9.2303716147657e-05 * Db - 0.52591263066186533 * Dr),
G: (Y - 0.12913289889050927 * Db + 0.26789932820759876 * Dr),
B: (Y + 0.66467905997895482 * Db - 7.9202543533108e-05 * Dr)
};
return {
R: Y + 9.2303716147657e-5 * Db - 0.52591263066186533 * Dr,
G: Y - 0.12913289889050927 * Db + 0.26789932820759876 * Dr,
B: Y + 0.66467905997895482 * Db - 7.9202543533108e-5 * Dr,
};
};

@@ -249,7 +268,7 @@

Colorspace.Rgb2Yiq = (R, G, B) => {
return {
Y: (0.299 * R + 0.587 * G + 0.114 * B),
I: (0.595716 * R - 0.274453 * G - 0.321263 * B),
Q: (0.211456 * R - 0.522591 * G + 0.311135 * B)
};
return {
Y: 0.299 * R + 0.587 * G + 0.114 * B,
I: 0.595716 * R - 0.274453 * G - 0.321263 * B,
Q: 0.211456 * R - 0.522591 * G + 0.311135 * B,
};
};

@@ -259,7 +278,7 @@

Colorspace.Yiq2Rgb = (Y, I, Q) => {
return {
R: (Y + 0.9562957197589482261 * I + 0.6210244164652610754 * Q),
G: (Y - 0.2721220993185104464 * I - 0.6473805968256950427 * Q),
B: (Y - 1.1069890167364901945 * I + 1.7046149983646481374 * Q),
};
return {
R: Y + 0.9562957197589482261 * I + 0.6210244164652610754 * Q,
G: Y - 0.2721220993185104464 * I - 0.6473805968256950427 * Q,
B: Y - 1.1069890167364901945 * I + 1.7046149983646481374 * Q,
};
};

@@ -292,25 +311,25 @@

Colorspace.Rgb2Hsv = (R, G, B) => {
let H, S, V;
let Max = Colorspace.MAX3(R, G, B);
let Min = Colorspace.MIN3(R, G, B);
let C = Max - Min;
let H, S, V;
let Max = Colorspace.MAX3(R, G, B);
let Min = Colorspace.MIN3(R, G, B);
let C = Max - Min;
V = Max;
V = Max;
if (C > 0) {
if (Max === R) {
H = (G - B) / C;
if (G < B) H += 6;
} else if (Max === G) {
H = 2 + (B - R) / C;
} else {
H = 4 + (R - G) / C;
}
H *= 60;
S = C / Max;
if (C > 0) {
if (Max === R) {
H = (G - B) / C;
if (G < B) H += 6;
} else if (Max === G) {
H = 2 + (B - R) / C;
} else {
H = S = 0;
H = 4 + (R - G) / C;
}
return { H, S, V };
H *= 60;
S = C / Max;
} else {
H = S = 0;
}
return { H, S, V };
};

@@ -335,46 +354,46 @@

Colorspace.Hsv2Rgb = (H, S, V) => {
let R, G, B;
let C = S * V;
let Min = V - C;
let X;
let R, G, B;
let C = S * V;
let Min = V - C;
let X;
H -= 360 * Math.floor(H / 360);
H /= 60;
X = C * (1 - Math.abs(H - 2 * Math.floor(H / 2) - 1));
H -= 360 * Math.floor(H / 360);
H /= 60;
X = C * (1 - Math.abs(H - 2 * Math.floor(H / 2) - 1));
switch (Math.round(H)) {
case 0:
R = Min + C;
G = Min + X;
B = Min;
break;
case 1:
R = Min + X;
G = Min + C;
B = Min;
break;
case 2:
R = Min;
G = Min + C;
B = Min + X;
break;
case 3:
R = Min;
G = Min + X;
B = Min + C;
break;
case 4:
R = Min + X;
G = Min;
B = Min + C;
break;
case 5:
R = Min + C;
G = Min;
B = Min + X;
break;
default:
R = G = B = 0;
}
return { R, G, B };
switch (Math.round(H)) {
case 0:
R = Min + C;
G = Min + X;
B = Min;
break;
case 1:
R = Min + X;
G = Min + C;
B = Min;
break;
case 2:
R = Min;
G = Min + C;
B = Min + X;
break;
case 3:
R = Min;
G = Min + X;
B = Min + C;
break;
case 4:
R = Min + X;
G = Min;
B = Min + C;
break;
case 5:
R = Min + C;
G = Min;
B = Min + X;
break;
default:
R = G = B = 0;
}
return { R, G, B };
};

@@ -401,25 +420,25 @@

Colorspace.Rgb2Hsl = (R, G, B) => {
let H, S, L;
let H, S, L;
let Max = Colorspace.MAX3(R, G, B);
let Min = Colorspace.MIN3(R, G, B);
let C = Max - Min;
L = (Max + Min) / 2;
let Max = Colorspace.MAX3(R, G, B);
let Min = Colorspace.MIN3(R, G, B);
let C = Max - Min;
L = (Max + Min) / 2;
if (C > 0) {
if (Max === R) {
H = (G - B) / C;
if (G < B) H += 6;
} else if (Max === G) {
H = 2 + (B - R) / C;
} else {
H = 4 + (R - G) / C;
}
H *= 60;
S = (L <= 0.5) ? (C / (2 * (L))) : (C / (2 - 2 * (L)));
if (C > 0) {
if (Max === R) {
H = (G - B) / C;
if (G < B) H += 6;
} else if (Max === G) {
H = 2 + (B - R) / C;
} else {
H = S = 0;
H = 4 + (R - G) / C;
}
H *= 60;
S = L <= 0.5 ? C / (2 * L) : C / (2 - 2 * L);
} else {
H = S = 0;
}
return { H, S, L };
return { H, S, L };
};

@@ -444,46 +463,46 @@

Colorspace.Hsl2Rgb = (H, S, L) => {
let R, G, B;
let C = (L <= 0.5) ? (2 * L * S) : ((2 - 2 * L) * S);
let Min = L - 0.5 * C;
let X;
let R, G, B;
let C = L <= 0.5 ? 2 * L * S : (2 - 2 * L) * S;
let Min = L - 0.5 * C;
let X;
H -= 360 * floor(H / 360);
H /= 60;
X = C * (1 - Math.abs(H - 2 * floor(H / 2) - 1));
H -= 360 * floor(H / 360);
H /= 60;
X = C * (1 - Math.abs(H - 2 * floor(H / 2) - 1));
switch (Math.round(H)) {
case 0:
R = Min + C;
G = Min + X;
B = Min;
break;
case 1:
R = Min + X;
G = Min + C;
B = Min;
break;
case 2:
R = Min;
G = Min + C;
B = Min + X;
break;
case 3:
R = Min;
G = Min + X;
B = Min + C;
break;
case 4:
R = Min + X;
G = Min;
B = Min + C;
break;
case 5:
R = Min + C;
G = Min;
B = Min + X;
break;
default:
R = G = B = 0;
}
return { R, G, B };
switch (Math.round(H)) {
case 0:
R = Min + C;
G = Min + X;
B = Min;
break;
case 1:
R = Min + X;
G = Min + C;
B = Min;
break;
case 2:
R = Min;
G = Min + C;
B = Min + X;
break;
case 3:
R = Min;
G = Min + X;
B = Min + C;
break;
case 4:
R = Min + X;
G = Min;
B = Min + C;
break;
case 5:
R = Min + C;
G = Min;
B = Min + X;
break;
default:
R = G = B = 0;
}
return { R, G, B };
};

@@ -508,14 +527,14 @@

Colorspace.Rgb2Hsi = (R, G, B) => {
let H, S, I;
let alpha = 0.5 * (2 * R - G - B);
let beta = 0.866025403784439 * (G - B);
I = (R + G + B) / 3;
if (I > 0) {
S = 1 - Colorspace.MIN3(R, G, B) / I;
H = Math.atan2(beta, alpha) * (180 / Colorspace.M_PI);
if (H < 0) H += 360;
} else {
H = S = 0;
}
return { H, S, I };
let H, S, I;
let alpha = 0.5 * (2 * R - G - B);
let beta = 0.866025403784439 * (G - B);
I = (R + G + B) / 3;
if (I > 0) {
S = 1 - Colorspace.MIN3(R, G, B) / I;
H = Math.atan2(beta, alpha) * (180 / Colorspace.M_PI);
if (H < 0) H += 360;
} else {
H = S = 0;
}
return { H, S, I };
};

@@ -540,20 +559,32 @@

Colorspace.Hsi2Rgb = (H, S, I) => {
let R, G, B;
H -= 360 * Math.floor(H / 360);
if (H < 120) {
B = I * (1 - S);
R = I * (1 + S * Math.cos(H * (Colorspace.M_PI / 180)) / Math.cos((60 - H) * (Colorspace.M_PI / 180)));
G = 3 * I - R - B;
} else if (H < 240) {
H -= 120;
R = I * (1 - S);
G = I * (1 + S * Math.cos(H * (Colorspace.M_PI / 180)) / Math.cos((60 - H) * (Colorspace.M_PI / 180)));
B = 3 * I - R - G;
} else {
H -= 240;
G = I * (1 - S);
B = I * (1 + S * Math.cos(H * (Colorspace.M_PI / 180)) / Math.cos((60 - H) * (Colorspace.M_PI / 180)));
R = 3 * I - G - B;
}
return { R, G, B };
let R, G, B;
H -= 360 * Math.floor(H / 360);
if (H < 120) {
B = I * (1 - S);
R =
I *
(1 +
(S * Math.cos(H * (Colorspace.M_PI / 180))) /
Math.cos((60 - H) * (Colorspace.M_PI / 180)));
G = 3 * I - R - B;
} else if (H < 240) {
H -= 120;
R = I * (1 - S);
G =
I *
(1 +
(S * Math.cos(H * (Colorspace.M_PI / 180))) /
Math.cos((60 - H) * (Colorspace.M_PI / 180)));
B = 3 * I - R - G;
} else {
H -= 240;
G = I * (1 - S);
B =
I *
(1 +
(S * Math.cos(H * (Colorspace.M_PI / 180))) /
Math.cos((60 - H) * (Colorspace.M_PI / 180)));
R = 3 * I - G - B;
}
return { R, G, B };
};

@@ -581,10 +612,19 @@

Colorspace.Rgb2Xyz = (R, G, B) => {
R = Colorspace.INVGAMMACORRECTION(R);
G = Colorspace.INVGAMMACORRECTION(G);
B = Colorspace.INVGAMMACORRECTION(B);
return {
X: (0.4123955889674142161 * R + 0.3575834307637148171 * G + 0.1804926473817015735 * B),
Y: (0.2125862307855955516 * R + 0.7151703037034108499 * G + 0.07220049864333622685 * B),
Z: (0.01929721549174694484 * R + 0.1191838645808485318 * G + 0.9504971251315797660 * B)
};
R = Colorspace.INVGAMMACORRECTION(R);
G = Colorspace.INVGAMMACORRECTION(G);
B = Colorspace.INVGAMMACORRECTION(B);
return {
X:
0.4123955889674142161 * R +
0.3575834307637148171 * G +
0.1804926473817015735 * B,
Y:
0.2125862307855955516 * R +
0.7151703037034108499 * G +
0.07220049864333622685 * B,
Z:
0.01929721549174694484 * R +
0.1191838645808485318 * G +
0.950497125131579766 * B,
};
};

@@ -600,8 +640,8 @@

Colorspace.Rgb2Xy = (R, G, B) => {
let C1 = Colorspace.Rgb2Xyz(R, G, B);
let C2 = Colorspace.Xyz2Xyb(C1.X, C1.Y, C1.Z);
return {
X: C2.X,
Y: C2.Y
};
let C1 = Colorspace.Rgb2Xyz(R, G, B);
let C2 = Colorspace.Xyz2Xyb(C1.X, C1.Y, C1.Z);
return {
X: C2.X,
Y: C2.Y,
};
};

@@ -622,39 +662,39 @@

Colorspace.Xyz2Rgb = (X, Y, Z) => {
let R, G, B;
let R1, B1, G1, Min;
let R, G, B;
let R1, B1, G1, Min;
R1 = (3.2406 * X - 1.5372 * Y - 0.4986 * Z);
G1 = (-0.9689 * X + 1.8758 * Y + 0.0415 * Z);
B1 = (0.0557 * X - 0.2040 * Y + 1.0570 * Z);
R1 = 3.2406 * X - 1.5372 * Y - 0.4986 * Z;
G1 = -0.9689 * X + 1.8758 * Y + 0.0415 * Z;
B1 = 0.0557 * X - 0.204 * Y + 1.057 * Z;
Min = Colorspace.MIN3(R1, G1, B1);
Min = Colorspace.MIN3(R1, G1, B1);
/* Force nonnegative values so that gamma correction is well-defined. */
if (Min < 0) {
R1 -= Min;
G1 -= Min;
B1 -= Min;
}
/* Force nonnegative values so that gamma correction is well-defined. */
if (Min < 0) {
R1 -= Min;
G1 -= Min;
B1 -= Min;
}
/* Convert value greather than 1 */
if (R1 > 1 && R1 > G1 && R1 > B1) {
R1 = 1;
G1 /= R1;
B1 /= R1;
} else if (G1 > 1 && G1 > R1 && G1 > B1) {
R1 /= G1;
G1 = 1;
B1 /= G1;
} else if (B1 > 1 && B1 > R1 && B1 > G1) {
R1 /= B1;
G1 /= B1;
B1 = 1;
}
/* Convert value greather than 1 */
if (R1 > 1 && R1 > G1 && R1 > B1) {
R1 = 1;
G1 /= R1;
B1 /= R1;
} else if (G1 > 1 && G1 > R1 && G1 > B1) {
R1 /= G1;
G1 = 1;
B1 /= G1;
} else if (B1 > 1 && B1 > R1 && B1 > G1) {
R1 /= B1;
G1 /= B1;
B1 = 1;
}
/* Transform from RGB to R'G'B' */
R = Colorspace.GAMMACORRECTION(R1);
G = Colorspace.GAMMACORRECTION(G1);
B = Colorspace.GAMMACORRECTION(B1);
/* Transform from RGB to R'G'B' */
R = Colorspace.GAMMACORRECTION(R1);
G = Colorspace.GAMMACORRECTION(G1);
B = Colorspace.GAMMACORRECTION(B1);
return { R, G, B };
return { R, G, B };
};

@@ -672,13 +712,13 @@

Colorspace.Xyz2Lab = (X, Y, Z) => {
let L, A, B;
X /= Colorspace.WHITEPOINT_X;
Y /= Colorspace.WHITEPOINT_Y;
Z /= Colorspace.WHITEPOINT_Z;
X = Colorspace.LABF(X);
Y = Colorspace.LABF(Y);
Z = Colorspace.LABF(Z);
L = 116 * Y - 16;
A = 500 * (X - Y);
B = 200 * (Y - Z);
return { L, A, B };
let L, A, B;
X /= Colorspace.WHITEPOINT_X;
Y /= Colorspace.WHITEPOINT_Y;
Z /= Colorspace.WHITEPOINT_Z;
X = Colorspace.LABF(X);
Y = Colorspace.LABF(Y);
Z = Colorspace.LABF(Z);
L = 116 * Y - 16;
A = 500 * (X - Y);
B = 200 * (Y - Z);
return { L, A, B };
};

@@ -696,10 +736,10 @@

Colorspace.Lab2Xyz = (L, A, B) => {
L = (L + 16) / 116;
A = L + A / 500;
B = L - B / 200;
return {
X: Colorspace.WHITEPOINT_X * Colorspace.LABINVF(A),
Y: Colorspace.WHITEPOINT_Y * Colorspace.LABINVF(L),
Z: Colorspace.WHITEPOINT_Z * Colorspace.LABINVF(B)
};
L = (L + 16) / 116;
A = L + A / 500;
B = L - B / 200;
return {
X: Colorspace.WHITEPOINT_X * Colorspace.LABINVF(A),
Y: Colorspace.WHITEPOINT_Y * Colorspace.LABINVF(L),
Z: Colorspace.WHITEPOINT_Z * Colorspace.LABINVF(B),
};
};

@@ -717,18 +757,18 @@

Colorspace.Xyz2Luv = (X, Y, Z) => {
let L, U, V;
let u1, v1, Denom;
let L, U, V;
let u1, v1, Denom;
if ((Denom = X + 15 * Y + 3 * Z) > 0) {
u1 = (4 * X) / Denom;
v1 = (9 * Y) / Denom;
} else {
u1 = v1 = 0;
}
if ((Denom = X + 15 * Y + 3 * Z) > 0) {
u1 = (4 * X) / Denom;
v1 = (9 * Y) / Denom;
} else {
u1 = v1 = 0;
}
Y /= Colorspace.WHITEPOINT_Y;
Y = Colorspace.LABF(Y);
L = 116 * Y - 16;
U = 13 * (L) * (u1 - Colorspace.WHITEPOINT_U);
V = 13 * (L) * (v1 - Colorspace.WHITEPOINT_V);
return { L, U, V };
Y /= Colorspace.WHITEPOINT_Y;
Y = Colorspace.LABF(Y);
L = 116 * Y - 16;
U = 13 * L * (u1 - Colorspace.WHITEPOINT_U);
V = 13 * L * (v1 - Colorspace.WHITEPOINT_V);
return { L, U, V };
};

@@ -746,16 +786,16 @@

Colorspace.Luv2Xyz = (L, U, V) => {
let X, Y, Z;
Y = (L + 16) / 116;
Y = Colorspace.WHITEPOINT_Y * Colorspace.LABINVF(Y);
let X, Y, Z;
Y = (L + 16) / 116;
Y = Colorspace.WHITEPOINT_Y * Colorspace.LABINVF(Y);
if (L !== 0) {
U /= L;
V /= L;
}
if (L !== 0) {
U /= L;
V /= L;
}
U = U / 13 + Colorspace.WHITEPOINT_U;
V = V / 13 + Colorspace.WHITEPOINT_V;
X = (Y) * ((9 * U) / (4 * V));
Z = (Y) * ((3 - 0.75 * U) / V - 5);
return { X, Y, Z };
U = U / 13 + Colorspace.WHITEPOINT_U;
V = V / 13 + Colorspace.WHITEPOINT_V;
X = Y * ((9 * U) / (4 * V));
Z = Y * ((3 - 0.75 * U) / V - 5);
return { X, Y, Z };
};

@@ -775,9 +815,9 @@

Colorspace.Xyz2Lch = (X, Y, Z) => {
let L, C, H;
let C1 = Colorspace.Xyz2Lab(X, Y, Z);
L = C1.L;
C = Math.sqrt(C1.A * C1.A + C1.B * C1.B);
H = Math.atan2(C1.B, C1.A) * 180.0 / Colorspace.M_PI;
if (H < 0) H += 360;
return { L, C, H };
let L, C, H;
let C1 = Colorspace.Xyz2Lab(X, Y, Z);
L = C1.L;
C = Math.sqrt(C1.A * C1.A + C1.B * C1.B);
H = (Math.atan2(C1.B, C1.A) * 180.0) / Colorspace.M_PI;
if (H < 0) H += 360;
return { L, C, H };
};

@@ -793,5 +833,5 @@

Colorspace.Lch2Xyz = (L, C, H) => {
let a = C * Math.cos(H * (Colorspace.M_PI / 180.0));
let b = C * Math.sin(H * (Colorspace.M_PI / 180.0));
return Colorspace.Lab2Xyz(L, a, b);
let a = C * Math.cos(H * (Colorspace.M_PI / 180.0));
let b = C * Math.sin(H * (Colorspace.M_PI / 180.0));
return Colorspace.Lab2Xyz(L, a, b);
};

@@ -801,7 +841,7 @@

Colorspace.Xyz2Cat02lms = (X, Y, Z) => {
return {
L: (0.7328 * X + 0.4296 * Y - 0.1624 * Z),
M: (-0.7036 * X + 1.6975 * Y + 0.0061 * Z),
S: (0.0030 * X + 0.0136 * Y + 0.9834 * Z)
};
return {
L: 0.7328 * X + 0.4296 * Y - 0.1624 * Z,
M: -0.7036 * X + 1.6975 * Y + 0.0061 * Z,
S: 0.003 * X + 0.0136 * Y + 0.9834 * Z,
};
};

@@ -811,7 +851,7 @@

Colorspace.Cat02lms2Xyz = (L, M, S) => {
return {
X: (1.096123820835514 * L - 0.278869000218287 * M + 0.182745179382773 * S),
Y: (0.454369041975359 * L + 0.473533154307412 * M + 0.072097803717229 * S),
Z: (-0.009627608738429 * L - 0.005698031216113 * M + 1.015325639954543 * S)
};
return {
X: 1.096123820835514 * L - 0.278869000218287 * M + 0.182745179382773 * S,
Y: 0.454369041975359 * L + 0.473533154307412 * M + 0.072097803717229 * S,
Z: -0.009627608738429 * L - 0.005698031216113 * M + 1.015325639954543 * S,
};
};

@@ -821,8 +861,8 @@

Colorspace.Xyb2Xyz = (X, Y, B) => {
let z = 1 - X - Y;
return {
X: (B / Y) * X,
Y: B,
Z: (B / Y) * z
};
let z = 1 - X - Y;
return {
X: (B / Y) * X,
Y: B,
Z: (B / Y) * z,
};
};

@@ -832,7 +872,7 @@

Colorspace.Xyz2Xyb = (X, Y, Z) => {
return {
X: X / (X + Y + Z),
Y: Y / (X + Y + Z),
B: Y,
};
return {
X: X / (X + Y + Z),
Y: Y / (X + Y + Z),
B: Y,
};
};

@@ -844,51 +884,51 @@

Colorspace.Rgb2Lab = (R, G, B) => {
let C1 = Colorspace.Rgb2Xyz(R, G, B);
return Colorspace.Xyz2Lab(C1.X, C1.Y, C1.Z);
let C1 = Colorspace.Rgb2Xyz(R, G, B);
return Colorspace.Xyz2Lab(C1.X, C1.Y, C1.Z);
};
Colorspace.Lab2Rgb = (L, A, B) => {
let C1 = Colorspace.Lab2Xyz(L, A, B);
return Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
let C1 = Colorspace.Lab2Xyz(L, A, B);
return Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
};
Colorspace.Rgb2Luv = (R, G, B) => {
let C1 = Colorspace.Rgb2Xyz(R, G, B);
return Colorspace.Xyz2Luv(C1.X, C1.Y, C1.Z);
let C1 = Colorspace.Rgb2Xyz(R, G, B);
return Colorspace.Xyz2Luv(C1.X, C1.Y, C1.Z);
};
Colorspace.Luv2Rgb = (L, U, V) => {
let C1 = Colorspace.Luv2Xyz(L, U, V);
return Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
let C1 = Colorspace.Luv2Xyz(L, U, V);
return Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
};
Colorspace.Rgb2Lch = (R, G, B) => {
let C1 = Colorspace.Rgb2Xyz(R, G, B);
return Colorspace.Xyz2Lch(C1.X, C1.Y, C1.Z);
let C1 = Colorspace.Rgb2Xyz(R, G, B);
return Colorspace.Xyz2Lch(C1.X, C1.Y, C1.Z);
};
Colorspace.Lch2Rgb = (L, C, H) => {
let C1 = Colorspace.Lch2Xyz(L, C, H);
return Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
let C1 = Colorspace.Lch2Xyz(L, C, H);
return Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
};
Colorspace.Rgb2Cat02lms = (R, G, B) => {
let C1 = Colorspace.Rgb2Xyz(R, G, B);
return Colorspace.Xyz2Cat02lms(C1.X, C1.Y, C1.Z);
let C1 = Colorspace.Rgb2Xyz(R, G, B);
return Colorspace.Xyz2Cat02lms(C1.X, C1.Y, C1.Z);
};
Colorspace.Cat02lms2Rgb = (L, M, S) => {
let C1 = Colorspace.Cat02lms2Xyz(L, M, S);
return Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
let C1 = Colorspace.Cat02lms2Xyz(L, M, S);
return Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
};
Colorspace.Xyb2Hsv = (X, Y, B) => {
let C1 = Colorspace.Xyb2Xyz(X, Y, B);
let C2 = Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
return Colorspace.Rgb2Hsv(C2.R, C2.G, C2.B);
let C1 = Colorspace.Xyb2Xyz(X, Y, B);
let C2 = Colorspace.Xyz2Rgb(C1.X, C1.Y, C1.Z);
return Colorspace.Rgb2Hsv(C2.R, C2.G, C2.B);
};
Colorspace.Hsv2Xyb = (H, S, V) => {
let C1 = Colorspace.Hsv2Rgb(H, S, V);
let C2 = Colorspace.Rgb2Xyz(C1.R, C1.G, C1.B);
return Colorspace.Xyz2Xyb(C2.X, C2.Y, C2.Z);
let C1 = Colorspace.Hsv2Rgb(H, S, V);
let C2 = Colorspace.Rgb2Xyz(C1.R, C1.G, C1.B);
return Colorspace.Xyz2Xyb(C2.X, C2.Y, C2.Z);
};

@@ -935,43 +975,78 @@

Colorspace.MiredColorTemperatureToXY = (temperature) => {
if (temperature < 153) temperature = 153;
if (temperature < 153) temperature = 153;
let localX, localY;
let temp = 1000000 / temperature;
let localX, localY;
let temp = 1000000 / temperature;
if (Colorspace.TEMPERATURE_TO_X_TEMPERATURE_TRESHOLD > temp)
localX = Colorspace.TEMPERATURE_TO_X_THIRD_FACTOR_FIRST_EQUATION / temp +
Colorspace.TEMPERATURE_TO_X_FOURTH_FACTOR_FIRST_EQUATION -
Colorspace.TEMPERATURE_TO_X_SECOND_FACTOR_FIRST_EQUATION / temp / temp -
Colorspace.TEMPERATURE_TO_X_FIRST_FACTOR_FIRST_EQUATION / temp / temp / temp;
else
localX = Colorspace.TEMPERATURE_TO_X_SECOND_FACTOR_SECOND_EQUATION / temp / temp +
Colorspace.TEMPERATURE_TO_X_THIRD_FACTOR_SECOND_EQUATION / temp +
Colorspace.TEMPERATURE_TO_X_FOURTH_FACTOR_SECOND_EQUATION -
Colorspace.TEMPERATURE_TO_X_FIRST_FACTOR_SECOND_EQUATION / temp / temp / temp;
if (Colorspace.TEMPERATURE_TO_X_TEMPERATURE_TRESHOLD > temp)
localX =
Colorspace.TEMPERATURE_TO_X_THIRD_FACTOR_FIRST_EQUATION / temp +
Colorspace.TEMPERATURE_TO_X_FOURTH_FACTOR_FIRST_EQUATION -
Colorspace.TEMPERATURE_TO_X_SECOND_FACTOR_FIRST_EQUATION / temp / temp -
Colorspace.TEMPERATURE_TO_X_FIRST_FACTOR_FIRST_EQUATION /
temp /
temp /
temp;
else
localX =
Colorspace.TEMPERATURE_TO_X_SECOND_FACTOR_SECOND_EQUATION / temp / temp +
Colorspace.TEMPERATURE_TO_X_THIRD_FACTOR_SECOND_EQUATION / temp +
Colorspace.TEMPERATURE_TO_X_FOURTH_FACTOR_SECOND_EQUATION -
Colorspace.TEMPERATURE_TO_X_FIRST_FACTOR_SECOND_EQUATION /
temp /
temp /
temp;
if (Colorspace.TEMPERATURE_TO_Y_FIRST_TEMPERATURE_TRESHOLD > temp)
localY = Colorspace.TEMPERATURE_TO_Y_THIRD_FACTOR_FIRST_EQUATION * localX / 65536 -
Colorspace.TEMPERATURE_TO_Y_FIRST_FACTOR_FIRST_EQUATION * localX * localX * localX / 281474976710656 -
Colorspace.TEMPERATURE_TO_Y_SECOND_FACTOR_FIRST_EQUATION * localX * localX / 4294967296 -
Colorspace.TEMPERATURE_TO_Y_FOURTH_FACTOR_FIRST_EQUATION;
else if (Colorspace.TEMPERATURE_TO_Y_SECOND_TEMPERATURE_TRESHOLD > temp)
localY = Colorspace.TEMPERATURE_TO_Y_THIRD_FACTOR_SECOND_EQUATION * localX / 65536 -
Colorspace.TEMPERATURE_TO_Y_FIRST_FACTOR_SECOND_EQUATION * localX * localX * localX / 281474976710656 -
Colorspace.TEMPERATURE_TO_Y_SECOND_FACTOR_SECOND_EQUATION * localX * localX / 4294967296 -
Colorspace.TEMPERATURE_TO_Y_FOURTH_FACTOR_SECOND_EQUATION;
else {
localY = Colorspace.TEMPERATURE_TO_Y_THIRD_FACTOR_THIRD_EQUATION * localX / 65536 +
Colorspace.TEMPERATURE_TO_Y_FIRST_FACTOR_THIRD_EQUATION * localX * localX * localX / 281474976710656 -
Colorspace.TEMPERATURE_TO_Y_SECOND_FACTOR_THIRD_EQUATION * localX * localX / 4294967296 -
Colorspace.TEMPERATURE_TO_Y_FOURTH_FACTOR_THIRD_EQUATION;
}
if (Colorspace.TEMPERATURE_TO_Y_FIRST_TEMPERATURE_TRESHOLD > temp)
localY =
(Colorspace.TEMPERATURE_TO_Y_THIRD_FACTOR_FIRST_EQUATION * localX) /
65536 -
(Colorspace.TEMPERATURE_TO_Y_FIRST_FACTOR_FIRST_EQUATION *
localX *
localX *
localX) /
281474976710656 -
(Colorspace.TEMPERATURE_TO_Y_SECOND_FACTOR_FIRST_EQUATION *
localX *
localX) /
4294967296 -
Colorspace.TEMPERATURE_TO_Y_FOURTH_FACTOR_FIRST_EQUATION;
else if (Colorspace.TEMPERATURE_TO_Y_SECOND_TEMPERATURE_TRESHOLD > temp)
localY =
(Colorspace.TEMPERATURE_TO_Y_THIRD_FACTOR_SECOND_EQUATION * localX) /
65536 -
(Colorspace.TEMPERATURE_TO_Y_FIRST_FACTOR_SECOND_EQUATION *
localX *
localX *
localX) /
281474976710656 -
(Colorspace.TEMPERATURE_TO_Y_SECOND_FACTOR_SECOND_EQUATION *
localX *
localX) /
4294967296 -
Colorspace.TEMPERATURE_TO_Y_FOURTH_FACTOR_SECOND_EQUATION;
else {
localY =
(Colorspace.TEMPERATURE_TO_Y_THIRD_FACTOR_THIRD_EQUATION * localX) /
65536 +
(Colorspace.TEMPERATURE_TO_Y_FIRST_FACTOR_THIRD_EQUATION *
localX *
localX *
localX) /
281474976710656 -
(Colorspace.TEMPERATURE_TO_Y_SECOND_FACTOR_THIRD_EQUATION *
localX *
localX) /
4294967296 -
Colorspace.TEMPERATURE_TO_Y_FOURTH_FACTOR_THIRD_EQUATION;
}
localY *= 4;
localY *= 4;
return {
X: localX,
Y: localY
};
return {
X: localX,
Y: localY,
};
};
module.exports = Colorspace;

@@ -6,417 +6,465 @@ const Utils = require("./Utils");

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: {},
};
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: {}
};
switch (this.type) {
case 'deconz_state':
switch (this.domain) {
case 'lights':
this.valid_domain.push('lights');
this.parseDeconzStateLightArgs();
break;
case 'covers':
this.valid_domain.push('covers');
this.parseDeconzStateCoverArgs();
break;
case 'groups':
this.valid_domain.push('groups');
this.parseDeconzStateLightArgs();
break;
case 'scene_call':
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');
this.parseCustomArgs();
break;
switch (this.type) {
case "deconz_state":
switch (this.domain) {
case "lights":
this.valid_domain.push("lights");
this.parseDeconzStateLightArgs();
break;
case "covers":
this.valid_domain.push("covers");
this.parseDeconzStateCoverArgs();
break;
case "groups":
this.valid_domain.push("groups");
this.parseDeconzStateLightArgs();
break;
case "scene_call":
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");
this.parseCustomArgs();
break;
}
}
parseDeconzStateLightArgs() {
// On command
this.result.state.on = 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;
parseDeconzStateLightArgs() {
// On command
this.result.state.on = 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 = 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]));
}
break;
case 'inc':
this.result.state[`${k}_inc`] = Number(this.getNodeProperty(this.arg[k]));
break;
case 'dec':
this.result.state[`${k}_inc`] = -Number(this.getNodeProperty(this.arg[k]));
break;
case 'detect_from_value':
let value = 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;
// 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 = 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]));
}
break;
case "inc":
this.result.state[`${k}_inc`] = Number(
this.getNodeProperty(this.arg[k])
);
break;
case "dec":
this.result.state[`${k}_inc`] = -Number(
this.getNodeProperty(this.arg[k])
);
break;
case "detect_from_value":
let value = 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] = this.getNodeProperty(this.arg[k]);
}
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] = this.getNodeProperty(this.arg[k]);
}
}
parseDeconzStateCoverArgs() {
this.result.state.open = this.getNodeProperty(
this.arg.open,
[
'toggle'
],
[
['keep', undefined],
['set.true', true],
['set.false', false]
]
);
parseDeconzStateCoverArgs() {
this.result.state.open = this.getNodeProperty(
this.arg.open,
["toggle"],
[
["keep", undefined],
["set.true", true],
["set.false", false],
]
);
this.result.state.stop = this.getNodeProperty(
this.arg.stop,
[],
[
['keep', undefined],
['set.true', true],
['set.false', false]
]
);
this.result.state.stop = this.getNodeProperty(
this.arg.stop,
[],
[
["keep", undefined],
["set.true", true],
["set.false", false],
]
);
this.result.state.lift = this.getNodeProperty(this.arg.lift, ['stop']);
this.result.state.tilt = this.getNodeProperty(this.arg.tilt);
this.result.state.lift = this.getNodeProperty(this.arg.lift, ["stop"]);
this.result.state.tilt = this.getNodeProperty(this.arg.tilt);
}
parseDeconzStateSceneCallArgs() {
switch (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),
};
break;
case "dynamic":
this.result.scene_call = {
mode: "dynamic",
sceneName: this.getNodeProperty(this.arg.scene_name),
};
break;
}
}
parseDeconzStateSceneCallArgs() {
switch (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)
};
break;
case 'dynamic':
this.result.scene_call = {
mode: 'dynamic',
sceneName: this.getNodeProperty(this.arg.scene_name)
};
break;
}
parseHomekitArgs(deviceMeta) {
let values = this.getNodeProperty(this.arg.payload);
let allValues = values;
if (dotProp.has(this.message_in, "hap.allChars")) {
allValues = dotProp.get(this.message_in, "hap.allChars");
}
parseHomekitArgs(deviceMeta) {
let values = 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 (
deviceMeta.hascolor === true &&
Array.isArray(deviceMeta.device_colorcapabilities) &&
!deviceMeta.device_colorcapabilities.includes('unknown')
dotProp.has(values, charsName) &&
!Utils.supportColorCapability(deviceMeta, mode)
) {
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');
this.node.warn(
`The light '${deviceMeta.name}' don't support '${charsName}' values. ` +
`You can use only '${deviceMeta.device_colorcapabilities.toString()}' modes.`
);
}
};
(new HomeKitFormatter.toDeconz()).parse(values, allValues, this.result, deviceMeta);
dotProp.set(this.result, 'state.transitiontime', this.getNodeProperty(this.arg.transitiontime));
checkColorModesCompatibility("Hue", "hs");
checkColorModesCompatibility("Saturation", "hs");
checkColorModesCompatibility("ColorTemperature", "ct");
}
parseCustomArgs() {
let target = 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);
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;
new HomeKitFormatter.toDeconz().parse(
values,
allValues,
this.result,
deviceMeta
);
dotProp.set(
this.result,
"state.transitiontime",
this.getNodeProperty(this.arg.transitiontime)
);
}
parseCustomArgs() {
let target = 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);
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 {*[]}
*/
getRequests(node, devices) {
let deconzApi = node.server.api;
let requests = [];
/**
*
* @param node Node
* @param devices Device[]
* @returns {*[]}
*/
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 (
(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;
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;
} 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;
// 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: {},
};
this.parseHomekitArgs(device.data);
}
// Parse HomeKit values with device Meta
if (this.type === 'homekit') {
this.result = {
config: {},
state: {}
};
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
);
// 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);
}
// 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);
// 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);
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;
}
// 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);
}
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);
}
// 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;
// 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);
}
}
}
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 Utils.getNodeProperty(property, this.node, this.message_in, noValueTypes);
}
// 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;
}
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 Utils.getNodeProperty(
property,
this.node,
this.message_in,
noValueTypes
);
}
}
module.exports = CommandParser;
module.exports = CommandParser;

@@ -1,422 +0,488 @@

const got = require('got');
const dns = require('dns');
const got = require("got");
const dns = require("dns");
const Utils = require("./Utils");
const dnsPromises = dns.promises;
class DeconzAPI {
constructor(options) {
options = Object.assign({}, this.defaultOptions, options);
this.name = options.name;
this.bridge_id = options.bridge_id;
this.ip = options.ip;
this.port = isNaN(options.port) ? undefined : Number(options.port);
this.ws_port = isNaN(options.ws_port) ? undefined : Number(options.ws_port);
this.apikey = options.apikey !== undefined ? options.apikey : "<nouser>";
this.secured = options.secured;
this.version = options.version;
this.polling = options.polling;
this.enableLogs =
options.enableLogs === undefined ? true : options.enableLogs;
this.versions = ["1", "1.1", "2"];
constructor(options) {
options = Object.assign({}, this.defaultOptions, options);
this.name = options.name;
this.bridge_id = options.bridge_id;
this.ip = options.ip;
this.port = isNaN(options.port) ? undefined : Number(options.port);
this.ws_port = isNaN(options.ws_port) ? undefined : Number(options.ws_port);
this.apikey = options.apikey !== undefined ? options.apikey : '<nouser>';
this.secured = options.secured;
this.version = options.version;
this.polling = options.polling;
this.enableLogs = options.enableLogs === undefined ? true : options.enableLogs;
this.versions = [
'1', '1.1', '2'
];
this.url = {
discover: () => "https://phoscon.de/discover",
api: () => `http${this.secured ? "s" : ""}://${this.ip}:${this.port}/api`,
challenge: () => `${this.url.api()}/challenge`, // Undocumented
main: () => `${this.url.api()}/${this.apikey}`,
config: {
main: () => `/config`,
whitelist: (api_key) =>
`${this.url.config.main()}/whitelist${
api_key !== undefined ? `/${api_key}` : ""
}`,
update: () => `${this.url.config.main()}/update`,
updatefirmware: () => `${this.url.config.main()}/updatefirmware`,
reset: () => `${this.url.config.main()}/reset`,
restart: () => `${this.url.config.main()}/restart`, // Undocumented
restartapp: () => `${this.url.config.main()}/restartapp`, // Undocumented
shutdown: () => `${this.url.config.main()}/shutdown`, // Undocumented
export: () => `${this.url.config.main()}/export`, // Undocumented
import: () => `${this.url.config.main()}/import`, // Undocumented
password: () => `${this.url.config.main()}/password`,
zigbee: (zigbee_id) =>
`${this.url.config.main()}/zigbee${
zigbee_id !== undefined ? `/${zigbee_id}` : ""
}`,
// Beta endpoint
wifi: {
main: () => `${this.url.config.main()}/wifi`,
restore: () => `${this.url.config.wifi.main()}/restore`,
},
wifiscan: () => `${this.url.config.main()}/wifiscan`, // Undocumented
},
capabilities: {
main: () => `/capabilities`,
},
info: {
main: () => `/info`,
timezones: () => `${this.url.info.main()}/timezones`,
},
groups: {
main: (group_id) =>
`/groups${group_id !== undefined ? `/${group_id}` : ""}`,
action: (group_id) => `${this.url.groups.main(group_id)}/action`,
scenes: {
main: (group_id, scene_id) =>
`${this.url.groups.main(group_id)}/scenes${
scene_id !== undefined ? `/${scene_id}` : ""
}`,
store: (group_id, scene_id) =>
`${this.url.groups.scenes.main(group_id, scene_id)}/store`,
recall: (group_id, scene_id) =>
`${this.url.groups.scenes.main(group_id, scene_id)}/recall`,
recallnext: (group_id) =>
`${this.url.groups.scenes.main(group_id, "next")}/recall`,
recallprev: (group_id) =>
`${this.url.groups.scenes.main(group_id, "prev")}/recall`,
light: {
main: (group_id, scene_id, light_id) =>
`${this.url.groups.scenes.main(group_id, scene_id)}/lights${
light_id !== undefined ? `/${light_id}/state` : ""
}`,
action: (group_id, scene_id, light_id) =>
`${this.url.groups.scenes.light.main(
group_id,
scene_id,
light_id
)}/state`,
},
},
},
lights: {
main: (light_id) =>
`/lights${light_id !== undefined ? `/${light_id}` : ""}`,
action: (light_id) => `${this.url.lights.main(light_id)}/state`,
groups: (light_id) => `${this.url.lights.main(light_id)}/groups`,
scenes: (light_id) => `${this.url.lights.main(light_id)}/scenes`,
connectivity: (light_id) =>
`${this.url.lights.main(light_id)}/connectivity`, // Undocumented and can crash deconz
},
resourcelinks: {
main: (resourcelink_id) =>
`/resourcelinks${
resourcelink_id !== undefined ? `/${resourcelink_id}` : ""
}`,
},
rules: {
main: (rule_id) =>
`/rules${rule_id !== undefined ? `/${rule_id}` : ""}`,
},
schedules: {
main: (schedule_id) =>
`/schedules${schedule_id !== undefined ? `/${schedule_id}` : ""}`,
},
sensors: {
main: (sensor_id) =>
`/sensors${sensor_id !== undefined ? `/${sensor_id}` : ""}`,
config: (sensor_id) => `${this.url.sensors.main(sensor_id)}/config`,
action: (sensor_id) => `${this.url.sensors.main(sensor_id)}/state`,
},
touchlink: {
main: () => `/touchlink`,
scan: () => `${this.url.touchlink.main()}/scan`,
identify: (result_id) =>
`${this.url.touchlink.main()}${
result_id !== undefined ? `/${result_id}` : ""
}/identify`,
reset: (result_id) =>
`${this.url.touchlink.main()}${
result_id !== undefined ? `/${result_id}` : ""
}/reset`,
},
device: {
// Beta endpoint
main: (device_id) =>
`/devices${device_id !== undefined ? `/${device_id}` : ""}`,
},
userparameter: {
main: (userparameter_id) =>
`/userparameters${
userparameter_id !== undefined ? `/${userparameter_id}` : ""
}`,
},
};
}
this.url = {
discover: () => 'https://phoscon.de/discover',
api: () => `http${this.secured ? 's' : ''}://${this.ip}:${this.port}/api`,
challenge: () => `${this.url.api()}/challenge`, // Undocumented
main: () => `${this.url.api()}/${this.apikey}`,
config: {
main: () => `/config`,
whitelist: (api_key) => `${this.url.config.main()}/whitelist${api_key !== undefined ? `/${api_key}` : ''}`,
update: () => `${this.url.config.main()}/update`,
updatefirmware: () => `${this.url.config.main()}/updatefirmware`,
reset: () => `${this.url.config.main()}/reset`,
restart: () => `${this.url.config.main()}/restart`, // Undocumented
restartapp: () => `${this.url.config.main()}/restartapp`, // Undocumented
shutdown: () => `${this.url.config.main()}/shutdown`, // Undocumented
export: () => `${this.url.config.main()}/export`, // Undocumented
import: () => `${this.url.config.main()}/import`, // Undocumented
password: () => `${this.url.config.main()}/password`,
zigbee: (zigbee_id) => `${this.url.config.main()}/zigbee${zigbee_id !== undefined ? `/${zigbee_id}` : ''}`,
// Beta endpoint
wifi: {
main: () => `${this.url.config.main()}/wifi`,
restore: () => `${this.url.config.wifi.main()}/restore`,
},
wifiscan: () => `${this.url.config.main()}/wifiscan`, // Undocumented
},
capabilities: {
main: () => `/capabilities`,
},
info: {
main: () => `/info`,
timezones: () => `${this.url.info.main()}/timezones`,
},
groups: {
main: (group_id) => `/groups${group_id !== undefined ? `/${group_id}` : ''}`,
action: (group_id) => `${this.url.groups.main(group_id)}/action`,
scenes: {
main: (group_id, scene_id) => `${this.url.groups.main(group_id)}/scenes${scene_id !== undefined ? `/${scene_id}` : ''}`,
store: (group_id, scene_id) => `${this.url.groups.scenes.main(group_id, scene_id)}/store`,
recall: (group_id, scene_id) => `${this.url.groups.scenes.main(group_id, scene_id)}/recall`,
recallnext: (group_id) => `${this.url.groups.scenes.main(group_id, 'next')}/recall`,
recallprev: (group_id) => `${this.url.groups.scenes.main(group_id, 'prev')}/recall`,
light: {
main: (group_id, scene_id, light_id) => `${this.url.groups.scenes.main(group_id, scene_id)}/lights${light_id !== undefined ? `/${light_id}/state` : ''}`,
action: (group_id, scene_id, light_id) => `${this.url.groups.scenes.light.main(group_id, scene_id, light_id)}/state`
}
}
},
lights: {
main: (light_id) => `/lights${light_id !== undefined ? `/${light_id}` : ''}`,
action: (light_id) => `${this.url.lights.main(light_id)}/state`,
groups: (light_id) => `${this.url.lights.main(light_id)}/groups`,
scenes: (light_id) => `${this.url.lights.main(light_id)}/scenes`,
connectivity: (light_id) => `${this.url.lights.main(light_id)}/connectivity` // Undocumented and can crash deconz
},
resourcelinks: {
main: (resourcelink_id) => `/resourcelinks${resourcelink_id !== undefined ? `/${resourcelink_id}` : ''}`
},
rules: {
main: (rule_id) => `/rules${rule_id !== undefined ? `/${rule_id}` : ''}`
},
schedules: {
main: (schedule_id) => `/schedules${schedule_id !== undefined ? `/${schedule_id}` : ''}`
},
sensors: {
main: (sensor_id) => `/sensors${sensor_id !== undefined ? `/${sensor_id}` : ''}`,
config: (sensor_id) => `${this.url.sensors.main(sensor_id)}/config`,
action: (sensor_id) => `${this.url.sensors.main(sensor_id)}/state`
},
touchlink: {
main: () => `/touchlink`,
scan: () => `${this.url.touchlink.main()}/scan`,
identify: (result_id) => `${this.url.touchlink.main()}${result_id !== undefined ? `/${result_id}` : ''}/identify`,
reset: (result_id) => `${this.url.touchlink.main()}${result_id !== undefined ? `/${result_id}` : ''}/reset`
},
device: {
// Beta endpoint
main: (device_id) => `/devices${device_id !== undefined ? `/${device_id}` : ''}`,
},
userparameter: {
main: (userparameter_id) => `/userparameters${userparameter_id !== undefined ? `/${userparameter_id}` : ''}`,
}
};
}
get defaultOptions() {
return {
secured: false,
};
}
get defaultOptions() {
return {
secured: false
};
}
async discoverSettings(opt) {
let options = Object.assign(
{},
{
targetGatewayID: undefined,
devicetype: "Unknown",
},
opt
);
async discoverSettings(opt) {
let options = Object.assign({}, {
targetGatewayID: undefined,
devicetype: 'Unknown'
}, opt);
//TODO check if the current values are valid.
//TODO check if the current values are valid.
let response = { log: [] };
let response = { log: [] };
response.log.push(`Fetching data from '${this.url.discover()}'.`);
let discoverResult = await this.getDiscoveryData();
if (discoverResult === undefined) {
response.log.push(`No data fetched from '${this.url.discover()}'.`);
} else {
response.log.push(
`Found ${discoverResult.length} gateways from '${this.url.discover()}'.`
);
}
response.log.push(`Fetching data from '${this.url.discover()}'.`);
let discoverResult = await this.getDiscoveryData();
if (discoverResult === undefined) {
response.log.push(`No data fetched from '${this.url.discover()}'.`);
} else {
response.log.push(`Found ${discoverResult.length} gateways from '${this.url.discover()}'.`);
}
let guesses = [];
if (typeof this.ip === 'string' && this.ip.length > 0 && this.port !== undefined) {
let ports = [80, 443, 8080];
if (!ports.includes(this.port)) ports.unshift(this.port);
guesses.push({
secured: this.secured || false,
ip: this.ip,
ports,
skipIdCheck: true,
logError: true
});
}
if (Array.isArray(discoverResult) && discoverResult.length > 0) {
for (const result of discoverResult) {
guesses.push({ secured: false, ip: result.internalipaddress, ports: [result.internalport] });
}
}
if (this.ip !== 'localhost') {
guesses.push({ secured: false, ip: 'localhost', ports: [80, 443, 8080] });
guesses.push({ secured: true, ip: 'localhost', ports: [80, 443, 8080] });
}
for (const ip of ['core-deconz.local.hass.io', 'homeassistant.local']) {
if (this.ip !== ip) {
let ports = [40850];
if (!ports.includes(this.port)) ports.unshift(this.port);
guesses.push({ secured: false, ip, ports });
}
}
let tryGuess = async (secured, ip, port, logError) => {
const invalid = [undefined, '', 0];
if (invalid.includes(ip) || invalid.includes(port)) return;
let api = new DeconzAPI({
secured: secured || false,
ip: ip,
port: port,
apikey: '<nouser>',
enableLogs: false
});
let config = await api.getConfig(undefined, 1000);
if (config === undefined) {
if (logError === true) response.log.push(`Requesting api key at ${api.url.main()}... Failed.`);
return;
}
response.log.push(`Found gateway ID "${config.bridgeid}" at "${api.url.main()}".`);
return {
bridge_id: config.bridgeid,
name: config.name,
secured: secured,
ip: ip,
port: port
};
};
let requests = [];
response.log.push(`Looking for gateways at ${guesses.length} locations.`);
for (const guess of guesses) {
for (const port of guess.ports) {
requests.push(tryGuess(guess.secured, guess.ip, port, guess.logError));
}
}
let results = await Promise.all(requests);
// Clean up results
results = results.filter((r) => r !== undefined);
response.log.push(`Found ${results.length} gateways.`);
// If no gateway found, send error
if (results.length === 0) {
response.error = {
code: 'NO_GATEWAY_FOUND',
description: 'No gateway found, please try to set an IP-Address.'
};
return response;
}
let bridgeIds = [];
results = results.filter((result) => {
if (!bridgeIds.includes(result.bridge_id)) {
bridgeIds.push(result.bridge_id);
return true;
}
return false;
let guesses = [];
if (
typeof this.ip === "string" &&
this.ip.length > 0 &&
this.port !== undefined
) {
let ports = [80, 443, 8080];
if (!ports.includes(this.port)) ports.unshift(this.port);
guesses.push({
secured: this.secured || false,
ip: this.ip,
ports,
skipIdCheck: true,
logError: true,
});
}
if (Array.isArray(discoverResult) && discoverResult.length > 0) {
for (const result of discoverResult) {
guesses.push({
secured: false,
ip: result.internalipaddress,
ports: [result.internalport],
});
}
}
if (this.ip !== "localhost") {
guesses.push({ secured: false, ip: "localhost", ports: [80, 443, 8080] });
guesses.push({ secured: true, ip: "localhost", ports: [80, 443, 8080] });
}
for (const ip of ["core-deconz.local.hass.io", "homeassistant.local"]) {
if (this.ip !== ip) {
let ports = [40850];
if (!ports.includes(this.port)) ports.unshift(this.port);
guesses.push({ secured: false, ip, ports });
}
}
// If multiple gateway found, let the user select.
if (results.length > 1 && options.targetGatewayID === undefined) {
response.log.push("Got mutiple result and no choice has already been made.");
response.log.push(JSON.stringify(results));
response.error = {
code: 'GATEWAY_CHOICE',
description: 'Multiple gateways founds.',
gateway_list: results
};
response.currentSettings = this.settings;
response.currentSettings.discoverParam = options;
return response;
}
let tryGuess = async (secured, ip, port, logError) => {
const invalid = [undefined, "", 0];
if (invalid.includes(ip) || invalid.includes(port)) return;
let api = new DeconzAPI({
secured: secured || false,
ip: ip,
port: port,
apikey: "<nouser>",
enableLogs: false,
});
let config = await api.getConfig(undefined, 1000);
if (config === undefined) {
if (logError === true)
response.log.push(
`Requesting api key at ${api.url.main()}... Failed.`
);
return;
}
response.log.push(
`Found gateway ID "${config.bridgeid}" at "${api.url.main()}".`
);
return {
bridge_id: config.bridgeid,
name: config.name,
secured: secured,
ip: ip,
port: port,
};
};
// If there is only one result use it.
if (options.targetGatewayID === undefined && results.length === 1)
options.targetGatewayID = results[0].bridge_id;
let requests = [];
response.log.push(`Looking for gateways at ${guesses.length} locations.`);
for (const guess of guesses) {
for (const port of guess.ports) {
requests.push(tryGuess(guess.secured, guess.ip, port, guess.logError));
}
}
response.log.push(`Trying to configure gateway "${options.targetGatewayID}"`);
let results = await Promise.all(requests);
// Clean up results
results = results.filter((r) => r !== undefined);
response.log.push(`Found ${results.length} gateways.`);
let gatewaySettings = results.filter((r) => r.bridge_id === options.targetGatewayID).shift();
if (gatewaySettings === undefined) {
response.log.push("Gateway settings not found.");
response.error = {
code: 'GATEWAY_NO_DATA',
description: "Can't fetch gateway settings."
};
return response;
}
// If no gateway found, send error
if (results.length === 0) {
response.error = {
code: "NO_GATEWAY_FOUND",
description: "No gateway found, please try to set an IP-Address.",
};
return response;
}
for (const [k, v] of Object.entries(gatewaySettings)) {
this[k] = v;
}
let bridgeIds = [];
results = results.filter((result) => {
if (!bridgeIds.includes(result.bridge_id)) {
bridgeIds.push(result.bridge_id);
return true;
}
return false;
});
response.log.push(`Checking api key ${this.apikey}`);
if (
(this.apikey === undefined || String(this.apikey).length === 0) ||
this.apikey === '<nouser>' ||
await this.getApiKeyMeta() === undefined
) {
response.log.push("No valid API key provided, trying acquiring one.");
this.apikey = '<nouser>';
let apiQuery;
apiQuery = await this.getAPIKey(options.devicetype);
if (apiQuery.error) {
response.log.push("Error while requesting api key.");
response.log.push(apiQuery.error.description);
response.error = {
code: 'DECONZ_ERROR',
type: apiQuery.error.type,
description: apiQuery.error.description
};
response.currentSettings = this.settings;
response.currentSettings.discoverParam = options;
return response;
}
// If multiple gateway found, let the user select.
if (results.length > 1 && options.targetGatewayID === undefined) {
response.log.push(
"Got mutiple result and no choice has already been made."
);
response.log.push(JSON.stringify(results));
response.error = {
code: "GATEWAY_CHOICE",
description: "Multiple gateways founds.",
gateway_list: results,
};
response.currentSettings = this.settings;
response.currentSettings.discoverParam = options;
return response;
}
if (apiQuery.success) {
response.log.push("Successfully got a key.");
this.apikey = apiQuery.success.username;
}
// If there is only one result use it.
if (options.targetGatewayID === undefined && results.length === 1)
options.targetGatewayID = results[0].bridge_id;
}
response.log.push(
`Trying to configure gateway "${options.targetGatewayID}"`
);
if (Utils.isIPAddress(this.ip)) {
let oldIP = this.ip;
try {
response.log.push(`Trying to get a dns name for fetched IP "${this.ip}".`);
let dnsNames = await dnsPromises.reverse(this.ip);
if (dnsNames.length === 0) {
response.log.push("No domain name found.");
} else if (dnsNames.length === 1) {
this.ip = dnsNames[0];
response.log.push(`Found domain name "${this.ip}".`);
} else {
this.ip = dnsNames[0];
response.log.push(`Found multiple domain name "${dnsNames.toString()}".`);
response.log.push(`Using domain name "${this.ip}".`);
}
} catch (e) {
response.log.push("No domain name found.");
}
let gatewaySettings = results
.filter((r) => r.bridge_id === options.targetGatewayID)
.shift();
if (gatewaySettings === undefined) {
response.log.push("Gateway settings not found.");
response.error = {
code: "GATEWAY_NO_DATA",
description: "Can't fetch gateway settings.",
};
return response;
}
if (oldIP !== this.ip) {
let oldEnableLogs = this.enableLogs;
this.enableLogs = false;
let newBridgeId = await this.getConfig('bridgeid', 1000);
this.enableLogs = oldEnableLogs;
if (newBridgeId === this.bridge_id) {
response.log.push(`The domain name seems to be valid. Using the domain name.`);
} else {
response.log.push(`The domain name seems to be invalid. Using the IP address.`);
this.ip = oldIP;
}
}
}
for (const [k, v] of Object.entries(gatewaySettings)) {
this[k] = v;
}
// TODO check if the websocket port is valid
if ((this.ws_port === undefined || this.ws_port === 0)) {
this.ws_port = await this.getConfig('websocketport');
}
response.success = true;
response.log.push(`Checking api key ${this.apikey}`);
if (
this.apikey === undefined ||
String(this.apikey).length === 0 ||
this.apikey === "<nouser>" ||
(await this.getApiKeyMeta()) === undefined
) {
response.log.push("No valid API key provided, trying acquiring one.");
this.apikey = "<nouser>";
let apiQuery;
apiQuery = await this.getAPIKey(options.devicetype);
if (apiQuery.error) {
response.log.push("Error while requesting api key.");
response.log.push(apiQuery.error.description);
response.error = {
code: "DECONZ_ERROR",
type: apiQuery.error.type,
description: apiQuery.error.description,
};
response.currentSettings = this.settings;
response.currentSettings.discoverParam = options;
return response;
}
if (apiQuery.success) {
response.log.push("Successfully got a key.");
this.apikey = apiQuery.success.username;
}
}
async getDiscoveryData() {
try {
const discover = await got(
this.url.discover(),
{
method: 'GET',
retry: 1,
responseType: 'json',
timeout: 2000
}
);
return discover.body;
} catch (e) {
if (this.enableLogs) console.warn(e);
if (Utils.isIPAddress(this.ip)) {
let oldIP = this.ip;
try {
response.log.push(
`Trying to get a dns name for fetched IP "${this.ip}".`
);
let dnsNames = await dnsPromises.reverse(this.ip);
if (dnsNames.length === 0) {
response.log.push("No domain name found.");
} else if (dnsNames.length === 1) {
this.ip = dnsNames[0];
response.log.push(`Found domain name "${this.ip}".`);
} else {
this.ip = dnsNames[0];
response.log.push(
`Found multiple domain name "${dnsNames.toString()}".`
);
response.log.push(`Using domain name "${this.ip}".`);
}
}
} catch (e) {
response.log.push("No domain name found.");
}
async getAPIKey(devicetype) {
try {
const discover = await got(
this.url.api(),
{
method: 'POST',
retry: 1,
json: { devicetype: devicetype },
responseType: 'json',
timeout: 2000
}
);
return discover.body[0];
} catch (e) {
if (e instanceof got.RequestError &&
e.response !== undefined &&
e.response.statusCode === 403
) {
if (Array.isArray(e.response.body)) {
return e.response.body[0];
}
} else {
if (this.enableLogs) console.warn(e);
}
if (oldIP !== this.ip) {
let oldEnableLogs = this.enableLogs;
this.enableLogs = false;
let newBridgeId = await this.getConfig("bridgeid", 1000);
this.enableLogs = oldEnableLogs;
if (newBridgeId === this.bridge_id) {
response.log.push(
`The domain name seems to be valid. Using the domain name.`
);
} else {
response.log.push(
`The domain name seems to be invalid. Using the IP address.`
);
this.ip = oldIP;
}
}
}
async getConfig(keyName, timeout) {
try {
const discover = await this.doRequest(this.url.config.main(), { timeout });
return keyName === undefined ? discover.body : discover.body[keyName];
} catch (e) {
if (this.enableLogs) console.warn(e);
}
// TODO check if the websocket port is valid
if (this.ws_port === undefined || this.ws_port === 0) {
this.ws_port = await this.getConfig("websocketport");
}
async getApiKeyMeta() {
let whitelist = await this.getConfig('whitelist');
return (whitelist === undefined) ? undefined : whitelist[this.apikey];
response.success = true;
response.currentSettings = this.settings;
return response;
}
async getDiscoveryData() {
try {
const discover = await got(this.url.discover(), {
method: "GET",
retry: 1,
responseType: "json",
timeout: 2000,
});
return discover.body;
} catch (e) {
if (this.enableLogs) console.warn(e);
}
}
get settings() {
return {
name: this.name,
ip: this.ip,
port: this.port,
apikey: this.apikey,
ws_port: this.ws_port,
secure: this.secured,
polling: this.polling
};
async getAPIKey(devicetype) {
try {
const discover = await got(this.url.api(), {
method: "POST",
retry: 1,
json: { devicetype: devicetype },
responseType: "json",
timeout: 2000,
});
return discover.body[0];
} catch (e) {
if (
e instanceof got.RequestError &&
e.response !== undefined &&
e.response.statusCode === 403
) {
if (Array.isArray(e.response.body)) {
return e.response.body[0];
}
} else {
if (this.enableLogs) console.warn(e);
}
}
}
async doRequest(endpoint, params = {}) {
// remove leading and trailing slashes
endpoint = endpoint.replace(/^\/|\/$/g, '');
if (typeof params !== 'object') params = {};
// make sure the method is valid
if (!['GET', 'POST', 'PUT', 'DELETE'].includes(params.method)) params.method = 'GET';
// make sure the timeout is valid
if (params.timeout === undefined) params.timeout = 2000;
async getConfig(keyName, timeout) {
try {
const discover = await this.doRequest(this.url.config.main(), {
timeout,
});
return keyName === undefined ? discover.body : discover.body[keyName];
} catch (e) {
if (this.enableLogs) console.warn(e);
}
}
let requestParams = {
method: params.method,
retry: 1,
responseType: 'json',
timeout: (params.timeout || 2000),
};
async getApiKeyMeta() {
let whitelist = await this.getConfig("whitelist");
return whitelist === undefined ? undefined : whitelist[this.apikey];
}
if (params.method !== 'GET') {
requestParams.json = params.body;
}
get settings() {
return {
name: this.name,
ip: this.ip,
port: this.port,
apikey: this.apikey,
ws_port: this.ws_port,
secure: this.secured,
polling: this.polling,
};
}
// return the response
return got(this.url.main() + '/' + endpoint, requestParams);
async doRequest(endpoint, params = {}) {
// remove leading and trailing slashes
endpoint = endpoint.replace(/^\/|\/$/g, "");
if (typeof params !== "object") params = {};
// make sure the method is valid
if (!["GET", "POST", "PUT", "DELETE"].includes(params.method))
params.method = "GET";
// make sure the timeout is valid
if (params.timeout === undefined) params.timeout = 2000;
let requestParams = {
method: params.method,
retry: 1,
responseType: "json",
timeout: params.timeout || 2000,
};
if (params.method !== "GET") {
requestParams.json = params.body;
}
// return the response
return got(this.url.main() + "/" + endpoint, requestParams);
}
}
module.exports = DeconzAPI;
module.exports = DeconzAPI;

@@ -1,154 +0,160 @@

const EventEmitter = require('events');
const WebSocket = require('ws');
const EventEmitter = require("events");
const WebSocket = require("ws");
class DeconzSocket extends EventEmitter {
constructor({
hostname,
port = 443,
token,
secure = false,
pingInterval = 10000,
pingTimeout = 3000,
reconnectInterval = 10000,
reconnectMaxRetries = Infinity,
autoConnect = true
} = {}) {
super();
constructor({
hostname,
port = 443,
token,
secure = false,
pingInterval = 10000,
pingTimeout = 3000,
reconnectInterval = 10000,
reconnectMaxRetries = Infinity,
autoConnect = true,
} = {}) {
super();
this.hostname = hostname;
this.port = port;
this.token = token;
this.secure = secure;
this.pingInterval = pingInterval;
this.pingTimeout = pingTimeout;
this.reconnectInterval = reconnectInterval;
this.reconnectMaxRetries = reconnectMaxRetries;
this.autoConnect = autoConnect;
this.hostname = hostname;
this.port = port;
this.token = token;
this.secure = secure;
this.pingInterval = pingInterval;
this.pingTimeout = pingTimeout;
this.reconnectInterval = reconnectInterval;
this.reconnectMaxRetries = reconnectMaxRetries;
this.autoConnect = autoConnect;
this.shouldClose = false;
this.retries = 0;
this.socket = null;
this.pinger = null;
this.awaitPong = null;
this.shouldClose = false;
this.retries = 0;
this.socket = null;
this.pinger = null;
this.awaitPong = null;
if (this.autoConnect) {
this.connect();
}
if (this.autoConnect) {
this.connect();
}
}
buildAddress() {
const protocol = this.secure ? 'wss' : 'ws';
return `${protocol}://${this.hostname}:${this.port}`;
buildAddress() {
const protocol = this.secure ? "wss" : "ws";
return `${protocol}://${this.hostname}:${this.port}`;
}
connect() {
if (this.retries++ >= this.reconnectMaxRetries) {
this.emit("reconnect-max-retries", this.reconnectMaxRetries);
}
connect() {
if (this.retries++ >= this.reconnectMaxRetries) {
this.emit('reconnect-max-retries', this.reconnectMaxRetries);
}
try {
this.socket = new WebSocket(this.buildAddress());
} catch (err) {
this.onClose(err);
throw err;
}
try {
this.socket = new WebSocket(this.buildAddress());
} catch (err) {
this.onClose(err);
throw err;
}
this.socket.on("open", (data) => this.onOpen(data));
this.socket.on("ping", (data) => this.onPing(data));
this.socket.on("pong", (data) => this.onPong(data));
this.socket.on("message", (data) => this.onMessage(data));
this.socket.on("error", (err) => this.onError(err));
this.socket.on("unexpected-response", (req, res) =>
this.onUnexpectedResponse(req, res)
);
this.socket.on("close", (code, reason) => this.onClose(code, reason));
}
this.socket.on('open', data => this.onOpen(data));
this.socket.on('ping', (data) => this.onPing(data));
this.socket.on('pong', (data) => this.onPong(data));
this.socket.on('message', data => this.onMessage(data));
this.socket.on('error', err => this.onError(err));
this.socket.on('unexpected-response', (req, res) => this.onUnexpectedResponse(req, res));
this.socket.on('close', (code, reason) => this.onClose(code, reason));
}
close() {
this.shouldClose = true;
this.socket.close();
this.socket = null;
}
close() {
this.shouldClose = true;
this.socket.close();
this.socket = null;
parseData(data) {
try {
return JSON.parse(data);
} catch (err) {
return this.emit("error", err);
}
}
parseData(data) {
try {
return JSON.parse(data);
} catch (err) {
return this.emit('error', err);
}
get isReady() {
return (
this.socket &&
this.socket.readyState === WebSocket.OPEN &&
!this.shouldClose
);
}
ping() {
if (this.isReady) {
this.socket.ping("Hi?");
this.awaitPong = setTimeout(() => {
this.emit("pong-timeout");
this.socket.terminate();
}, this.pingTimeout);
}
}
get isReady() {
return this.socket && this.socket.readyState === WebSocket.OPEN && !this.shouldClose;
onOpen(data) {
this.retries = 0;
this.emit("open", data);
this.ping();
}
onClose(code, reason) {
if (this.pinger) {
clearTimeout(this.pinger);
this.pinger = null;
}
ping() {
if (this.isReady) {
this.socket.ping('Hi?');
this.awaitPong = setTimeout(() => {
this.emit('pong-timeout');
this.socket.terminate();
}, this.pingTimeout);
}
if (this.awaitPong) {
clearTimeout(this.awaitPong);
this.awaitPong = null;
}
onOpen(data) {
this.retries = 0;
this.emit('open', data);
this.ping();
if (!this.shouldClose) {
setTimeout(() => this.connect(), this.reconnectInterval);
}
onClose(code, reason) {
if (this.pinger) {
clearTimeout(this.pinger);
this.pinger = null;
}
this.emit("close", code, reason);
}
if (this.awaitPong) {
clearTimeout(this.awaitPong);
this.awaitPong = null;
}
if (!this.shouldClose) {
setTimeout(() => this.connect(), this.reconnectInterval);
}
this.emit('close', code, reason);
onPing() {
if (this.isReady) {
// received ping from plex, be kind and say hi back
// also, plex kills the connection if you don't do this
this.socket.pong("Hi!");
}
}
onPing() {
if (this.isReady) {
// received ping from plex, be kind and say hi back
// also, plex kills the connection if you don't do this
this.socket.pong('Hi!');
}
onPong() {
if (this.awaitPong) {
clearTimeout(this.awaitPong);
}
onPong() {
if (this.awaitPong) {
clearTimeout(this.awaitPong);
}
this.pinger = setTimeout(this.ping.bind(this), this.pingInterval);
this.emit("pong");
}
this.pinger = setTimeout(this.ping.bind(this), this.pingInterval);
this.emit('pong');
onMessage(data) {
const payload = this.parseData(data);
if (payload) {
this.emit("message", payload);
}
}
onMessage(data) {
const payload = this.parseData(data);
if (payload) {
this.emit('message', payload);
}
onUnexpectedResponse(req, res) {
if (res && res.statusCode === 401) {
return this.emit("unauthorized"), req, res;
}
onUnexpectedResponse(req, res) {
if (res && res.statusCode === 401) {
return this.emit('unauthorized'), req, res;
}
return this.emit("unexpected-response", req, res);
}
return this.emit('unexpected-response', req, res);
}
onError(err) {
this.emit('error', err);
}
onError(err) {
this.emit("error", err);
}
}
module.exports = DeconzSocket;
module.exports = DeconzSocket;

@@ -1,146 +0,149 @@

const Query = require('./Query');
const Query = require("./Query");
const Utils = require("./Utils");
class DeviceList {
constructor() {
this.domains = ["groups", "lights", "sensors"];
this.devices = {};
this.all_group_real_id = undefined;
for (const resource of this.domains)
this.devices[resource] = {
ById: {},
ByUniqueID: {},
};
}
constructor() {
this.domains = ['groups', 'lights', 'sensors'];
this.devices = {};
this.all_group_real_id = undefined;
for (const resource of this.domains) this.devices[resource] = {
ById: {},
ByUniqueID: {}
};
}
parse(data, onNewDevice = undefined) {
//TODO find a better way than recreate all object from scratch each time
let newDevices = {};
for (const resourceName of this.domains) {
if (data[resourceName] === undefined) continue;
newDevices[resourceName] = {
ById: {},
ByUniqueID: {},
};
parse(data, onNewDevice = undefined) {
//TODO find a better way than recreate all object from scratch each time
let newDevices = {};
for (const resourceName of this.domains) {
if (data[resourceName] === undefined) continue;
newDevices[resourceName] = {
ById: {},
ByUniqueID: {}
};
for (const [id, device] of Object.entries(data[resourceName])) {
device.device_type = resourceName;
device.device_id = Number(id);
let idPath = this.getIDPathByDevice(device, true);
let uniquePath = this.getUniqueIDPathByDevice(device, true);
device.device_path = uniquePath || idPath;
Utils.convertLightsValues(device);
if (idPath) newDevices[resourceName].ById[device.device_id] = device;
if (uniquePath) newDevices[resourceName].ByUniqueID[device.uniqueid] = device;
if (this.getDeviceByPath(device.device_path) === undefined && typeof onNewDevice === 'function') {
onNewDevice(device);
}
}
for (const [id, device] of Object.entries(data[resourceName])) {
device.device_type = resourceName;
device.device_id = Number(id);
let idPath = this.getIDPathByDevice(device, true);
let uniquePath = this.getUniqueIDPathByDevice(device, true);
device.device_path = uniquePath || idPath;
Utils.convertLightsValues(device);
if (idPath) newDevices[resourceName].ById[device.device_id] = device;
if (uniquePath)
newDevices[resourceName].ByUniqueID[device.uniqueid] = device;
if (
this.getDeviceByPath(device.device_path) === undefined &&
typeof onNewDevice === "function"
) {
onNewDevice(device);
}
this.devices = newDevices;
}
}
this.devices = newDevices;
}
getDeviceByPath(path) {
let parts = path.split("/");
let resourceName = parts.shift();
let domain = parts.shift();
if (resourceName === 'uniqueid') return this.getDeviceByUniqueID(domain);
let sub_path = parts.join("/");
switch (domain) {
case 'device_id':
return this.devices[resourceName].ById[sub_path];
case 'uniqueid':
return this.devices[resourceName].ByUniqueID[sub_path];
}
getDeviceByPath(path) {
let parts = path.split("/");
let resourceName = parts.shift();
let domain = parts.shift();
if (resourceName === "uniqueid") return this.getDeviceByUniqueID(domain);
let sub_path = parts.join("/");
switch (domain) {
case "device_id":
return this.devices[resourceName].ById[sub_path];
case "uniqueid":
return this.devices[resourceName].ByUniqueID[sub_path];
}
}
/**
* Get the device from his unique ID.
* Warning, some devices have the same uniqueID.
* @param uniqueID
* @returns device
*/
getDeviceByUniqueID(uniqueID) {
for (const domain of Object.values(this.devices)) {
let found = domain.ByUniqueID[uniqueID];
if (found) return found;
}
/**
* Get the device from his unique ID.
* Warning, some devices have the same uniqueID.
* @param uniqueID
* @returns device
*/
getDeviceByUniqueID(uniqueID) {
for (const domain of Object.values(this.devices)) {
let found = domain.ByUniqueID[uniqueID];
if (found) return found;
}
}
/**
* Get the device from his domain and his ID.
* @param domain string {'groups', 'lights', 'sensors'}
* @param deviceID string
* @returns device
*/
getDeviceByDomainID(domain, deviceID) {
return this.domains.includes(domain) ? this.devices[domain].ById[deviceID] : undefined;
}
/**
* Get the device from his domain and his ID.
* @param domain string {'groups', 'lights', 'sensors'}
* @param deviceID string
* @returns device
*/
getDeviceByDomainID(domain, deviceID) {
return this.domains.includes(domain)
? this.devices[domain].ById[deviceID]
: undefined;
}
createQuery(device) {
return { device_path: this.getPathByDevice(device) };
}
createQuery(device) {
return { device_path: this.getPathByDevice(device) };
}
getAllDevices() {
return this.getDevicesByQuery("all");
}
getAllDevices() {
return this.getDevicesByQuery("all");
}
getDevicesByQuery(queryParams, options = {}) {
getDevicesByQuery(queryParams, options = {}) {
let opt = {
includeNotMatched: options.includeNotMatched || false,
extractValue: options.extractValue,
};
let opt = {
includeNotMatched: options.includeNotMatched || false,
extractValue: options.extractValue
};
let query = new Query(queryParams);
let result = {
matched: [],
rejected: [],
};
for (const domain of Object.values(this.devices)) {
for (const device of Object.values(domain.ById)) {
let value = opt.extractValue ? device[opt.extractValue] : device;
if (query.match(device)) {
result.matched.push(value);
} else {
result.rejected.push(value);
}
}
let query = new Query(queryParams);
let result = {
matched: [],
rejected: [],
};
for (const domain of Object.values(this.devices)) {
for (const device of Object.values(domain.ById)) {
let value = opt.extractValue ? device[opt.extractValue] : device;
if (query.match(device)) {
result.matched.push(value);
} else {
result.rejected.push(value);
}
return result;
}
}
return result;
}
getPathByDevice(device, includeDeviceType = true) {
return (
this.getUniqueIDPathByDevice(device, includeDeviceType) ||
this.getIDPathByDevice(device, includeDeviceType)
);
}
getPathByDevice(device, includeDeviceType = true) {
return this.getUniqueIDPathByDevice(device, includeDeviceType) || this.getIDPathByDevice(device, includeDeviceType);
}
getIDPathByDevice(device, includeDeviceType = true) {
let path = "";
if (includeDeviceType) path += device.device_type + "/";
path += "device_id/" + device.device_id;
return path;
}
getIDPathByDevice(device, includeDeviceType = true) {
let path = "";
if (includeDeviceType) path += device.device_type + "/";
path += "device_id/" + device.device_id;
return path;
}
getUniqueIDPathByDevice(device, includeDeviceType = true) {
if (device.uniqueid === undefined) return;
let path = "";
if (includeDeviceType) path += device.device_type + "/";
path += "uniqueid/" + device.uniqueid;
return path;
}
getUniqueIDPathByDevice(device, includeDeviceType = true) {
if (device.uniqueid === undefined) return;
let path = "";
if (includeDeviceType) path += device.device_type + "/";
path += "uniqueid/" + device.uniqueid;
return path;
get count() {
let result = 0;
for (const resource of this.domains) {
result += Object.keys(this.devices[resource].ById).length;
}
get count() {
let result = 0;
for (const resource of this.domains) {
result += Object.keys(this.devices[resource].ById).length;
}
return result;
}
return result;
}
}
module.exports = DeviceList;
module.exports = DeviceList;

@@ -6,562 +6,659 @@ const dotProp = require("dot-prop");

class Attribute {
constructor() {
this.requiredAttribute = [];
this.requiredEventMeta = [];
this.requiredDeviceMeta = [];
this.servicesList = [];
}
constructor() {
this.requiredAttribute = [];
this.requiredEventMeta = [];
this.requiredDeviceMeta = [];
this.servicesList = [];
this.valueLimits = {
min: -Infinity,
max: Infinity,
};
}
needAttribute(attribute) {
this.requiredAttribute = this.requiredAttribute.concat(Utils.sanitizeArray(attribute));
return this;
}
needAttribute(attribute) {
this.requiredAttribute = this.requiredAttribute.concat(
Utils.sanitizeArray(attribute)
);
return this;
}
needEventMeta(event) {
this.requiredEventMeta = this.requiredEventMeta.concat(Utils.sanitizeArray(event));
return this;
}
needEventMeta(event) {
this.requiredEventMeta = this.requiredEventMeta.concat(
Utils.sanitizeArray(event)
);
return this;
}
needDeviceMeta(meta) {
this.requiredDeviceMeta = this.requiredDeviceMeta.concat(Utils.sanitizeArray(meta));
return this;
}
needDeviceMeta(meta) {
this.requiredDeviceMeta = this.requiredDeviceMeta.concat(
Utils.sanitizeArray(meta)
);
return this;
}
needColorCapabilities(value) {
this.needDeviceMeta((deviceMeta) => Utils.supportColorCapability(deviceMeta, value));
return this;
}
needColorCapabilities(value) {
this.needDeviceMeta((deviceMeta) =>
Utils.supportColorCapability(deviceMeta, value)
);
return this;
}
to(method) {
this.toMethod = method;
return this;
}
to(method) {
this.toMethod = (...attr) => {
let value = method(...attr);
if (typeof value === "number") {
value = Math.max(value, this.valueLimits.min);
value = Math.min(value, this.valueLimits.max);
}
return value;
};
return this;
}
from(method) {
if (this._name !== undefined) {
console.error("Can't use name with from method");
}
this.fromMethod = method;
return this;
from(method) {
if (this._name !== undefined) {
console.error("Can't use name with from method");
}
this.fromMethod = method;
return this;
}
services(services) {
this.servicesList = this.servicesList.concat(Utils.sanitizeArray(services));
return this;
}
/**
*
* @param {number} min
* @param {number} max
*/
limit(min, max) {
this.valueLimits.min = min;
this.valueLimits.max = max;
return this;
}
name(name) {
if (this.fromMethod !== undefined) {
console.error("Can't use name with from method");
}
this._name = name;
return this;
services(services) {
this.servicesList = this.servicesList.concat(Utils.sanitizeArray(services));
return this;
}
name(name) {
if (this.fromMethod !== undefined) {
console.error("Can't use name with from method");
}
this._name = name;
return this;
}
priority(priority) {
this._priority = priority;
return this;
priority(priority) {
this._priority = priority;
return this;
}
targetIsValid(rawEvent, deviceMeta) {
for (const meta of this.requiredEventMeta) {
if (typeof meta === "function") {
if (meta(rawEvent, deviceMeta) === false) return false;
} else if (Attribute._checkProperties(rawEvent, meta) === false) {
return false;
}
}
for (const meta of this.requiredDeviceMeta) {
if (typeof meta === "function") {
if (meta(deviceMeta) === false) return false;
} else if (Attribute._checkProperties(deviceMeta, meta) === false) {
return false;
}
}
return true;
}
targetIsValid(rawEvent, deviceMeta) {
for (const meta of this.requiredEventMeta) {
if (typeof meta === 'function') {
if (meta(rawEvent, deviceMeta) === false) return false;
} else if (Attribute._checkProperties(rawEvent, meta) === false) {
return false;
}
}
for (const meta of this.requiredDeviceMeta) {
if (typeof meta === 'function') {
if (meta(deviceMeta) === false) return false;
} else if (Attribute._checkProperties(deviceMeta, meta) === false) {
return false;
}
}
return true;
attributeIsValid(result) {
let resultList = Array.isArray(result) ? result : Object.keys(result);
for (let attribute of this.requiredAttribute) {
if (!resultList.includes(attribute)) {
return false;
}
}
return true;
}
attributeIsValid(result) {
let resultList = Array.isArray(result) ? result : Object.keys(result);
for (let attribute of this.requiredAttribute) {
if (!resultList.includes(attribute)) {
return false;
static _checkProperties(data, propertyMeta) {
switch (typeof propertyMeta) {
case "string":
return dotProp.has(data, propertyMeta);
case "object":
for (const [propertyName, expectedValue] of Object.entries(
propertyMeta
)) {
if (!dotProp.has(data, propertyName)) return false;
const currentValue = dotProp.get(data, propertyName);
if (Array.isArray(expectedValue)) {
if (expectedValue.includes(currentValue) === false) {
return false;
}
} else {
if (currentValue !== expectedValue) return false;
}
}
return true;
break;
}
static _checkProperties(data, propertyMeta) {
switch (typeof propertyMeta) {
case 'string':
return dotProp.has(data, propertyMeta);
case 'object':
for (const [propertyName, expectedValue] of Object.entries(propertyMeta)) {
if (!dotProp.has(data, propertyName)) return false;
const currentValue = dotProp.get(data, propertyName);
if (Array.isArray(expectedValue)) {
if (expectedValue.includes(currentValue) === false) {
return false;
}
} else {
if (currentValue !== expectedValue) return false;
}
}
break;
}
return true;
}
return true;
}
}
const HomeKitFormat = (() => {
let directMap = (direction, path) => {
let attr = new Attribute();
attr.needEventMeta(path);
if (direction.includes("to"))
attr.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, path));
if (direction.includes("from"))
attr.from((value, allValues, result) => dotProp.set(result, path, value));
return attr;
};
let directMap = (direction, path) => {
let attr = new Attribute();
attr.needEventMeta(path);
if (direction.includes('to')) attr.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, path));
if (direction.includes('from')) attr.from((value, allValues, result) => dotProp.set(result, path, value));
return attr;
};
const HKF = {};
//#region Switchs
HKF.ServiceLabelIndex = new Attribute()
.services("Stateless Programmable Switch")
.needAttribute("ProgrammableSwitchEvent")
.needEventMeta("state.buttonevent")
.to((rawEvent, deviceMeta) =>
Math.floor(dotProp.get(rawEvent, "state.buttonevent") / 1000)
);
HKF.ProgrammableSwitchEvent = new Attribute()
.services("Stateless Programmable Switch")
.needAttribute("ServiceLabelIndex")
.needEventMeta("state.buttonevent")
.to((rawEvent, deviceMeta) => {
switch (dotProp.get(rawEvent, "state.buttonevent") % 1000) {
case 1: // Hold Down
return 2; // Long Press
case 2: // Short press
return 0; // Single Press
case 4: // Double press
case 5: // Triple press
case 6: // Quadtruple press
case 10: // Many press
/*
* Merge all many press event to 1 because homekit only support double press events.
*/
return 1; // Double Press
}
});
//#endregion
//#region Sensors
HKF.CurrentTemperature = new Attribute()
.services(["Heater Cooler", "Thermostat", "Temperature Sensor"])
.needEventMeta("state.temperature")
.to(
(rawEvent, deviceMeta) => dotProp.get(rawEvent, "state.temperature") / 100
)
.limit(0, 100);
HKF.CurrentRelativeHumidity = new Attribute()
.services(["Thermostat", "Humidity Sensor"])
.needEventMeta("state.humidity")
.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, "state.humidity") / 100)
.limit(0, 100);
HKF.CurrentAmbientLightLevel = directMap(["to"], "state.lux")
.services("Light Sensor")
.limit(0.0001, 100000);
HKF.SmokeDetected = directMap(["to"], "state.fire").services("Smoke Sensor");
HKF.OutletInUse = new Attribute()
.services("Outlet")
.needEventMeta("state.power")
.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, "state.power") > 0);
HKF.LeakDetected = new Attribute()
.services("Leak Sensor")
.needEventMeta("state.water")
.to((rawEvent, deviceMeta) =>
dotProp.get(rawEvent, "state.water") ? 1 : 0
);
HKF.MotionDetected = directMap(["to"], "state.presence").services(
"Motion Sensor"
);
HKF.ContactSensorState = new Attribute()
.services("Contact Sensor")
.needDeviceMeta((deviceMeta) => {
return !["Window covering controller", "Window covering device"].includes(
deviceMeta.type
);
})
.needEventMeta(
(rawEvent, deviceMeta) =>
dotProp.has(rawEvent, "state.open") ||
dotProp.has(rawEvent, "state.vibration")
)
.to((rawEvent, deviceMeta) => {
if (dotProp.has(rawEvent, "state.vibration")) {
return dotProp.get(rawEvent, "state.vibration") ? 1 : 0;
}
if (dotProp.has(rawEvent, "state.open")) {
return dotProp.get(rawEvent, "state.open") ? 1 : 0;
}
});
//#endregion
//#region ZHAThermostat
HKF.HeatingThresholdTemperature = new Attribute()
.services(["Heater Cooler", "Thermostat"])
.needDeviceMeta({ type: "ZHAThermostat" })
.needEventMeta("config.heatsetpoint")
.to(
(rawEvent, deviceMeta) =>
dotProp.get(rawEvent, "config.heatsetpoint") / 100
)
.from((value, allValues, result, deviceMeta) => {
dotProp.set(result, "config.heatsetpoint", value * 100);
})
.limit(0, 25);
HKF.CoolingThresholdTemperature = new Attribute()
.services(["Heater Cooler", "Thermostat"])
.needDeviceMeta({ type: "ZHAThermostat" })
.needEventMeta("config.coolsetpoint")
.to(
(rawEvent, deviceMeta) =>
dotProp.get(rawEvent, "config.coolsetpoint") / 100
)
.from((value, allValues, result, deviceMeta) => {
dotProp.set(result, "config.coolsetpoint", value * 100);
})
.limit(10, 35);
HKF.TargetTemperature = new Attribute()
.services("Thermostat")
.needDeviceMeta({ type: "ZHAThermostat" })
.needEventMeta(
(rawEvent, deviceMeta) =>
dotProp.has(rawEvent, "config.heatsetpoint") ||
dotProp.has(rawEvent, "config.coolsetpoint")
)
.to((rawEvent, deviceMeta) => {
// Device have only a heatsetpoint.
if (!dotProp.has(rawEvent, "config.coolsetpoint")) {
return dotProp.get(rawEvent, "config.heatsetpoint") / 100;
}
// Device have only a coolsetpoint.
if (!dotProp.has(rawEvent, "config.heatsetpoint")) {
return dotProp.get(rawEvent, "config.coolsetpoint") / 100;
}
// Device have heat and cool set points.
let currentTemp = HKF.CurrentTemperature.toMethod(rawEvent, deviceMeta);
// It's too cold.
if (currentTemp <= dotProp.get(rawEvent, "config.heatsetpoint")) {
return dotProp.get(rawEvent, "config.heatsetpoint") / 100;
}
// It's too hot.
if (currentTemp >= dotProp.get(rawEvent, "config.coolsetpoint")) {
return dotProp.get(rawEvent, "config.coolsetpoint") / 100;
}
// It's in the range I can't determine what the device is doing.
})
.from((value, allValues, result, deviceMeta) => {
if (!dotProp.has(deviceMeta, "config.coolsetpoint")) {
// Device have only a heatsetpoint.
dotProp.set(result, "config.heatsetpoint", value * 100);
} else if (!dotProp.has(deviceMeta, "config.heatsetpoint")) {
// Device have only a coolsetpoint.
dotProp.set(result, "config.coolsetpoint", value * 100);
} else {
// Don't know what to do with that.
}
})
.limit(10, 38);
HKF.Active = new Attribute()
.services("Heater Cooler")
.needDeviceMeta({ type: "ZHAThermostat" })
.needEventMeta("state.on")
.to((rawEvent, deviceMeta) => {
return dotProp.get(rawEvent, "state.on") === true ? 1 : 0;
})
.from((value, allValues, result, deviceMeta) => {
if (value === 1) dotProp.set(result, "state.on", true);
if (value === 0) dotProp.set(result, "state.on", false);
});
HKF.CurrentHeatingCoolingState = new Attribute()
.services("Thermostat")
.needDeviceMeta({ type: "ZHAThermostat" })
.needEventMeta("state.on")
.to((rawEvent, deviceMeta) => {
if (dotProp.get(rawEvent, "state.on") === false) return 0; // Off.
const HKF = {};
//#region Switchs
HKF.ServiceLabelIndex = new Attribute()
.services('Stateless Programmable Switch')
.needAttribute('ProgrammableSwitchEvent')
.needEventMeta('state.buttonevent')
.to((rawEvent, deviceMeta) =>
Math.floor(dotProp.get(rawEvent, 'state.buttonevent') / 1000)
);
HKF.ProgrammableSwitchEvent = new Attribute()
.services('Stateless Programmable Switch')
.needAttribute('ServiceLabelIndex')
.needEventMeta('state.buttonevent')
.to((rawEvent, deviceMeta) => {
switch (dotProp.get(rawEvent, 'state.buttonevent') % 1000) {
case 1: // Hold Down
return 2; // Long Press
case 2: // Short press
return 0; // Single Press
case 4: // Double press
case 5: // Triple press
case 6: // Quadtruple press
case 10: // Many press
/*
* Merge all many press event to 1 because homekit only support double press events.
*/
return 1; // Double Press
}
});
//#endregion
//#region Sensors
HKF.CurrentTemperature = new Attribute()
.services(['Heater Cooler', 'Thermostat', 'Temperature Sensor'])
.needEventMeta('state.temperature')
.to((rawEvent, deviceMeta) =>
dotProp.get(rawEvent, 'state.temperature') / 100
);
HKF.CurrentRelativeHumidity = new Attribute()
.services(['Thermostat', 'Humidity Sensor'])
.needEventMeta('state.humidity')
.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, 'state.humidity') / 100);
HKF.CurrentAmbientLightLevel = directMap(['to'], 'state.lux')
.services('Light Sensor');
HKF.SmokeDetected = directMap(['to'], 'state.fire')
.services('Smoke Sensor');
HKF.OutletInUse = new Attribute()
.services('Outlet')
.needEventMeta('state.power')
.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, 'state.power') > 0);
HKF.LeakDetected = new Attribute()
.services('Leak Sensor')
.needEventMeta('state.water')
.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, 'state.water') ? 1 : 0);
HKF.MotionDetected = directMap(['to'], 'state.presence')
.services('Motion Sensor');
HKF.ContactSensorState = new Attribute()
.services('Contact Sensor')
.needDeviceMeta((deviceMeta) => {
return !['Window covering controller', 'Window covering device'].includes(deviceMeta.type);
})
.needEventMeta((rawEvent, deviceMeta) =>
dotProp.has(rawEvent, 'state.open') ||
dotProp.has(rawEvent, 'state.vibration')
)
.to((rawEvent, deviceMeta) => {
if (dotProp.has(rawEvent, 'state.vibration')) {
return dotProp.get(rawEvent, 'state.vibration') ? 1 : 0;
}
if (dotProp.has(rawEvent, 'state.open')) {
return dotProp.get(rawEvent, 'state.open') ? 1 : 0;
}
});
//#endregion
//#region ZHAThermostat
HKF.HeatingThresholdTemperature = new Attribute()
.services(['Heater Cooler', 'Thermostat'])
.needDeviceMeta({ type: 'ZHAThermostat' })
.needEventMeta('config.heatsetpoint')
.to((rawEvent, deviceMeta) =>
dotProp.get(rawEvent, 'config.heatsetpoint') / 100
)
.from((value, allValues, result, deviceMeta) => {
dotProp.set(result, 'config.heatsetpoint', value * 100);
});
HKF.CoolingThresholdTemperature = new Attribute()
.services(['Heater Cooler', 'Thermostat'])
.needDeviceMeta({ type: 'ZHAThermostat' })
.needEventMeta('config.coolsetpoint')
.to((rawEvent, deviceMeta) =>
dotProp.get(rawEvent, 'config.coolsetpoint') / 100
)
.from((value, allValues, result, deviceMeta) => {
dotProp.set(result, 'config.coolsetpoint', value * 100);
});
HKF.TargetTemperature = new Attribute()
.services('Thermostat')
.needDeviceMeta({ type: 'ZHAThermostat' })
.needEventMeta((rawEvent, deviceMeta) =>
dotProp.has(rawEvent, 'config.heatsetpoint') ||
dotProp.has(rawEvent, 'config.coolsetpoint')
)
.to((rawEvent, deviceMeta) => {
// Device have only a heatsetpoint.
if (!dotProp.has(rawEvent, 'config.coolsetpoint')) {
return dotProp.get(rawEvent, 'config.heatsetpoint') / 100;
}
// Device have only a coolsetpoint.
if (!dotProp.has(rawEvent, 'config.heatsetpoint')) {
return dotProp.get(rawEvent, 'config.coolsetpoint') / 100;
}
// Device have heat and cool set points.
let currentTemp = HKF.CurrentTemperature.toMethod(rawEvent, deviceMeta);
// It's too cold.
if (currentTemp <= dotProp.get(rawEvent, 'config.heatsetpoint')) {
return dotProp.get(rawEvent, 'config.heatsetpoint') / 100;
}
// It's too hot.
if (currentTemp >= dotProp.get(rawEvent, 'config.coolsetpoint')) {
return dotProp.get(rawEvent, 'config.coolsetpoint') / 100;
}
// It's in the range I can't determine what the device is doing.
})
.from((value, allValues, result, deviceMeta) => {
if (!dotProp.has(deviceMeta, 'config.coolsetpoint')) {
// Device have only a heatsetpoint.
dotProp.set(result, 'config.heatsetpoint', value * 100);
} else if (!dotProp.has(deviceMeta, 'config.heatsetpoint')) {
// Device have only a coolsetpoint.
dotProp.set(result, 'config.coolsetpoint', value * 100);
} else {
// Don't know what to do with that.
}
});
HKF.Active = new Attribute()
.services('Heater Cooler')
.needDeviceMeta({ type: 'ZHAThermostat' })
.needEventMeta('state.on')
.to((rawEvent, deviceMeta) => {
return dotProp.get(rawEvent, 'state.on') === true ? 1 : 0;
}).from((value, allValues, result, deviceMeta) => {
if (value === 1) dotProp.set(result, 'state.on', true);
if (value === 0) dotProp.set(result, 'state.on', false);
});
HKF.CurrentHeatingCoolingState = new Attribute()
.services('Thermostat')
.needDeviceMeta({ type: 'ZHAThermostat' })
.needEventMeta('state.on')
.to((rawEvent, deviceMeta) => {
if (dotProp.get(rawEvent, 'state.on') === false) return 0; // Off.
// Device have only a heatsetpoint.
if (
dotProp.has(deviceMeta, "config.heatsetpoint") &&
!dotProp.has(deviceMeta, "config.coolsetpoint")
)
return 1; // Heat. The Heater is currently on
// Device have only a heatsetpoint.
if (dotProp.has(deviceMeta, 'config.heatsetpoint') &&
!dotProp.has(deviceMeta, 'config.coolsetpoint')
) return 1; // Heat. The Heater is currently on
// Device have only a coolsetpoint.
if (
dotProp.has(deviceMeta, "config.coolsetpoint") &&
!dotProp.has(deviceMeta, "config.heatsetpoint")
)
return 2; // Cool. Cooler is currently on
// Device have only a coolsetpoint.
if (dotProp.has(deviceMeta, 'config.coolsetpoint') &&
!dotProp.has(deviceMeta, 'config.heatsetpoint')
) return 2; // Cool. Cooler is currently on
// Device can heat and cool
let targetTemp = HKF.TargetTemperature.toMethod(rawEvent, deviceMeta);
let currentTemp = HKF.CurrentTemperature.toMethod(rawEvent, deviceMeta);
if (targetTemp === undefined || currentTemp === undefined) return;
if (currentTemp < targetTemp) return 1; // Heat. The Heater is currently on
if (currentTemp > targetTemp) return 2; // Cool. Cooler is currently on
});
HKF.TargetHeatingCoolingState = new Attribute()
.services('Thermostat')
.needDeviceMeta({ type: 'ZHAThermostat' })
.needEventMeta('config.mode')
.to((rawEvent, deviceMeta) => {
switch (dotProp.get(rawEvent, 'config.mode')) {
case 'off':
case 'sleep':
case 'fan only':
case 'dry':
return 0; // Off
case 'heat':
case 'emergency heating':
return 1; // Heat
case 'cool':
case 'precooling':
return 2; // Cool
case 'auto':
return 3; // Auto
}
});
HKF.LockPhysicalControls = new Attribute()
.services('Heater Cooler')
.needDeviceMeta({ type: 'ZHAThermostat' })
.needEventMeta('config.locked')
.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, 'config.locked') === true ? 1 : 0)
.from((value, allValues, result, deviceMeta) => {
if (value === 0) dotProp.set(result, 'config.locked', false);
if (value === 1) dotProp.set(result, 'config.locked', true);
});
HKF.TemperatureDisplayUnits_Celsius = new Attribute()
.services(['Heater Cooler', 'Thermostat'])
.name('TemperatureDisplayUnits')
.priority(10)
.needDeviceMeta({ type: 'ZHAThermostat' })
.to((rawEvent, deviceMeta) => 0); // Celsius
HKF.TemperatureDisplayUnits_Fahrenheit = new Attribute()
.services(['Heater Cooler', 'Thermostat'])
.name('TemperatureDisplayUnits')
.priority(0)
.needDeviceMeta({ type: 'ZHAThermostat' })
.to((rawEvent, deviceMeta) => 1); // Fahrenheit
//#endregion
//#region Lights
HKF.On = directMap(['to', 'from'], 'state.on')
.services(['Lightbulb', 'Outlet'])
.needDeviceMeta((deviceMeta) => {
return ![
'Window covering controller',
'Window covering device',
'Door Lock'
].includes(deviceMeta.type);
});
HKF.Brightness = new Attribute()
.services('Lightbulb')
.needEventMeta('state.bri')
.to((rawEvent, deviceMeta) => {
if (dotProp.get(rawEvent, 'state.on') !== true) return;
let bri = dotProp.get(rawEvent, 'state.bri');
return Utils.convertRange(bri, [0, 255], [0, 100], true, true);
})
.from((value, allValues, result, deviceMeta) => {
let bri = Utils.convertRange(value, [0, 100], [0, 255], true, true);
dotProp.set(result, 'state.bri', bri);
dotProp.set(result, 'state.on', bri > 0);
});
HKF.Hue = new Attribute()
.services('Lightbulb')
.needEventMeta('state.hue')
.needColorCapabilities(['hs', 'unknown'])
.needDeviceMeta({ 'state.colormode': 'hs' })
.to((rawEvent, deviceMeta) => {
if (dotProp.get(rawEvent, 'state.on') !== true) return;
let hue = dotProp.get(rawEvent, 'state.hue');
return Utils.convertRange(hue, [0, 65535], [0, 360], true, true);
})
.from((value, allValues, result, deviceMeta) => {
let hue = Utils.convertRange(value, [0, 360], [0, 65535], true, true);
dotProp.set(result, 'state.hue', hue);
});
HKF.Saturation = new Attribute()
.services('Lightbulb')
.needEventMeta('state.sat')
.needColorCapabilities(['hs', 'unknown'])
.needDeviceMeta({ 'state.colormode': 'hs' })
.to((rawEvent, deviceMeta) => {
if (dotProp.get(rawEvent, 'state.on') !== true) return;
let sat = dotProp.get(rawEvent, 'state.sat');
return Utils.convertRange(sat, [0, 255], [0, 100], true, true);
})
.from((value, allValues, result) => {
let sat = Utils.convertRange(value, [0, 100], [0, 255], true, true);
dotProp.set(result, 'state.sat', sat);
});
HKF.ColorTemperature = directMap(['from', 'to'], 'state.ct')
.services('Lightbulb')
.needColorCapabilities(['ct', 'unknown'])
.needDeviceMeta({ 'state.colormode': 'ct' });
//#endregion
//#region Window cover
HKF.CurrentPosition = new Attribute()
.services('Window Covering')
.needEventMeta('state.lift')
.to((rawEvent, deviceMeta) =>
Utils.convertRange(dotProp.get(rawEvent, 'state.lift'), [0, 100], [100, 0], true, true)
).from((value, allValues, result) =>
dotProp.set(result, 'state.lift', Utils.convertRange(value, [100, 0], [0, 100], true, true))
);
HKF.TargetPosition = HKF.CurrentPosition;
HKF.CurrentHorizontalTiltAngle = new Attribute()
.services('Window Covering')
.needDeviceMeta({ type: ['Window covering controller', 'Window covering device'] })
.needEventMeta('state.tilt')
.to((rawEvent, deviceMeta) =>
Utils.convertRange(dotProp.get(rawEvent, 'state.tilt'), [0, 100], [-90, 90], true, true)
).from((value, allValues, result) =>
dotProp.set(result, 'state.tilt', Utils.convertRange(value, [-90, 90], [0, 100], true, true))
);
HKF.TargetHorizontalTiltAngle = HKF.CurrentHorizontalTiltAngle;
HKF.CurrentVerticalTiltAngle = HKF.CurrentHorizontalTiltAngle;
HKF.TargetVerticalTiltAngle = HKF.CurrentHorizontalTiltAngle;
HKF.PositionState = new Attribute()
.services('Window Covering')
.needDeviceMeta({ type: ['Window covering controller', 'Window covering device'] })
.to((rawEvent, deviceMeta) => 2); // Stopped
//#endregion
//#region Battery
HKF.BatteryLevel = directMap(['to'], 'config.battery')
.services('Battery');
HKF.StatusLowBattery = new Attribute()
.services('Battery')
.needEventMeta('config.battery')
.to((rawEvent, deviceMeta) => dotProp.get(rawEvent, 'config.battery') <= 15 ? 1 : 0);
//#endregion
//#region Lock Mechanism
HKF.LockTargetState = new Attribute()
.services('Lock Mechanism')
.needDeviceMeta({ type: 'Door Lock' })
.to((rawEvent, deviceMeta) => {
const map = {
false: 0,
true: 1
};
return map[dotProp.get(deviceMeta, 'state.on')];
})
.from((value, allValues, result) => {
const map = {
0: false,
1: true
};
dotProp.set(result, 'state.on', map[value]);
});
HKF.LockCurrentState = new Attribute()
.services('Lock Mechanism')
.needDeviceMeta({ type: 'Door Lock' })
.needEventMeta('state.on')
.to((rawEvent, deviceMeta) => {
const map = {
false : 0,
true : 1
};
const result = map[dotProp.get(rawEvent, 'state.on')];
return result !== undefined ? result : map.undefined;
});
//#endregion
return HKF;
// Device can heat and cool
let targetTemp = HKF.TargetTemperature.toMethod(rawEvent, deviceMeta);
let currentTemp = HKF.CurrentTemperature.toMethod(rawEvent, deviceMeta);
if (targetTemp === undefined || currentTemp === undefined) return;
if (currentTemp < targetTemp) return 1; // Heat. The Heater is currently on
if (currentTemp > targetTemp) return 2; // Cool. Cooler is currently on
});
HKF.TargetHeatingCoolingState = new Attribute()
.services("Thermostat")
.needDeviceMeta({ type: "ZHAThermostat" })
.needEventMeta("config.mode")
.to((rawEvent, deviceMeta) => {
switch (dotProp.get(rawEvent, "config.mode")) {
case "off":
case "sleep":
case "fan only":
case "dry":
return 0; // Off
case "heat":
case "emergency heating":
return 1; // Heat
case "cool":
case "precooling":
return 2; // Cool
case "auto":
return 3; // Auto
}
});
HKF.LockPhysicalControls = new Attribute()
.services("Heater Cooler")
.needDeviceMeta({ type: "ZHAThermostat" })
.needEventMeta("config.locked")
.to((rawEvent, deviceMeta) =>
dotProp.get(rawEvent, "config.locked") === true ? 1 : 0
)
.from((value, allValues, result, deviceMeta) => {
if (value === 0) dotProp.set(result, "config.locked", false);
if (value === 1) dotProp.set(result, "config.locked", true);
});
HKF.TemperatureDisplayUnits_Celsius = new Attribute()
.services(["Heater Cooler", "Thermostat"])
.name("TemperatureDisplayUnits")
.priority(10)
.needDeviceMeta({ type: "ZHAThermostat" })
.to((rawEvent, deviceMeta) => 0); // Celsius
HKF.TemperatureDisplayUnits_Fahrenheit = new Attribute()
.services(["Heater Cooler", "Thermostat"])
.name("TemperatureDisplayUnits")
.priority(0)
.needDeviceMeta({ type: "ZHAThermostat" })
.to((rawEvent, deviceMeta) => 1); // Fahrenheit
//#endregion
//#region Lights
HKF.On = directMap(["to", "from"], "state.on")
.services(["Lightbulb", "Outlet"])
.needDeviceMeta((deviceMeta) => {
return ![
"Window covering controller",
"Window covering device",
"Door Lock",
].includes(deviceMeta.type);
});
HKF.Brightness = new Attribute()
.services("Lightbulb")
.needEventMeta("state.bri")
.to((rawEvent, deviceMeta) => {
if (dotProp.get(rawEvent, "state.on") !== true) return;
let bri = dotProp.get(rawEvent, "state.bri");
return Utils.convertRange(bri, [0, 255], [0, 100], true, true);
})
.from((value, allValues, result, deviceMeta) => {
let bri = Utils.convertRange(value, [0, 100], [0, 255], true, true);
dotProp.set(result, "state.bri", bri);
dotProp.set(result, "state.on", bri > 0);
});
HKF.Hue = new Attribute()
.services("Lightbulb")
.needEventMeta("state.hue")
.needColorCapabilities(["hs", "unknown"])
.needDeviceMeta({ "state.colormode": "hs" })
.to((rawEvent, deviceMeta) => {
if (dotProp.get(rawEvent, "state.on") !== true) return;
let hue = dotProp.get(rawEvent, "state.hue");
return Utils.convertRange(hue, [0, 65535], [0, 360], true, true);
})
.from((value, allValues, result, deviceMeta) => {
let hue = Utils.convertRange(value, [0, 360], [0, 65535], true, true);
dotProp.set(result, "state.hue", hue);
});
HKF.Saturation = new Attribute()
.services("Lightbulb")
.needEventMeta("state.sat")
.needColorCapabilities(["hs", "unknown"])
.needDeviceMeta({ "state.colormode": "hs" })
.to((rawEvent, deviceMeta) => {
if (dotProp.get(rawEvent, "state.on") !== true) return;
let sat = dotProp.get(rawEvent, "state.sat");
return Utils.convertRange(sat, [0, 255], [0, 100], true, true);
})
.from((value, allValues, result) => {
let sat = Utils.convertRange(value, [0, 100], [0, 255], true, true);
dotProp.set(result, "state.sat", sat);
});
HKF.ColorTemperature = directMap(["from", "to"], "state.ct")
.services("Lightbulb")
.needColorCapabilities(["ct", "unknown"])
.needDeviceMeta({ "state.colormode": "ct" });
//#endregion
//#region Window cover
HKF.CurrentPosition = new Attribute()
.services("Window Covering")
.needEventMeta("state.lift")
.to((rawEvent, deviceMeta) =>
Utils.convertRange(
dotProp.get(rawEvent, "state.lift"),
[0, 100],
[100, 0],
true,
true
)
)
.from((value, allValues, result) =>
dotProp.set(
result,
"state.lift",
Utils.convertRange(value, [100, 0], [0, 100], true, true)
)
);
HKF.TargetPosition = HKF.CurrentPosition;
HKF.CurrentHorizontalTiltAngle = new Attribute()
.services("Window Covering")
.needDeviceMeta({
type: ["Window covering controller", "Window covering device"],
})
.needEventMeta("state.tilt")
.to((rawEvent, deviceMeta) =>
Utils.convertRange(
dotProp.get(rawEvent, "state.tilt"),
[0, 100],
[-90, 90],
true,
true
)
)
.from((value, allValues, result) =>
dotProp.set(
result,
"state.tilt",
Utils.convertRange(value, [-90, 90], [0, 100], true, true)
)
);
HKF.TargetHorizontalTiltAngle = HKF.CurrentHorizontalTiltAngle;
HKF.CurrentVerticalTiltAngle = HKF.CurrentHorizontalTiltAngle;
HKF.TargetVerticalTiltAngle = HKF.CurrentHorizontalTiltAngle;
HKF.PositionState = new Attribute()
.services("Window Covering")
.needDeviceMeta({
type: ["Window covering controller", "Window covering device"],
})
.to((rawEvent, deviceMeta) => 2); // Stopped
//#endregion
//#region Battery
HKF.BatteryLevel = directMap(["to"], "config.battery")
.services("Battery")
.limit(0, 100);
HKF.StatusLowBattery = new Attribute()
.services("Battery")
.needEventMeta("config.battery")
.to((rawEvent, deviceMeta) =>
dotProp.get(rawEvent, "config.battery") <= 15 ? 1 : 0
);
//#endregion
//#region Lock Mechanism
HKF.LockTargetState = new Attribute()
.services("Lock Mechanism")
.needDeviceMeta({ type: "Door Lock" })
.to((rawEvent, deviceMeta) => {
const map = {
false: 0,
true: 1,
};
return map[dotProp.get(deviceMeta, "state.on")];
})
.from((value, allValues, result) => {
const map = {
0: false,
1: true,
};
dotProp.set(result, "state.on", map[value]);
});
HKF.LockCurrentState = new Attribute()
.services("Lock Mechanism")
.needDeviceMeta({ type: "Door Lock" })
.needEventMeta("state.on")
.to((rawEvent, deviceMeta) => {
const map = {
false: 0,
true: 1,
};
const result = map[dotProp.get(rawEvent, "state.on")];
return result !== undefined ? result : map.undefined;
});
//#endregion
return HKF;
})();
class BaseFormatter {
constructor(options = {}) {
this.format = HomeKitFormat;
this.propertyList = Object.keys(HomeKitFormat);
constructor(options = {}) {
this.format = HomeKitFormat;
this.propertyList = Object.keys(HomeKitFormat);
this.options = Object.assign({
attributeWhitelist: [],
attributeBlacklist: [],
}, options);
this.options = Object.assign(
{
attributeWhitelist: [],
attributeBlacklist: [],
},
options
);
this.options.attributeWhitelist = Utils.sanitizeArray(this.options.attributeWhitelist);
if (this.options.attributeWhitelist.length > 0) {
this.propertyList = this.propertyList.filter((property) =>
this.options.attributeWhitelist.includes(property)
);
}
this.options.attributeWhitelist = Utils.sanitizeArray(
this.options.attributeWhitelist
);
if (this.options.attributeWhitelist.length > 0) {
this.propertyList = this.propertyList.filter((property) =>
this.options.attributeWhitelist.includes(property)
);
}
this.options.attributeBlacklist = Utils.sanitizeArray(this.options.attributeBlacklist);
if (this.options.attributeBlacklist.length > 0) {
this.propertyList = this.propertyList.filter((property) =>
!this.options.attributeBlacklist.includes(property)
);
}
this.options.attributeBlacklist = Utils.sanitizeArray(
this.options.attributeBlacklist
);
if (this.options.attributeBlacklist.length > 0) {
this.propertyList = this.propertyList.filter(
(property) => !this.options.attributeBlacklist.includes(property)
);
}
// Sort properties by name
const get = (property, key) => HomeKitFormat[property][key] !== undefined ?
HomeKitFormat[property][key] :
property;
const getName = (property) => get(property, '_name');
const getPriority = (property) => get(property, '_priority');
this.propertyList.sort((propertyA, propertyB) => {
const aName = getName(propertyA);
const bName = getName(propertyB);
if (aName === bName) {
const aPriority = getPriority(propertyA);
const bPriority = getPriority(propertyB);
return (aPriority === undefined || bPriority === undefined) ? 0 : aPriority - bPriority;
} else {
return aName < bName ? -1 : 1;
}
});
}
// Sort properties by name
const get = (property, key) =>
HomeKitFormat[property][key] !== undefined
? HomeKitFormat[property][key]
: property;
const getName = (property) => get(property, "_name");
const getPriority = (property) => get(property, "_priority");
this.propertyList.sort((propertyA, propertyB) => {
const aName = getName(propertyA);
const bName = getName(propertyB);
if (aName === bName) {
const aPriority = getPriority(propertyA);
const bPriority = getPriority(propertyB);
return aPriority === undefined || bPriority === undefined
? 0
: aPriority - bPriority;
} else {
return aName < bName ? -1 : 1;
}
});
}
}
class fromDeconz extends BaseFormatter {
parse(rawEvent, deviceMeta) {
let result = {};
let propertyMap = {};
parse(rawEvent, deviceMeta) {
let result = {};
let propertyMap = {};
for (const property of this.propertyList) {
const propertyName =
HomeKitFormat[property]._name !== undefined
? HomeKitFormat[property]._name
: property;
if (!HomeKitFormat[property].targetIsValid(rawEvent, deviceMeta))
continue;
if (HomeKitFormat[property].toMethod === undefined) continue;
propertyMap[propertyName] = property;
const resultValue = HomeKitFormat[property].toMethod(
rawEvent,
deviceMeta
);
if (resultValue !== undefined) result[propertyName] = resultValue;
}
for (const property of this.propertyList) {
const propertyName = HomeKitFormat[property]._name !== undefined ?
HomeKitFormat[property]._name :
property;
if (!HomeKitFormat[property].targetIsValid(rawEvent, deviceMeta)) continue;
if (HomeKitFormat[property].toMethod === undefined) continue;
propertyMap[propertyName] = property;
const resultValue = HomeKitFormat[property].toMethod(rawEvent, deviceMeta);
if (resultValue !== undefined) result[propertyName] = resultValue;
}
// Cleanup invalid attributes
for (const property of Object.keys(result)) {
if (!HomeKitFormat[propertyMap[property]].attributeIsValid(result)) {
delete result[property];
}
}
return result;
// Cleanup invalid attributes
for (const property of Object.keys(result)) {
if (!HomeKitFormat[propertyMap[property]].attributeIsValid(result)) {
delete result[property];
}
}
getValidPropertiesList(deviceMeta) {
let result = [];
for (const property of this.propertyList) {
if (!HomeKitFormat[property].targetIsValid(deviceMeta, deviceMeta)) continue;
if (HomeKitFormat[property].toMethod === undefined) continue;
result.push(property);
}
return result;
}
// Cleanup invalid attributes
result = result.filter((value) => HomeKitFormat[value].attributeIsValid(result));
return result;
getValidPropertiesList(deviceMeta) {
let result = [];
for (const property of this.propertyList) {
if (!HomeKitFormat[property].targetIsValid(deviceMeta, deviceMeta))
continue;
if (HomeKitFormat[property].toMethod === undefined) continue;
result.push(property);
}
// Cleanup invalid attributes
result = result.filter((value) =>
HomeKitFormat[value].attributeIsValid(result)
);
return result;
}
}
class toDeconz extends BaseFormatter {
parse(values, allValues, result, deviceMeta) {
if (result === undefined) result = {};
for (const [property, value] of Object.entries(values)) {
if (!this.propertyList.includes(property)) continue;
if (HomeKitFormat[property].fromMethod === undefined) continue;
HomeKitFormat[property].fromMethod(value, allValues, result, deviceMeta);
}
parse(values, allValues, result, deviceMeta) {
if (result === undefined) result = {};
for (const [property, value] of Object.entries(values)) {
if (!this.propertyList.includes(property)) continue;
if (HomeKitFormat[property].fromMethod === undefined) continue;
HomeKitFormat[property].fromMethod(value, allValues, result, deviceMeta);
}
for (const property of Object.keys(result)) {
if (result[property] === undefined) {
delete result[property];
}
}
return result;
for (const property of Object.keys(result)) {
if (result[property] === undefined) {
delete result[property];
}
}
return result;
}
}
module.exports = { fromDeconz, toDeconz };

@@ -1,2 +0,2 @@

const dotProp = require('dot-prop');
const dotProp = require("dot-prop");
const Utils = require("./Utils");

@@ -6,325 +6,412 @@ const HomeKitFormatter = require("./HomeKitFormatter");

class OutputMsgFormatter {
constructor(rule, node_type, config) {
this.rule = Object.assign(
{
type: "state",
payload: ["__complete__"],
format: "single",
output: "always",
onstart: true,
onerror: true,
},
rule
);
this.node_type = node_type;
this.config = config;
}
constructor(rule, node_type, config) {
this.rule = Object.assign({
type: 'state',
payload: ["__complete__"],
format: 'single',
output: "always",
onstart: true,
onerror: true
}, rule);
this.node_type = node_type;
this.config = config;
/**
*
* @param devices
* @param rawEvent only for input node
* @param options
*/
getMsgs(devices, rawEvent, options) {
if (!Array.isArray(devices)) devices = [devices];
//console.log({rule: this.rule, config: this.config, devices, rawEvent});
let resultMsgs = [];
// Check if the raw event contains data of the rule type
if (rawEvent !== undefined) {
switch (this.rule.type) {
case "state":
case "config":
if (rawEvent[this.rule.type] === undefined) return resultMsgs;
break;
case "homekit":
if (rawEvent.state === undefined && rawEvent.config === undefined)
return resultMsgs;
break;
}
}
/**
*
* @param devices
* @param rawEvent only for input node
* @param options
*/
getMsgs(devices, rawEvent, options) {
if (!Array.isArray(devices)) devices = [devices];
//console.log({rule: this.rule, config: this.config, devices, rawEvent});
let resultMsgs = [];
let checkOutputMethod;
if (this.node_type === "deconz-input")
checkOutputMethod = this.checkOutputTimeNodeInput;
// Check if the raw event contains data of the rule type
if (rawEvent !== undefined) {
switch (this.rule.type) {
case 'state':
case 'config':
if (rawEvent[this.rule.type] === undefined) return resultMsgs;
break;
case 'homekit':
if (rawEvent.state === undefined && rawEvent.config === undefined) return resultMsgs;
break;
}
let generateMsgPayload = (device_list) => {
let result = {};
let generateOne = (device, payloadFormat) => {
if (
checkOutputMethod === undefined ||
checkOutputMethod.call(this, device, payloadFormat, options)
) {
let msg = this.formatDeviceMsg(
device,
rawEvent,
payloadFormat,
options
);
if (msg === null) return;
if (result[payloadFormat] === undefined) result[payloadFormat] = [];
result[payloadFormat].push(msg);
}
};
let checkOutputMethod;
if (this.node_type === 'deconz-input')
checkOutputMethod = this.checkOutputTimeNodeInput;
for (const device of device_list) {
if (this.rule.type === "homekit") {
generateOne(device, "homekit");
} else if (this.rule.payload.includes("__complete__")) {
generateOne(device, "__complete__");
} else if (this.rule.payload.includes("__each__")) {
for (const payloadFormat of this.getDevicePayloadList(device)) {
generateOne(device, payloadFormat);
}
} else {
for (const payloadFormat of this.rule.payload) {
generateOne(device, payloadFormat);
}
}
}
let generateMsgPayload = (device_list) => {
let result = {};
let generateOne = (device, payloadFormat) => {
if (checkOutputMethod === undefined || checkOutputMethod.call(this, device, payloadFormat, options)) {
let msg = this.formatDeviceMsg(device, rawEvent, payloadFormat, options);
if (msg === null) return;
if (result[payloadFormat] === undefined) result[payloadFormat] = [];
result[payloadFormat].push(msg);
}
};
return result;
};
for (const device of device_list) {
if (this.rule.type === 'homekit') {
generateOne(device, 'homekit');
} else if (this.rule.payload.includes('__complete__')) {
generateOne(device, '__complete__');
} else if (this.rule.payload.includes('__each__')) {
for (const payloadFormat of this.getDevicePayloadList(device)) {
generateOne(device, payloadFormat);
}
let src_msg;
switch (this.rule.format) {
case "single":
for (const [payloadFormat, msgs] of Object.entries(
generateMsgPayload(devices)
)) {
resultMsgs = resultMsgs.concat(msgs);
}
break;
case "array":
src_msg = options.src_msg;
options.src_msg = undefined;
for (const [payloadFormat, msgs] of Object.entries(
generateMsgPayload(devices)
)) {
let msg = this.generateNewMsg(src_msg);
msg.payload_format = payloadFormat;
msg.payload = msgs;
msg.payload_count = msgs.length;
resultMsgs.push(msg);
}
break;
case "average":
case "sum":
case "min":
case "max":
let mergeData;
let mergeMethod;
if (this.rule.format === "average") {
let payloadTotal = {};
mergeData = (
prefix,
targetData,
targetCount,
currentData,
mergeMethod
) => {
for (const [k, v] of Object.entries(currentData)) {
if (k === "device_id") continue;
if (typeof v === "number") {
let count = dotProp.get(targetCount, prefix + k, 0) + 1;
let total = dotProp.get(payloadTotal, prefix + k, 0) + v;
dotProp.set(targetData, prefix + k, total / count);
dotProp.set(targetCount, prefix + k, count);
dotProp.set(payloadTotal, prefix + k, total);
} else if (["state", "config"].includes(k)) {
mergeData(`${k}.`, targetData, targetCount, v, mergeMethod);
}
}
};
} else {
switch (this.rule.format) {
case "sum":
mergeMethod = (a, b) => a + b;
break;
case "min":
mergeMethod = Math.min;
break;
case "max":
mergeMethod = Math.max;
break;
}
mergeData = (
prefix,
targetData,
targetCount,
currentData,
mergeMethod
) => {
for (const [k, v] of Object.entries(currentData)) {
if (k === "device_id") continue;
if (typeof v === "number") {
let currentValue = dotProp.get(targetData, prefix + k);
let value;
if (currentValue !== undefined) {
value = mergeMethod(currentValue, v);
} else {
for (const payloadFormat of this.rule.payload) {
generateOne(device, payloadFormat);
}
value = v;
}
let count = dotProp.get(targetCount, prefix + k, 0) + 1;
dotProp.set(targetData, prefix + k, value);
dotProp.set(targetCount, prefix + k, count);
} else if (["state", "config"].includes(k)) {
mergeData(`${k}.`, targetData, targetCount, v, mergeMethod);
}
}
};
}
src_msg = options.src_msg;
options.src_msg = undefined;
for (const [payloadFormat, msgs] of Object.entries(
generateMsgPayload(devices)
)) {
let msg = this.generateNewMsg(src_msg);
msg.payload = {};
msg.payload_count = {};
msg.payload_format = payloadFormat;
msg.meta = [];
let isSingleValue = false;
for (const data of msgs) {
msg.meta.push(data.meta);
if (
typeof data.payload === "object" &&
!Array.isArray(data.payload)
) {
mergeData(
"",
msg.payload,
msg.payload_count,
data.payload,
mergeMethod
);
} else {
isSingleValue = true;
mergeData(
"",
msg.payload,
msg.payload_count,
{ value: data.payload },
mergeMethod
);
}
}
if (isSingleValue === true) {
msg.payload = msg.payload.value;
msg.payload_count = msg.payload_count.value;
}
resultMsgs.push(msg);
}
break;
}
return result;
};
return resultMsgs;
}
let src_msg;
generateNewMsg(src_msg) {
if (src_msg === undefined) return {};
return Utils.cloneMessage(src_msg, [
"payload",
"payload_format",
"payload_raw",
"meta",
"meta_changed",
]);
}
switch (this.rule.format) {
case 'single':
for (const [payloadFormat, msgs] of Object.entries(generateMsgPayload(devices))) {
resultMsgs = resultMsgs.concat(msgs);
}
break;
case 'array':
src_msg = options.src_msg;
options.src_msg = undefined;
for (const [payloadFormat, msgs] of Object.entries(generateMsgPayload(devices))) {
let msg = this.generateNewMsg(src_msg);
msg.payload_format = payloadFormat;
msg.payload = msgs;
msg.payload_count = msgs.length;
resultMsgs.push(msg);
}
break;
case 'average':
case 'sum':
case 'min':
case 'max':
let mergeData;
let mergeMethod;
if (this.rule.format === 'average') {
let payloadTotal = {};
mergeData = (prefix, targetData, targetCount, currentData, mergeMethod) => {
for (const [k, v] of Object.entries(currentData)) {
if (k === 'device_id') continue;
if (typeof v === 'number') {
let count = dotProp.get(targetCount, prefix + k, 0) + 1;
let total = dotProp.get(payloadTotal, prefix + k, 0) + v;
dotProp.set(targetData, prefix + k, total / count);
dotProp.set(targetCount, prefix + k, count);
dotProp.set(payloadTotal, prefix + k, total);
} else if (['state', 'config'].includes(k)) {
mergeData(`${k}.`, targetData, targetCount, v, mergeMethod);
}
}
};
} else {
switch (this.rule.format) {
case 'sum':
mergeMethod = (a, b) => (a + b);
break;
case 'min':
mergeMethod = Math.min;
break;
case 'max':
mergeMethod = Math.max;
break;
}
mergeData = (prefix, targetData, targetCount, currentData, mergeMethod) => {
for (const [k, v] of Object.entries(currentData)) {
if (k === 'device_id') continue;
if (typeof v === 'number') {
let currentValue = dotProp.get(targetData, prefix + k);
let value;
if (currentValue !== undefined) {
value = mergeMethod(currentValue, v);
} else {
value = v;
}
let count = dotProp.get(targetCount, prefix + k, 0) + 1;
dotProp.set(targetData, prefix + k, value);
dotProp.set(targetCount, prefix + k, count);
} else if (['state', 'config'].includes(k)) {
mergeData(`${k}.`, targetData, targetCount, v, mergeMethod);
}
}
};
}
src_msg = options.src_msg;
options.src_msg = undefined;
for (const [payloadFormat, msgs] of Object.entries(generateMsgPayload(devices))) {
let msg = this.generateNewMsg(src_msg);
msg.payload = {};
msg.payload_count = {};
msg.payload_format = payloadFormat;
msg.meta = [];
let isSingleValue = false;
for (const data of msgs) {
msg.meta.push(data.meta);
if (typeof data.payload === 'object' && !Array.isArray(data.payload)) {
mergeData('', msg.payload, msg.payload_count, data.payload, mergeMethod);
} else {
isSingleValue = true;
mergeData('', msg.payload, msg.payload_count, { value: data.payload }, mergeMethod);
}
}
if (isSingleValue === true) {
msg.payload = msg.payload.value;
msg.payload_count = msg.payload_count.value;
}
resultMsgs.push(msg);
}
break;
}
formatDeviceMsg(device, rawEvent, payloadFormat, options) {
let msg = this.generateNewMsg(options.src_msg);
return resultMsgs;
}
// Filter scene call events
if (
typeof rawEvent === "object" &&
((rawEvent.e === "scene-called" && this.rule.type !== "scene_call") ||
(rawEvent.e !== "scene-called" && this.rule.type === "scene_call"))
)
return null;
generateNewMsg(src_msg) {
if (src_msg === undefined) return {};
return Utils.cloneMessage(src_msg, ['payload', 'payload_format', 'payload_raw', 'meta', 'meta_changed']);
switch (this.rule.type) {
case "attribute":
if (dotProp.has(device, "data"))
msg.payload = this.formatDevicePayload(
device.data,
payloadFormat,
options
);
break;
case "state":
if (dotProp.has(device, "data.state"))
msg.payload = this.formatDevicePayload(
device.data.state,
payloadFormat,
options
);
break;
case "config":
if (dotProp.has(device, "data.config"))
msg.payload = this.formatDevicePayload(
device.data.config,
payloadFormat,
options
);
break;
case "homekit":
if (dotProp.has(device, "data"))
msg = this.formatHomeKit(
device.data,
device.changed,
rawEvent,
options
);
break;
case "scene_call":
if (dotProp.has(device, "data.scenes"))
msg.payload = device.data.scenes
.filter((v) => v.id === rawEvent.scid)
.shift();
break;
}
formatDeviceMsg(device, rawEvent, payloadFormat, options) {
let msg = this.generateNewMsg(options.src_msg);
// If we don't have payload drop the msg
if (msg === null || msg.payload === undefined) return null;
// Filter scene call events
if (typeof rawEvent === 'object' && (
(rawEvent.e === 'scene-called' && this.rule.type !== 'scene_call') ||
(rawEvent.e !== 'scene-called' && this.rule.type === 'scene_call')
)) return null;
if (["deconz-input", "deconz-battery"].includes(this.node_type))
msg.topic = this.config.topic;
if (payloadFormat !== undefined) msg.payload_format = payloadFormat;
if (rawEvent !== undefined) msg.payload_raw = rawEvent;
msg.payload_type = this.rule.type;
msg.meta = device.data;
if (device.changed !== undefined) msg.meta_changed = device.changed;
switch (this.rule.type) {
case 'attribute':
if (dotProp.has(device, 'data'))
msg.payload = this.formatDevicePayload(device.data, payloadFormat, options);
break;
case 'state':
if (dotProp.has(device, 'data.state'))
msg.payload = this.formatDevicePayload(device.data.state, payloadFormat, options);
break;
case 'config':
if (dotProp.has(device, 'data.config'))
msg.payload = this.formatDevicePayload(device.data.config, payloadFormat, options);
break;
case 'homekit':
if (dotProp.has(device, 'data'))
msg = this.formatHomeKit(device.data, device.changed, rawEvent, options);
break;
case 'scene_call':
if (dotProp.has(device, 'data.scenes'))
msg.payload = device.data.scenes.filter((v) => v.id === rawEvent.scid).shift();
break;
}
return msg;
}
// If we don't have payload drop the msg
if (msg === null || msg.payload === undefined) return null;
if (['deconz-input', 'deconz-battery'].includes(this.node_type)) msg.topic = this.config.topic;
if (payloadFormat !== undefined) msg.payload_format = payloadFormat;
if (rawEvent !== undefined) msg.payload_raw = rawEvent;
msg.payload_type = this.rule.type;
msg.meta = device.data;
if (device.changed !== undefined) msg.meta_changed = device.changed;
return msg;
formatDevicePayload(device, payloadFormat, options) {
if (payloadFormat === "__complete__") {
return device;
} else {
return dotProp.get(device, payloadFormat);
}
}
formatDevicePayload(device, payloadFormat, options) {
if (payloadFormat === '__complete__') {
return device;
} else {
return dotProp.get(device, payloadFormat);
}
getDevicePayloadList(device) {
switch (this.rule.type) {
case "attribute":
let list = Object.keys(device.data);
list = list.filter((e) => e !== "state" && e !== "config");
list = list.concat(
Object.keys(device.data.state).map((e) => "state." + e)
);
list = list.concat(
Object.keys(device.data.config).map((e) => "config." + e)
);
return list;
case "state":
case "config":
return Object.keys(device.data[this.rule.type]);
}
}
getDevicePayloadList(device) {
switch (this.rule.type) {
case 'attribute':
let list = Object.keys(device.data);
list = list.filter(e => e !== 'state' && e !== 'config');
list = list.concat(Object.keys(device.data.state).map(e => 'state.' + e));
list = list.concat(Object.keys(device.data.config).map(e => 'config.' + e));
return list;
case 'state':
case 'config':
return Object.keys(device.data[this.rule.type]);
}
checkOutputTimeNodeInput(device, payloadFormat, options) {
// The On start output are priority
if (options.initialEvent === true) return true;
switch (this.rule.output) {
case "always":
return true;
case "onchange":
let payloadPath = payloadFormat;
if (this.rule.type === "state" || this.rule.type === "config")
payloadPath = `${this.rule.type}.${payloadPath}`;
return (
device &&
Array.isArray(device.changed) &&
((payloadFormat === "__complete__" && device.changed.length > 0) ||
(payloadFormat !== "__complete__" &&
device.changed.includes(payloadPath)))
);
case "onupdate":
return (
device &&
Array.isArray(device.changed) &&
device.changed.includes("state.lastupdated")
);
}
}
checkOutputTimeNodeInput(device, payloadFormat, options) {
// The On start output are priority
if (options.initialEvent === true) return true;
formatHomeKit(device, changed, rawEvent, options) {
let node = this;
switch (this.rule.output) {
case 'always':
return true;
case 'onchange':
let payloadPath = payloadFormat;
if (this.rule.type === 'state' || this.rule.type === 'config')
payloadPath = `${this.rule.type}.${payloadPath}`;
return device && Array.isArray(device.changed) && (
(payloadFormat === '__complete__' && device.changed.length > 0) ||
(payloadFormat !== '__complete__' && device.changed.includes(payloadPath))
);
case 'onupdate':
return device && Array.isArray(device.changed) && device.changed.includes('state.lastupdated');
}
// Override rawEvent for initialEvent because in this case rawEvent is an empty object
if (options.initialEvent === true || options.errorEvent === true) {
rawEvent = device;
}
formatHomeKit(device, changed, rawEvent, options) {
let node = this;
let no_reponse = options.errorEvent === true;
// Override rawEvent for initialEvent because in this case rawEvent is an empty object
if (options.initialEvent === true || options.errorEvent === true) {
rawEvent = device;
}
if (
(rawEvent.state !== undefined && rawEvent.state.reachable === false) ||
(rawEvent.config !== undefined && rawEvent.config.reachable === false)
) {
no_reponse = true;
if (this.rule.onerror === false) {
return null;
}
}
let no_reponse = options.errorEvent === true;
let msg = {};
if (
(rawEvent.state !== undefined && rawEvent.state.reachable === false) ||
(rawEvent.config !== undefined && rawEvent.config.reachable === false)
) {
no_reponse = true;
if (this.rule.onerror === false) {
return null;
}
}
let batteryAttributes = ["BatteryLevel", "StatusLowBattery"];
let msg = {};
const opts = {
attributeWhitelist: [],
attributeBlacklist: [],
};
let batteryAttributes = ['BatteryLevel', 'StatusLowBattery'];
if (this.rule.payload.includes("__auto__")) {
opts.attributeBlacklist =
this.node_type === "deconz-input" ? batteryAttributes : [];
opts.attributeWhitelist =
this.node_type === "deconz-battery" ? batteryAttributes : [];
} else {
opts.attributeWhitelist = this.rule.payload;
}
const opts = {
attributeWhitelist: [],
attributeBlacklist: []
};
let characteristic = new HomeKitFormatter.fromDeconz(opts).parse(
rawEvent,
device
);
if (this.rule.payload.includes('__auto__')) {
opts.attributeBlacklist = this.node_type === 'deconz-input' ? batteryAttributes : [];
opts.attributeWhitelist = this.node_type === 'deconz-battery' ? batteryAttributes : [];
} else {
opts.attributeWhitelist = this.rule.payload;
}
if (no_reponse) {
for (const name of Object.keys(characteristic)) {
characteristic[name] = "NO_RESPONSE";
}
}
let characteristic = (new HomeKitFormatter.fromDeconz(opts)).parse(rawEvent, device);
if (dotProp.has(device, "state.lastupdated")) {
msg.lastupdated = dotProp.get(device, "state.lastupdated");
}
if (no_reponse) {
for (const name of Object.keys(characteristic)) {
characteristic[name] = 'NO_RESPONSE';
}
}
if (Object.keys(characteristic).length === 0) return null; //empty response
if (dotProp.has(device, 'state.lastupdated')) {
msg.lastupdated = dotProp.get(device, 'state.lastupdated');
}
if (Object.keys(characteristic).length === 0) return null; //empty response
msg.payload = characteristic;
return msg;
}
msg.payload = characteristic;
return msg;
}
}
module.exports = OutputMsgFormatter;
module.exports = OutputMsgFormatter;

@@ -1,465 +0,483 @@

const dotProp = require('dot-prop');
const compareVersion = require('compare-versions');
const dotProp = require("dot-prop");
const compareVersion = require("compare-versions");
const getRuleConstructor = (rule) => {
if (rule === 'all') return RuleAlways;
if (rule === 'none') return RuleNever;
if (typeof rule !== 'object') throw Error("A rule should be an object. Got : " + rule.toString());
if (rule === "all") return RuleAlways;
if (rule === "none") return RuleNever;
if (typeof rule !== "object")
throw Error("A rule should be an object. Got : " + rule.toString());
switch (rule.type) {
case 'basic':
return RuleBasic;
case 'match':
return RuleMatch;
case 'queries':
return RuleQueries;
case undefined:
// Detect rule type
if (rule.device_type || rule.device_id || rule.device_path || rule.uniqueid) {
return RuleBasic;
} else if (rule.match) {
return RuleMatch;
} else if (rule.queries) {
return RuleQueries;
}
return RuleNever;
default:
throw Error("Invalid rule type provided. Got : " + rule.toString());
}
switch (rule.type) {
case "basic":
return RuleBasic;
case "match":
return RuleMatch;
case "queries":
return RuleQueries;
case undefined:
// Detect rule type
if (
rule.device_type ||
rule.device_id ||
rule.device_path ||
rule.uniqueid
) {
return RuleBasic;
} else if (rule.match) {
return RuleMatch;
} else if (rule.queries) {
return RuleQueries;
}
return RuleNever;
default:
throw Error("Invalid rule type provided. Got : " + rule.toString());
}
};
const getComparaisonConstructor = (field, value) => {
switch (typeof value) {
case "undefined":
case "boolean":
case "number":
case "bigint":
case "string":
return ComparaisonStrictEqual;
case "object":
if (Array.isArray(value)) {
return ComparaisonArray;
} else {
if (value.type === undefined) {
if (value.value !== undefined) value.type = 'complex';
if (value.after !== undefined || value.before !== undefined) value.type = 'date';
if (value.regex !== undefined) value.type = 'regex';
if (value.version !== undefined) value.type = 'version';
}
switch (typeof value) {
case "undefined":
case "boolean":
case "number":
case "bigint":
case "string":
return ComparaisonStrictEqual;
case "object":
if (Array.isArray(value)) {
return ComparaisonArray;
} else {
if (value.type === undefined) {
if (value.value !== undefined) value.type = "complex";
if (value.after !== undefined || value.before !== undefined)
value.type = "date";
if (value.regex !== undefined) value.type = "regex";
if (value.version !== undefined) value.type = "version";
}
switch (value.type) {
case 'complex':
return ComparaisonComplex;
case 'date':
return ComparaisonDate;
case 'regex':
return ComparaisonRegex;
case 'version':
return ComparaisonVersion;
switch (value.type) {
case "complex":
return ComparaisonComplex;
case "date":
return ComparaisonDate;
case "regex":
return ComparaisonRegex;
case "version":
return ComparaisonVersion;
default:
throw Error("Invalid comparaison type provided. Got : " + (typeof value.type).toString());
}
}
break;
default:
throw Error("Invalid comparaison type provided. Got : " + (typeof value).toString());
}
default:
throw Error(
"Invalid comparaison type provided. Got : " +
(typeof value.type).toString()
);
}
}
break;
default:
throw Error(
"Invalid comparaison type provided. Got : " + (typeof value).toString()
);
}
};
class Query {
constructor(query, depth) {
this.depth = depth + 1 || 0;
if (this.depth > 10) {
throw Error("Query depth limit reached.");
}
// Make sure that the query is an array
if (Array.isArray(query)) this.query = query;
else this.query = [query];
// Create rule
this.createRules(this.query);
constructor(query, depth) {
this.depth = depth + 1 || 0;
if (this.depth > 10) {
throw Error("Query depth limit reached.");
}
// Make sure that the query is an array
if (Array.isArray(query)) this.query = query;
else this.query = [query];
match(device) {
return this.rules.every((rule) => rule.match(device));
}
// Create rule
this.createRules(this.query);
}
createRules(rules) {
this.rules = [];
for (const rule of rules) {
let constructor = getRuleConstructor(rule);
this.rules.push(new constructor(rule, this.depth));
}
match(device) {
return this.rules.every((rule) => rule.match(device));
}
createRules(rules) {
this.rules = [];
for (const rule of rules) {
let constructor = getRuleConstructor(rule);
this.rules.push(new constructor(rule, this.depth));
}
}
}
class Rule {
constructor(options, depth) {
this.options = Object.assign({}, this.defaultOptions, options);
this.depth = depth;
this.comparaisons = [];
constructor(options, depth) {
this.options = Object.assign({}, this.defaultOptions, options);
this.depth = depth;
this.comparaisons = [];
if (this.options.method) {
switch (this.options.method.toUpperCase()) {
case 'AND':
case '&&':
this.matchFunction = this.options.inverted === true ? this.matchAndInverted : this.matchAnd;
break;
case 'OR':
case '||':
this.matchFunction = this.options.inverted === true ? this.matchOrInverted : this.matchOr;
break;
default:
throw Error(`Invalid match method expected 'AND' or 'OR' and got ${this.options.method}`);
}
}
if (this.options.method) {
switch (this.options.method.toUpperCase()) {
case "AND":
case "&&":
this.matchFunction =
this.options.inverted === true
? this.matchAndInverted
: this.matchAnd;
break;
case "OR":
case "||":
this.matchFunction =
this.options.inverted === true
? this.matchOrInverted
: this.matchOr;
break;
default:
throw Error(
`Invalid match method expected 'AND' or 'OR' and got ${this.options.method}`
);
}
}
}
get defaultOptions() {
return {};
}
get defaultOptions() {
return {};
}
match(device) {
return this.matchFunction(device);
}
match(device) {
return this.matchFunction(device);
}
matchAnd(device) {
return this.comparaisons.every((key) => {
return key.match(device);
});
}
matchAnd(device) {
return this.comparaisons.every((key) => {
return key.match(device);
});
}
matchAndInverted(device) {
return !this.matchAnd(device);
}
matchAndInverted(device) {
return !this.matchAnd(device);
}
matchOr(device) {
return this.comparaisons.some((key) => {
return key.match(device);
});
}
matchOr(device) {
return this.comparaisons.some((key) => {
return key.match(device);
});
}
matchOrInverted(device) {
return !this.matchOr(device);
}
matchOrInverted(device) {
return !this.matchOr(device);
}
throw(message) {
throw Error(message);
}
throw(message) {
throw Error(message);
}
}
class RuleNever extends Rule {
match(device) {
return this.options.inverted === true;
}
match(device) {
return this.options.inverted === true;
}
}
class RuleAlways extends Rule {
match(device) {
return this.options.inverted !== true;
}
match(device) {
return this.options.inverted !== true;
}
}
class RuleBasic extends Rule {
constructor(options, depth) {
super(options, depth);
let acceptedKeys = ["device_type", "device_id", "device_path", "uniqueid"];
constructor(options, depth) {
super(options, depth);
let acceptedKeys = [
'device_type',
'device_id',
'device_path',
'uniqueid'
];
this.keys = Object.keys(this.options).filter((key) => {
return acceptedKeys.includes(key);
});
this.keys = Object.keys(this.options).filter((key) => {
return acceptedKeys.includes(key);
});
if (this.keys.length === 0) {
this.throw(`Invalid query, the Basic rule expect at least one of the values ${acceptedKeys.join(',')}.`);
}
if (this.keys.length === 0) {
this.throw(
`Invalid query, the Basic rule expect at least one of the values ${acceptedKeys.join(
","
)}.`
);
}
}
match(device) {
return this.keys.every((key) => {
return (key === 'type') || this.options[key] === device[key];
});
}
match(device) {
return this.keys.every((key) => {
return key === "type" || this.options[key] === device[key];
});
}
}
class RuleMatch extends Rule {
constructor(options, depth) {
super(options, depth);
constructor(options, depth) {
super(options, depth);
this.createComparaisons(options.match);
}
get defaultOptions() {
return {
inverted: false,
method: "AND",
};
}
this.createComparaisons(options.match);
createComparaisons(comparaisons) {
if (comparaisons === undefined) throw Error("No match data found");
for (const [field, value] of Object.entries(comparaisons)) {
let constructor = getComparaisonConstructor(field, value);
this.comparaisons.push(new constructor(field, value));
}
get defaultOptions() {
return {
inverted: false,
method: 'AND'
};
}
createComparaisons(comparaisons) {
if (comparaisons === undefined) throw Error('No match data found');
for (const [field, value] of Object.entries(comparaisons)) {
let constructor = getComparaisonConstructor(field, value);
this.comparaisons.push(new constructor(field, value));
}
}
}
}
class RuleQueries extends Rule {
constructor(options, depth) {
super(options, depth);
this.createComparaisons(options.queries);
}
constructor(options, depth) {
super(options, depth);
this.createComparaisons(options.queries);
}
get defaultOptions() {
return {
inverted: false,
method: "AND",
};
}
get defaultOptions() {
return {
inverted: false,
method: 'AND'
};
createComparaisons(queries) {
if (queries === undefined) throw Error("No match data found");
if (!Array.isArray(queries)) queries = [queries];
for (const query of queries) {
this.comparaisons.push(new Query(query, this.depth));
}
createComparaisons(queries) {
if (queries === undefined) throw Error('No match data found');
if (!Array.isArray(queries)) queries = [queries];
for (const query of queries) {
this.comparaisons.push(new Query(query, this.depth));
}
}
}
}
class Comparaison {
constructor(field, value) {
this.field = field;
this.target = value;
}
constructor(field, value) {
this.field = field;
this.target = value;
}
/**
* Check if the device match the rule.
* @abstract
* @param device
* @return {boolean}
*/
match(device) {
throw new Error('Rule match method called, this should not happen.');
}
/**
* Check if the device match the rule.
* @abstract
* @param device
* @return {boolean}
*/
match(device) {
throw new Error("Rule match method called, this should not happen.");
}
}
class ComparaisonStrictEqual extends Comparaison {
match(device) {
return dotProp.get(device, this.field) === this.target;
}
match(device) {
return dotProp.get(device, this.field) === this.target;
}
}
class ComparaisonArray extends Comparaison {
match(device) {
return this.target.includes(dotProp.get(device, this.field));
}
match(device) {
return this.target.includes(dotProp.get(device, this.field));
}
}
class ComparaisonComplex extends Comparaison {
constructor(field, value) {
super(field, value);
constructor(field, value) {
super(field, value);
if (
(value.convertLeft !== undefined || value.convertRight !== undefined) &&
value.convertTo === undefined
) {
throw Error(
`You ask for convertion but do not provide any conversion method.`
);
}
if ((value.convertLeft !== undefined || value.convertRight !== undefined) && value.convertTo === undefined) {
throw Error(`You ask for convertion but do not provide any conversion method.`);
}
this.target = Array.isArray(value.value) ? value.value : [value.value];
this.target = Array.isArray(value.value) ? value.value : [value.value];
if (value.convertTo) {
let conversionMethod = this.getConvertionMethod(value.convertTo);
if (value.convertTo) {
let conversionMethod = this.getConvertionMethod(value.convertTo);
if (value.convertRight === true) {
this.target = this.target.map((target) =>
target !== undefined ? conversionMethod(target) : target
);
}
if (value.convertRight === true) {
this.target = this.target.map(target => target !== undefined ? conversionMethod(target) : target);
}
if (value.convertLeft === true) {
this.conversionMethod = conversionMethod;
}
}
if (value.strict === true) {
this.strictCompareTo = this.target.map(target => typeof target);
this.matchMethod = this.strictMatch;
} else {
this.matchMethod = this.notStrictMatch;
}
this.operator = this.getOperatorMethod(value.operator);
if (value.convertLeft === true) {
this.conversionMethod = conversionMethod;
}
}
getConvertionMethod(target) {
if (target && typeof target === 'string') {
switch (target.toLowerCase()) {
case 'boolean':
return Boolean;
case 'number':
return Number;
case 'string':
return String;
case 'date':
return Date.parse;
}
}
if (value.strict === true) {
this.strictCompareTo = this.target.map((target) => typeof target);
this.matchMethod = this.strictMatch;
} else {
this.matchMethod = this.notStrictMatch;
}
getOperatorMethod(operator) {
switch (operator) {
case '===':
case undefined:
return (a, b) => a === b;
case '!==':
return (a, b) => a !== b;
case '==':
// noinspection EqualityComparisonWithCoercionJS
return (a, b) => a == b;
case '!=':
// noinspection EqualityComparisonWithCoercionJS
return (a, b) => a != b;
case '>':
return (a, b) => a > b;
case '>=':
return (a, b) => a >= b;
case '<':
return (a, b) => a < b;
case '<=':
return (a, b) => a <= b;
default:
throw Error(`Invalid operator, got ${operator}`);
}
}
this.operator = this.getOperatorMethod(value.operator);
}
strictMatch(value) {
return this.target.some((target, index) => {
if (this.strictCompareTo[index] !== undefined && this.strictCompareTo[index] !== typeof value) return false;
return this.operator(value, target);
});
getConvertionMethod(target) {
if (target && typeof target === "string") {
switch (target.toLowerCase()) {
case "boolean":
return Boolean;
case "number":
return Number;
case "string":
return String;
case "date":
return Date.parse;
}
}
}
notStrictMatch(value) {
return this.target.some((target) => {
return this.operator(value, target);
});
getOperatorMethod(operator) {
switch (operator) {
case "===":
case undefined:
return (a, b) => a === b;
case "!==":
return (a, b) => a !== b;
case "==":
// noinspection EqualityComparisonWithCoercionJS
return (a, b) => a == b;
case "!=":
// noinspection EqualityComparisonWithCoercionJS
return (a, b) => a != b;
case ">":
return (a, b) => a > b;
case ">=":
return (a, b) => a >= b;
case "<":
return (a, b) => a < b;
case "<=":
return (a, b) => a <= b;
default:
throw Error(`Invalid operator, got ${operator}`);
}
}
match(device) {
let value = dotProp.get(device, this.field);
if (this.conversionMethod !== undefined) value = this.conversionMethod(value);
return this.matchMethod(value);
}
strictMatch(value) {
return this.target.some((target, index) => {
if (
this.strictCompareTo[index] !== undefined &&
this.strictCompareTo[index] !== typeof value
)
return false;
return this.operator(value, target);
});
}
notStrictMatch(value) {
return this.target.some((target) => {
return this.operator(value, target);
});
}
match(device) {
let value = dotProp.get(device, this.field);
if (this.conversionMethod !== undefined)
value = this.conversionMethod(value);
return this.matchMethod(value);
}
}
class ComparaisonDate extends Comparaison {
constructor(field, value) {
super(field, value);
for (const side of ['before', 'after']) {
switch (typeof value[side]) {
case 'string':
let result = Date.parse(value[side]);
if (Number.isNaN(result)) {
throw Error(`Invalid value provided for date comparaison, can't parse '${typeof value[side]}'`);
} else {
this[side] = result;
this.valid = true;
}
break;
case 'number':
this[side] = value[side];
this.valid = true;
break;
case 'undefined':
break;
default:
throw Error(`Invalid value type provided for date comparaison, got '${typeof value[side]}' and expect 'string' or 'number'`);
}
}
constructor(field, value) {
super(field, value);
for (const side of ["before", "after"]) {
switch (typeof value[side]) {
case "string":
let result = Date.parse(value[side]);
if (Number.isNaN(result)) {
throw Error(
`Invalid value provided for date comparaison, can't parse '${typeof value[
side
]}'`
);
} else {
this[side] = result;
this.valid = true;
}
break;
case "number":
this[side] = value[side];
this.valid = true;
break;
case "undefined":
break;
default:
throw Error(
`Invalid value type provided for date comparaison, got '${typeof value[
side
]}' and expect 'string' or 'number'`
);
}
}
}
match(device) {
if (this.valid !== true) return false;
let value = dotProp.get(device, this.field);
if (value === undefined) return false;
if (typeof value === 'string') value = Date.parse(value);
return !(
Number.isNaN(value) ||
this.after !== undefined && value < this.after ||
this.before !== undefined && value > this.before
);
}
match(device) {
if (this.valid !== true) return false;
let value = dotProp.get(device, this.field);
if (value === undefined) return false;
if (typeof value === "string") value = Date.parse(value);
return !(
Number.isNaN(value) ||
(this.after !== undefined && value < this.after) ||
(this.before !== undefined && value > this.before)
);
}
}
class ComparaisonRegex extends Comparaison {
constructor(field, value) {
super(field, value);
if (typeof value.regex !== "string") return;
this.patt = new RegExp(value.regex, value.flag);
}
constructor(field, value) {
super(field, value);
if (typeof value.regex !== 'string') return;
this.patt = new RegExp(value.regex, value.flag);
}
match(device) {
if (this.patt === undefined) return false;
this.patt.lastIndex = 0;
return this.patt.test(dotProp.get(device, this.field));
}
match(device) {
if (this.patt === undefined) return false;
this.patt.lastIndex = 0;
return this.patt.test(dotProp.get(device, this.field));
}
}
class ComparaisonVersion extends Comparaison {
constructor(field, value) {
super(field, value);
constructor(field, value) {
super(field, value);
this.version = compareVersion.validate(value.version) ? value.version : undefined;
switch (value.operator) {
case "===":
case "==":
this.operator = "=";
break;
case "!==":
case "!=":
this.operator = "!=";
break;
case undefined:
this.operator = '>=';
break;
default:
this.operator = value.operator;
break;
}
this.version = compareVersion.validate(value.version)
? value.version
: undefined;
switch (value.operator) {
case "===":
case "==":
this.operator = "=";
break;
case "!==":
case "!=":
this.operator = "!=";
break;
case undefined:
this.operator = ">=";
break;
default:
this.operator = value.operator;
break;
}
}
match(device) {
let value = String(dotProp.get(device, this.field));
if (this.version === undefined || compareVersion.validate(value) === false) return false;
if (this.operator === '!=') return value !== this.version;
return compareVersion.compare(value, this.version, this.operator);
}
match(device) {
let value = String(dotProp.get(device, this.field));
if (this.version === undefined || compareVersion.validate(value) === false)
return false;
if (this.operator === "!=") return value !== this.version;
return compareVersion.compare(value, this.version, this.operator);
}
}
module.exports = Query;
module.exports = Query;

@@ -6,141 +6,157 @@ const REDUtil = require("@node-red/util/lib/util");

class Utils {
static sleep(ms, defaultValue) {
if (typeof ms !== 'number') ms = defaultValue;
return new Promise((resolve) => setTimeout(() => resolve(), ms));
}
static sleep(ms, defaultValue) {
if (typeof ms !== "number") ms = defaultValue;
return new Promise((resolve) => setTimeout(() => resolve(), ms));
}
static cloneMessage(message_in, moveData) {
if (moveData === undefined) moveData = [];
if (!Array.isArray(moveData)) moveData = [moveData];
let msg = REDUtil.cloneMessage(message_in);
for (const key of moveData) {
if (msg[key] !== undefined) {
msg[key + '_in'] = msg[key];
delete msg[key];
}
}
return msg;
static cloneMessage(message_in, moveData) {
if (moveData === undefined) moveData = [];
if (!Array.isArray(moveData)) moveData = [moveData];
let msg = REDUtil.cloneMessage(message_in);
for (const key of moveData) {
if (msg[key] !== undefined) {
msg[key + "_in"] = msg[key];
delete msg[key];
}
}
return msg;
}
static 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);
}
static 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
);
}
static convertRange(value, r1, r2, roundValue = false, limitValue = false) {
if (typeof value !== 'number') return;
if (limitValue) {
if (r1[0] < r1[1]) {
if (value < r1[0]) value = r1[0];
if (value > r1[1]) value = r1[1];
} else {
if (value > r1[0]) value = r1[0];
if (value < r1[1]) value = r1[1];
}
}
let result = (value - r1[0]) * (r2[1] - r2[0]) / (r1[1] - r1[0]) + r2[0];
return roundValue ? Math.ceil(result) : result;
static convertRange(value, r1, r2, roundValue = false, limitValue = false) {
if (typeof value !== "number") return;
if (limitValue) {
if (r1[0] < r1[1]) {
if (value < r1[0]) value = r1[0];
if (value > r1[1]) value = r1[1];
} else {
if (value > r1[0]) value = r1[0];
if (value < r1[1]) value = r1[1];
}
}
let result = ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0];
return roundValue ? Math.ceil(result) : result;
}
static isDeviceCover(device) {
if (typeof device !== 'object') return;
return device.type === 'Window covering controller' ||
device.type === 'Window covering device';
}
static isDeviceCover(device) {
if (typeof device !== "object") return;
return (
device.type === "Window covering controller" ||
device.type === "Window covering device"
);
}
static clone(object) {
return Object.assign({}, object);
}
static clone(object) {
return Object.assign({}, object);
}
static sanitizeArray(value) {
if (value === undefined || value === null) return [];
if (!Array.isArray(value)) return [value];
return value;
}
static sanitizeArray(value) {
if (value === undefined || value === null) return [];
if (!Array.isArray(value)) return [value];
return value;
}
static isIPAddress(address) {
const ipv4RegexExp = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gi;
const ipv6RegexExp = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/gi;
return ipv4RegexExp.test(address) || ipv6RegexExp.test(address);
static isIPAddress(address) {
const ipv4RegexExp =
/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gi;
const ipv6RegexExp =
/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/gi;
return ipv4RegexExp.test(address) || ipv6RegexExp.test(address);
}
static async waitForReady(target, maxDelay = 10000, pauseDelay = 100) {
let pauseCount = 0;
while (target.ready === false) {
await Utils.sleep(pauseDelay);
pauseCount++;
if (pauseCount * pauseDelay >= maxDelay) {
break;
}
}
}
static async waitForReady(target, maxDelay = 10000, pauseDelay = 100) {
let pauseCount = 0;
while (target.ready === false) {
await Utils.sleep(pauseDelay);
pauseCount++;
if (pauseCount * pauseDelay >= maxDelay) {
break;
}
}
static async waitForEverythingReady(node) {
if (node.config.statustext_type === "auto")
clearTimeout(node.cleanStatusTimer);
// Wait until the server is ready
if (node.server.ready === false) {
node.status({
fill: "yellow",
shape: "dot",
text: "node-red-contrib-deconz/server:status.wait_for_server_start",
});
await this.waitForReady(node.server.state, 30000);
if (node.server.ready === false) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.server_node_error",
});
console.error(
"Timeout, the server node is not ready after 30 seconds."
);
return "node-red-contrib-deconz/server:status.server_node_error";
} else {
node.status({});
}
}
static async waitForEverythingReady(node) {
if (node.config.statustext_type === 'auto')
clearTimeout(node.cleanStatusTimer);
// Wait until the server is ready
if (node.server.ready === false) {
node.status({
fill: "yellow",
shape: "dot",
text: "node-red-contrib-deconz/server:status.wait_for_server_start"
});
await this.waitForReady(node.server.state, 30000);
if (node.server.ready === false) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.server_node_error"
});
console.error('Timeout, the server node is not ready after 30 seconds.');
return "node-red-contrib-deconz/server:status.server_node_error";
} else {
node.status({});
}
}
await this.waitForReady(node, 30000);
await this.waitForReady(node, 30000);
if (node.ready === false) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.node_error"
});
console.error('Timeout, the node is not ready after 30 seconds.');
return "node-red-contrib-deconz/server:status.node_error";
}
if (node.ready === false) {
node.status({
fill: "red",
shape: "dot",
text: "node-red-contrib-deconz/server:status.node_error",
});
console.error("Timeout, the node is not ready after 30 seconds.");
return "node-red-contrib-deconz/server:status.node_error";
}
}
static convertColorCapabilities(mask) {
let result = [];
if (mask === 0) result.push('unknown');
if ((mask & 0x1) === 0x1 || (mask & 0x2) === 0x2) result.push('hs');
if ((mask & 0x8) === 0x8) result.push('xy');
if ((mask & 0x4) === 0x4) result.push('effect');
if ((mask & 0x10) === 0x10) result.push('ct');
return result;
}
static convertColorCapabilities(mask) {
let result = [];
if (mask === 0) result.push("unknown");
if ((mask & 0x1) === 0x1 || (mask & 0x2) === 0x2) result.push("hs");
if ((mask & 0x8) === 0x8) result.push("xy");
if ((mask & 0x4) === 0x4) result.push("effect");
if ((mask & 0x10) === 0x10) result.push("ct");
return result;
}
static supportColorCapability(deviceMeta, value) {
let deviceCapabilities = Utils.convertColorCapabilities(deviceMeta.colorcapabilities);
if (deviceMeta.colorcapabilities === 0) return deviceCapabilities.includes('unknown');
let values = Utils.sanitizeArray(value);
for (const v of values) {
if (deviceCapabilities.includes(v)) return true;
}
return false;
static supportColorCapability(deviceMeta, value) {
let deviceCapabilities = Utils.convertColorCapabilities(
deviceMeta.colorcapabilities
);
if (deviceMeta.colorcapabilities === 0)
return deviceCapabilities.includes("unknown");
let values = Utils.sanitizeArray(value);
for (const v of values) {
if (deviceCapabilities.includes(v)) return true;
}
return false;
}
static convertLightsValues(deviceMeta) {
if (deviceMeta.colorcapabilities !== undefined) {
deviceMeta.device_colorcapabilities = Utils.convertColorCapabilities(deviceMeta.colorcapabilities);
}
static convertLightsValues(deviceMeta) {
if (deviceMeta.colorcapabilities !== undefined) {
deviceMeta.device_colorcapabilities = Utils.convertColorCapabilities(
deviceMeta.colorcapabilities
);
}
}
}
module.exports = Utils;

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc