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

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,19 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 4.1.18** - January 2026<br/>
9
+ **Version 4.1.21** - January 2026<br/>
10
+
11
+ - NEW: **KNX Multi Routing**: added KNX routing counter (hop count) handling to prevent telegram loops: optional **Respect routing counter (drop if 0)** and **Decrement routing counter when routing**.<br/>
12
+ - FIX: **KNX Multi Routing**: improved behaviour with rewritten telegrams by relying on coherent cEMI; `knx.routingCounter` is exposed based on `knx.cemi.hex`.<br/>
13
+ - CHANGE: **KNX Router Filter**: cEMI consistency is now always enforced when rewriting `knx.source`/`knx.destination` (updates `knx.cemi.hex` accordingly; removed the related toggle option).<br/>
14
+ - CHANGE: **KNX Router Filter**: added `cemiSynced` metadata on passed messages (`msg.payload.knxRouterFilter.cemiSynced`).<br/>
15
+ - NEW: **KNX Router Filter**: runtime configuration via `msg.setConfig` (all node parameters), retained until next `msg.setConfig` or redeploy/restart; config messages are not forwarded.<br/>
16
+ - IMPROVE: **KNX Multi Routing Server KNX/IP**: better status/diagnostics and docs note about **Advertise host** for clients that show “connected” but the server receives no telegrams (multi-homed/Docker/VM).<br/>
17
+ - Docs/help/wiki: updated **KNX Multi Routing** and **KNX Router Filter** pages in all supported languages to document routing counter, rewrite+cEMI sync behaviour, and `msg.setConfig` usage (with examples).<br/>
18
+
19
+ **Version 4.1.19** - February 2026<br/>
20
+
21
+ - 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
22
 
11
23
  - NEW: KNX AI sidebar: added another TAB in the Node-Red's toolbar buttons dedicated to AI analysis.<br>
12
24
  - NEW: added **KNX AI** node (traffic analyzer + optional LLM assistant) and **KNX AI** sidebar tab.<br/>
@@ -18,7 +30,7 @@
18
30
  - 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
31
  - Docs: added the new nodes to the wiki navbar in all languages (so they appear in the left navigation menu).<br/>
20
32
  - 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/>
33
+ - 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
34
 
23
35
  **Version 4.1.15** - January 2026<br/>
24
36
 
@@ -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,21 @@
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
+ respectRoutingCounter: { value: true },
14
+ decrementRoutingCounter: { value: false },
15
+
16
+ // KNX/IP tunneling server options (mode=server)
17
+ tunnelListenHost: { value: "0.0.0.0", required: false },
18
+ tunnelListenPort: { value: 3671, required: false, validate: RED.validators.number() },
19
+ tunnelAdvertiseHost: { value: "", required: false },
20
+ tunnelAssignedIndividualAddress: { value: "15.15.255", required: false },
21
+ tunnelMaxSessions: { value: 1, required: false, validate: RED.validators.number() },
22
+ tunnelGatewayId: { value: "", required: false }
12
23
  },
13
24
  inputs: 1,
14
25
  outputs: 1,
@@ -20,6 +31,19 @@
20
31
  paletteLabel: "KNX Multi Routing",
21
32
  oneditprepare: function () {
22
33
  try { RED.sidebar.show("help"); } catch (error) { }
34
+
35
+ const toggleMode = () => {
36
+ const mode = $("#node-input-mode").val() || "gateway";
37
+ if (mode === "server") {
38
+ $(".knxmr-row-gateway").hide();
39
+ $(".knxmr-row-tunnel").show();
40
+ } else {
41
+ $(".knxmr-row-gateway").show();
42
+ $(".knxmr-row-tunnel").hide();
43
+ }
44
+ };
45
+ $("#node-input-mode").on("change", toggleMode);
46
+ toggleMode();
23
47
  },
