node-red-contrib-xkeys_writedata
Version:
Xkeys Write Data node for Node-RED with Dynamic Control Data Protocol (DCDP)
307 lines (283 loc) • 9.56 kB
JavaScript
module.exports = function(RED) {
const XKEYS_VENDOR_ID = "1523";
var mqtt = require('mqtt');
const connectUrl = 'mqtt://localhost';
const qos = 0;
var httpAdminDataProducts = {};
var httpAdminDataDevices = [];
function XkeysWriteData(config) {
RED.nodes.createNode(this, config);
var node = this;
node.config = config;
//node.log(`node.config = ${JSON.stringify(node.config)}`);
//node.log(`myId = ${node.config.id}`);
var client = mqtt.connect(connectUrl);
client.on('reconnect', (error) => {
node.log('reconnecting:', error)
})
client.on('error', (error) => {
node.log('Connection failed:', error)
})
client.on('connect', () => {
node.log('connected')
client.subscribe({'/dcdp/server/#':{qos:qos}}, function (err, granted) {
if (!err) {
node.log("Subscribed OK, granted: " + JSON.stringify(granted));
client.publish('/dcdp/node', JSON.stringify({msg_type:"all_product_data"}));
client.publish('/dcdp/node/xkeys_writedata', JSON.stringify({msg_type:"list_attached"}));
} else {
node.log('Subscription failed: ' + err)
}
})
})
client.on('close', () => {
node.log("connection closed");
})
client.on('message', (topic, message) => {
//node.log(`received topic:${topic}, msg: ${message}`);
var message_obj = "";
try {
message_obj = JSON.parse(message);
//node.log("SID = " + message_obj.sid);
if (message_obj.msg_type == "hello") {
console.log(`Hello from DCDP server at ${message_obj.sid} - must have just (re)started `);
// In case dcdp-server restarted with updated devices/product list
client.publish('/dcdp/node', JSON.stringify({msg_type:"all_product_data"}));
client.publish('/dcdp/node', JSON.stringify({msg_type:"list_attached"}));
}
else if (message_obj.msg_type == "list_attached_result") {
// data should be an array of infos
httpAdminDataDevices = [];
message_obj.devices.forEach( (device) => {
// Limit to xkeys devices
if (device.vendorId == XKEYS_VENDOR_ID) {
httpAdminDataDevices.push(device);
}
});
// Based on attached deviceList, are we connected to something?
var pid_list = this.config.pid_list || "[]";
if (device_connected(this.config.vendor_id || XKEYS_VENDOR_ID, JSON.parse(pid_list), this.config.unit_id, this.config.duplicate_id)) {
node.status( {fill:"green",shape:"dot",text:"connected"} );
} else {
node.status( {fill:"red",shape:"ring",text:"disconnected"} );
}
}
else if (message_obj.msg_type == "all_product_data_result") {
// data should be a dict of product objects
httpAdminDataProducts = {};
Object.keys(message_obj.data).forEach( (device) => {
// Restrict to X-keys products
if (message_obj.data[device].vendorId == XKEYS_VENDOR_ID ) {
httpAdminDataProducts[device] = message_obj.data[device];
}
});
}
else {
// Logging here may be useful but is quietened for production
//node.log(`Received unhandled request: ${message_obj.msg_type}`);
}
}
catch (e) {
node.log('ERROR parsing message: ' + e);
}
})
/*
* Input messages
* We expect a msg.payloads of
*
*/
node.on('input', function(msg) {
//console.log(`INPUT: ${JSON.stringify(msg)}`);
console.log(`msg.payload: ${JSON.stringify(msg.payload)}`);
/*
* For this node we require a pid_list, unit_id & duplicate_id all to be configured in the node itself.
*
* A commandcode can originate from the node itself (internal),
* or from another prior node in the flow (external).
* An internal command code takes precedence over an external one,
* so only use the external one if no internal commandcode exists.
*/
var commandcode = [];
var configured_commandcode = [];
var message_commandcode = [];
if (!node.config.commandcode ) { node.config.commandcode = "[]"; }
try {
configured_commandcode = JSON.parse(node.config.commandcode);
}
catch (err) {
node.log("Error parsing configured commandcode: " + err);
return;
}
if (configured_commandcode.length > 0) {
commandcode = configured_commandcode;
} else {
try {
message_commandcode = JSON.parse(msg.payload.commandcode);
commandcode = message_commandcode;
}
catch (err) {
node.log("Error parsing commandcode: " + err);
return;
}
}
/* Don't bother sending an empty commandcode */
if (commandcode.length == 0) { return; }
/* If pid_list has multiple members, find the (first) one that matches an attached device.
*/
var configured_pid_list = [];
var target_pid;
try {
configured_pid_list = JSON.parse(node.config.pid_list);
configured_pid_list.every( (pid) => {
if (httpAdminDataDevices.find( (device) => device.productId == pid)) {
target_pid = pid.toString();
return false;
}
return true;
});
}
catch (err) {
console.log(`Caught exception parsing node.config.pid_list. ${err}`);
}
var quad_list = [];
var quad_list_regex_string = XKEYS_VENDOR_ID;
quad_list_regex_string += "-" + target_pid;
if (node.config.unit_id) {
quad_list_regex_string += "-" + node.config.unit_id;
} else {
quad_list_regex_string += "-\[0-9\]+";
}
if (node.config.duplicate_id) {
quad_list_regex_string += "-" + node.config.duplicate_id;
} else {
quad_list_regex_string += "-\[0-9\]+";
}
//console.log(`quad_list_regex_string = ${quad_list_regex_string}`);
var regex = new RegExp(quad_list_regex_string);
httpAdminDataDevices.forEach( (dev) => {
//console.log(`dev: ${JSON.stringify(dev.device_quad)}`);
if (regex.test(dev.device_quad)) {
quad_list.push(dev.device_quad);
}
});
//console.log(`quad_list = ${JSON.stringify(quad_list)}`);
if (quad_list.length == 0 ) {
node.log(`No devices matched: discontinuing`);
return;
}
/* Prepare output msg but only bother if we have a device with matching device_quad.
*/
httpAdminDataDevices.every( (device) => {
if (quad_list.includes(device.device_quad)) {
const command_message = {
msg_type : "command",
command_type : "write_data",
vendor_id : XKEYS_VENDOR_ID,
product_id : device.device_quad.split('-')[1],
unit_id : device.device_quad.split('-')[2],
duplicate_id : device.device_quad.split('-')[3],
byte_array : commandcode
}
console.log(`command_message = ${JSON.stringify(command_message)}`);
client.publish('/dcdp/node', JSON.stringify(command_message));
// Don't break the every loop on first match - find all matches.
//return false;
}
return true;
});
})
this.on('close', function(done) {
client.end();
done();
})
// Does any attached device match specified pids, unit_id & dup_id ?
// pids: array of possible PIDs for a device (empty => ANY)
// unit_id: unitId of a device
// dup_id: duplicate_id of a device
function device_connected(...deviceArgs) {
const vendor_id = deviceArgs[0];
const pids = deviceArgs[1];
const unit_id = deviceArgs[2];
const dup_id = deviceArgs[3];
var device_matched = false;
var regex_string = ""
var regex;
if (pids.length == 0) {
if (vendor_id) {
regex_string = vendor_id + "-";
} else {
regex_string = "\[0-9\]+-";
}
// No product_ids provided => ANY product_id
regex_string = regex_string + "\[0-9\]+-";
if (unit_id) {
regex_string = regex_string + unit_id + "-";
} else {
regex_string = regex_string + "\[0-9\]+-";
}
if (dup_id) {
regex_string = regex_string + dup_id;
} else {
regex_string = regex_string + "\[0-9\]+";
}
regex = new RegExp(regex_string);
httpAdminDataDevices.forEach( (item) => {
if (regex.test(item.device_quad)) { device_matched = true; }
})
} else {
// An array of endpoints provided
pids.forEach(function (item) {
if (vendor_id) {
regex_string = vendor_id + "-";
} else {
regex_string = "\[0-9\]+-"
}
regex_string = regex_string + item + "-";
if (unit_id) {
regex_string = regex_string + unit_id + "-";
} else {
regex_string = regex_string + "\[0-9\]+-";
}
if (dup_id) {
regex_string = regex_string + dup_id;
} else {
regex_string = regex_string + "\[0-9\]+";
}
regex = new RegExp(regex_string);
httpAdminDataDevices.forEach( (item) => {
if (regex.test(item.device_quad)) { device_matched = true; }
})
})
}
return device_matched;
} // function device_connected
} // function XkeysWriteData
RED.nodes.registerType("xkeys_writedata", XkeysWriteData);
RED.httpAdmin.get("/xkeys_writedata/products", function (req, res) {
res.json(httpAdminDataProducts);
});
RED.httpAdmin.get("/xkeys_writedata/devices", function (req, res) {
res.json(httpAdminDataDevices);
});
RED.httpAdmin.post("/xkeys_writedata_inject/:id", RED.auth.needsPermission("xkeys_writedata_inject.write"), function(req,res) {
//console.log("posting something: " + JSON.stringify(req.body));
var node = RED.nodes.getNode(req.params.id);
if (node != null) {
try {
if (req.body) {
node.receive(req.body);
} else {
node.receive();
}
res.sendStatus(200);
}
catch (err) {
res.sendStatus(500);
node.error(RED._("inject.failed",{error:err.toString()}));
}
} else {
console.log("bad post");
res.sendStatus(404);
}
});
}