node-red-contrib-deconz
Advanced tools
Comparing version 1.3.3 to 2.0.0-beta.1
142
deconz.js
@@ -1,11 +0,13 @@ | ||
var request = require('request'); | ||
var NODE_PATH = '/deconz/'; | ||
const NODE_PATH = '/node-red-contrib-deconz/'; | ||
const path = require('path'); | ||
const ConfigMigration = require("./src/migration/ConfigMigration"); | ||
module.exports = function (RED) { | ||
/** | ||
* Enable http route to static files | ||
* Enable http route to multiple-select static files | ||
*/ | ||
RED.httpAdmin.get(NODE_PATH + 'static/*', function (req, res) { | ||
var options = { | ||
root: __dirname + '/static/', | ||
RED.httpAdmin.get(NODE_PATH + 'multiple-select/*', function (req, res) { | ||
let options = { | ||
root: path.dirname(require.resolve('multiple-select')), | ||
dotfiles: 'deny' | ||
@@ -16,3 +18,2 @@ }; | ||
/** | ||
@@ -22,37 +23,104 @@ * 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) { | ||
var config = req.query; | ||
var controller = RED.nodes.getNode(config.controllerID); | ||
var forceRefresh = config.forceRefresh ? ['1', 'yes', 'true'].includes(config.forceRefresh.toLowerCase()) : false; | ||
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") { | ||
controller.getItemsList(function (items, groups) { | ||
if (items) { | ||
res.json({items: items, groups: groups}); | ||
} else { | ||
res.status(404).end(); | ||
(async () => { | ||
if (forceRefresh) await controller.discoverDevices({forceRefresh: true}); | ||
try { | ||
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 | ||
}); | ||
} | ||
}, forceRefresh); | ||
})(); | ||
} else { | ||
res.status(404).end(); | ||
return res.json({ | ||
error_message: "Can't find the server node. Did you press deploy ?" | ||
}); | ||
} | ||
}); | ||
RED.httpAdmin.get(NODE_PATH + 'statelist', function (req, res) { | ||
var config = req.query; | ||
var controller = RED.nodes.getNode(config.controllerID); | ||
if (controller && controller.constructor.name === "ServerNode") { | ||
var item = controller.getDevice(config.uniqueid); | ||
if (item) { | ||
res.json(item.state); | ||
['attribute', 'state', 'config'].forEach(function (type) { | ||
RED.httpAdmin.get(NODE_PATH + type + 'list', function (req, res) { | ||
let config = req.query; | ||
let controller = RED.nodes.getNode(config.controllerID); | ||
let devicesIDs = JSON.parse(config.devices); | ||
const isAttribute = type === 'attribute'; | ||
if (controller && controller.constructor.name === "ServerNode" && devicesIDs) { | ||
let type_list = (isAttribute) ? ['state', 'config'] : [type]; | ||
let sample = {}; | ||
let count = {}; | ||
for (const _type of type_list) { | ||
sample[_type] = {}; | ||
count[_type] = {}; | ||
} | ||
if (isAttribute) { | ||
sample[type] = {}; | ||
count[type] = {}; | ||
} | ||
for (const deviceID of devicesIDs) { | ||
let device = controller.device_list.getDeviceByPath(deviceID); | ||
if (!device) continue; | ||
if (isAttribute) { | ||
for (const value of Object.keys(device)) { | ||
if (type_list.includes(value)) continue; | ||
count[type][value] = (count[type][value] || 0) + 1; | ||
sample[type][value] = device[value]; | ||
} | ||
} | ||
for (const _type of type_list) { | ||
if (!device[_type]) continue; | ||
for (const value of Object.keys(device[_type])) { | ||
count[_type][value] = (count[_type][value] || 0) + 1; | ||
sample[_type][value] = device[_type][value]; | ||
} | ||
} | ||
} | ||
res.json({count: count, sample: sample}); | ||
} else { | ||
res.status(404).end(); | ||
} | ||
} else { | ||
res.status(404).end(); | ||
} | ||
}); | ||
}); | ||
/** | ||
* @deprecated getScenesByDevice | ||
*/ | ||
RED.httpAdmin.get(NODE_PATH + 'getScenesByDevice', function (req, res) { | ||
var config = req.query; | ||
var controller = RED.nodes.getNode(config.controllerID); | ||
let config = req.query; | ||
let controller = RED.nodes.getNode(config.controllerID); | ||
if (controller && controller.constructor.name === "ServerNode") { | ||
@@ -69,6 +137,6 @@ if ("scenes" in controller.items[config.device] && config.device in controller.items) { | ||
// RED.httpAdmin.get(NODE_PATH + 'gwscanner', function (req, res) { | ||
// // var ip = require("ip"); | ||
// // let ip = require("ip"); | ||
// // console.log ( ip.address() ); | ||
// | ||
// var portscanner = require('portscanner'); | ||
// let portscanner = require('portscanner'); | ||
// | ||
@@ -82,2 +150,12 @@ // // 127.0.0.1 is the default hostname; not required to provide | ||
// }); | ||
} | ||
RED.httpAdmin.get(NODE_PATH + 'configurationMigration', function (req, res) { | ||
let data = req.query; | ||
let config = JSON.parse(data.config); | ||
let configMigration = new ConfigMigration(data.type, config); | ||
let controller = RED.nodes.getNode(config.server); | ||
let result = configMigration.migrate(controller); | ||
res.json(result); | ||
}); | ||
}; |
@@ -0,1 +1,5 @@ | ||
const OutputMsgFormatter = require("../src/runtime/OutputMsgFormatter"); | ||
const ConfigMigration = require("../src/migration/ConfigMigration"); | ||
const NodeType = 'deconz-battery'; | ||
module.exports = function (RED) { | ||
@@ -6,17 +10,15 @@ class deConzItemBattery { | ||
var node = this; | ||
let node = this; | ||
node.config = config; | ||
// Config migration | ||
let configMigration = new ConfigMigration(NodeType, node.config); | ||
let migrationResult = configMigration.applyMigration(node.config, node); | ||
if (Array.isArray(migrationResult.errors) && migrationResult.errors.length > 0) { | ||
migrationResult.errors.forEach(error => console.error(error)); | ||
} | ||
//get server node | ||
node.server = RED.nodes.getNode(node.config.server); | ||
if (node.server) { | ||
node.server.on('onClose', () => this.onClose()); | ||
node.server.on('onSocketError', () => this.onSocketError()); | ||
node.server.on('onSocketClose', () => this.onSocketClose()); | ||
node.server.on('onSocketOpen', () => this.onSocketOpen()); | ||
node.server.on('onSocketPongTimeout', () => this.onSocketPongTimeout()); | ||
node.server.on('onNewDevice', (uniqueid) => this.onNewDevice(uniqueid)); | ||
node.sendLastState(); | ||
} else { | ||
if (!node.server) { | ||
node.status({ | ||
@@ -27,6 +29,52 @@ fill: "red", | ||
}); | ||
return; | ||
} | ||
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); | ||
} | ||
} | ||
handleDeconzEvent(device, changed, rawEvent, opt) { | ||
let node = this; | ||
let msgs = new Array(this.config.output_rules.length); | ||
let options = Object.assign({ | ||
initialEvent: false, | ||
errorEvent: false | ||
}, opt); | ||
this.config.output_rules.forEach((rule, index) => { | ||
// 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); | ||
// 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); | ||
// Make sure that the result is an array | ||
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend]; | ||
// 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); | ||
} | ||
} | ||
//TODO display msg payload if it's possible (one rule and payload a non object value | ||
node.status({ | ||
fill: "green", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/server:status.connected" | ||
}); | ||
}); | ||
} | ||
sendState(device) { | ||
@@ -106,66 +154,8 @@ var node = this; | ||
formatHomeKit(device) { | ||
var msg = {}; | ||
var characteristic = {}; | ||
//battery status | ||
if ("config" in device) { | ||
if (device.config['battery'] !== undefined && device.config['battery'] != null) { | ||
characteristic.BatteryLevel = parseInt(device.config['battery']); | ||
characteristic.StatusLowBattery = parseInt(device.config['battery']) <= 15 ? 1 : 0; | ||
msg.payload = characteristic; | ||
// msg.topic = "battery"; | ||
return msg; | ||
} | ||
} | ||
return null; | ||
} | ||
onSocketPongTimeout() { | ||
var node = this; | ||
node.onSocketError(); | ||
} | ||
onSocketError() { | ||
var node = this; | ||
node.status({ | ||
fill: "yellow", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/battery:status.reconnecting" | ||
}); | ||
} | ||
onClose() { | ||
var node = this; | ||
node.onSocketClose(); | ||
} | ||
onSocketClose() { | ||
var node = this; | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/battery:status.disconnected" | ||
}); | ||
} | ||
onSocketOpen() { | ||
var node = this; | ||
node.sendLastState(); | ||
} | ||
onNewDevice(uniqueid) { | ||
var node = this; | ||
if (node.config.device === uniqueid) { | ||
node.sendLastState(); | ||
} | ||
} | ||
} | ||
RED.nodes.registerType('deconz-battery', deConzItemBattery); | ||
RED.nodes.registerType(NodeType, deConzItemBattery); | ||
}; | ||
@@ -6,6 +6,6 @@ module.exports = function (RED) { | ||
var node = this; | ||
let node = this; | ||
node.config = config; | ||
node.cleanTimer = null; | ||
node.status({}); //clean | ||
//node.cleanTimer = null; | ||
//node.status({}); //clean | ||
@@ -15,78 +15,26 @@ //get server node | ||
if (node.server) { | ||
node.server.devices[node.id] = 'event'; | ||
node.server.on('onClose', () => this.onClose()); | ||
node.server.on('onSocketError', () => this.onSocketError()); | ||
node.server.on('onSocketClose', () => this.onSocketClose()); | ||
node.server.on('onSocketOpen', () => this.onSocketOpen()); | ||
node.server.on('onSocketMessage', (data) => this.onSocketMessage(data)); | ||
node.server.on('onSocketPongTimeout', () => this.onSocketPongTimeout()); | ||
} else { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/event:status.server_node_error" | ||
}); | ||
node.server.registerEventNode(node.id); | ||
} | ||
node.sendLastState(); | ||
} | ||
sendLastState() { | ||
var node = this; | ||
node.status({}); | ||
} | ||
onSocketPongTimeout() { | ||
var node = this; | ||
node.onSocketError(); | ||
} | ||
onSocketError() { | ||
var node = this; | ||
node.status({ | ||
fill: "yellow", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/event:status.reconnecting" | ||
handleDeconzEvent(device, changed, rawEvent, opt) { | ||
let node = this; | ||
node.send({ | ||
payload: rawEvent, | ||
meta: device | ||
}); | ||
} | ||
onClose() { | ||
var node = this; | ||
node.onSocketClose(); | ||
} | ||
onSocketClose() { | ||
var node = this; | ||
/* | ||
clearTimeout(node.cleanTimer); | ||
node.status({ | ||
fill: "red", | ||
fill: "green", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/event:status.disconnected" | ||
text: "node-red-contrib-deconz/event:status.event" | ||
}); | ||
node.cleanTimer = setTimeout(function () { | ||
node.status({}); //clean | ||
}, 3000); | ||
*/ | ||
} | ||
onSocketOpen() { | ||
var node = this; | ||
node.sendLastState(); | ||
} | ||
onSocketMessage(data) { | ||
var node = this; | ||
// console.log(data); | ||
if ("t" in data && data.t === "event") { | ||
node.send({'payload': data}); | ||
clearTimeout(node.cleanTimer); | ||
node.status({ | ||
fill: "green", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/event:status.event" | ||
}); | ||
node.cleanTimer = setTimeout(function () { | ||
node.status({}); //clean | ||
}, 3000); | ||
} | ||
} | ||
} | ||
@@ -93,0 +41,0 @@ |
157
nodes/get.js
@@ -0,1 +1,6 @@ | ||
const ConfigMigration = require("../src/migration/ConfigMigration"); | ||
const OutputMsgFormatter = require("../src/runtime/OutputMsgFormatter"); | ||
const NodeType = 'deconz-get'; | ||
module.exports = function (RED) { | ||
@@ -6,5 +11,12 @@ class deConzItemGet { | ||
var node = this; | ||
let node = this; | ||
node.config = config; | ||
node.config = config; | ||
// Config migration | ||
let configMigration = new ConfigMigration(NodeType, node.config); | ||
let migrationResult = configMigration.applyMigration(node.config, node); | ||
if (Array.isArray(migrationResult.errors) && migrationResult.errors.length > 0) { | ||
migrationResult.errors.forEach(error => console.error(error)); | ||
} | ||
node.cleanTimer = null; | ||
@@ -14,24 +26,3 @@ | ||
node.server = RED.nodes.getNode(node.config.server); | ||
if (node.server) { | ||
node.server.devices[node.id] = node.config.device; //register node in devices list | ||
if (typeof (node.config.device) == 'string' && node.config.device.length) { | ||
var deviceMeta = node.server.getDevice(node.config.device); | ||
if (deviceMeta !== undefined && deviceMeta && "uniqueid" in deviceMeta) { | ||
node.server.devices[node.id] = deviceMeta.uniqueid; //register node in devices list | ||
} else { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/get:status.device_not_set" | ||
}); | ||
} | ||
} else { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/get:status.device_not_set" | ||
}); | ||
} | ||
} else { | ||
if (!node.server) { | ||
node.status({ | ||
@@ -42,65 +33,89 @@ fill: "red", | ||
}); | ||
return; | ||
} | ||
if (typeof (config.device) == 'string' && config.device.length) { | ||
node.status({}); //clean | ||
if (node.config.search_type === 'device' && node.config.device_list.length === 0) { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/get:status.device_not_set" | ||
}); | ||
return; | ||
} | ||
node.on('input', function (message_in) { | ||
clearTimeout(node.cleanTimer); | ||
var deviceMeta = node.server.getDevice(node.config.device); | ||
// Cleanup old status | ||
node.status({}); | ||
if (deviceMeta) { | ||
node.server.devices[node.id] = deviceMeta.uniqueid; | ||
node.meta = deviceMeta; | ||
node.on('input', async (message_in) => { | ||
clearTimeout(node.cleanTimer); | ||
//status | ||
if ("state" in deviceMeta && deviceMeta.state !== undefined && "reachable" in deviceMeta.state && deviceMeta.state.reachable === false) { | ||
node.status({ | ||
fill: "red", | ||
shape: "ring", | ||
text: "node-red-contrib-deconz/get:status.not_reachable" | ||
}); | ||
} else if ("config" in deviceMeta && deviceMeta.config !== undefined && "reachable" in deviceMeta.config && deviceMeta.config.reachable === false) { | ||
node.status({ | ||
fill: "red", | ||
shape: "ring", | ||
text: "node-red-contrib-deconz/get:status.not_reachable" | ||
}); | ||
} else { | ||
node.status({ | ||
fill: "green", | ||
shape: "dot", | ||
text: (config.state in node.meta.state) ? (node.meta.state[config.state]).toString() : "node-red-contrib-deconz/get:status.received", | ||
}); | ||
// Wait until the server is ready | ||
if (node.server.ready === false) { | ||
await node.server.waitForReady(); | ||
if (node.server.ready === false) { | ||
//TODO send error, the server is not ready | ||
return; | ||
} | ||
} | ||
node.send({ | ||
payload: (config.state in node.meta.state) ? node.meta.state[config.state] : node.meta.state, | ||
payload_in: message_in.payload, | ||
meta: deviceMeta, | ||
}); | ||
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 | ||
); | ||
for (let r of node.server.device_list.getDevicesByQuery(querySrc).matched) { | ||
devices.push({data: r}); | ||
} | ||
break; | ||
} | ||
node.cleanTimer = setTimeout(function () { | ||
node.status({}); //clean | ||
}, 3000); | ||
} else { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/get:status.device_not_set" | ||
}); | ||
node.config.output_rules.forEach((rule, index) => { | ||
// Only if it's not on start and the start msg are blocked | ||
// 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) | ||
}); | ||
// Make sure that the result is an array | ||
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend]; | ||
// Send msgs | ||
for (let msg of msgToSend) { | ||
msgs[index] = msg; | ||
node.send(msgs); | ||
} | ||
}); | ||
}); | ||
} else { | ||
// TODO Display something usefull ? | ||
node.status({ | ||
fill: "red", | ||
fill: "green", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/get:status.device_not_set" | ||
text: "node-red-contrib-deconz/get:status.received", | ||
}); | ||
} | ||
node.cleanTimer = setTimeout(function () { | ||
node.status({}); //clean | ||
}, 3000); | ||
}); | ||
} | ||
} | ||
RED.nodes.registerType('deconz-get', deConzItemGet); | ||
RED.nodes.registerType(NodeType, deConzItemGet); | ||
}; |
389
nodes/in.js
const DeconzHelper = require('../lib/DeconzHelper.js'); | ||
const dotProp = require('dot-prop'); | ||
const ConfigMigration = require("../src/migration/ConfigMigration"); | ||
const OutputMsgFormatter = require("../src/runtime/OutputMsgFormatter"); | ||
const NodeType = 'deconz-input'; | ||
module.exports = function (RED) { | ||
@@ -9,18 +12,18 @@ class deConzItemIn { | ||
var node = this; | ||
node.lastSendTimestamp = null; | ||
let node = this; | ||
node.config = config; | ||
// Config migration | ||
let configMigration = new ConfigMigration(NodeType, node.config); | ||
let migrationResult = configMigration.applyMigration(node.config, node); | ||
if (Array.isArray(migrationResult.errors) && migrationResult.errors.length > 0) { | ||
migrationResult.errors.forEach(error => console.error(error)); | ||
} | ||
// Format : {'state':{__PATH__ : {"buttonevent": 1002}}} | ||
//node.oldValues = {'state': {}, 'config': {} /*, 'name': false*/}; | ||
//get server node | ||
node.server = RED.nodes.getNode(node.config.server); | ||
if (node.server) { | ||
node.server.on('onClose', () => this.onClose()); | ||
node.server.on('onSocketError', () => this.onSocketError()); | ||
node.server.on('onSocketClose', () => this.onSocketClose()); | ||
node.server.on('onSocketOpen', () => this.onSocketOpen()); | ||
node.server.on('onSocketPongTimeout', () => this.onSocketPongTimeout()); | ||
node.server.on('onNewDevice', (uniqueid) => this.onNewDevice(uniqueid)); | ||
node.sendLastState(); //tested for duplicate send with onSocketOpen | ||
} else { | ||
if (!node.server) { | ||
node.status({ | ||
@@ -31,338 +34,76 @@ fill: "red", | ||
}); | ||
return; | ||
} | ||
} | ||
sendLastState() { | ||
var node = this; | ||
if (typeof (node.config.device) == 'string' && node.config.device.length) { | ||
var deviceMeta = node.server.getDevice(node.config.device); | ||
if (deviceMeta !== undefined && deviceMeta && "uniqueid" in deviceMeta) { | ||
node.server.devices[node.id] = deviceMeta.uniqueid; | ||
node.meta = deviceMeta; | ||
if (node.config.outputAtStartup) { | ||
setTimeout(function () { | ||
node.sendState(deviceMeta, true); | ||
}, 1500); //we need this timeout after restart of node-red (homekit delays) | ||
} else { | ||
setTimeout(function () { | ||
node.status({}); //clean | ||
node.getState(deviceMeta); | ||
node.sendStateHomekitOnly(deviceMeta); //always send for homekit | ||
}, 1500); //update status with the same delay | ||
} | ||
} else { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/in:status.disconnected" | ||
}); | ||
} | ||
} else { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/in:status.device_not_set" | ||
if (node.config.search_type === "device") { | ||
node.config.device_list.forEach(function (item) { | ||
node.server.registerNodeByDevicePath(node.config.id, item); | ||
}); | ||
} | ||
} | ||
getState(device) { | ||
var node = this; | ||
if (device.state === undefined) { | ||
return; | ||
// console.log("CODE: #66"); | ||
// console.log(device); | ||
} else { | ||
//status | ||
if ("state" in device && "reachable" in device.state && device.state.reachable === false) { | ||
node.status({ | ||
fill: "red", | ||
shape: "ring", | ||
text: "node-red-contrib-deconz/in:status.not_reachable" | ||
}); | ||
} else if ("config" in device && "reachable" in device.config && device.config.reachable === false) { | ||
node.status({ | ||
fill: "red", | ||
shape: "ring", | ||
text: "node-red-contrib-deconz/in:status.not_reachable" | ||
}); | ||
} else { | ||
var nodeState = (node.config.state in device.state) ? (device.state[node.config.state]) : null; | ||
node.status({ | ||
fill: "green", | ||
shape: "dot", | ||
text: nodeState !== null ? nodeState.toString() : "node-red-contrib-deconz/in:status.connected" | ||
}); | ||
} | ||
if (node.oldState === undefined && device.state[node.config.state]) { | ||
node.oldState = device.state[node.config.state]; | ||
} | ||
if (node.prevUpdateTime === undefined && device.state['lastupdated']) { | ||
node.prevUpdateTime = device.state['lastupdated']; | ||
} | ||
return (device) | ||
node.server.registerNodeWithQuery(node.config.id); | ||
} | ||
}; | ||
sendState(device, force = false) { | ||
var node = this; | ||
device = node.getState(device); | ||
if (!device) { | ||
return; | ||
} | ||
node.status({ | ||
fill: "blue", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/in:status.starting" | ||
}); | ||
//filter output | ||
if (!force && 'onchange' === node.config.output && device.state[node.config.state] === node.oldState) return; | ||
if (!force && 'onupdate' === node.config.output && device.state['lastupdated'] === node.prevUpdateTime) return; | ||
node.server.on('onStart', () => { | ||
// Display usefull info | ||
node.status({ | ||
fill: "green", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/server:status.connected" | ||
}); | ||
//outputs | ||
node.send([ | ||
{ | ||
topic: node.config.topic, | ||
payload: (node.config.state in device.state) ? device.state[node.config.state] : device.state, | ||
payload_raw: device, | ||
meta: node.server.getDevice(node.config.device) | ||
}, | ||
node.formatHomeKit(device) | ||
]); | ||
console.log('OnStart'); | ||
}); | ||
node.oldState = device.state[node.config.state]; | ||
node.prevUpdateTime = device.state['lastupdated']; | ||
node.lastSendTimestamp = new Date().getTime(); | ||
}; | ||
} | ||
handleDeconzEvent(device, changed, rawEvent, opt) { | ||
let node = this; | ||
let msgs = new Array(this.config.output_rules.length); | ||
let options = Object.assign({ | ||
initialEvent: false, | ||
errorEvent: false | ||
}, opt); | ||
this.config.output_rules.forEach((rule, index) => { | ||
// 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); | ||
sendStateHomekitOnly(device) { | ||
var node = this; | ||
device = node.getState(device); | ||
if (!device) { | ||
return; | ||
} | ||
// 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); | ||
//outputs | ||
node.send([ | ||
null, | ||
node.formatHomeKit(device) | ||
]); | ||
}; | ||
// Make sure that the result is an array | ||
if (!Array.isArray(msgToSend)) msgToSend = [msgToSend]; | ||
formatHomeKit(device, options) { | ||
var node = this; | ||
var state = device.state; | ||
var config = device.config; | ||
var deviceMeta = node.server.getDevice(node.config.device); | ||
var no_reponse = false; | ||
if (state !== undefined && state['reachable'] !== undefined && state['reachable'] != null && state['reachable'] === false) { | ||
no_reponse = true; | ||
} | ||
if (config !== undefined && config['reachable'] !== undefined && config['reachable'] != null && config['reachable'] === false) { | ||
no_reponse = true; | ||
} | ||
if (options !== undefined && "reachable" in options && !options['reachable']) { | ||
no_reponse = true; | ||
} | ||
var msg = {}; | ||
// console.log(device.state); | ||
// console.log(new Date().getTime()-node.lastSendTimestamp); | ||
var characteristic = {}; | ||
if (state !== undefined) { | ||
//by types | ||
if ("type" in deviceMeta && (deviceMeta.type).toLowerCase() === 'window covering device') { | ||
characteristic.CurrentPosition = Math.ceil(state['bri'] / 2.55); | ||
characteristic.TargetPosition = Math.ceil(state['bri'] / 2.55); | ||
if (no_reponse) { | ||
characteristic.CurrentPosition = "NO_RESPONSE"; | ||
characteristic.TargetPosition = "NO_RESPONSE"; | ||
// Send msgs | ||
for (let msg of msgToSend) { | ||
msg.topic = this.config.topic; | ||
msgs[index] = msg; | ||
node.send(msgs); | ||
} | ||
//by params | ||
} else { | ||
if (state['temperature'] !== undefined) { | ||
characteristic.CurrentTemperature = state['temperature'] / 100; | ||
if (no_reponse) characteristic.CurrentTemperature = "NO_RESPONSE"; | ||
} | ||
if (state['humidity'] !== undefined) { | ||
characteristic.CurrentRelativeHumidity = state['humidity'] / 100; | ||
if (no_reponse) characteristic.CurrentRelativeHumidity = "NO_RESPONSE"; | ||
} | ||
if (state['lux'] !== undefined) { | ||
characteristic.CurrentAmbientLightLevel = state['lux']; | ||
if (no_reponse) characteristic.CurrentAmbientLightLevel = "NO_RESPONSE"; | ||
} | ||
if (state['fire'] !== undefined) { | ||
characteristic.SmokeDetected = state['fire']; | ||
if (no_reponse) characteristic.SmokeDetected = "NO_RESPONSE"; | ||
} | ||
if (state['buttonevent'] !== undefined) { | ||
//https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Xiaomi-WXKG01LM | ||
// Event Button Action | ||
// 1000 One initial press | ||
// 1001 One single hold | ||
// 1002 One single short release | ||
// 1003 One single hold release | ||
// 1004 One double short press | ||
// 1005 One triple short press | ||
// 1006 One quad short press | ||
// 1010 One five+ short press | ||
if ([1002, 2002, 3002, 4002, 5002, 6002].indexOf(state['buttonevent']) >= 0) characteristic.ProgrammableSwitchEvent = 0; | ||
else if ([1004, 2004, 3004, 4004, 5004, 6004].indexOf(state['buttonevent']) >= 0) characteristic.ProgrammableSwitchEvent = 1; | ||
else if ([1001, 2001, 3001, 4001, 5001, 6001].indexOf(state['buttonevent']) >= 0) characteristic.ProgrammableSwitchEvent = 2; | ||
else if ([1005, 2005, 3005, 4005, 5005, 6005].indexOf(state['buttonevent']) >= 0) characteristic.ProgrammableSwitchEvent = 3; | ||
else if ([1006, 2006, 3006, 4006, 5006, 6006].indexOf(state['buttonevent']) >= 0) characteristic.ProgrammableSwitchEvent = 4; | ||
else if ([1010, 2010, 3010, 4010, 5010, 6010].indexOf(state['buttonevent']) >= 0) characteristic.ProgrammableSwitchEvent = 5; | ||
if (no_reponse) characteristic.ProgrammableSwitchEvent = "NO_RESPONSE"; | ||
//index of btn | ||
if ([1001, 1002, 1004, 1005, 1006, 1010].indexOf(state['buttonevent']) >= 0) characteristic.ServiceLabelIndex = 1; | ||
else if ([2001, 2002, 2004, 2005, 2006, 2010].indexOf(state['buttonevent']) >= 0) characteristic.ServiceLabelIndex = 2; | ||
else if ([3001, 3002, 3004, 3005, 3006, 3010].indexOf(state['buttonevent']) >= 0) characteristic.ServiceLabelIndex = 3; | ||
else if ([4001, 4002, 4004, 4005, 4006, 4010].indexOf(state['buttonevent']) >= 0) characteristic.ServiceLabelIndex = 4; | ||
else if ([5001, 5002, 5004, 5005, 5006, 5010].indexOf(state['buttonevent']) >= 0) characteristic.ServiceLabelIndex = 5; | ||
else if ([6001, 6002, 6004, 6005, 6006, 6010].indexOf(state['buttonevent']) >= 0) characteristic.ServiceLabelIndex = 6; | ||
} | ||
// if (state['consumption'] !== null){ | ||
// characteristic.OutletInUse = state['consumption']; | ||
// } | ||
if (state['power'] !== undefined) { | ||
characteristic.OutletInUse = state['power'] > 0; | ||
if (no_reponse) characteristic.OutletInUse = "NO_RESPONSE"; | ||
} | ||
if (state['water'] !== undefined) { | ||
characteristic.LeakDetected = state['water'] ? 1 : 0; | ||
if (no_reponse) characteristic.LeakDetected = "NO_RESPONSE"; | ||
} | ||
if (state['presence'] !== undefined) { | ||
characteristic.MotionDetected = state['presence']; | ||
if (no_reponse) characteristic.MotionDetected = "NO_RESPONSE"; | ||
} | ||
if (state['open'] !== undefined) { | ||
characteristic.ContactSensorState = state['open'] ? 1 : 0; | ||
if (no_reponse) characteristic.ContactSensorState = "NO_RESPONSE"; | ||
} | ||
if (state['vibration'] !== undefined) { | ||
characteristic.ContactSensorState = state['vibration'] ? 1 : 0; | ||
if (no_reponse) characteristic.ContactSensorState = "NO_RESPONSE"; | ||
} | ||
if (state['on'] !== undefined) { | ||
characteristic.On = state['on']; | ||
if (no_reponse) characteristic.On = "NO_RESPONSE"; | ||
} | ||
if (state['bri'] !== undefined) { | ||
characteristic.Brightness = DeconzHelper.convertRange(state['bri'], [0, 255], [0, 100]); | ||
if (no_reponse) characteristic.Brightness = "NO_RESPONSE"; | ||
} | ||
//colors | ||
// if (state['colormode'] === 'hs' || state['colormode'] === 'xy') { | ||
if (state['hue'] !== undefined) { | ||
characteristic.Hue = DeconzHelper.convertRange(state['hue'], [0, 65535], [0, 360]); | ||
if (no_reponse) characteristic.Hue = "NO_RESPONSE"; | ||
} | ||
if (state['sat'] !== undefined) { | ||
characteristic.Saturation = DeconzHelper.convertRange(state['sat'], [0, 255], [0, 100]); | ||
if (no_reponse) characteristic.Saturation = "NO_RESPONSE"; | ||
} | ||
// } else if (state['colormode'] === 'ct') { | ||
if (state['ct'] !== undefined) { //lightbulb bug: use hue or ct | ||
characteristic.ColorTemperature = DeconzHelper.convertRange(state['ct'], [153, 500], [140, 500]); | ||
if (no_reponse) characteristic.ColorTemperature = "NO_RESPONSE"; | ||
} | ||
// } | ||
} | ||
} | ||
//battery status | ||
if (config !== undefined) { | ||
if (config['battery'] !== undefined && config['battery'] != null) { | ||
//TODO display msg payload if it's possible (one rule and payload a non object value | ||
node.status({ | ||
fill: "green", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/server:status.connected" | ||
}); | ||
if (device.type !== 'ZHASwitch') { //exclude | ||
characteristic.StatusLowBattery = parseInt(device.config['battery']) <= 15 ? 1 : 0; | ||
if (no_reponse) characteristic.StatusLowBattery = "NO_RESPONSE"; | ||
} | ||
} | ||
} | ||
if (Object.keys(characteristic).length === 0) return null; //empty response | ||
msg.topic = node.config.topic; | ||
msg.lastupdated = device.state['lastupdated']; | ||
msg.payload = characteristic; | ||
return msg; | ||
} | ||
onSocketPongTimeout() { | ||
var node = this; | ||
node.onSocketError(); | ||
} | ||
onSocketError() { | ||
var node = this; | ||
node.status({ | ||
fill: "yellow", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/in:status.reconnecting" | ||
}); | ||
//send NO_RESPONSE | ||
var deviceMeta = node.server.getDevice(node.config.device); | ||
if (deviceMeta) { | ||
node.send([ | ||
null, | ||
node.formatHomeKit(deviceMeta, {reachable: false}) | ||
]); | ||
} | ||
} | ||
onClose() { | ||
var node = this; | ||
node.onSocketClose(); | ||
} | ||
onSocketClose() { | ||
var node = this; | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/in:status.disconnected" | ||
}); | ||
} | ||
onSocketOpen() { | ||
var node = this; | ||
node.sendLastState(); | ||
} | ||
onNewDevice(uniqueid) { | ||
var node = this; | ||
if (node.config.device === uniqueid) { | ||
node.sendLastState(); | ||
} | ||
} | ||
} | ||
RED.nodes.registerType('deconz-input', deConzItemIn); | ||
RED.nodes.registerType(NodeType, deConzItemIn); | ||
}; | ||
@@ -7,3 +7,6 @@ { | ||
"state": "State", | ||
"config": "Config", | ||
"output": "Output", | ||
"state_output": "State Output", | ||
"config_output": "Config Output", | ||
"refresh": "Refresh", | ||
@@ -16,5 +19,13 @@ "refresh_devices_list": "Refresh Devices List", | ||
"on_state_change": "On state change", | ||
"on_config_change": "On config change", | ||
"on_update": "On update", | ||
"devices": "Devices", | ||
"light_groups": "Light Groups" | ||
"light_groups": "Light Groups", | ||
"search_type": "Search", | ||
"query": "Query", | ||
"query_result": "Query result", | ||
"output_device_separator": "Devices selection", | ||
"output_state_separator": "Output State", | ||
"output_homekit_separator": "Output Homekit", | ||
"output_3_separator": "Output Config" | ||
}, | ||
@@ -30,3 +41,4 @@ "placeholder": { | ||
"device_not_set": "device not set", | ||
"reconnecting": "reconnecting..." | ||
"reconnecting": "reconnecting...", | ||
"starting": "starting..." | ||
}, | ||
@@ -38,5 +50,11 @@ "multiselect": { | ||
"refresh": "Refresh devices list", | ||
"refresh_query": "Refresh query result", | ||
"none_selected": "None selected", | ||
"complete_payload": "Complete state payload" | ||
"complete_payload": "Complete state payload", | ||
"each_state": "Each state payload", | ||
"each_changed_state": "Each changed state payload", | ||
"complete_config_payload": "Complete config payload", | ||
"each_config": "Each config payload", | ||
"each_changed_config": "Each changed config payload" | ||
} | ||
} |
@@ -17,5 +17,518 @@ { | ||
"tip": { | ||
"deploy": "<b>Important:</b> deploy server node to get devices list", | ||
"secured_apikey_warning_message_update": "<b>Important:</b> Please click on update to save the API key in the vault" | ||
"deploy": "<b>Important:</b> deploy server node to get devices list.", | ||
"secured_apikey_warning_message_update": "<b>Important:</b> Please click on update to save the API key in the vault.", | ||
"input_device_warning_message_update": "<b>Important:</b> The device save format changed. Please click on Done to save with the new format." | ||
}, | ||
"status": { | ||
"disconnected": "disconnected", | ||
"connected": "connected", | ||
"server_node_error": "server node error", | ||
"not_reachable": "not reachable", | ||
"device_not_set": "device not set", | ||
"reconnecting": "reconnecting...", | ||
"starting": "starting...", | ||
"query_error": "can't read device query", | ||
"only_always": "Complete payload only accept Always as output setting" | ||
}, | ||
"editor": { | ||
"multiselect": { | ||
"none_selected": "None selected" | ||
}, | ||
"inputs": { | ||
"separator": { | ||
"device": "Devices selection", | ||
"specific": "Specific options", | ||
"outputs": "Outputs", | ||
"commands": "Commands" | ||
}, | ||
"server": { | ||
"label": "Server" | ||
}, | ||
"device": { | ||
"query": { | ||
"label": "Query", | ||
"options": { | ||
"device": "Device" | ||
} | ||
}, | ||
"device": { | ||
"label": "Device", | ||
"filter": "Filter devices..." | ||
}, | ||
"query_result": { | ||
"label": "Query result" | ||
}, | ||
"refresh": { | ||
"label": "Refresh", | ||
"button_text": "Refresh devices List" | ||
}, | ||
"refresh_query": { | ||
"label": "Refresh", | ||
"button_text": "Refresh query result" | ||
} | ||
}, | ||
"outputs": { | ||
"format": { | ||
"label": "Format", | ||
"icon": "file-code-o", | ||
"options": { | ||
"single": "Single x ... y ... z", | ||
"array": "Array [x,y,z]", | ||
"sum": "Sum x+y+z", | ||
"average": "Average x+y+z/3", | ||
"min": "Min X+y+Z = y", | ||
"max": "Max X+y+z = X" | ||
} | ||
}, | ||
"type": { | ||
"label": "Type", | ||
"icon": "file-text", | ||
"options": { | ||
"attribute": "Attribute", | ||
"state": "State", | ||
"config": "Config", | ||
"homekit": "Homekit" | ||
}, | ||
"add_button": { | ||
"label": "Add __type__", | ||
"icon": "plus", | ||
"title": "Add __type__ output" | ||
} | ||
}, | ||
"payload": { | ||
"label": "Payload", | ||
"icon": "ellipsis-h", | ||
"group_label": { | ||
"attribute": "Attribute", | ||
"state": "State", | ||
"config": "Config" | ||
}, | ||
"item_list": "__name__ (__sample__)", | ||
"item_list_mix": "__name__ [__item_count__/__device_count__] (__sample__)", | ||
"options": { | ||
"complete": "Complete payload", | ||
"each": "Each payload" | ||
} | ||
}, | ||
"output": { | ||
"label": "Output", | ||
"icon": "sign-out", | ||
"options": { | ||
"always": "Always", | ||
"onchange": "On change", | ||
"onupdate": "On update" | ||
} | ||
}, | ||
"on_start": { | ||
"label": "Start output", | ||
"icon": "share-square", | ||
"desc": "Send msg on start or socket reconnect." | ||
}, | ||
"on_error": { | ||
"label": "Error output", | ||
"icon": "external-link-square", | ||
"desc": "Send NO_RESPONSE on socket error." | ||
} | ||
}, | ||
"commands": { | ||
"type": { | ||
"label": "Type", | ||
"icon": "file-code-o", | ||
"add_button": { | ||
"label": "Add __type__", | ||
"icon": "plus", | ||
"title": "Add __type__ command" | ||
}, | ||
"options": { | ||
"common": { | ||
"fields": { | ||
"target": { | ||
"label": "Target", | ||
"icon": "dot-circle-o", | ||
"options": { | ||
"attribute": { | ||
"label": "Attribute", | ||
"icon": "object-group" | ||
}, | ||
"state": { | ||
"label": "State", | ||
"icon": "lightbulb-o" | ||
}, | ||
"config": { | ||
"label": "Config", | ||
"icon": "wrench" | ||
} | ||
} | ||
}, | ||
"command": { | ||
"label": "Command", | ||
"icon": "tasks", | ||
"options": { | ||
"object": { | ||
"label": "Object", | ||
"icon": "object-group" | ||
} | ||
} | ||
}, | ||
"payload": { | ||
"label": "Payload", | ||
"icon": "envelope" | ||
} | ||
} | ||
}, | ||
"deconz_state": { | ||
"label": "Deconz state", | ||
"icon": "deconz", | ||
"options": { | ||
"common": { | ||
"fields": { | ||
"transitiontime": { | ||
"label": "Transition", | ||
"icon": "hourglass-o", | ||
"title": "Transition time in 1/10 seconds between two states.", | ||
"placeholder": "10 = 1 second" | ||
}, | ||
"retryonerror": { | ||
"label": "Retry on error", | ||
"icon": "repeat", | ||
"title": "Retry on when command failed (max 2)." | ||
}, | ||
"aftererror": { | ||
"label": "After error", | ||
"icon": "exclamation", | ||
"title": "Action to do when a command failed after retry.", | ||
"options": { | ||
"continue": { | ||
"label": "Continue", | ||
"icon": "play" | ||
}, | ||
"stop": { | ||
"label": "Stop", | ||
"icon": "stop" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"lights": { | ||
"label": "Lights", | ||
"fields": { | ||
"on": { | ||
"label": "On/Off", | ||
"icon": "lightbulb-o", | ||
"options": { | ||
"keep": { | ||
"label": "Don't change", | ||
"icon": "deconz" | ||
}, | ||
"set": { | ||
"label": "Turn", | ||
"icon": "deconz", | ||
"options": { | ||
"true": { | ||
"label": "On", | ||
"icon": "deconz" | ||
}, | ||
"false": { | ||
"label": "Off", | ||
"icon": "deconz" | ||
} | ||
} | ||
}, | ||
"toggle": { | ||
"label": "Toggle", | ||
"icon": "deconz" | ||
} | ||
} | ||
}, | ||
"lightFields": { | ||
"options": { | ||
"keep": { | ||
"label": "Don't change", | ||
"icon": "deconz" | ||
}, | ||
"set": { | ||
"label": "Set", | ||
"icon": "arrow-circle-right" | ||
}, | ||
"inc": { | ||
"label": "Increment by", | ||
"icon": "arrow-circle-up" | ||
}, | ||
"dec": { | ||
"label": "Decrement by", | ||
"icon": "arrow-circle-down" | ||
}, | ||
"detect_from_value": { | ||
"label": "Detect from value", | ||
"icon": "magic" | ||
} | ||
} | ||
}, | ||
"bri": { | ||
"label": "Brightness", | ||
"icon": "lightbulb-o" | ||
}, | ||
"sat": { | ||
"label": "Color saturation", | ||
"icon": "lightbulb-o" | ||
}, | ||
"hue": { | ||
"label": "Color hue", | ||
"icon": "lightbulb-o" | ||
}, | ||
"ct": { | ||
"label": "Mired color temperature", | ||
"icon": "lightbulb-o", | ||
"options": { | ||
"deconz": { | ||
"label": "Deconz", | ||
"icon": "deconz", | ||
"options": { | ||
"cold": { | ||
"label": "Cold", | ||
"icon": "thermometer-empty" | ||
}, | ||
"white": { | ||
"label": "White", | ||
"icon": "thermometer-half" | ||
}, | ||
"warm": { | ||
"label": "Warm", | ||
"icon": "thermometer-full" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"xy": { | ||
"label": "CIE xy color", | ||
"icon": "lightbulb-o" | ||
}, | ||
"alert": { | ||
"label": "Alert", | ||
"icon": "bell-o", | ||
"options": { | ||
"deconz": { | ||
"label": "Deconz", | ||
"icon": "deconz", | ||
"options": { | ||
"none": { | ||
"label": "None", | ||
"icon": "deconz" | ||
}, | ||
"select": { | ||
"label": "Blinking a short time", | ||
"icon": "deconz" | ||
}, | ||
"lselect": { | ||
"label": "Blinking a longer time", | ||
"icon": "deconz" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"effect": { | ||
"label": "Effect", | ||
"icon": "star-o", | ||
"options": { | ||
"deconz": { | ||
"label": "Deconz", | ||
"icon": "deconz", | ||
"options": { | ||
"none": { | ||
"label": "None", | ||
"icon": "deconz" | ||
}, | ||
"colorloop": { | ||
"label": "Color Loop", | ||
"icon": "deconz" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"colorloopspeed": { | ||
"label": "Color Loop Speed", | ||
"icon": "star-o", | ||
"title": "1 = very fast 255 = very slow", | ||
"placeholder": "default: 15" | ||
} | ||
} | ||
}, | ||
"groups": { | ||
"label": "Groups" | ||
}, | ||
"covers": { | ||
"label": "Windows Cover", | ||
"fields": { | ||
"open": { | ||
"label": "Open/Close", | ||
"icon": "window-maximize", | ||
"title": "Set to true to lift the shutter to 0%, false to lift it to 100%.", | ||
"options": { | ||
"keep": { | ||
"label": "Don't change", | ||
"icon": "deconz" | ||
}, | ||
"set": { | ||
"label": "Set", | ||
"icon": "deconz", | ||
"options": { | ||
"true": { | ||
"label": "Open" | ||
}, | ||
"false": { | ||
"label": "Close" | ||
} | ||
} | ||
}, | ||
"toggle": { | ||
"label": "Toggle", | ||
"icon": "deconz" | ||
} | ||
} | ||
}, | ||
"stop": { | ||
"label": "Stop/Continue", | ||
"icon": "stop", | ||
"title": "Stops the current action.", | ||
"options": { | ||
"keep": { | ||
"label": "Don't change", | ||
"icon": "deconz" | ||
}, | ||
"set": { | ||
"label": "Set", | ||
"icon": "deconz", | ||
"options": { | ||
"true": { | ||
"label": "Stop", | ||
"icon": "deconz" | ||
}, | ||
"false": { | ||
"label": "Continue", | ||
"icon": "deconz" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"lift": { | ||
"label": "Lift", | ||
"icon": "arrows-v", | ||
"title": "Supported range is 0–100 or special value \"stop\".\nlift is best understood as “percentage closed”. So for any lift value below 100%, open is true.", | ||
"options": { | ||
"stop": { | ||
"label": "Stop", | ||
"icon": "deconz" | ||
} | ||
} | ||
}, | ||
"tilt": { | ||
"label": "Tilt", | ||
"icon": "arrows-h", | ||
"title": "Sets the tilt angle of the shutter (0–100%)." | ||
} | ||
} | ||
}, | ||
"scene_call": { | ||
"label": "Scenes call", | ||
"fields": { | ||
"picker": { | ||
"label": "Picker", | ||
"icon": "crosshairs", | ||
"filter_place_holder": "Filter scenes..." | ||
}, | ||
"group": { | ||
"label": "Group ID", | ||
"icon": "object-group", | ||
"title": "Select the group of the scene", | ||
"options": { | ||
"from_device": { | ||
"label": "From device", | ||
"icon": "search" | ||
} | ||
} | ||
}, | ||
"scene": { | ||
"label": "Scene ID", | ||
"icon": "picture-o", | ||
"title": "Select the scene to call", | ||
"options": { | ||
"deconz": { | ||
"label": "Deconz", | ||
"icon": "deconz", | ||
"options": { | ||
"next": { | ||
"label": "Next" | ||
}, | ||
"prev": { | ||
"label": "Previous" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"homekit": { | ||
"label": "HomeKit", | ||
"icon": "homekit" | ||
}, | ||
"custom": { | ||
"label": "Custom", | ||
"icon": "cogs" | ||
}, | ||
"animation": { | ||
"label": "Animation", | ||
"icon": "spinner" | ||
}, | ||
"pause": { | ||
"label": "Pause", | ||
"icon": "pause", | ||
"fields": { | ||
"delay": { | ||
"label": "Delay", | ||
"icon": "hourglass-o", | ||
"title": "Delay in ms before executing next command." | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"specific": { | ||
"output": { | ||
"delay": { | ||
"label": "Delay", | ||
"icon": "hourglass-o", | ||
"title": "Delay between api request in ms." | ||
}, | ||
"result": { | ||
"label": "Result", | ||
"icon": "table", | ||
"title": "Return the result of the api call.", | ||
"options": { | ||
"never": { | ||
"label": "Never", | ||
"icon": "hourglass-start" | ||
}, | ||
"after_command": { | ||
"label": "After each command", | ||
"icon": "hourglass-half" | ||
}, | ||
"at_end": { | ||
"label": "At the end", | ||
"icon": "hourglass-end" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
356
nodes/out.js
@@ -1,4 +0,7 @@ | ||
const DeconzHelper = require('../lib/DeconzHelper.js'); | ||
var request = require('request'); | ||
const CommandParser = require("../src/runtime/CommandParser"); | ||
const Utils = require("../src/runtime/Utils"); | ||
const got = require('got'); | ||
const ConfigMigration = require("../src/migration/ConfigMigration"); | ||
const NodeType = 'deconz-output'; | ||
module.exports = function (RED) { | ||
@@ -9,5 +12,12 @@ class deConzOut { | ||
var node = this; | ||
let node = this; | ||
node.config = config; | ||
// Config migration | ||
let configMigration = new ConfigMigration(NodeType, node.config); | ||
let migrationResult = configMigration.applyMigration(node.config, node); | ||
if (Array.isArray(migrationResult.errors) && migrationResult.errors.length > 0) { | ||
migrationResult.errors.forEach(error => console.error(error)); | ||
} | ||
node.status({}); //clean | ||
@@ -17,264 +27,148 @@ | ||
node.server = RED.nodes.getNode(node.config.server); | ||
if (node.server) { | ||
node.server.devices[node.id] = node.config.device; //register node in devices list | ||
} else { | ||
if (!node.server) { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/out:status.server_node_error" | ||
text: "node-red-contrib-deconz/in:status.server_node_error" | ||
}); | ||
return; | ||
} | ||
node.payload = config.payload; | ||
node.payloadType = config.payloadType; | ||
node.command = config.command; | ||
node.commandType = config.commandType; | ||
node.cleanTimer = null; | ||
// if (typeof(config.device) == 'string' && config.device.length) { | ||
this.on('input', async (message_in, send, done) => { | ||
let delay = Utils.getNodeProperty(node.config.specific.delay, this, message_in); | ||
if (typeof delay !== 'number') delay = 50; | ||
this.on('input', function (message) { | ||
clearTimeout(node.cleanTimer); | ||
var payload; | ||
switch (node.payloadType) { | ||
case 'flow': | ||
case 'global': { | ||
RED.util.evaluateNodeProperty(node.payload, node.payloadType, this, message, function (error, result) { | ||
if (error) { | ||
node.error(error, message); | ||
} else { | ||
payload = result; | ||
} | ||
}); | ||
//clearTimeout(node.cleanTimer); | ||
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 'date': { | ||
payload = Date.now(); | ||
case 'json': | ||
case 'jsonata': | ||
let querySrc = RED.util.evaluateJSONataExpression( | ||
RED.util.prepareJSONataExpression(node.config.query, node), | ||
message_in, | ||
undefined | ||
); | ||
for (let r of node.server.device_list.getDevicesByQuery(querySrc).matched) { | ||
devices.push({data: r}); | ||
} | ||
break; | ||
} | ||
case 'deconz_payload': | ||
payload = node.payload; | ||
break; | ||
} | ||
case 'num': { | ||
payload = parseInt(node.config.payload); | ||
break; | ||
} | ||
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'; | ||
case 'str': { | ||
payload = node.config.payload; | ||
break; | ||
for (const [id, command] of node.config.commands.entries()) { | ||
if (command.type === 'pause') { | ||
await Utils.sleep(Utils.getNodeProperty(command.arg.delay, this, message_in), 2000); | ||
continue; | ||
} | ||
case 'object': { | ||
payload = node.config.payload; | ||
break; | ||
} | ||
try { | ||
let cp = new CommandParser(command, message_in, node); | ||
let requests = cp.getRequests(node, devices); | ||
for (const request of requests) { | ||
try { | ||
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 ? | ||
} | ||
); | ||
case 'homekit': | ||
case 'msg': | ||
default: { | ||
payload = message[node.payload]; | ||
break; | ||
} | ||
} | ||
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); | ||
} | ||
var command; | ||
switch (node.commandType) { | ||
case 'msg': { | ||
command = message[node.command]; | ||
break; | ||
} | ||
case 'deconz_cmd': | ||
command = node.command; | ||
switch (command) { | ||
case 'on': | ||
payload = payload && payload !== '0'; | ||
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; | ||
} | ||
case 'toggle': | ||
command = "on"; | ||
var deviceMeta = node.server.getDevice(node.config.device); | ||
if (deviceMeta !== undefined && "device_type" in deviceMeta && deviceMeta.device_type === 'groups' && deviceMeta && "state" in deviceMeta && "all_on" in deviceMeta.state) { | ||
payload = !deviceMeta.state.all_on; | ||
} else if (deviceMeta !== undefined && deviceMeta && "state" in deviceMeta && "on" in deviceMeta.state) { | ||
payload = !deviceMeta.state.on; | ||
} else { | ||
payload = false; | ||
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); | ||
} | ||
} | ||
break; | ||
await Utils.sleep(delay - response.timings.phases.total); | ||
} catch (error) { | ||
if (resultTiming !== 'never') { | ||
let errorMsg = {}; | ||
if (resultTiming === 'after_command') { | ||
errorMsg = Utils.cloneMessage(message_in, ['request', 'meta', 'payload', 'errors']); | ||
} | ||
case 'bri': | ||
case 'hue': | ||
case 'sat': | ||
case 'ct': | ||
case 'scene': // added scene, payload is the scene ID | ||
case 'colorloopspeed': | ||
// case 'transitiontime': | ||
payload = parseInt(payload); | ||
break; | ||
errorMsg.request = request.params; | ||
errorMsg.meta = request.meta; | ||
errorMsg.errors = [{ | ||
type: 0, | ||
code: error.response.statusCode, | ||
message: error.response.statusMessage, | ||
description: `${error.name}: ${error.message}`, | ||
apiEndpoint: request.endpoint | ||
}]; | ||
case 'json': | ||
case 'alert': | ||
case 'effect': | ||
default: { | ||
break; | ||
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; | ||
await Utils.sleep(delay - error.timings.phases.total); | ||
} | ||
} | ||
break; | ||
} catch (error) { | ||
node.error(`Error while processing command #${id + 1}, ${error}`, message_in); | ||
} | ||
case 'homekit': | ||
payload = node.formatHomeKit(message, payload); | ||
break; | ||
case 'str': | ||
default: { | ||
command = node.command; | ||
break; | ||
} | ||
} | ||
//empty payload, stop | ||
if (payload === null) { | ||
return false; | ||
if (resultTiming === 'at_end') { | ||
let endMsg = Utils.cloneMessage(message_in, ['payload', 'errors']); | ||
endMsg.payload = resultMsgs; | ||
if (errorMsgs.length > 0) | ||
endMsg.errors = errorMsgs; | ||
send(endMsg); | ||
} | ||
//send data to API | ||
var deviceMeta = node.server.getDevice(node.config.device); | ||
if (deviceMeta !== undefined && deviceMeta && "device_id" in deviceMeta) { | ||
let url = 'http://' + node.server.ip + ':' + node.server.port + '/api/' + node.server.credentials.secured_apikey; | ||
if (command == 'scene') { // make a new URL for recalling the scene | ||
var groupid = ((node.config.device).split('group_').join('')); | ||
url += '/groups/' + groupid + '/scenes/' + payload + '/recall'; | ||
} else if ((/group_/g).test(node.config.device)) { | ||
var groupid = ((node.config.device).split('group_').join('')); | ||
url += '/groups/' + groupid + '/action'; | ||
} else { | ||
url += '/lights/' + deviceMeta.device_id + '/state'; | ||
} | ||
var post = {}; | ||
if (node.commandType == 'object' || node.commandType == 'homekit') { | ||
post = payload; | ||
} else if (command != 'scene') { // scene doesn't have a post payload, so keep it empty. | ||
if (command != 'on') post['on'] = true; | ||
if (command == 'bri') post['on'] = payload > 0 ? true : false; | ||
post[command] = payload; | ||
} | ||
let transitionTime = parseInt(RED.util.evaluateNodeProperty(config.transitionTime, config.transitionTimeType || "num", node, message)); | ||
if (config.transitionTime !== "" && transitionTime >= 0) { | ||
post['transitiontime'] = transitionTime; | ||
} | ||
node.postData(url, post); | ||
} else { | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/out:status.device_not_set" | ||
}); | ||
node.cleanTimer = setTimeout(function () { | ||
node.status({}); //clean | ||
}, 3000); | ||
} | ||
}); | ||
// } else { | ||
// node.status({ | ||
// fill: "red", | ||
// shape: "dot", | ||
// text: 'Device not set' | ||
// }); | ||
// } | ||
} | ||
postData(url, post) { | ||
var node = this; | ||
// node.log('Requesting url: '+url); | ||
// console.log(post); | ||
request.put({ | ||
url: url, | ||
form: JSON.stringify(post) | ||
}, function (error, response, body) { | ||
if (error && typeof (error) === 'object') { | ||
node.warn(error); | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/out:status.connection" | ||
}); | ||
node.cleanTimer = setTimeout(function () { | ||
node.status({}); //clean | ||
}, 3000); | ||
} else if (body) { | ||
var response = JSON.parse(body)[0]; | ||
if ('success' in response) { | ||
node.status({ | ||
fill: "green", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/out:status.ok" | ||
}); | ||
} else if ('error' in response) { | ||
response.error.post = post; //add post data | ||
node.warn('deconz-out ERROR: ' + response.error.description); | ||
node.warn(response.error); | ||
node.status({ | ||
fill: "red", | ||
shape: "dot", | ||
text: "node-red-contrib-deconz/out:status.error" | ||
}); | ||
} | ||
node.cleanTimer = setTimeout(function () { | ||
node.status({}); //clean | ||
}, 3000); | ||
} | ||
}); | ||
} | ||
formatHomeKit(message, payload) { | ||
if (message.hap.context === undefined) { | ||
return null; | ||
} | ||
var node = this; | ||
// var deviceMeta = node.server.getDevice(node.config.device); | ||
var msg = {}; | ||
if (payload.On !== undefined) { | ||
msg['on'] = payload.On; | ||
} else if (payload.Brightness !== undefined) { | ||
msg['bri'] = DeconzHelper.convertRange(payload.Brightness, [0, 100], [0, 255]); | ||
if (payload.Brightness >= 254) payload.Brightness = 255; | ||
msg['on'] = payload.Brightness > 0 | ||
} else if (payload.Hue !== undefined) { | ||
msg['hue'] = DeconzHelper.convertRange(payload.Hue, [0, 360], [0, 65535]); | ||
msg['on'] = true; | ||
} else if (payload.Saturation !== undefined) { | ||
msg['sat'] = DeconzHelper.convertRange(payload.Saturation, [0, 100], [0, 255]); | ||
msg['on'] = true; | ||
} else if (payload.ColorTemperature !== undefined) { | ||
msg['ct'] = DeconzHelper.convertRange(payload.ColorTemperature, [140, 500], [153, 500]); | ||
msg['on'] = true; | ||
} else if (payload.TargetPosition !== undefined) { | ||
msg['on'] = payload.TargetPosition > 0; | ||
msg['bri'] = DeconzHelper.convertRange(payload.TargetPosition, [0, 100], [0, 255]); | ||
} | ||
return msg; | ||
} | ||
} | ||
RED.nodes.registerType('deconz-output', deConzOut); | ||
RED.nodes.registerType(NodeType, deConzOut); | ||
}; | ||
@@ -281,0 +175,0 @@ |
@@ -1,189 +0,372 @@ | ||
var request = require('request'); | ||
const DeconzSocket = require('../lib/deconz-socket'); | ||
const got = require('got'); | ||
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 Utils = require("../src/runtime/Utils"); | ||
module.exports = function (RED) { | ||
class ServerNode { | ||
constructor(n) { | ||
RED.nodes.createNode(this, n); | ||
constructor(config) { | ||
RED.nodes.createNode(this, config); | ||
let node = this; | ||
node.config = config; | ||
node.discoverProcessRunning = false; | ||
node.ready = false; | ||
var node = this; | ||
node.items = undefined; | ||
node.items_list = undefined; | ||
node.discoverProcess = false; | ||
node.name = n.name; | ||
node.ip = n.ip; | ||
node.port = n.port; | ||
node.ws_port = n.ws_port; | ||
node.secure = n.secure || false; | ||
// Prior 1.2.0 the apikey was not stored in credentials | ||
if (node.credentials.secured_apikey === undefined && n.apikey !== undefined) { | ||
node.credentials.secured_apikey = n.apikey; | ||
// Config migration | ||
let configMigration = new ConfigMigration('deconz-server', node.config); | ||
let migrationResult = configMigration.applyMigration(node.config, node); | ||
if (Array.isArray(migrationResult.errors) && migrationResult.errors.length > 0) { | ||
migrationResult.errors.forEach(error => console.error(error)); | ||
} | ||
node.devices = {}; | ||
node.device_list = new DeviceList(); | ||
node.api = new DeconzAPI({ | ||
ip: node.config.ip, | ||
port: node.config.port, | ||
key: node.credentials.secured_apikey | ||
}); | ||
// Example : ["ea9cd132.08f36"] | ||
node.nodesWithQuery = []; | ||
node.nodesEvent = []; | ||
node.nodesByDevicePath = {}; | ||
node.setMaxListeners(255); | ||
node.refreshDiscoverTimer = null; | ||
node.refreshDiscoverInterval = n.polling >= 3 ? n.polling * 1000 : 15000; | ||
node.refreshDiscoverInterval = node.config.polling >= 3 ? node.config.polling * 1000 : 15000; | ||
node.on('close', () => this.onClose()); | ||
(async () => { | ||
//TODO make the delay configurable | ||
await Utils.sleep(1500); | ||
await node.discoverDevices({ | ||
forceRefresh: true | ||
}); | ||
this.refreshDiscoverTimer = setInterval(() => { | ||
node.discoverDevices({ | ||
forceRefresh: true | ||
}); | ||
}, node.refreshDiscoverInterval); | ||
node.ready = true; | ||
this.setupDeconzSocket(node); | ||
})(); | ||
} | ||
async waitForReady(maxDelay = 10000) { | ||
const pauseDelay = 100; | ||
let pauseCount = 0; | ||
while (this.ready === false) { | ||
await Utils.sleep(pauseDelay); | ||
pauseCount++; | ||
if (pauseCount * pauseDelay >= maxDelay) { | ||
break; | ||
} | ||
} | ||
} | ||
setupDeconzSocket(node) { | ||
node.socket = new DeconzSocket({ | ||
hostname: this.ip, | ||
port: this.ws_port, | ||
secure: this.secure | ||
hostname: node.config.ip, | ||
port: node.config.ws_port, | ||
secure: node.config.secure || false | ||
}); | ||
node.socket.on('close', (code, reason) => this.onSocketClose(code, reason)); | ||
node.socket.on('close', (code, reason) => { | ||
if (reason) { // don't bother the user unless there's a reason | ||
node.warn(`WebSocket disconnected: ${code} - ${reason}`); | ||
} | ||
if (node.ready) node.propagateErrorNews(code, reason); | ||
}); | ||
node.socket.on('unauthorized', () => this.onSocketUnauthorized()); | ||
node.socket.on('open', () => this.onSocketOpen()); | ||
node.socket.on('open', () => { | ||
node.log(`WebSocket opened`); | ||
// This is used only on websocket reconnect, not the initial connection. | ||
if (node.ready) node.propagateStartNews(); | ||
}); | ||
node.socket.on('message', (payload) => this.onSocketMessage(payload)); | ||
node.socket.on('error', (err) => this.onSocketError(err)); | ||
node.socket.on('pong-timeout', () => this.onSocketPongTimeout()); | ||
} | ||
node.on('close', () => this.onClose()); | ||
async discoverDevices(opt) { | ||
let node = this; | ||
let options = Object.assign({ | ||
forceRefresh: false, | ||
callback: () => { | ||
} | ||
}, opt); | ||
node.discoverDevices(function () { | ||
}, true); | ||
if (options.forceRefresh === false || node.discoverProcessRunning === true) { | ||
node.log('discoverDevices: Using cached devices'); | ||
return; | ||
} | ||
this.refreshDiscoverTimer = setInterval(function () { | ||
node.discoverDevices(function () { | ||
}, true); | ||
}, node.refreshDiscoverInterval); | ||
node.discoverProcessRunning = true; | ||
const response = await got(node.api.url.main()).json(); | ||
node.device_list.parse(response); | ||
node.log(`discoverDevices: Updated ${node.device_list.count}`); | ||
node.discoverProcessRunning = false; | ||
} | ||
propagateStartNews() { | ||
let node = this; | ||
// Node with device selected | ||
for (let [device_path, nodeIDs] of Object.entries(node.nodesByDevicePath)) { | ||
node.propagateNews(nodeIDs, { | ||
type: 'start', | ||
node_type: 'device_path', | ||
device: node.device_list.getDeviceByPath(device_path) | ||
}); | ||
} | ||
discoverDevices(callback, forceRefresh = false) { | ||
var node = this; | ||
// Node with quety | ||
for (let nodeID of node.nodesWithQuery) { | ||
let target = RED.nodes.getNode(nodeID); | ||
if (forceRefresh || node.items === undefined) { | ||
node.discoverProcess = true; | ||
// node.log('discoverDevices: Refreshing devices list'); | ||
if (!target) { | ||
console.warn('ERROR: cant get ' + nodeID + ' node for start news, removed from list NodeWithQuery'); | ||
node.unregisterNodeWithQuery(nodeID); | ||
continue; | ||
} | ||
var url = "http://" + node.ip + ":" + node.port + "/api/" + node.credentials.secured_apikey; | ||
// node.log('discoverDevices: Requesting: ' + url); | ||
// TODO Cache JSONata expresssions ? | ||
let querySrc = RED.util.evaluateJSONataExpression( | ||
RED.util.prepareJSONataExpression(target.config.query, target), | ||
{}, | ||
undefined | ||
); | ||
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, | ||
}); | ||
} | ||
} | ||
} | ||
propagateErrorNews(code, reason) { | ||
let node = this; | ||
request.get(url, function (error, result, data) { | ||
// 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: `WebSocket disconnected: ${reason || 'no reason provided'}` | ||
}); | ||
} | ||
if (error) { | ||
node.discoverProcess = false; | ||
callback(false); | ||
return; | ||
} | ||
// Node with quety | ||
for (let nodeID of node.nodesWithQuery) { | ||
let target = RED.nodes.getNode(nodeID); | ||
try { | ||
var dataParsed = JSON.parse(data); | ||
} catch (e) { | ||
node.discoverProcess = false; | ||
callback(false); | ||
return; | ||
} | ||
if (!target) { | ||
console.warn('ERROR: cant get ' + nodeID + ' node for error news, removed from list NodeWithQuery'); | ||
node.unregisterNodeWithQuery(nodeID); | ||
continue; | ||
} | ||
node.oldItemsList = node.items !== undefined ? node.items : undefined; | ||
node.items = []; | ||
if (dataParsed) { | ||
for (var index in dataParsed.sensors) { | ||
var prop = dataParsed.sensors[index]; | ||
prop.device_type = 'sensors'; | ||
prop.device_id = parseInt(index); | ||
// TODO Cache JSONata expresssions ? | ||
let querySrc = RED.util.evaluateJSONataExpression( | ||
RED.util.prepareJSONataExpression(target.config.query, target), | ||
{}, | ||
undefined | ||
); | ||
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: `WebSocket disconnected: ${reason || 'no reason provided'}` | ||
}); | ||
} | ||
} | ||
} | ||
if (node.oldItemsList !== undefined && prop.uniqueid in node.oldItemsList) { | ||
} else { | ||
node.items[prop.uniqueid] = prop; | ||
node.emit("onNewDevice", prop.uniqueid); | ||
} | ||
node.items[prop.uniqueid] = prop; | ||
} | ||
/** | ||
* | ||
* @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; | ||
for (var index in dataParsed.lights) { | ||
var prop = dataParsed.lights[index]; | ||
prop.device_type = 'lights'; | ||
prop.device_id = parseInt(index); | ||
// 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]; | ||
if (node.oldItemsList !== undefined && prop.uniqueid in node.oldItemsList) { | ||
} else { | ||
node.items[prop.uniqueid] = prop; | ||
node.emit("onNewDevice", prop.uniqueid); | ||
} | ||
node.items[prop.uniqueid] = prop; | ||
for (const nodeID of nodeIDs) { | ||
let target = RED.nodes.getNode(nodeID); | ||
// If the target does not exist we remove it from the node list | ||
if (!target) { | ||
switch (news.node_type) { | ||
case 'device_path': | ||
console.warn('ERROR: cant get ' + nodeID + ' node, removed from list nodesByDevicePath'); | ||
node.unregisterNodeByDevicePath(nodeID, news.device.device_path); | ||
break; | ||
case 'query': | ||
console.warn('ERROR: cant get ' + nodeID + ' node, removed from list nodesWithQuery'); | ||
node.unregisterNodeWithQuery(nodeID); | ||
break; | ||
case 'event_node': | ||
console.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; | ||
} | ||
for (var index in dataParsed.groups) { | ||
var prop = dataParsed.groups[index]; | ||
prop.device_type = 'groups'; | ||
var groupid = "group_" + parseInt(index); | ||
prop.device_id = groupid; | ||
prop.uniqueid = groupid; | ||
break; | ||
case 'event': | ||
let dataParsed = news.eventData; | ||
switch (dataParsed.t) { | ||
case "event": | ||
if (target.type === "deconz-event") { | ||
target.handleDeconzEvent( | ||
news.device, | ||
news.changed, | ||
dataParsed | ||
); | ||
} 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 { | ||
console.warn("WTF this is used : We tried to send a msg to a non input node."); | ||
continue; | ||
} | ||
break; | ||
case "scene-called": | ||
// TODO Implement This | ||
console.warn("Need to implement onSocketMessageSceneCalled for " + JSON.stringify(dataParsed)); | ||
break; | ||
default: | ||
console.warn("Unknown event of type '" + dataParsed.e + "'. " + JSON.stringify(dataParsed)); | ||
break; | ||
} | ||
} | ||
break; | ||
default: | ||
console.warn("Unknown message of type '" + dataParsed.t + "'. " + JSON.stringify(dataParsed)); | ||
break; | ||
} | ||
if (node.oldItemsList !== undefined && prop.uniqueid in node.oldItemsList) { | ||
} else { | ||
node.items[prop.uniqueid] = prop; | ||
node.emit("onNewDevice", prop.uniqueid); | ||
} | ||
node.items[prop.uniqueid] = prop; | ||
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; | ||
} | ||
node.discoverProcess = false; | ||
callback(node.items); | ||
return node.items; | ||
}); | ||
} else { | ||
node.log('discoverDevices: Using cached devices'); | ||
callback(node.items); | ||
return node.items; | ||
} | ||
} | ||
getDiscoverProcess() { | ||
var node = this; | ||
return node.discoverProcess; | ||
registerEventNode(nodeID) { | ||
let node = this; | ||
if (!node.nodesEvent.includes(nodeID)) node.nodesEvent.push(nodeID); | ||
} | ||
getDevice(uniqueid) { | ||
var node = this; | ||
var result = false; | ||
if (node.items !== undefined && node.items) { | ||
for (var index in (node.items)) { | ||
var item = (node.items)[index]; | ||
if (index === uniqueid) { | ||
result = item; | ||
break; | ||
} | ||
} | ||
} | ||
return result; | ||
unregisterEventNode(nodeID) { | ||
let node = this; | ||
let index = node.nodesEvent.indexOf(nodeID); | ||
if (index !== -1) node.nodesEvent.splice(index, 1); | ||
} | ||
getItemsList(callback, forceRefresh = false) { | ||
var node = this; | ||
node.discoverDevices(function (items) { | ||
node.items_list = []; | ||
Object.keys(items).forEach(function (index) { | ||
var prop = items[index]; | ||
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); | ||
} | ||
node.items_list.push({ | ||
device_name: prop.name + ' : ' + prop.type, | ||
uniqueid: prop.uniqueid, | ||
meta: prop | ||
}); | ||
}); | ||
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); | ||
} | ||
registerNodeWithQuery(nodeID) { | ||
let node = this; | ||
if (!node.nodesWithQuery.includes(nodeID)) node.nodesWithQuery.push(nodeID); | ||
} | ||
callback(node.items_list); | ||
return node.items_list; | ||
}, forceRefresh); | ||
unregisterNodeWithQuery(nodeID) { | ||
let node = this; | ||
let index = node.nodesWithQuery.indexOf(nodeID); | ||
if (index !== -1) node.nodesWithQuery.splice(index, 1); | ||
} | ||
onClose() { | ||
var that = this; | ||
that.log('WebSocket connection closed'); | ||
that.emit('onClose'); | ||
clearInterval(that.refreshDiscoverTimer); | ||
that.socket.close(); | ||
that.socket = null; | ||
let node = this; | ||
node.ready = false; | ||
node.log('WebSocket connection closed'); | ||
node.emit('onClose'); | ||
clearInterval(node.refreshDiscoverTimer); | ||
node.socket.close(); | ||
node.socket = undefined; | ||
} | ||
onSocketPongTimeout() { | ||
var that = this; | ||
let that = this; | ||
that.warn('WebSocket connection timeout, reconnecting'); | ||
@@ -194,3 +377,3 @@ that.emit('onSocketPongTimeout'); | ||
onSocketUnauthorized() { | ||
var that = this; | ||
let that = this; | ||
that.warn('WebSocket authentication failed'); | ||
@@ -201,3 +384,3 @@ that.emit('onSocketUnauthorized'); | ||
onSocketError(err) { | ||
var that = this; | ||
let that = this; | ||
that.warn(`WebSocket error: ${err}`); | ||
@@ -208,3 +391,3 @@ that.emit('onSocketError'); | ||
onSocketClose(code, reason) { | ||
var that = this; | ||
let that = this; | ||
if (reason) { // don't bother the user unless there's a reason | ||
@@ -217,3 +400,3 @@ that.warn(`WebSocket disconnected: ${code} - ${reason}`); | ||
onSocketOpen(err) { | ||
var that = this; | ||
let that = this; | ||
that.log(`WebSocket opened`); | ||
@@ -223,42 +406,91 @@ that.emit('onSocketOpen'); | ||
onSocketMessage(dataParsed) { | ||
var that = this; | ||
that.emit('onSocketMessage', dataParsed); | ||
updateDevice(device, dataParsed) { | ||
let node = this; | ||
let changed = []; | ||
if (dataParsed.r == "scenes") { | ||
return; | ||
if (dotProp.has(dataParsed, 'name')) { | ||
device.name = dotProp.get(dataParsed, 'name'); | ||
changed.push('name'); | ||
} | ||
if (dataParsed.r == "groups") { | ||
dataParsed.uniqueid = "group_" + dataParsed.id; | ||
} | ||
['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; | ||
} | ||
for (var nodeId in that.devices) { | ||
var item = that.devices[nodeId]; | ||
var node = RED.nodes.getNode(nodeId); | ||
onSocketMessageSceneCalled(dataParsed) { | ||
console.warn("Need to implement onSocketMessageSceneCalled for " + JSON.stringify(dataParsed)); | ||
// TODO implement | ||
} | ||
if (dataParsed.uniqueid === item) { | ||
if (node && "server" in node) { | ||
//update server items db | ||
var serverNode = RED.nodes.getNode(node.server.id); | ||
if ("state" in dataParsed && dataParsed.state !== undefined && "items" in serverNode && dataParsed.uniqueid in serverNode.items) { | ||
serverNode.items[dataParsed.uniqueid].state = dataParsed.state; | ||
onSocketMessage(dataParsed) { | ||
let node = this; | ||
node.emit('onSocketMessage', dataParsed); //Used by event node, TODO Really used ? | ||
if (node.type === "deconz-input") { | ||
node.sendState(dataParsed); | ||
} | ||
} | ||
} else { | ||
console.log('ERROR: cant get ' + nodeId + ' node, removed from list'); | ||
delete that.devices[nodeId]; | ||
let device = node.device_list.getDeviceByDomainID(dataParsed.r, dataParsed.id); | ||
if (device === undefined) return; | ||
let changed = node.updateDevice(device, dataParsed); | ||
if (node && "server" in node) { | ||
var serverNode = RED.nodes.getNode(node.server.id); | ||
delete serverNode.items[dataParsed.uniqueid]; | ||
} | ||
} | ||
// 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); | ||
if (!target) { | ||
console.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 | ||
); | ||
let query = new Query(querySrc); | ||
if (query.match(device)) { | ||
matched.push(nodeID); | ||
} | ||
} | ||
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 | ||
}); | ||
} | ||
} | ||
@@ -265,0 +497,0 @@ |
@@ -18,5 +18,8 @@ { | ||
"dependencies": { | ||
"@node-red/util": "^2.0.6", | ||
"compare-versions": "^3.6.0", | ||
"dot-prop": "^6.0.1", | ||
"events": "latest", | ||
"multiple-select": "^1.4.1", | ||
"request": "latest", | ||
"got": "^11.8.2", | ||
"multiple-select": "^1.5.2", | ||
"ws": "latest" | ||
@@ -36,2 +39,3 @@ }, | ||
"nodes": { | ||
"api": "deconz.js", | ||
"in": "nodes/in.js", | ||
@@ -42,4 +46,3 @@ "get": "nodes/get.js", | ||
"battery": "nodes/battery.js", | ||
"server": "nodes/server.js", | ||
"api": "deconz.js" | ||
"server": "nodes/server.js" | ||
} | ||
@@ -51,3 +54,26 @@ }, | ||
}, | ||
"version": "1.3.3" | ||
"version": "2.0.0-beta.1", | ||
"devDependencies": { | ||
"grunt": "^1.3.0", | ||
"grunt-contrib-jshint": "^3.0.0", | ||
"grunt-contrib-uglify": "^5.0.1", | ||
"grunt-contrib-watch": "^1.1.0", | ||
"load-grunt-tasks": "^5.1.0", | ||
"mocha": "^8.3.2", | ||
"nyc": "^15.1.0", | ||
"should": "^13.2.3" | ||
}, | ||
"files": [ | ||
"deconz.*", | ||
"/nodes/", | ||
"/lib/", | ||
"/icons/*.png", | ||
"/resources/", | ||
"/examples/", | ||
"/src/migration", | ||
"/src/runtime" | ||
], | ||
"scripts": { | ||
"test": "mocha" | ||
} | ||
} |
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
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
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
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
1
325466
7
8
67
5790
+ Added@node-red/util@^2.0.6
+ Addedcompare-versions@^3.6.0
+ Addeddot-prop@^6.0.1
+ Addedgot@^11.8.2
+ Added@babel/runtime@7.25.6(transitive)
+ Added@node-red/util@2.2.3(transitive)
+ Added@sindresorhus/is@4.6.0(transitive)
+ Added@szmarczak/http-timer@4.0.6(transitive)
+ Added@types/cacheable-request@6.0.3(transitive)
+ Added@types/http-cache-semantics@4.0.4(transitive)
+ Added@types/keyv@3.1.4(transitive)
+ Added@types/node@22.5.5(transitive)
+ Added@types/responselike@1.0.3(transitive)
+ Addedcacheable-lookup@5.0.4(transitive)
+ Addedcacheable-request@7.0.4(transitive)
+ Addedclone-response@1.0.3(transitive)
+ Addedcompare-versions@3.6.0(transitive)
+ Addeddecompress-response@6.0.0(transitive)
+ Addeddefer-to-connect@2.0.1(transitive)
+ Addeddot-prop@6.0.1(transitive)
+ Addedend-of-stream@1.4.4(transitive)
+ Addedfs-extra@10.0.0(transitive)
+ Addedget-stream@5.2.0(transitive)
+ Addedgot@11.8.6(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedhttp-cache-semantics@4.1.1(transitive)
+ Addedhttp2-wrapper@1.0.3(transitive)
+ Addedi18next@21.6.11(transitive)
+ Addedis-obj@2.0.0(transitive)
+ Addedjson-buffer@3.0.1(transitive)
+ Addedjsonata@1.8.6(transitive)
+ Addedjsonfile@6.1.0(transitive)
+ Addedkeyv@4.5.4(transitive)
+ Addedlodash.clonedeep@4.5.0(transitive)
+ Addedlowercase-keys@2.0.0(transitive)
+ Addedmimic-response@1.0.13.1.0(transitive)
+ Addedmoment@2.30.1(transitive)
+ Addedmoment-timezone@0.5.34(transitive)
+ Addednormalize-url@6.1.0(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedp-cancelable@2.1.1(transitive)
+ Addedpump@3.0.2(transitive)
+ Addedquick-lru@5.1.1(transitive)
+ Addedregenerator-runtime@0.14.1(transitive)
+ Addedresolve-alpn@1.2.1(transitive)
+ Addedresponselike@2.0.1(transitive)
+ Addedundici-types@6.19.8(transitive)
+ Addeduniversalify@2.0.1(transitive)
+ Addedwrappy@1.0.2(transitive)
- Removedrequest@latest
- Removedajv@6.12.6(transitive)
- Removedasn1@0.2.6(transitive)
- Removedassert-plus@1.0.0(transitive)
- Removedasynckit@0.4.0(transitive)
- Removedaws-sign2@0.7.0(transitive)
- Removedaws4@1.13.2(transitive)
- Removedbcrypt-pbkdf@1.0.2(transitive)
- Removedcaseless@0.12.0(transitive)
- Removedcombined-stream@1.0.8(transitive)
- Removedcore-util-is@1.0.2(transitive)
- Removeddashdash@1.14.1(transitive)
- Removeddelayed-stream@1.0.0(transitive)
- Removedecc-jsbn@0.1.2(transitive)
- Removedextend@3.0.2(transitive)
- Removedextsprintf@1.3.0(transitive)
- Removedfast-deep-equal@3.1.3(transitive)
- Removedfast-json-stable-stringify@2.1.0(transitive)
- Removedforever-agent@0.6.1(transitive)
- Removedform-data@2.3.3(transitive)
- Removedgetpass@0.1.7(transitive)
- Removedhar-schema@2.0.0(transitive)
- Removedhar-validator@5.1.5(transitive)
- Removedhttp-signature@1.2.0(transitive)
- Removedis-typedarray@1.0.0(transitive)
- Removedisstream@0.1.2(transitive)
- Removedjsbn@0.1.1(transitive)
- Removedjson-schema@0.4.0(transitive)
- Removedjson-schema-traverse@0.4.1(transitive)
- Removedjsprim@1.4.2(transitive)
- Removedmime-db@1.52.0(transitive)
- Removedmime-types@2.1.35(transitive)
- Removedoauth-sign@0.9.0(transitive)
- Removedperformance-now@2.1.0(transitive)
- Removedpsl@1.9.0(transitive)
- Removedpunycode@2.3.1(transitive)
- Removedqs@6.5.3(transitive)
- Removedrequest@2.88.2(transitive)
- Removedsafe-buffer@5.2.1(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedsshpk@1.18.0(transitive)
- Removedtough-cookie@2.5.0(transitive)
- Removedtunnel-agent@0.6.0(transitive)
- Removedtweetnacl@0.14.5(transitive)
- Removeduri-js@4.4.1(transitive)
- Removeduuid@3.4.0(transitive)
- Removedverror@1.10.0(transitive)
Updatedmultiple-select@^1.5.2