24
48
  oneditsave: function () {
25
49
  try { RED.sidebar.show("info"); } catch (error) { }
@@ -34,6 +58,14 @@
34
58
  <div class="form-row">
35
59
  <b><span data-i18n="knxUltimateMultiRouting.title"></span></b>
36
60
  <br/><br/>
61
+ <label for="node-input-mode"><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateMultiRouting.properties.mode"></span></label>
62
+ <select id="node-input-mode">
63
+ <option value="gateway" data-i18n="knxUltimateMultiRouting.properties.modeGateway"></option>
64
+ <option value="server" data-i18n="knxUltimateMultiRouting.properties.modeServer"></option>
65
+ </select>
66
+ </div>
67
+
68
+ <div class="form-row knxmr-row-gateway">
37
69
  <label for="node-input-server"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateMultiRouting.properties.server"></span></label>
38
70
  <input type="text" id="node-input-server">
39
71
  </div>
@@ -52,4 +84,44 @@
52
84
  <input type="checkbox" id="node-input-dropIfSameGateway" style="display:inline-block; width:auto; vertical-align:top;">
53
85
  <label style="width:auto" for="node-input-dropIfSameGateway">&nbsp;&nbsp;<span data-i18n="knxUltimateMultiRouting.properties.dropIfSameGateway"></span></label>
54
86
  </div>
55
- </script>
87
+
88
+ <div class="form-row">
89
+ <input type="checkbox" id="node-input-respectRoutingCounter" style="display:inline-block; width:auto; vertical-align:top;">
90
+ <label style="width:auto" for="node-input-respectRoutingCounter">&nbsp;&nbsp;<span data-i18n="knxUltimateMultiRouting.properties.respectRoutingCounter"></span></label>
91
+ </div>
92
+
93
+ <div class="form-row">
94
+ <input type="checkbox" id="node-input-decrementRoutingCounter" style="display:inline-block; width:auto; vertical-align:top;">
95
+ <label style="width:auto" for="node-input-decrementRoutingCounter">&nbsp;&nbsp;<span data-i18n="knxUltimateMultiRouting.properties.decrementRoutingCounter"></span></label>
96
+ </div>
97
+
98
+ <div class="form-row knxmr-row-tunnel">
99
+ <hr/>
100
+ <b><span data-i18n="knxUltimateMultiRouting.properties.tunnelTitle"></span></b>
101
+ </div>
102
+
103
+ <div class="form-row knxmr-row-tunnel">
104
+ <label for="node-input-tunnelListenHost"><i class="fa fa-globe"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelListenHost"></span></label>
105
+ <input type="text" id="node-input-tunnelListenHost" placeholder="0.0.0.0">
106
+ </div>
107
+ <div class="form-row knxmr-row-tunnel">
108
+ <label for="node-input-tunnelListenPort"><i class="fa fa-plug"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelListenPort"></span></label>
109
+ <input type="text" id="node-input-tunnelListenPort" placeholder="3671">
110
+ </div>
111
+ <div class="form-row knxmr-row-tunnel">
112
+ <label for="node-input-tunnelAdvertiseHost"><i class="fa fa-bullhorn"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelAdvertiseHost"></span></label>
113
+ <input type="text" id="node-input-tunnelAdvertiseHost" data-i18n="[placeholder]knxUltimateMultiRouting.properties.tunnelAdvertiseHostPlaceholder">
114
+ </div>
115
+ <div class="form-row knxmr-row-tunnel">
116
+ <label for="node-input-tunnelAssignedIndividualAddress"><i class="fa fa-id-card-o"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelAssignedIndividualAddress"></span></label>
117
+ <input type="text" id="node-input-tunnelAssignedIndividualAddress" placeholder="15.15.255">
118
+ </div>
119
+ <div class="form-row knxmr-row-tunnel">
120
+ <label for="node-input-tunnelGatewayId"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelGatewayId"></span></label>
121
+ <input type="text" id="node-input-tunnelGatewayId" data-i18n="[placeholder]knxUltimateMultiRouting.properties.tunnelGatewayIdPlaceholder">
122
+ </div>
123
+ <div class="form-row knxmr-row-tunnel">
124
+ <label for="node-input-tunnelMaxSessions"><i class="fa fa-users"></i> <span data-i18n="knxUltimateMultiRouting.properties.tunnelMaxSessions"></span></label>
125
+ <input type="text" id="node-input-tunnelMaxSessions" placeholder="1">
126
+ </div>
127
+ </script>