node-red-contrib-knx-ultimate 4.0.7 → 4.0.9

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/nodes/commonFunctions.js +15 -15
  3. package/nodes/knxUltimate-config.html +175 -39
  4. package/nodes/knxUltimate-config.js +279 -2
  5. package/nodes/knxUltimate.js +2 -1
  6. package/nodes/knxUltimateAutoResponder.js +2 -1
  7. package/nodes/knxUltimateHueLight.html +6 -12
  8. package/nodes/knxUltimateHueLight.js +219 -217
  9. package/nodes/knxUltimateHueScene.js +2 -1
  10. package/nodes/knxUltimateLoadControl.js +2 -1
  11. package/nodes/knxUltimateSceneController.js +2 -1
  12. package/nodes/locales/de/knxUltimate-config.json +0 -1
  13. package/nodes/locales/en-US/knxUltimate-config.json +0 -1
  14. package/nodes/locales/it/knxUltimate-config.json +0 -1
  15. package/nodes/locales/zh-CN/knxUltimate-config.json +0 -1
  16. package/package.json +2 -2
  17. package/tutorial/hue-config-teleprompter.txt +43 -0
  18. package/tutorial/hue-config.md +44 -0
  19. package/tutorial/knxUltimate-config-teleprompter.txt +87 -0
  20. package/tutorial/knxUltimate-config.md +65 -0
  21. package/tutorial/knxUltimate-teleprompter.txt +60 -0
  22. package/tutorial/knxUltimate.md +59 -0
  23. package/tutorial/knxUltimateAlerter-teleprompter.txt +48 -0
  24. package/tutorial/knxUltimateAlerter.md +49 -0
  25. package/tutorial/knxUltimateAutoResponder-teleprompter.txt +43 -0
  26. package/tutorial/knxUltimateAutoResponder.md +46 -0
  27. package/tutorial/knxUltimateGlobalContext-teleprompter.txt +44 -0
  28. package/tutorial/knxUltimateGlobalContext.md +44 -0
  29. package/tutorial/knxUltimateHATranslator-teleprompter.txt +45 -0
  30. package/tutorial/knxUltimateHATranslator.md +43 -0
  31. package/tutorial/knxUltimateHueBattery-teleprompter.txt +38 -0
  32. package/tutorial/knxUltimateHueBattery.md +40 -0
  33. package/tutorial/knxUltimateHueButton-teleprompter.txt +45 -0
  34. package/tutorial/knxUltimateHueButton.md +54 -0
  35. package/tutorial/knxUltimateHueContactSensor-teleprompter.txt +35 -0
  36. package/tutorial/knxUltimateHueContactSensor.md +45 -0
  37. package/tutorial/knxUltimateHueLight-teleprompter.txt +50 -0
  38. package/tutorial/knxUltimateHueLight.md +66 -0
  39. package/tutorial/knxUltimateHueLightSensor-teleprompter.txt +42 -0
  40. package/tutorial/knxUltimateHueLightSensor.md +44 -0
  41. package/tutorial/knxUltimateHueMotion-teleprompter.txt +39 -0
  42. package/tutorial/knxUltimateHueMotion.md +40 -0
  43. package/tutorial/knxUltimateHueScene-teleprompter.txt +45 -0
  44. package/tutorial/knxUltimateHueScene.md +52 -0
  45. package/tutorial/knxUltimateHueTapDial-teleprompter.txt +40 -0
  46. package/tutorial/knxUltimateHueTapDial.md +40 -0
  47. package/tutorial/knxUltimateHueTemperatureSensor-teleprompter.txt +42 -0
  48. package/tutorial/knxUltimateHueTemperatureSensor.md +43 -0
  49. package/tutorial/knxUltimateHueZigbeeConnectivity-teleprompter.txt +41 -0
  50. package/tutorial/knxUltimateHueZigbeeConnectivity.md +43 -0
  51. package/tutorial/knxUltimateHuedevice_software_update-teleprompter.txt +42 -0
  52. package/tutorial/knxUltimateHuedevice_software_update.md +44 -0
  53. package/tutorial/knxUltimateLoadControl-teleprompter.txt +40 -0
  54. package/tutorial/knxUltimateLoadControl.md +52 -0
  55. package/tutorial/knxUltimateLogger-teleprompter.txt +39 -0
  56. package/tutorial/knxUltimateLogger.md +42 -0
  57. package/tutorial/knxUltimateSceneController-teleprompter.txt +38 -0
  58. package/tutorial/knxUltimateSceneController.md +48 -0
  59. package/tutorial/knxUltimateViewer-teleprompter.txt +37 -0
  60. package/tutorial/knxUltimateViewer.md +41 -0
  61. package/tutorial/knxUltimateWatchDog-teleprompter.txt +39 -0
  62. package/tutorial/knxUltimateWatchDog.md +42 -0
  63. package/http2Testing/myhttp2.js +0 -103
  64. package/http2Testing/ratelimiter.js +0 -24
  65. package/http2Testing/ratelimitertestbed.js +0 -7
  66. package/http2Testing/testbed.js +0 -36
