diff --git a/loxone/loxone.js b/loxone/loxone.js index aed5051..fd9dc14 100644 --- a/loxone/loxone.js +++ b/loxone/loxone.js @@ -2,6 +2,17 @@ module.exports = function (RED) { "use strict"; const node_lox_ws_api = require("node-lox-ws-api"); const http = require('http'); + + // Helper functions on module scope (not recreated per instance) + const JSON_START = new Set(["{", "["]); + + const limitString = (text, limit) => + text.length <= limit ? text : `${text.slice(0, limit)}...(${text.length})`; + + function parseEventValue(v) { + if (typeof v !== 'string' || v === '' || !JSON_START.has(v[0])) return v; + try { return JSON.parse(v); } catch { return v; } + } const encMethods = { 0: 'Token-Enc', 1: 'AES-256-CBC', @@ -99,13 +110,6 @@ module.exports = function (RED) { node.handleEvent(uuid, evt); } - function _limitString(text, limit) { - if (text.length <= limit) { - return text; - } - return text.substr(0, limit) + '...(' + text.length + ')'; - } - RED.nodes.createNode(this, config); let node = this; @@ -115,6 +119,7 @@ module.exports = function (RED) { node.structureData = null; node.active = true; //config.active; node._onlineNodes = []; + node._keepaliveNodes = []; // Performance: direct array instead of eachNode() node._inputNodes = []; node._outputNodes = []; node._streamInNodes = []; @@ -210,7 +215,7 @@ module.exports = function (RED) { client.on('message_text', function (message) { switch (message.type) { case 'json': - data.json = _limitString(JSON.stringify(message.json), text_logger_limit); + data.json = limitString(JSON.stringify(message.json), text_logger_limit); node.log("received text message: " + data.json); break; @@ -260,28 +265,21 @@ module.exports = function (RED) { break; default: - data.text = _limitString(message.data, text_logger_limit); + data.text = limitString(message.data, text_logger_limit); node.log("received text message: " + data.text); } }); client.on('keepalive', function (time) { - RED.nodes.eachNode(function (nodeData) { - if (nodeData.type === 'loxone-keepalive' && - nodeData.hasOwnProperty('miniserver') && - nodeData.miniserver === node.id) { - - let keepaliveNode = RED.nodes.getNode(nodeData.id); - if (keepaliveNode) { - keepaliveNode.send({ - topic: 'keepalive', - payload: time, - msInfo: node.structureData.msInfo, - lastModified: node.structureData.lastModified - }); - } - } - }); + // Performance: Direct iteration instead of eachNode() + for (let i = 0; i < node._keepaliveNodes.length; i++) { + node._keepaliveNodes[i].send({ + topic: 'keepalive', + payload: time, + msInfo: node.structureData.msInfo, + lastModified: node.structureData.lastModified + }); + } }); client.on('message_header', function (header) { @@ -347,6 +345,19 @@ module.exports = function (RED) { this._streamAllNodes.push(handler); }; + LoxoneMiniserver.prototype.registerKeepaliveNode = function (handler) { + if (this._keepaliveNodes.indexOf(handler) === -1) { + this._keepaliveNodes.push(handler); + } + }; + + LoxoneMiniserver.prototype.deregisterKeepaliveNode = function (handler) { + const idx = this._keepaliveNodes.indexOf(handler); + if (idx !== -1) { + this._keepaliveNodes.splice(idx, 1); + } + }; + LoxoneMiniserver.prototype.addWebserviceNodeToQueue = function (handler, msg, uri) { this._webserviceNodeQueue.push({ 'handler': handler, @@ -356,59 +367,54 @@ module.exports = function (RED) { }; LoxoneMiniserver.prototype.deregisterOnlineNode = function (handler) { - this._onlineNodes.forEach(function (node, i, onlineNodes) { - if (node === handler) { - onlineNodes.splice(i, 1); - } - }); + const idx = this._onlineNodes.indexOf(handler); + if (idx !== -1) { + this._onlineNodes.splice(idx, 1); + } }; LoxoneMiniserver.prototype.deregisterInputNode = function (handler) { - this._inputNodes.forEach(function (node, i, inputNodes) { - if (node === handler) { - inputNodes.splice(i, 1); - } - }); + const idx = this._inputNodes.indexOf(handler); + if (idx !== -1) { + this._inputNodes.splice(idx, 1); + } }; LoxoneMiniserver.prototype.deregisterOutputNode = function (handler) { - this._outputNodes.forEach(function (node, i, outputNodes) { - if (node === handler) { - outputNodes.splice(i, 1); - } - }); + const idx = this._outputNodes.indexOf(handler); + if (idx !== -1) { + this._outputNodes.splice(idx, 1); + } }; LoxoneMiniserver.prototype.deregisterStreamInNode = function (handler) { - this._streamInNodes.forEach(function (node, i, streamInNodes) { - if (node === handler) { - streamInNodes.splice(i, 1); - } - }); + const idx = this._streamInNodes.indexOf(handler); + if (idx !== -1) { + this._streamInNodes.splice(idx, 1); + } }; LoxoneMiniserver.prototype.deregisterStreamAllNode = function (handler) { - this._streamAllNodes.forEach(function (node, i, streamAllNodes) { - if (node === handler) { - streamAllNodes.splice(i, 1); - } - }); + const idx = this._streamAllNodes.indexOf(handler); + if (idx !== -1) { + this._streamAllNodes.splice(idx, 1); + } }; LoxoneMiniserver.prototype.deregisterWebserviceNode = function (handler) { - this._webserviceNodes.forEach(function (node, i, outputNodes) { - if (node === handler) { - outputNodes.splice(i, 1); - } - }); + const idx = this._webserviceNodes.indexOf(handler); + if (idx !== -1) { + this._webserviceNodes.splice(idx, 1); + } }; LoxoneMiniserver.prototype.removeWebserviceNodeFromQueue = function (handler, uri) { - this._webserviceNodeQueue.forEach(function (node, i, outputNodes) { - if (node.handler === handler && node.uri === uri) { - outputNodes.splice(i, 1); - } + const idx = this._webserviceNodeQueue.findIndex(function (node) { + return node.handler === handler && node.uri === uri; }); + if (idx !== -1) { + this._webserviceNodeQueue.splice(idx, 1); + } }; LoxoneMiniserver.prototype.setConnectionStatusMsg = function (color, text, shape) { @@ -430,77 +436,27 @@ module.exports = function (RED) { }; LoxoneMiniserver.prototype.findControlByState = function (uuid) { - //search in all controls for given state uuid to find the corresponding control - for (let wantedControlUuid in this.structureData.controls) { - if ( - this.structureData.controls.hasOwnProperty(wantedControlUuid) && - this.structureData.controls[wantedControlUuid].hasOwnProperty('states') - ) { - for (let curState in this.structureData.controls[wantedControlUuid].states) { - - if ( - this.structureData.controls[wantedControlUuid].states.hasOwnProperty(curState) && - (this.structureData.controls[wantedControlUuid].states[curState].indexOf(uuid) > -1 || - this.structureData.controls[wantedControlUuid].states[curState] === uuid) - ) { - return this.structureData.controls[wantedControlUuid]; - } - } - } - } - - return null; + // O(1) lookup via Map instead of O(n) linear search + const entry = this.structureData._stateIndex.get(uuid); + return entry ? entry.control : null; }; LoxoneMiniserver.prototype.buildMsgObject = function (event, uuid, controlStructure) { - //get state name - let stateName; - for (stateName in controlStructure.states) { - if ( - controlStructure.states.hasOwnProperty(stateName) && - (controlStructure.states[stateName] === uuid || controlStructure.states[stateName].indexOf(uuid) > -1) - ) { - - if (typeof controlStructure.states[stateName] !== 'string' && controlStructure.states[stateName].indexOf(uuid) > -1) { - stateName += ':' + controlStructure.states[stateName].indexOf(uuid); - } - - break; - } - } - - //evaluate payload - let payload; - - try { - payload = JSON.parse(event); - } catch (err) { - payload = event; - } - - //check if control has a room - let room = null; - if (controlStructure.room && this.structureData.rooms[controlStructure.room].name) { - room = this.structureData.rooms[controlStructure.room].name; - } - - //check if control has a category - let category = null; - if (controlStructure.cat && this.structureData.cats[controlStructure.cat].name) { - category = this.structureData.cats[controlStructure.cat].name; - } + const tpl = this.structureData._messageTemplateCache.get(uuid); + if (tpl) return { ...tpl, payload: parseEventValue(event) }; + const entry = this.structureData._stateIndex.get(uuid); return { - payload: payload, + payload: parseEventValue(event), topic: controlStructure.name || null, - state: stateName, - room: room, - category: category, + state: entry?.stateName ?? null, + room: entry?.roomName ?? null, + category: entry?.categoryName ?? null, details: controlStructure.details || null, type: controlStructure.type || null, isFavorite: controlStructure.isFavorite || null, isSecured: controlStructure.isSecured || null, - uuid: uuid || null, + uuid, uuidAction: controlStructure.uuidAction || null, msInfo: this.structureData.msInfo, lastModified: this.structureData.lastModified @@ -559,9 +515,12 @@ module.exports = function (RED) { cats: data.cats || {}, controls: {}, msInfo: data.msInfo || {}, - lastModified: data.lastModified + lastModified: data.lastModified, + _stateIndex: new Map(), // Performance: O(1) lookup for findControlByState + _messageTemplateCache: new Map() // Performance: Pre-built message templates }; + // Pass 1: Build controls structure (parent first, then subcontrols) for (let uuid in data.controls) { if (!data.controls.hasOwnProperty(uuid)) { continue; @@ -582,33 +541,98 @@ module.exports = function (RED) { } } + // Pass 2: Build state index for O(1) lookups + // Parent controls are processed first, so they get priority if UUID appears in multiple places + for (let controlUuid in structure.controls) { + if (!structure.controls.hasOwnProperty(controlUuid)) { + continue; + } + + const control = structure.controls[controlUuid]; + if (!control.states) { + continue; + } + + for (let stateName in control.states) { + if (!control.states.hasOwnProperty(stateName)) { + continue; + } + + const stateValue = control.states[stateName]; + + // Pre-resolve room and category names (Opt #9: Denormalized Index) + const roomName = control.room && data.rooms[control.room] + ? data.rooms[control.room].name + : null; + const categoryName = control.cat && data.cats[control.cat] + ? data.cats[control.cat].name + : null; + + // Handle both string UUIDs and array of UUIDs + if (typeof stateValue === 'string') { + // Single UUID string + if (!structure._stateIndex.has(stateValue)) { + structure._stateIndex.set(stateValue, { + control: control, + stateName: stateName, + roomName: roomName, + categoryName: categoryName + }); + } + } else if (Array.isArray(stateValue)) { + // Array of UUIDs (e.g., for multi-value states) + for (let i = 0; i < stateValue.length; i++) { + const uuid = stateValue[i]; + if (typeof uuid === 'string' && !structure._stateIndex.has(uuid)) { + structure._stateIndex.set(uuid, { + control: control, + stateName: stateName + ':' + i, // Include array index in state name + roomName: roomName, + categoryName: categoryName, + arrayIndex: i + }); + } + } + } + } + } + + // Pass 3: Build message template cache (Opt #10: Message Template Cache) + // Pre-build templates with all static fields - only payload changes per event + structure._stateIndex.forEach(function(entry, stateUuid) { + const control = entry.control; + structure._messageTemplateCache.set(stateUuid, { + payload: null, // Will be set per event + topic: control.name || null, + state: entry.stateName, + room: entry.roomName, + category: entry.categoryName, + details: control.details || null, + type: control.type || null, + isFavorite: control.isFavorite || null, + isSecured: control.isSecured || null, + uuid: stateUuid, + uuidAction: control.uuidAction || null, + msInfo: structure.msInfo, + lastModified: structure.lastModified + }); + }); + return structure; } function sendOnlineNodeMsg(online, configId) { online = online || false; - RED.nodes.eachNode(function (theNode) { - if (theNode.type === 'loxone-online' && - theNode.hasOwnProperty('miniserver') && - theNode.miniserver === configId) { - - let node = RED.nodes.getNode(theNode.id); - if (node) { - /* - node.status({ - fill: (node.miniserver.active !== true) ? 'grey' : 'yellow', - shape: 'dot', - text: (node.miniserver.active !== true) ? 'connection disabled' : 'offline' - }); - */ - - node.send({ - payload: online - }); - } + // Performance: Direct iteration instead of eachNode() + const miniserver = RED.nodes.getNode(configId); + if (miniserver && miniserver._onlineNodes) { + for (let i = 0; i < miniserver._onlineNodes.length; i++) { + miniserver._onlineNodes[i].send({ + payload: online + }); } - }); + } } function LoxoneControlInNode(config) { @@ -741,6 +765,19 @@ module.exports = function (RED) { function LoxoneKeepaliveNode(config) { RED.nodes.createNode(this, config); + let node = this; + + node.miniserver = RED.nodes.getNode(config.miniserver); + if (node.miniserver) { + node.miniserver.registerKeepaliveNode(node); + + this.on('close', function (done) { + if (node.miniserver) { + node.miniserver.deregisterKeepaliveNode(node); + } + done(); + }); + } } RED.nodes.registerType('loxone-keepalive', LoxoneKeepaliveNode); diff --git a/package.json b/package.json index d268285..da733c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red-contrib-loxone", - "version": "0.10.13", + "version": "0.10.13.1", "description": "Connecting the Loxone Miniserver to node-red via Websocket API", "license": "MIT", "keywords": [ @@ -17,7 +17,7 @@ "Dustin Utecht (https://www.codm.de/)" ], "dependencies": { - "node-lox-ws-api": "github:codm/node-lox-ws-api#0.4.5-bugfix4" + "node-lox-ws-api": "github:Jakob-Gliwa/node-lox-ws-api" }, "repository": "codmpm/node-red-contrib-loxone", "engines": {