node-red-contrib-xkeys_led
Version:
Xkeys LED node for Node-RED using Dynamic Control Data Protocol (DCDP)
363 lines (335 loc) • 11.1 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 XkeysLED(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.server_id}`);
if (message_obj.msg_type == "hello") {
node.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 if ((message_obj.msg_type == "command_result") && (message_obj.command_type == "set_indicator_led")) {
node.log(`command_result: ${message}`);
}
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 msg.payload of at least:
* { action: STARTSTOP }
* All other fields
* (ledid, flashing, pid_list/product_id, unit_id & duplicate_id)
* are optional.
* If not supplied as part of the message,
* these values are taken from the node's configuration.
*/
node.on('input', function(msg) {
var mkeys = Object.keys(msg.payload);
//console.log(`mkeys: ${JSON.stringify(mkeys)}`);
//node.log(`msg.payload = ${JSON.stringify(msg.payload)}`);
if (mkeys.includes("action")) {
var action = msg.payload.action;
var ledids = [];
var flashing = true;
if ((action != "start") && (action != "stop")) {
node.log("Unknown action: " + action);
return;
}
if (mkeys.includes("ledid")) {
ledids = msg.payload.ledid.split(',');
} else {
// use configured ledid instead
if (node.config.ledid) {
// comma separated numbers
ledids = node.config.ledid.split(',');
} else {
// No ledids specified - use the default (2)
ledids.push("2");
}
}
if (mkeys.includes("flashing")) {
flashing = msg.payload.flashing;
} else {
// else use configured setting
if (node.config.flashing) {
flashing = (node.config.flashing.toLowerCase()=="true");
}
}
var pid_list = "[]";
var unit_id = "";
var duplicate_id = "";
/* If product_id is specified, install it into pid_list
* (similar to node configuration editor).
* Otherwise use pid_list "as is".
*/
if (mkeys.includes("product_id")) {
var temp = [];
temp.push(parseInt(msg.payload.product_id));
pid_list = JSON.stringify(temp);
} else {
if (mkeys.includes("pid_list")) {
pid_list = msg.payload.pid_list;
} else {
pid_list = node.config.pid_list;
}
}
//console.log(`pid_list: ${pid_list}`);
if (mkeys.includes("unit_id")) {
unit_id = msg.payload.unit_id;
} else {
unit_id = node.config.unit_id;
}
if (mkeys.includes("duplicate_id")) {
duplicate_id = msg.payload.duplicate_id;
} else {
duplicate_id = node.config.duplicate_id;
}
/* Find first member of pid_list that matches an attached device.
*/
var product_id = "";
try {
var configured_pid_list = JSON.parse(pid_list);
configured_pid_list.every( (pid) => {
if (httpAdminDataDevices.find( (device) => device.productId == pid)) {
product_id = pid.toString();
return false;
}
return true;
});
}
catch (err) {
node.log(`Caught exception parsing pid_list. ${err}`);
}
//console.log(`product_id = ${product_id}`);
/* An empty pid_list will not have given us a valid product_id.
* That's OK since empty pid_list => all possible product_ids.
*
* However if no product_id has been found when pid_list is not empty,
* it means no attached device matched what was in the pid_list
* i.e. nothing to do
*/
if ( (!product_id) && (JSON.parse(pid_list).length > 0) ) {
node.log(`No device matching product_id or pid_list was found. Discontinuing.`);
return;
}
/* Generate a list of device_quad targets.
*/
var quad_list = [];
var quad_list_regex_string = XKEYS_VENDOR_ID;
if (product_id) {
quad_list_regex_string += "-" + product_id;
} else {
quad_list_regex_string += "-\[0-9\]+";
}
if (unit_id) {
quad_list_regex_string += "-" + unit_id;
} else {
quad_list_regex_string += "-\[0-9\]+";
}
if (duplicate_id) {
quad_list_regex_string += "-" + 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 : "set_indicator_led",
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],
control_id : ledids.join(),
value : (action=="start")?1:0,
flash : (flashing)?1:0
}
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;
});
} else {
console.log("Not interested in this msg (no \"action\" field)");
}
})
this.on('close', function(done) {
client.end();
done();
})
// Does any attached device match specified vendor_id, pids, unit_id & dup_id ?
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 XkeysLED
RED.nodes.registerType("xkeys_led", XkeysLED);
RED.httpAdmin.get("/xkeys_led/products", function (req, res) {
res.json(httpAdminDataProducts);
});
RED.httpAdmin.get("/xkeys_led/devices", function (req, res) {
res.json(httpAdminDataDevices);
});
RED.httpAdmin.post("/xkeys_led_inject/:id", RED.auth.needsPermission("xkeys_led_inject.write"), function(req,res) {
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);
}
});
}