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.

1,106 lines (1,038 loc) 84.3 kB
/* eslint-disable prefer-template */ /* eslint-disable no-inner-declarations */ /* eslint-disable curly */ /* eslint-disable max-len */ /* eslint-disable prefer-arrow-callback */ const fs = require("fs"); const path = require("path"); const net = require("net"); const _ = require("lodash"); const knx = require("knxultimate"); // 2025-09: Use KNXUltimate built-in keyring for KNX Secure validation let Keyring; try { // Not exported by default; import from build path ({ Keyring } = require("knxultimate/build/secure/keyring")); } catch (e) { Keyring = null; } //const dptlib = require('knxultimate').dptlib; const dptlib = require('knxultimate').dptlib; const loggerClass = require('./utils/sysLogger') // const { Server } = require('http') const payloadRounder = require("./utils/payloadManipulation"); const utils = require('./utils/utils'); // DATAPONT MANIPULATION HELPERS // #################### const sortBy = (field) => (a, b) => { if (a[field] > b[field]) { return 1; } else { return -1; } }; const onlyDptKeys = (kv) => { return kv[0].startsWith("DPT"); }; const extractBaseNo = (kv) => { return { subtypes: kv[1].subtypes, base: parseInt(kv[1].id.replace("DPT", "")), }; }; const convertSubtype = (baseType) => (kv) => { const value = `${baseType.base}.${kv[0]}`; // let sRet = value + " " + kv[1].name + (kv[1].unit === undefined ? "" : " (" + kv[1].unit + ")"); const sRet = value + " " + kv[1].name; return { value, text: sRet, }; }; const toConcattedSubtypes = (acc, baseType) => { const subtypes = Object.entries(baseType.subtypes).sort(sortBy(0)).map(convertSubtype(baseType)); return acc.concat(subtypes); }; // #################### module.exports = (RED) => { function knxUltimateConfigNode(config) { RED.nodes.createNode(this, config); const node = this; node.host = config.host; node.port = parseInt(config.port); node.physAddr = config.physAddr // the KNX physical address we'd like to use node.suppressACKRequest = typeof config.suppressACKRequest === "undefined" ? true : config.suppressACKRequest; // enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this node.linkStatus = "disconnected"; // Can be: connected or disconnected node.nodeClients = []; // Stores the registered clients node.KNXEthInterface = typeof config.KNXEthInterface === "undefined" ? "Auto" : config.KNXEthInterface; node.KNXEthInterfaceManuallyInput = typeof config.KNXEthInterfaceManuallyInput === "undefined" ? "" : config.KNXEthInterfaceManuallyInput; // If you manually set the interface name, it will be wrote here node.timerDoInitialRead = null; // 17/02/2020 Timer (timeout) to do initial read of all nodes requesting initial read, after all nodes have been registered to the sercer node.stopETSImportIfNoDatapoint = typeof config.stopETSImportIfNoDatapoint === "undefined" ? "stop" : config.stopETSImportIfNoDatapoint; // 09/01/2020 Stop, Import Fake or Skip the import if a group address has unset datapoint node.userDir = path.join(RED.settings.userDir, "knxultimatestorage"); // 04/04/2021 Supergiovane: Storage for service files node.exposedGAs = []; node.loglevel = config.loglevel !== undefined ? config.loglevel : "error"; // 18/02/2020 Loglevel default error if (node.loglevel === 'trace') node.loglevel = 'debug'; // Backward compatibility if (node.loglevel === 'silent') node.loglevel = 'disable'; // Backward compatibility node.sysLogger = null; // 20/03/2022 Default try { node.sysLogger = new loggerClass({ loglevel: node.loglevel, setPrefix: node.type + " <" + (node.name || node.id || '') + ">" }); } catch (error) { console.log(error.stack) } node.csv = readCSV(config.csv); // Array from ETS CSV Group Addresses {ga:group address, dpt: datapoint, devicename: full device name with main and subgroups} // 12/11/2021 Connect at start delay node.autoReconnect = true; // 20/03/2022 Default if (config.autoReconnect === "no" || config.autoReconnect === false) { node.autoReconnect = false; } else { node.autoReconnect = true; } node.ignoreTelegramsWithRepeatedFlag = config.ignoreTelegramsWithRepeatedFlag === undefined ? false : config.ignoreTelegramsWithRepeatedFlag; // 24/07/2021 KNX Secure checks... node.keyringFileXML = typeof config.keyringFileXML === "undefined" || config.keyringFileXML.trim() === "" ? "" : config.keyringFileXML; node.knxSecureSelected = typeof config.knxSecureSelected === "undefined" ? false : config.knxSecureSelected; node.secureCredentialsMode = typeof config.secureCredentialsMode === "undefined" ? "keyring" : config.secureCredentialsMode; // 2025-09 Secure Tunnel Interface IA selection (Auto/Manual) node.tunnelIASelection = typeof config.tunnelIASelection === "undefined" ? "Auto" : config.tunnelIASelection; node.tunnelIA = typeof config.tunnelIA === "undefined" ? "" : config.tunnelIA; node.tunnelInterfaceIndividualAddress = typeof config.tunnelInterfaceIndividualAddress === "undefined" ? "" : config.tunnelInterfaceIndividualAddress; node.name = config.name === undefined || config.name === "" ? node.host : config.name; // 12/08/2021 node.timerKNXUltimateCheckState = null; // 08/10/2021 Check the state. If not connected and autoreconnect is true, retrig the connetion attempt. node.knxConnectionProperties = null; // Retains the connection properties node.allowLauch_initKNXConnection = true; // See the node.timerKNXUltimateCheckState function node.hostProtocol = config.hostProtocol === undefined ? "Auto" : config.hostProtocol; // 20/03/2022 Default node.knxConnection = null; // 20/03/2022 Default node.delaybetweentelegrams = (config.delaybetweentelegrams === undefined || config.delaybetweentelegrams === null || config.delaybetweentelegrams === '') ? 25 : Number(config.delaybetweentelegrams); if (node.delaybetweentelegrams < 25) node.delaybetweentelegrams = 25; // Protection avoiding handleKNXQueue hangs if (node.delaybetweentelegrams > 100) node.delaybetweentelegrams = 100; // Protection avoiding handleKNXQueue hangs node.timerSaveExposedGAs = null; // Timer to save the exposed GA every once in a while // 05/12/2021 Set the protocol (this is undefined if coming from ild versions if (node.hostProtocol === "Auto") { // Auto set protocol based on IP if ( node.host.startsWith("224.") || node.host.startsWith("225.") || node.host.startsWith("232.") || node.host.startsWith("233.") || node.host.startsWith("234.") || node.host.startsWith("235.") || node.host.startsWith("239.") ) { node.hostProtocol = "Multicast"; } else { node.hostProtocol = "TunnelUDP"; } node.sysLogger?.info("IP Protocol AUTO SET to " + node.hostProtocol + ", based on IP " + node.host); } node.setAllClientsStatus = (_status, _color, _text) => { node.nodeClients.forEach((oClient) => { try { if (oClient.setNodeStatus !== undefined) oClient.setNodeStatus({ fill: _color, shape: "dot", text: _status + " " + _text, payload: "", GA: oClient.topic, dpt: "", devicename: "", }); } catch (error) { node.sysLogger?.warn("Wow setAllClientsStatus error " + error.message); } }); }; // // KNX-SECURE // Validate keyring (if available) and prepare secure configuration // node.secureTunnelConfig = undefined; (async () => { try { if (node.knxSecureSelected) { const secureMode = typeof node.secureCredentialsMode === "string" ? node.secureCredentialsMode : "keyring"; if (secureMode === "manual") { // Manual credentials: no keyring, only IA + password node.secureTunnelConfig = { tunnelInterfaceIndividualAddress: (node.tunnelInterfaceIndividualAddress || "").trim(), tunnelUserPassword: node.credentials?.tunnelUserPassword || "", }; if (!node.secureTunnelConfig.tunnelInterfaceIndividualAddress) { delete node.secureTunnelConfig.tunnelInterfaceIndividualAddress; } RED.log.info("KNX-Secure: secure mode selected. Using manual tunnel credentials."); } else { // Prepare secure config for KNXClient. The loader accepts either a // filesystem path to .knxkeys or the raw XML/base64 content. node.secureTunnelConfig = { knxkeys_file_path: node.keyringFileXML || "", knxkeys_password: node.credentials?.keyringFilePassword || "", }; // Manual IA selection based on keyring content try { if (node.tunnelIASelection === "Manual" && typeof node.tunnelIA === "string" && node.tunnelIA.trim() !== "") { node.secureTunnelConfig.tunnelInterfaceIndividualAddress = node.tunnelIA.trim(); } else { node.secureTunnelConfig.tunnelInterfaceIndividualAddress = ""; // Auto } } catch (e) { /* empty */ } // Optional early validation to give immediate feedback (non-fatal) if (Keyring && node.keyringFileXML && (node.credentials?.keyringFilePassword || "") !== "") { try { const kr = new Keyring(); await kr.load(node.keyringFileXML, node.credentials.keyringFilePassword); const createdBy = kr.getCreatedBy?.() || "unknown"; const created = kr.getCreated?.() || "unknown"; RED.log.info(`KNX-Secure: Keyring validated (Created by ${createdBy} on ${created}) using node ${node.name || node.id}`); } catch (err) { node.sysLogger?.error("KNX Secure: keyring validation failed: " + err.message); // Keep secure enabled: KNXClient will emit detailed errors on connect } } else { RED.log.info("KNX-Secure: secure mode selected. Using provided keyring and password."); } } } else { RED.log.info("KNX-Unsecure: connection to insecure interface/router using node " + (node.name || node.id)); } } catch (error) { node.sysLogger?.error("KNX Secure: error preparing secure configuration: " + error.message); node.secureTunnelConfig = undefined; node.knxSecureSelected = false; const t = setTimeout(() => node.setAllClientsStatus("Error", "red", "KNX Secure " + error.message), 2000); } })(); // 04/04/2021 Supergiovane, creates the service paths where the persistent files are created. // The values file is stored only upon disconnection/close // ************************ function setupDirectory(_aPath) { if (!fs.existsSync(_aPath)) { // Create the path try { fs.mkdirSync(_aPath); return true; } catch (error) { return false; } } else { return true; } } if (!setupDirectory(node.userDir)) { node.sysLogger?.error("Unable to set up MAIN directory: " + node.userDir); } if (!setupDirectory(path.join(node.userDir, "knxpersistvalues"))) { node.sysLogger?.error("Unable to set up cache directory: " + path.join(node.userDir, "knxpersistvalues")); } else { node.sysLogger?.info("payload cache set to " + path.join(node.userDir, "knxpersistvalues")); } async function saveExposedGAs() { const sFile = path.join(node.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json"); try { if (node.exposedGAs.length > 0) { fs.writeFileSync(sFile, JSON.stringify(node.exposedGAs)); //node.sysLogger?.debug("wrote peristent values to the file " + sFile); } } catch (err) { node.sysLogger?.error("unable to write peristent values to the file " + sFile + " " + err.message); } } function loadExposedGAs() { const sFile = path.join(node.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json"); try { node.exposedGAs = JSON.parse(fs.readFileSync(sFile, "utf8")); } catch (err) { node.exposedGAs = []; node.sysLogger?.info("unable to read peristent file " + sFile + " " + err.message); } } // ************************ // 16/02/2020 KNX-Ultimate nodes calls this function, then this funcion calls the same function on the Watchdog node.reportToWatchdogCalledByKNXUltimateNode = (_oError) => { // _oError is = { nodeid: node.id, topic: node.outputtopic, devicename: devicename, GA: GA, text: text }; const readHistory = []; const delay = 0; node.nodeClients .filter((_oClient) => _oClient.isWatchDog !== undefined && _oClient.isWatchDog === true) .forEach((_oClient) => { _oClient.signalNodeErrorCalledByConfigNode(_oError); }); }; node.addClient = (_Node) => { // Check if node already exists if (node.nodeClients.filter((x) => x.id === _Node.id).length === 0) { // Add _Node to the clients array if (node.autoReconnect) { _Node.setNodeStatus({ fill: "grey", shape: "ring", text: "Node initialized.", payload: "", GA: "", dpt: "", devicename: "", }); } else { _Node.setNodeStatus({ fill: "red", shape: "ring", text: "Autoconnect disabled. Please manually connect.", payload: "", GA: "", dpt: "", devicename: "", }); } node.nodeClients.push(_Node); } }; node.removeClient = async (_Node) => { // Remove the client node from the clients array try { node.nodeClients = node.nodeClients.filter((x) => x.id !== _Node.id); } catch (error) { /* empty */ } // If no clien nodes, disconnect from bus. if (node.nodeClients.length === 0) { try { await node.Disconnect(); } catch (error) { /* empty */ } } }; // 17/02/2020 Do initial read (called by node.timerDoInitialRead timer) function DoInitialReadFromKNXBusOrFile() { if (node.linkStatus !== "connected") return; // 29/08/2019 If not connected, exit node.sysLogger?.info("Do DoInitialReadFromKNXBusOrFile"); loadExposedGAs(); // 04/04/2021 load the current values of GA payload node.sysLogger?.info("Loaded persist GA values", node.exposedGAs?.length); if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs); node.timerSaveExposedGAs = setInterval(async () => { await saveExposedGAs(); }, 5000); node.sysLogger?.info("Started timerSaveExposedGAs with array lenght ", node.exposedGAs?.length); try { const readHistory = []; // First, read from file. This allow all virtual devices to get their values from file. node.nodeClients .filter((_oClient) => _oClient.initialread === 2 || _oClient.initialread === 3) .filter((_oClient) => _oClient.hasOwnProperty("isWatchDog") === false) .forEach((_oClient) => { if (node.linkStatus !== "connected") return; // 16/08/2021 If not connected, exit // 04/04/2020 selected READ FROM FILE 2 or from file then from bus 3 if (_oClient.listenallga === true) { // 13/12/2021 DA FARE } else { try { if (node.exposedGAs.length > 0) { const oExposedGA = node.exposedGAs.find((a) => a.ga === _oClient.topic); if (oExposedGA !== undefined) { // Retrieve the value from exposedGAs const msg = buildInputMessage({ _srcGA: "", _destGA: _oClient.topic, _event: "GroupValue_Response", _Rawvalue: Buffer.from(oExposedGA.rawValue.data), _inputDpt: _oClient.dpt, _devicename: _oClient.name ? _oClient.name : "", _outputtopic: _oClient.outputtopic, _oNode: _oClient, _echoed: false }); _oClient.previouspayload = ""; // 05/04/2021 Added previous payload _oClient.currentPayload = msg.payload; _oClient.setNodeStatus({ fill: "grey", shape: "dot", text: "Update value from persist file", payload: _oClient.currentPayload, GA: _oClient.topic, dpt: _oClient.dpt, devicename: _oClient.name || "", }); // 06/05/2021 If, after the rawdata has been savad to file, the user changes the datapoint, the buildInputMessage returns payload null, because it's unable to convert the value if (msg.payload === null) { // Delete the exposedGA node.exposedGAs = node.exposedGAs.filter((item) => item.ga !== _oClient.topic); _oClient.setNodeStatus({ fill: "yellow", shape: "dot", text: "Datapoint has been changed, remove the value from persist file", payload: _oClient.currentPayload, GA: _oClient.topic, dpt: _oClient.dpt, devicename: _oClient.devicename || "", }); node.sysLogger?.error("DoInitialReadFromKNXBusOrFile: Datapoint may have been changed, remove the value from persist file of " + _oClient.topic + " Devicename " + _oClient.name + " Currend DPT " + _oClient.dpt + " Node.id " + _oClient.id); } else { if (_oClient.notifyresponse) _oClient.handleSend(msg); } } else { if (_oClient.initialread === 3) { // Not found, issue a READ to the bus if (!readHistory.includes(_oClient.topic)) { node.sysLogger?.debug("DoInitialReadFromKNXBusOrFile 3: sent read request to GA " + _oClient.topic); _oClient.setNodeStatus({ fill: "grey", shape: "dot", text: "Persist value not found, issuing READ request to BUS", payload: _oClient.currentPayload, GA: _oClient.topic, dpt: _oClient.dpt, devicename: _oClient.devicename || "", }); node.sendKNXTelegramToKNXEngine({ grpaddr: _oClient.topic, payload: "", dpt: "", outputtype: "read", nodecallerid: _oClient.id, }); readHistory.push(_oClient.topic); } } } } } catch (error) { node.sysLogger?.error("DoInitialReadFromKNXBusOrFile: " + error.stack); } } }); // Then, after all values have been read from file, read from BUS // This allow the virtual devices to get their values before this will be readed from bus node.nodeClients .filter((_oClient) => _oClient.initialread === 1) .filter((_oClient) => _oClient.hasOwnProperty("isWatchDog") === false) .forEach((_oClient) => { if (node.linkStatus !== "connected") return; // 16/08/2021 If not connected, exit // 04/04/2020 selected READ FROM BUS 1 if (_oClient.hasOwnProperty("isalertnode") && _oClient.isalertnode) { _oClient.initialReadAllDevicesInRules(); } else if (_oClient.hasOwnProperty("isLoadControlNode") && _oClient.isLoadControlNode) { _oClient.initialReadAllDevicesInRules(); } else if (_oClient.listenallga === true) { for (let index = 0; index < node.csv.length; index++) { const element = node.csv[index]; if (!readHistory.includes(element.ga)) { node.sendKNXTelegramToKNXEngine({ grpaddr: element.ga, payload: "", dpt: "", outputtype: "read", nodecallerid: element.id, }); readHistory.push(element.ga); node.sysLogger?.debug("DoInitialReadFromKNXBusOrFile from Universal Node: sent read request to GA " + element.ga); } } } else { if (!readHistory.includes(_oClient.topic)) { node.sendKNXTelegramToKNXEngine({ grpaddr: _oClient.topic, payload: "", dpt: "", outputtype: "read", nodecallerid: _oClient.id, }); readHistory.push(_oClient.topic); node.sysLogger?.debug("DoInitialReadFromKNXBusOrFile: sent read request to GA " + _oClient.topic); } } }); } catch (error) { } } // 01/02/2020 Dinamic change of the KNX Gateway IP, Port and Physical Address // This new thing has been requested by proServ RealKNX staff. node.setGatewayConfig = async ( /** @type {string} */ _sIP, /** @type {number} */ _iPort, /** @type {string} */ _sPhysicalAddress, /** @type {string} */ _sBindToEthernetInterface, /** @type {string} */ _Protocol, /** @type {string} */ _CSV, ) => { if (typeof _sIP !== "undefined" && _sIP !== "") node.host = _sIP; if (typeof _iPort !== "undefined" && _iPort !== 0) node.port = _iPort; if (typeof _sPhysicalAddress !== "undefined" && _sPhysicalAddress !== "") node.physAddr = _sPhysicalAddress; if (typeof _sBindToEthernetInterface !== "undefined") node.KNXEthInterface = _sBindToEthernetInterface; if (typeof _Protocol !== "undefined") node.hostProtocol = _Protocol; if (typeof _CSV !== "undefined" && _CSV !== "") { try { const sTemp = readCSV(_CSV); // 27/09/2022 Set the new CSV node.csv = sTemp; } catch (error) { node.sysLogger?.info("Node's main config setting error. " + error.message || ""); } } node.sysLogger?.info( "Node's main config setting has been changed. New config: IP " + node.host + " Port " + node.port + " PhysicalAddress " + node.physAddr + " BindToInterface " + node.KNXEthInterface + (typeof _CSV !== "undefined" && _CSV !== "" ? ". A new group address CSV has been imported." : ""), ); try { await node.Disconnect(); // node.setKnxConnectionProperties(); // 28/12/2021 Commented node.setAllClientsStatus("CONFIG", "yellow", "KNXUltimage-config:setGatewayConfig: disconnected by new setting..."); node.sysLogger?.debug("KNXUltimage-config:setGatewayConfig: disconnected by setGatewayConfig."); } catch (error) { } }; // 05/05/2021 force connection or disconnection from the KNX BUS and disable the autoreconenctions attempts. // This new thing has been requested by proServ RealKNX staff. node.connectGateway = async (_bConnection) => { if (_bConnection === undefined) return; node.sysLogger?.info( (_bConnection === true ? "Forced connection from watchdog" : "Forced disconnection from watchdog") + node.host + " Port " + node.port + " PhysicalAddress " + node.physAddr + " BindToInterface " + node.KNXEthInterface, ); if (_bConnection === true) { // CONNECT AND ENABLE RECONNECTION ATTEMPTS try { await node.Disconnect(); node.setAllClientsStatus("CONFIG", "yellow", "Forced GW connection from watchdog."); node.autoReconnect = true; } catch (error) { } } else { // DISCONNECT AND DISABLE RECONNECTION ATTEMPTS try { node.autoReconnect = false; await node.Disconnect(); const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setAllClientsStatus("CONFIG", "yellow", "Forced GW disconnection and stop reconnection attempts, from watchdog."); }, 2000); } catch (error) { } } }; node.setKnxConnectionProperties = () => { // 25/08/2021 Moved out of node.initKNXConnection node.knxConnectionProperties = { ipAddr: node.host, ipPort: node.port, physAddr: node.physAddr, // the KNX physical address we'd like to use suppress_ack_ldatareq: node.suppressACKRequest, loglevel: node.loglevel, hostProtocol: node.hostProtocol, isSecureKNXEnabled: node.knxSecureSelected, secureTunnelConfig: node.knxSecureSelected ? node.secureTunnelConfig : undefined, localIPAddress: "", // Riempito da KNXEngine KNXQueueSendIntervalMilliseconds: Number(node.delaybetweentelegrams), connectionKeepAliveTimeout: 30 // Every 30 seconds, send a connectionstatus_request }; // 11/07/2022 Test if the IP is a valid one or is a DNS Name switch (net.isIP(node.host)) { case 0: // Invalid IP, resolve the DNS name. const dns = require("dns-sync"); let resolvedIP = null; try { resolvedIP = dns.resolve(node.host); } catch (error) { throw new Error("net.isIP: INVALID IP OR DNS NAME. Error checking the Gateway Host in Config node. " + error.message); } if (resolvedIP === null || net.isIP(resolvedIP) === 0) { // Error in resolving DNS Name node.sysLogger?.error( "net.isIP: INVALID IP OR DNS NAME. Check the Gateway Host in Config node " + node.name + " " + node.host, ); throw new Error("net.isIP: INVALID IP OR DNS NAME. Check the Gateway Host in Config node."); } node.sysLogger?.info( "net.isIP: The gateway is not specified as IP. The DNS resolver pointed me to the IP " + node.host + ", in Config node " + node.name, ); node.knxConnectionProperties.ipAddr = resolvedIP; case 4: // It's an IPv4 break; case 6: // It's an IPv6 break; default: break; } if (node.KNXEthInterface !== "Auto") { let sIfaceName = ""; if (node.KNXEthInterface === "Manual") { sIfaceName = node.KNXEthInterfaceManuallyInput; node.sysLogger?.info("Bind KNX Bus to interface : " + sIfaceName + " (Interface's name entered by hand). Node " + node.name); } else { sIfaceName = node.KNXEthInterface; node.sysLogger?.info( "Bind KNX Bus to interface : " + sIfaceName + " (Interface's name selected from dropdown list). Node " + node.name, ); } node.knxConnectionProperties.interface = sIfaceName; } else { // 08/10/2021 Delete the interface try { delete node.knxConnectionProperties.interface; } catch (error) { } node.sysLogger?.info("Bind KNX Bus to interface (Auto). Node " + node.name); } }; // node.setKnxConnectionProperties(); 28/12/2021 Commented node.initKNXConnection = async () => { try { node.setKnxConnectionProperties(); // 28/12/2021 Added } catch (error) { node.sysLogger?.error("setKnxConnectionProperties: " + error.message); if (node.linkStatus !== "disconnected") await node.Disconnect(); return; } // 12/08/2021 Avoid start connection if there are no knx-ultimate nodes linked to this gateway // At start, initKNXConnection is already called only if the gateway has clients, but in the successive calls from the error handler, this check is not done. if (node.nodeClients.length === 0) { try { node.sysLogger?.info("No nodes linked to this gateway " + node.name); try { if (node.linkStatus !== "disconnected") await node.Disconnect(); } catch (error) { } return; } catch (error) { } } try { // 02/01/2022 This is important to free the tunnel in case of hard disconnection. await node.Disconnect(); } catch (error) { // node.sysLogger?.info(error) } try { // Unsetting handlers if node.knxConnection was existing try { if (node.knxConnection !== null && node.knxConnection !== undefined) { await node.knxConnection.Disconnect(); node.sysLogger?.debug("removing old handlers. Node " + node.name); node.knxConnection.removeAllListeners(); } } catch (error) { node.sysLogger?.info("BANANA ERRORINO", error); } //node.knxConnectionProperties.localSocketAddress = { address: '192.168.2.2', port: 59000 } node.knxConnection = new knx.KNXClient(node.knxConnectionProperties); // Setting handlers // ###################################### node.knxConnection.on(knx.KNXClientEvents.indication, handleBusEvents); node.knxConnection.on(knx.KNXClientEvents.error, (err) => { try { node.sysLogger?.error("received KNXClientEvents.error: " + (err.message === undefined ? err : err.message)); } catch (error) { } // 31/03/2022 Don't care about some errors if (err.message !== undefined && (err.message === "ROUTING_LOST_MESSAGE" || err.message === "ROUTING_BUSY")) { node.sysLogger?.error( "KNXClientEvents.error: " + (err.message === undefined ? err : err.message) + " consider DECREASING the transmission speed, by increasing the telegram's DELAY in the gateway configuration node!", ); return; } node.Disconnect("Disconnected by error " + (err.message === undefined ? err : err.message), "red"); node.sysLogger?.error("Disconnected by: " + (err.message === undefined ? err : err.message)); }); node.knxConnection.on(knx.KNXClientEvents.disconnected, (info) => { if (node.linkStatus !== "disconnected") { node.linkStatus = "disconnected"; node.sysLogger?.warn("Disconnected event %s", info); node.Disconnect("Disconnected by event: " + info || "", "red"); // 11/03/2022 } }); node.knxConnection.on(knx.KNXClientEvents.close, (info) => { node.sysLogger?.debug("KNXClient socket closed."); node.linkStatus = "disconnected"; }); node.knxConnection.on(knx.KNXClientEvents.connected, (info) => { node.linkStatus = "connected"; // Start the timer to do initial read. if (node.timerDoInitialRead !== null) clearTimeout(node.timerDoInitialRead); node.timerDoInitialRead = setTimeout(() => { try { DoInitialReadFromKNXBusOrFile(); } catch (error) { node.sysLogger?.error("DoInitialReadFromKNXBusOrFile " + error.stack); } }, 1000); // 17/02/2020 Do initial read of all nodes requesting initial read const t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setAllClientsStatus("Connected.", "green", "On duty."); }, 500); node.sysLogger?.info("Connected to %o", info); }); node.knxConnection.on(knx.KNXClientEvents.connecting, (info) => { node.linkStatus = "connecting"; node.sysLogger?.debug("Connecting to" + info.ipAddr || ""); node.setAllClientsStatus(info.ipAddr || "", "grey", "Connecting..."); }); // ###################################### node.setAllClientsStatus("Connecting... ", "grey", ""); node.sysLogger?.info("perform websocket connection on " + node.name); try { node.sysLogger?.info("Connecting... " + node.name); node.knxConnection.Connect(); } catch (error) { node.sysLogger?.error("node.knxConnection.Connect() " + node.name + ": " + error.message); node.linkStatus = "disconnected"; throw error; } } catch (error) { if (node.sysLogger !== null) { node.sysLogger.error("Error in instantiating knxConnection " + error.stack + " Node " + node.name); node.error("KNXUltimate-config: Error in instantiating knxConnection " + error.message + " Node " + node.name); } node.linkStatus = "disconnected"; // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". const t = setTimeout(() => node.setAllClientsStatus("Error in instantiating knxConnection " + error.message, "red", "Error"), 200); } }; // Handle BUS events // --------------------------------------------------------------------------------------- function handleBusEvents(_datagram, _echoed) { // console.time('handleBusEvents'); let _rawValue = null; try { _rawValue = _datagram.cEMIMessage.npdu.dataValue; } catch (error) { return; } let _evt = null; if (_datagram.cEMIMessage.npdu.isGroupRead) _evt = "GroupValue_Read"; if (_datagram.cEMIMessage.npdu.isGroupResponse) _evt = "GroupValue_Response"; if (_datagram.cEMIMessage.npdu.isGroupWrite) _evt = "GroupValue_Write"; let _src = null; _src = _datagram.cEMIMessage.srcAddress.toString(); let _dest = null; _dest = _datagram.cEMIMessage.dstAddress.toString(); if (_evt === null || _src === null || _dest === null) { node.sysLogger?.error("HandleBusEvent: unable to parse telegram, ignored"); return }; _echoed = _echoed || false; const isRepeated = _datagram.cEMIMessage.control.repeat !== 1; // 06/06/2021 Supergiovane: check if i can handle the telegrams with "Repeated" flag if (node.ignoreTelegramsWithRepeatedFlag === true && isRepeated) { node.sysLogger?.warn("Ignored telegram with Repeated Flag " + _evt + " Src:" + _src + " Dest:" + _dest); return; } // 23/03/2021 Supergiovane: Added the CEMI telegram for ETS Diagnostic // ##################################################################### let _cemiETS = ""; if (_echoed) { // I'm sending a telegram to the BUS in Tunneling mode, with echo enabled. // Tunnel: TX to BUS: OK try { const sCemiFromDatagram = _datagram.cEMIMessage.toBuffer().toString("hex"); _cemiETS = "2900BCD0" + sCemiFromDatagram.substr(8); } catch (error) { _cemiETS = ""; } } else { try { // Multicast: RX from BUS: OK // Multicast TX to BUS: OK // Tunnel: RX from BUS: OK // Tunnel: TX to BUS: see the _echoed above _cemiETS = _datagram.cEMIMessage.toBuffer().toString("hex"); } catch (error) { _cemiETS = ""; } } // ##################################################################### // 04/04/2021 Supergiovane: save value to node.exposedGAs if (typeof _dest === "string" && _rawValue !== undefined && (_evt === "GroupValue_Write" || _evt === "GroupValue_Response")) { try { const ret = { ga: _dest, rawValue: _rawValue, dpt: undefined, devicename: undefined }; node.exposedGAs = node.exposedGAs.filter((item) => item.ga !== _dest); // Remove previous if (node.csv !== undefined && node.csv !== '' && node.csv.length !== 0) { // Add the dpt const found = node.csv.find(a => a.ga === _dest); if (found !== undefined) { ret.dpt = found.dpt; ret.devicename = found.devicename; } } node.exposedGAs.push(ret); // add the new } catch (error) { } } switch (_evt) { case "GroupValue_Write": // console.time('GroupValue_Write'); // 05/04/2022 Fatto test velocità tra for..loop e forEach. E' risultato sempre comunque più veloce il forEach! node.nodeClients .filter((_input) => _input.notifywrite === true) .forEach((_input) => { // 21/10/2024 check wether is a HUE device if (_input.type.includes('knxUltimateHue')) { const msg = { knx: { event: _evt, destination: _dest, rawValue: _rawValue, } }; _input.handleSend(msg); } else if (_input.hasOwnProperty("isSceneController")) {// 19/03/2020 in the middle of coronavirus. Whole italy is red zone, closed down. Scene Controller implementation // 12/08/2020 Check wether is a learn (save) command or a activate (play) command. if (_dest === _input.topic || _dest === _input.topicSave) { // Prepare the two messages to be evaluated directly into the Scene Controller node. new Promise((resolve) => { if (_dest === _input.topic) { try { const msgRecall = buildInputMessage({ _srcGA: _src, _destGA: _dest, _event: _evt, _Rawvalue: _rawValue, _inputDpt: _input.dpt, _devicename: _input.name ? _input.name : "", _outputtopic: _input.outputtopic, _oNode: null, _echoed: _echoed }); _input.RecallScene(msgRecall.payload, false); } catch (error) { } } // 12/08/2020 Do NOT use "else", because both topics must be evaluated in case both recall and save have same group address. if (_dest === _input.topicSave) { try { const msgSave = buildInputMessage({ _srcGA: _src, _destGA: _dest, _event: _evt, _Rawvalue: _rawValue, _inputDpt: _input.dptSave, _devicename: _input.name || "", _outputtopic: _dest, _oNode: null, _echoed: _echoed }); _input.SaveScene(msgSave.payload, false); } catch (error) { } } resolve(true); // fulfilled // reject("error"); // rejected }) .then(function () { }) .catch(function () { }); } else { // 19/03/2020 Check and Update value if the input is part of a scene controller new Promise((resolve) => { // Check and update the values of each device in the scene and update the rule array accordingly. for (let i = 0; i < _input.rules.length; i++) { // rule is { topic: rowRuleTopic, devicename: rowRuleDeviceName, dpt:rowRuleDPT, send: rowRuleSend} const oDevice = _input.rules[i]; if (typeof oDevice !== "undefined" && oDevice.topic == _dest) { const msg = buildInputMessage({ _srcGA: _src, _destGA: _dest, _event: _evt, _Rawvalue: _rawValue, _inputDpt: oDevice.dpt, _devicename: oDevice.name || "", _outputtopic: oDevice.outputtopic, _oNode: null, _echoed: _echoed }); oDevice.currentPayload = msg.payload; _input.setNodeStatus({ fill: "grey", shape: "dot", text: "Update dev in scene", payload: oDevice.currentPayload, GA: oDevice.topic, dpt: oDevice.dpt, devicename: oDevice.devicename || "", }); break; } } resolve(true); // fulfilled // reject("error"); // rejected }) .then(function () { }) .catch(function () { }); } } else if (_input.hasOwnProperty("isLogger")) { // 26/03/2020 Coronavirus is slightly decreasing the affected numer of people. Logger Node // 24/03/2021 Logger Node, i'll pass cemiETS if (_cemiETS !== undefined) { // new Promise((resolve, reject) => { _input.handleSend(_cemiETS); // resolve(true); // fulfilled // reject("error"); // rejected // }).then(function () { }).catch(function () { }); } } else if (_input.listenallga === true) { // 25/10/2019 TRY TO AUTO DECODE IF Group address not found in the CSV const msg = buildInputMessage({ _srcGA: _src, _destGA: _dest, _event: _evt, _Rawvalue: _rawValue, _outputtopic: _dest, _oNode: _input, _echoed: _echoed }); _input.setNodeStatus({ fill: "green", shape: "dot", text: "", payload: msg.payload, GA: msg.knx.destination, dpt: msg.knx.dpt, devicename: msg.devicename, }); _input.handleSend(msg); } else if (_input.topic == _dest) { if (_input.hasOwnProperty("isWatchDog")) { // 04/02/2020 Watchdog implementation // Is a watchdog node } else { const msg = buildInputMessage({ _srcGA: _src, _destGA: _dest, _event: _evt, _Rawvalue: _rawValue, _inputDpt: _input.dpt, _devicename: _input.name ? _input.name : "", _outputtopic: _input.outputtopic, _oNode: _input, _echoed: _echoed }); // Check RBE INPUT from KNX Bus, to avoid send the payload to the flow, if it's equal to the current payload if (!checkRBEInputFromKNXBusAllowSend(_input, msg.payload)) { _input.setNodeStatus({ fill: "grey", shape: "ring", text: "rbe block (" + msg.payload + ") from KNX", payload: "", GA: "", dpt: "", devicename: "", }); return; } msg.previouspayload = typeof _input.currentPayload !== "undefined" ? _input.currentPayload : ""; // 24/01/2020 Added previous payload _input.currentPayload = msg.payload; // Set the current value for the RBE input _input.setNodeStatus({ fill: "green", shape: "dot", text: "", payload: msg.payload, GA: _input.topic, dpt: _input.dpt, devicename: "", }); _input.handleSend(msg); } } }); // console.timeEnd('GroupValue_Write'); break; case "GroupValue_Response": node.nodeClients .filter((_input) => _input.notifyresponse === true) .forEach((_input) => { if (_input.hasOwnProperty("isLogger")) { // 26/03/2020 Coronavirus is slightly decreasing the affected numer of people. Logger Node // 24/03/2021 Logger Node, i'll pass cemiETS if (_cemiETS !== undefined) { // new Promise((resolve, reject) => { _input.handleSend(_cemiETS); // resolve(true); // fulfilled // reject("error"); // rejected // }).then(function () { }).catch(function () { }); } } else if (_input.listenallga === true) { const msg = buildInputMessage({ _srcGA: _src, _destGA: _dest, _event: _evt, _Rawvalue: _rawValue, _outputtopic: _dest, _oNode: _input, _echoed: _echoed }); _input.setNodeStatus({ fill: "blue", shape: "dot", text: "", payload: msg.payload, GA: msg.knx.destination, dpt: msg.knx.dpt, devicename: msg.devicename, }); _input.handleSend(msg); } else if (_input.topic === _dest) { // 04/02/2020 Watchdog implementation if (_input.hasOwnProperty("isWatchDog")) { // Is a watchdog node _input.watchDogTimerReset(); } else { const msg = buildInputMessage({ _srcGA: _src, _destGA: _dest, _event: _evt, _Rawvalue: _rawValue, _inputDpt: _input.dpt, _devicename: _input.name ? _input.name : "", _outputtopic: _input.outputtopic, _oNode: _input, _echoed: _echoed }); // Check RBE INPUT from KNX Bus, to avoid send the payload to the flow, if it's equal to the current payload if (!checkRBEInputFromKNXBusAllowSend(_input, msg.payload)) { _input.setNodeStatus({ fill: "grey", shape: "ring", text: "rbe INPUT filter applied on " + msg.payload, payload: msg.payload, GA: _dest, }); return; } msg.previouspayload = typeof _input.currentPayload !== "undefined" ? _input.currentPayload : ""; // 24/01/2020 Added previous payload _input.currentPayload = msg.payload; // Set the current value for the RBE input _input.setNodeStatus({ fill: "blue", shape: "dot", text: "", payload: msg.payload, GA: _input.topic, dpt: msg.knx.dpt, devicename: msg.devicename, }); _input.handleSend(msg); } } }); break; case "GroupValue_Read": node.nodeClients .filter((_input) => _input.notifyreadrequest === true) .forEach((_input) => { if (_input.hasOwnProperty("isLogger")) { // 26/03/2020 Coronavirus is slightly decreasing the affected numer of people. Logger Node // node.sysLogger?.info("BANANA isLogger", _evt, _src, _dest, _rawValue, _cemiETS); // 24/03/2021 Logger Node, i'll pass cemiETS if (_cemiETS !== undefined) { // new Promise((resolve, reject) => { _input.handleSend(_cemiETS); // resolve(true); // fulfilled // reject("error"); // rejected // }).then(function () { }).catch(function () { }); } } else if (_input.listenallga === true) { // Read Request const msg = buildInputMessage({ _srcGA: _src, _destGA: _dest, _event: _evt, _Rawvalue: null, _outputtopic: _dest, _oNode: _input, _echoed: _echoed