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, ETS group address importer, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.

444 lines (411 loc) 20.5 kB
// 10/09/2024 Setup the color logger const loggerClass = require('./utils/sysLogger') module.exports = function (RED) { function knxUltimateLoadControl (config) { // const Address = require('knxultimate') RED.nodes.createNode(this, config) const node = this node.serverKNX = RED.nodes.getNode(config.server) || undefined if (node.serverKNX === undefined) { node.status({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' }) return } node.name = config.name || 'KNX Load Control' node.topic = config.topic node.dpt = config.dpt node.listenallga = true // Dont' remove this. node.notifyreadrequest = false node.notifyresponse = true node.notifywrite = true // Dont' remove this. node.initialread = false node.outputtype = 'write' node.outputRBE = 'false' node.inputRBE = 'false' node.isLoadControlNode = true // Signal to config node, that this is a Load Control node node.initialread = true node.formatmultiplyvalue = 1 node.formatnegativevalue = 'zero' node.formatdecimalsvalue = 0 node.setLocalStatusTotalWattTimer = null node.sheddingStage = 0 node.timerIncreaseShedding = null node.timerDecreaseShedding = null node.controlMode = (config.controlMode || 'auto').toString() node.isDisabled = false node.sheddingCheckInterval = config.sheddingCheckInterval !== undefined ? config.sheddingCheckInterval * 1000 : 10000 node.sheddingRestoreDelay = config.sheddingRestoreDelay !== undefined ? config.sheddingRestoreDelay * 1000 : 60000 node.mainTimer = null node.totalWatt = 0 // Current total watt consumption node.wattLimit = config.wattLimit === undefined ? 3000 : Number(config.wattLimit) 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) } 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) } node.deviceList = [] for (let index = 1; index < 6; index++) { // Eval, the magic. Fill in the device list. DEFINITION DEVICELIST node.deviceList.push({ ga: eval('config.GA' + index), dpt: eval('config.DPT' + index), name: eval('config.Name' + index), autoRestore: eval('config.autoRestore' + index), monitorGA: eval('config.MonitorGA' + index), monitorDPT: eval('config.MonitorDPT' + index), monitorName: eval('config.MonitorName' + index), monitorVal: null }) } // Used to call the status update from the config node. node.setNodeStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => { if (node.serverKNX === null) return // Log only service statuses, not the GA values try { if (dpt !== '') return const dDate = new Date() const ts = (node.serverKNX && typeof node.serverKNX.formatStatusTimestamp === 'function') ? node.serverKNX.formatStatusTimestamp(dDate) : `${dDate.getDate()}, ${dDate.toLocaleTimeString()}` // 30/08/2019 Display only the things selected in the config GA = (typeof GA === 'undefined' || GA == '') ? '' : '(' + GA + ') ' devicename = devicename || '' dpt = (typeof dpt === 'undefined' || dpt == '') ? '' : ' DPT' + dpt payload = typeof payload === 'object' ? JSON.stringify(payload) : payload updateStatus({ fill, shape, text: GA + payload + (node.listenallga === true ? ' ' + devicename : '') + ' (' + ts + ') ' + text }) } catch (error) { } } // Used to call the status update from this node node.setLocalStatus = ({ fill = 'green', shape = 'ring', text = '' }) => { if (text !== '') text += '.' const dDate = new Date() const ts = (node.serverKNX && typeof node.serverKNX.formatStatusTimestamp === 'function') ? node.serverKNX.formatStatusTimestamp(dDate) : `${dDate.getDate()}, ${dDate.toLocaleTimeString()}` try { const modeText = node.controlMode === 'msg' ? ' Mode:MSG' : '' updateStatus({ fill, shape, text: text + ' Shed:' + node.sheddingStage + ' Power:' + node.totalWatt + 'W' + ' Limit:' + node.wattLimit + 'W' + modeText + ' (' + ts + ')' }) } catch (error) { } } // This function is called by the knx-ultimate config node. node.handleSend = msg => { // Update the Total Watt? if (msg.topic === node.topic && msg.payload !== '' && msg.payload !== null && msg.payload !== undefined) { node.totalWatt = msg.payload // Update current consumption only if the node is in idle state if (node.timerIncreaseShedding === null && node.timerDecreaseShedding === null) { if (node.setLocalStatusTotalWattTimer === null) clearInterval(node.setLocalStatusTotalWattTimer) node.setLocalStatusTotalWattTimer = setTimeout(() => { node.setLocalStatus({ fill: 'grey' }) }, 2000) } return } // Update the node.deviceList for (let i = 0; i < node.deviceList.length; i++) { // deviceList is an array of objects: // ga: eval("config.GA" + index), // dpt: eval("config.DPT" + index), // name: eval("config.Name" + index), // autoRestore : eval("config.autoRestore" + index), // monitorGA: eval("config.MonitorGA" + index), // monitorDPT: eval("config.MonitorDPT" + index), // monitorName: eval("config.MonitorName" + index), // monitorVal: null const oRow = node.deviceList[i] if (msg.topic === oRow.monitorGA && msg.payload !== null && msg.payload !== undefined) { oRow.monitorVal = msg.payload // node.setLocalStatus({ fill: "blue", shape: "dot", text: "Updated", payload: oRow.monitorVal, GA: msg.topic, dpt: "", devicename: oRow.monitorName }); } } } // 03/02/2022 perform a read on all GA in the list node.initialReadAllDevicesInRules = () => { if (node.serverKNX) { // Read status of the Total Power GA if (node.topic !== undefined && node.topic !== null && node.topic !== '') node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr: node.topic, payload: '', dpt: '', outputtype: 'read', nodecallerid: node.id }) for (let i = 0; i < node.deviceList.length; i++) { const grpaddr = node.deviceList[i].monitorGA if (grpaddr !== undefined && grpaddr !== '' && grpaddr !== null) { try { // Check if it's a group address // const ret = Address.KNXAddress.createFromString(grpaddr, Address.KNXAddress.TYPE_GROUP) // node.setLocalStatus({ fill: "grey", shape: "dot", text: "Read Power from BUS" }); node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr, payload: '', dpt: '', outputtype: 'read', nodecallerid: node.id }) } catch (error) { node.setLocalStatus({ fill: 'grey', shape: 'dot', text: 'Not a KNX GA ' + error.message }) } } } } else { node.setLocalStatus({ fill: 'red', shape: 'ring', text: 'No gateway selected. Unable to read from KNX bus' }) } } node.startMainTimer = () => { if (node.controlMode === 'msg') return if (node.isDisabled) return if (node.mainTimer !== null) clearInterval(node.mainTimer)// Clear the timer node.mainTimer = setInterval(() => { if (node.isDisabled) return // Issue a READ on all GA's node.initialReadAllDevicesInRules() // Check consumption if (node.totalWatt > node.wattLimit) { // Start increasing shedding! if (node.sheddingStage < node.deviceList.length) { if (node.timerIncreaseShedding === null) { const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setLocalStatus({ fill: 'yellow', shape: 'dot', text: "I'm about to shed the load " + node.sheddingStage }) }, 2000) if (node.timerDecreaseShedding !== null) clearTimeout(node.timerDecreaseShedding)// Clear the decreasing timer node.startTimerIncreaseShedding() } } } else if (node.totalWatt <= node.wattLimit) { // Start decreasing shedding! if (node.sheddingStage > 0) { if (node.timerDecreaseShedding === null) { const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setLocalStatus({ fill: 'yellow', shape: 'dot', text: "I'm about to unshed the load " + node.sheddingStage }) }, 2000) if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding)// Clear the increasing timer node.startTimerDecreaseShedding() } } } }, 20000) } // Start the timer node.startTimerIncreaseShedding = () => { // Increase shedding timer (Switch off devices) if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding) node.timerIncreaseShedding = setTimeout(() => { if (node.serverKNX) { // Check consumption if (node.totalWatt > node.wattLimit) { // Start increasing shedding! if (node.sheddingStage < node.deviceList.length) { node.increaseShedding() } } } node.timerIncreaseShedding = null // Nullify the timer. }, node.sheddingCheckInterval) } // Start the timer node.startTimerDecreaseShedding = () => { // Decrease shedding timer (Switch devices on again) if (node.timerDecreaseShedding !== null) clearTimeout(node.timerDecreaseShedding) node.timerDecreaseShedding = setTimeout(() => { if (node.serverKNX) { // Check consumption if (node.totalWatt <= node.wattLimit) { // Start decreasing shedding! if (node.sheddingStage > 0) { node.decreaseShedding() } } } node.timerDecreaseShedding = null // Nullify timer }, node.sheddingRestoreDelay) } node.increaseShedding = () => { // deviceList is an array of objects: // ga: eval("config.GA" + index), // dpt: eval("config.DPT" + index), // name: eval("config.Name" + index), // autoRestore : eval("config.autoRestore" + index), // monitorGA: eval("config.MonitorGA" + index), // monitorDPT: eval("config.MonitorDPT" + index), // monitorName: eval("config.MonitorName" + index), // monitorVal: null if (node.sheddingStage >= node.deviceList.length) { node.sheddingStage = node.deviceList.length node.setLocalStatus({ fill: 'red', shape: 'dot', text: 'No more loads to shed!!' }) return } node.sheddingStage++ const iRowIndex = node.sheddingStage - 1 // Array is base 0 const oRow = node.deviceList[iRowIndex] if (oRow.ga !== undefined && oRow.ga !== '' && oRow.ga !== null) { // Check if the device is in use. If not, turn off the device and further increase the shedding stage to turn off the next one. node.setLocalStatus({ fill: 'red', shape: 'dot', text: 'OFF ' + oRow.name }) node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr: oRow.ga, payload: false, dpt: oRow.dpt, outputtype: 'write', nodecallerid: node.id }) } else { node.setLocalStatus({ fill: 'grey', shape: 'dot', text: 'No GA defined' }) } node.send({ topic: node.name || node.topic, operation: 'Increase Shedding', device: oRow.name || '', ga: oRow.ga || '', totalPowerConsumption: node.totalWatt, wattLimit: node.wattLimit, payload: node.sheddingStage }) // Go furhter ? if (oRow.monitorGA !== undefined && oRow.monitorGA !== '' && oRow.monitorGA !== null) { // Minimum consumption must be at lease xx Watt if (oRow.monitorVal === null || oRow.monitorVal === undefined || oRow.monitorVal < 30) { // Switch off the next load, because this is already off, because the power consumption trascurable node.increaseShedding() } } } node.decreaseShedding = () => { // deviceList is an array of objects: // ga: eval("config.GA" + index), // dpt: eval("config.DPT" + index), // name: eval("config.Name" + index), // autoRestore : eval("config.autoRestore" + index), // monitorGA: eval("config.MonitorGA" + index), // monitorDPT: eval("config.MonitorDPT" + index), // monitorName: eval("config.MonitorName" + index), // monitorVal: null if (node.sheddingStage <= 0) { node.sheddingStage = 0 node.setLocalStatus({ fill: 'green', shape: 'dot', text: 'All loads are ON' }) return } node.sheddingStage-- let iRowIndex = node.sheddingStage // Array is base 0 if (iRowIndex < 0) iRowIndex = 0 if (iRowIndex > node.deviceList.length - 1) return const oRow = node.deviceList[iRowIndex] if (oRow.ga !== undefined && oRow.ga !== '' && oRow.ga !== null) { if (oRow.autoRestore === true) { // Check if the device is in use. If not, turn off the device and further increase the shedding stage to turn off the next one. node.setLocalStatus({ fill: 'green', shape: 'dot', text: 'ON ' + oRow.name }) node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr: oRow.ga, payload: true, dpt: oRow.dpt, outputtype: 'write', nodecallerid: node.id }) } else { // Cannot auto switch on the load. node.setLocalStatus({ fill: 'yellow', shape: 'dot', text: 'Auto Restore disabled ' + oRow.name }) } } else { // No load GA defined node.setLocalStatus({ fill: 'grey', shape: 'dot', text: 'No Load GA defined' }) } node.send({ topic: node.name || node.topic, operation: 'Decrease Shedding', device: oRow.name || '', ga: oRow.ga || '', totalPowerConsumption: node.totalWatt, wattLimit: node.wattLimit, payload: node.sheddingStage }) if (node.sheddingStage < 0) { node.sheddingStage = 0 const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setLocalStatus({ fill: 'green', shape: 'dot', text: 'All loads have been restored' }) }, 1000) } } // Start if (node.controlMode !== 'msg') { node.startMainTimer() } else { const t = setTimeout(() => { // keep consistent async behavior node.setLocalStatus({ fill: 'grey', shape: 'ring', text: 'Manual mode (msg.shedding)' }) }, 500) } node.on('input', function (msg) { if (typeof msg === 'undefined') return // Reset the shedding and activate all loads if (msg.hasOwnProperty('reset')) { if (node.timerDecreaseShedding !== null) clearTimeout(node.timerDecreaseShedding) if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding) node.sheddingStage = 0 for (let index = 0; index < node.deviceList.length; index++) { const oRow = node.deviceList[index] if (oRow.autoRestore === true) node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr: oRow.ga, payload: true, dpt: oRow.dpt, outputtype: 'write', nodecallerid: node.id }) } const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setLocalStatus({ fill: 'green', shape: 'dot', text: 'All loads have been restored' }) }, 1000) node.send({ topic: node.name || node.topic, operation: 'Reset', payload: node.sheddingStage }) } // Disable the shedding node if (msg.hasOwnProperty('disable')) { node.isDisabled = true if (node.timerDecreaseShedding !== null) clearTimeout(node.timerDecreaseShedding) if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding) if (node.mainTimer !== null) clearInterval(node.mainTimer) const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setLocalStatus({ fill: 'grey', shape: 'dot', text: 'Disabled' }) }, 1000) node.send({ topic: node.name || node.topic, operation: 'Disabled', payload: node.sheddingStage }) } // Disable the shedding node if (msg.hasOwnProperty('enable')) { node.isDisabled = false if (node.timerDecreaseShedding !== null) clearTimeout(node.timerDecreaseShedding) if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding) const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setLocalStatus({ fill: 'green', shape: 'dot', text: 'Enabled' }) // Restart timer node.startMainTimer() }, 1000) node.send({ topic: node.name || node.topic, operation: 'Enabled', payload: node.sheddingStage }) } // 24/04/2021 if payload is read or the Telegram type is set to "read", do a read if ((msg.hasOwnProperty('readstatus') && msg.readstatus === true)) { node.initialReadAllDevicesInRules() } // 'shed', 'unshed', 'auto' // | `msg.shedding` | String. *shed* to start the formward shedding sequence, *unshed* to start reverse shedding. Use this msg to force the shedding timer to start/stop, ignoring the **Monitor Wh** group address. Set *auto* to enable again the **Monitor Wh** group address monitoring. | if (msg.shedding !== undefined) { if (node.isDisabled) { node.setLocalStatus({ fill: 'grey', shape: 'dot', text: 'Disabled. Ignored msg.shedding.' }) return } if (node.controlMode === 'msg') { switch (msg.shedding) { case 'shed': node.setLocalStatus({ fill: 'red', shape: 'dot', text: 'msg.shedding: SHED received.' }) node.increaseShedding() break case 'unshed': node.setLocalStatus({ fill: 'green', shape: 'dot', text: 'msg.shedding: UNSHED received.' }) node.decreaseShedding() break default: node.setLocalStatus({ fill: 'grey', shape: 'ring', text: 'Manual mode: unknown msg.shedding value.' }) break } return } switch (msg.shedding) { case 'shed': node.wattLimit = 1 // Faking to shed node.setLocalStatus({ fill: 'red', shape: 'dot', text: 'msg.shedding: SHED received.' }) break case 'unshed': node.wattLimit = 100000 // Faking to unshed node.setLocalStatus({ fill: 'green', shape: 'dot', text: 'msg.shedding: UNSHED received.' }) break case 'auto': node.wattLimit = config.wattLimit === undefined ? 3000 : Number(config.wattLimit) node.setLocalStatus({ fill: 'green', shape: 'ring', text: 'msg.shedding: AUTO received.' }) break default: break } } }) node.on('close', function (done) { if (node.mainTimer !== null) clearInterval(node.mainTimer) if (node.timerDecreaseShedding !== null) clearTimeout(node.timerDecreaseShedding) if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding) if (node.serverKNX) { node.serverKNX.removeClient(node) } done() }) // On each deploy, unsubscribe+resubscribe if (node.serverKNX) { node.serverKNX.removeClient(node) if (node.topic !== '') { node.serverKNX.addClient(node) } } } RED.nodes.registerType('knxUltimateLoadControl', knxUltimateLoadControl) }