UNPKG

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, and KNX routing between interfaces. Easy to use and highly configurable.

293 lines (274 loc) 12.6 kB
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) }