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