UNPKG

node-red-contrib-xkeys_backlight

Version:

Xkeys Backlight node for Node-RED using Dynamic Cotrol Data Protocol (DCDP)

444 lines (411 loc) 13.8 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 XkeysBacklight(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.request == "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); } }); 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_backlight" || message_obj.command_type == "set_all_backlights")) { 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 * Minimal message expected has msg.payload of * { action: STARTSTOP } * * The msg.payload may also inlude control_id:ID and flashing:YESNO * as well as the usual pidlist/product_id, unit_id & duplicate_id. * If these are not included in the message, values from node configuration are used. */ node.on('input', function(msg) { //console.log("msg.payload = " + JSON.stringify(msg.payload)); var mkeys = Object.keys(msg.payload); if (mkeys.includes("action")) { var action = msg.payload.action; var control_ids = []; var flashing = false; if ((action != "start") && (action != "stop")) { node.log(`Unknown action: ${action}. Discontinuing ...`); return; } /* For some convenience later on, temporarily convert * (string) control_id into (array) control_ids */ if (mkeys.includes("control_id")) { control_ids = msg.payload.control_id.split(','); } else { // use configured control_id instead if (node.config.control_id != "") { // comma separated numbers control_ids = node.config.control_id.split(','); } else { // No control_id specified => all buttons? } } if (mkeys.includes("flashing")) { flashing = msg.payload.flashing; } else { // else use configured setting if (node.config.flashing) { flashing = (node.config.flashing.toLowerCase()=="true"); } } var hue_channels = ""; if (mkeys.includes("hue_channels")) { hue_channels = msg.payload.hue_channels; } else { // else use configured setting hue_channels = this.config.hue_channels; } /* */ 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. */ const control_ids_is_empty = (control_ids.length == 0); httpAdminDataDevices.every( (device) => { if (quad_list.includes(device.device_quad)) { /* If control_ids is empty (=> apply to all keys), * we need to figure out how many keys this device has * (unless flashing is off, in which case we can use 'set_all_backlights') */ if (control_ids_is_empty && flashing) { control_ids = []; for (var i=0;i<device.colCount*device.rowCount;i++) { control_ids.push(i+1); } } const command_message = { msg_type : "command", command_type : control_ids.length==0?"set_all_backlights":"set_backlight", 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 : control_ids.join(), value : action=="start"?1:0, flash : flashing?1:0 } if (action == "start") { command_message["color"] = hue_channels.length==0?"ffffff":hue_channels; } else { command_message["color"] = "000000"; } node.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 { node.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 /* // Does any attached device match configured PID & UID ? // pids: array of possible PIDs for a device (empty => ANY) // unit_id: integer unitId of a device function device_connected (pids, unit_id) { var matched_pid; var matched_unit_id; var unit_id = unit_id; if (! unit_id) { unit_id = "ANY" } var device_connected = false; var devs = Object.keys(httpAdminDataDevices); if (devs.length > 0 ) { if (pids.length == 0) { matched_pid = "ANY"; // Since any pid will do, // we just need to find one matching unit_id if (unit_id == "ANY") { matched_unit_id = "ANY"; } else { devs.forEach(function (item) { if (! matched_unit_id) { if (unit_id == httpAdminDataDevices[item].unitId) { matched_unit_id = httpAdminDataDevices[item].unitId; } } }) } } else { // A particular PID was specified, // so need to find matching UID for that device devs.forEach(function (item) { if (pids.includes(httpAdminDataDevices[item].productId)) { matched_pid = httpAdminDataDevices[item].productId if (unit_id == "ANY") { matched_unit_id = "ANY"; } else { if (unit_id == httpAdminDataDevices[item].unitId) { matched_unit_id = httpAdminDataDevices[item].unitId; } } } }); } } else { // no attached devices matched_pid = false; matched_unit_id = false; } return (pids.length==0 || pids.includes(matched_pid)) && (matched_unit_id == unit_id) ; } */ } // function XkeysBacklight RED.nodes.registerType("xkeys_backlight", XkeysBacklight); RED.httpAdmin.get("/xkeys_backlight/products", function (req, res) { res.json(httpAdminDataProducts); }); RED.httpAdmin.get("/xkeys_backlight/devices", function (req, res) { res.json(httpAdminDataDevices); }); RED.httpAdmin.post("/xkeys_backlight_inject/:id", RED.auth.needsPermission("xkeys_backlight_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); } }); }