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.

343 lines (330 loc) 13.7 kB
/* eslint-disable max-len */ const dptlib = require('knxultimate').dptlib// require('knxultimate').dptlib; const loggerClass = require('./utils/sysLogger') module.exports = function (RED) { function knxUltimateHueScene (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.hueDevice = config.hueDevice node.initializingAtStart = false try { const baseLogLevel = (node.serverKNX && node.serverKNX.loglevel) ? node.serverKNX.loglevel : 'error' node.sysLogger = new loggerClass({ loglevel: baseLogLevel, setPrefix: node.type + ' <' + (node.name || node.id || '') + '>' }) } catch (error) { console.log(error.stack) } // Multi scene config.GAsceneMulti = config.GAsceneMulti === undefined ? '' : config.GAsceneMulti config.namesceneMulti = config.namesceneMulti === undefined ? '' : config.namesceneMulti config.dptsceneMulti = config.dptsceneMulti === undefined ? '' : config.dptsceneMulti config.rules = config.rules === undefined ? [] : config.rules config.selectedModeTabNumber = config.selectedModeTabNumber === undefined ? 0 : Number(config.selectedModeTabNumber) // Transform as number 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}` }) } } // 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} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})` 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} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})` updateStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') }) } catch (error) { } } let hueSceneReady = false let pendingHueSceneSnapshot = null const needsSceneReady = () => Number(config.selectedModeTabNumber) === 0 && config.hueDevice !== undefined && config.hueDevice !== '' const ensureHueSceneReady = async ({ forceRefresh = false } = {}) => { if (!needsSceneReady()) { hueSceneReady = true return true } if (!node.serverHue || typeof node.serverHue.getHueResourceSnapshot !== 'function') { hueSceneReady = false return false } if (pendingHueSceneSnapshot) { try { return await pendingHueSceneSnapshot } catch (error) { return false } } pendingHueSceneSnapshot = (async () => { try { const resource = await node.serverHue.getHueResourceSnapshot(config.hueDevice, { forceRefresh }) if (!resource) { hueSceneReady = false node.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'Hue scene not found', payload: '' }) return false } hueSceneReady = true try { node.handleSendHUE(resource) } catch (error) { RED.log.debug(`knxUltimateHueScene: ensureHueSceneReady handleSendHUE error ${error.message}`) } return true } catch (error) { hueSceneReady = false RED.log.warn(`knxUltimateHueScene: ensureHueSceneReady error ${error.message}`) node.setNodeStatusHue({ fill: 'red', shape: 'ring', text: `Sync failed ${error.message}`, payload: '' }) return false } finally { pendingHueSceneSnapshot = null } })() return pendingHueSceneSnapshot } const processKnxSceneCommand = (msg) => { let state = {} if (Number(config.selectedModeTabNumber) === 0) { try { switch (msg.knx.destination) { case config.GAscene: { msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptscene)) if (config.dptscene.startsWith('1.')) { if (msg.payload === true) { state = { recall: { action: config.hueSceneRecallType } } node.serverHue.hueManager.writeHueQueueAdd(config.hueDevice, state, 'setScene') } else { (async () => { try { node.serverHue.hueManager.writeHueQueueAdd(config.hueDevice, { on: { on: false } }, 'stopScene') } catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info(`KNXUltimateHUEConfig: stopScene: ${error.message}`) } })() } } else if (Number(config.valscene) === msg.payload.scenenumber && msg.payload.save_recall === 0) { state = { recall: { action: config.hueSceneRecallType } } node.serverHue.hueManager.writeHueQueueAdd(config.hueDevice, state, 'setScene') } node.setNodeStatusHue({ fill: 'green', shape: 'dot', text: 'KNX->HUE', payload: msg.payload }) break } default: break } } catch (error) { updateStatus({ fill: 'red', shape: 'dot', text: `KNX->HUE single: ${error.message} (${new Date().getDate()}, ${new Date().toLocaleTimeString()})` }) } } else if (Number(config.selectedModeTabNumber) === 1) { try { if (config.GAsceneMulti !== undefined && config.dptsceneMulti !== undefined && config.rules.length !== 0 && config.GAsceneMulti === msg.knx.destination) { msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptsceneMulti)) config.rules.forEach((row) => { if (row.rowRuleKNXSceneNumber !== undefined && row.rowRuleHUESceneID !== undefined && row.rowRuleRecallAs !== undefined && Number(row.rowRuleKNXSceneNumber) === Number(msg.payload.scenenumber) && Number(msg.payload.save_recall) === 0 && node.serverHue.hueManager !== undefined) { state = { recall: { action: row.rowRuleRecallAs } } node.serverHue.hueManager.writeHueQueueAdd(row.rowRuleHUESceneID, state, 'setScene') node.setNodeStatusHue({ fill: 'green', shape: 'dot', text: 'KNX->HUE', payload: msg.payload }) } }) } } catch (error) { updateStatus({ fill: 'red', shape: 'dot', text: `KNX->HUE multi: ${error.message} (${new Date().getDate()}, ${new Date().toLocaleTimeString()})` }) } } } // This function is called by the knx-hue config node node.handleSend = (msg) => { if (needsSceneReady() && !hueSceneReady) { node.setNodeStatusHue({ fill: 'yellow', shape: 'ring', text: 'Syncing with HUE bridge', payload: '' }) const clonedMsg = RED.util.cloneMessage(msg) ensureHueSceneReady({ forceRefresh: true }).then((ready) => { if (ready) processKnxSceneCommand(clonedMsg) }).catch(() => { /* handled in ensure */ }) return } processKnxSceneCommand(msg) } node.handleSendHUE = (_event) => { try { if (Number(config.selectedModeTabNumber) === 0 && config.hueDevice !== undefined && _event.id === config.hueDevice) { // Single mode const knxMsgPayload = {} knxMsgPayload.topic = config.GAsceneStatus knxMsgPayload.dpt = config.dptsceneStatus if (_event.hasOwnProperty('status') && _event.status.hasOwnProperty('active')) { knxMsgPayload.payload = _event.status.active !== 'inactive' // Send to KNX bus if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) { safeSendToKNX({ grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write' }, 'write') } updateStatus({ fill: 'blue', shape: 'dot', text: `HUE->KNX ${JSON.stringify(knxMsgPayload.payload)} (${new Date().getDate()}, ${new Date().toLocaleTimeString()})` }) // Output the msg to the flow node.send(_event) } } else if (Number(config.selectedModeTabNumber) === 1 && config.rules !== undefined) { // Multi mode config.rules.forEach(row => { if (row.rowRuleHUESceneID !== undefined && _event.id === row.rowRuleHUESceneID) { // Output the msg to the flow node.send(_event) } }) } } catch (error) { updateStatus({ fill: 'red', shape: 'dot', text: 'HUE->KNX error ' + error.message + ' (' + new Date().getDate() + ', ' + new Date().toLocaleTimeString() + ')' }) } } // 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) ensureHueSceneReady({ forceRefresh: true }) } const processFlowSceneCommand = (msg) => { try { const state = RED.util.cloneMessage(msg) node.serverHue.hueManager.writeHueQueueAdd(config.hueDevice, state, 'setScene') const statusValue = state.payload !== undefined ? state.payload : state node.setNodeStatusHue({ fill: 'green', shape: 'dot', text: '->HUE', payload: statusValue }) } catch (error) { node.setNodeStatusHue({ fill: 'red', shape: 'dot', text: '->HUE', payload: error.message }) throw error } } node.on('input', (msg, send, done) => { const finalize = (error) => { if (done) done(error) } if (needsSceneReady() && !hueSceneReady) { node.setNodeStatusHue({ fill: 'yellow', shape: 'ring', text: 'Syncing with HUE bridge', payload: '' }) const clonedMsg = RED.util.cloneMessage(msg) ensureHueSceneReady({ forceRefresh: true }).then((ready) => { if (ready) { try { processFlowSceneCommand(clonedMsg) finalize() } catch (error) { finalize(error) } } else { finalize(new Error('Hue scene initialization failed')) } }).catch((error) => { finalize(error) }) return } try { processFlowSceneCommand(msg) finalize() } catch (error) { finalize(error) } }) node.on('close', function (done) { if (node.serverKNX) { node.serverKNX.removeClient(node) } if (node.serverHue) { node.serverHue.removeClient(node) } done() }) } RED.nodes.registerType('knxUltimateHueScene', knxUltimateHueScene) }