UNPKG

node-red-contrib-openzwave

Version:

ZWave for node-red through OpenZWave, the open source ZWave library

582 lines (534 loc) 19.1 kB
/* OpenZWave nodes for IBM's Node-Red https://github.com/ekarak/node-red-contrib-openzwave (c) 2014-2017, Elias Karakoulakis <elias.karakoulakis@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ var fs = require('fs'); var path = require('path'); var util = require('util'); var UUIDPREFIX = "_macaddr_"; var HOMENAME = "_homename_"; var ozwsharedpath = path.dirname(path.dirname(require.resolve('openzwave-shared'))); var ozwsharedpackage = JSON.parse(fs.readFileSync(ozwsharedpath+"/package.json")); var thispackage = JSON.parse(fs.readFileSync(__dirname+'/package.json')); var gm = require('getmac'); if (typeof gm.default=='function') { UUIDPREFIX = gm.default().replace(/:/gi, ); } else { gm.getMac(function(err, macAddress) { if (err) throw err; UUIDPREFIX = macAddress.replace(/:/gi, ''); }); } module.exports = function(RED) { function log(level, message, method) { if (level >= logging) { RED.log[method || 'info'].apply(this, ["OpenZwave: "+message]); } } RED.log.info('node-red-contrib-openzwave: ' + thispackage.version); RED.log.info('openzwave-shared: ' + ozwsharedpackage.version); var OpenZWave = require('openzwave-shared'); var ozwConfig = {}; var ozwDriver = null; var ozwConnected = false; var ozwBoundEvents = false; var driverReadyStatus = false; var allowunreadyupdates = false; var logging = "minimal"; // Provide context.global access to ZWave node info. RED.settings.functionGlobalContext.openzwaveNodes = {}; // event routing map: which NR node gets notified for each zwave event var nrNodeSubscriptions = {}; // {'event1' => {node1: closure1, node2: closure2...}, 'event2' => ...} /* ============================================================================ * ZWSUBSCRIBE: subscribe a Node-Red node to OpenZWave events * ============================================================================ **/ function zwsubscribe(nrNode, event, callback) { if (!(event in nrNodeSubscriptions)) { nrNodeSubscriptions[event] = {}; } nrNodeSubscriptions[event][nrNode.id] = callback; log('full', util.format('%s(%s) subscribed to \"%s\"', nrNode.type, nrNode.id, event)); } // and unsubscribe function zwunsubscribe(nrNode) { for (var event in nrNodeSubscriptions) { if (nrNodeSubscriptions.hasOwnProperty(event)) { delete nrNodeSubscriptions[event][nrNode.id]; log('full', util.format('%s(%s) unsubscribed from \"%s\"', nrNode.type, nrNode.id, event)); } } } /* ============================================================================ * ZWCALLBACK: dispatch OpenZwave events onto all active Node-Red subscriptions * ============================================================================ **/ function zwcallback(event, arghash) { log('full', util.format("%s, args: %j", event, arghash)); // Add uuid if (arghash.nodeid !== undefined && HOMENAME !== undefined) arghash.uuid = UUIDPREFIX + '-' + HOMENAME + '-' + arghash.nodeid; if (nrNodeSubscriptions.hasOwnProperty(event)) { var nrNodes = nrNodeSubscriptions[event]; // an event might be subscribed by multiple NR nodes for (var nrnid in nrNodes) { var nrNode = RED.nodes.getNode(nrnid); log('full', "\t\\==> " + nrnid + " event:" + event); nrNodes[nrnid].call(nrNode, event, arghash); // update the node status accordingly var status = {fill: "yellow", text: event, shape: "ring"}; var transient = true; switch(event) { case 'node event': case 'node ready': case 'node removed': status.text = util.format('node %j: %s', arghash.nodeid, event); break; case 'value changed': status.text = util.format('node %j: %s', arghash.nodeid, event); break; case 'notification': case 'controller command': transient = false; status.text = util.format('%s', arghash.help); break; default: break; } updateNodeRedStatus(nrNode, status); if (transient) { setTimeout(function() { updateNodeRedStatus(nrNode); }, 500); } } } } // update the NR node's status indicator function updateNodeRedStatus(nrNode, options) { // update NR node status nrNode.status(options || { fill: driverReadyStatus ? "green" : "red", text: driverReadyStatus ? "connected" : "disconnected", shape: "ring" }); } function driverReady(homeid) { driverReadyStatus = true; ozwConfig.homeid = homeid; var homeHex = '0x' + homeid.toString(16); HOMENAME = homeHex; ozwConfig.name = homeHex; log('minimal', 'scanning network with homeid: ' + homeHex); zwcallback('driver ready', ozwConfig); } function driverFailed() { zwcallback('driver failed', ozwConfig); process.exit(); } function nodeAdded(nodeid) { RED.settings.functionGlobalContext.openzwaveNodes[nodeid] = { manufacturer: '', manufacturerid: '', product: '', producttype: '', productid: '', type: '', name: '', loc: '', classes: {}, ready: false, }; zwcallback('node added', { "nodeid": nodeid }); } function nodeRemoved(nodeid) { RED.settings.functionGlobalContext.openzwaveNodes[nodeid] = { manufacturer: '', manufacturerid: '', product: '', producttype: '', productid: '', type: '', name: '', loc: '', classes: {}, ready: false, }; zwcallback('node removed', { "nodeid": nodeid }); } function valueAdded(nodeid, comclass, valueId) { var ozwnode = RED.settings.functionGlobalContext.openzwaveNodes[nodeid]; if (!ozwnode) { log('off', 'valueAdded: no such node: '+nodeid, 'error'); } if (!ozwnode['classes'][comclass]) ozwnode['classes'][comclass] = {}; if (!ozwnode['classes'][comclass][valueId.instance]) ozwnode['classes'][comclass][valueId.instance] = {}; // add to cache log('full', "added "+JSON.stringify(valueId)); ozwnode['classes'][comclass][valueId.instance][valueId.index] = valueId; // tell NR zwcallback('value added', { "nodeid": nodeid, "cmdclass": comclass, "instance": valueId.instance, "cmdidx": valueId.index, "currState": valueId['value'], "label": valueId['label'], "units": valueId['units'], "value": valueId }); } function valueChanged(nodeid, comclass, valueId) { var ozwnode = RED.settings.functionGlobalContext.openzwaveNodes[nodeid]; if (!ozwnode) { log('off', 'valueChanged: no such node: '+nodeid, 'error'); } else { // valueId: OpenZWave ValueID (struct) - not just a boolean var oldst; if (ozwnode.ready || allowunreadyupdates) { oldst = ozwnode['classes'][comclass][valueId.instance][valueId.index].value; log('full', util.format( 'zwave node %d: changed: %d:%s:%s -> %j', nodeid, comclass, valueId['label'], oldst, JSON.stringify(valueId))); // tell NR only if the node is marked as ready zwcallback('value changed', { "nodeid": nodeid, "cmdclass": comclass, "cmdidx": valueId.index, "instance": valueId.instance, "oldState": oldst, "currState": valueId['value'], "label": valueId['label'], "units": valueId['units'], "value": valueId }); } // update cache ozwnode['classes'][comclass][valueId.instance][valueId.index] = valueId; } } function valueRemoved(nodeid, comclass, instance, index) { var ozwnode = RED.settings.functionGlobalContext.openzwaveNodes[nodeid]; if (ozwnode && ozwnode['classes'] && ozwnode['classes'][comclass] && ozwnode['classes'][comclass][instance] && ozwnode['classes'][comclass][instance][index]) { delete ozwnode['classes'][comclass][instance][index]; zwcallback('value deleted', { "nodeid": nodeid, "cmdclass": comclass, "cmdidx": index, "instance": instance }); } else { log('off', 'valueRemoved: no such node: '+nodeid, 'error');} } function nodeReady(nodeid, nodeinfo) { var ozwnode = RED.settings.functionGlobalContext.openzwaveNodes[nodeid]; if (ozwnode) { for (var attrname in nodeinfo) { if (nodeinfo.hasOwnProperty(attrname)) { ozwnode[attrname] = nodeinfo[attrname]; } } ozwnode.ready = true; // log('full', 'only|R|W| (nodeid-cmdclass-instance-index): type : current state'); for (var comclass in ozwnode['classes']) { switch (comclass) { case 0x25: // COMMAND_CLASS_SWITCH_BINARY case 0x26: // COMMAND_CLASS_SWITCH_MULTILEVEL case 0x30: // COMMAND_CLASS_SENSOR_BINARY case 0x31: // COMMAND_CLASS_SENSOR_MULTILEVEL case 0x60: // COMMAND_CLASS_MULTI_INSTANCE ozwDriver.enablePoll(nodeid, comclass); break; } var values = ozwnode['classes'][comclass]; for (var inst in values) for (var idx in values[inst]) { var ozwval = values[inst][idx]; var rdonly = ozwval.read_only ? '*' : ' '; var wronly = ozwval.write_only ? '*' : ' '; log('full', util.format( '\t|%s|%s| %s: %s:\t%s\t', rdonly, wronly, ozwval.value_id, ozwval.label, ozwval.value)); } } // zwcallback('node ready', { nodeid: nodeid, nodeinfo: nodeinfo }); } } function nodeEvent(nodeid, evtcode) { zwcallback('node event', { "nodeid": nodeid, "event": evtcode }); } function sceneEvent(nodeid, scene) { zwcallback('scene event', { "nodeid": nodeid, "scene": scene }); } function notification(nodeid, notif, help) { log('full', util.format('node %d: %s', nodeid, help)); zwcallback('notification', { nodeid: nodeid, notification: notif, help: help }); } function scanComplete() { log('minimal', 'network scan complete.'); zwcallback('scan complete', {}); } function controllerCommand(nodeid, state, errcode, help) { var obj = { nodeid: nodeid, state: state, errcode: errcode, help: help }; log('full', util.format('command feedback received: %j', JSON.stringify(obj))); zwcallback('controller command', obj); } // list of events emitted by OpenZWave and redirected to Node flows by the mapped function var ozwEvents = { 'driver ready': driverReady, 'driver failed': driverFailed, 'node added': nodeAdded, 'node ready': nodeReady, 'node event': nodeEvent, 'scene event': sceneEvent, 'value added': valueAdded, 'value changed': valueChanged, 'value removed': valueRemoved, 'notification': notification, 'scan complete': scanComplete, 'controller command': controllerCommand, 'node removed': nodeRemoved }; // ========================== function ZWaveController(cfg) { // ========================== RED.nodes.createNode(this, cfg); var node = this; this.name = cfg.port; logging = cfg.logging; // initialize OpenZWave upon boot or fetch it from the global reference // (used across Node-Red deployments which recreate all the nodes) // so we only get to initialise one single OZW driver (a lengthy process) if (!ozwDriver) { ozwDriver = new OpenZWave({ Logging: (logging != "off"), ConsoleOutput: (logging != "off"), QueueLogLevel: ((logging == "full") ? 8 : 6), UserPath: RED.settings.userDir, DriverMaxAttempts: cfg.driverattempts, NetworkKey: cfg.networkkey||"" }); } if (!ozwBoundEvents) { /* =========== bind to low-level OpenZWave events ============== */ Object.keys(ozwEvents).forEach(function(evt) { log('full', node.name + ' addListener ' + evt); ozwDriver.on(evt, ozwEvents[evt]); }); ozwBoundEvents = true; } /* =============== Node-Red events ================== */ this.on("close", function() { log('full', 'zwave-controller: close'); // write out zwcfg_homeid.xml to disk ozwDriver.writeConfig(); // controller should also unbind from the C++ addon if (ozwDriver) { ozwDriver.removeAllListeners(); ozwBoundEvents = false; } }); zwsubscribe(node, 'driver failed', function(event, data) { log('minimal', 'Driver failed. Please check if there a ZWave stick attached to ' + cfg.port, 'error'); }); zwsubscribe(node, 'scan complete', function(event, data) { ozwDriver.setPollInterval(cfg.pollinterval); allowunreadyupdates = cfg.allowunreadyupdates; }); /* time to connect */ if (!ozwConnected) { log('minimal', 'connecting to '+cfg.port); ozwDriver.connect(cfg.port); ozwConnected = true; } } // RED.nodes.registerType("zwave-controller", ZWaveController); // // ========================= function ZWaveIn(config) { // ========================= RED.nodes.createNode(this, config); this.name = config.name; // var node = this; var zwaveController = RED.nodes.getNode(config.controller); if (!zwaveController) { log('minimal', 'no ZWave controller class defined!', 'error'); return; } /* =============== Node-Red events ================== */ this.on("close", function() { // set zwave node status as disconnected updateNodeRedStatus(node, {fill: "red", shape: "ring", text: "disconnected"}); // remove all event subscriptions for this node zwunsubscribe(this); log('full', 'zwave-in: close'); }); this.on("error", function() { // what? there are no errors. there never were. updateNodeRedStatus(node, {fill: "yellow",shape: "ring", text: "error"}); }); /* =============== OpenZWave events ================== */ Object.keys(ozwEvents).forEach(function(key) { zwsubscribe(node, key, function(event, data) { var msg = { 'topic': 'zwave: ' + event }; if (data) msg.payload = data; node.send(msg); }); }); // set initial node status upon creation updateNodeRedStatus(node); } // RED.nodes.registerType("zwave-in", ZWaveIn); // // ========================= function ZWaveOut(config) { // ========================= RED.nodes.createNode(this, config); this.name = config.name; // var node = this; var zwaveController = RED.nodes.getNode(config.controller); if (!zwaveController) { log('minimal', 'no ZWave controller class defined!', 'error'); return; } /* =============== Node-Red events ================== */ // this.on("input", function(msg) { log('full', util.format("input: %j", msg)); var payload; try { payload = (typeof(msg.payload) === "string") ? JSON.parse(msg.payload) : msg.payload; } catch (err) { node.error(node.name + ': illegal msg.payload! (' + err + ')'); return; } switch (true) { // switch On/Off: for basic single-instance switches and dimmers case /switchOn/.test(msg.topic): ozwDriver.setValue(payload.nodeid, 37, 1, 0, true); break; case /switchOff/.test(msg.topic): ozwDriver.setValue(payload.nodeid, 37, 1, 0, false); break; // setLevel: for dimmers case /setLevel/.test(msg.topic): ozwDriver.setValue(payload.nodeid, 38, 1, 0, payload.value); break; // setValue: for everything else case /setValue/.test(msg.topic): log('full', util.format("ZWaveOut.setValue payload: %j", payload)); ozwDriver.setValue( { node_id: payload.nodeid, class_id: (payload.cmdclass || 37), // default cmdclass: on-off instance: (payload.instance || 1), // default instance index: (payload.cmdidx || 0), // default cmd index }, payload.value ); break; /* EXPERIMENTAL: send basically every available command down * to OpenZWave, just name the function in the message topic * and pass in the arguments as "payload.args" as an array: * {"topic": "someOpenZWaveCommand", "payload": {"args": [1, 2, 3]}} * If the command needs the HomeID as the 1st arg, use "payload.prependHomeId" * */ default: if (msg.topic && typeof ozwDriver[msg.topic] === 'function' && payload) { var args = payload.args || []; if (payload.prependHomeId) args.unshift(ozwConfig.homeid); log('minimal', 'attempting direct API call to ' + msg.topic + '()'); try { var result = ozwDriver[msg.topic].apply(ozwDriver, args); log('minimal', 'direct API call success, result=' + JSON.stringify(result)); if (typeof result != 'undefined') { msg.payload.result = result; // send off the direct API call's result to the output node.send(msg); } } catch (err) { log('minimal', 'direct API call to ' + msg.topic + ' failed: ' + err, 'error'); } } } }); this.on("close", function() { // set zwave node status as disconnected updateNodeRedStatus(node, { fill: "red", shape: "ring", text: "disconnecting" }); // remove all event subscriptions for this node zwunsubscribe(this); log('full', 'close'); }); this.on("error", function() { updateNodeRedStatus(node, { fill: "yellow", shape: "ring", text: "error" }); }); /* =============== OpenZWave events ================== */ Object.keys(ozwEvents).forEach(function(key) { zwsubscribe(node, key, function(event, data) { // nuttin'! callback exists simply to update the node status }); }); // set initial node status upon creation updateNodeRedStatus(node); } // RED.nodes.registerType("zwave-out", ZWaveOut); // }