UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX intallation via Node-Red! Single Node KNX IN/OUT with optional ETS group address importer. Easy to use and highly configurable.

377 lines (333 loc) 19.6 kB
module.exports = function (RED) { function knxUltimateLoadControl(config) { const Address = require('./../KNXEngine/protocol/KNXAddress') const KnxConstants = require("./../KNXEngine/protocol/KNXConstants"); RED.nodes.createNode(this, config) var node = this; node.server = RED.nodes.getNode(config.server) 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.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); 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 }); } try { node.sysLogger = require("./utils/sysLogger.js").get({ loglevel: node.server.loglevel || "error" }); // 08/04/2021 new logger to adhere to the loglevel selected in the config-window } catch (error) { node.sysLogger = "error"; } // Used to call the status update from the config node. node.setNodeStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => { if (node.server === null) return; // Log only service statuses, not the GA values try { if (dpt !== "") return; var dDate = new Date(); // 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; node.status({ fill: fill, shape: shape, text: GA + payload + ((node.listenallga && node.server.statusDisplayDeviceNameWhenALL) === true ? " " + devicename : "") + (node.server.statusDisplayDataPoint === true ? dpt : "") + (node.server.statusDisplayLastUpdate === true ? " (" + dDate.getDate() + ", " + dDate.toLocaleTimeString() + ")" : "") + " " + text }); } catch (error) { } } // Used to call the status update from this node node.setLocalStatus = ({ fill = "green", shape = "ring", text = "" }) => { if (text !== "") text += "."; var dDate = new Date(); try { node.status({ fill: fill, shape: shape, text: text + " Shed:" + node.sheddingStage + " Power:" + node.totalWatt + "W" + " Limit:" + node.wattLimit + "W (" + dDate.getDate() + ", " + dDate.toLocaleTimeString() + ")" }); } 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 (var 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 var 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.server) { // Read status of the Total Power GA if (node.topic !== undefined && node.topic !== null && node.topic !== "") node.server.writeQueueAdd({ grpaddr: node.topic, payload: "", dpt: "", outputtype: "read", nodecallerid: node.id }); for (var i = 0; i < node.deviceList.length; i++) { let grpaddr = node.deviceList[i].monitorGA; if (grpaddr !== undefined && grpaddr !== "" && grpaddr !== null) { try { // Check if it's a group address let ret = Address.KNXAddress.createFromString(grpaddr, Address.KNXAddress.TYPE_GROUP); //node.setLocalStatus({ fill: "grey", shape: "dot", text: "Read Power from BUS" }); node.server.writeQueueAdd({ grpaddr: 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.mainTimer !== null) clearInterval(node.mainTimer);// Clear the timer node.mainTimer = setInterval(() => { // 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) { let 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) { let 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(); } } } }, 10000); } // Start the timer node.startTimerIncreaseShedding = () => { // Increase shedding timer (Switch off devices) if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding); node.timerIncreaseShedding = setTimeout(() => { if (node.server) { // 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.server) { // 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++; let 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.server.writeQueueAdd({ 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.server.writeQueueAdd({ 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; let 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 node.startMainTimer(); 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.server.writeQueueAdd({ grpaddr: oRow.ga, payload: true, dpt: oRow.dpt, outputtype: "write", nodecallerid: node.id }); } let 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")) { if (node.timerDecreaseShedding !== null) clearTimeout(node.timerDecreaseShedding); if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding); if (node.mainTimer !== null) clearInterval(node.mainTimer); let 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")) { if (node.timerDecreaseShedding !== null) clearTimeout(node.timerDecreaseShedding); if (node.timerIncreaseShedding !== null) clearTimeout(node.timerIncreaseShedding); let 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 output type is set to "read", do a read if ((msg.hasOwnProperty('readstatus') && msg.readstatus === true)) { node.initialReadAllDevicesInRules(); } }) 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.server) { node.server.removeClient(node) } done(); }) // On each deploy, unsubscribe+resubscribe if (node.server) { node.server.removeClient(node); if (node.topic !== "") { node.server.addClient(node); } } } RED.nodes.registerType("knxUltimateLoadControl", knxUltimateLoadControl) }