UNPKG

node-red-contrib-knx

Version:

KNX for node-red, utilizing pure JavaScript KNXnet/IP driver (both works: tunelling & routing)

535 lines (494 loc) 24.6 kB
/** * Created by aborovsky on 27.08.2015. */ var util = require('util'), KnxConnectionTunneling = require('knx.js').KnxConnectionTunneling; module.exports = function (RED) { var knxjs = require('knx.js'); /** * ====== Knx-CONTROLLER ================ * Holds configuration for knxjs host+port, * initializes new knxjs connections * ======================================= */ function KnxControllerNode(config) { RED.nodes.createNode(this, config); this.name = config.name; this.host = config.host; config.port = parseInt(config.port); this.port = config.port; this.mode = config.mode; this.knxjsconn = null; var node = this; //node.log("new KnxControllerNode, config: " + util.inspect(config)); /** * Initialize an knxjs socket, calling the handler function * when successfully connected, passing it the knxjs connection */ this.initializeKnxConnection = function (handler) { if (node.knxjsconn) { node.log('already connected to knxjs server at ' + config.host + ':' + config.port + ' in mode[' + config.mode + ']'); if (handler && (typeof handler === 'function')) handler(node.knxjsconn); return node.knxjsconn; } node.log('connecting to knxjs server at ' + config.host + ':' + config.port + ' in mode[' + config.mode + ']'); node.knxjsconn = null; if (config.mode === 'tunnel/unicast') { node.knxjsconn = new KnxConnectionTunneling(config.host, config.port, '0.0.0.0', 0); node.knxjsconn.Connect(function (err) { if (err) node.warn('cannot connect to knxjs server at ' + config.host + ':' + config.port + ' in mode[' + config.mode + '], cause: ' + util.inspect(err)); else node.log('Knx: successfully connected to ' + config.host + ':' + config.port + ' in mode[' + config.mode + ']'); handler(node.knxjsconn); } ); } else throw 'Unsupported mode[' + config.mode + ']' return node.knxjsconn; }; this.on("close", function () { node.log('disconnecting from knxjs server at ' + config.host + ':' + config.port + ' in mode[' + config.mode + ']'); node.knxjsconn && node.knxjsconn.Disconnect && node.knxjsconn.Disconnect(); }); } RED.nodes.registerType("knx-controller", KnxControllerNode); /** * ====== Knx-OUT ======================= * Sends outgoing KNX telegrams from * messages received via node-red flows * ======================================= */ function KnxOut(config) { RED.nodes.createNode(this, config); this.name = config.name; this.debug = config.debug; this.ctrl = RED.nodes.getNode(config.controller); var node = this; //node.log('new Knx-OUT, config: ' + util.inspect(config)); this.on("input", function (msg) { if (node.debug) { node.log('knxout.onInput, msg[' + util.inspect(msg) + ']'); } if (!(msg && msg.hasOwnProperty('payload'))) return; var payload; if (typeof(msg.payload) === "object") { payload = msg.payload; } else if (typeof(msg.payload) === "string") { payload = JSON.parse(msg.payload); } if (payload == null) { node.log('knxout.onInput: illegal msg.payload!'); return; } if (payload.dstgad == null) { node.log('knxout.onInput: illegal msg.payload.dstgad!'); return; } var action; switch (true) { case /read/.test(msg.topic): action = 'read'; break; case /respon/.test(msg.topic): action = 'response'; break; default: action = 'write'; } if (payload.value == null) action = 'read'; this.groupAddrSend(payload.dstgad, payload.value, payload.dpt, action, function (err) { if (err) { node.error('groupAddrSend error: ' + util.inspect(err)); } }); }); this.on("close", function () { node.log('knxOut.close'); }); node.status({fill: "yellow", shape: "dot", text: "inactive"}); function nodeStatusConnected() { node.status({fill: "green", shape: "dot", text: "connected"}); } function nodeStatusDisconnected() { node.status({fill: "red", shape: "dot", text: "disconnected"}); } function nodeStatusConnecting() { node.status({fill: "green", shape: "ring", text: "connecting"}); } /** * send a group write telegram to a group address * Initializes new knxjs connection per request * dstgad: dest group address '1/2/34' * dpt: DataPointType eg. '1' for boolean * value: the value to write * callback: * * Usage: * groupAddrSend({ host: 'localhost', port: 6720}, '1/2/34', 1, 1, function(err) { * if(err) console.error(err); * }); * * Datatypes: * KNX/EIB Function Information length EIS DPT Value Switch 1 Bit EIS 1 DPT 1 0,1 Dimming (Position, Control, Value) 1 Bit, 4 Bit, 8 Bit EIS 2 DPT 3 [0,0]...[1,7] Time 3 Byte EIS 3 DPT 10 Day [0..7] Hours [0..23] Minutes [0..59] Seconds [0..59] Date 3 Byte EIS 4 DPT 11 Floating point 2 Byte EIS 5 DPT 9 -671088,64 - 670760,96 8-bit unsigned value 1 Byte EIS 6 DPT 5 0...255 8-bit unsigned value 1 Byte DPT 5.001 DPT 5.001 0...100 Blinds / Roller shutter 1 Bit EIS 7 DPT 1 0,1 Priority 2 Bit EIS 8 DPT 2 [0,0]...[1,1] IEEE Floating point 4 Byte EIS 9 DPT 14 4-Octet Float Value IEEE 754 16-bit unsigned value 2 Byte EIS 10 DPT 7 0...65535 16-bit signed value 2 Byte DPT 8 DPT 8 -32768...32767 32-bit unsigned value 4 Byte EIS 11 DPT 12 0...4294967295 32-bit signed value 4 Byte DPT 13 DPT 13 -2147483648...2147483647 Access control 1 Byte EIS 12 DPT 15 ASCII character 1 Byte EIS 13 DPT 4 8859_1 character 1 Byte DPT 4.002 DPT 4.002 8-bit signed value 1 Byte EIS 14 DPT 6 -128...127 14 character ASCII 14 Byte EIS 15 DPT 16 14 character 8859_1 14 Byte DPT 16.001 DPT 16.001 Scene 1 Byte DPT 17 DPT 17 0...63 HVAC 1 Byte DPT 20 DPT 20 0..255 Unlimited string 8859_1 . DPT 24 DPT 24 List 3-byte value 3 Byte DPT 232 DPT 232 RGB[0,0,0]...[255,255,255] * */ this.groupAddrSend = function (dstgad, value, dpt, action, callback) { dpt = dpt ? dpt.toString(): '1'; if (action !== 'write' && action!== 'read') throw 'Unsupported action[' + action + '] inside of groupAddrSend'; if (node.debug) { node.log('groupAddrSend action[' + action + '] dstgad:' + dstgad + ', value:' + value + ', dpt:' + dpt); } if (action === 'write') { switch (dpt) { case '1': //Switch value = (value.toString() === 'true' || value.toString() === '1') break; case '3': // Dimmer, control bit + 3 bit value if (typeof(value.c) !== 'undefined' && value.c !== null && typeof(value.amount) !== 'undefined' && value.amount !== null) { if (value.amount <= 7) { value = ((value.c.toString() === 'true' || value.c.toString() === '1') << 3) | parseInt(value.amount) & 7; buf = new Buffer(1); buf[0] = value & 15; value = buf; } else { throw 'Value step amount too big for DPT 3'; } } else if (!isNaN(parseInt(value))) { buf = new Buffer(1); buf[0] = parseInt(value) & 15; value = buf; } else if (!Buffer.isBuffer(value)) { throw 'Value is incorrect for DPT 3 (type of value should be Buffer or Value = 0..15 or Object with fields "value.c" = 0..1 and "value.amount" = 0..7)'; } break; case '9': //Floating point value = parseFloat(value); buf = new Buffer(4); buf.writeFloatLE(value, 0); value = buf; break; case '5': //8-bit unsigned value 1 Byte EIS 6 DPT 5 0...255 break; case '5.001': //8-bit unsigned value 1 Byte DPT 5.001 DPT 5.001 0...100 break; case '6': //8-bit signed value 1 Byte EIS 14 DPT 6 -128...127 break; case '7': //16-bit unsigned value 2 Byte EIS 10 DPT 7 0...65535 break; case '8': //16-bit signed value 2 Byte DPT 8 DPT 8 -32768...32767 break; case '10': //Time 3 Byte EIS 3 DPT 10 Day [0..7] Hour [0..23] Minutes [0..59] Seconds [0..59] var day = 0; var hours = 0; var minutes = 0; var seconds = 0; // Get values from object or parse from input value as wire format if (typeof(value.day) !== 'undefined' && typeof(value.hours) !== 'undefined' && typeof(value.minutes) !== 'undefined' && typeof(value.seconds) !== 'undefined') { day = value.day; hours = value.hours; minutes = value.minutes; seconds = value.seconds; } else { value = parseInt(value); if (value <= 16202555 ) { // Day 3 bit [0..7] day = (value >> 21) & 0x07 // Hour 5 bit [0..23] hours = (value >> 16) & 0x1F // Minutes 6 bit [0..59] minutes = (value >> 8) & 0x3F; // Seconds 6 bit [0..59] seconds = value & 0x3F; } else { date = new Date(value); day = date.getDay(); if ( day === 0 ) day = 7; hours = date.getHours(); minutes = date.getMinutes(); seconds = date.getSeconds(); } } // Limit to max. values hours = (hours <= 23) ? hours : 23; minutes = (minutes <= 59) ? minutes : 59; seconds = (seconds <= 59) ? seconds : 59; // Write 3 byte wire time format: | day, hour | minute | second | buf = new Buffer(3); buf[2] = seconds & 0x3F; buf[1] = minutes & 0x3F; buf[0] = ((day & 0x07) << 5) | hours & 0x1F; value = buf; break; case '11': //Date 3 Byte EIS 3 DPT 11 Day [1..31] Month [1..12] Year [0..89] var day = 0; var month = 0; var year = 0; // Get values from object or parse from input value as wire format if (typeof(value.day) !== 'undefined' && typeof(value.month) !== 'undefined' && typeof(value.year) !== 'undefined') { day = value.day; month = value.month; year = value.year - 2000; } else { value = parseInt(value); if (value <= 2034777 ) { // day 5 bit [1..31] day = (value >> 16) & 0x1F // month 4 bit [1..12] month = (value >> 8) & 0x0F; // year 7 bit [0..89] year = value & 0x7F; } else { date = new Date(value); day = date.getDate(); month = date.getMonth() + 1; year = date.getFullYear() - 2000; } } // Limit to max. values day = (day <= 31) ? day : 31; month = (month <= 12) ? month : 12; year = (year <= 89) ? year : 89; // Write 3 byte wire time format: | day, hour | minute | second | buf = new Buffer(3); buf[2] = year & 0x7F; buf[1] = month & 0x0F; buf[0] = day & 0x1F; value = buf; break; case '12': //32-bit unsigned value 4 Byte EIS 11 DPT 12 0...4294967295 break; case '13': //32-bit signed value 4 Byte DPT 13 DPT 13 -2147483648...2147483647 break; case '16': //String 14 Byte DPT 16 DPT 16 ASCII or ISO 8859-1/Latin-1 buf = Buffer.alloc(14, 0); // Limit length to 14 byte if (value.length > 14) { value = value.substr(0,14); } // Write object value into buffer buf.fill(value, 0, value.length, 'ascii') value = buf; break; case '10': //Time 3 Byte EIS 3 DPT 10 Day [0..7] Hour [0..23] Minutes [0..59] Seconds [0..59] var day = 0; var hours = 0; var minutes = 0; var seconds = 0; // Get values from object or parse from input value as wire format if (typeof(value.day) !== 'undefined' && typeof(value.hours) !== 'undefined' && typeof(value.minutes) !== 'undefined' && typeof(value.seconds) !== 'undefined') { day = value.day; hours = value.hours; minutes = value.minutes; seconds = value.seconds; } else { value = parseInt(value); // Day 3 bit [0..7] day = (value >> 21) & 0x07 // Hour 5 bit [0..23] hours = (value >> 16) & 0x1F // Minutes 6 bit [0..59] minutes = (value >> 8) & 0x3F; // Seconds 6 bit [0..59] seconds = value & 0x3F; } // Limit to max. values hours = (hours <= 23) ? hours : 23; minutes = (minutes <= 59) ? minutes : 59; seconds = (seconds <= 59) ? seconds : 59; // Write 3 byte wire time format: | day, hour | minute | second | buf = new Buffer(3); buf[2] = seconds & 0x3F; buf[1] = minutes & 0x3F; buf[0] = ((day & 0x07) << 5) | hours & 0x1F; value = buf; break; case '17': //Scene 1 Byte DPT 17 DPT 17 0...63 break; case '20': //HVAC 1 Byte DPT 20 DPT 20 0..255 value = parseInt(value); buf = new Buffer(2); if (value <= 255) { buf[0] = 0x00; buf[1] = value & 255; value = buf; } else if (value <= 65535) { buf[0] = value & 255; buf[1] = (value >> 8) & 255; value = buf; } break; default: throw 'Unsupported dpt[' + dpt + '] inside groupAddrSend of knx node' } } if (!this.ctrl) node.error('Cannot proceed groupAddrSend, cause no controller-node specified!'); else // init a new one-off connection from the effectively singleton KnxController // there seems to be no way to reuse the outgoing conn in adreek/node-knxjs this.ctrl.initializeKnxConnection(function (connection) { if (connection.connected) nodeStatusConnected(); else nodeStatusDisconnected(); connection.removeListener('connecting', nodeStatusConnecting); connection.on('connecting', nodeStatusConnecting); connection.removeListener('connected', nodeStatusConnected); connection.on('connected', nodeStatusConnected); connection.removeListener('disconnected', nodeStatusDisconnected); connection.on('disconnected', nodeStatusDisconnected); try { if (node.debug) { node.log("sendAPDU: " + util.inspect(value)); } if (action === 'read') connection.RequestStatus(dstgad.toString()); else if (action === 'write') connection.Action(dstgad.toString(), value, null); callback && callback(); } catch (err) { node.error('error calling groupAddrSend: ' + err); callback(err); } }); } } // RED.nodes.registerType("knx-out", KnxOut); /** * ====== KNX-IN ======================== * Handles incoming KNX events, injecting * json into node-red flows * ======================================= */ function KnxIn(config) { RED.nodes.createNode(this, config); this.name = config.name; this.debug = config.debug; this.connection = null; var node = this; //node.log('new KNX-IN, config: ' + util.inspect(config)); var knxjsController = RED.nodes.getNode(config.controller); /* ===== Node-Red events ===== */ this.on("input", function (msg) { if (msg != null) { } }); var node = this; this.on("close", function () { if (node.receiveEvent && node.connection) node.connection.removeListener('event', node.receiveEvent); if (node.receiveStatus && node.connection) node.connection.removeListener('status', node.receiveStatus); }); function nodeStatusConnecting() { node.status({fill: "green", shape: "ring", text: "connecting"}); } function nodeStatusConnected() { node.status({fill: "green", shape: "dot", text: "connected"}); } function nodeStatusDisconnected() { node.status({fill: "red", shape: "dot", text: "disconnected"}); } node.receiveEvent = function (gad, data, datagram) { if (node.debug) { node.log('knx event gad[' + gad + ']data[' + data.toString('hex') + ']'); } node.send({ topic: 'knx:event', payload: { 'srcphy': datagram.source_address, 'dstgad': gad, 'dpt': 'no_dpt', 'value': data.toString(), 'data': datagram.dptData.apdu, 'dptData': datagram.dptData, 'type': 'event' } }); }; node.receiveStatus = function (gad, data, datagram) { if (node.debug) { node.log('knx status gad[' + gad + ']data[' + data.toString('hex') + ']'); } node.send({ topic: 'knx:status', payload: { 'srcphy': datagram.source_address, 'dstgad': gad, 'dpt': 'no_dpt', 'value': data.toString(), 'data': datagram.dptData.apdu, 'dptData': datagram.dptData, 'type': 'status' } }); }; // this.on("error", function(msg) {}); /* ===== knxjs events ===== */ // initialize incoming KNX event socket (openGroupSocket) // there's only one connection for knxjs-in: knxjsController && knxjsController.initializeKnxConnection(function (connection) { node.connection = connection; node.connection.removeListener('event', node.receiveEvent); node.connection.on('event', node.receiveEvent); node.connection.removeListener('status', node.receiveStatus); node.connection.on('status', node.receiveStatus); if (node.connection.connected) nodeStatusConnected(); else nodeStatusDisconnected(); node.connection.removeListener('connecting', nodeStatusConnecting); node.connection.on('connecting', nodeStatusConnecting); node.connection.removeListener('connected', nodeStatusConnected); node.connection.on('connected', nodeStatusConnected); node.connection.removeListener('disconnected', nodeStatusDisconnected); node.connection.on('disconnected', nodeStatusDisconnected); }); } // RED.nodes.registerType("knx-in", KnxIn); }