package/CHANGELOG.md CHANGED
@@ -6,6 +6,10 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 4.0.9** - September 2025<br/>
10
+ - KNX Config node: now the ethernet interface is automatically selected, based on the KNX Gateway's IP subnet.<br/>
11
+ - Added the details of keyring in clear text, when the loglevel is set to "debug".<br/>
12
+
9
13
  **Version 4.0.7** - September 2025<br/>
10
14
  - Fixed an issue where some secure fields were missing when creating a new config-node.<br/>
11
15
  - HUE Light: enhanced the Homeassistant export yaml.<br/>
@@ -299,21 +299,21 @@ module.exports = (RED) => {
299
299
  try {
300
300
  const oiFaces = oOS.networkInterfaces();
301
301
  Object.keys(oiFaces).forEach((ifname) => {
302
- // Interface with single IP
303
- if (Object.keys(oiFaces[ifname]).length === 1) {
304
- if (Object.keys(oiFaces[ifname])[0].internal === false) {
305
- jListInterfaces.push({
306
- name: ifname,
307
- address: Object.keys(oiFaces[ifname])[0].address,
308
- });
309
- }
310
- } else {
311
- let sAddresses = "";
312
- oiFaces[ifname].forEach((iface) => {
313
- if (iface.internal === false) sAddresses += `+${iface.address}`;
314
- });
315
- if (sAddresses !== "") jListInterfaces.push({ name: ifname, address: sAddresses });
316
- }
302
+ const ifaceEntries = Array.isArray(oiFaces[ifname]) ? oiFaces[ifname] : [];
303
+ const externalEntries = ifaceEntries.filter((iface) => iface && iface.internal === false);
304
+ if (externalEntries.length === 0) return;
305
+ const addresses = externalEntries.map((iface) => ({
306
+ address: iface.address,
307
+ family: iface.family,
308
+ netmask: iface.netmask,
309
+ cidr: iface.cidr || null,
310
+ }));
311
+ const displayAddress = addresses.map((entry) => entry.address).join(", ");
312
+ jListInterfaces.push({
313
+ name: ifname,
314
+ address: displayAddress,
315
+ addresses,
316
+ });
317
317
  });
318
318
  } catch (error) { }
319
319
  res.json(jListInterfaces);
@@ -140,6 +140,43 @@
140
140
  return first >= 224 && first <= 239;
141
141
  }
142
142
 
143
+ const NETMASK_BIT_LOOKUP = (() => {
144
+ const lookup = new Array(256);
145
+ for (let i = 0; i < 256; i++) {
146
+ let bits = 0;
147
+ let value = i;
148
+ while (value > 0) {
149
+ bits += value & 1;
150
+ value >>= 1;
151
+ }
152
+ lookup[i] = bits;
153
+ }
154
+ return lookup;
155
+ })();
156
+
157
+ function countNetmaskBits(maskOctets) {
158
+ if (!Array.isArray(maskOctets) || maskOctets.length !== 4) return 0;
159
+ let total = 0;
160
+ for (let i = 0; i < 4; i++) {
161
+ const oct = maskOctets[i];
162
+ if (!Number.isInteger(oct) || oct < 0 || oct > 255) return 0;
163
+ total += NETMASK_BIT_LOOKUP[oct];
164
+ }
165
+ return total;
166
+ }
167
+
168
+ function computeIPv4NetworkKey(ipOctets, maskOctets) {
169
+ if (!Array.isArray(ipOctets) || ipOctets.length !== 4 || !Array.isArray(maskOctets) || maskOctets.length !== 4) return null;
170
+ const parts = [];
171
+ for (let i = 0; i < 4; i++) {
172
+ const ipVal = ipOctets[i];
173
+ const maskVal = maskOctets[i];
174
+ if (!Number.isInteger(ipVal) || ipVal < 0 || ipVal > 255 || !Number.isInteger(maskVal) || maskVal < 0 || maskVal > 255) return null;
175
+ parts.push(ipVal & maskVal);
176
+ }
177
+ return parts.join('.');
178
+ }
179
+
143
180
  function normalizeTunnelIAValue(value) {
144
181
  if (typeof value !== 'string') return '';
145
182
  const trimmed = value.trim();
@@ -499,6 +536,7 @@
499
536
  } catch (e) { }
500
537
  const phys = `${prefix}.${rnd}`;
501
538
  $("#node-config-input-physAddr").val(phys);
539
+ autoSelectEthernetInterfaceForHost({ force: true });
502
540
  blinkBackgroundArray(["#node-config-input-host", "#node-config-input-port", "#node-config-input-name", "#node-config-input-physAddr"]);
503
541
  return false;
504
542
  },
@@ -552,40 +590,120 @@
552
590
  });
553
591
  } catch (e) { }
554
592
 
593
+ const $knxEthInterfaceSelect = $("#node-config-input-KNXEthInterface");
594
+ const $knxEthInterfaceManualRow = $("#divKNXEthInterfaceManuallyInput");
595
+ const $knxEthInterfaceManualInput = $("#node-config-input-KNXEthInterfaceManuallyInput");
596
+ let ethernetInterfaces = [];
597
+ let suppressInterfaceSelectionChange = false;
598
+ let userLockedInterfaceSelection = false;
555
599
 
556
- $("#node-config-input-KNXEthInterface").append($("<option></option>")
557
- .attr("value", "Auto")
558
- .text(node._('knxUltimate-config.properties.iface_auto'))
559
- );
560
- $("#node-config-input-KNXEthInterface").append($("<option></option>")
561
- .attr("value", "Manual")
562
- .text(node._('knxUltimate-config.properties.iface_manual'))
563
- );
564
-
565
- $.getJSON('knxUltimateETHInterfaces', (data) => {
566
- data.sort().forEach(iFace => {
567
- $("#node-config-input-KNXEthInterface").append($("<option></option>")
568
- .attr("value", iFace.name)
569
- .text(iFace.name + " (" + iFace.address + ")")
570
- )
571
- });
572
- $("#node-config-input-KNXEthInterface").val(typeof node.KNXEthInterface === "undefined" ? "Auto" : node.KNXEthInterface)
573
- if (node.KNXEthInterface === "Manual") {
574
- // Show input
575
- $("#divKNXEthInterfaceManuallyInput").show();
600
+ function updateEthernetManualVisibility(value) {
601
+ if (value === 'Manual') {
602
+ $knxEthInterfaceManualRow.show();
576
603
  } else {
577
- $("#divKNXEthInterfaceManuallyInput").hide()
604
+ $knxEthInterfaceManualRow.hide();
578
605
  }
606
+ }
579
607
 
580
- $("#node-config-input-KNXEthInterface").on('change', function () {
581
- if ($("#node-config-input-KNXEthInterface").val() === "Manual") {
582
- // Show input
583
- $("#divKNXEthInterfaceManuallyInput").show();
584
- } else {
585
- // Hide input
586
- $("#divKNXEthInterfaceManuallyInput").hide()
608
+ function setEthernetInterfaceSelection(value, triggerChange) {
609
+ const finalValue = value || 'Auto';
610
+ if ($knxEthInterfaceSelect.val() === finalValue) {
611
+ if (triggerChange) {
612
+ suppressInterfaceSelectionChange = true;
613
+ $knxEthInterfaceSelect.trigger('change');
614
+ suppressInterfaceSelectionChange = false;
587
615
  }
616
+ return;
617
+ }
618
+ suppressInterfaceSelectionChange = true;
619
+ $knxEthInterfaceSelect.val(finalValue);
620
+ if (triggerChange) {
621
+ $knxEthInterfaceSelect.trigger('change');
622
+ }
623
+ suppressInterfaceSelectionChange = false;
624
+ }
625
+
626
+ function findEthernetInterfaceMatch(ipOctets) {
627
+ if (!Array.isArray(ipOctets) || ethernetInterfaces.length === 0) return null;
628
+ let bestMatch = null;
629
+ let bestMaskBits = -1;
630
+ ethernetInterfaces.forEach((iface) => {
631
+ const entries = Array.isArray(iface.addresses) ? iface.addresses : [];
632
+ entries.forEach((entry) => {
633
+ if (!entry || entry.family !== 'IPv4') return;
634
+ const ifaceOctets = parseIPv4Address(entry.address);
635
+ const maskOctets = parseIPv4Address(entry.netmask || '');
636
+ if (!ifaceOctets || !maskOctets) return;
637
+ const ifaceNetwork = computeIPv4NetworkKey(ifaceOctets, maskOctets);
638
+ const hostNetwork = computeIPv4NetworkKey(ipOctets, maskOctets);
639
+ if (!ifaceNetwork || !hostNetwork || ifaceNetwork !== hostNetwork) return;
640
+ const maskBits = countNetmaskBits(maskOctets);
641
+ if (maskBits > bestMaskBits) {
642
+ bestMaskBits = maskBits;
643
+ bestMatch = iface;
644
+ }
645
+ });
588
646
  });
647
+ return bestMatch;
648
+ }
649
+
650
+ function autoSelectEthernetInterfaceForHost(options = {}) {
651
+ const { force } = options;
652
+ if (!force && userLockedInterfaceSelection) return;
653
+ const hostValue = String($("#node-config-input-host").val() || '').trim();
654
+ const hostOctets = parseIPv4Address(hostValue);
655
+ if (!hostOctets || isMulticastIPv4(hostOctets)) return;
656
+ const match = findEthernetInterfaceMatch(hostOctets);
657
+ if (!match || !match.name) return;
658
+ if ($knxEthInterfaceSelect.val() === match.name) return;
659
+ setEthernetInterfaceSelection(match.name, true);
660
+ }
661
+
662
+ $knxEthInterfaceSelect.empty()
663
+ .append($("<option></option>")
664
+ .attr("value", "Auto")
665
+ .text(node._('knxUltimate-config.properties.iface_auto')))
666
+ .append($("<option></option>")
667
+ .attr("value", "Manual")
668
+ .text(node._('knxUltimate-config.properties.iface_manual')));
669
+
670
+ $knxEthInterfaceManualInput.val(typeof node.KNXEthInterfaceManuallyInput === "undefined" ? "" : node.KNXEthInterfaceManuallyInput);
671
+
672
+ $knxEthInterfaceSelect.on('change', function () {
673
+ const value = $knxEthInterfaceSelect.val();
674
+ updateEthernetManualVisibility(value);
675
+ if (!suppressInterfaceSelectionChange) {
676
+ userLockedInterfaceSelection = !!value && value !== 'Auto';
677
+ }
678
+ });
679
+
680
+ $.getJSON('knxUltimateETHInterfaces', (data) => {
681
+ ethernetInterfaces = Array.isArray(data) ? data.slice() : [];
682
+ ethernetInterfaces.sort((a, b) => {
683
+ const nameA = (a && a.name) ? String(a.name) : '';
684
+ const nameB = (b && b.name) ? String(b.name) : '';
685
+ return nameA.localeCompare(nameB);
686
+ });
687
+ $knxEthInterfaceSelect.find('option').filter(function () {
688
+ const val = $(this).val();
689
+ return val !== 'Auto' && val !== 'Manual';
690
+ }).remove();
691
+ ethernetInterfaces.forEach((iFace) => {
692
+ if (!iFace || !iFace.name) return;
693
+ const labelAddress = (typeof iFace.address === 'string' && iFace.address.length > 0) ? iFace.address : '';
694
+ const displayText = labelAddress ? `${iFace.name} (${labelAddress})` : iFace.name;
695
+ const $opt = $("<option></option>")
696
+ .attr("value", iFace.name)
697
+ .text(displayText);
698
+ $opt.data('iface', iFace);
699
+ $knxEthInterfaceSelect.append($opt);
700
+ });
701
+ const initialValue = (typeof node.KNXEthInterface === "undefined") ? "Auto" : node.KNXEthInterface;
702
+ userLockedInterfaceSelection = initialValue !== 'Auto';
703
+ setEthernetInterfaceSelection(initialValue, true);
704
+ if (!userLockedInterfaceSelection) {
705
+ autoSelectEthernetInterfaceForHost({ force: true });
706
+ }
589
707
  });
590
708
 
591
709
  // Abilita manualmente la lista dei protocolli quando l'utente modifica l'IP a mano
@@ -633,6 +751,7 @@
633
751
  if (octets && !isMulticastIPv4(octets)) enforceProtocolFromIP();
634
752
  }
635
753
  } catch (e) { }
754
+ autoSelectEthernetInterfaceForHost();
636
755
  });
637
756
  } catch (e) { }
638
757
 
@@ -767,7 +886,7 @@
767
886
  margin-bottom: 6px;
768
887
  }
769
888
 
770
- #knxUltimate-config-template .form-row > label {
889
+ #knxUltimate-config-template .form-row>label {
771
890
  flex: 0 0 230px;
772
891
  display: flex;
773
892
  align-items: center;
@@ -775,9 +894,9 @@
775
894
  margin: 0;
776
895
  }
777
896
 
778
- #knxUltimate-config-template .form-row > label + input,
779
- #knxUltimate-config-template .form-row > label + select,
780
- #knxUltimate-config-template .form-row > label + textarea {
897
+ #knxUltimate-config-template .form-row>label+input,
898
+ #knxUltimate-config-template .form-row>label+select,
899
+ #knxUltimate-config-template .form-row>label+textarea {
781
900
  flex: 1 1 auto;
782
901
  }
783
902
 
@@ -803,6 +922,27 @@
803
922
  margin-left: 230px;
804
923
  }
805
924
 
925
+ #knxUltimate-config-template .form-row.form-row-no-label {
926
+ display: block;
927
+ }
928
+
929
+ #knxUltimate-config-template i.fa.fa-shield {
930
+ color: #0f5d16;
931
+ }
932
+
933
+ #node-config-input-keyringFileXML-editor .monaco-editor .minimap,
934
+ #node-config-input-csv-editor .monaco-editor .minimap {
935
+ display: none !important;
936
+ }
937
+
938
+ #node-config-input-keyringFileXML-editor .ace_gutter {
939
+ display: none !important;
940
+ }
941
+
942
+ #node-config-input-keyringFileXML-editor .ace_scroller {
943
+ left: 0 !important;
944
+ }
945
+
806
946
  #knxUltimate-config-template .form-section-heading {
