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.
388 lines (357 loc) • 18.3 kB
JavaScript
// 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.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)
try {
node.sysLogger = new loggerClass({ loglevel: node.serverKNX.loglevel, 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()
// 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
node.status({ fill, shape, text: GA + payload + (node.listenallga === true ? ' ' + devicename : '') + ' (' + 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 += '.'
const dDate = new Date()
try {
node.status({ fill, 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 (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.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) {
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
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.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')) {
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')) {
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) {
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)
}