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.
296 lines (272 loc) • 12.1 kB
JavaScript
module.exports = function (RED) {
function knxUltimateAlerter (config) {
const fs = require('fs')
const path = require('path')
const mkdirp = require('mkdirp')
// const Address = require('knxultimate')
// const KnxConstants = require('knxultimate/protocol/KNXConstants')
RED.nodes.createNode(this, config)
const node = this
node.serverKNX = RED.nodes.getNode(config.server) || undefined
const pushStatus = (status) => {
if (status === undefined || status === null) return
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
}
node.name = config.name || 'KNX Alerter'
node.listenallga = true // Dont' remove this.
node.notifyreadrequest = false
node.notifyresponse = true
node.notifywrite = true // Dont' remove this.
node.initialread = 0
node.outputtype = 'write'
node.outputRBE = 'false'
node.inputRBE = 'false'
node.rules = config.rules || [{}]
node.isalertnode = true // Signal to config node, that this is a node scene controller
node.userDir = path.join(RED.settings.userDir, 'knxultimatestorage') // 09/03/2020 Storage of ttsultimate (otherwise, at each upgrade to a newer version, the node path is wiped out and recreated, loosing all custom files)
node.alertedDevices = []
node.curIndexAlertedDevice = 0
node.timerSend = null
node.whentostart = config.whentostart === undefined ? 'ifnewalert' : config.whentostart
node.timerinterval = (config.timerinterval === undefined || config.timerinterval == '') ? '2' : config.timerinterval
if (config.initialreadGAInRules === undefined) {
node.initialread = 1
} else {
node.initialread = Number(config.initialreadGAInRules)
}
// Used to call the status update from the config node.
node.setNodeStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => {
try {
if (node.serverKNX === null) return
// Log only service statuses, not the GA values
if (dpt !== undefined) return
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
pushStatus({ fill, shape, text: GA + payload + (node.listenallga === true ? ' ' + devicename : '') + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ' ' + text })
} catch (error) {
}
}
// Used to call the status update from the config node.
node.setLocalStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => {
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
try {
pushStatus({ fill, shape, text: GA + payload + (node.listenallga === true ? ' ' + devicename : '') + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ' ' + text })
} catch (error) {
}
}
// This function is called by the knx-ultimate config node, to output a msg.payload.
node.handleSend = msg => {
let bFound = false // 24/04/2021 true if the cycle below found a match, otherwise false
// Update the node.rules with the values taken from the file, if any, otherwise leave the default value
for (let i = 0; i < node.rules.length; i++) {
// rule is { topic: rowRuleTopic, devicename: rowRuleDeviceName, longdevicename: rowRuleLongDeviceName}
var rule = node.rules[i]
if (msg.topic === rule.topic) {
if (msg.payload === true) {
bFound = true
// Add the device to the array of alertedDevices
const oTrovato = node.alertedDevices.find(a => a.topic === rule.topic)
if (oTrovato === undefined) {
node.alertedDevices.unshift({ topic: rule.topic, devicename: rule.devicename, longdevicename: rule.longdevicename }) // Add to the begin of array
if (node.whentostart === 'ifnewalert') node.send([null, null, node.getThirdPinMSG()])
}
node.setLocalStatus({ fill: 'red', shape: 'dot', text: 'Alert', payload: '', GA: msg.topic, dpt: '', devicename: rule.devicename })
} else {
// Remove the device from the array
node.alertedDevices = node.alertedDevices.filter(a => a.topic !== msg.topic)
node.setLocalStatus({ fill: 'green', shape: 'dot', text: 'Restore', payload: '', GA: msg.topic, dpt: '', devicename: rule.devicename })
}
}
}
// If there's some device to alert, stop current timer and restart
// This allow the last alerted device to be outputted immediately
if (bFound && node.whentostart === 'ifnewalert' && node.alertedDevices.length > 0) {
clearTimeout(node.timerSend)
// Send directly the second and third message PIN
node.send([null, node.getSecondPinMSG(), null])
node.curIndexAlertedDevice = 0 // Restart form the beginning
node.startTimer()
}
}
// Get the msg to be outputted on second PIN
node.getSecondPinMSG = () => {
if (node.alertedDevices.length > 0) {
const msg = {}
let sRet = ''
let sRetLong = ''
let sTopic = ''
node.alertedDevices.forEach(function (item) {
sTopic += item.topic + ', '
if (item.devicename !== undefined && item.devicename !== '') sRet += item.devicename + ', '
if (item.longdevicename !== undefined && item.longdevicename !== '') sRetLong += item.longdevicename + ', '
})
sTopic = sTopic.slice(0, -2)
if (sRet.length > 2) sRet = sRet.slice(0, -2)
if (sRetLong.length > 2) sRetLong = sRetLong.slice(0, -2)
msg.topic = sTopic
msg.devicename = sRet
msg.longdevicename = sRetLong
msg.count = node.alertedDevices.length
msg.payload = true
return msg
}
}
// Get the msg to be outputted on third PIN
node.getThirdPinMSG = () => {
if (node.alertedDevices.length > 0) {
const msg = {}
let sRet = ''
let sRetLong = ''
let sTopic = ''
const item = node.alertedDevices[0] // Pick the last alerted device
sTopic = item.topic
if (item.devicename !== undefined && item.devicename !== '') sRet = item.devicename
if (item.longdevicename !== undefined && item.longdevicename !== '') sRetLong = item.longdevicename
msg.topic = sTopic
msg.devicename = sRet
msg.longdevicename = sRetLong
msg.count = node.alertedDevices.length
msg.payload = true
return msg
}
}
// 24/04/2021 perform a read on all GA in the rule list. Called both from node.on("input") and knxUltimate-config
node.initialReadAllDevicesInRules = () => {
if (node.serverKNX) {
node.setLocalStatus({ fill: 'grey', shape: 'ring', text: 'Reasy', payload: '', GA: '', dpt: '', devicename: '' })
let grpaddr = ''
for (let i = 0; i < node.rules.length; i++) {
// rule is { topic: rowRuleTopic, devicename: rowRuleDeviceName, longdevicename: rowRuleLongDeviceName}
const rule = node.rules[i]
// READ: Send a Read request to the bus
grpaddr = rule.topic
try {
// Check if it's a group address
// const ret = Address.KNXAddress.createFromString(grpaddr, Address.KNXAddress.TYPE_GROUP)
setTimeout(() => {
node.setLocalStatus({ fill: 'blue', shape: 'dot', text: 'Read', payload: '', GA: grpaddr, dpt: '', devicename: rule.devicename })
}, 500)
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, payload: '', GA: grpaddr, dpt: '', devicename: rule.devicename })
}
}
} else {
node.setLocalStatus({ fill: 'red', shape: 'ring', text: 'No gateway selected. Unable to read from KNX bus', payload: '', GA: '', dpt: '', devicename: '' })
}
}
node.on('input', function (msg) {
if (typeof msg === 'undefined') return
if (msg.hasOwnProperty('start')) {
clearTimeout(node.timerSend)
node.curIndexAlertedDevice = 0 // Restart form the beginning
if (node.alertedDevices.length > 0) {
node.send([null, node.getSecondPinMSG(), node.getThirdPinMSG()])
node.startTimer()
} else {
// Nothing more to output
node.sendNoMoreDevices()
}
return
}
// 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()
return
}
if (msg.topic === undefined) {
node.setLocalStatus({ fill: 'grey', shape: 'dot', text: 'ERROR: You must provide a msg.topic', payload: '', GA: '', dpt: '', devicename: '' })
return
}
if (msg.payload === undefined) {
node.setLocalStatus({ fill: 'grey', shape: 'dot', text: 'ERROR: You must provide payload (true/false)', payload: '', GA: '', dpt: '', devicename: '' })
return
}
msg.knx = { dpt: '1.001' }
node.handleSend(msg)
})
node.on('close', function (done) {
clearTimeout(node.timerSend)
if (node.serverKNX) {
node.serverKNX.removeClient(node)
}
done()
})
node.handleTimer = () => {
if (node.alertedDevices.length > 0) {
const count = node.alertedDevices.length
if (node.curIndexAlertedDevice > count - 1) {
node.curIndexAlertedDevice = 0
if (node.whentostart === 'manualstart') {
node.curIndexAlertedDevice = 0 // Restart form the beginning
return
}
}
// Create output message
try {
const curDev = node.alertedDevices[node.curIndexAlertedDevice] // is { topic: rule.topic, devicename: rule.devicename }
const msg = {}
msg.topic = curDev.topic
msg.count = count
msg.devicename = curDev.devicename
msg.longdevicename = curDev.longdevicename
msg.payload = true
node.send([msg, null, null])
} catch (error) {
}
node.curIndexAlertedDevice += 1
// Restart timer
node.startTimer()
} else {
// Nothing more to output
node.sendNoMoreDevices()
}
}
// Start timer
node.startTimer = () => {
clearTimeout(node.timerSend)
node.timerSend = setTimeout(() => {
node.handleTimer()
}, node.timerinterval * 1000)
}
// As soon as there no more devices..
node.sendNoMoreDevices = () => {
const msg = {}
msg.topic = ''
msg.count = 0
msg.devicename = ''
msg.longdevicename = ''
msg.payload = false
node.send([msg, msg, msg])
}
// Init
node.sendNoMoreDevices()
// On each deploy, unsubscribe+resubscribe
if (node.serverKNX) {
node.serverKNX.removeClient(node)
if (node.topic !== '' || node.topicSave !== '') {
node.serverKNX.addClient(node)
node.initialReadAllDevicesInRules()
}
}
}
RED.nodes.registerType('knxUltimateAlerter', knxUltimateAlerter)
}