UNPKG

node-red-contrib-xkeys_intensity

Version:

Node-RED node to set intensity Backlight nodes using Dynamic Control Data Protocol (DCDP)

360 lines (331 loc) 11.3 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 = []; var httpAdminDataIntensity = {}; function XkeysIntensity(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_backlight_intensity")) { 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 a msg.payloads of at least: * {"intensity": VAL} * in which case the node's configured pid_list & uinitid values are used * */ node.on('input', function(msg) { var mkeys = Object.keys(msg.payload); //node.log(`msg.payload: ${JSON.stringify(msg.payload)}`); //console.log(`mkeys: ${JSON.stringify(mkeys)}`); if (mkeys.includes("intensity")) { try { var intensity = []; /* Check input integrity. * * Must be a number (within range) * or 2-element array (each element within range) */ var intensity_in = JSON.parse(msg.payload.intensity); if (typeof(intensity_in) == "number") { intensity.push(intensity_in); intensity.push(intensity_in); } if (Array.isArray(intensity_in) && (intensity_in.length == 2)) { intensity_in.forEach( (val) => { intensity.push(val); }); } if ( intensity.length != 2 ) { node.log(`Can't determine good intensity input value from ${msg.payload.intensity}. Discontinuing ...`) return; } var good_input = true; intensity.forEach( (val) => { if( (isNaN(parseInt(val))) || (parseInt(val) < 0) || (parseInt(val) > 255) ) { good_input = false; } }); if (!good_input) { node.log(`array element in ${intensity} is no good. Discontinuing ...`); return; } // Now check the rest var pid_list = "[]"; var unit_id = ""; var duplicate_id = ""; 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 = ""; 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; }); //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_backlight_intensity", vendor_id : device.device_quad.split('-')[0], product_id : device.device_quad.split('-')[1], unit_id : device.device_quad.split('-')[2], duplicate_id : device.device_quad.split('-')[3], value : intensity } 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; }); } catch (err) { node.log(`Trouble with input: ${err}`); } } else { console.log("Not interested in this msg (no \"intensity\" 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 XkeysIntensity RED.nodes.registerType("xkeys_intensity", XkeysIntensity); RED.httpAdmin.get("/xkeys_intensity/products", function (req, res) { res.json(httpAdminDataProducts); }); RED.httpAdmin.get("/xkeys_intensity/devices", function (req, res) { res.json(httpAdminDataDevices); }); RED.httpAdmin.post("/xkeys_intensity_inject/:id", RED.auth.needsPermission("xkeys_intensity_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); } }); }