Socket
Socket
Sign inDemoInstall

node-red-contrib-deconz

Package Overview
Dependencies
Maintainers
2
Versions
146
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

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

Comparing version 1.3.3 to 2.0.0-beta.1

resources/css/common.css

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 @@

@@ -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);
};
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"
}
}
}
}
}
}
}
}

@@ -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

SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc