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.
- package/CHANGELOG.md +4 -0
- package/nodes/commonFunctions.js +15 -15
- package/nodes/knxUltimate-config.html +175 -39
- package/nodes/knxUltimate-config.js +279 -2
- package/nodes/knxUltimate.js +2 -1
- package/nodes/knxUltimateAutoResponder.js +2 -1
- package/nodes/knxUltimateHueLight.html +6 -12
- package/nodes/knxUltimateHueLight.js +219 -217
- package/nodes/knxUltimateHueScene.js +2 -1
- package/nodes/knxUltimateLoadControl.js +2 -1
- package/nodes/knxUltimateSceneController.js +2 -1
- package/nodes/locales/de/knxUltimate-config.json +0 -1
- package/nodes/locales/en-US/knxUltimate-config.json +0 -1
- package/nodes/locales/it/knxUltimate-config.json +0 -1
- package/nodes/locales/zh-CN/knxUltimate-config.json +0 -1
- package/package.json +2 -2
- package/tutorial/hue-config-teleprompter.txt +43 -0
- package/tutorial/hue-config.md +44 -0
- package/tutorial/knxUltimate-config-teleprompter.txt +87 -0
- package/tutorial/knxUltimate-config.md +65 -0
- package/tutorial/knxUltimate-teleprompter.txt +60 -0
- package/tutorial/knxUltimate.md +59 -0
- package/tutorial/knxUltimateAlerter-teleprompter.txt +48 -0
- package/tutorial/knxUltimateAlerter.md +49 -0
- package/tutorial/knxUltimateAutoResponder-teleprompter.txt +43 -0
- package/tutorial/knxUltimateAutoResponder.md +46 -0
- package/tutorial/knxUltimateGlobalContext-teleprompter.txt +44 -0
- package/tutorial/knxUltimateGlobalContext.md +44 -0
- package/tutorial/knxUltimateHATranslator-teleprompter.txt +45 -0
- package/tutorial/knxUltimateHATranslator.md +43 -0
- package/tutorial/knxUltimateHueBattery-teleprompter.txt +38 -0
- package/tutorial/knxUltimateHueBattery.md +40 -0
- package/tutorial/knxUltimateHueButton-teleprompter.txt +45 -0
- package/tutorial/knxUltimateHueButton.md +54 -0
- package/tutorial/knxUltimateHueContactSensor-teleprompter.txt +35 -0
- package/tutorial/knxUltimateHueContactSensor.md +45 -0
- package/tutorial/knxUltimateHueLight-teleprompter.txt +50 -0
- package/tutorial/knxUltimateHueLight.md +66 -0
- package/tutorial/knxUltimateHueLightSensor-teleprompter.txt +42 -0
- package/tutorial/knxUltimateHueLightSensor.md +44 -0
- package/tutorial/knxUltimateHueMotion-teleprompter.txt +39 -0
- package/tutorial/knxUltimateHueMotion.md +40 -0
- package/tutorial/knxUltimateHueScene-teleprompter.txt +45 -0
- package/tutorial/knxUltimateHueScene.md +52 -0
- package/tutorial/knxUltimateHueTapDial-teleprompter.txt +40 -0
- package/tutorial/knxUltimateHueTapDial.md +40 -0
- package/tutorial/knxUltimateHueTemperatureSensor-teleprompter.txt +42 -0
- package/tutorial/knxUltimateHueTemperatureSensor.md +43 -0
- package/tutorial/knxUltimateHueZigbeeConnectivity-teleprompter.txt +41 -0
- package/tutorial/knxUltimateHueZigbeeConnectivity.md +43 -0
- package/tutorial/knxUltimateHuedevice_software_update-teleprompter.txt +42 -0
- package/tutorial/knxUltimateHuedevice_software_update.md +44 -0
- package/tutorial/knxUltimateLoadControl-teleprompter.txt +40 -0
- package/tutorial/knxUltimateLoadControl.md +52 -0
- package/tutorial/knxUltimateLogger-teleprompter.txt +39 -0
- package/tutorial/knxUltimateLogger.md +42 -0
- package/tutorial/knxUltimateSceneController-teleprompter.txt +38 -0
- package/tutorial/knxUltimateSceneController.md +48 -0
- package/tutorial/knxUltimateViewer-teleprompter.txt +37 -0
- package/tutorial/knxUltimateViewer.md +41 -0
- package/tutorial/knxUltimateWatchDog-teleprompter.txt +39 -0
- package/tutorial/knxUltimateWatchDog.md +42 -0
- package/http2Testing/myhttp2.js +0 -103
- package/http2Testing/ratelimiter.js +0 -24
- package/http2Testing/ratelimitertestbed.js +0 -7
- 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/>
|
package/nodes/commonFunctions.js
CHANGED
|
@@ -299,21 +299,21 @@ module.exports = (RED) => {
|
|
|
299
299
|
try {
|
|
300
300
|
const oiFaces = oOS.networkInterfaces();
|
|
301
301
|
Object.keys(oiFaces).forEach((ifname) => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
$
|
|
604
|
+
$knxEthInterfaceManualRow.hide();
|
|
578
605
|
}
|
|
606
|
+
}
|
|
579
607
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
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
|
|
779
|
-
#knxUltimate-config-template .form-row
|
|
780
|
-
#knxUltimate-config-template .form-row
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
package/nodes/knxUltimate.js
CHANGED
|
@@ -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
|
-
|
|
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;
|