node-red-contrib-deconz
Advanced tools
Comparing version 2.3.5 to 2.3.6
@@ -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(); | ||
} | ||
}); | ||
}; |
259
nodes/api.js
@@ -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); | ||
}; |
299
nodes/get.js
@@ -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); | ||
}; |
284
nodes/in.js
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); | ||
}; |
653
nodes/out.js
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); | ||
}; | ||
1591
nodes/server.js
@@ -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(" ")),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(` → <span class="node-input-rule-index">${this.index+1}</span> `).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(" ")),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(` → <span class="node-input-rule-index">${this.index+1}</span> `).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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
7
8773
527434