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
JavaScript
/* 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