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.

342 lines (317 loc) 12.7 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 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}` }) } } 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) { /* 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} (${formatTs(dDate)})` 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) }