node-red-contrib-knx-ultimate 3.3.39 → 3.3.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,7 +6,11 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 3.3.39** - Juli 2025<br/>
9
+ **Version 3.3.40** - August 2025<br/>
10
+ - KNX Peristent value files are now saved every 5 seconds, other than at the node disconnection.<br/>
11
+ - Fixed an issue preventing the AutoResponder node to start, if the imported ETS file contains malformed structure.<br/>
12
+
13
+ **Version 3.3.39** - July 2025<br/>
10
14
  - Added other snippets to the KNX Device function tab.<br/>
11
15
 
12
16
  **Version 3.3.38** - May 2025<br/>
Binary file
@@ -68,7 +68,6 @@ module.exports = (RED) => {
68
68
  node.KNXEthInterfaceManuallyInput = typeof config.KNXEthInterfaceManuallyInput === "undefined" ? "" : config.KNXEthInterfaceManuallyInput; // If you manually set the interface name, it will be wrote here
69
69
  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
70
70
  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
71
- node.csv = readCSV(config.csv); // Array from ETS CSV Group Addresses {ga:group address, dpt: datapoint, devicename: full device name with main and subgroups}
72
71
  node.localEchoInTunneling = typeof config.localEchoInTunneling !== "undefined" ? config.localEchoInTunneling : true;
73
72
  node.userDir = path.join(RED.settings.userDir, "knxultimatestorage"); // 04/04/2021 Supergiovane: Storage for service files
74
73
  node.exposedGAs = [];
@@ -79,6 +78,8 @@ module.exports = (RED) => {
79
78
  try {
80
79
  node.sysLogger = new loggerClass({ loglevel: node.loglevel, setPrefix: node.type + " <" + (node.name || node.id || '') + ">" });
81
80
  } catch (error) { console.log(error.stack) }
81
+ node.csv = readCSV(config.csv); // Array from ETS CSV Group Addresses {ga:group address, dpt: datapoint, devicename: full device name with main and subgroups}
82
+
82
83
  // 12/11/2021 Connect at start delay
83
84
  node.autoReconnect = true; // 20/03/2022 Default
84
85
  if (config.autoReconnect === "no" || config.autoReconnect === false) {
@@ -99,6 +100,7 @@ module.exports = (RED) => {
99
100
  node.delaybetweentelegrams = (config.delaybetweentelegrams === undefined || config.delaybetweentelegrams === null || config.delaybetweentelegrams === '') ? 25 : Number(config.delaybetweentelegrams);
100
101
  if (node.delaybetweentelegrams < 25) node.delaybetweentelegrams = 25; // Protection avoiding handleKNXQueue hangs
101
102
  if (node.delaybetweentelegrams > 100) node.delaybetweentelegrams = 100; // Protection avoiding handleKNXQueue hangs
103
+ node.timerSaveExposedGAs = null; // Timer to save the exposed GA every once in a while
102
104
 
103
105
  // 05/12/2021 Set the protocol (this is undefined if coming from ild versions
104
106
  if (node.hostProtocol === "Auto") {
@@ -194,7 +196,7 @@ module.exports = (RED) => {
194
196
  node.sysLogger?.info("payload cache set to " + path.join(node.userDir, "knxpersistvalues"));
195
197
  }
196
198
 
197
- function saveExposedGAs() {
199
+ async function saveExposedGAs() {
198
200
  const sFile = path.join(node.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json");
199
201
  try {
200
202
  if (node.exposedGAs.length > 0) {
@@ -277,10 +279,16 @@ module.exports = (RED) => {
277
279
  // 17/02/2020 Do initial read (called by node.timerDoInitialRead timer)
278
280
  function DoInitialReadFromKNXBusOrFile() {
279
281
  if (node.linkStatus !== "connected") return; // 29/08/2019 If not connected, exit
282
+ node.sysLogger?.info("Do DoInitialReadFromKNXBusOrFile");
280
283
  loadExposedGAs(); // 04/04/2021 load the current values of GA payload
284
+ node.sysLogger?.info("Loaded persist GA values", node.exposedGAs?.length);
285
+
286
+ if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs);
287
+ node.timerSaveExposedGAs = setInterval(async () => {
288
+ await saveExposedGAs();
289
+ }, 5000);
290
+ node.sysLogger?.info("Started timerSaveExposedGAs with array lenght ", node.exposedGAs?.length);
281
291
 
282
- node.sysLogger?.info("Loaded saved GA values", node.exposedGAs?.length);
283
- node.sysLogger?.info("Do DoInitialReadFromKNXBusOrFile");
284
292
  try {
285
293
  const readHistory = [];
286
294
 
@@ -1592,87 +1600,92 @@ module.exports = (RED) => {
1592
1600
  let sSecondGroupName = "";
1593
1601
  let sFather = "";
1594
1602
  for (let index = 0; index < fileGA.length; index++) {
1595
- let element = fileGA[index];
1596
- element = element.replace(/\"/g, ""); // Rimuovo le virgolette
1597
- element = element.replace(/\#/g, ""); // Rimuovo evetuali #
1598
-
1599
- if (element !== "") {
1600
- // Main and secondary group names
1601
- if ((element.split("\t")[1].match(/-/g) || []).length == 2) {
1602
- // Found main group family name (Example Light Actuators)
1603
- sFirstGroupName = element.split("\t")[0] || "";
1604
- sSecondGroupName = "";
1605
- }
1606
- if ((element.split("\t")[1].match(/-/g) || []).length == 1) {
1607
- // Found second group family name (Example First Floor light)
1608
- sSecondGroupName = element.split("\t")[0] || "";
1609
- }
1610
- if (sFirstGroupName !== "" && sSecondGroupName !== "") {
1611
- sFather = "(" + sFirstGroupName + "->" + sSecondGroupName + ") ";
1612
- }
1603
+ try {
1604
+ let element = fileGA[index];
1605
+ element = element.replace(/\"/g, ""); // Rimuovo le virgolette
1606
+ element = element.replace(/\#/g, ""); // Rimuovo evetuali #
1607
+
1608
+ if (element !== "") {
1609
+ // Main and secondary group names
1610
+ if ((element.split("\t")[1].match(/-/g) || []).length == 2) {
1611
+ // Found main group family name (Example Light Actuators)
1612
+ sFirstGroupName = element.split("\t")[0] || "";
1613
+ sSecondGroupName = "";
1614
+ }
1615
+ if ((element.split("\t")[1].match(/-/g) || []).length == 1) {
1616
+ // Found second group family name (Example First Floor light)
1617
+ sSecondGroupName = element.split("\t")[0] || "";
1618
+ }
1619
+ if (sFirstGroupName !== "" && sSecondGroupName !== "") {
1620
+ sFather = "(" + sFirstGroupName + "->" + sSecondGroupName + ") ";
1621
+ }
1613
1622
 
1614
- if (element.split("\t")[1].search("-") == -1 && element.split("\t")[1].search("/") !== -1) {
1615
- // Ho trovato una riga contenente un GA valido, cioè con 2 "/"
1616
- if (element.split("\t")[5] == "") {
1617
- if (node.stopETSImportIfNoDatapoint === "stop") {
1618
- node.error(
1619
- "KNXUltimate-config: ABORT IMPORT OF ETS CSV FILE. To skip the invalid datapoint and continue import, change the related setting, located in the config node in the ETS import section.",
1620
- );
1621
- return;
1622
- }
1623
- if (node.stopETSImportIfNoDatapoint === "fake") {
1624
- // 02/03/2020 Whould you like to continue without datapoint? Good. Here a totally fake datapoint
1625
- node.warn(
1626
- "KNXUltimate-config: WARNING IMPORT OF ETS CSV FILE. Datapoint not set. You choosed to continue import with a fake datapoint 1.001. -> " +
1627
- element.split("\t")[0] +
1628
- " " +
1629
- element.split("\t")[1],
1630
- );
1623
+ if (element.split("\t")[1].search("-") == -1 && element.split("\t")[1].search("/") !== -1) {
1624
+ // Ho trovato una riga contenente un GA valido, cioè con 2 "/"
1625
+ if (element.split("\t")[5] == "") {
1626
+ if (node.stopETSImportIfNoDatapoint === "stop") {
1627
+ node.error(
1628
+ "KNXUltimate-config: ABORT IMPORT OF ETS CSV FILE. To skip the invalid datapoint and continue import, change the related setting, located in the config node in the ETS import section.",
1629
+ );
1630
+ return;
1631
+ }
1632
+ if (node.stopETSImportIfNoDatapoint === "fake") {
1633
+ // 02/03/2020 Whould you like to continue without datapoint? Good. Here a totally fake datapoint
1634
+ node.warn(
1635
+ "KNXUltimate-config: WARNING IMPORT OF ETS CSV FILE. Datapoint not set. You choosed to continue import with a fake datapoint 1.001. -> " +
1636
+ element.split("\t")[0] +
1637
+ " " +
1638
+ element.split("\t")[1],
1639
+ );
1640
+ ajsonOutput.push({
1641
+ ga: element.split("\t")[1],
1642
+ dpt: "1.001",
1643
+ devicename: sFather + element.split("\t")[0] + " (DPT NOT SET IN ETS - FAKE DPT USED)",
1644
+ });
1645
+ } else {
1646
+ // 31/03/2020 Skip import
1647
+ node.warn(
1648
+ "KNXUltimate-config: WARNING IMPORT OF ETS CSV FILE. Datapoint not set. You choosed to skip -> " +
1649
+ element.split("\t")[0] +
1650
+ " " +
1651
+ element.split("\t")[1],
1652
+ );
1653
+ }
1654
+ } else {
1655
+ const DPTa = element.split("\t")[5].split("-")[1];
1656
+ let DPTb = element.split("\t")[5].split("-")[2];
1657
+ if (typeof DPTb === "undefined") {
1658
+ node.warn(
1659
+ "KNXUltimate-config: WARNING: Datapoint not fully set (there is only the main type). I applied a default .001, but please check if i'ts ok ->" +
1660
+ element.split("\t")[0] +
1661
+ " " +
1662
+ element.split("\t")[1] +
1663
+ " Datapoint: " +
1664
+ element.split("\t")[5],
1665
+ );
1666
+ DPTb = "001"; // default
1667
+ }
1668
+ // Trailing zeroes
1669
+ if (DPTb.length == 1) {
1670
+ DPTb = "00" + DPTb;
1671
+ } else if (DPTb.length == 2) {
1672
+ DPTb = "0" + DPTb;
1673
+ }
1674
+ if (DPTb.length == 3) {
1675
+ DPTb = "" + DPTb; // stupid, but for readability
1676
+ }
1631
1677
  ajsonOutput.push({
1632
1678
  ga: element.split("\t")[1],
1633
- dpt: "1.001",
1634
- devicename: sFather + element.split("\t")[0] + " (DPT NOT SET IN ETS - FAKE DPT USED)",
1679
+ dpt: DPTa + "." + DPTb,
1680
+ devicename: sFather + element.split("\t")[0],
1635
1681
  });
1636
- } else {
1637
- // 31/03/2020 Skip import
1638
- node.warn(
1639
- "KNXUltimate-config: WARNING IMPORT OF ETS CSV FILE. Datapoint not set. You choosed to skip -> " +
1640
- element.split("\t")[0] +
1641
- " " +
1642
- element.split("\t")[1],
1643
- );
1644
- }
1645
- } else {
1646
- const DPTa = element.split("\t")[5].split("-")[1];
1647
- let DPTb = element.split("\t")[5].split("-")[2];
1648
- if (typeof DPTb === "undefined") {
1649
- node.warn(
1650
- "KNXUltimate-config: WARNING: Datapoint not fully set (there is only the main type). I applied a default .001, but please check if i'ts ok ->" +
1651
- element.split("\t")[0] +
1652
- " " +
1653
- element.split("\t")[1] +
1654
- " Datapoint: " +
1655
- element.split("\t")[5],
1656
- );
1657
- DPTb = "001"; // default
1658
- }
1659
- // Trailing zeroes
1660
- if (DPTb.length == 1) {
1661
- DPTb = "00" + DPTb;
1662
- } else if (DPTb.length == 2) {
1663
- DPTb = "0" + DPTb;
1664
1682
  }
1665
- if (DPTb.length == 3) {
1666
- DPTb = "" + DPTb; // stupid, but for readability
1667
- }
1668
- ajsonOutput.push({
1669
- ga: element.split("\t")[1],
1670
- dpt: DPTa + "." + DPTb,
1671
- devicename: sFather + element.split("\t")[0],
1672
- });
1673
1683
  }
1674
1684
  }
1685
+ } catch (error) {
1686
+ node.sysLogger?.error("readCSV " + " ROW:" + fileGA[index] + " Error:" + error.stack);
1675
1687
  }
1688
+
1676
1689
  }
1677
1690
 
1678
1691
  return ajsonOutput;
@@ -1840,6 +1853,7 @@ module.exports = (RED) => {
1840
1853
  }, 10000);
1841
1854
 
1842
1855
  node.Disconnect = async (_sNodeStatus = "", _sColor = "grey") => {
1856
+ if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs);
1843
1857
  if (node.linkStatus === "disconnected") {
1844
1858
  node.sysLogger?.debug("Disconnect: already not connected:" + node.linkStatus + ", node.autoReconnect:" + node.autoReconnect);
1845
1859
  return;
@@ -1854,7 +1868,7 @@ module.exports = (RED) => {
1854
1868
  );
1855
1869
  }
1856
1870
  node.setAllClientsStatus("Disconnected", _sColor, _sNodeStatus);
1857
- saveExposedGAs(); // 04/04/2021 save the current values of GA payload
1871
+ await saveExposedGAs(); // 04/04/2021 save the current values of GA payload
1858
1872
  node.sysLogger?.debug("Disconnected, node.autoReconnect:" + node.autoReconnect);
1859
1873
  };
1860
1874
 
@@ -121,7 +121,7 @@ module.exports = function (RED) {
121
121
  node.inputmessage = {}; // Stores the input message to be passed through
122
122
  node.timerTTLInputMessage = null; // The stored node.inputmessage has a ttl.
123
123
  try {
124
- node.sysLogger = new loggerClass({ loglevel: node.serverKNX.loglevel, setPrefix: node.type + " <" + (node.name || node.id || '') + ">" });
124
+ node.sysLogger = new loggerClass({ loglevel: node.serverKNX.loglevel || 'error', setPrefix: node.type + " <" + (node.name || node.id || '') + ">" });
125
125
  } catch (error) { console.log(error.stack) }
126
126
  node.sendMsgToKNXCode = config.sendMsgToKNXCode || undefined;
127
127
  node.receiveMsgFromKNXCode = config.receiveMsgFromKNXCode || undefined;
@@ -35,7 +35,7 @@ module.exports = function (RED) {
35
35
  function knxUltimateAutoResponder(config) {
36
36
  RED.nodes.createNode(this, config)
37
37
  const node = this
38
- node.serverKNX = RED.nodes.getNode(config.server) || undefined
38
+ node.serverKNX = RED.nodes.getNode(config.server)
39
39
  node.topic = node.name
40
40
  node.name = config.name === undefined ? 'Auto responder' : config.name
41
41
  node.outputtopic = node.name
@@ -50,6 +50,9 @@ module.exports = function (RED) {
50
50
  node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS)
51
51
  node.exposedGAs = [];
52
52
  node.commandText = []; // Raw list Respond To
53
+ node.timerSaveExposedGAs = null;
54
+ if (node.serverKNX === null) { node.status({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return; }
55
+
53
56
  try {
54
57
  node.sysLogger = new loggerClass({ loglevel: node.serverKNX.loglevel, setPrefix: node.type + " <" + (node.name || node.id || '') + ">" });
55
58
  } catch (error) { console.log(error.stack) }
@@ -66,7 +69,7 @@ module.exports = function (RED) {
66
69
  // }
67
70
  }
68
71
 
69
- node.saveExposedGAs = () => {
72
+ node.saveExposedGAs = async () => {
70
73
  const sFile = path.join(node.serverKNX.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json");
71
74
  try {
72
75
  if (node.exposedGAs.length > 0) {
@@ -77,6 +80,7 @@ module.exports = function (RED) {
77
80
  if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error("knxUltimateAutoResponder: unable to write peristent values to the file " + sFile + " " + err.message);
78
81
  }
79
82
  }
83
+
80
84
  node.loadExposedGAs = () => {
81
85
  const sFile = path.join(node.serverKNX.userDir, "knxpersistvalues", "knxpersist" + node.id + ".json");
82
86
  try {
@@ -94,10 +98,14 @@ module.exports = function (RED) {
94
98
  node.exposedGAs.forEach(element => {
95
99
  element.enabled = false;
96
100
  })
101
+ if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs);
102
+ node.sysLogger?.info("Started timerSaveExposedGAs with array lenght ", node.exposedGAs?.length);
103
+ node.timerSaveExposedGAs = setInterval(async () => {
104
+ await node.saveExposedGAs();
105
+ }, 5000);
97
106
  } catch (error) {
98
107
  }
99
108
 
100
-
101
109
  // Add the ETS CSV file list to exposedGAs
102
110
  if (node.serverKNX.csv === undefined || node.serverKNX.csv === '' || node.serverKNX.csv.length === 0) {
103
111
  node.status({ fill: 'grey', shape: 'ring', text: 'No ETS file imported', payload: '', dpt: '', devicename: '' });
@@ -219,6 +227,7 @@ module.exports = function (RED) {
219
227
 
220
228
  node.on('close', function (done) {
221
229
  try {
230
+ if (node.timerSaveExposedGAs !== null) clearInterval(node.timerSaveExposedGAs);
222
231
  node.saveExposedGAs();
223
232
  } catch (error) {
224
233
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "3.3.39",
6
+ "version": "3.3.40",
7
7
  "description": "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.",
8
8
  "dependencies": {
9
9
  "binary-parser": "2.2.1",