UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX intallation via Node-Red! Single Node KNX IN/OUT with optional ETS group address importer. Easy to use and highly configurable.

841 lines (748 loc) 113 kB
// const knx = require('./../knxultimate-api2'); // const dptlib = require('./../knxultimate-api2/src/dptlib'); const knx = require("./../KNXEngine"); const dptlib = require('./../KNXEngine/dptlib'); const oOS = require('os'); const net = require("net"); const _ = require("lodash"); const path = require("path"); var fs = require('fs'); const { Server } = require("http"); //Helpers let sortBy = (field) => (a, b) => { if (a[field] > b[field]) { return 1 } else { return -1 } }; let onlyDptKeys = (kv) => { return kv[0].startsWith("DPT") }; let extractBaseNo = (kv) => { return { subtypes: kv[1].subtypes, base: parseInt(kv[1].id.replace("DPT", "")) } }; let convertSubtype = (baseType) => (kv) => { let value = `${baseType.base}.${kv[0]}`; //let sRet = value + " " + kv[1].name + (kv[1].unit === undefined ? "" : " (" + kv[1].unit + ")"); let sRet = value + " " + kv[1].name; return { value: value , text: sRet } } // 06/02/2020 To be tested // convertSubtype = (baseType) => (kv) => { // let value = `${baseType.base}.${kv[0]}` // return { // value: value // , text: value + ` (${kv[1].name}${kv[1].unit !== undefined?" - " + kv[1].unit:""})` // } // } let toConcattedSubtypes = (acc, baseType) => { let subtypes = Object.entries(baseType.subtypes) .sort(sortBy(0)) .map(convertSubtype(baseType)) return acc.concat(subtypes) }; module.exports = (RED) => { "use strict"; RED.httpAdmin.get("/knxUltimateDpts", RED.auth.needsPermission('knxUltimate-config.read'), function (req, res) { const dpts = Object.entries(dptlib) .filter(onlyDptKeys) .map(extractBaseNo) .sort(sortBy("base")) .reduce(toConcattedSubtypes, []) res.json(dpts) // Utilità per visualizzare i datapoints, da copiare in README // var stringa = ""; // for (let index = 0; index < dpts.length; index++) { // const element = dpts[index]; // stringa += element.text + "<br/>\n"; // } // if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn(stringa) }); // 15/09/2020 Supergiovane, read datapoint help usage RED.httpAdmin.get("/knxUltimateDptsGetHelp", RED.auth.needsPermission('knxUltimate-config.read'), function (req, res) { var sDPT = req.query.dpt.split(".")[0]; // Takes only the main type var jRet; if (sDPT == "0") { // Special fake datapoint, meaning "Universal Mode" jRet = { "help": `// KNX-Ultimate set as UNIVERSAL NODE // Example of a function that sends a message to the KNX-Ultimate msg.destination = "0/0/1"; // Set the destination msg.payload = false; // issues a write or response (based on the options Output Type above) to the KNX bus msg.event = "GroupValue_Write"; // "GroupValue_Write" or "GroupValue_Response", overrides the option Output Type above. msg.dpt = "1.001"; // for example "1.001", overrides the Datapoint option. (Datapoints can be sent as 9 , "9" , "9.001" or "DPT9.001") return msg;`, "helplink": "https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki" }; res.json(jRet); return; } jRet = { "help": "NO", "helplink": "https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/-SamplesHome" }; const dpts = Object.entries(dptlib) .filter(onlyDptKeys) for (let index = 0; index < dpts.length; index++) { if (dpts[index][0].toUpperCase() === "DPT" + sDPT) { jRet = { "help": (dpts[index][1].basetype.hasOwnProperty("help") ? dpts[index][1].basetype.help : "NO"), "helplink": (dpts[index][1].basetype.hasOwnProperty("helplink") ? dpts[index][1].basetype.helplink : "https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/-SamplesHome") }; break; } } res.json(jRet); }); function knxUltimateConfigNode(config) { RED.nodes.createNode(this, config) var node = this; node.host = config.host node.port = parseInt(config.port); node.physAddr = config.physAddr || "15.15.22"; // 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.statusDisplayLastUpdate = typeof config.statusDisplayLastUpdate === "undefined" ? true : config.statusDisplayLastUpdate; node.statusDisplayDeviceNameWhenALL = typeof config.statusDisplayDeviceNameWhenALL === "undefined" ? false : config.statusDisplayDeviceNameWhenALL; node.statusDisplayDataPoint = typeof config.statusDisplayDataPoint === "undefined" ? false : config.statusDisplayDataPoint; node.telegramsQueue = []; // 02/01/2020 Queue containing telegrams node.timerSendTelegramFromQueue = null; node.delaybetweentelegramsfurtherdelayREAD = (typeof config.delaybetweentelegramsfurtherdelayREAD === "undefined" || Number(config.delaybetweentelegramsfurtherdelayREAD < 1)) ? 1 : Number(config.delaybetweentelegramsfurtherdelayREAD); // 18/05/2020 delay multiplicator only for "read" telegrams. node.delaybetweentelegramsREADCount = 0;// 18/05/2020 delay multiplicator only for "read" telegrams. 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.csv = readCSV(config.csv); // Array from ETS CSV Group Addresses {ga:group address, dpt: datapoint, devicename: full device name with main and subgroups} node.localEchoInTunneling = typeof config.localEchoInTunneling !== "undefined" ? config.localEchoInTunneling : true; 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 node.sysLogger = null; // 20/03/2022 Default try { node.sysLogger = require("./utils/sysLogger.js").get({ loglevel: node.loglevel }); // 08/04/2021 new logger to adhere to the loglevel selected in the config-window } catch (error) { } // 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.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.lockHandleTelegramQueue = false; // 12/11/2021 Lock sending telegrams if node disconnected or if already handling the queue node.knxConnectionProperties = null; // Retains the connection properties node.allowLauch_initKNXConnection = true; // See the node.timerKNXUltimateCheckState function node.timerClearTelegramQueue = null; // Timer to clear the telegram's queue after long disconnection node.hostProtocol = "TunnelUDP"; // 20/03/2022 Default node.knxConnection = null; // 20/03/2022 Default // 15/12/2021 // 05/12/2021 Set the protocol (this is undefined if coming from ild versions if (config.hostProtocol === undefined) { // 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 { // 11/07/2022 node.hostProtocol = "TunnelUDP"; } if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("IP Protocol auto adapded to " + node.hostProtocol + ", based on IP " + node.host); } else { node.hostProtocol = config.hostProtocol; } node.setAllClientsStatus = (_status, _color, _text) => { function nextStatus(_oClient) { let oClient = RED.nodes.getNode(_oClient.id); oClient.setNodeStatus({ fill: _color, shape: "dot", text: _status + " " + _text, payload: "", GA: oClient.topic, dpt: "", devicename: "" }); oClient = null; } node.nodeClients.map(nextStatus); } // 21/01/2022 TTL Timer for clearung the node.telegramsQueue if the connection stays down for long time node.startTimerClearTelegramQueue = () => { if (node.timerClearTelegramQueue === null) { node.timerClearTelegramQueue = setTimeout(() => { let t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setAllClientsStatus("Queue", "grey", "Deleted TX"); }, 200); node.telegramsQueue = []; node.timerClearTelegramQueue = null; }, 30000); } } // // KNX-SECURE // 15/11/2021 Function to load the keyring file exported from ETS // // node.jKNXSecureKeyring = null; try { (async () => { if (node.knxSecureSelected) { node.jKNXSecureKeyring = await knx.KNXSecureKeyring.keyring.load(node.keyringFileXML, node.credentials.keyringFilePassword); RED.log.info("KNX-Secure: Keyring for ETS proj " + node.jKNXSecureKeyring.ETSProjectName + ", created by " + node.jKNXSecureKeyring.ETSCreatedBy + " on " + node.jKNXSecureKeyring.ETSCreated + " succesfully validated with provided password, using node " + node.name || node.id); } else { RED.log.info("KNX-Unsecure: connection to insecure interface/router using node " + node.name || node.id); } })(); } catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("KNXUltimate-config: KNX Secure: error parsing the keyring XML: " + error.message); node.jKNXSecureKeyring = null; node.knxSecureSelected = false; let t = setTimeout(() => node.setAllClientsStatus("Error", "red", "KNX Secure " + error.message), 2000); // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". } // 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)) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('KNXUltimate-config: Unable to set up MAIN directory: ' + node.userDir); } if (!setupDirectory(path.join(node.userDir, "knxpersistvalues"))) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('KNXUltimate-config: Unable to set up cache directory: ' + path.join(node.userDir, "knxpersistvalues")); } else { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info('KNXUltimate-config: payload cache set to ' + path.join(node.userDir, "knxpersistvalues")); } function saveExposedGAs() { let sFile = path.join(node.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json"); try { if (node.exposedGAs.length > 0) { fs.writeFileSync(sFile, JSON.stringify(node.exposedGAs)); if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info('KNXUltimate-config: wrote peristent values to the file ' + sFile); } } catch (err) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('KNXUltimate-config: unable to write peristent values to the file ' + sFile + " " + err.message); } } function loadExposedGAs() { let sFile = path.join(node.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json"); try { node.exposedGAs = JSON.parse(fs.readFileSync(sFile, 'utf8')); } catch (err) { node.exposedGAs = []; if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn('KNXUltimate-config: unable to read peristent file ' + sFile + " " + err.message); } } // ************************ // Endpoint for reading csv from the other nodes RED.httpAdmin.get("/knxUltimatecsv", RED.auth.needsPermission('knxUltimate-config.read'), function (req, res) { if (typeof req.query.nodeID !== "undefined" && req.query.nodeID !== null && req.query.nodeID !== "") { var _node = RED.nodes.getNode(req.query.nodeID);// Retrieve node.id of the config node. if (_node !== null) res.json(RED.nodes.getNode(_node.id).csv); } else { // Get the first knxultimate-config having a valid csv try { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("KNXUltimate-config: Requested csv maybe from visu-ultimate?"); RED.nodes.eachNode(function (_node) { if (_node.hasOwnProperty("csv") && _node.type == "knxUltimate-config" && _node.csv !== "") { res.json(RED.nodes.getNode(_node.id).csv); return; } }); } catch (error) { } } }); // 14/08/2019 Endpoint for retrieving the ethernet interfaces RED.httpAdmin.get("/knxUltimateETHInterfaces", RED.auth.needsPermission('knxUltimate-config.read'), function (req, res) { var oiFaces = oOS.networkInterfaces(); var jListInterfaces = []; try { Object.keys(oiFaces).forEach(ifname => { // Interface with single IP if (Object.keys(oiFaces[ifname]).length === 1) { if (Object.keys(oiFaces[ifname])[0].internal === false) jListInterfaces.push({ name: ifname, address: Object.keys(oiFaces[ifname])[0].address }); } else { var sAddresses = ""; oiFaces[ifname].forEach(function (iface) { if (iface.internal === false) sAddresses += "+" + iface.address; }); if (sAddresses !== "") jListInterfaces.push({ name: ifname, address: sAddresses }); } }) } catch (error) { } res.json(jListInterfaces) }); // 14/02/2020 Endpoint for retrieving all nodes in all flows RED.httpAdmin.get("/nodeList", RED.auth.needsPermission('knxUltimate-config.read'), function (req, res) { var sNodeID = req.query.nodeID; // Retrieve node.id of the config node. var _node = RED.nodes.getNode(sNodeID); if (_node === null) { // 27/09/2020 Something wrong return; } var sNodes = "\"Group Address\"\t\"Datapoint\"\t\"Node ID\"\t\"Device Name\"\t\"Options\"\n"; // Contains the text with nodes var sGA = ""; var sDPT = ""; var sName = ""; let sOptions = ""; try { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("KNXUltimate-config: Total knx-ultimate nodes: " + _node.nodeClients.length || 0); _node.nodeClients //.map( a => a.topic.indexOf("/") !== -1 ? a.topic.split('/').map( n => +n+100000 ).join('/'):0 ).sort().map( a => a.topic.indexOf("/") !== -1 ? a.topic.split('/').map( n => +n-100000 ).join('/'):0 ) .sort((a, b) => { if (a.topic !== undefined && b.topic !== undefined) { if (a.topic.indexOf("/") === -1) return -1; if (b.topic.indexOf("/") === -1) return -1; var date1 = a.topic.split("/"); var date2 = b.topic.split("/"); date1 = date1[0].padStart(2, "0") + date1[1].padStart(2, "0") + date1[2].padStart(2, "0"); date2 = date2[0].padStart(2, "0") + date2[1].padStart(2, "0") + date2[2].padStart(2, "0"); return date1.localeCompare(date2); } else { return -1; } }) .forEach(_input => { let input = RED.nodes.getNode(_input.id); sNodeID = "\"" + input.id + "\""; sName = "\"" + (input.name !== undefined ? input.name : "") + "\""; sOptions = "\"" + "\""; if (input.listenallga === true) { if (input.hasOwnProperty("isSceneController")) { // Is a Scene Controller sGA = "\"Scene Controller\""; sDPT = "\"Any\""; } else if (input.hasOwnProperty("isLogger")) { // Is a Scene Controller sGA = "\"Logger\""; sDPT = "\"Any\""; } else if (input.hasOwnProperty("isalertnode")) { // Is a Scene Controller sGA = "\"Alerter\""; sDPT = "\"Any\""; } else if (input.hasOwnProperty("isLoadControlNode")) { // Is a Load Controller sGA = "\"LoadControl\""; sDPT = "\"Any\""; } else { // Is a ListenallGA sGA = "\"Universal Node\""; sDPT = "\"Any\""; sOptions = "\"" + "No Initial Read" + ", "; sOptions += (input.notifywrite === true ? "React to Write" : "No React to Write") + ", "; sOptions += (input.notifyresponse === true ? "React to Response" : "No React to Response") + ", "; sOptions += (input.notifyreadrequest === true ? "React to Read" : "No React to Read") + ", "; sOptions += "No Autorespond to Read Requests" + ", "; sOptions += "Output type " + input.outputtype + ", "; sOptions += "No RBE on Output to Bus" + ", "; sOptions += "No RBE on Input from Bus" + "\""; } } else { sGA = "\"" + (input.topic !== undefined ? input.topic : "") + "\""; sDPT = "\"" + (input.dpt !== undefined ? input.dpt : "") + "\""; if (input.hasOwnProperty("isWatchDog")) { // Is a watchdog node } else { // Is a device node sOptions = "\"" + (Number(input.initialread) > 0 ? "Initial Read" : "No Initial Read") + ", "; sOptions += (input.notifywrite === true ? "React to Write" : "No React to Write") + ", "; sOptions += (input.notifyresponse === true ? "React to Response" : "No React to Response") + ", "; sOptions += (input.notifyreadrequest === true ? "React to Read" : "No React to Read") + ", "; sOptions += (input.notifyreadrequestalsorespondtobus === true ? "Autorespond to Read Requests" : "No Autorespond to Read Requests") + ", "; sOptions += "Output type " + input.outputtype + ", "; sOptions += (input.outputRBE === true ? "RBE on Output to Bus" : "No RBE on Output to Bus") + ", "; sOptions += (input.inputRBE === true ? "RBE on Input from Bus" : "No RBE on Input from Bus") + "\""; }; }; sNodes += sGA + "\t" + sDPT + "\t" + sNodeID + "\t" + sName + "\t" + sOptions + "\n"; }); res.json(sNodes) } catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn("D " + error) } }); // 12/08/2021 Endpoint for deleting the GA persistent file for the current gateway RED.httpAdmin.get("/deletePersistGAFile", RED.auth.needsPermission('knxUltimate-config.read'), function (req, res) { if (typeof req.query.nodeID !== "undefined" && req.query.nodeID !== null && req.query.nodeID !== "") { let sFile = path.join(node.userDir, "knxpersistvalues", "knxpersist" + req.query.nodeID + ".json"); try { fs.unlinkSync(sFile); } catch (error) { } res.json({ error: "No error" }); } else { res.json({ error: "No NodeID specified" }); } }); // 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 }; var readHistory = []; let delay = 0; node.nodeClients .filter(_oClient => (_oClient.isWatchDog !== undefined && _oClient.isWatchDog === true)) .forEach(_oClient => { let oClient = RED.nodes.getNode(_oClient.id); 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.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: "" }); } // 05/04/2022 create the Json variable and add it to the list let jNode = {}; jNode.id = _Node.id; jNode.topic = _Node.topic; if (_Node.hasOwnProperty("isWatchDog")) jNode.isWatchDog = _Node.isWatchDog; jNode.initialread = _Node.initialread; jNode.notifywrite = _Node.notifywrite; jNode.notifyresponse = _Node.notifyresponse; jNode.notifyreadrequest = _Node.notifyreadrequest; node.nodeClients.push(jNode); } } node.removeClient = (_Node) => { // Remove the client node from the clients array try { node.nodeClients = node.nodeClients.filter(x => x.id !== _Node.id) } catch (error) { } // If no clien nodes, disconnect from bus. if (node.nodeClients.length === 0) { try { node.Disconnect(); } catch (error) { } } } // 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 loadExposedGAs(); // 04/04/2021 load the current values of GA payload try { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("KNXUltimate-config: Loaded saved GA values", node.exposedGAs.length); } catch (error) { } if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("KNXUltimate-config: Do DoInitialReadFromKNXBusOrFile"); try { var 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 => { let oClient = RED.nodes.getNode(_oClient.id); // 05/04/2022 Get the real node 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) { let oExposedGA = node.exposedGAs.find(a => a.ga === oClient.topic); if (oExposedGA !== undefined) { // Retrieve the value from exposedGAs let 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 }) 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 || "" }); if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("knxUltimate-config: 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)) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.debug("KNXUltimate-config: 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.writeQueueAdd({ grpaddr: oClient.topic, payload: "", dpt: "", outputtype: "read", nodecallerid: oClient.id }); readHistory.push(oClient.topic); }; }; }; } } catch (error) { } } }) // 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 => { let oClient = RED.nodes.getNode(_oClient.id); // 05/04/2022 Get the real node 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.writeQueueAdd({ grpaddr: element.ga, payload: "", dpt: "", outputtype: "read", nodecallerid: element.id }); readHistory.push(element.ga); if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.debug("KNXUltimate-config: DoInitialReadFromKNXBusOrFile from Universal Node: sent read request to GA " + element.ga); }; } } else { if (!readHistory.includes(oClient.topic)) { node.writeQueueAdd({ grpaddr: oClient.topic, payload: "", dpt: "", outputtype: "read", nodecallerid: oClient.id }); readHistory.push(oClient.topic); if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.debug("KNXUltimate-config: 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 = (_sIP, _iPort, _sPhysicalAddress, _sBindToEthernetInterface, _Protocol) => { 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 (node.sysLogger !== undefined && node.sysLogger !== null) 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); try { node.Disconnect(); // node.setKnxConnectionProperties(); // 28/12/2021 Commented node.setAllClientsStatus("CONFIG", "yellow", "KNXUltimage-config:setGatewayConfig: disconnected by new setting..."); if (node.sysLogger !== undefined && node.sysLogger !== null) 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 = (_bConnection) => { if (_bConnection === undefined) return; if (node.sysLogger !== undefined && node.sysLogger !== null) 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 { 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; node.Disconnect(); let 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) { } } }; // 08/10/2021 // node.knxConnectionProperties must be: // const optionsDefaults = { // physAddr: '15.15.200', // connectionKeepAliveTimeout: KNXConstants.KNX_CONSTANTS.CONNECTION_ALIVE_TIME, // ipAddr: "224.0.23.12", // ipPort: 3671, // hostProtocol: "TunnelUDP", // TunnelUDP, TunnelTCP, Multicast // isSecureKNXEnabled: false, // suppress_ack_ldatareq: false, // loglevel: "info", // localEchoInTunneling: true, // localIPAddress: "", // jKNXSecureKeyring: node.jKNXSecureKeyring // interface: "" // }; 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, localEchoInTunneling: node.localEchoInTunneling, // 14/03/2020 local echo in tunneling mode (see API Supergiovane) hostProtocol: node.hostProtocol, isSecureKNXEnabled: node.knxSecureSelected, jKNXSecureKeyring: node.jKNXSecureKeyring, localIPAddress: "" // Riempito da KNXEngine }; // 11/07/2022 Test if the IP is a valid one or is a DNS Name if (node.host.toUpperCase() !== "EMULATE") { switch (net.isIP(node.host)) { case 0: // Invalid IP, resolve the DNS name. let 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 if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("knxUltimate-config: 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."); } if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("knxUltimate-config: 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") { var sIfaceName = ""; if (node.KNXEthInterface === "Manual") { sIfaceName = node.KNXEthInterfaceManuallyInput; if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("KNXUltimate-config: Bind KNX Bus to interface : " + sIfaceName + " (Interface's name entered by hand). Node " + node.name); } else { sIfaceName = node.KNXEthInterface; if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("KNXUltimate-config: 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) { } if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("KNXUltimate-config: Bind KNX Bus to interface (Auto). Node " + node.name); } } //node.setKnxConnectionProperties(); 28/12/2021 Commented node.initKNXConnection = () => { try { node.setKnxConnectionProperties(); //28/12/2021 Added } catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("knxUltimate-config: setKnxConnectionProperties: " + error.message); if (node.linkStatus !== "disconnected") 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 { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("knxUltimate-config: No nodes linked to this gateway " + node.name); try { if (node.linkStatus !== "disconnected") node.Disconnect(); } catch (error) { } return } catch (error) { } } // 26/01/2021 Emulation mode if (node.host.toUpperCase() === "EMULATE") { node.knxConnection = true; // Must not be null node.telegramsQueue = []; // 01/10/2020 Supergiovane: clear the telegram queue node.linkStatus = "connected"; node.setAllClientsStatus("Emulation", "green", "Waiting for telegram.") // Start the timer to do initial read. if (node.timerDoInitialRead !== null) clearTimeout(node.timerDoInitialRead); node.timerDoInitialRead = setTimeout(DoInitialReadFromKNXBusOrFile, 3000); // 17/02/2020 Do initial read of all nodes requesting initial read, after all nodes have been registered to the sercer return; } try { // 02/01/2022 This is important to free the tunnel in case of hard disconnection. node.Disconnect(); } catch (error) { //console.log(error) } try { // Unsetting handlers if node.knxConnection was existing try { if (node.knxConnection !== null && node.knxConnection !== undefined) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.debug("knxUltimate-config: removing old handlers. Node " + node.name); node.knxConnection.removeAllListeners(); } } catch (error) { console.log("BANANA ERRORINO", error); } node.knxConnection = new knx.KNXClient(node.knxConnectionProperties); // Setting handlers // ###################################### node.knxConnection.on(knx.KNXClient.KNXClientEvents.indication, handleBusEvents); node.knxConnection.on(knx.KNXClient.KNXClientEvents.error, err => { try { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("knxUltimate-config: 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")) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("knxUltimate-config: 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"); if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("knxUltimate-config: Disconnected by: " + (err.message === undefined ? err : err.message)); }); // Call discoverCB when a knx gateway has been discovered. // node.knxConnection.on(knx.KNXClient.KNXClientEvents.discover, info => { // const [ip, port] = info.split(":"); // discoverCB(ip, port); // }); node.knxConnection.on(knx.KNXClient.KNXClientEvents.disconnected, info => { node.startTimerClearTelegramQueue(); // 21/01/2022 Clear the telegram queue after a while if (node.linkStatus !== "disconnected") { node.linkStatus = "disconnected"; if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn("knxUltimate-config: Disconnected event %s", info); node.Disconnect("Disconnected by event: " + info || "", "red"); // 11/03/2022 } }); node.knxConnection.on(knx.KNXClient.KNXClientEvents.close, info => { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.debug("knxUltimate-config: KNXClient socket closed."); }); node.knxConnection.on(knx.KNXClient.KNXClientEvents.connected, info => { if (node.timerClearTelegramQueue !== null) { clearTimeout(node.timerClearTelegramQueue); // Connected. Stop the timer that clears the telegrams queue. node.timerClearTelegramQueue = null; } // 12/11/2021 Starts the telegram out queue handler if (node.timerSendTelegramFromQueue !== null) clearInterval(node.timerSendTelegramFromQueue); node.timerSendTelegramFromQueue = setInterval(handleTelegramQueue, (config.delaybetweentelegrams === undefined || Number(config.delaybetweentelegrams) < 20) ? 20 : Number(config.delaybetweentelegrams)); // 02/01/2020 Start the timer that handles the queue of telegrams node.linkStatus = "connected"; // Start the timer to do initial read. if (node.timerDoInitialRead !== null) clearTimeout(node.timerDoInitialRead); node.timerDoInitialRead = setTimeout(DoInitialReadFromKNXBusOrFile, 6000); // 17/02/2020 Do initial read of all nodes requesting initial read let t = setTimeout(() => { // 21/03/2022 fixed possible memory leak. Previously was setTimeout without "let t = ". node.setAllClientsStatus("Connected.", "green", "On duty.") }, 500); if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info("knxUltimate-config: Connected to %o", info); }); node.knxConnection.on(knx.KNXClient.KNXClientEvents.connecting, info => { node.linkStatus = "connecting"; if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.debug("knxUltimate-config: Connecting to" + info.ipAddr || "");