UNPKG

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