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,087 lines (1,015 loc) 101 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 os = require("os"); 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); }; // #################### const BIT_COUNT_TABLE = Array.from({ length: 256 }, (_, value) => { let count = 0; let temp = value; while (temp) { temp &= temp - 1; count++; } return count; }); const parseIPv4Address = (str) => { if (typeof str !== "string") return null; const parts = str.trim().split('.'); if (parts.length !== 4) return null; const octets = []; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (!/^\d+$/.test(part)) return null; const value = Number(part); if (!Number.isInteger(value) || value < 0 || value > 255) return null; octets.push(value); } return octets; }; const countNetmaskBits = (maskOctets) => { if (!Array.isArray(maskOctets) || maskOctets.length !== 4) return 0; let total = 0; for (let i = 0; i < 4; i++) { const oct = maskOctets[i]; if (!Number.isInteger(oct) || oct < 0 || oct > 255) return 0; total += BIT_COUNT_TABLE[oct]; } return total; }; const computeIPv4NetworkKey = (ipOctets, maskOctets) => { if (!Array.isArray(ipOctets) || ipOctets.length !== 4 || !Array.isArray(maskOctets) || maskOctets.length !== 4) return null; const result = []; for (let i = 0; i < 4; i++) { const ipVal = ipOctets[i]; const maskVal = maskOctets[i]; if (!Number.isInteger(ipVal) || ipVal < 0 || ipVal > 255 || !Number.isInteger(maskVal) || maskVal < 0 || maskVal > 255) return null; result.push(ipVal & maskVal); } return result.join('.'); }; const buildNetmaskOctetsFromPrefix = (prefix) => { if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return null; const octets = [0, 0, 0, 0]; let remaining = prefix; for (let i = 0; i < 4; i++) { const bits = Math.max(0, Math.min(remaining, 8)); octets[i] = bits === 0 ? 0 : ((0xff << (8 - bits)) & 0xff); remaining -= bits; } return octets; }; const deriveNetmaskOctets = (iface) => { if (!iface) return null; const netmask = typeof iface.netmask === "string" && iface.netmask.trim() !== "" ? iface.netmask : null; if (netmask) { const octets = parseIPv4Address(netmask); if (octets) return octets; } if (typeof iface.cidr === "string" && iface.cidr.includes('/')) { const parts = iface.cidr.split('/'); if (parts.length === 2) { const prefix = Number(parts[1]); const octets = buildNetmaskOctetsFromPrefix(prefix); if (octets) return octets; } } return null; }; const isMulticastIPv4 = (octets) => { if (!Array.isArray(octets) || octets.length !== 4) return false; const first = octets[0]; return first >= 224 && first <= 239; }; const findAutoEthernetInterface = (targetIP) => { const targetOctets = parseIPv4Address(targetIP); if (!targetOctets || isMulticastIPv4(targetOctets)) return null; const interfaces = os.networkInterfaces(); if (!interfaces || typeof interfaces !== "object") return null; let bestMatch = null; let bestMaskBits = -1; Object.keys(interfaces).forEach((ifname) => { const entries = Array.isArray(interfaces[ifname]) ? interfaces[ifname] : []; entries.forEach((entry) => { if (!entry || entry.internal) return; const family = entry.family === 'IPv4' || entry.family === 4; if (!family) return; const ifaceOctets = parseIPv4Address(entry.address); if (!ifaceOctets) return; const maskOctets = deriveNetmaskOctets(entry); if (!maskOctets) return; const ifaceNetwork = computeIPv4NetworkKey(ifaceOctets, maskOctets); const targetNetwork = computeIPv4NetworkKey(targetOctets, maskOctets); if (!ifaceNetwork || !targetNetwork || ifaceNetwork !== targetNetwork) return; const maskBits = countNetmaskBits(maskOctets); if (maskBits > bestMaskBits) { bestMaskBits = maskBits; bestMatch = { name: ifname, address: entry.address, netmask: maskOctets.join('.'), maskBits, }; } }); }); return bestMatch; }; 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; const throttleSecondsRaw = Number(config.statusUpdateThrottle); node.statusUpdateThrottleMs = Number.isFinite(throttleSecondsRaw) && throttleSecondsRaw > 0 ? throttleSecondsRaw * 1000 : 0; node.applyStatusUpdate = (targetNode, status) => { try { if (!targetNode || typeof targetNode.status !== 'function') return; const throttle = node.statusUpdateThrottleMs; if (!throttle) { targetNode.status(status); return; } if (!targetNode.__knxStatusThrottle) { targetNode.__knxStatusThrottle = { pending: undefined, timer: null }; } const tracker = targetNode.__knxStatusThrottle; tracker.pending = status; if (tracker.timer) return; tracker.timer = setTimeout(() => { try { if (tracker.pending !== undefined) { targetNode.status(tracker.pending); } } catch (timerError) { node.sysLogger?.warn('Unable to apply throttled status: ' + timerError.message); } finally { tracker.pending = undefined; tracker.timer = null; } }, throttle); } catch (error) { node.sysLogger?.warn('applyStatusUpdate error: ' + error.message); } }; // 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 === null ? "" : String(config.tunnelIA); node.tunnelInterfaceIndividualAddress = typeof config.tunnelInterfaceIndividualAddress === "undefined" || config.tunnelInterfaceIndividualAddress === null ? "" : String(config.tunnelInterfaceIndividualAddress); const normalizedTunnelIA = (value) => { if (typeof value !== "string") return ""; const trimmed = value.trim(); return trimmed === "undefined" ? "" : trimmed; }; node.tunnelIA = normalizedTunnelIA(node.tunnelIA); node.tunnelInterfaceIndividualAddress = normalizedTunnelIA(node.tunnelInterfaceIndividualAddress); if (!node.tunnelInterfaceIndividualAddress && node.tunnelIA) { node.tunnelInterfaceIndividualAddress = node.tunnelIA; } else if (!node.tunnelIA && node.tunnelInterfaceIndividualAddress) { node.tunnelIA = node.tunnelInterfaceIndividualAddress; } node.tunnelUserPassword = typeof config.tunnelUserPassword === "undefined" ? "" : config.tunnelUserPassword; node.tunnelUserId = typeof config.tunnelUserId === "undefined" ? "" : config.tunnelUserId; 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 { const isSecure = node.knxSecureSelected === true || node.knxSecureSelected === "true"; node.hostProtocol = isSecure ? "TunnelTCP" : "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"; const useManual = secureMode === "manual" || secureMode === "combined"; const useKeyring = secureMode === "keyring" || secureMode === "combined"; const secureConfig = {}; const modeParts = []; if (useManual) { const manualIA = (node.tunnelInterfaceIndividualAddress || "").trim(); const manualUserId = (node.tunnelUserId || "").trim(); const manualPwd = node.tunnelUserPassword || ""; if (manualIA) { secureConfig.tunnelInterfaceIndividualAddress = manualIA; } if (manualUserId) { secureConfig.tunnelUserId = manualUserId; } // Always include password property so KNX library receives the intended value (even if empty) secureConfig.tunnelUserPassword = manualPwd; modeParts.push("manual tunnel credentials"); } if (useKeyring) { secureConfig.knxkeys_file_path = node.keyringFileXML || ""; secureConfig.knxkeys_password = node.credentials?.keyringFilePassword || ""; try { const manualSelectionIA = normalizedTunnelIA(node.tunnelIA); if (node.tunnelIASelection === "Manual" && manualSelectionIA) { if (!secureConfig.tunnelInterfaceIndividualAddress) { secureConfig.tunnelInterfaceIndividualAddress = manualSelectionIA; } } else if (!secureConfig.tunnelInterfaceIndividualAddress) { secureConfig.tunnelInterfaceIndividualAddress = ""; // Auto (let KNX stack select) } } 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); if (node.loglevel === "debug") { try { const toIAString = (value) => { if (!value) return ""; return typeof value.toString === "function" ? value.toString() : String(value); }; const toBufferString = (value) => { if (!value) return ""; if (Buffer.isBuffer(value)) return value.toString("hex"); return String(value); }; const interfaceMap = kr.getInterfaces?.(); const interfaces = Array.from(interfaceMap ? interfaceMap.values() : []).map((iface) => ({ type: iface.type || "", individualAddress: toIAString(iface.individualAddress), host: toIAString(iface.host), userId: typeof iface.userId === "number" ? iface.userId : "", password: iface.password || "", decryptedPassword: iface.decryptedPassword || "", authentication: iface.authentication || "", decryptedAuthentication: iface.decryptedAuthentication || "", groupAddresses: Array.from(iface.groupAddresses ? iface.groupAddresses.entries() : []).map(([ga, senders]) => ({ address: ga, senders: Array.isArray(senders) ? senders.map(toIAString) : [], })), })); const backbones = (kr.getBackbones?.() || []).map((backbone) => ({ multicastAddress: backbone.multicastAddress || "", latency: typeof backbone.latency === "number" ? backbone.latency : "", key: backbone.key || "", decryptedKey: toBufferString(backbone.decryptedKey), })); const groupAddressMap = kr.getGroupAddresses?.(); const groupAddresses = Array.from(groupAddressMap ? groupAddressMap.values() : []).map((group) => ({ address: toIAString(group.address), key: group.key || "", decryptedKey: toBufferString(group.decryptedKey), })); const deviceMap = kr.getDevices?.(); const devices = Array.from(deviceMap ? deviceMap.values() : []).map((device) => ({ individualAddress: toIAString(device.individualAddress), toolKey: device.toolKey || "", decryptedToolKey: toBufferString(device.decryptedToolKey), managementPassword: device.managementPassword || "", decryptedManagementPassword: device.decryptedManagementPassword || "", authentication: device.authentication || "", decryptedAuthentication: device.decryptedAuthentication || "", sequenceNumber: typeof device.sequenceNumber === "number" ? device.sequenceNumber : "", serialNumber: device.serialNumber || "", })); const lines = []; lines.push("================ KNX Secure keyring debug dump ================"); lines.push(`Node: ${node.name || node.id || ""}`); lines.push(`Created By: ${kr.getCreatedBy?.() || ""}`); lines.push(`Created On: ${kr.getCreated?.() || ""}`); lines.push(`Password (node credentials): ${node.credentials?.keyringFilePassword || ""}`); lines.push(""); lines.push("Interfaces:"); if (interfaces.length === 0) { lines.push(" (none)"); } else { interfaces.forEach((iface, idx) => { lines.push(` [${idx + 1}] ${iface.individualAddress || "(unknown)"} (${iface.type || ""})`); lines.push(` Host: ${iface.host || ""}`); lines.push(` User ID: ${iface.userId === "" ? "" : iface.userId}`); lines.push(` Password (encoded): ${iface.password || ""}`); lines.push(` Password (decoded): ${iface.decryptedPassword || ""}`); lines.push(` Authentication (encoded): ${iface.authentication || ""}`); lines.push(` Authentication (decoded): ${iface.decryptedAuthentication || ""}`); if (!iface.groupAddresses || iface.groupAddresses.length === 0) { lines.push(" Group Addresses: (none)"); } else { lines.push(" Group Addresses:"); iface.groupAddresses.forEach((ga) => { const senders = ga.senders && ga.senders.length > 0 ? ga.senders.join(", ") : "(none)"; lines.push(` - ${ga.address}: senders ${senders}`); }); } lines.push(""); }); } lines.push("Backbones:"); if (backbones.length === 0) { lines.push(" (none)"); } else { backbones.forEach((backbone, idx) => { lines.push(` [${idx + 1}] Multicast: ${backbone.multicastAddress || ""}`); lines.push(` Latency: ${backbone.latency === "" ? "" : backbone.latency}`); lines.push(` Key (encoded): ${backbone.key || ""}`); lines.push(` Key (decoded hex): ${backbone.decryptedKey || ""}`); lines.push(""); }); } lines.push("Group Addresses:"); if (groupAddresses.length === 0) { lines.push(" (none)"); } else { groupAddresses.forEach((group, idx) => { lines.push(` [${idx + 1}] ${group.address || ""}`); lines.push(` Key (encoded): ${group.key || ""}`); lines.push(` Key (decoded hex): ${group.decryptedKey || ""}`); lines.push(""); }); } lines.push("Devices:"); if (devices.length === 0) { lines.push(" (none)"); } else { devices.forEach((device, idx) => { lines.push(` [${idx + 1}] ${device.individualAddress || ""}`); lines.push(` Tool Key (encoded): ${device.toolKey || ""}`); lines.push(` Tool Key (decoded hex): ${device.decryptedToolKey || ""}`); lines.push(` Management Password (encoded): ${device.managementPassword || ""}`); lines.push(` Management Password (decoded): ${device.decryptedManagementPassword || ""}`); lines.push(` Authentication (encoded): ${device.authentication || ""}`); lines.push(` Authentication (decoded): ${device.decryptedAuthentication || ""}`); lines.push(` Sequence Number: ${device.sequenceNumber === "" ? "" : device.sequenceNumber}`); lines.push(` Serial Number: ${device.serialNumber || ""}`); lines.push(""); }); } lines.push("Raw keyring (XML/base64 as provided):"); lines.push(node.keyringFileXML || "(empty)"); lines.push("================ End of keyring debug dump ================"); node.sysLogger?.debug(lines.join("\n")); } catch (dumpError) { node.sysLogger?.error("KNX Secure: unable to log keyring details: " + dumpError.message); } } 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 } } modeParts.push("keyring file/password"); } if (Object.keys(secureConfig).length > 0) { node.secureTunnelConfig = secureConfig; } else { node.secureTunnelConfig = undefined; } if (modeParts.length > 0) { RED.log.info(`KNX-Secure: secure mode selected (${modeParts.join(" + ")}). Node ${node.name || node.id}`); } } 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 { // Remove any manual binding and try to auto-select based on subnet try { delete node.knxConnectionProperties.interface; } catch (error) { } const targetIP = node.knxConnectionProperties?.ipAddr || node.host; const autoInterface = typeof targetIP === "string" ? findAutoEthernetInterface(targetIP) : null; if (autoInterface && autoInterface.name) { node.knxConnectionProperties.interface = autoInterface.name; const maskInfo = autoInterface.netmask ? autoInterface.netmask + (autoInterface.maskBits ? " (" + autoInterface.maskBits + ")" : "") : (autoInterface.maskBits ? String(autoInterface.maskBits) : "mask unknown"); node.sysLogger?.info( "Bind KNX Bus to interface (Auto) -> " + autoInterface.name + " (" + autoInterface.address + " " + maskInfo + ")" + ". Node " + node.name, ); } else { node.sysLogger?.info( "Bind KNX Bus to interface (Auto). Node " + node.name + (targetIP ? " - no matching local interface found for " + targetIP + "." : "."), ); } } }; // 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 "l