807
947
  margin-bottom: 12px;
808
948
  }
@@ -856,9 +996,9 @@
856
996
  <div id="SecureKNX" style="margin: 5px 5px 5px 5px;" >
857
997
  <p>
858
998
  <div class="form-row">
859
- <i class="fa fa-youtube"></i>
999
+ <i style="color:red" class="fa fa-youtube"></i>
860
1000
  <a href="https://youtu.be/OpR7ZQTlMRU" target="_blank">
861
- <span data-i18n="knxUltimate-config.ets.youtubeKeyring"></span>
1001
+ <span data-i18n="knxUltimate-config.ets.youtubeKeyring"></span>
862
1002
  </a>
863
1003
  </div>
864
1004
  <div class="form-row">
@@ -870,8 +1010,7 @@
870
1010
  </select>
871
1011
  </div>
872
1012
  <div id="secureKeyringFields">
873
- <div class="form-row">
874
- <label for="node-config-input-keyringFileXML"><span data-i18n="knxUltimate-config.ets.keyring_file"></span></label>
1013
+ <div class="form-row form-row-no-label">
875
1014
  <div id="node-config-input-keyringFileXML-editor" class="node-text-editor" style="height:200px; min-height:140px; width:100%"></div>
876
1015
  <input type="hidden" id="node-config-input-keyringFileXML" data-i18n="[placeholder]knxUltimate-config.ets.keyring" />
877
1016
  </div>
@@ -1016,9 +1155,6 @@
1016
1155
  <div class="form-row">
1017
1156
  <span data-i18n="knxUltimate-config.ets.description"></span>
1018
1157
  </div>
1019
- <div class="form-row">
1020
- <span style="color:red" data-i18n="[html]knxUltimate-config.ets.instruction"></span>
1021
- </div>
1022
1158
  <div class="form-row">
1023
1159
  <span style="color:red" data-i18n="[html]knxUltimate-config.ets.youtube"></span>
1024
1160
  </div>
@@ -6,6 +6,7 @@
6
6
  const fs = require("fs");
7
7
  const path = require("path");
8
8
  const net = require("net");
9
+ const os = require("os");
9
10
  const _ = require("lodash");
10
11
  const knx = require("knxultimate");
11
12
  // 2025-09: Use KNXUltimate built-in keyring for KNX Secure validation
@@ -61,6 +62,128 @@ const toConcattedSubtypes = (acc, baseType) => {
61
62
  };
62
63
  // ####################
63
64
 
65
+ const BIT_COUNT_TABLE = Array.from({ length: 256 }, (_, value) => {
66
+ let count = 0;
67
+ let temp = value;
68
+ while (temp) {
69
+ temp &= temp - 1;
70
+ count++;
71
+ }
72
+ return count;
73
+ });
74
+
75
+ const parseIPv4Address = (str) => {
76
+ if (typeof str !== "string") return null;
77
+ const parts = str.trim().split('.');
78
+ if (parts.length !== 4) return null;
79
+ const octets = [];
80
+ for (let i = 0; i < parts.length; i++) {
81
+ const part = parts[i];
82
+ if (!/^\d+$/.test(part)) return null;
83
+ const value = Number(part);
84
+ if (!Number.isInteger(value) || value < 0 || value > 255) return null;
85
+ octets.push(value);
86
+ }
87
+ return octets;
88
+ };
89
+
90
+ const countNetmaskBits = (maskOctets) => {
91
+ if (!Array.isArray(maskOctets) || maskOctets.length !== 4) return 0;
92
+ let total = 0;
93
+ for (let i = 0; i < 4; i++) {
94
+ const oct = maskOctets[i];
95
+ if (!Number.isInteger(oct) || oct < 0 || oct > 255) return 0;
96
+ total += BIT_COUNT_TABLE[oct];
97
+ }
98
+ return total;
99
+ };
100
+
101
+ const computeIPv4NetworkKey = (ipOctets, maskOctets) => {
102
+ if (!Array.isArray(ipOctets) || ipOctets.length !== 4 || !Array.isArray(maskOctets) || maskOctets.length !== 4) return null;
103
+ const result = [];
104
+ for (let i = 0; i < 4; i++) {
105
+ const ipVal = ipOctets[i];
106
+ const maskVal = maskOctets[i];
107
+ if (!Number.isInteger(ipVal) || ipVal < 0 || ipVal > 255 || !Number.isInteger(maskVal) || maskVal < 0 || maskVal > 255) return null;
108
+ result.push(ipVal & maskVal);
109
+ }
110
+ return result.join('.');
111
+ };
112
+
113
+ const buildNetmaskOctetsFromPrefix = (prefix) => {
114
+ if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return null;
115
+ const octets = [0, 0, 0, 0];
116
+ let remaining = prefix;
117
+ for (let i = 0; i < 4; i++) {
118
+ const bits = Math.max(0, Math.min(remaining, 8));
119
+ octets[i] = bits === 0 ? 0 : ((0xff << (8 - bits)) & 0xff);
120
+ remaining -= bits;
121
+ }
122
+ return octets;
123
+ };
124
+
125
+ const deriveNetmaskOctets = (iface) => {
126
+ if (!iface) return null;
127
+ const netmask = typeof iface.netmask === "string" && iface.netmask.trim() !== "" ? iface.netmask : null;
128
+ if (netmask) {
129
+ const octets = parseIPv4Address(netmask);
130
+ if (octets) return octets;
131
+ }
132
+ if (typeof iface.cidr === "string" && iface.cidr.includes('/')) {
133
+ const parts = iface.cidr.split('/');
134
+ if (parts.length === 2) {
135
+ const prefix = Number(parts[1]);
136
+ const octets = buildNetmaskOctetsFromPrefix(prefix);
137
+ if (octets) return octets;
138
+ }
139
+ }
140
+ return null;
141
+ };
142
+
143
+ const isMulticastIPv4 = (octets) => {
144
+ if (!Array.isArray(octets) || octets.length !== 4) return false;
145
+ const first = octets[0];
146
+ return first >= 224 && first <= 239;
147
+ };
148
+
149
+ const findAutoEthernetInterface = (targetIP) => {
150
+ const targetOctets = parseIPv4Address(targetIP);
151
+ if (!targetOctets || isMulticastIPv4(targetOctets)) return null;
152
+ const interfaces = os.networkInterfaces();
153
+ if (!interfaces || typeof interfaces !== "object") return null;
154
+
155
+ let bestMatch = null;
156
+ let bestMaskBits = -1;
157
+
158
+ Object.keys(interfaces).forEach((ifname) => {
159
+ const entries = Array.isArray(interfaces[ifname]) ? interfaces[ifname] : [];
160
+ entries.forEach((entry) => {
161
+ if (!entry || entry.internal) return;
162
+ const family = entry.family === 'IPv4' || entry.family === 4;
163
+ if (!family) return;
164
+ const ifaceOctets = parseIPv4Address(entry.address);
165
+ if (!ifaceOctets) return;
166
+ const maskOctets = deriveNetmaskOctets(entry);
167
+ if (!maskOctets) return;
168
+ const ifaceNetwork = computeIPv4NetworkKey(ifaceOctets, maskOctets);
169
+ const targetNetwork = computeIPv4NetworkKey(targetOctets, maskOctets);
170
+ if (!ifaceNetwork || !targetNetwork || ifaceNetwork !== targetNetwork) return;
171
+ const maskBits = countNetmaskBits(maskOctets);
172
+ if (maskBits > bestMaskBits) {
173
+ bestMaskBits = maskBits;
174
+ bestMatch = {
175
+ name: ifname,
176
+ address: entry.address,
177
+ netmask: maskOctets.join('.'),
178
+ maskBits,
179
+ };
180
+ }
181
+ });
182
+ });
183
+
184
+ return bestMatch;
185
+ };
186
+
64
187
 
