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
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 = [];
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);
}
});
}