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.
845 lines (807 loc) • 42.9 kB
JavaScript
const loggerClass = require('./utils/sysLogger')
const coerceBoolean = (value) => (value === true || value === 'true')
let buttonEndpointRegistered = false
/* eslint-disable max-len */
module.exports = function (RED) {
if (!buttonEndpointRegistered) {
const parseRawValue = (raw) => {
if (raw === undefined || raw === null) return undefined
if (typeof raw !== 'string') return raw
const trimmed = raw.trim()
if (trimmed === '') return undefined
if (/^(true|false)$/i.test(trimmed)) return trimmed.toLowerCase() === 'true'
const numericValue = Number(trimmed)
if (!Number.isNaN(numericValue) && trimmed === numericValue.toString()) return numericValue
try {
return JSON.parse(trimmed)
} catch (error) {
return trimmed
}
}
const coerceValueForDpt = (dpt, rawValue) => {
const value = parseRawValue(rawValue)
if (!dpt || typeof dpt !== 'string') return value
const main = dpt.split('.')[0]
switch (main) {
case '1':
case '2': {
if (typeof value === 'boolean') return value
if (typeof value === 'number') return value !== 0
if (typeof value === 'string') {
const lowered = value.trim().toLowerCase()
if (['1', 'true', 'on', 'open'].includes(lowered)) return true
if (['0', 'false', 'off', 'close', 'closed'].includes(lowered)) return false
}
throw new Error('Boolean value required for this datapoint')
}
case '3':
if (value && typeof value === 'object') return value
throw new Error('Object value required for this datapoint (e.g. {"decr_incr":1,"data":3})')
case '5':
case '6':
case '7':
case '8':
case '9':
case '12':
case '13':
case '14':
case '20': {
const num = typeof value === 'number' ? value : Number(value)
if (Number.isNaN(num)) throw new Error('Numeric value required for this datapoint')
return num
}
case '16':
return value !== undefined && value !== null ? String(value) : ''
default:
return value
}
}
const handleButtonAction = (req, res) => {
try {
const { id } = req.body || {}
if (!id) {
res.status(400).json({ error: 'Missing node id' })
return
}
const targetNode = RED.nodes.getNode(id)
if (!targetNode) {
res.status(404).json({ error: 'KNX node not found' })
return
}
if (!targetNode.serverKNX) {
res.status(400).json({ error: 'KNX gateway not configured' })
return
}
const mode = (req.body && req.body.mode ? String(req.body.mode) : 'read').toLowerCase()
const grpaddr = targetNode.topic
if (mode !== 'read') {
if (!grpaddr || String(grpaddr).trim() === '') {
res.status(400).json({ error: 'Group address not set' })
return
}
if (!targetNode.dpt || targetNode.dpt === '' || targetNode.dpt === 'auto') {
res.status(400).json({ error: 'Datapoint not defined for this node' })
return
}
}
if (mode === 'read') {
if (targetNode.listenallga === true || targetNode.listenallga === 'true') {
res.status(400).json({ error: 'Manual read is not available when universal mode is enabled' })
return
}
if (!grpaddr || String(grpaddr).trim() === '') {
res.status(400).json({ error: 'Group address not set' })
return
}
targetNode.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr,
payload: '',
dpt: '',
outputtype: 'read',
nodecallerid: targetNode.id
})
try {
if (typeof targetNode.setNodeStatus === 'function') {
targetNode.setNodeStatus({
fill: 'blue',
shape: 'ring',
text: 'BTN->KNX READ',
payload: '',
GA: grpaddr,
dpt: targetNode.dpt,
devicename: targetNode.name || ''
})
}
targetNode.sysLogger?.info(`Manual KNX read triggered via editor button for ${grpaddr}`)
} catch (error) {
targetNode.sysLogger?.warn(`Manual KNX read status update failed: ${error.message}`)
}
res.json({ status: 'ok' })
return
}
if (mode === 'toggle') {
if (!/^1\./.test(targetNode.dpt || '')) {
res.status(400).json({ error: 'Toggle is available only for datapoint 1.x' })
return
}
if (targetNode.buttonEnabled !== true && targetNode.buttonEnabled !== 'true') {
res.status(400).json({ error: 'Button is disabled for this node' })
return
}
if (typeof targetNode._buttonToggleState !== 'boolean') {
targetNode._buttonToggleState = coerceBoolean(targetNode.buttonToggleInitial)
}
targetNode._buttonToggleState = !targetNode._buttonToggleState
const payload = targetNode._buttonToggleState
targetNode.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr,
payload,
dpt: targetNode.dpt,
outputtype: 'write',
nodecallerid: targetNode.id
})
try {
if (typeof targetNode.setNodeStatus === 'function') {
targetNode.setNodeStatus({
fill: 'green',
shape: 'dot',
text: 'BTN->KNX TOGGLE',
payload,
GA: grpaddr,
dpt: targetNode.dpt,
devicename: targetNode.name || ''
})
}
targetNode.sysLogger?.info(`Manual KNX toggle triggered via editor button for ${grpaddr}, value: ${payload}`)
} catch (error) {
targetNode.sysLogger?.warn(`Manual KNX toggle status update failed: ${error.message}`)
}
res.json({ status: 'ok', payload })
return
}
if (mode === 'value') {
if (targetNode.buttonEnabled !== true && targetNode.buttonEnabled !== 'true') {
res.status(400).json({ error: 'Button is disabled for this node' })
return
}
const userValue = req.body ? req.body.value : undefined
if (userValue === undefined || userValue === null || userValue === '') {
res.status(400).json({ error: 'Custom value is required' })
return
}
let payload
try {
payload = coerceValueForDpt(targetNode.dpt, userValue)
} catch (error) {
res.status(400).json({ error: error.message })
return
}
targetNode.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr,
payload,
dpt: targetNode.dpt,
outputtype: 'write',
nodecallerid: targetNode.id
})
try {
if (typeof targetNode.setNodeStatus === 'function') {
targetNode.setNodeStatus({
fill: 'green',
shape: 'dot',
text: 'BTN->KNX WRITE',
payload,
GA: grpaddr,
dpt: targetNode.dpt,
devicename: targetNode.name || ''
})
}
targetNode.sysLogger?.info(`Manual KNX write triggered via editor button for ${grpaddr}, value: ${JSON.stringify(payload)}`)
} catch (error) {
targetNode.sysLogger?.warn(`Manual KNX write status update failed: ${error.message}`)
}
res.json({ status: 'ok', payload })
return
}
res.status(400).json({ error: 'Unsupported button mode' })
} catch (error) {
res.status(500).json({ error: error.message || 'KNX command failed' })
}
}
RED.httpAdmin.post('/knxUltimate/buttonAction', RED.auth.needsPermission('knxUltimate-config.write'), handleButtonAction)
RED.httpAdmin.post('/knxUltimate/manualRead', RED.auth.needsPermission('knxUltimate-config.write'), (req, res) => {
if (!req.body) req.body = {}
if (!req.body.mode) req.body.mode = 'read'
handleButtonAction(req, res)
})
buttonEndpointRegistered = true
}
const _ = require('lodash')
const KNXUtils = require('knxultimate')
const payloadRounder = require('./utils/payloadManipulation')
const dptlib = require('knxultimate').dptlib
function knxUltimate (config) {
RED.nodes.createNode(this, config)
const node = this
node.serverKNX = RED.nodes.getNode(config.server) || undefined
const pushStatus = (status) => {
const provider = node.serverKNX
if (provider && typeof provider.applyStatusUpdate === 'function') {
provider.applyStatusUpdate(node, status)
} else {
node.status(status)
}
}
if (node.serverKNX === undefined) {
pushStatus({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' })
return
}
// Used to call the status update from the config node.
node.setNodeStatus = ({
fill, shape, text, payload, GA, dpt, devicename
}) => {
try {
if (node.serverKNX === null) { pushStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return }
if (node.icountMessageInWindow == -999) return // Locked out, doesn't change status.
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
const statusText = `${GA + payload + (node.listenallga === true ? ` ${devicename}` : '')} (day ${dDate.getDate()}, ${dDate.toLocaleTimeString()}) ${text}`
pushStatus({ fill, shape, text: statusText })
// 16/02/2020 signal errors to the server
if (fill.toUpperCase() === 'RED') {
if (node.serverKNX) {
const oError = {
nodeid: node.id, topic: node.outputtopic, devicename, GA, text
}
node.serverKNX.reportToWatchdogCalledByKNXUltimateNode(oError)
}
}
// Validate the Address to advise the user. The address can be undefined, because the
// group address can be set via setConfig
if (node.listenallga === false) {
try {
KNXUtils.validateKNXAddress(node.topic, true)
} catch (error) {
node.setNodeStatus({
fill: 'grey', shape: 'ring', text: 'DISABLED: ' + error.message, payload: '', GA: node.topic, dpt: '', devicename: ''
})
}
}
} catch (error) {
}
}
// Get the Group Address from various sources
if (config.setTopicType === undefined || config.setTopicType === 'str') {
node.topic = config.topic
node.dpt = config.dpt || '1.001'
} else if (config.setTopicType === 'flow') {
try {
node.topic = node.context().flow.get(config.topic)
node.dpt = 'auto'
payloadRounder.KNXULtimateChangeConfigByInputMSG({ setConfig: { setGroupAddress: node.topic, setDPT: node.dpt } }, node, config)
} catch (error) {
node.topic = undefined
}
} else if (config.setTopicType === 'global') {
try {
node.topic = node.context().global.get(config.topic)
node.dpt = 'auto'
payloadRounder.KNXULtimateChangeConfigByInputMSG({ setConfig: { setGroupAddress: node.topic, setDPT: node.dpt } }, node, config)
} catch (error) {
node.topic = undefined
}
} else if (config.setTopicType === 'env') {
try {
node.topic = RED.util.getSetting(node, config.topic) // takes care of the subflow's env vairables
node.dpt = 'auto'
payloadRounder.KNXULtimateChangeConfigByInputMSG({ setConfig: { setGroupAddress: node.topic, setDPT: node.dpt } }, node, config)
} catch (error) {
node.topic = undefined
}
}
node.outputtopic = (config.outputtopic === undefined || config.outputtopic === '') ? node.topic : config.outputtopic // 07/02/2020 Importante, per retrocompatibilità
node.name = config.name
node.notifyreadrequest = config.notifyreadrequest || false
node.notifyreadrequestalsorespondtobus = config.notifyreadrequestalsorespondtobus || 'false' // Auto respond if notifireadrequest is true
node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = config.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized || ''
node.notifyresponse = config.notifyresponse || false
node.notifywrite = config.notifywrite
node.initialread = config.initialread || 0
if (node.initialread === true) node.initialread = 1 // 04/04/2021 Backward compatibility
if (node.initialread === false) node.initialread = 0 // 04/04/2021 Backward compatibility
node.initialread = Number(config.initialread)
node.listenallga = config.listenallga || false
node.outputtype = config.outputtype || 'write'// When the node is used as output
node.outputRBE = config.outputRBE || 'false' // Apply or not RBE to the output (Messages coming from flow)
node.inputRBE = config.inputRBE || 'false' // Apply or not RBE to the input (Messages coming from BUS)
// Backward compatibility
if (node.outputRBE === true) node.outputRBE = 'true'
if (node.outputRBE === false) node.outputRBE = 'false'
if (node.inputRBE === true) node.inputRBE = 'true'
if (node.inputRBE === false) node.inputRBE = 'false'
node.currentPayload = '' // Current value for the RBE input and for the .previouspayload msg
node.icountMessageInWindow = 0 // Used to prevent looping messages
node.messageQueue = [] // 01/01/2020 All messages from the flow to the node, will be queued and will be sent separated by 60 milliseconds each. Use uf the underlying api "minimumDelay" is not possible because the telegram order isn't mantained.
node.formatmultiplyvalue = (typeof config.formatmultiplyvalue === 'undefined' ? 1 : config.formatmultiplyvalue)
node.formatnegativevalue = (typeof config.formatnegativevalue === 'undefined' ? 'leave' : config.formatnegativevalue)
node.formatdecimalsvalue = (typeof config.formatdecimalsvalue === 'undefined' ? 999 : config.formatdecimalsvalue)
node.passthrough = (typeof config.passthrough === 'undefined' ? 'no' : config.passthrough)
node.inputmessage = {} // Stores the input message to be passed through
node.timerTTLInputMessage = null // The stored node.inputmessage has a ttl.
node.buttonEnabled = coerceBoolean(config.buttonEnabled)
node.buttonMode = config.buttonMode || 'read'
node.buttonStaticValue = config.buttonStaticValue || ''
node.buttonToggleInitial = coerceBoolean(config.buttonToggleInitial)
node._buttonToggleState = node.buttonToggleInitial
const syncButtonToggleState = (value) => {
if (node.buttonMode === 'toggle' && /^1\./.test(node.dpt || '')) {
node._buttonToggleState = coerceBoolean(value)
}
}
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.sendMsgToKNXCode = config.sendMsgToKNXCode || undefined
node.receiveMsgFromKNXCode = config.receiveMsgFromKNXCode || undefined
if (node.sendMsgToKNXCode === '') node.sendMsgToKNXCode = undefined
if (node.receiveMsgFromKNXCode === '') node.receiveMsgFromKNXCode = undefined
// Check if the node has a valid dpt
if (node.listenallga === false) {
if (node.dpt === undefined || node.dpt === '') {
node.setNodeStatus({
fill: 'red', shape: 'dot', text: 'The Datapoint cannot be empty.', payload: '', GA: '', dpt: '', devicename: ''
})
return
}
}
// Used in the KNX Function TAB
const getGAValue = function getGAValue (_ga = undefined, _dpt = undefined) {
try {
if (_ga === undefined) return
// The GA can have the devicename as well, separated by a blank space (1/1/0 light table ovest),
// I must take the GA only
const blankSpacePosition = _ga.indexOf(' ')
if (blankSpacePosition > -1) _ga = _ga.substring(0, blankSpacePosition)
// Is there a GA in the server's exposedGAs?
const found = node.serverKNX.exposedGAs.find(a => a.ga === _ga)
if (found !== undefined) {
if (_dpt === undefined && found.dpt === undefined) {
const errM = 'getGaValue: node ID:' + node.id + ' ' + 'No CSV file imported. Please provide the dpt manually'
RED.log.error(errM)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(errM)
return
};
return dptlib.fromBuffer(found.rawValue, dptlib.resolve(_dpt || found.dpt))
} else {
const errM = 'getGaValue: node ID:' + node.id + ' ' + 'Group Address not yet read, try later.'
RED.log.error(errM)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(errM)
}
} catch (error) {
const errM = 'getGaValue: node ID:' + node.id + ' ' + error.stack
RED.log.error(errM)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(errM)
}
}
// Used in the KNX Function TAB
const setGAValue = function setGAValue (_ga = undefined, _value = undefined, _dpt = undefined) {
try {
if (_ga === undefined) return
// The GA can have the devicename as well, separated by a blank space (1/1/0 light table ovest),
// I must take the GA only
const blankSpacePosition = _ga.indexOf(' ')
if (blankSpacePosition > -1) _ga = _ga.substring(0, blankSpacePosition)
if (_dpt === undefined) {
// Try getting dpt from ETS CSV
const found = node.serverKNX.exposedGAs.find(a => a.ga === _ga)
if (found === undefined || found.dpt === undefined) {
const errM = 'setGAValue: node ID:' + node.id + ' ' + 'No CSV file imported. Please provide the dpt manually'
RED.log.error(errM)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(errM)
return
}
}
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr: _ga, payload: _value, dpt: _dpt, outputtype: 'write', nodecallerid: node.id
})
} catch (error) {
const errM = 'setGAValue: node ID:' + node.id + ' ' + error.stack
RED.log.error(errM)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(errM)
}
}
// Used in the KNX Function TAB
const self = function self (_value) {
try {
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr: node.topic, payload: _value, dpt: node.dpt, outputtype: 'write', nodecallerid: node.id
})
} catch (error) {
const errM = 'self: node ID:' + node.id + ' ' + error.stack
RED.log.error(errM)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(errM)
}
}
// Used in the KNX Function TAB
const toggle = function toggle () {
if (node.currentPayload === true || node.currentPayload === false) {
try {
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr: node.topic, payload: !node.currentPayload, dpt: node.dpt, outputtype: 'write', nodecallerid: node.id
})
} catch (error) {
const errM = 'toggle: node ID:' + node.id + ' ' + error.stack
RED.log.error(errM)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(errM)
}
}
}
// This function is called by the knx-ultimate config node, to output a msg.payload.
node.handleSend = (msg) => {
// 27/03/2020 can i merge the last input msg arrived, with the output?
try {
if (node.passthrough === 'yes') {
// Respect the order! Object.assign(target, master). On master will be copied to target and properties of master will overwrite the same properties on target!
if (node.timerTTLInputMessage !== null) clearTimeout(node.timerTTLInputMessage)
msg = Object.assign(RED.util.cloneMessage(node.inputmessage), msg)
node.inputmessage = {}
} else if (node.passthrough === 'yesownprop') {
// Yes, but in an own prop
if (node.timerTTLInputMessage !== null) clearTimeout(node.timerTTLInputMessage)
msg.inputmessage = RED.util.cloneMessage(node.inputmessage)
node.inputmessage = {}
}
} catch (error) { }
// #region "Inject the msg to the JS code, then output msg to the flow"
// -+++++++++++++++++++++++++++++++++++++++++++
if (node.receiveMsgFromKNXCode !== undefined) {
try {
const receiveMsgFromKNXCode = new Function('msg', 'getGAValue', 'node', 'RED', 'self', 'toggle', 'setGAValue', node.receiveMsgFromKNXCode)
msg = receiveMsgFromKNXCode(msg, getGAValue, node, RED, self, toggle, setGAValue)
} catch (error) {
RED.log.error('knxUltimate: receiveMsgFromKNXCode: node ID:' + node.id + ' ' + error.message)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`receiveMsgFromKNXCode: node id ${node.id} ` || ' ' + error.stack)
return
}
}
// -+++++++++++++++++++++++++++++++++++++++++++
// #endregion
// if (msg.echoed !== undefined && msg.echoed === true) {
// node.setNodeStatus({
// fill: 'grey', shape: 'dot', text: 'Output echoed msg', payload: '', GA: node.topic, dpt: '', devicename: '',
// });
// }
if (msg !== undefined) node.send(msg)
}
node.on('input', (msg) => {
if (typeof msg === 'undefined') return
if (!node.serverKNX) return // 29/08/2019 Server not instantiate
// 11/01/2021 Accept properties change from msg
// *********************************
if (msg.hasOwnProperty('setConfig')) {
payloadRounder.KNXULtimateChangeConfigByInputMSG(msg, node, config)
return
}
// *********************************
// 16/06/2024 Check wether the node has a group address set.
// Validate the Address
if (node.listenallga === false) {
try {
KNXUtils.validateKNXAddress(node.topic, true)
} catch (error) {
node.setNodeStatus({
fill: 'red', shape: 'dot', text: error.message, payload: '', GA: node.topic, dpt: '', devicename: ''
})
return
}
}
// 19/06/2022 Reset the RBE filter https://github.com/Supergiovane/node-red-contrib-knx-ultimate/issues/191
// *********************************
if (msg.hasOwnProperty('resetRBE')) {
node.currentPayload = ''
node.setNodeStatus({
fill: 'grey', shape: 'ring', text: 'Reset RBE filter on this node.', payload: '', GA: '', dpt: '', devicename: ''
})
return
}
// *********************************
if (node.passthrough !== 'no') { // 27/03/2020 Save the input message to be passed out to msg output
// The msg has a TTL of 3 seconds
if (node.timerTTLInputMessage !== null) clearTimeout(node.timerTTLInputMessage)
node.timerTTLInputMessage = setTimeout(() => { node.inputmessage = {} }, 3000)
node.inputmessage = RED.util.cloneMessage(msg) // 28/03/2020 Store the message to be passed through.
}
// #region "Inject the msg to the JS code, then output msg to the flow"
// -+++++++++++++++++++++++++++++++++++++++++++
if (node.sendMsgToKNXCode !== undefined) {
try {
const sendMsgToKNXCode = new Function('msg', 'getGAValue', 'node', 'RED', 'self', 'toggle', 'setGAValue', node.sendMsgToKNXCode)
msg = sendMsgToKNXCode(msg, getGAValue, node, RED, self, toggle, setGAValue)
if (msg === undefined) return
} catch (error) {
RED.log.error('knxUltimate: sendMsgToKNXCode: node ID:' + node.id + ' ' + error.message)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`sendMsgToKNXCode: node id ${node.id} ` || ' ' + error.stack)
return
}
}
// -+++++++++++++++++++++++++++++++++++++++++++
// #endregion
// 25/07/2019 if payload is read or the Telegram type is set to "read", do a read, otherwise, write to the bus
if ((msg.hasOwnProperty('readstatus') && msg.readstatus === true) || node.outputtype === 'read') {
// READ: Send a Read request to the bus
let grpaddr = ''
if (node.listenallga == false) {
grpaddr = node.topic
if (msg.hasOwnProperty('destination')) grpaddr = msg.destination
// 29/12/2020 Protection over circular references (for example, if you link two Ultimate Nodes toghether with the same group address), to prevent infinite loops
if (msg.hasOwnProperty('knx')) {
if (msg.knx.destination == grpaddr && ((msg.knx.event === 'GroupValue_Response' || msg.knx.event === 'GroupValue_Read'))) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Circular reference protection during READ. The node ${node.id} has been temporary disabled. Two nodes with same group address and reaction/Telegram type are linked. See the FAQ in the Wiki. Msg:${JSON.stringify(msg)}`)
const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
node.setNodeStatus({
fill: 'red', shape: 'ring', text: `DISABLED due to a circulare reference while READ (${grpaddr}).`, payload: '', GA: '', dpt: '', devicename: ''
})
}, 1000)
return
}
}
node.setNodeStatus({
fill: 'grey', shape: 'dot', text: 'Read', payload: '', GA: grpaddr, dpt: '', devicename: ''
})
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr, payload: '', dpt: '', outputtype: 'read', nodecallerid: node.id
})
} else { // Listen all GAs
if (msg.hasOwnProperty('destination')) {
// listenallga is true, but the user specified own group address
grpaddr = msg.destination
// 29/12/2020 Protection over circular references (for example, if you link two Ultimate Nodes toghether with the same group address), to prevent infinite loops
if (msg.hasOwnProperty('knx')) {
if (msg.knx.destination == grpaddr && ((msg.knx.event === 'GroupValue_Response' || msg.knx.event === 'GroupValue_Read'))) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Circular reference protection during READ-2. The node ${node.id} has been temporary disabled. Two nodes with same group address and reaction/Telegram type are linked. See the FAQ in the Wiki. Msg:${JSON.stringify(msg)}`)
const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
node.setNodeStatus({
fill: 'red', shape: 'ring', text: `DISABLED due to a circulare reference while READ-2 (${grpaddr}).`, payload: '', GA: '', dpt: '', devicename: ''
})
}, 1000)
return
}
}
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr, payload: '', dpt: '', outputtype: 'read', nodecallerid: node.id
})
} else {
// Issue read to all group addresses
// 25/10/2019 the user is able not import the csv, so i need to check for it. This option should be unckecked by the knxUltimate html config, but..
if (typeof node.serverKNX.csv !== 'undefined') {
let delay = 0
for (let index = 0; index < node.serverKNX.csv.length; index++) {
const element = node.serverKNX.csv[index]
const grpaddr = element.ga
// 29/12/2020 Protection over circular references (for example, if you link two Ultimate Nodes toghether with the same group address), to prevent infinite loops
if (msg.hasOwnProperty('knx')) {
if (msg.knx.destination == grpaddr && ((msg.knx.event === 'GroupValue_Response' || msg.knx.event === 'GroupValue_Read'))) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Circular reference protection during READ-3. Node ${node.id} The read request hasn't been sent. Two nodes with same group address and reaction/Telegram type are linked. See the FAQ in the Wiki. Msg:${JSON.stringify(msg)}`)
node.setNodeStatus({
fill: 'red', shape: 'ring', text: `NOT SENT due to a circulare reference while READ-3 (${grpaddr}).`, payload: '', GA: '', dpt: '', devicename: ''
})
}
} else {
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr, payload: '', dpt: '', outputtype: 'read', nodecallerid: node.id
})
const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
// Timeout is only for the status update.
node.setNodeStatus({
fill: 'grey', shape: 'dot', text: 'Add Read to queue...', payload: '', GA: grpaddr, dpt: element.dpt, devicename: element.devicename
})
}, delay)
delay += 10
}
}
} else {
// No csv. A chi cavolo dovrei mandare la richiesta read?
const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
// Timeout is only for the status update.
node.setNodeStatus({
fill: 'red', shape: 'dot', text: "Read: ETS file not set, i don't know where to send the read request.", payload: '', GA: '', dpt: '', devicename: node.name
})
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`KNX-Ultimate: ETS file not set, i don't know where to send the read request. I'm the node ${node.id}`)
}, 100)
}
}
}
} else {
if (node.listenallga === false) {
// 23/12/2020 Applying RBE filter
if (node.outputRBE === 'true') {
// 19/01/2023 CHECKING THE INPUT PAYLOAD (ROUND, ETC) BASED ON THE NODE CONFIG
//* ********************************************************
const pTest = payloadRounder.Manipulate(node, msg.payload)
//* ********************************************************
if (_.isEqual(node.currentPayload, pTest)) {
// RBE kicks in, doesn't send the payload
node.setNodeStatus({
fill: 'grey', shape: 'ring', text: `rbe block (${msg.payload}) to KNX`, payload: '', GA: '', dpt: '', devicename: ''
})
return
}
}
}
// 07/02/2020 Revamped flood protection (avoid accepting too many messages as input)
if (node.icountMessageInWindow == -999) return // Locked out
if (node.icountMessageInWindow == 0) {
const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
if (node.icountMessageInWindow >= 120) {
// Looping detected
node.setNodeStatus({
fill: 'red', shape: 'ring', text: 'DISABLED! Flood protection! Too many msg at the same time.', payload: '', GA: '', dpt: '', devicename: ''
})
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Node ${node.id} has been disabled due to Flood Protection. Too many messages in a timeframe. Check your flow's design or use RBE option.`)
node.icountMessageInWindow = -999 // Lock out node
} else { node.icountMessageInWindow = -1 }
}, 1000)
}
node.icountMessageInWindow += 1
// OUTPUT: Send message to the bus (write/response)
if (node.serverKNX.knxConnection) {
let { outputtype } = node
let grpaddr = ''
let dpt = ''
// 29/12/2020 Check wheter the input message contains the "event" property, that overwrite the node's outputtype
if (msg.hasOwnProperty('event')) {
if (msg.event === 'GroupValue_Write') outputtype = 'write'
if (msg.event === 'GroupValue_Response') outputtype = 'response'
if (msg.event === 'Update_NoWrite') outputtype = 'update' // 05/01/2021 Doesn't send anything to the bus. Only updates the node currentPayload
}
if (node.listenallga === true) {
// The node is set to Universal mode (listen to all Group Addresses). Some fields are needed
if (msg.hasOwnProperty('destination')) {
grpaddr = msg.destination
} else {
node.setNodeStatus({
fill: 'red', shape: 'dot', text: 'msg.destination not set!', payload: '', GA: '', dpt: '', devicename: ''
})
return
}
if (msg.hasOwnProperty('dpt') && msg.dpt !== undefined && msg.dpt !== '') {
dpt = msg.dpt
} else {
// No datapoint set. If the CSV is loaded, try to get it from there.
if (!msg.hasOwnProperty('writeraw')) { // In raw mode, Datapoint is useless
// Get the datapoint from the CSV
if (typeof node.serverKNX.csv !== 'undefined') {
const oGA = node.serverKNX.csv.filter((sga) => sga.ga == grpaddr)[0]
if (oGA !== undefined) {
dpt = oGA.dpt
} else {
node.setNodeStatus({
fill: 'red', shape: 'dot', text: 'msg.dpt not set and not found in the CSV!', payload: '', GA: '', dpt: '', devicename: ''
})
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`node id: ${node.id} ` + 'msg.dpt not set and not found in the CSV!')
return
}
} else {
node.setNodeStatus({
fill: 'red', shape: 'dot', text: "msg.dpt not set and there's no CSV to search for!", payload: '', GA: '', dpt: '', devicename: ''
})
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`node id: ${node.id} ` + 'msg.dpt not set and there\'s no CSV to search for!')
return
}
}
}
} else {
grpaddr = msg.hasOwnProperty('destination') ? msg.destination : node.topic
dpt = (msg.hasOwnProperty('dpt') && msg.dpt !== undefined && msg.dpt !== '') ? msg.dpt : node.dpt
}
// Protection over circular references (for example, if you link two Ultimate Nodes toghether with the same group address), to prevent infinite loops
if (msg.hasOwnProperty('knx')) {
if (msg.knx.destination == grpaddr && ((msg.knx.event === 'GroupValue_Write' && outputtype === 'write') || (msg.knx.event === 'GroupValue_Response' && outputtype === 'response') || (msg.knx.event === 'GroupValue_Response' && outputtype === 'read') || (msg.knx.event === 'GroupValue_Read' && outputtype === 'read'))) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Circular reference protection. The node ${node.id} has been temporarely disabled. Two nodes with same group address and reaction/Telegram type are linked. See the FAQ in the Wiki. Msg:${JSON.stringify(msg)}`)
const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
node.setNodeStatus({
fill: 'red', shape: 'ring', text: `DISABLED due to a circulare reference (${grpaddr}).`, payload: '', GA: '', dpt: '', devicename: ''
})
}, 1000)
return
}
}
// 01/12/2020 Write RAW added.
// If you encode the values by yourself, you can write raw buffers with writeRaw(groupaddress: string, buffer: Buffer, bitlength?: Number, callback?: () => void).
// The third (optional) parameter bitlength is necessary for datapoint types where the bitlength does not equal the buffers bytelength * 8. This is the case for dpt 1 (bitlength 1), 2 (bitlength 2) and 3 (bitlength 4). For other dpts the paramter can be omitted.
// // Write raw buffer to a groupaddress with dpt 1 (e.g light on = value true = Buffer<01>) with a bitlength of 1
// connection.writeRaw('1/0/0', Buffer.from('01', 'hex'), 1)
// // Write raw buffer to a groupaddress with dpt 9 (e.g temperature 18.4 °C = Buffer<0730>) without bitlength
// connection.writeRaw('1/0/0', Buffer.from('0730', 'hex'))
if (msg.hasOwnProperty('writeraw') && msg.hasOwnProperty('writeraw') !== null) {
try {
if (msg.hasOwnProperty('bitlenght') && msg.bitlenght !== null) {
node.serverKNX.knxConnection.writeRaw(grpaddr, msg.writeraw, msg.bitlenght)
} else {
node.serverKNX.knxConnection.writeRaw(grpaddr, msg.writeraw)
}
node.setNodeStatus({
fill: 'green', shape: 'dot', text: 'RAW Write', payload: '', GA: grpaddr, dpt: '', devicename: ''
})
} catch (error) {
node.setNodeStatus({
fill: 'red', shape: 'dot', text: `Error RAW Write: ${error}`, payload: '', GA: grpaddr, dpt: '', devicename: ''
})
}
return
}
if (outputtype == 'response') {
try {
node.currentPayload = msg.payload// 31/12/2019 Set the current value (because, if the node is a virtual device, then it'll never fire "GroupValue_Write" in the server node, causing the currentPayload to never update)
syncButtonToggleState(msg.payload)
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr, payload: msg.payload, dpt, outputtype, nodecallerid: node.id
})
node.setNodeStatus({
fill: 'blue', shape: 'dot', text: 'Responding', payload: msg.payload, GA: grpaddr, dpt, devicename: ''
})
} catch (error) { }
} else if (outputtype == 'update') {
// 05/01/2021 Updates only the internal currentPayload value.
try {
node.currentPayload = msg.payload
syncButtonToggleState(msg.payload)
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr, payload: msg.payload, dpt, outputtype, nodecallerid: node.id
})
node.setNodeStatus({
fill: 'grey', shape: 'dot', text: 'Updating internal value', payload: msg.payload, GA: grpaddr, dpt, devicename: ''
})
} catch (error) { }
} else {
try {
node.currentPayload = msg.payload// 31/12/2019 Set the current value (because, if the node is a virtual device, then it'll never fire "GroupValue_Write" in the server node, causing the currentPayload to never update)
syncButtonToggleState(msg.payload)
node.setNodeStatus({
fill: 'green', shape: 'dot', text: 'Writing', payload: msg.payload, GA: grpaddr, dpt, devicename: ''
})
// if (node.serverKNX.linkStatus === "connected") {
node.serverKNX.sendKNXTelegramToKNXEngine({
grpaddr, payload: msg.payload, dpt, outputtype, nodecallerid: node.id
})
} catch (error) { }
}
}
}
})
node.on('close', (done) => {
if (node.timerTTLInputMessage !== null) clearTimeout(node.timerTTLInputMessage)
node.inputmessage = {}
if (node.serverKNX) {
node.serverKNX.removeClient(node)
try {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`Close: node id ${node.id} with topic ${node.topic || ''} has been removed from the server.`)
} catch (error) { }
}
done()
})
// On each deploy, add the node to the server list
if (node.serverKNX) {
node.serverKNX.addClient(node)
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`addClient: node id ${node.id}` || '' + ` with topic ${node.topic || ''} has been added to the server.`)
// 05/11/2021 if the node is set to read from bus, issue a read.
// "node-input-initialread0": "No",
// "node-input-initialread1": "Leggi dal BUS KNX",
// "node-input-initialread2": "Leggi l'ultimo valore salvato su file prima della disconnessione.",
// "node-input-initialread3": "Leggi l'ultimo valore salvato su file prima della disconnessione. Se inesistente, leggi dal BUS KNX",
if (node.serverKNX.linkStatus === 'connected' && node.initialread === 1 || node.initialread === 3) {
node.setNodeStatus({
fill: 'yellow', shape: 'dot', text: 'Get value from BUS.', payload: '', GA: node.topic || '', dpt: '', devicename: ''
})
const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ".
node.emit('input', { readstatus: true })
}, 3000)
}
}
}
RED.nodes.registerType('knxUltimate', knxUltimate)
}