node-red-contrib-knx-ultimate
Version:
Control your KNX 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,118 lines (1,051 loc) • 82.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");
//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 || "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.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.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
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.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
// 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) {
node.sysLogger?.error("KNX Secure: error parsing the keyring XML: " + error.message);
node.jKNXSecureKeyring = null;
node.knxSecureSelected = false;
const 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)) {
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?.info("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) { }
}
};
// 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: "",
// KNXQueueSendIntervalMilliseconds: Number(node.delaybetweentelegrams)
// };
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
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();
_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
});
_input.setNodeStatus({
fill: "grey",
shape: "dot",
text: "Read",
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
} else {
// Read Request
const msg = buildInputMessage({
_srcGA: _src,
_destGA: _dest,
_event: _evt,
_Rawvalue: null,
_inputDpt: _input.dpt,
_devicename: _input.name || "",
_outputtopic: _input.outputtopic,
_oNode: _input,
_echoed: _echoed
});
msg.previouspayload = typeof _input.currentPayload !== "undefined" ? _input.currentPayload : ""; // 24/01/2020 Reset previous payload
// 24/09/2019 Autorespond to BUS
if (_input.hasOwnProperty("notifyreadrequestalsorespondtobus") && _input.notifyreadrequestalsorespondtobus === true) {
if (typeof _input.currentPayload === "undefined" || _input.currentPayload === "" || _input.currentPayload === null) {
// 14/08/2021 Added || input.currentPayload === null
node.sendKNXTelegramToKNXEngine({
grpaddr: _dest,
payload: _input.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized,
dpt: _input.dpt,
outputtype: "response",
nodecallerid: _input.id,
});