node-red-contrib-knx-ultimate 4.1.18 → 4.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,7 +6,9 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 4.1.18** - January 2026<br/>
9
+ **Version 4.1.19** - February 2026<br/>
10
+
11
+ - NEW: **KNX Multi Routing**: added **Server KNX/IP** mode (standalone KNXnet/IP tunneling server) that outputs/accepts RAW telegrams on the node ports (no gateway required).<br/>
10
12
 
11
13
  - NEW: KNX AI sidebar: added another TAB in the Node-Red's toolbar buttons dedicated to AI analysis.<br>
12
14
  - NEW: added **KNX AI** node (traffic analyzer + optional LLM assistant) and **KNX AI** sidebar tab.<br/>
@@ -18,7 +20,7 @@
18
20
  - Docs: completed the help + wiki pages for **KNX AI**, **KNX Multi Routing** and **KNX Router Filter** in all supported languages, and added them to the docs homepages.<br/>
19
21
  - Docs: added the new nodes to the wiki navbar in all languages (so they appear in the left navigation menu).<br/>
20
22
  - Docs: added new wiki **Samples** pages (with diagrams) for the 3 new nodes.<br/>
21
- - Examples: added importable Node-RED flow JSON examples for **KNX AI**, **KNX Multi Routing** and **KNX Router Filter**.<br/>
23
+ - Examples: added importable Node-RED flow JSON examples for **KNX AI**, **KNX Multi Routing** and **KNX Router Filter** (including KNX Multi Routing **Server KNX/IP** sample).<br/>
22
24
 
23
25
  **Version 4.1.15** - January 2026<br/>
24
26
 