65
188
  module.exports = (RED) => {
66
189
  function knxUltimateConfigNode(config) {
@@ -220,6 +343,141 @@ module.exports = (RED) => {
220
343
  try {
221
344
  const kr = new Keyring();
222
345
  await kr.load(node.keyringFileXML, node.credentials.keyringFilePassword);
346
+ if (node.loglevel === "debug") {
347
+ try {
348
+ const toIAString = (value) => {
349
+ if (!value) return "";
350
+ return typeof value.toString === "function" ? value.toString() : String(value);
351
+ };
352
+ const toBufferString = (value) => {
353
+ if (!value) return "";
354
+ if (Buffer.isBuffer(value)) return value.toString("hex");
355
+ return String(value);
356
+ };
357
+ const interfaceMap = kr.getInterfaces?.();
358
+ const interfaces = Array.from(interfaceMap ? interfaceMap.values() : []).map((iface) => ({
359
+ type: iface.type || "",
360
+ individualAddress: toIAString(iface.individualAddress),
361
+ host: toIAString(iface.host),
362
+ userId: typeof iface.userId === "number" ? iface.userId : "",
363
+ password: iface.password || "",
364
+ decryptedPassword: iface.decryptedPassword || "",
365
+ authentication: iface.authentication || "",
366
+ decryptedAuthentication: iface.decryptedAuthentication || "",
367
+ groupAddresses: Array.from(iface.groupAddresses ? iface.groupAddresses.entries() : []).map(([ga, senders]) => ({
368
+ address: ga,
369
+ senders: Array.isArray(senders) ? senders.map(toIAString) : [],
370
+ })),
371
+ }));
372
+ const backbones = (kr.getBackbones?.() || []).map((backbone) => ({
373
+ multicastAddress: backbone.multicastAddress || "",
374
+ latency: typeof backbone.latency === "number" ? backbone.latency : "",
375
+ key: backbone.key || "",
376
+ decryptedKey: toBufferString(backbone.decryptedKey),
377
+ }));
378
+ const groupAddressMap = kr.getGroupAddresses?.();
379
+ const groupAddresses = Array.from(groupAddressMap ? groupAddressMap.values() : []).map((group) => ({
380
+ address: toIAString(group.address),
381
+ key: group.key || "",
382
+ decryptedKey: toBufferString(group.decryptedKey),
383
+ }));
384
+ const deviceMap = kr.getDevices?.();
385
+ const devices = Array.from(deviceMap ? deviceMap.values() : []).map((device) => ({
386
+ individualAddress: toIAString(device.individualAddress),
387
+ toolKey: device.toolKey || "",
388
+ decryptedToolKey: toBufferString(device.decryptedToolKey),
389
+ managementPassword: device.managementPassword || "",
390
+ decryptedManagementPassword: device.decryptedManagementPassword || "",
391
+ authentication: device.authentication || "",
392
+ decryptedAuthentication: device.decryptedAuthentication || "",
393
+ sequenceNumber: typeof device.sequenceNumber === "number" ? device.sequenceNumber : "",
394
+ serialNumber: device.serialNumber || "",
395
+ }));
396
+ const lines = [];
397
+ lines.push("================ KNX Secure keyring debug dump ================");
398
+ lines.push(`Node: ${node.name || node.id || ""}`);
399
+ lines.push(`Created By: ${kr.getCreatedBy?.() || ""}`);
400
+ lines.push(`Created On: ${kr.getCreated?.() || ""}`);
401
+ lines.push(`Password (node credentials): ${node.credentials?.keyringFilePassword || ""}`);
402
+ lines.push("");
403
+
404
+ lines.push("Interfaces:");
405
+ if (interfaces.length === 0) {
406
+ lines.push(" (none)");
407
+ } else {
408
+ interfaces.forEach((iface, idx) => {
409
+ lines.push(` [${idx + 1}] ${iface.individualAddress || "(unknown)"} (${iface.type || ""})`);
410
+ lines.push(` Host: ${iface.host || ""}`);
411
+ lines.push(` User ID: ${iface.userId === "" ? "" : iface.userId}`);
412
+ lines.push(` Password (encoded): ${iface.password || ""}`);
413
+ lines.push(` Password (decoded): ${iface.decryptedPassword || ""}`);
414
+ lines.push(` Authentication (encoded): ${iface.authentication || ""}`);
415
+ lines.push(` Authentication (decoded): ${iface.decryptedAuthentication || ""}`);
416
+ if (!iface.groupAddresses || iface.groupAddresses.length === 0) {
417
+ lines.push(" Group Addresses: (none)");
418
+ } else {
419
+ lines.push(" Group Addresses:");
420
+ iface.groupAddresses.forEach((ga) => {
421
+ const senders = ga.senders && ga.senders.length > 0 ? ga.senders.join(", ") : "(none)";
422
+ lines.push(` - ${ga.address}: senders ${senders}`);
423
+ });
424
+ }
425
+ lines.push("");
426
+ });
427
+ }
428
+
429
+ lines.push("Backbones:");
430
+ if (backbones.length === 0) {
431
+ lines.push(" (none)");
432
+ } else {
433
+ backbones.forEach((backbone, idx) => {
434
+ lines.push(` [${idx + 1}] Multicast: ${backbone.multicastAddress || ""}`);
435
+ lines.push(` Latency: ${backbone.latency === "" ? "" : backbone.latency}`);
436
+ lines.push(` Key (encoded): ${backbone.key || ""}`);
437
+ lines.push(` Key (decoded hex): ${backbone.decryptedKey || ""}`);
438
+ lines.push("");
439
+ });
440
+ }
441
+
442
+ lines.push("Group Addresses:");
443
+ if (groupAddresses.length === 0) {
444
+ lines.push(" (none)");
445
+ } else {
446
+ groupAddresses.forEach((group, idx) => {
447
+ lines.push(` [${idx + 1}] ${group.address || ""}`);
448
+ lines.push(` Key (encoded): ${group.key || ""}`);
449
+ lines.push(` Key (decoded hex): ${group.decryptedKey || ""}`);
450
+ lines.push("");
451
+ });
452
+ }
453
+
454
+ lines.push("Devices:");
455
+ if (devices.length === 0) {
456
+ lines.push(" (none)");
457
+ } else {
458
+ devices.forEach((device, idx) => {
459
+ lines.push(` [${idx + 1}] ${device.individualAddress || ""}`);
460
+ lines.push(` Tool Key (encoded): ${device.toolKey || ""}`);
461
+ lines.push(` Tool Key (decoded hex): ${device.decryptedToolKey || ""}`);
462
+ lines.push(` Management Password (encoded): ${device.managementPassword || ""}`);
463
+ lines.push(` Management Password (decoded): ${device.decryptedManagementPassword || ""}`);
464
+ lines.push(` Authentication (encoded): ${device.authentication || ""}`);
465
+ lines.push(` Authentication (decoded): ${device.decryptedAuthentication || ""}`);
466
+ lines.push(` Sequence Number: ${device.sequenceNumber === "" ? "" : device.sequenceNumber}`);
467
+ lines.push(` Serial Number: ${device.serialNumber || ""}`);
468
+ lines.push("");
469
+ });
470
+ }
471
+
472
+ lines.push("Raw keyring (XML/base64 as provided):");
473
+ lines.push(node.keyringFileXML || "(empty)");
474
+ lines.push("================ End of keyring debug dump ================");
475
+
476
+ node.sysLogger?.debug(lines.join("\n"));
477
+ } catch (dumpError) {
478
+ node.sysLogger?.error("KNX Secure: unable to log keyring details: " + dumpError.message);
479
+ }
480
+ }
223
481
  const createdBy = kr.getCreatedBy?.() || "unknown";
224
482
  const created = kr.getCreated?.() || "unknown";
225
483
  RED.log.info(`KNX-Secure: Keyring validated (Created by ${createdBy} on ${created}) using node ${node.name || node.id}`);
@@ -647,11 +905,30 @@ module.exports = (RED) => {
647
905
  }
648
906
  node.knxConnectionProperties.interface = sIfaceName;
649
907
  } else {
650
- // 08/10/2021 Delete the interface
908
+ // Remove any manual binding and try to auto-select based on subnet
651
909
  try {
652
910
  delete node.knxConnectionProperties.interface;
653
911
  } catch (error) { }
654
- node.sysLogger?.info("Bind KNX Bus to interface (Auto). Node " + node.name);
912
+ const targetIP = node.knxConnectionProperties?.ipAddr || node.host;
913
+ const autoInterface = typeof targetIP === "string" ? findAutoEthernetInterface(targetIP) : null;
914
+ if (autoInterface && autoInterface.name) {
915
+ node.knxConnectionProperties.interface = autoInterface.name;
916
+ const maskInfo = autoInterface.netmask
917
+ ? autoInterface.netmask + (autoInterface.maskBits ? " (" + autoInterface.maskBits + ")" : "")
918
+ : (autoInterface.maskBits ? String(autoInterface.maskBits) : "mask unknown");
919
+ node.sysLogger?.info(
920
+ "Bind KNX Bus to interface (Auto) -> " +
921
+ autoInterface.name +
922
+ " (" + autoInterface.address + " " + maskInfo + ")" +
923
+ ". Node " + node.name,
924
+ );
925
+ } else {
926
+ node.sysLogger?.info(
927
+ "Bind KNX Bus to interface (Auto). Node " +
928
+ node.name +
929
+ (targetIP ? " - no matching local interface found for " + targetIP + "." : "."),
930
+ );
931
+ }
655
932
  }
656
933
  };
657
934
  // node.setKnxConnectionProperties(); 28/12/2021 Commented
@@ -121,7 +121,8 @@ 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 || 'error', setPrefix: node.type + " <" + (node.name || node.id || '') + ">" });
124
+ const baseLogLevel = (node.serverKNX && node.serverKNX.loglevel) ? node.serverKNX.loglevel : 'error';
125
+ node.sysLogger = new loggerClass({ loglevel: baseLogLevel, setPrefix: node.type + " <" + (node.name || node.id || '') + ">" });
125
126
  } catch (error) { console.log(error.stack) }
126
127
  node.sendMsgToKNXCode = config.sendMsgToKNXCode || undefined;
127
128
  node.receiveMsgFromKNXCode = config.receiveMsgFromKNXCode || undefined;