node-red-contrib-knx-ultimate 4.0.3 → 4.0.5
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 +6 -0
- package/README.md +2 -4
- package/nodes/knxUltimate-config.html +170 -9
- package/nodes/knxUltimate-config.js +80 -27
- package/nodes/locales/de/knxUltimate-config.json +8 -0
- package/nodes/locales/de-DE/knxUltimate-config.html +8 -0
- package/nodes/locales/en-US/knxUltimate-config.html +8 -0
- package/nodes/locales/en-US/knxUltimate-config.json +8 -0
- package/nodes/locales/it/knxUltimate-config.json +8 -0
- package/nodes/locales/it-IT/knxUltimate-config.html +8 -0
- package/nodes/locales/zh-CN/knxUltimate-config.html +8 -0
- package/nodes/locales/zh-CN/knxUltimate-config.json +8 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 4.0.4** - September 2025<br/>
|
|
10
|
+
- NEW: You have now the choide to select multiple modes to authenticate to a KNX Gateway.<br/>
|
|
11
|
+
|
|
12
|
+
**Version 4.0.4** - September 2025<br/>
|
|
13
|
+
- NEW: you can now login to your secure gateway using only tunnel and password, without the keyring file.<br/>
|
|
14
|
+
|
|
9
15
|
**Version 4.0.3** - September 2025<br/>
|
|
10
16
|
- GlobalContextNode: Added "lastupdate" property.<br/>
|
|
11
17
|
- Device Node. Refractor snippet list and UI.<br/>
|
package/README.md
CHANGED
|
@@ -86,10 +86,8 @@ Please subscribe to the [Youtube channel](https://www.youtube.com/playlist?list=
|
|
|
86
86
|
|--|--|
|
|
87
87
|
| KNX Tunnelling |  |
|
|
88
88
|
| KNX Routing |  |
|
|
89
|
+
| KNX IP Secure/Data secure |  |
|
|
89
90
|
| Philips Hue v2 |  |
|
|
90
|
-
| KNX Secure Tunnelling |  |
|
|
91
|
-
| KNX Secure Routing |  |
|
|
92
|
-
|
|
93
91
|
|
|
94
92
|
<br/>
|
|
95
93
|
|
|
@@ -97,7 +95,7 @@ Please subscribe to the [Youtube channel](https://www.youtube.com/playlist?list=
|
|
|
97
95
|
## DOCUMENTATION
|
|
98
96
|
|
|
99
97
|
* [Wiki and Help](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki)
|
|
100
|
-
* [FAQ + Troubleshoot](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/
|
|
98
|
+
* [FAQ + Troubleshoot](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/FAQ-Troubleshoot)
|
|
101
99
|
* [Security best practices](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/SECURITY)
|
|
102
100
|
|
|
103
101
|
|
|
@@ -21,8 +21,12 @@
|
|
|
21
21
|
ignoreTelegramsWithRepeatedFlag: { value: false, required: false },
|
|
22
22
|
keyringFileXML: { value: "" },
|
|
23
23
|
knxSecureSelected: { value: false },
|
|
24
|
+
secureCredentialsMode: { value: "keyring" },
|
|
24
25
|
tunnelIASelection: { value: "Auto" },
|
|
25
26
|
tunnelIA: { value: "" },
|
|
27
|
+
tunnelInterfaceIndividualAddress: { value: "" },
|
|
28
|
+
tunnelUserPassword: { value: "" },
|
|
29
|
+
tunnelUserId: { value: "" },
|
|
26
30
|
autoReconnect: { value: "yes" }
|
|
27
31
|
},
|
|
28
32
|
credentials: {
|
|
@@ -68,13 +72,13 @@
|
|
|
68
72
|
active: (node.knxSecureSelected === true || node.knxSecureSelected === 'true') ? 1 : 0,
|
|
69
73
|
activate: function (event, ui) {
|
|
70
74
|
node.knxSecureSelected = $(ui.newTab).index() === 1;
|
|
71
|
-
try { updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); } catch (e) { }
|
|
75
|
+
try { updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); } catch (e) { }
|
|
72
76
|
}
|
|
73
77
|
});
|
|
74
78
|
// Ensure the correct tab is enforced after all UI init
|
|
75
79
|
try {
|
|
76
80
|
const initialActive = (node.knxSecureSelected === true || node.knxSecureSelected === 'true') ? 1 : 0;
|
|
77
|
-
setTimeout(() => { $("#tabsMain").tabs("option", "active", initialActive); updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); }, 0);
|
|
81
|
+
setTimeout(() => { $("#tabsMain").tabs("option", "active", initialActive); updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); }, 0);
|
|
78
82
|
} catch (e) { }
|
|
79
83
|
|
|
80
84
|
// Keyring ACE editor setup
|
|
@@ -98,6 +102,96 @@
|
|
|
98
102
|
try { node._csvEditor.getSession().setUseWrapMode(true); } catch (e) { }
|
|
99
103
|
} catch (e) { }
|
|
100
104
|
|
|
105
|
+
function getSecureCredentialsMode() {
|
|
106
|
+
try {
|
|
107
|
+
const sel = $("#node-config-input-secureCredentialsMode").val();
|
|
108
|
+
return (typeof sel === 'string' && sel.length > 0) ? sel : 'keyring';
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return 'keyring';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isSecureTabActive() {
|
|
115
|
+
try {
|
|
116
|
+
return $("#tabsMain").tabs("option", "active") === 1;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return !!(typeof node.knxSecureSelected !== 'undefined' ? node.knxSecureSelected : false);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseIPv4Address(str) {
|
|
123
|
+
if (typeof str !== 'string') return null;
|
|
124
|
+
const parts = str.trim().split('.');
|
|
125
|
+
if (parts.length !== 4) return null;
|
|
126
|
+
const octets = [];
|
|
127
|
+
for (let i = 0; i < parts.length; i++) {
|
|
128
|
+
const part = parts[i];
|
|
129
|
+
if (!/^\d+$/.test(part)) return null;
|
|
130
|
+
const value = Number(part);
|
|
131
|
+
if (value < 0 || value > 255) return null;
|
|
132
|
+
octets.push(value);
|
|
133
|
+
}
|
|
134
|
+
return octets;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isMulticastIPv4(octets) {
|
|
138
|
+
if (!Array.isArray(octets) || octets.length !== 4) return false;
|
|
139
|
+
const first = octets[0];
|
|
140
|
+
return first >= 224 && first <= 239;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeTunnelIAValue(value) {
|
|
144
|
+
if (typeof value !== 'string') return '';
|
|
145
|
+
const trimmed = value.trim();
|
|
146
|
+
return (trimmed && trimmed !== 'undefined') ? trimmed : '';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setTunnelIAValue(value) {
|
|
150
|
+
const normalized = normalizeTunnelIAValue(value || '');
|
|
151
|
+
try { $("#node-config-input-tunnelIA").val(normalized); } catch (e) { }
|
|
152
|
+
try { $("#node-config-input-tunnelInterfaceIndividualAddress").val(normalized); } catch (e) { }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function syncTunnelIAFromManual() {
|
|
156
|
+
try { setTunnelIAValue($("#node-config-input-tunnelInterfaceIndividualAddress").val()); } catch (e) { }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function syncTunnelIAFromKeyring() {
|
|
160
|
+
try { setTunnelIAValue($("#node-config-input-tunnelIA").val()); } catch (e) { }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function enforceProtocolFromIP() {
|
|
164
|
+
try {
|
|
165
|
+
const ipTyped = String($("#node-config-input-host").val() || '').trim();
|
|
166
|
+
if (!ipTyped) return;
|
|
167
|
+
const octets = parseIPv4Address(ipTyped);
|
|
168
|
+
if (!octets || isMulticastIPv4(octets)) return;
|
|
169
|
+
const autoProto = isSecureTabActive() ? 'TunnelTCP' : 'TunnelUDP';
|
|
170
|
+
const $sel = $("#node-config-input-hostProtocol");
|
|
171
|
+
if ($sel.val() !== autoProto) {
|
|
172
|
+
$sel.val(autoProto);
|
|
173
|
+
updateTunnelIAVisibility();
|
|
174
|
+
updateSecureMulticastHint();
|
|
175
|
+
updatePhysAddrVisibility();
|
|
176
|
+
}
|
|
177
|
+
} catch (e) { }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function updateSecureCredentialMode() {
|
|
181
|
+
try {
|
|
182
|
+
const mode = getSecureCredentialsMode();
|
|
183
|
+
const showKeyringFields = (mode === 'keyring' || mode === 'combined');
|
|
184
|
+
const showKeyringIAControls = (mode === 'keyring');
|
|
185
|
+
$("#secureKeyringFields").toggle(showKeyringFields);
|
|
186
|
+
if (!showKeyringIAControls) {
|
|
187
|
+
$("#rowTunnelIASelection").hide();
|
|
188
|
+
$("#divTunnelIA").hide();
|
|
189
|
+
}
|
|
190
|
+
} catch (e) { }
|
|
191
|
+
updateTunnelIAVisibility();
|
|
192
|
+
enforceProtocolFromIP();
|
|
193
|
+
}
|
|
194
|
+
|
|
101
195
|
// Helper: show/hide Tunnel IA controls depending on mode
|
|
102
196
|
function updateTunnelIAVisibility() {
|
|
103
197
|
try {
|
|
@@ -105,7 +199,10 @@
|
|
|
105
199
|
const hostProtocol = $("#node-config-input-hostProtocol").val();
|
|
106
200
|
const ip = $("#node-config-input-host").val();
|
|
107
201
|
const isMulticast = (hostProtocol === 'Multicast') || (ip === '224.0.23.12');
|
|
108
|
-
const
|
|
202
|
+
const mode = getSecureCredentialsMode();
|
|
203
|
+
const keyringMode = (mode === 'keyring');
|
|
204
|
+
const manualMode = (mode === 'manual' || mode === 'combined');
|
|
205
|
+
const showTunnelIA = isSecure && !isMulticast && keyringMode;
|
|
109
206
|
if (showTunnelIA) {
|
|
110
207
|
$("#rowTunnelIASelection").show();
|
|
111
208
|
if ($("#node-config-input-tunnelIASelection").val() === 'Manual') {
|
|
@@ -117,6 +214,15 @@
|
|
|
117
214
|
$("#rowTunnelIASelection").hide();
|
|
118
215
|
$("#divTunnelIA").hide();
|
|
119
216
|
}
|
|
217
|
+
const showManualTunnelFields = isSecure && !isMulticast && manualMode;
|
|
218
|
+
$("#rowTunnelInterfaceIndividualAddress").toggle(showManualTunnelFields);
|
|
219
|
+
$("#rowTunnelUserPassword").toggle(showManualTunnelFields);
|
|
220
|
+
$("#rowTunnelUserId").toggle(showManualTunnelFields);
|
|
221
|
+
if (!showManualTunnelFields) {
|
|
222
|
+
try {
|
|
223
|
+
$("#node-config-input-tunnelInterfaceIndividualAddress").removeClass('input-error');
|
|
224
|
+
} catch (e) { }
|
|
225
|
+
}
|
|
120
226
|
} catch (e) { }
|
|
121
227
|
}
|
|
122
228
|
// Helper: ensure Physical Address row is visible (required also in multicast)
|
|
@@ -203,6 +309,18 @@
|
|
|
203
309
|
let aTunnelIAs = [];
|
|
204
310
|
function populateTunnelIAList() {
|
|
205
311
|
try {
|
|
312
|
+
const mode = getSecureCredentialsMode();
|
|
313
|
+
const keyringMode = (mode === 'keyring');
|
|
314
|
+
if (!keyringMode) {
|
|
315
|
+
aTunnelIAs = [];
|
|
316
|
+
try {
|
|
317
|
+
const $ia = $("#node-config-input-tunnelIA");
|
|
318
|
+
if ($ia.data('ui-autocomplete')) {
|
|
319
|
+
$ia.autocomplete('option', 'source', aTunnelIAs);
|
|
320
|
+
}
|
|
321
|
+
} catch (e) { }
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
206
324
|
const keyring = $("#node-config-input-keyringFileXML").val();
|
|
207
325
|
const pwd = $("#node-config-input-keyringFilePassword").val();
|
|
208
326
|
const params = { serverId: node.id };
|
|
@@ -231,8 +349,14 @@
|
|
|
231
349
|
|
|
232
350
|
// Secure: Tunnel IA selection
|
|
233
351
|
try {
|
|
352
|
+
const storedTunnelIA = (() => {
|
|
353
|
+
const manualStored = normalizeTunnelIAValue(node.tunnelInterfaceIndividualAddress);
|
|
354
|
+
const keyringStored = normalizeTunnelIAValue(node.tunnelIA);
|
|
355
|
+
return manualStored || keyringStored;
|
|
356
|
+
})();
|
|
357
|
+
setTunnelIAValue(storedTunnelIA);
|
|
234
358
|
$("#node-config-input-tunnelIASelection").val(typeof node.tunnelIASelection === 'undefined' ? 'Auto' : node.tunnelIASelection);
|
|
235
|
-
$("#node-config-input-tunnelIA").
|
|
359
|
+
$("#node-config-input-tunnelIA").on('change input', syncTunnelIAFromKeyring);
|
|
236
360
|
const toggleTunnelIA = () => {
|
|
237
361
|
if ($("#node-config-input-tunnelIASelection").val() === 'Manual') {
|
|
238
362
|
$("#divTunnelIA").show();
|
|
@@ -255,8 +379,8 @@
|
|
|
255
379
|
$("#node-config-input-tunnelIA").autocomplete({
|
|
256
380
|
minLength: 0,
|
|
257
381
|
source: function (request, response) { response(aTunnelIAs); },
|
|
258
|
-
focus: function (event, ui) {
|
|
259
|
-
select: function (event, ui) {
|
|
382
|
+
focus: function (event, ui) { setTunnelIAValue(ui.item.value); return false; },
|
|
383
|
+
select: function (event, ui) { setTunnelIAValue(ui.item.value); return false; }
|
|
260
384
|
}).on('focus click', function () {
|
|
261
385
|
// Show dropdown on focus/click
|
|
262
386
|
$(this).autocomplete('search', '');
|
|
@@ -264,6 +388,18 @@
|
|
|
264
388
|
} catch (e) { }
|
|
265
389
|
} catch (error) { }
|
|
266
390
|
|
|
391
|
+
try {
|
|
392
|
+
$("#node-config-input-secureCredentialsMode").val(typeof node.secureCredentialsMode === 'undefined' ? 'keyring' : node.secureCredentialsMode);
|
|
393
|
+
$("#node-config-input-tunnelInterfaceIndividualAddress").on('change input', syncTunnelIAFromManual);
|
|
394
|
+
$("#node-config-input-tunnelUserPassword").val(typeof node.tunnelUserPassword === 'undefined' ? '' : node.tunnelUserPassword);
|
|
395
|
+
$("#node-config-input-tunnelUserId").val(typeof node.tunnelUserId === 'undefined' ? '' : node.tunnelUserId);
|
|
396
|
+
$("#node-config-input-secureCredentialsMode").on('change', function () {
|
|
397
|
+
updateSecureCredentialMode();
|
|
398
|
+
updateSecureMulticastHint();
|
|
399
|
+
});
|
|
400
|
+
updateSecureCredentialMode();
|
|
401
|
+
} catch (error) { }
|
|
402
|
+
|
|
267
403
|
// Autocomplete with KNX IP Interfaces + Refresh capability
|
|
268
404
|
let aKNXInterfaces = [];
|
|
269
405
|
function refreshKNXGateways(openDropdown) {
|
|
@@ -454,9 +590,7 @@
|
|
|
454
590
|
// Abilita manualmente la lista dei protocolli quando l'utente modifica l'IP a mano
|
|
455
591
|
try {
|
|
456
592
|
$("#node-config-input-host").on('input', function () {
|
|
457
|
-
const isSecure = (
|
|
458
|
-
try { return $("#tabsMain").tabs("option", "active") === 1; } catch (e) { return !!node.knxSecureSelected; }
|
|
459
|
-
})();
|
|
593
|
+
const isSecure = isSecureTabActive();
|
|
460
594
|
const $sel = $("#node-config-input-hostProtocol");
|
|
461
595
|
// Abilita select
|
|
462
596
|
$sel.prop('disabled', false);
|
|
@@ -494,6 +628,8 @@
|
|
|
494
628
|
}
|
|
495
629
|
// If discovery knows protocol, preselect it (keep select enabled for manual override)
|
|
496
630
|
if (proto) { $("#node-config-input-hostProtocol").val(proto); }
|
|
631
|
+
const octets = parseIPv4Address(ipTyped);
|
|
632
|
+
if (octets && !isMulticastIPv4(octets)) enforceProtocolFromIP();
|
|
497
633
|
}
|
|
498
634
|
} catch (e) { }
|
|
499
635
|
});
|
|
@@ -560,6 +696,9 @@
|
|
|
560
696
|
node._csvEditor.destroy();
|
|
561
697
|
node._csvEditor = null;
|
|
562
698
|
}
|
|
699
|
+
try {
|
|
700
|
+
$("#node-config-input-tunnelIA").val($("#node-config-input-tunnelInterfaceIndividualAddress").val());
|
|
701
|
+
} catch (e) { }
|
|
563
702
|
} catch (e) { }
|
|
564
703
|
// Check if the csv file contains errors
|
|
565
704
|
if (($("#node-config-input-csv").val() != 'undefined' && $("#node-config-input-csv").val() != "") || ($("#node-config-input-keyring").val() != 'undefined' && $("#node-config-input-keyring").val() != "")) {
|
|
@@ -675,6 +814,15 @@
|
|
|
675
814
|
<span data-i18n="knxUltimate-config.ets.youtubeKeyring"></span>
|
|
676
815
|
</a>
|
|
677
816
|
</div>
|
|
817
|
+
<div class="form-row">
|
|
818
|
+
<label for="node-config-input-secureCredentialsMode"><i class="fa fa-shield"></i> <span data-i18n="knxUltimate-config.ets.secure_credentials_mode"></span></label>
|
|
819
|
+
<select id="node-config-input-secureCredentialsMode" style="width:200px;">
|
|
820
|
+
<option value="keyring" data-i18n="knxUltimate-config.ets.secure_credentials_mode_keyring"></option>
|
|
821
|
+
<option value="manual" data-i18n="knxUltimate-config.ets.secure_credentials_mode_manual"></option>
|
|
822
|
+
<option value="combined" data-i18n="knxUltimate-config.ets.secure_credentials_mode_combined"></option>
|
|
823
|
+
</select>
|
|
824
|
+
</div>
|
|
825
|
+
<div id="secureKeyringFields">
|
|
678
826
|
<div class="form-row">
|
|
679
827
|
<label for="node-config-input-keyringFileXML"><span data-i18n="knxUltimate-config.ets.keyring_file"></span></label>
|
|
680
828
|
<div id="node-config-input-keyringFileXML-editor" class="node-text-editor" style="height:200px; min-height:140px; width:100%"></div>
|
|
@@ -695,6 +843,19 @@
|
|
|
695
843
|
<label for="node-config-input-tunnelIA"><i class="fa fa-map-marker"></i> <span data-i18n="knxUltimate-config.properties.tunnel_ia_input_label"></span></label>
|
|
696
844
|
<input type="text" id="node-config-input-tunnelIA" style="width:150px;" data-i18n="[placeholder]knxUltimate-config.properties.tunnel_ia_placeholder"/>
|
|
697
845
|
</div>
|
|
846
|
+
</div>
|
|
847
|
+
<div class="form-row" id="rowTunnelInterfaceIndividualAddress" style="display:none;">
|
|
848
|
+
<label for="node-config-input-tunnelInterfaceIndividualAddress"><i class="fa fa-map-marker"></i> <span data-i18n="knxUltimate-config.ets.tunnel_interface_individual_address"></span></label>
|
|
849
|
+
<input type="text" id="node-config-input-tunnelInterfaceIndividualAddress" style="width:150px;" data-i18n="[placeholder]knxUltimate-config.ets.tunnel_interface_individual_address_placeholder"/>
|
|
850
|
+
</div>
|
|
851
|
+
<div class="form-row" id="rowTunnelUserId" style="display:none;">
|
|
852
|
+
<label for="node-config-input-tunnelUserId"><i class="fa fa-id-badge"></i> <span data-i18n="knxUltimate-config.ets.tunnel_user_id"></span></label>
|
|
853
|
+
<input type="text" id="node-config-input-tunnelUserId" style="width:150px;" />
|
|
854
|
+
</div>
|
|
855
|
+
<div class="form-row" id="rowTunnelUserPassword" style="display:none;">
|
|
856
|
+
<label for="node-config-input-tunnelUserPassword"><i class="fa fa-lock"></i> <span data-i18n="knxUltimate-config.ets.tunnel_user_password"></span></label>
|
|
857
|
+
<input type="text" id="node-config-input-tunnelUserPassword" style="width:200px;" />
|
|
858
|
+
</div>
|
|
698
859
|
</p>
|
|
699
860
|
</div>
|
|
700
861
|
</div>
|
|
@@ -98,9 +98,27 @@ module.exports = (RED) => {
|
|
|
98
98
|
// 24/07/2021 KNX Secure checks...
|
|
99
99
|
node.keyringFileXML = typeof config.keyringFileXML === "undefined" || config.keyringFileXML.trim() === "" ? "" : config.keyringFileXML;
|
|
100
100
|
node.knxSecureSelected = typeof config.knxSecureSelected === "undefined" ? false : config.knxSecureSelected;
|
|
101
|
+
node.secureCredentialsMode = typeof config.secureCredentialsMode === "undefined" ? "keyring" : config.secureCredentialsMode;
|
|
101
102
|
// 2025-09 Secure Tunnel Interface IA selection (Auto/Manual)
|
|
102
103
|
node.tunnelIASelection = typeof config.tunnelIASelection === "undefined" ? "Auto" : config.tunnelIASelection;
|
|
103
|
-
node.tunnelIA = typeof config.tunnelIA === "undefined" ? "" : config.tunnelIA;
|
|
104
|
+
node.tunnelIA = typeof config.tunnelIA === "undefined" || config.tunnelIA === null ? "" : String(config.tunnelIA);
|
|
105
|
+
node.tunnelInterfaceIndividualAddress = typeof config.tunnelInterfaceIndividualAddress === "undefined" || config.tunnelInterfaceIndividualAddress === null
|
|
106
|
+
? ""
|
|
107
|
+
: String(config.tunnelInterfaceIndividualAddress);
|
|
108
|
+
const normalizedTunnelIA = (value) => {
|
|
109
|
+
if (typeof value !== "string") return "";
|
|
110
|
+
const trimmed = value.trim();
|
|
111
|
+
return trimmed === "undefined" ? "" : trimmed;
|
|
112
|
+
};
|
|
113
|
+
node.tunnelIA = normalizedTunnelIA(node.tunnelIA);
|
|
114
|
+
node.tunnelInterfaceIndividualAddress = normalizedTunnelIA(node.tunnelInterfaceIndividualAddress);
|
|
115
|
+
if (!node.tunnelInterfaceIndividualAddress && node.tunnelIA) {
|
|
116
|
+
node.tunnelInterfaceIndividualAddress = node.tunnelIA;
|
|
117
|
+
} else if (!node.tunnelIA && node.tunnelInterfaceIndividualAddress) {
|
|
118
|
+
node.tunnelIA = node.tunnelInterfaceIndividualAddress;
|
|
119
|
+
}
|
|
120
|
+
node.tunnelUserPassword = typeof config.tunnelUserPassword === "undefined" ? "" : config.tunnelUserPassword;
|
|
121
|
+
node.tunnelUserId = typeof config.tunnelUserId === "undefined" ? "" : config.tunnelUserId;
|
|
104
122
|
node.name = config.name === undefined || config.name === "" ? node.host : config.name; // 12/08/2021
|
|
105
123
|
node.timerKNXUltimateCheckState = null; // 08/10/2021 Check the state. If not connected and autoreconnect is true, retrig the connetion attempt.
|
|
106
124
|
node.knxConnectionProperties = null; // Retains the connection properties
|
|
@@ -126,7 +144,8 @@ module.exports = (RED) => {
|
|
|
126
144
|
) {
|
|
127
145
|
node.hostProtocol = "Multicast";
|
|
128
146
|
} else {
|
|
129
|
-
node.
|
|
147
|
+
const isSecure = node.knxSecureSelected === true || node.knxSecureSelected === "true";
|
|
148
|
+
node.hostProtocol = isSecure ? "TunnelTCP" : "TunnelUDP";
|
|
130
149
|
}
|
|
131
150
|
node.sysLogger?.info("IP Protocol AUTO SET to " + node.hostProtocol + ", based on IP " + node.host);
|
|
132
151
|
}
|
|
@@ -158,35 +177,68 @@ module.exports = (RED) => {
|
|
|
158
177
|
(async () => {
|
|
159
178
|
try {
|
|
160
179
|
if (node.knxSecureSelected) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
const secureMode = typeof node.secureCredentialsMode === "string" ? node.secureCredentialsMode : "keyring";
|
|
181
|
+
const useManual = secureMode === "manual" || secureMode === "combined";
|
|
182
|
+
const useKeyring = secureMode === "keyring" || secureMode === "combined";
|
|
183
|
+
|
|
184
|
+
const secureConfig = {};
|
|
185
|
+
const modeParts = [];
|
|
186
|
+
|
|
187
|
+
if (useManual) {
|
|
188
|
+
const manualIA = (node.tunnelInterfaceIndividualAddress || "").trim();
|
|
189
|
+
const manualUserId = (node.tunnelUserId || "").trim();
|
|
190
|
+
const manualPwd = node.tunnelUserPassword || "";
|
|
191
|
+
|
|
192
|
+
if (manualIA) {
|
|
193
|
+
secureConfig.tunnelInterfaceIndividualAddress = manualIA;
|
|
194
|
+
}
|
|
195
|
+
if (manualUserId) {
|
|
196
|
+
secureConfig.tunnelUserId = manualUserId;
|
|
173
197
|
}
|
|
174
|
-
|
|
198
|
+
// Always include password property so KNX library receives the intended value (even if empty)
|
|
199
|
+
secureConfig.tunnelUserPassword = manualPwd;
|
|
200
|
+
modeParts.push("manual tunnel credentials");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (useKeyring) {
|
|
204
|
+
secureConfig.knxkeys_file_path = node.keyringFileXML || "";
|
|
205
|
+
secureConfig.knxkeys_password = node.credentials?.keyringFilePassword || "";
|
|
175
206
|
|
|
176
|
-
// Optional early validation to give immediate feedback (non-fatal)
|
|
177
|
-
if (Keyring && node.keyringFileXML && (node.credentials?.keyringFilePassword || "") !== "") {
|
|
178
207
|
try {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
const manualSelectionIA = normalizedTunnelIA(node.tunnelIA);
|
|
209
|
+
if (node.tunnelIASelection === "Manual" && manualSelectionIA) {
|
|
210
|
+
if (!secureConfig.tunnelInterfaceIndividualAddress) {
|
|
211
|
+
secureConfig.tunnelInterfaceIndividualAddress = manualSelectionIA;
|
|
212
|
+
}
|
|
213
|
+
} else if (!secureConfig.tunnelInterfaceIndividualAddress) {
|
|
214
|
+
secureConfig.tunnelInterfaceIndividualAddress = ""; // Auto (let KNX stack select)
|
|
215
|
+
}
|
|
216
|
+
} catch (e) { /* empty */ }
|
|
217
|
+
|
|
218
|
+
// Optional early validation to give immediate feedback (non-fatal)
|
|
219
|
+
if (Keyring && node.keyringFileXML && (node.credentials?.keyringFilePassword || "") !== "") {
|
|
220
|
+
try {
|
|
221
|
+
const kr = new Keyring();
|
|
222
|
+
await kr.load(node.keyringFileXML, node.credentials.keyringFilePassword);
|
|
223
|
+
const createdBy = kr.getCreatedBy?.() || "unknown";
|
|
224
|
+
const created = kr.getCreated?.() || "unknown";
|
|
225
|
+
RED.log.info(`KNX-Secure: Keyring validated (Created by ${createdBy} on ${created}) using node ${node.name || node.id}`);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
node.sysLogger?.error("KNX Secure: keyring validation failed: " + err.message);
|
|
228
|
+
// Keep secure enabled: KNXClient will emit detailed errors on connect
|
|
229
|
+
}
|
|
187
230
|
}
|
|
231
|
+
modeParts.push("keyring file/password");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (Object.keys(secureConfig).length > 0) {
|
|
235
|
+
node.secureTunnelConfig = secureConfig;
|
|
188
236
|
} else {
|
|
189
|
-
|
|
237
|
+
node.secureTunnelConfig = undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (modeParts.length > 0) {
|
|
241
|
+
RED.log.info(`KNX-Secure: secure mode selected (${modeParts.join(" + ")}). Node ${node.name || node.id}`);
|
|
190
242
|
}
|
|
191
243
|
} else {
|
|
192
244
|
RED.log.info("KNX-Unsecure: connection to insecure interface/router using node " + (node.name || node.id));
|
|
@@ -750,7 +802,8 @@ module.exports = (RED) => {
|
|
|
750
802
|
_dest = _datagram.cEMIMessage.dstAddress.toString();
|
|
751
803
|
|
|
752
804
|
if (_evt === null || _src === null || _dest === null) {
|
|
753
|
-
node.sysLogger?.
|
|
805
|
+
node.sysLogger?.warn(`HandleBusEvent: unable to parse telegram, ignored (evt=${_evt || 'n/a'} src=${_src || 'n/a'} dest=${_dest || 'n/a'})`);
|
|
806
|
+
return;
|
|
754
807
|
};
|
|
755
808
|
|
|
756
809
|
_echoed = _echoed || false;
|
|
@@ -85,6 +85,14 @@
|
|
|
85
85
|
"youtubeKeyring": "Anleitung des XML Keyring Exports auf Youtube",
|
|
86
86
|
"keyring_file": "Keyring-Datei",
|
|
87
87
|
"password": "Passwort",
|
|
88
|
+
"secure_credentials_mode": "Quelle für Secure-Zugangsdaten",
|
|
89
|
+
"secure_credentials_mode_keyring": "ETS-Keyring-Datei",
|
|
90
|
+
"secure_credentials_mode_manual": "Manuelle Zugangsdaten",
|
|
91
|
+
"secure_credentials_mode_combined": "Keyring + manuelles Tunnel-Passwort",
|
|
92
|
+
"tunnel_interface_individual_address": "Individuelle Adresse der Tunnel-Schnittstelle",
|
|
93
|
+
"tunnel_interface_individual_address_placeholder": "1.1.1",
|
|
94
|
+
"tunnel_user_id": "Tunnel-Benutzer-ID",
|
|
95
|
+
"tunnel_user_password": "Tunnel-Benutzerpasswort",
|
|
88
96
|
"ga_list_label": "ETS-Gruppenadressliste"
|
|
89
97
|
},
|
|
90
98
|
"utility": {
|
|
@@ -19,6 +19,14 @@ Dieser Node stellt die Verbindung zu deinem KNX/IP‑Gateway her.
|
|
|
19
19
|
| KNX Physical Address | Physikalische KNX‑Adresse, z. B. `1.1.200`. Standard: `15.15.22`. |
|
|
20
20
|
| Bind to local interface | Lokales Netzwerk‑Interface für die Kommunikation. "Auto" wählt automatisch. Bei mehreren Interfaces (Ethernet/WLAN) ist eine manuelle Auswahl empfehlenswert, damit keine UDP‑Telegramme verloren gehen. |
|
|
21
21
|
| Automatically connect to KNX BUS at start | Automatisch beim Start verbinden. Standard: "Yes". |
|
|
22
|
+
| Secure credentials source | Leg fest, wie KNX Secure Daten bereitgestellt werden: **ETS-Keyring-Datei** (Data-Secure-Schlüssel und ggf. Tunnel-Zugang aus dem Keyring), **Manuelle Zugangsdaten** (nur KNX IP Tunnelling Secure mit manuell eingetragenem Benutzer) oder **Keyring + manuelles Tunnel-Passwort** (Data-Secure-Schlüssel aus dem Keyring, Tunnel-Benutzer/-Passwort manuell). Achtung: KNX Data Secure Telegramme benötigen immer einen Keyring. |
|
|
23
|
+
| Tunnel interface individual address | Sichtbar, sobald die gewählte Option manuelle Zugangsdaten enthält (Manuelle Zugangsdaten oder Keyring + manuelles Tunnel-Passwort). Optionale KNX-Individualadresse der Tunnel-Schnittstelle (z. B. `1.1.1`); leer lassen, damit KNX Ultimate automatisch verhandelt. |
|
|
24
|
+
| Tunnel user ID | Sichtbar bei manuellen Zugangsdaten. Optionale Kennung des KNX Secure Tunnel-Benutzers aus ETS. |
|
|
25
|
+
| Tunnel user password | Sichtbar bei manuellen Zugangsdaten. Passwort des KNX Secure Tunnel-Benutzers aus ETS. |
|
|
26
|
+
|
|
27
|
+
> **KNX Secure im Überblick**
|
|
28
|
+
> • *KNX Data Secure* schützt Gruppenadress-Telegramme und benötigt stets eine Keyring-Datei mit den Gruppenschlüsseln.
|
|
29
|
+
> • *KNX IP Tunnelling Secure* schützt den Verbindungsaufbau mithilfe eines Commissioning-Passworts. Dieses kann – je nach Modus – aus dem Keyring kommen oder manuell eingetragen werden.
|
|
22
30
|
|
|
23
31
|
<br/>
|
|
24
32
|
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
| KNX Physical Address | The physical KNX address, example 1.1.200. Default is "15.15.22".|
|
|
20
20
|
| Bind to local interface | The Node will use this local interface for communications. Leave "Auto" for automatic selection. If you have more than one lan connection, for example Ethernet and Wifi, it's strongly recommended to manually select the interface, otherwise not all UDP telegram will reach your computer, thus the Node may not work as expected. Default is "Auto". |
|
|
21
21
|
| Automatically connect to KNX BUS at start | Auto connect to the bus at start. Default is "Yes". |
|
|
22
|
+
| Secure credentials source | Choose how KNX Secure data is provided: **ETS keyring file** (Data Secure keys and, if available, tunnelling credentials come from the keyring), **Manual credentials** (only KNX IP Tunnelling Secure with a manually entered user) or **Keyring + manual tunnel password** (Data Secure keys from the keyring while the secure tunnel uses the manual user/password). Remember: KNX Data Secure telegrams always require a keyring. |
|
|
23
|
+
| Tunnel interface individual address | Visible whenever the selected mode includes manual credentials (Manual or Keyring + manual tunnel password). Optional KNX individual address for the secure tunnel interface (for example `1.1.1`); leave empty to let KNX Ultimate negotiate it automatically. |
|
|
24
|
+
| Tunnel user ID | Visible when manual credentials are used. Optional KNX Secure tunnel user identifier defined in ETS. |
|
|
25
|
+
| Tunnel user password | Visible when manual credentials are used. Password of the secure tunnelling user configured in ETS. |
|
|
26
|
+
|
|
27
|
+
> **KNX Secure at a glance**
|
|
28
|
+
> • *KNX Data Secure* protects group-address telegrams and **always** requires a keyring file containing the group keys.
|
|
29
|
+
> • *KNX IP Tunnelling Secure* protects the connection handshake using a commissioning password. The password can come from the keyring or be entered manually depending on the selected mode.
|
|
22
30
|
|
|
23
31
|
<br/>
|
|
24
32
|
|
|
@@ -82,6 +82,14 @@
|
|
|
82
82
|
"youtubeKeyring": "See how to export the Keyring XML file on Youtube",
|
|
83
83
|
"keyring_file": "Keyring file",
|
|
84
84
|
"password": "Password",
|
|
85
|
+
"secure_credentials_mode": "Secure credentials source",
|
|
86
|
+
"secure_credentials_mode_keyring": "ETS keyring file",
|
|
87
|
+
"secure_credentials_mode_manual": "Manual credentials",
|
|
88
|
+
"secure_credentials_mode_combined": "Keyring + manual tunnel password",
|
|
89
|
+
"tunnel_interface_individual_address": "Tunnel interface individual address",
|
|
90
|
+
"tunnel_interface_individual_address_placeholder": "1.1.1",
|
|
91
|
+
"tunnel_user_id": "Tunnel user ID",
|
|
92
|
+
"tunnel_user_password": "Tunnel user password",
|
|
85
93
|
"ga_list_label": "ETS group address list"
|
|
86
94
|
},
|
|
87
95
|
"utility": {
|
|
@@ -86,6 +86,14 @@
|
|
|
86
86
|
"youtubeKeyring": "Guarda come si esporta il Keyring XML su YouTube",
|
|
87
87
|
"keyring_file": "File Keyring",
|
|
88
88
|
"password": "Password",
|
|
89
|
+
"secure_credentials_mode": "Origine credenziali KNX Secure",
|
|
90
|
+
"secure_credentials_mode_keyring": "File keyring ETS",
|
|
91
|
+
"secure_credentials_mode_manual": "Credenziali manuali",
|
|
92
|
+
"secure_credentials_mode_combined": "File keyring + password tunnel manuale",
|
|
93
|
+
"tunnel_interface_individual_address": "Indirizzo individuale interfaccia tunnel",
|
|
94
|
+
"tunnel_interface_individual_address_placeholder": "1.1.1",
|
|
95
|
+
"tunnel_user_id": "ID utente tunnel",
|
|
96
|
+
"tunnel_user_password": "Password utente tunnel",
|
|
89
97
|
"ga_list_label": "Lista indirizzi di gruppo ETS"
|
|
90
98
|
},
|
|
91
99
|
"utility": {
|
|
@@ -19,6 +19,14 @@ Questo nodo si connette al tuo KNX/IP Gateway.
|
|
|
19
19
|
| KNX Physical Address | Indirizzo fisico KNX, es. `1.1.200`. Default: `15.15.22`. |
|
|
20
20
|
| Bind to local interface | Interfaccia di rete locale usata dal nodo. Lascia "Auto" per selezione automatica. Se hai più interfacce (Ethernet/Wi‑Fi), è consigliato impostarla manualmente per evitare perdita di telegrammi UDP. Default: "Auto". |
|
|
21
21
|
| Automatically connect to KNX BUS at start | Connessione automatica al BUS all’avvio. Default: "Yes". |
|
|
22
|
+
| Secure credentials source | Scegli come fornire i dati KNX Secure: **File keyring ETS** (le chiavi Data Secure e, se presenti, le credenziali di tunnelling arrivano dal keyring), **Credenziali manuali** (solo KNX IP Tunnelling Secure con utente inserito a mano) oppure **File keyring + password tunnel manuale** (le chiavi Data Secure dal keyring e l’utente/password del tunnel impostati manualmente). Ricorda: i telegrammi KNX Data Secure richiedono sempre un keyring. |
|
|
23
|
+
| Tunnel interface individual address | Visibile quando la modalità selezionata prevede credenziali manuali (Credenziali manuali o File keyring + password tunnel manuale). Indirizzo individuale opzionale dell’interfaccia tunnel sicura (es. `1.1.1`); lascia vuoto per negoziazione automatica da parte di KNX Ultimate. |
|
|
24
|
+
| Tunnel user ID | Visibile quando sono attive credenziali manuali. ID opzionale dell’utente tunnel KNX Secure definito in ETS. |
|
|
25
|
+
| Tunnel user password | Visibile quando sono attive credenziali manuali. Password dell’utente tunnel KNX Secure configurata in ETS. |
|
|
26
|
+
|
|
27
|
+
> **Nota su KNX Secure**
|
|
28
|
+
> • *KNX Data Secure* protegge i telegrammi sugli indirizzi di gruppo e richiede sempre un file keyring con le chiavi di gruppo.
|
|
29
|
+
> • *KNX IP Tunnelling Secure* protegge l’handshake di connessione tramite una password di commissioning, che può essere letta dal keyring oppure inserita manualmente in base alla modalità scelta.
|
|
22
30
|
|
|
23
31
|
<br/>
|
|
24
32
|
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
| KNX Physical Address | 物理地址,如 `1.1.200`。默认 `15.15.22`。|
|
|
20
20
|
| Bind to local interface | 使用的本地网络接口。"Auto" 自动选择。若有多网卡(以太网/无线),建议手动指定,避免 UDP 丢包。|
|
|
21
21
|
| Automatically connect to KNX BUS at start | 启动时自动连接总线。默认 "Yes"。|
|
|
22
|
+
| Secure credentials source | 选择如何提供 KNX Secure 数据:**ETS 密钥环文件**(Data Secure 密钥以及—若存在—隧道凭据来自密钥环)、**手动凭据**(仅启用 KNX IP 安全隧道,手动输入用户)或 **密钥环 + 手动隧道密码**(Data Secure 由密钥环提供,隧道用户/密码手动输入)。注意:KNX Data Secure 报文始终需要密钥环文件。|
|
|
23
|
+
| Tunnel interface individual address | 当所选模式包含手动凭据时显示(手动凭据或密钥环 + 手动隧道密码)。可选的安全隧道 KNX 个人地址(如 `1.1.1`);留空则由 KNX Ultimate 自动协商。|
|
|
24
|
+
| Tunnel user ID | 启用手动凭据时显示。可选的 KNX Secure 隧道用户 ID(在 ETS 中配置)。|
|
|
25
|
+
| Tunnel user password | 启用手动凭据时显示。输入 ETS 中配置的 KNX Secure 隧道用户密码。|
|
|
26
|
+
|
|
27
|
+
> **KNX Secure 概览**
|
|
28
|
+
> • *KNX Data Secure* 用于保护组地址报文,**始终** 需要包含组密钥的密钥环文件。
|
|
29
|
+
> • *KNX IP Tunnelling Secure* 通过调试密码保护连接握手,密码可以根据模式从密钥环中读取,或在界面中手动输入。
|
|
22
30
|
|
|
23
31
|
<br/>
|
|
24
32
|
|
|
@@ -81,6 +81,14 @@
|
|
|
81
81
|
"youtubeKeyring": "在 Youtube 上了解如何导出 Keyring XML 文件",
|
|
82
82
|
"keyring_file": "Keyring 文件",
|
|
83
83
|
"password": "密码",
|
|
84
|
+
"secure_credentials_mode": "安全凭据来源",
|
|
85
|
+
"secure_credentials_mode_keyring": "ETS 密钥环文件",
|
|
86
|
+
"secure_credentials_mode_manual": "手动凭据",
|
|
87
|
+
"secure_credentials_mode_combined": "密钥环 + 手动隧道密码",
|
|
88
|
+
"tunnel_interface_individual_address": "隧道接口个人地址",
|
|
89
|
+
"tunnel_interface_individual_address_placeholder": "1.1.1",
|
|
90
|
+
"tunnel_user_id": "隧道用户 ID",
|
|
91
|
+
"tunnel_user_password": "隧道用户密码",
|
|
84
92
|
"ga_list_label": "ETS 组地址列表"
|
|
85
93
|
},
|
|
86
94
|
"utility": {
|
package/package.json
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "4.0.
|
|
6
|
+
"version": "4.0.5",
|
|
7
7
|
"description": "Control your KNX and KNX Secure 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",
|
|
10
10
|
"crypto-js": "4.2.0",
|
|
11
11
|
"dns-sync": "0.2.1",
|
|
12
12
|
"js-yaml": "4.1.0",
|
|
13
|
-
"knxultimate": "5.
|
|
13
|
+
"knxultimate": "5.1.1",
|
|
14
14
|
"lodash": "4.17.21",
|
|
15
15
|
"mkdirp": "3.0.1",
|
|
16
16
|
"node-color-log": "12.0.1",
|