@@ -0,0 +1,133 @@
1
+ [
2
+ {
3
+ "id": "tab_knx_mr_srv_sample",
4
+ "type": "tab",
5
+ "label": "KNX/IP Server (MultiRouting)",
6
+ "disabled": false,
7
+ "info": ""
8
+ },
9
+ {
10
+ "id": "cmt_knx_mr_srv_1",
11
+ "type": "comment",
12
+ "z": "tab_knx_mr_srv_sample",
13
+ "name": "Use knxUltimateMultiRouting as a standalone KNXnet/IP Tunneling Server (UDP).",
14
+ "info": "1) Deploy.\n2) Configure your KNX/IP tunneling client to connect to this Node-RED host on tunnelListenPort (default 3671).\n3) Watch the RAW telegrams in the Debug node.\n\nTo test the INPUT side: after at least one telegram is received, click the inject node \"Replay last\" to inject the last received telegram back to connected tunneling client(s).",
15
+ "x": 430,
16
+ "y": 40,
17
+ "wires": []
18
+ },
19
+ {
20
+ "id": "mr_tunnel_server",
21
+ "type": "knxUltimateMultiRouting",
22
+ "z": "tab_knx_mr_srv_sample",
23
+ "mode": "server",
24
+ "server": "",
25
+ "name": "KNX/IP Tunneling Server",
26
+ "outputtopic": "",
27
+ "dropIfSameGateway": true,
28
+ "tunnelListenHost": "0.0.0.0",
29
+ "tunnelListenPort": "3671",
30
+ "tunnelAdvertiseHost": "",
31
+ "tunnelAssignedIndividualAddress": "15.15.255",
32
+ "tunnelGatewayId": "knxip-server",
33
+ "tunnelMaxSessions": "1",
34
+ "x": 240,
35
+ "y": 160,
36
+ "wires": [
37
+ [
38
+ "fn_cache_last",
39
+ "dbg_tunnel_raw"
40
+ ]
41
+ ]
42
+ },
43
+ {
44
+ "id": "dbg_tunnel_raw",
45
+ "type": "debug",
46
+ "z": "tab_knx_mr_srv_sample",
47
+ "name": "RAW from tunneling client",
48
+ "active": true,
49
+ "tosidebar": true,
50
+ "console": false,
51
+ "tostatus": false,
52
+ "complete": "payload.knx",
53
+ "targetType": "msg",
54
+ "x": 520,
55
+ "y": 160,
56
+ "wires": []
57
+ },
58
+ {
59
+ "id": "fn_cache_last",
60
+ "type": "function",
61
+ "z": "tab_knx_mr_srv_sample",
62
+ "name": "Cache last telegram",
63
+ "func": "try {\n if (msg && msg.payload && msg.payload.knx) {\n flow.set('last_knx_raw', msg.payload.knx);\n }\n} catch (e) {}\nreturn null;",
64
+ "outputs": 0,
65
+ "noerr": 0,
66
+ "initialize": "",
67
+ "finalize": "",
68
+ "libs": [],
69
+ "x": 500,
70
+ "y": 220,
71
+ "wires": []
72
+ },
73
+ {
74
+ "id": "inj_replay_last",
75
+ "type": "inject",
76
+ "z": "tab_knx_mr_srv_sample",
77
+ "name": "Replay last",
78
+ "props": [
79
+ {
80
+ "p": "payload"
81
+ }
82
+ ],
83
+ "repeat": "",
84
+ "crontab": "",
85
+ "once": false,
86
+ "onceDelay": 0.1,
87
+ "topic": "",
88
+ "payload": "",
89
+ "payloadType": "date",
90
+ "x": 170,
91
+ "y": 300,
92
+ "wires": [
93
+ [
94
+ "fn_replay_last"
95
+ ]
96
+ ]
97
+ },
98
+ {
99
+ "id": "fn_replay_last",
100
+ "type": "function",
101
+ "z": "tab_knx_mr_srv_sample",
102
+ "name": "Build replay msg",
103
+ "func": "const last = flow.get('last_knx_raw');\nif (!last) {\n node.warn('No cached telegram yet. Generate one from a tunneling client first.');\n return null;\n}\n\n// IMPORTANT:\n// - Server mode expects msg.payload.knx.cemi.hex (or msg.payload.knx.cemi) to be present.\n// - Do NOT attach knxMultiRouting.gateway.id here, otherwise dropIfSameGateway may discard it.\nreturn {\n topic: last.destination || '',\n payload: {\n knx: last\n }\n};",
104
+ "outputs": 1,
105
+ "noerr": 0,
106
+ "initialize": "",
107
+ "finalize": "",
108
+ "libs": [],
109
+ "x": 360,
110
+ "y": 300,
111
+ "wires": [
112
+ [
113
+ "mr_tunnel_server",
114
+ "dbg_replay"
115
+ ]
116
+ ]
117
+ },
118
+ {
119
+ "id": "dbg_replay",
120
+ "type": "debug",
121
+ "z": "tab_knx_mr_srv_sample",
122
+ "name": "Replay msg",
123
+ "active": true,
124
+ "tosidebar": true,
125
+ "console": false,
126
+ "tostatus": false,
127
+ "complete": "true",
128
+ "targetType": "full",
129
+ "x": 520,
130
+ "y": 300,
131
+ "wires": []
132
+ }
133
+ ]
@@ -725,10 +725,14 @@ module.exports = function (RED) {
725
725
  const pushStatus = (status) => {
726
726
  if (!status) return
727
727
  const provider = node.serverKNX
728
- if (provider && typeof provider.applyStatusUpdate === 'function') {
729
- provider.applyStatusUpdate(node, status)
730
- } else {
731
- node.status(status)
728
+ try {
729
+ if (provider && typeof provider.applyStatusUpdate === 'function') {
730
+ provider.applyStatusUpdate(node, status)
731
+ } else {
732
+ node.status(status)
733
+ }
734
+ } catch (error) {
735
+ try { node.status(status) } catch (e2) { /* ignore */ }
732
736
  }
733
737
  }
734
738
 
@@ -999,14 +1003,19 @@ module.exports = function (RED) {
999
1003
  }
1000
1004
 
1001
1005
  const emitSummary = () => {
1002
- const now = nowMs()
1003
- trimHistory(now)
1004
- const summary = buildSummary(now)
1005
- node._lastSummary = summary
1006
- node._lastSummaryAt = now
1007
- node.send([{ topic: node.outputtopic, payload: summary, knxAi: { type: 'summary' } }, null, null])
1008
- const best = summary.topGAs && summary.topGAs[0] ? `${summary.topGAs[0].ga} (${summary.topGAs[0].count})` : 'no traffic'
1009
- updateStatus({ fill: 'green', shape: 'dot', text: `AI ${summary.counters.overallRatePerSec}/s top ${best}` })
1006
+ try {
1007
+ const now = nowMs()
1008
+ trimHistory(now)
1009
+ const summary = buildSummary(now)
1010
+ node._lastSummary = summary
1011
+ node._lastSummaryAt = now
1012
+ node.send([{ topic: node.outputtopic, payload: summary, knxAi: { type: 'summary' } }, null, null])
1013
+ const best = summary.topGAs && summary.topGAs[0] ? `${summary.topGAs[0].ga} (${summary.topGAs[0].count})` : 'no traffic'
1014
+ updateStatus({ fill: 'green', shape: 'dot', text: `AI ${summary.counters.overallRatePerSec}/s top ${best}` })
1015
+ } catch (error) {
1016
+ try { node.sysLogger?.error(`knxUltimateAI emitSummary error: ${error.message || error}`) } catch (e) { /* ignore */ }
1017
+ updateStatus({ fill: 'red', shape: 'dot', text: `AI summary error: ${error.message || error}` })
1018
+ }
1010
1019
  }
1011
1020
 
1012
1021
  const recordAnomaly = (payload) => {
@@ -1110,85 +1119,109 @@ module.exports = function (RED) {
1110
1119
 
1111
1120
  // Called by knxUltimate-config.js
1112
1121
  node.handleSend = (msg) => {
1113
- const telegram = extractTelegram(msg)
1114
- if (!telegram) return
1115
- node._history.push(telegram)
1116
- const now = telegram.ts
1117
- trimHistory(now)
1118
- maybeEmitGAAnomalies(telegram)
1119
- maybeEmitOverallAnomaly(now)
1122
+ try {
1123
+ const telegram = extractTelegram(msg)
1124
+ if (!telegram) return
1125
+ node._history.push(telegram)
1126
+ const now = telegram.ts
1127
+ trimHistory(now)
1128
+ maybeEmitGAAnomalies(telegram)
1129
+ maybeEmitOverallAnomaly(now)
1130
+ } catch (error) {
1131
+ try { node.sysLogger?.error(`knxUltimateAI handleSend error: ${error.message || error}`) } catch (e) { /* ignore */ }
1132
+ }
1120
1133
  }
1121
1134
 
1122
1135
  const handleCommand = async (msg) => {
1123
- const cmd = (msg && msg.topic !== undefined) ? String(msg.topic).toLowerCase() : ''
1124
- if (cmd === 'reset') {
1125
- node._history = []
1126
- node._gaState = new Map()
1127
- updateStatus({ fill: 'grey', shape: 'dot', text: 'AI reset' })
1128
- node.send([{ topic: node.outputtopic, payload: { ok: true }, knxAi: { type: 'reset' } }, null, null])
1129
- return
1130
- }
1136
+ try {
1137
+ const cmd = (msg && msg.topic !== undefined) ? String(msg.topic).toLowerCase() : ''
1138
+ if (cmd === 'reset') {
1139
+ node._history = []
1140
+ node._gaState = new Map()
1141
+ updateStatus({ fill: 'grey', shape: 'dot', text: 'AI reset' })
1142
+ node.send([{ topic: node.outputtopic, payload: { ok: true }, knxAi: { type: 'reset' } }, null, null])
1143
+ return
1144
+ }
1131
1145
 
1132
- if (cmd === 'summary' || cmd === 'stats' || cmd === 'top' || cmd === '') {
1133
- emitSummary()
1134
- return
1135
- }
1146
+ if (cmd === 'summary' || cmd === 'stats' || cmd === 'top' || cmd === '') {
1147
+ emitSummary()
1148
+ return
1149
+ }
1136
1150
 
1137
- if (cmd === 'ask') {
1138
- const question = (msg.prompt !== undefined)
1139
- ? String(msg.prompt)
1140
- : (typeof msg.payload === 'string' ? msg.payload : safeStringify(msg.payload))
1141
- updateStatus({ fill: 'blue', shape: 'ring', text: 'AI thinking...' })
1142
- try {
1143
- const ret = await callLLM({ question })
1144
- node._assistantLog.push({ at: new Date().toISOString(), question, content: ret.content, provider: ret.provider, model: ret.model })
1145
- while (node._assistantLog.length > 50) node._assistantLog.shift()
1146
- node.send([null, null, {
1147
- topic: node.outputtopic,
1148
- payload: ret.content,
1149
- knxAi: { type: 'llm', provider: ret.provider, model: ret.model, question },
1150
- summary: ret.summary
1151
- }])
1152
- updateStatus({ fill: 'green', shape: 'dot', text: 'AI answer ready' })
1153
- } catch (error) {
1154
- node._assistantLog.push({ at: new Date().toISOString(), question, error: error.message || String(error) })
1155
- while (node._assistantLog.length > 50) node._assistantLog.shift()
1156
- node.send([null, null, {
1157
- topic: node.outputtopic,
1158
- payload: { error: error.message || String(error) },
1159
- knxAi: { type: 'llm_error', question }
1160
- }])
1161
- updateStatus({ fill: 'red', shape: 'dot', text: `AI error: ${error.message || error}` })
1151
+ if (cmd === 'ask') {
1152
+ const question = (msg.prompt !== undefined)
1153
+ ? String(msg.prompt)
1154
+ : (typeof msg.payload === 'string' ? msg.payload : safeStringify(msg.payload))
1155
+ updateStatus({ fill: 'blue', shape: 'ring', text: 'AI thinking...' })
1156
+ try {
1157
+ const ret = await callLLM({ question })
1158
+ node._assistantLog.push({ at: new Date().toISOString(), question, content: ret.content, provider: ret.provider, model: ret.model })
1159
+ while (node._assistantLog.length > 50) node._assistantLog.shift()
1160
+ node.send([null, null, {
1161
+ topic: node.outputtopic,
1162
+ payload: ret.content,
1163
+ knxAi: { type: 'llm', provider: ret.provider, model: ret.model, question },
1164
+ summary: ret.summary
1165
+ }])
1166
+ updateStatus({ fill: 'green', shape: 'dot', text: 'AI answer ready' })
1167
+ } catch (error) {
1168
+ node._assistantLog.push({ at: new Date().toISOString(), question, error: error.message || String(error) })
1169
+ while (node._assistantLog.length > 50) node._assistantLog.shift()
1170
+ node.send([null, null, {
1171
+ topic: node.outputtopic,
1172
+ payload: { error: error.message || String(error) },
1173
+ knxAi: { type: 'llm_error', question }
1174
+ }])
1175
+ updateStatus({ fill: 'red', shape: 'dot', text: `AI error: ${error.message || error}` })
1176
+ }
1177
+ return
1162
1178
  }
1163
- return
1164
- }
1165
1179
 
1166
- node.warn(`knxUltimateAI: unknown command '${cmd}'. Supported: reset, summary, ask`)
1180
+ node.warn(`knxUltimateAI: unknown command '${cmd}'. Supported: reset, summary, ask`)
1181
+ } catch (error) {
1182
+ try { node.sysLogger?.error(`knxUltimateAI handleCommand error: ${error.message || error}`) } catch (e) { /* ignore */ }
1183
+ try { node.error(error) } catch (e) { /* ignore */ }
1184
+ updateStatus({ fill: 'red', shape: 'dot', text: `AI command error: ${error.message || error}` })
1185
+ }
1167
1186
  }
1168
1187
 
1169
1188
  node.getSidebarState = ({ fresh = false } = {}) => {
1170
- const now = nowMs()
1171
- trimHistory(now)
1172
- const summary = fresh ? buildSummary(now) : (node._lastSummary || buildSummary(now))
1173
- if (fresh) {
1174
- node._lastSummary = summary
1175
- node._lastSummaryAt = now
1176
- }
1177
- return {
1178
- node: {
1179
- id: node.id,
1180
- type: node.type,
1181
- name: node.name || '',
1182
- topic: node.topic || '',
1183
- gatewayId: node.serverKNX ? node.serverKNX.id : '',
1184
- gatewayName: (node.serverKNX && node.serverKNX.name) ? node.serverKNX.name : '',
1185
- llmEnabled: !!node.llmEnabled,
1186
- llmProvider: node.llmProvider || '',
1187
- llmModel: node.llmModel || ''
1188
- },
1189
- summary,
1190
- anomalies: node._anomalies.slice(-50),
1191
- assistant: node._assistantLog.slice(-30)
1189
+ try {
1190
+ const now = nowMs()
1191
+ trimHistory(now)
1192
+ const summary = fresh ? buildSummary(now) : (node._lastSummary || buildSummary(now))
1193
+ if (fresh) {
1194
+ node._lastSummary = summary
1195
+ node._lastSummaryAt = now
1196
+ }
1197
+ return {
1198
+ node: {
1199
+ id: node.id,
1200
+ type: node.type,
1201
+ name: node.name || '',
1202
+ topic: node.topic || '',
1203
+ gatewayId: node.serverKNX ? node.serverKNX.id : '',
1204
+ gatewayName: (node.serverKNX && node.serverKNX.name) ? node.serverKNX.name : '',
1205
+ llmEnabled: !!node.llmEnabled,
1206
+ llmProvider: node.llmProvider || '',
1207
+ llmModel: node.llmModel || ''
1208
+ },
1209
+ summary,
1210
+ anomalies: node._anomalies.slice(-50),
1211
+ assistant: node._assistantLog.slice(-30)
1212
+ }
1213
+ } catch (error) {
1214
+ return {
1215
+ node: {
1216
+ id: node.id,
1217
+ type: node.type,
1218
+ name: node.name || '',
1219
+ topic: node.topic || ''
1220
+ },
1221
+ summary: { error: error.message || String(error) },
1222
+ anomalies: [],
1223
+ assistant: []
1224
+ }
1192
1225
  }
1193
1226
  }
1194
1227
 
@@ -1204,7 +1237,18 @@ module.exports = function (RED) {
1204
1237
  }
1205
1238
 
1206
1239
  node.on('input', function (msg) {
1207
- handleCommand(msg)
1240
+ try {
1241
+ const p = handleCommand(msg)
1242
+ if (p && typeof p.catch === 'function') {
1243
+ p.catch((error) => {
1244
+ try { node.sysLogger?.error(`knxUltimateAI input error: ${error.message || error}`) } catch (e) { /* ignore */ }
1245
+ try { node.error(error) } catch (e) { /* ignore */ }
1246
+ })
1247
+ }
1248
+ } catch (error) {
1249
+ try { node.sysLogger?.error(`knxUltimateAI input error: ${error.message || error}`) } catch (e) { /* ignore */ }
1250
+ try { node.error(error) } catch (e) { /* ignore */ }
1251
+ }
1208
1252
  })
1209
1253
 
1210
1254
  node.on('close', function (done) {
@@ -1220,13 +1264,15 @@ module.exports = function (RED) {
1220
1264
 
1221
1265
  // On each deploy, unsubscribe+resubscribe
1222
1266
  if (node.serverKNX) {
1223
- node.serverKNX.removeClient(node)
1224
- node.serverKNX.addClient(node)
1267
+ try { node.serverKNX.removeClient(node) } catch (e) { /* ignore */ }
1268
+ try { node.serverKNX.addClient(node) } catch (e) { /* ignore */ }
1225
1269
  }
1226
1270
 
1227
1271
  if (node.emitIntervalSec && node.emitIntervalSec > 0) {
1228
1272
  if (node._timerEmit) clearInterval(node._timerEmit)
1229
- node._timerEmit = setInterval(emitSummary, Math.max(5, node.emitIntervalSec) * 1000)
1273
+ node._timerEmit = setInterval(() => {
1274
+ try { emitSummary() } catch (e) { /* emitSummary already guards */ }
1275
+ }, Math.max(5, node.emitIntervalSec) * 1000)
1230
1276
  }
1231
1277
 
1232
1278
  updateStatus({ fill: 'grey', shape: 'dot', text: 'AI ready' })
@@ -5,10 +5,19 @@
5
5
  category: "KNX Ultimate",
6
6
  color: 'lightblue',
7
7
  defaults: {
8
- server: { type: "knxUltimate-config", required: true },
8
+ mode: { value: "gateway", required: true },
9
+ server: { type: "knxUltimate-config", required: false },
9
10
  name: { value: "KNX Multi Routing", required: false },
10
11
  outputtopic: { value: "", required: false },
11
- dropIfSameGateway: { value: true }
12
+ dropIfSameGateway: { value: true },
13
+
14
+ // KNX/IP tunneling server options (mode=server)
15
+ tunnelListenHost: { value: "0.0.0.0", required: false },
16
+ tunnelListenPort: { value: 3671, required: false, validate: RED.validators.number() },
17
+ tunnelAdvertiseHost: { value: "", required: false },
18
+ tunnelAssignedIndividualAddress: { value: "15.15.255", required: false },
19
+ tunnelMaxSessions: { value: 1, required: false, validate: RED.validators.number() },
20
+ tunnelGatewayId: { value: "", required: false }
12
21
  },
13
22
  inputs: 1,
14
23
  outputs: 1,
@@ -20,6 +29,19 @@
20
29
  paletteLabel: "KNX Multi Routing",
21
30
  oneditprepare: function () {
22
31
  try { RED.sidebar.show("help"); } catch (error) { }
32
+
33
+ const toggleMode = () => {
34
+ const mode = $("#node-input-mode").val() || "gateway";
35
+ if (mode === "server") {
36
+ $(".knxmr-row-gateway").hide();
37
+ $(".knxmr-row-tunnel").show();
38
+ } else {
39
+ $(".knxmr-row-gateway").show();
40
+ $(".knxmr-row-tunnel").hide();
41
+ }
42
+ };
43
+ $("#node-input-mode").on("change", toggleMode);
44
+ toggleMode();
23
45
  },
24
46
  oneditsave: function () {
25
47
  try { RED.sidebar.show("info"); } catch (error) { }
@@ -34,6 +56,14 @@
34
56
  <div class="form-row">
35
57
  <b><span data-i18n="knxUltimateMultiRouting.title"></span></b>
36
58
  <br/><br/>
59
+ <label for="node-input-mode"><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateMultiRouting.properties.mode"></span></label>
60
+ <select id="node-input-mode">
61
+ <option value="gateway" data-i18n="knxUltimateMultiRouting.properties.modeGateway"></option>
62
+ <option value="server" data-i18n="knxUltimateMultiRouting.properties.modeServer"></option>
63
+ </select>
64
+ </div>
65
+
66
+ <div class="form-row knxmr-row-gateway">
37
67
  <label for="node-input-server"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateMultiRouting.properties.server"></span></label>
38
68
  <input type="text" id="node-input-server">
39
69
  </div>
@@ -52,4 +82,34 @@
52
82
  <input type="checkbox" id="node-input-dropIfSameGateway" style="display:inline-block; width:auto; vertical-align:top;">
53
83
  <label style="width:auto" for="node-input-dropIfSameGateway">&nbsp;&nbsp;<span data-i18n="knxUltimateMultiRouting.properties.dropIfSameGateway"></span></label>
54
84
  </div>
55
- </script>
85
+
86
+ <div class="form-row knxmr-row-tunnel">
87
+ <hr/>
88
+ <b><span data-i18n="knxUltimateMultiRouting.properties.tunnelTitle"></span></b>
89
+ </div>
90
+
91
+ <div class="form-row knxmr-row-tunnel">
92
+ <label for="node-input-tunnelListenHost"><i class="fa fa-globe"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelListenHost"></span></label>
93
+ <input type="text" id="node-input-tunnelListenHost" placeholder="0.0.0.0">
94
+ </div>
95
+ <div class="form-row knxmr-row-tunnel">
96
+ <label for="node-input-tunnelListenPort"><i class="fa fa-plug"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelListenPort"></span></label>
97
+ <input type="text" id="node-input-tunnelListenPort" placeholder="3671">
98
+ </div>
99
+ <div class="form-row knxmr-row-tunnel">
100
+ <label for="node-input-tunnelAdvertiseHost"><i class="fa fa-bullhorn"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelAdvertiseHost"></span></label>
101
+ <input type="text" id="node-input-tunnelAdvertiseHost" data-i18n="[placeholder]knxUltimateMultiRouting.properties.tunnelAdvertiseHostPlaceholder">
102
+ </div>
103
+ <div class="form-row knxmr-row-tunnel">
104
+ <label for="node-input-tunnelAssignedIndividualAddress"><i class="fa fa-id-card-o"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelAssignedIndividualAddress"></span></label>
105
+ <input type="text" id="node-input-tunnelAssignedIndividualAddress" placeholder="15.15.255">
106
+ </div>
107
+ <div class="form-row knxmr-row-tunnel">
108
+ <label for="node-input-tunnelGatewayId"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelGatewayId"></span></label>
109
+ <input type="text" id="node-input-tunnelGatewayId" data-i18n="[placeholder]knxUltimateMultiRouting.properties.tunnelGatewayIdPlaceholder">
110
+ </div>
111
+ <div class="form-row knxmr-row-tunnel">
112
+ <label for="node-input-tunnelMaxSessions"><i class="fa fa-users"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelMaxSessions"></span></label>
113
+ <input type="text" id="node-input-tunnelMaxSessions" placeholder="1">
114
+ </div>
115
+ </script>
@@ -1,6 +1,18 @@
1
1
  // KNX Multi Routing - interconnect multiple KNX Ultimate gateways via Node-RED flows
2
2
  const loggerClass = require('./utils/sysLogger')
3
3
 
4
+ const safeNumber = (value, fallback) => {
5
+ const n = Number(value)
6
+ return Number.isFinite(n) ? n : fallback
7
+ }
8
+
9
+ const normalizeHex = (value) => {
10
+ if (value === undefined || value === null) return ''
11
+ const s = String(value).trim()
12
+ if (s === '') return ''
13
+ return s.replace(/^0x/i, '').replace(/[^0-9a-fA-F]/g, '')
14
+ }
15
+
4
16
  const bufferFromMaybe = (value) => {
5
17
  if (value === undefined || value === null) return null
6
18
  if (Buffer.isBuffer(value)) return value
@@ -21,8 +33,9 @@ module.exports = function (RED) {
21
33
  RED.nodes.createNode(this, config)
22
34
  const node = this
23
35
 
24
- node.serverKNX = RED.nodes.getNode(config.server) || undefined
25
- if (node.serverKNX === undefined) {
36
+ node.mode = config.mode || 'gateway' // 'gateway' | 'server'
37
+ node.serverKNX = (config.server && RED.nodes.getNode(config.server)) || undefined
38
+ if (node.mode !== 'server' && node.serverKNX === undefined) {
26
39
  node.status({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' })
27
40
  return
28
41
  }
@@ -49,13 +62,23 @@ module.exports = function (RED) {
49
62
  // Basic loop protection: drop messages already tagged as originating from this same gateway.
50
63
  node.dropIfSameGateway = config.dropIfSameGateway !== undefined ? (config.dropIfSameGateway === true || config.dropIfSameGateway === 'true') : true
51
64
 
65
+ // KNX/IP tunneling server (optional)
66
+ node.tunnelServer = null
67
+ node.tunnelSessions = new Set()
68
+ node.tunnelGatewayId = ''
69
+ node.tunnelAssignedIndividualAddress = ''
70
+
52
71
  const pushStatus = (status) => {
53
72
  if (!status) return
54
73
  const provider = node.serverKNX
55
- if (provider && typeof provider.applyStatusUpdate === 'function') {
56
- provider.applyStatusUpdate(node, status)
57
- } else {
58
- node.status(status)
74
+ try {
75
+ if (provider && typeof provider.applyStatusUpdate === 'function') {
76
+ provider.applyStatusUpdate(node, status)
77
+ } else {
78
+ node.status(status)
79
+ }
80
+ } catch (error) {
81
+ try { node.status(status) } catch (e2) { /* ignore */ }
59
82
  }
60
83
  }
61
84
 
@@ -67,7 +90,7 @@ module.exports = function (RED) {
67
90
  // Used to call the status update from the config node.
68
91
  node.setNodeStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => {
69
92
  try {
70
- if (node.serverKNX === null) { updateStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return }
93
+ if ((node.mode !== 'server') && (node.serverKNX === null || node.serverKNX === undefined)) { updateStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return }
71
94
  const dDate = new Date()
72
95
  const ts = (node.serverKNX && typeof node.serverKNX.formatStatusTimestamp === 'function')
73
96
  ? node.serverKNX.formatStatusTimestamp(dDate)
@@ -85,6 +108,13 @@ module.exports = function (RED) {
85
108
  node.sysLogger = new loggerClass({ loglevel: baseLogLevel, setPrefix: node.type + ' <' + (node.name || node.id || '') + '>' })
86
109
  } catch (error) { /* empty */ }
87
110
 
111
+ const localGatewayIds = () => {
112
+ const ids = new Set()
113
+ if (node.serverKNX && node.serverKNX.id) ids.add(String(node.serverKNX.id))
114
+ if (node.tunnelGatewayId) ids.add(String(node.tunnelGatewayId))
115
+ return ids
116
+ }
117
+
88
118
  // Called by knxUltimate-config.js to deliver bus telegrams (raw APDU + addresses)
89
119
  node.handleSend = (msg) => {
90
120
  try {
@@ -105,17 +135,18 @@ module.exports = function (RED) {
105
135
  const apdu = k.apdu || {}
106
136
  const apduData = bufferFromMaybe(apdu.data !== undefined ? apdu.data : (k.rawValue !== undefined ? k.rawValue : k.apduData))
107
137
  const bitlength = Number(apdu.bitlength !== undefined ? apdu.bitlength : (k.bitlength !== undefined ? k.bitlength : (apduData ? apduData.length * 8 : 0)))
138
+ const cemiHex = (k.cemi && (k.cemi.hex || k.cemi)) ? (k.cemi.hex || k.cemi) : ''
108
139
 
109
140
  const routing = (p && p.knxMultiRouting) ? p.knxMultiRouting : (msg && msg.knxMultiRouting ? msg.knxMultiRouting : null)
110
141
  const originGatewayId = routing && routing.gateway && routing.gateway.id ? String(routing.gateway.id) : ''
111
- return { event, destination, source, apduData, bitlength, originGatewayId }
142
+ return { event, destination, source, apduData, bitlength, originGatewayId, cemiHex }
112
143
  }
113
144
 
114
- const canForward = (parsed) => {
145
+ const canForwardToGateway = (parsed) => {
115
146
  if (!parsed) return false
116
147
  if (!parsed.destination || typeof parsed.destination !== 'string') return false
117
148
  if (!node.serverKNX || node.serverKNX.linkStatus !== 'connected' || !node.serverKNX.knxConnection) return false
118
- if (node.dropIfSameGateway && parsed.originGatewayId && node.serverKNX && node.serverKNX.id && parsed.originGatewayId === node.serverKNX.id) return false
149
+ if (node.dropIfSameGateway && parsed.originGatewayId && localGatewayIds().has(String(parsed.originGatewayId))) return false
119
150
  return true
120
151
  }
121
152
 
@@ -141,20 +172,121 @@ module.exports = function (RED) {
141
172
  }
142
173
  }
143
174
 
175
+ const canForwardToTunnel = (parsed) => {
176
+ if (!parsed) return false
177
+ if (!node.tunnelServer) return false
178
+ if (!parsed.cemiHex) return false
179
+ if (node.dropIfSameGateway && parsed.originGatewayId && localGatewayIds().has(String(parsed.originGatewayId))) return false
180
+ return true
181
+ }
182
+
183
+ const forwardToTunnel = (parsed) => {
184
+ const clean = normalizeHex(parsed.cemiHex)
185
+ if (!clean || clean.length % 2 !== 0) return
186
+ let KNXTunnelingRequest
187
+ try { ({ KNXTunnelingRequest } = require('knxultimate')) } catch (e) { throw new Error('knxultimate KNXTunnelingRequest not available') }
188
+ const cemi = KNXTunnelingRequest.parseCEMIMessage(Buffer.from(clean, 'hex'), 0)
189
+ node.tunnelServer.injectCemi(cemi)
190
+ }
191
+
192
+ const updateTunnelStatus = (extraText) => {
193
+ if (node.mode !== 'server') return
194
+ const addr = node.tunnelServer && typeof node.tunnelServer.address === 'object' ? node.tunnelServer.address : null
195
+ const host = addr ? addr.host : (config.tunnelListenHost || '0.0.0.0')
196
+ const port = addr ? addr.port : safeNumber(config.tunnelListenPort, 3671)
197
+ const sessions = node.tunnelSessions ? node.tunnelSessions.size : 0
198
+ const tail = extraText ? ` ${extraText}` : ''
199
+ updateStatus({ fill: 'green', shape: 'dot', text: `Tunnel ${host}:${port} sessions:${sessions}${tail}` })
200
+ }
201
+
202
+ const startTunnelServerIfNeeded = () => {
203
+ if (node.mode !== 'server') return
204
+ let KNXIPTunnelServer
205
+ try { ({ KNXIPTunnelServer } = require('knxultimate')) } catch (e) {
206
+ updateStatus({ fill: 'red', shape: 'dot', text: 'KNX/IP Server: knxultimate missing KNXIPTunnelServer' })
207
+ node.error('KNX/IP Server mode requires knxultimate with KNXIPTunnelServer exported.')
208
+ return
209
+ }
210
+
211
+ node.tunnelGatewayId = (config.tunnelGatewayId && String(config.tunnelGatewayId).trim()) || String(node.id)
212
+ node.tunnelAssignedIndividualAddress = (config.tunnelAssignedIndividualAddress && String(config.tunnelAssignedIndividualAddress).trim()) || '15.15.255'
213
+
214
+ const listenHost = (config.tunnelListenHost && String(config.tunnelListenHost).trim()) || '0.0.0.0'
215
+ const listenPort = safeNumber(config.tunnelListenPort, 3671)
216
+ const advertiseHost = (config.tunnelAdvertiseHost && String(config.tunnelAdvertiseHost).trim()) || undefined
217
+ const maxSessions = Math.max(1, safeNumber(config.tunnelMaxSessions, 1))
218
+
219
+ const loglevel = (node.serverKNX && node.serverKNX.loglevel) ? node.serverKNX.loglevel : 'error'
220
+
221
+ node.tunnelServer = new KNXIPTunnelServer({
222
+ listenHost,
223
+ listenPort,
224
+ advertiseHost,
225
+ assignedIndividualAddress: node.tunnelAssignedIndividualAddress,
226
+ maxSessions,
227
+ loglevel
228
+ })
229
+
230
+ node.tunnelServer.on('error', (err) => {
231
+ updateStatus({ fill: 'red', shape: 'dot', text: `Tunnel error: ${err.message}` })
232
+ node.error(err)
233
+ })
234
+
235
+ node.tunnelServer.on('listening', () => {
236
+ updateTunnelStatus('')
237
+ })
238
+
239
+ node.tunnelServer.on('sessionUp', (s) => {
240
+ try { node.tunnelSessions.add(s.channelId) } catch (e) { /* ignore */ }
241
+ updateTunnelStatus('client connected')
242
+ })
243
+
244
+ node.tunnelServer.on('sessionDown', (s) => {
245
+ try { if (s && s.channelId) node.tunnelSessions.delete(s.channelId) } catch (e) { /* ignore */ }
246
+ updateTunnelStatus('client disconnected')
247
+ })
248
+
249
+ node.tunnelServer.on('rawTelegram', (knx, info) => {
250
+ try {
251
+ const msg = {
252
+ topic: node.outputtopic || knx.destination,
253
+ payload: {
254
+ knx,
255
+ knxMultiRouting: {
256
+ gateway: { id: node.tunnelGatewayId, name: node.name || '', physAddr: node.tunnelAssignedIndividualAddress || '' },
257
+ receivedAt: Date.now(),
258
+ tunnel: { channelId: info && info.channelId ? info.channelId : undefined }
259
+ }
260
+ }
261
+ }
262
+ node.send(msg)
263
+ } catch (error) {
264
+ node.sysLogger?.error(`knxUltimateMultiRouting: tunnel rawTelegram output error: ${error.message}`)
265
+ }
266
+ })
267
+
268
+ updateStatus({ fill: 'grey', shape: 'dot', text: 'Starting KNX/IP Server...' })
269
+ Promise.resolve()
270
+ .then(() => node.tunnelServer.start())
271
+ .then(() => updateTunnelStatus(''))
272
+ .catch((err) => {
273
+ updateStatus({ fill: 'red', shape: 'dot', text: `Tunnel start failed: ${err.message}` })
274
+ node.error(err)
275
+ })
276
+ }
277
+
144
278
  node.on('input', function (msg) {
145
279
  try {
146
280
  const parsed = parseIncoming(msg)
147
- if (!canForward(parsed)) return
148
- forwardToBus(parsed)
149
- node.setNodeStatus({
150
- fill: 'green',
151
- shape: 'dot',
152
- text: 'Forwarded',
153
- payload: parsed.event || '',
154
- GA: parsed.destination,
155
- dpt: '',
156
- devicename: parsed.source || ''
157
- })
281
+ if (node.mode === 'server') {
282
+ if (!canForwardToTunnel(parsed)) return
283
+ forwardToTunnel(parsed)
284
+ } else {
285
+ if (!canForwardToGateway(parsed)) return
286
+ forwardToBus(parsed)
287
+ }
288
+
289
+ node.setNodeStatus({ fill: 'green', shape: 'dot', text: 'Forwarded', payload: parsed.event || '', GA: parsed.destination, dpt: '', devicename: parsed.source || '' })
158
290
  } catch (error) {
159
291
  node.setNodeStatus({ fill: 'red', shape: 'dot', text: `Forward error: ${error.message || error}`, payload: '', GA: '', dpt: '', devicename: '' })
160
292
  node.error(error)
@@ -162,19 +294,27 @@ module.exports = function (RED) {
162
294
  })
163
295
 
164
296
  node.on('close', function (done) {
165
- if (node.serverKNX) {
166
- node.serverKNX.removeClient(node)
297
+ const shutdown = async () => {
298
+ if (node.serverKNX) {
299
+ try { node.serverKNX.removeClient(node) } catch (e) { /* ignore */ }
300
+ }
301
+ if (node.tunnelServer) {
302
+ try { await node.tunnelServer.stop() } catch (e) { /* ignore */ }
303
+ node.tunnelServer = null
304
+ }
167
305
  }
168
- done()
306
+ Promise.resolve(shutdown()).then(() => done()).catch(() => done())
169
307
  })
170
308
 
171
309
  // On each deploy, unsubscribe+resubscribe
172
310
  if (node.serverKNX) {
173
- node.serverKNX.removeClient(node)
174
- node.serverKNX.addClient(node)
311
+ try { node.serverKNX.removeClient(node) } catch (e) { /* ignore */ }
312
+ try { node.serverKNX.addClient(node) } catch (e) { /* ignore */ }
175
313
  }
176
314
 
177
- updateStatus({ fill: 'grey', shape: 'dot', text: 'Routing ready' })
315
+ startTunnelServerIfNeeded()
316
+
317
+ if (node.mode !== 'server') updateStatus({ fill: 'grey', shape: 'dot', text: 'Routing ready' })
178
318
  }
179
319
 
180
320
  RED.nodes.registerType('knxUltimateMultiRouting', knxUltimateMultiRouting)
@@ -2,10 +2,23 @@
2
2
  "knxUltimateMultiRouting": {
3
3
  "title": "KNX Multi Routing",
4
4
  "properties": {
5
+ "mode": "Mode",
6
+ "modeGateway": "Gateway / Routing",
7
+ "modeServer": "Server KNX/IP",
5
8
  "server": "Gateway",
6
9
  "name": "Name",
7
10
  "outputtopic": "Output topic",
8
- "dropIfSameGateway": "Drop messages already tagged for this gateway"
11
+ "dropIfSameGateway": "Drop messages already tagged for this gateway",
12
+ "tunnelTitle": "KNX/IP Server",
13
+ "tunnelListenHost": "Listen host",
14
+ "tunnelListenPort": "Listen port",
15
+ "tunnelAdvertiseHost": "Advertise host",
16
+ "tunnelAdvertiseHostPlaceholder": "(optional) Host/IP to advertise to clients",
17
+ "tunnelAssignedIndividualAddress": "Assigned individual address",
18
+ "tunnelGatewayId": "Gateway id (tag)",
19
+ "tunnelGatewayIdPlaceholder": "(auto) Used as knxMultiRouting.gateway.id",
20
+ "tunnelMaxSessions": "Max sessions",
21
+ "bridgeGateway": "Bridge tunnel <-> selected gateway"
9
22
  },
10
23
  "outputs": {
11
24
  "raw": "RAW telegrams"
@@ -4,6 +4,10 @@ This node is used to **bridge multiple KNX Ultimate gateways** (multiple `knxUlt
4
4
  It outputs **RAW telegram information** (APDU + cEMI hex + addresses) for every telegram received from the KNX bus of the selected gateway.
5
5
  It can also accept those RAW telegram objects on its input and forward them to the selected gateway.
6
6
 
7
+ ## Server KNX/IP mode
8
+ Set **Mode** to **Server KNX/IP** to start an embedded KNXnet/IP tunneling server (UDP). Incoming client telegrams are emitted as the same RAW format used by MultiRouting.
9
+ The node also accepts RAW telegram objects on its input and injects them to the connected tunneling client(s).
10
+
7
11
  ## Output message format
8
12
  `msg.payload` contains:
9
13
  - `knx.event`: `GroupValue_Write` / `GroupValue_Response` / `GroupValue_Read`
@@ -2,10 +2,23 @@
2
2
  "knxUltimateMultiRouting": {
3
3
  "title": "KNX Multi Routing",
4
4
  "properties": {
5
+ "mode": "Mode",
6
+ "modeGateway": "Gateway / Routing",
7
+ "modeServer": "Server KNX/IP",
5
8
  "server": "Gateway",
6
9
  "name": "Name",
7
10
  "outputtopic": "Output topic",
8
- "dropIfSameGateway": "Drop messages already tagged for this gateway"
11
+ "dropIfSameGateway": "Drop messages already tagged for this gateway",
12
+ "tunnelTitle": "KNX/IP Server",
13
+ "tunnelListenHost": "Listen host",
14
+ "tunnelListenPort": "Listen port",
15
+ "tunnelAdvertiseHost": "Advertise host",
16
+ "tunnelAdvertiseHostPlaceholder": "(optional) Host/IP to advertise to clients",
17
+ "tunnelAssignedIndividualAddress": "Assigned individual address",
18
+ "tunnelGatewayId": "Gateway id (tag)",
19
+ "tunnelGatewayIdPlaceholder": "(auto) Used as knxMultiRouting.gateway.id",
20
+ "tunnelMaxSessions": "Max sessions",
21
+ "bridgeGateway": "Bridge tunnel <-> selected gateway"
9
22
  },
10
23
  "outputs": {
11
24
  "raw": "RAW telegrams"
@@ -2,10 +2,23 @@
2
2
  "knxUltimateMultiRouting": {
3
3
  "title": "KNX Multi Routing",
4
4
  "properties": {
5
+ "mode": "Mode",
6
+ "modeGateway": "Gateway / Routing",
7
+ "modeServer": "Server KNX/IP",
5
8
  "server": "Gateway",
6
9
  "name": "Name",
7
10
  "outputtopic": "Output topic",
8
- "dropIfSameGateway": "Drop messages already tagged for this gateway"
11
+ "dropIfSameGateway": "Drop messages already tagged for this gateway",
12
+ "tunnelTitle": "KNX/IP Server",
13
+ "tunnelListenHost": "Listen host",
14
+ "tunnelListenPort": "Listen port",
15
+ "tunnelAdvertiseHost": "Advertise host",
16
+ "tunnelAdvertiseHostPlaceholder": "(optional) Host/IP to advertise to clients",
17
+ "tunnelAssignedIndividualAddress": "Assigned individual address",
18
+ "tunnelGatewayId": "Gateway id (tag)",
19
+ "tunnelGatewayIdPlaceholder": "(auto) Used as knxMultiRouting.gateway.id",
20
+ "tunnelMaxSessions": "Max sessions",
21
+ "bridgeGateway": "Bridge tunnel <-> selected gateway"
9
22
  },
10
23
  "outputs": {
11
24
  "raw": "RAW telegrams"
@@ -2,10 +2,23 @@
2
2
  "knxUltimateMultiRouting": {
3
3
  "title": "KNX Multi Routing",
4
4
  "properties": {
5
+ "mode": "Mode",
6
+ "modeGateway": "Gateway / Routing",
7
+ "modeServer": "Server KNX/IP",
5
8
  "server": "Gateway",
6
9
  "name": "Name",
7
10
  "outputtopic": "Output topic",
8
- "dropIfSameGateway": "Drop messages already tagged for this gateway"
11
+ "dropIfSameGateway": "Drop messages already tagged for this gateway",
12
+ "tunnelTitle": "KNX/IP Server",
13
+ "tunnelListenHost": "Listen host",
14
+ "tunnelListenPort": "Listen port",
15
+ "tunnelAdvertiseHost": "Advertise host",
16
+ "tunnelAdvertiseHostPlaceholder": "(optional) Host/IP to advertise to clients",
17
+ "tunnelAssignedIndividualAddress": "Assigned individual address",
18
+ "tunnelGatewayId": "Gateway id (tag)",
19
+ "tunnelGatewayIdPlaceholder": "(auto) Used as knxMultiRouting.gateway.id",
20
+ "tunnelMaxSessions": "Max sessions",
21
+ "bridgeGateway": "Bridge tunnel <-> selected gateway"
9
22
  },
10
23
  "outputs": {
11
24
  "raw": "RAW telegrams"
@@ -4,6 +4,10 @@ Questo nodo serve per **collegare tra loro più gateway KNX Ultimate** (più `kn
4
4
  In output emette un oggetto con le informazioni **RAW** del telegramma (APDU + cEMI hex + indirizzi) per ogni telegramma ricevuto dal BUS KNX del gateway selezionato.
5
5
  In input può ricevere gli stessi oggetti RAW e inoltrarli sul BUS KNX del gateway selezionato.
6
6
 
7
+ ## Modalità Server KNX/IP
8
+ Imposta **Modalità** su **Server KNX/IP** per avviare un server KNXnet/IP tunneling (UDP). I telegrammi ricevuti dai client vengono emessi nello stesso formato RAW usato dal MultiRouting.
9
+ Il nodo accetta anche in input gli oggetti RAW e li inietta verso i client tunneling connessi.
10
+
7
11
  ## Formato messaggio in output
8
12
  `msg.payload` contiene:
9
13
  - `knx.event`: `GroupValue_Write` / `GroupValue_Response` / `GroupValue_Read`
@@ -2,10 +2,23 @@
2
2
  "knxUltimateMultiRouting": {
3
3
  "title": "KNX Multi Routing",
4
4
  "properties": {
5
+ "mode": "Modalità",
6
+ "modeGateway": "Gateway / Routing",
7
+ "modeServer": "Server KNX/IP",
5
8
  "server": "Gateway",
6
9
  "name": "Nome",
7
10
  "outputtopic": "Topic output",
8
- "dropIfSameGateway": "Scarta messaggi già marcati per questo gateway"
11
+ "dropIfSameGateway": "Scarta messaggi già marcati per questo gateway",
12
+ "tunnelTitle": "Server KNX/IP",
13
+ "tunnelListenHost": "Host in ascolto",
14
+ "tunnelListenPort": "Porta in ascolto",
15
+ "tunnelAdvertiseHost": "Host annunciato",
16
+ "tunnelAdvertiseHostPlaceholder": "(opzionale) Host/IP da annunciare ai client",
17
+ "tunnelAssignedIndividualAddress": "Indirizzo individuale assegnato",
18
+ "tunnelGatewayId": "Gateway id (tag)",
19
+ "tunnelGatewayIdPlaceholder": "(auto) Usato come knxMultiRouting.gateway.id",
20
+ "tunnelMaxSessions": "Sessioni max",
21
+ "bridgeGateway": "Bridge tunnel <-> gateway selezionato"
9
22
  },
10
23
  "outputs": {
11
24
  "raw": "Telegrammi RAW"
@@ -2,10 +2,23 @@
2
2
  "knxUltimateMultiRouting": {
3
3
  "title": "KNX Multi Routing",
4
4
  "properties": {
5
+ "mode": "Mode",
6
+ "modeGateway": "Gateway / Routing",
7
+ "modeServer": "Server KNX/IP",
5
8
  "server": "Gateway",
6
9
  "name": "Name",
7
10
  "outputtopic": "Output topic",
8
- "dropIfSameGateway": "Drop messages already tagged for this gateway"
11
+ "dropIfSameGateway": "Drop messages already tagged for this gateway",
12
+ "tunnelTitle": "KNX/IP Server",
13
+ "tunnelListenHost": "Listen host",
14
+ "tunnelListenPort": "Listen port",
15
+ "tunnelAdvertiseHost": "Advertise host",
16
+ "tunnelAdvertiseHostPlaceholder": "(optional) Host/IP to advertise to clients",
17
+ "tunnelAssignedIndividualAddress": "Assigned individual address",
18
+ "tunnelGatewayId": "Gateway id (tag)",
19
+ "tunnelGatewayIdPlaceholder": "(auto) Used as knxMultiRouting.gateway.id",
20
+ "tunnelMaxSessions": "Max sessions",
21
+ "bridgeGateway": "Bridge tunnel <-> selected gateway"
9
22
  },
10
23
  "outputs": {
11
24
  "raw": "RAW telegrams"
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "4.1.18",
6
+ "version": "4.1.19",
7
7
  "description": "Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, and KNX routing between interfaces. Easy to use and highly configurable.",
8
8
  "files": [
9
9
  "nodes/",
@@ -20,7 +20,7 @@
20
20
  "crypto-js": "4.2.0",
21
21
  "dns-sync": "0.2.1",
22
22
  "js-yaml": "4.1.1",
23
- "knxultimate": "5.2.6",
23
+ "knxultimate": "5.2.7",
24
24
  "lodash": "4.17.21",
25
25
  "mkdirp": "3.0.1",
26
26
  "node-color-log": "12.0.1",