UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control and ETS group address importer. Easy to use and highly configurable.

262 lines (241 loc) 12 kB
const loggerClass = require('./utils/sysLogger') module.exports = function (RED) { const dptlib = require('knxultimate').dptlib const fs = require('fs') const path = require('path') // msg is: // // Build final input message object // return { // topic: _outputtopic // , payload: jsValue // , devicename: (typeof _devicename !== 'undefined') ? _devicename : "" // , payloadmeasureunit: sPayloadmeasureunit // , payloadsubtypevalue: sPayloadsubtypevalue // , knx: // { // event: _event // , dpt: sInputDpt // //, details: dpt // , dptdesc: sDptdesc // , source: _srcGA // , destination: _destGA // , rawValue: _Rawvalue // } // }; // The node.exposedGAs is and array of: // { // address, // dpt, // payload // } function knxUltimateAutoResponder (config) { RED.nodes.createNode(this, config) const node = this node.serverKNX = RED.nodes.getNode(config.server) node.topic = node.name node.name = config.name === undefined ? 'Auto responder' : config.name node.outputtopic = node.name node.dpt = '' node.notifyreadrequest = true node.notifyresponse = true node.notifywrite = true node.initialread = false node.listenallga = true node.outputtype = 'write' node.outputRBE = 'false' // Apply or not RBE to the output (Messages coming from flow) node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS) node.exposedGAs = [] node.commandText = [] // Raw list Respond To node.timerSaveExposedGAs = null const pushStatus = (status) => { if (!status) return const provider = node.serverKNX if (provider && typeof provider.applyStatusUpdate === 'function') { provider.applyStatusUpdate(node, status) } else { node.status(status) } } const updateStatus = (status) => { if (!status) return pushStatus(status) } if (node.serverKNX === null) { updateStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return } try { const baseLogLevel = (node.serverKNX && node.serverKNX.loglevel) ? node.serverKNX.loglevel : 'error' node.sysLogger = new loggerClass({ loglevel: baseLogLevel, setPrefix: node.type + ' <' + (node.name || node.id || '') + '>' }) } catch (error) { console.log(error.stack) } // Used to call the status update from the config node. node.setNodeStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => { // try { // if (node.serverKNX === null) { node.status({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return } // GA = GA === undefined ? '' : GA // payload = payload === undefined ? '' : payload // payload = typeof payload === 'object' ? JSON.stringify(payload) : payload // const dDate = new Date() // node.status({ fill, shape, text: GA + ' ' + payload + ' ' + text + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ')' }) // } catch (error) { // } } node.saveExposedGAs = async () => { const sFile = path.join(node.serverKNX.userDir, 'knxpersistvalues', 'knxpersist' + node.id + '.json') try { if (node.exposedGAs.length > 0) { fs.writeFileSync(sFile, JSON.stringify(node.exposedGAs)) if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info('knxUltimateAutoResponder: wrote peristent values to the file ' + sFile) } } catch (err) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('knxUltimateAutoResponder: unable to write peristent values to the file ' + sFile + ' ' + err.message) } } node.loadExposedGAs = () => { const sFile = path.join(node.serverKNX.userDir, 'knxpersistvalues', 'knxpersist' + node.id + '.json') try { node.exposedGAs = JSON.parse(fs.readFileSync(sFile, 'utf8')) } catch (err) { node.exposedGAs = [] if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn('knxUltimateAutoResponder: unable to read peristent file ' + sFile + ' ' + err.message) } } // Load persistent file try { node.loadExposedGAs() // Set all saved GAs to disabled. Will be enabled later (directive's list) node.exposedGAs.forEach(element => { element.enabled = false }) if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs) node.sysLogger?.info('Started timerSaveExposedGAs with array lenght ', node.exposedGAs?.length) node.timerSaveExposedGAs = setInterval(async () => { await node.saveExposedGAs() }, 5000) } catch (error) { } // Add the ETS CSV file list to exposedGAs if (node.serverKNX.csv === undefined || node.serverKNX.csv === '' || node.serverKNX.csv.length === 0) { updateStatus({ fill: 'grey', shape: 'ring', text: 'No ETS file imported', payload: '', dpt: '', devicename: '' }) // return; } else { node.serverKNX.csv.forEach(element => { const curGa = node.exposedGAs.find(a => a.address === element.ga) if (curGa === undefined) { node.exposedGAs.push({ address: element.ga, dpt: element.dpt, default: undefined, payload: undefined, enabled: false }) // "enabled" will be used to filter only the node.commandText directiver } else { curGa.enabled = false } }) updateStatus({ fill: 'green', shape: 'ring', text: 'ETS file loaded', payload: '', dpt: '', devicename: '' }) } // Fill the filter list try { node.commandText = JSON.parse(config.commandText) } catch (error) { updateStatus({ fill: 'red', shape: 'dot', text: 'JSON error: ' + error.message, payload: '', dpt: '', devicename: '' }) if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: node.commandText = JSON.parse(config.commandText) ${error.stack}`) return } // Decode the commandText list be exploding the format 2/2/.. node.commandText.forEach(element => { if (element.ga !== undefined && element.default !== undefined) { const defaultVal = element.default const dpt = element.dpt if (element.ga.includes('..')) { const start = Number(element.ga.substring(element.ga.lastIndexOf('/') + 1, element.ga.indexOf('..'))) const end = Number(element.ga.substring(element.ga.indexOf('..') + 2)) const twoLevel = element.ga.substring(0, element.ga.lastIndexOf('/') + 1) for (let index = start; index < end; index++) { const decAdd = twoLevel + index // Add also to the exposedGAs list, if not already present const curGa = node.exposedGAs.find(a => a.address === decAdd) if (curGa === undefined) { node.exposedGAs.push({ address: decAdd, dpt, default: defaultVal, payload: undefined, enabled: true }) } else { if (dpt !== undefined) curGa.dpt = dpt // Take the Datapoint from the commandText directive, replacing from ETS CSV file, if exists. curGa.enabled = true } } } else { const curGa = node.exposedGAs.find(a => a.address === element.ga) if (curGa === undefined) { node.exposedGAs.push({ address: element.ga, dpt, default: defaultVal, payload: undefined, enabled: true }) } else { if (dpt !== undefined) curGa.dpt = dpt // Take the Datapoint from the commandText directive, replacing from ETS CSV file, if exists. curGa.enabled = true } } // Delete all not wanted GAs, that aren't in the node.commandText directive list. node.exposedGAs = node.exposedGAs.filter(a => (a.enabled !== undefined && a.enabled === true)) updateStatus({ fill: 'green', shape: 'ring', text: 'JSON parsed: ' + node.commandText.length + ' directive(s).', payload: '', dpt: '', devicename: '' }) } else { // Error updateStatus({ fill: 'red', shape: 'dot', text: 'JSON error: ga or default keys not set. Abort.', payload: '', dpt: '', devicename: '' }) if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('knxUltimateAutoResponder: node.commandText.forEach(element.. JSON error: ga or default keys not set. Abort.') } }) // This function is called by the knx-ultimate config node, to output a msg.payload. node.handleSend = msg => { if (msg.knx !== undefined && msg.knx.event !== undefined && msg.knx.event !== 'GroupValue_Read') { // Save the value try { var oGa = node.exposedGAs.find(ga => ga.address === msg.knx.destination) } catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: var oGa = node.exposedGAs.find(ga => ga.address === msg.knx.destination) ${error.stack}`) } if (oGa !== undefined) { let decodedPayload try { // Don't care about the decoded payload, because knxUltimate-config could pass a TryToFindDatapoint from raw data // Take only RAW data and decode it with the dpt specified by the commandText directive decodedPayload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(oGa.dpt)) } catch (error) { updateStatus({ fill: 'red', shape: 'dot', text: 'const decodedPayload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(oGa.dpt)); ' + error.message, payload: '', dpt: '', devicename: '' }) if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: const decodedPayload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(oGa.dpt)); ${error.stack}`) } oGa.payload = decodedPayload } } else { try { let retVal const oFoundGA = node.exposedGAs.find(ga => ga.address === msg.knx.destination) if (oFoundGA === undefined) return if (oFoundGA.payload === undefined) { retVal = oFoundGA.default } else { retVal = oFoundGA.payload } if (retVal !== undefined) { const dDate = new Date() if (oFoundGA.address !== undefined && oFoundGA.dpt !== undefined && retVal !== undefined) { node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr: oFoundGA.address, payload: retVal, dpt: oFoundGA.dpt, outputtype: 'response', nodecallerid: node.id }) updateStatus({ fill: 'blue', shape: 'dot', text: 'Respond ' + oFoundGA.address + ' => ' + retVal + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ')' }) } else { updateStatus({ fill: 'yellow', shape: 'ring', text: 'Issue responding ' + oFoundGA.address + ' => ' + retVal + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ')' }) } } } catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`knxUltimateAutoResponder: after bFound ${error.stack}`) } } } node.on('input', function (msg) { }) node.on('close', function (done) { try { if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs) node.saveExposedGAs() } catch (error) { } node.exposedGAs = [] if (node.serverKNX) { node.serverKNX.removeClient(node) } done() }) // On each deploy, unsubscribe+resubscribe if (node.serverKNX) { node.serverKNX.removeClient(node) node.serverKNX.addClient(node) } } RED.nodes.registerType('knxUltimateAutoResponder', knxUltimateAutoResponder) }