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 and ETS group address importer. Easy to use and highly configurable.

335 lines (311 loc) 12.5 kB
const cloneDeep = require('lodash/cloneDeep') const dptlib = require('knxultimate').dptlib module.exports = function (RED) { function knxUltimateHuePlug (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 || config.name === '' ? 'Hue Plug' : 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' node.inputRBE = 'false' node.currentPayload = '' node.passthrough = 'no' node.formatmultiplyvalue = 1 node.formatnegativevalue = 'leave' node.formatdecimalsvalue = 2 const hueDeviceParts = (config.hueDevice || '').split('#') node.hueDevice = hueDeviceParts[0] || '' node.hueDeviceType = hueDeviceParts[1] || 'plug' node.initializingAtStart = config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes' node.enableNodePINS = config.enableNodePINS === 'yes' node.inputs = node.enableNodePINS ? 1 : 0 node.outputs = node.enableNodePINS ? 1 : 0 node.currentHUEDevice = null const pendingKnxMessages = [] const MAX_PENDING_KNX_MESSAGES = 5 const PENDING_KNX_TTL_MS = 10000 let pendingHueDeviceSnapshotPromise = null const prunePendingKnxMessages = (now = Date.now()) => { if (!pendingKnxMessages.length) return now while (pendingKnxMessages.length > 0 && (now - pendingKnxMessages[0].enqueuedAt) > PENDING_KNX_TTL_MS) { pendingKnxMessages.shift() } return now } const enqueuePendingKnxMessage = (msg) => { const now = prunePendingKnxMessages() const snapshot = { msg: (() => { try { return cloneDeep(msg) } catch (error) { return msg } })(), enqueuedAt: now } if (pendingKnxMessages.length >= MAX_PENDING_KNX_MESSAGES) { pendingKnxMessages.shift() } pendingKnxMessages.push(snapshot) } const ensureCurrentHueDevice = async ({ forceRefresh = false } = {}) => { if (!node.serverHue || typeof node.serverHue.getHueResourceSnapshot !== 'function') return undefined if (!node.hueDevice) return undefined if (pendingHueDeviceSnapshotPromise) { try { return await pendingHueDeviceSnapshotPromise } catch (error) { return undefined } } pendingHueDeviceSnapshotPromise = (async () => { try { const resource = await node.serverHue.getHueResourceSnapshot(node.hueDevice, { forceRefresh }) if (!resource) return undefined node.currentHUEDevice = cloneDeep(resource) try { node.handleSendHUE(node.currentHUEDevice) } catch (error) { RED.log.debug(`knxUltimateHuePlug: ensureCurrentHueDevice handleSendHUE error ${error.message}`) } const now = prunePendingKnxMessages() const queued = pendingKnxMessages.splice(0).filter((entry) => (now - entry.enqueuedAt) <= PENDING_KNX_TTL_MS) queued.forEach(({ msg }) => { try { node.handleSend(msg) } catch (error) { RED.log.warn(`knxUltimateHuePlug: replay queued KNX command error ${error.message}`) } }) return node.currentHUEDevice } catch (error) { RED.log.warn(`knxUltimateHuePlug: ensureCurrentHueDevice error ${error.message}`) return undefined } finally { pendingHueDeviceSnapshotPromise = null } })() return pendingHueDeviceSnapshotPromise } 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 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}) (${now.getDate()}, ${now.toLocaleTimeString()})` }) return } node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id }) } catch (error) { updateStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` }) } } 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} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})` updateStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') }) } catch (error) { /* empty */ } } 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} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})` updateStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') }) } catch (error) { /* empty */ } } node.updateKNXPlugState = function updateKNXPlugState (_value, _outputtype = 'write') { if (!config.GAPlugState) return const knxMsgPayload = {} knxMsgPayload.topic = config.GAPlugState knxMsgPayload.dpt = config.dptPlugState knxMsgPayload.payload = Boolean(_value) if (knxMsgPayload.topic) { safeSendToKNX({ grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: _outputtype }, _outputtype) } node.setNodeStatusHue({ fill: 'blue', shape: knxMsgPayload.payload ? 'dot' : 'ring', text: 'HUE->KNX On/Off', payload: knxMsgPayload.payload }) } node.updateKNXPlugPowerState = function updateKNXPlugPowerState (_value, _outputtype = 'write') { if (!config.GAPlugPowerState) return const knxMsgPayload = {} knxMsgPayload.topic = config.GAPlugPowerState knxMsgPayload.dpt = config.dptPlugPowerState knxMsgPayload.payload = Boolean(_value) if (knxMsgPayload.topic) { safeSendToKNX({ grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: _outputtype }, _outputtype) } node.setNodeStatusHue({ fill: 'blue', shape: knxMsgPayload.payload ? 'dot' : 'ring', text: 'HUE->KNX Power', payload: knxMsgPayload.payload }) } node.handleSend = (msg) => { if (node.hueDevice === '') { node.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'Missing HUE plug selection', payload: '' }) return } if (!node.currentHUEDevice) { if (node.serverHue && node.serverHue.linkStatus === 'connected') { node.setNodeStatusHue({ fill: 'yellow', shape: 'ring', text: 'Syncing with HUE bridge', payload: '' }) enqueuePendingKnxMessage(msg) ensureCurrentHueDevice({ forceRefresh: true }) } else { node.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'HUE bridge unavailable', payload: '' }) } return } try { if (msg.knx.event !== 'GroupValue_Read') { switch (msg.knx.destination) { case config.GAPlugSwitch: { const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptPlugSwitch)) const desiredState = value === true || value === 1 node.serverHue?.hueManager.writeHueQueueAdd(node.hueDevice, { on: { on: desiredState } }, 'setPlug', node.hueDeviceType || 'plug') node.setNodeStatusHue({ fill: 'green', shape: 'dot', text: 'KNX->HUE On/Off', payload: desiredState }) if (!node.currentHUEDevice) node.currentHUEDevice = {} if (!node.currentHUEDevice.on) node.currentHUEDevice.on = {} node.currentHUEDevice.on.on = desiredState break } default: break } } else { switch (msg.knx.destination) { case config.GAPlugState: if (node.currentHUEDevice?.on?.on !== undefined) node.updateKNXPlugState(node.currentHUEDevice.on.on, 'response') break case config.GAPlugPowerState: if (node.currentHUEDevice?.power_state?.power_state !== undefined) { const powerState = node.currentHUEDevice.power_state.power_state === 'on' node.updateKNXPlugPowerState(powerState, 'response') } break default: break } } } catch (error) { node.setNodeStatusHue({ fill: 'red', shape: 'dot', text: `KNX->HUE error ${error.message}`, payload: '' }) } } node.handleSendHUE = (_event) => { try { if (_event === undefined) return const eventType = (_event.type || '').toLowerCase() if (!['plug', 'smartplug', 'smart_plug', 'light'].includes(eventType)) return if (_event.id !== node.hueDevice) return node.currentHUEDevice = cloneDeep(_event) const onState = _event.on?.on === true node.updateKNXPlugState(onState) if (_event.power_state && _event.power_state.power_state !== undefined) { node.updateKNXPlugPowerState(_event.power_state.power_state === 'on') } node.setNodeStatusHue({ fill: onState ? 'green' : 'blue', shape: onState ? 'dot' : 'ring', text: 'HUE plug', payload: onState }) if (node.enableNodePINS) { const flowMsg = { payload: onState, on: _event.on, power_state: _event.power_state, rawEvent: _event } node.send(flowMsg) } } catch (error) { node.setNodeStatusHue({ fill: 'red', shape: 'dot', text: `HUE->KNX error ${error.message}`, payload: '' }) } } if (node.serverKNX) { node.serverKNX.removeClient(node) node.serverKNX.addClient(node) } if (node.serverHue) { try { node.serverHue.removeClient(node) node.serverHue.addClient(node) if (typeof node.serverHue.getHueResourceSnapshot === 'function') { ensureCurrentHueDevice({ forceRefresh: false }) } } catch (error) { RED.log.error(`knxUltimateHuePlug: register client error ${error.message}`) } } node.on('input', (msg, send, done) => { if (!node.enableNodePINS) { if (done) done() return } try { const state = RED.util.cloneMessage(msg) node.serverHue?.hueManager.writeHueQueueAdd(node.hueDevice, state, 'setPlug', node.hueDeviceType || 'plug') node.setNodeStatusHue({ fill: 'green', shape: 'dot', text: 'Flow->HUE', payload: state }) if (done) done() } catch (error) { node.setNodeStatusHue({ fill: 'red', shape: 'dot', text: `Flow error ${error.message}`, payload: '' }) if (done) done(error) } }) node.on('close', () => { if (node.serverKNX) node.serverKNX.removeClient(node) if (node.serverHue) node.serverHue.removeClient(node) }) } RED.nodes.registerType('knxUltimateHuePlug', knxUltimateHuePlug) }