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
JavaScript
// 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 || "");