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.
293 lines (274 loc) • 12.6 kB
JavaScript
module.exports = function (RED) {
const dptlib = require('knxultimate').dptlib
function knxUltimateHueButton (config) {
RED.nodes.createNode(this, config)
const node = this
node.serverKNX = RED.nodes.getNode(config.server) || undefined
node.serverHue = RED.nodes.getNode(config.serverHue) || undefined
node.topic = node.name
node.name = config.name === undefined ? 'Hue' : config.name
node.dpt = ''
node.notifyreadrequest = false
node.notifyreadrequestalsorespondtobus = 'false'
node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
node.notifyresponse = false
node.notifywrite = true
node.initialread = true
node.listenallga = true // Don't remove
node.outputtype = 'write'
node.outputRBE = 'false' // Apply or not RBE to the output (Messages coming from flow)
node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS)
node.currentPayload = '' // Current value for the RBE input and for the .previouspayload msg
node.passthrough = 'no'
node.formatmultiplyvalue = 1
node.formatnegativevalue = 'leave'
node.formatdecimalsvalue = 2
node.short_releaseValue = false
node.isTimerDimStopRunning = false
node.hueDevice = config.hueDevice
node.initializingAtStart = false
// When toggle status is disabled, uses these values
node.switchSend = config.switchSend === undefined ? 'true' : config.switchSend
node.switchSend = node.switchSend === 'true' // The typedvalue in the html returns a string, so i convert it to bool
node.dimSend = config.dimSend === undefined ? 'up' : config.dimSend
if (node.dimSend === 'up') node.dimSend = { decr_incr: 1, data: 3 }
if (node.dimSend === 'down') node.dimSend = { decr_incr: 0, data: 3 }
if (node.dimSend === 'stop') node.dimSend = { decr_incr: 0, data: 0 }
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)
}
const formatTs = (date) => {
const d = date instanceof Date ? date : new Date(date)
const provider = node.serverKNX
if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d)
return `${d.getDate()}, ${d.toLocaleTimeString()}`
}
const safeSendToKNX = (telegram, context = 'write') => {
try {
if (!node.serverKNX || typeof node.serverKNX.sendKNXTelegramToKNXEngine !== 'function') {
const now = new Date()
updateStatus({ fill: 'red', shape: 'dot', text: `KNX server missing (${context}) (${formatTs(now)})` })
return
}
node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id })
} catch (error) {
updateStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` })
}
}
// Used to call the status update from the config node.
node.setNodeStatus = ({
fill, shape, text, payload
}) => {
try {
if (payload === undefined) payload = ''
const dDate = new Date()
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${formatTs(dDate)})`
updateStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
} catch (error) { }
}
// Used to call the status update from the HUE config node.
node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
try {
if (payload === undefined) payload = ''
const dDate = new Date()
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${formatTs(dDate)})`
updateStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
} catch (error) { }
}
// This function is called by the knx-ultimate config node, to output a msg.payload.
node.handleSend = (msg) => {
try {
switch (msg.knx.destination) {
case config.GAshort_releaseStatus:
msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptshort_release))
node.short_releaseValue = msg.payload
node.setNodeStatusHue({
fill: 'green', shape: 'dot', text: 'KNX->HUE Short Release Status', payload: msg.payload
})
break
case config.GArepeatStatus:
msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptrepeat))
node.toggleGArepeat = msg.payload.decr_incr === 1
node.setNodeStatusHue({
fill: 'green', shape: 'dot', text: 'KNX->HUE Repeat Status', payload: msg.payload
})
break
default:
break
}
} catch (error) {
node.setNodeStatusHue({
fill: 'red', shape: 'dot', text: `KNX->HUE error ${error.message}`, payload: ''
})
}
}
node.handleSendHUE = (_event) => {
try {
if (_event.id === config.hueDevice) {
const buttonEvent = _event?.button?.button_report?.event || _event?.button?.last_event
if (!_event.hasOwnProperty('button') || buttonEvent === undefined) return
const knxMsgPayload = {}
let flowMsgPayload = true
let didUpdateStatus = false
// Handling events with toggles
// KNX Dimming reminder tips
// { decr_incr: 1, data: 1 } : Start increasing until { decr_incr: 0, data: 0 } is received.
// { decr_incr: 0, data: 1 } : Start decreasing until { decr_incr: 0, data: 0 } is received.
switch (buttonEvent) {
case 'initial_press':
if (node.initial_pressValue === undefined) node.initial_pressValue = false
node.initial_pressValue = config.toggleValues ? !node.initial_pressValue : node.switchSend
flowMsgPayload = node.initial_pressValue
break
case 'long_release':
flowMsgPayload = node.long_pressValue
// if the dimmer was running, send the STOP telegram to the KNX bus wires, using the GArepeat Group address and dpt.
if (node.isTimerDimStopRunning) {
knxMsgPayload.topic = config.GArepeat
knxMsgPayload.dpt = config.dptrepeat
node.stopDIM(knxMsgPayload)
}
break
case 'double_short_release':
if (node.double_short_releaseValue === undefined) node.double_short_releaseValue = false
node.double_short_releaseValue = config.toggleValues ? !node.double_short_releaseValue : node.switchSend
flowMsgPayload = node.double_short_releaseValue
break
case 'long_press':
if (node.long_pressValue === undefined) node.long_pressValue = false
node.long_pressValue = config.toggleValues ? !node.long_pressValue : node.dimSend
flowMsgPayload = node.long_pressValue
break
case 'short_release':
node.short_releaseValue = config.toggleValues ? !node.short_releaseValue : node.switchSend
flowMsgPayload = node.short_releaseValue
if (config.GAshort_release !== undefined && config.GAshort_release !== '') {
knxMsgPayload.topic = config.GAshort_release
knxMsgPayload.dpt = config.dptshort_release
knxMsgPayload.payload = node.short_releaseValue
// Send to KNX bus
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
safeSendToKNX({
grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write'
}, 'write')
}
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
node.setNodeStatusHue({
fill: 'blue', shape: 'dot', text: `HUE->KNX ${buttonEvent}`, payload: knxMsgPayload.payload
})
didUpdateStatus = true
}
}
break
case 'repeat':
flowMsgPayload = node.long_pressValue
if (config.GArepeat !== undefined && config.GArepeat !== '') {
if (node.isTimerDimStopRunning === false) {
// Set KNX Dim up/down start
knxMsgPayload.topic = config.GArepeat
knxMsgPayload.dpt = config.dptrepeat
if (typeof (node.long_pressValue) === 'object') {
knxMsgPayload.payload = node.long_pressValue // Send fixed value when toggleValues is false
} else {
knxMsgPayload.payload = node.long_pressValue ? { decr_incr: 0, data: 3 } : { decr_incr: 1, data: 3 } // If the light is turned on, the initial DIM direction must be down, otherwise, up
}
// Send to KNX bus
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
safeSendToKNX({
grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write'
}, 'write')
}
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
node.setNodeStatusHue({
fill: 'blue', shape: 'dot', text: 'HUE->KNX START DIM', payload: ''
})
didUpdateStatus = true
}
}
node.startDimStopper(knxMsgPayload)
}
break
default:
break
}
if (!didUpdateStatus) {
node.setNodeStatusHue({
fill: 'blue',
shape: 'dot',
text: `HUE ${buttonEvent}`,
payload: flowMsgPayload
})
}
// Setup the output msg
const flowMsg = {}
flowMsg.name = node.name
flowMsg.event = buttonEvent
if (_event.button?.button_report?.updated) flowMsg.updated = _event.button.button_report.updated
flowMsg.rawEvent = _event
flowMsg.payload = flowMsgPayload
node.send(flowMsg)
}
} catch (error) {
node.setNodeStatusHue({
fill: 'red', shape: 'dot', text: `HUE->KNX error ${error.message}`, payload: ''
})
}
}
// Timer to stop the dimming sequence
node.startDimStopper = function (knxMsgPayload) {
if (node.timerDimStop !== undefined) clearTimeout(node.timerDimStop)
node.isTimerDimStopRunning = true
node.timerDimStop = setTimeout(() => {
node.stopDIM(knxMsgPayload)
}, 2000)
}
node.stopDIM = function (knxMsgPayload) {
// KNX Stop DIM
if (node.timerDimStop !== undefined) clearTimeout(node.timerDimStop)
node.isTimerDimStopRunning = false
knxMsgPayload.payload = { decr_incr: 0, data: 0 } // Payload for the output msg
// Send to KNX bus
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
safeSendToKNX({
grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write'
}, 'write')
node.setNodeStatusHue({
fill: 'grey', shape: 'ring', text: 'HUE->KNX STOP DIM', payload: knxMsgPayload.payload
})
}
}
// On each deploy, unsubscribe+resubscribe
if (node.serverKNX) {
node.serverKNX.removeClient(node)
node.serverKNX.addClient(node)
}
if (node.serverHue) {
node.serverHue.removeClient(node)
node.serverHue.addClient(node)
}
node.on('input', (msg) => {
})
node.on('close', (done) => {
if (node.serverKNX) {
node.serverKNX.removeClient(node)
}
if (node.serverHue) {
node.serverHue.removeClient(node)
}
done()
})
}
RED.nodes.registerType('knxUltimateHueButton', knxUltimateHueButton)
}