node-red-contrib-knx-ultimate 4.1.19 → 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,6 +6,16 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
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
+
9
19
  **Version 4.1.19** - February 2026<br/>
10
20
 
11
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,6 +10,8 @@
10
10
  name: { value: "KNX Multi Routing", required: false },
11
11
  outputtopic: { value: "", required: false },
12
12
  dropIfSameGateway: { value: true },
13
+ respectRoutingCounter: { value: true },
14
+ decrementRoutingCounter: { value: false },
13
15
 
14
16
  // KNX/IP tunneling server options (mode=server)
15
17
  tunnelListenHost: { value: "0.0.0.0", required: false },
@@ -83,6 +85,16 @@
83
85
  <label style="width:auto" for="node-input-dropIfSameGateway">&nbsp;&nbsp;<span data-i18n="knxUltimateMultiRouting.properties.dropIfSameGateway"></span></label>
84
86
  </div>
85
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
+
86
98
  <div class="form-row knxmr-row-tunnel">
87
99
  <hr/>
88
100
  <b><span data-i18n="knxUltimateMultiRouting.properties.tunnelTitle"></span></b>
@@ -1,5 +1,16 @@
1
1
  // KNX Multi Routing - interconnect multiple KNX Ultimate gateways via Node-RED flows
2
2
  const loggerClass = require('./utils/sysLogger')
3
+ const os = require('os')
4
+
5
+ const toBoolean = (value, fallback) => {
6
+ if (value === undefined || value === null) return fallback
7
+ if (typeof value === 'boolean') return value
8
+ if (typeof value === 'number') return value !== 0
9
+ const s = String(value).trim().toLowerCase()
10
+ if (s === 'true' || s === '1' || s === 'yes' || s === 'on') return true
11
+ if (s === 'false' || s === '0' || s === 'no' || s === 'off') return false
12
+ return fallback
13
+ }
3
14
 
4
15
  const safeNumber = (value, fallback) => {
5
16
  const n = Number(value)
@@ -28,6 +39,34 @@ const bufferFromMaybe = (value) => {
28
39
  return null
29
40
  }
30
41
 
42
+ const CEMI_L_DATA_REQ = 0x11
43
+ const CEMI_L_DATA_IND = 0x29
44
+
45
+ const isWildcardHost = (host) => {
46
+ const h = host === undefined || host === null ? '' : String(host).trim()
47
+ return h === '' || h === '0.0.0.0' || h === '::' || h === '::0'
48
+ }
49
+
50
+ const guessAdvertiseHost = (listenHost) => {
51
+ if (listenHost && !isWildcardHost(listenHost)) return String(listenHost)
52
+ try {
53
+ const ifaces = os.networkInterfaces()
54
+ for (const entries of Object.values(ifaces)) {
55
+ for (const entry of entries || []) {
56
+ if (entry.family === 'IPv4' && !entry.internal) return entry.address
57
+ }
58
+ }
59
+ } catch (e) { /* ignore */ }
60
+ return '127.0.0.1'
61
+ }
62
+
63
+ let _knxultimateCache = null
64
+ const getKnxultimate = () => {
65
+ if (_knxultimateCache) return _knxultimateCache
66
+ _knxultimateCache = require('knxultimate')
67
+ return _knxultimateCache
68
+ }
69
+
31
70
  module.exports = function (RED) {
32
71
  function knxUltimateMultiRouting (config) {
33
72
  RED.nodes.createNode(this, config)
@@ -61,12 +100,18 @@ module.exports = function (RED) {
61
100
  // Forwarding controls (input -> KNX bus)
62
101
  // Basic loop protection: drop messages already tagged as originating from this same gateway.
63
102
  node.dropIfSameGateway = config.dropIfSameGateway !== undefined ? (config.dropIfSameGateway === true || config.dropIfSameGateway === 'true') : true
103
+ node.respectRoutingCounter = toBoolean(config.respectRoutingCounter, true)
104
+ node.decrementRoutingCounter = toBoolean(config.decrementRoutingCounter, false)
64
105
 
65
106
  // KNX/IP tunneling server (optional)
66
107
  node.tunnelServer = null
67
108
  node.tunnelSessions = new Set()
68
109
  node.tunnelGatewayId = ''
69
110
  node.tunnelAssignedIndividualAddress = ''
111
+ node.tunnelAdvertiseHostTimer = null
112
+ node.tunnelStatusRefreshTimer = null
113
+ node.tunnelRxCount = 0
114
+ node.tunnelLastRxAt = 0
70
115
 
71
116
  const pushStatus = (status) => {
72
117
  if (!status) return
@@ -118,12 +163,80 @@ module.exports = function (RED) {
118
163
  // Called by knxUltimate-config.js to deliver bus telegrams (raw APDU + addresses)
119
164
  node.handleSend = (msg) => {
120
165
  try {
121
- node.send(msg)
166
+ const processed = applyRoutingCounterOnOutboundMsg(msg)
167
+ if (!processed) return
168
+ node.send(processed)
122
169
  } catch (error) {
123
170
  node.sysLogger?.error(`knxUltimateMultiRouting: output error: ${error.message}`)
124
171
  }
125
172
  }
126
173
 
174
+ const tryParseCemiHex = (cemiHex) => {
175
+ const clean = normalizeHex(cemiHex)
176
+ if (!clean || clean.length % 2 !== 0) return null
177
+ let KNXTunnelingRequest
178
+ try { ({ KNXTunnelingRequest } = getKnxultimate()) } catch (e) { return null }
179
+ try { return KNXTunnelingRequest.parseCEMIMessage(Buffer.from(clean, 'hex'), 0) } catch (e) { return null }
180
+ }
181
+
182
+ const getHopCountFromCemiHex = (cemiHex) => {
183
+ const cemi = tryParseCemiHex(cemiHex)
184
+ const hop = cemi && cemi.control ? Number(cemi.control.hopCount) : NaN
185
+ return Number.isFinite(hop) ? hop : null
186
+ }
187
+
188
+ const decrementHopCountInCemiHex = (cemiHex) => {
189
+ const cemi = tryParseCemiHex(cemiHex)
190
+ if (!cemi || !cemi.control) return null
191
+ const hop = Number(cemi.control.hopCount)
192
+ if (!Number.isFinite(hop)) return null
193
+ if (hop <= 0) return { oldHopCount: hop, newHopCount: hop, cemiHex: cemi.toBuffer().toString('hex') }
194
+ const newHop = hop - 1
195
+ cemi.control.hopCount = newHop
196
+ return { oldHopCount: hop, newHopCount: newHop, cemiHex: cemi.toBuffer().toString('hex') }
197
+ }
198
+
199
+ const applyRoutingCounterOnOutboundMsg = (msg) => {
200
+ if (!msg) return msg
201
+ const p = msg.payload !== undefined ? msg.payload : msg
202
+ const k = (p && p.knx) ? p.knx : (msg && msg.knx ? msg.knx : null)
203
+ if (!k || typeof k !== 'object') return msg
204
+ const cemiHex = (k.cemi && (k.cemi.hex || k.cemi)) ? (k.cemi.hex || k.cemi) : ''
205
+ if (!cemiHex) return msg
206
+
207
+ const hopCount = getHopCountFromCemiHex(cemiHex)
208
+ if (hopCount !== null) {
209
+ try { k.routingCounter = hopCount } catch (e) { /* ignore */ }
210
+ try {
211
+ if (k.cemi && typeof k.cemi === 'object') k.cemi.hopCount = hopCount
212
+ } catch (e) { /* ignore */ }
213
+ }
214
+
215
+ if (node.respectRoutingCounter && hopCount === 0) {
216
+ node.setNodeStatus({ fill: 'grey', shape: 'ring', text: 'Dropped (routing counter 0)', payload: k.event || '', GA: k.destination || '', dpt: '', devicename: k.source || '' })
217
+ return null
218
+ }
219
+
220
+ if (!node.decrementRoutingCounter) return msg
221
+ if (hopCount === null) return msg
222
+
223
+ const dec = decrementHopCountInCemiHex(cemiHex)
224
+ if (!dec) return msg
225
+
226
+ if (node.respectRoutingCounter && dec.newHopCount === 0) {
227
+ node.setNodeStatus({ fill: 'grey', shape: 'ring', text: 'Dropped (routing counter 0)', payload: k.event || '', GA: k.destination || '', dpt: '', devicename: k.source || '' })
228
+ return null
229
+ }
230
+
231
+ try {
232
+ if (k.cemi && typeof k.cemi === 'object' && 'hex' in k.cemi) k.cemi.hex = dec.cemiHex
233
+ else k.cemi = dec.cemiHex
234
+ } catch (e) { /* ignore */ }
235
+ try { k.routingCounter = dec.newHopCount } catch (e) { /* ignore */ }
236
+ try { if (k.cemi && typeof k.cemi === 'object') k.cemi.hopCount = dec.newHopCount } catch (e) { /* ignore */ }
237
+ return msg
238
+ }
239
+
127
240
  const parseIncoming = (msg) => {
128
241
  const p = (msg && msg.payload !== undefined) ? msg.payload : msg
129
242
  const k = (p && p.knx) ? p.knx : (msg && msg.knx ? msg.knx : p)
@@ -136,10 +249,11 @@ module.exports = function (RED) {
136
249
  const apduData = bufferFromMaybe(apdu.data !== undefined ? apdu.data : (k.rawValue !== undefined ? k.rawValue : k.apduData))
137
250
  const bitlength = Number(apdu.bitlength !== undefined ? apdu.bitlength : (k.bitlength !== undefined ? k.bitlength : (apduData ? apduData.length * 8 : 0)))
138
251
  const cemiHex = (k.cemi && (k.cemi.hex || k.cemi)) ? (k.cemi.hex || k.cemi) : ''
252
+ const hopCount = cemiHex ? getHopCountFromCemiHex(cemiHex) : null
139
253
 
140
254
  const routing = (p && p.knxMultiRouting) ? p.knxMultiRouting : (msg && msg.knxMultiRouting ? msg.knxMultiRouting : null)
141
255
  const originGatewayId = routing && routing.gateway && routing.gateway.id ? String(routing.gateway.id) : ''
142
- return { event, destination, source, apduData, bitlength, originGatewayId, cemiHex }
256
+ return { event, destination, source, apduData, bitlength, originGatewayId, cemiHex, hopCount }
143
257
  }
144
258
 
145
259
  const canForwardToGateway = (parsed) => {
@@ -147,11 +261,55 @@ module.exports = function (RED) {
147
261
  if (!parsed.destination || typeof parsed.destination !== 'string') return false
148
262
  if (!node.serverKNX || node.serverKNX.linkStatus !== 'connected' || !node.serverKNX.knxConnection) return false
149
263
  if (node.dropIfSameGateway && parsed.originGatewayId && localGatewayIds().has(String(parsed.originGatewayId))) return false
264
+ if (node.respectRoutingCounter && parsed.hopCount === 0) return false
150
265
  return true
151
266
  }
152
267
 
153
268
  const forwardToBus = (parsed) => {
154
269
  const client = node.serverKNX.knxConnection
270
+ if (parsed.cemiHex) {
271
+ const cemi = tryParseCemiHex(parsed.cemiHex)
272
+ if (cemi && cemi.control) {
273
+ // Apply routing-counter checks while forwarding to the BUS.
274
+ const hop = Number(cemi.control.hopCount)
275
+ if (Number.isFinite(hop)) {
276
+ if (node.respectRoutingCounter && hop === 0) return
277
+ }
278
+
279
+ const isSerial = typeof client.isSerialTransport === 'function' ? client.isSerialTransport() : false
280
+ const hostProtocol = client && client._options && client._options.hostProtocol ? String(client._options.hostProtocol) : ''
281
+ let KNXProtocol
282
+ try { ({ KNXProtocol } = getKnxultimate()) } catch (e) { KNXProtocol = null }
283
+ if (!KNXProtocol || typeof client.send !== 'function') {
284
+ // fall back to legacy methods below
285
+ } else if (hostProtocol === 'Multicast' || isSerial) {
286
+ cemi.msgCode = hostProtocol === 'Multicast' ? CEMI_L_DATA_IND : CEMI_L_DATA_REQ
287
+ const knxPacketRequest = KNXProtocol.newKNXRoutingIndication(cemi)
288
+ const expected = (typeof client.getSeqNumber === 'function') ? client.getSeqNumber() : 0
289
+ client.send(knxPacketRequest, undefined, false, expected)
290
+ return
291
+ } else {
292
+ // Tunneling
293
+ cemi.msgCode = CEMI_L_DATA_REQ
294
+ try {
295
+ if (hostProtocol === 'TunnelTCP') cemi.control.ack = 0
296
+ else cemi.control.ack = (client._options && client._options.suppress_ack_ldatareq) ? 0 : 1
297
+ } catch (e) { /* ignore */ }
298
+
299
+ const seqNum = (hostProtocol === 'TunnelTCP' && typeof client.secureIncTunnelSeq === 'function')
300
+ ? client.secureIncTunnelSeq()
301
+ : (typeof client.incSeqNumber === 'function' ? client.incSeqNumber() : 0)
302
+
303
+ const ch = (client.channelID !== undefined && client.channelID !== null) ? client.channelID : (client._channelID || 0)
304
+ const knxPacketRequest = KNXProtocol.newKNXTunnelingRequest(ch, seqNum, cemi)
305
+ const wantsAck = !(client._options && client._options.suppress_ack_ldatareq)
306
+ client.send(knxPacketRequest, wantsAck ? knxPacketRequest : undefined, false, seqNum)
307
+ return
308
+ }
309
+ }
310
+ }
311
+
312
+ // Legacy: re-create telegram from event+APDU (routing counter will be reset by the library).
155
313
  const ga = parsed.destination
156
314
  if (parsed.event === 'GroupValue_Write') {
157
315
  if (!parsed.apduData) return
@@ -177,6 +335,7 @@ module.exports = function (RED) {
177
335
  if (!node.tunnelServer) return false
178
336
  if (!parsed.cemiHex) return false
179
337
  if (node.dropIfSameGateway && parsed.originGatewayId && localGatewayIds().has(String(parsed.originGatewayId))) return false
338
+ if (node.respectRoutingCounter && parsed.hopCount === 0) return false
180
339
  return true
181
340
  }
182
341
 
@@ -184,7 +343,7 @@ module.exports = function (RED) {
184
343
  const clean = normalizeHex(parsed.cemiHex)
185
344
  if (!clean || clean.length % 2 !== 0) return
186
345
  let KNXTunnelingRequest
187
- try { ({ KNXTunnelingRequest } = require('knxultimate')) } catch (e) { throw new Error('knxultimate KNXTunnelingRequest not available') }
346
+ try { ({ KNXTunnelingRequest } = getKnxultimate()) } catch (e) { throw new Error('knxultimate KNXTunnelingRequest not available') }
188
347
  const cemi = KNXTunnelingRequest.parseCEMIMessage(Buffer.from(clean, 'hex'), 0)
189
348
  node.tunnelServer.injectCemi(cemi)
190
349
  }
@@ -194,9 +353,16 @@ module.exports = function (RED) {
194
353
  const addr = node.tunnelServer && typeof node.tunnelServer.address === 'object' ? node.tunnelServer.address : null
195
354
  const host = addr ? addr.host : (config.tunnelListenHost || '0.0.0.0')
196
355
  const port = addr ? addr.port : safeNumber(config.tunnelListenPort, 3671)
197
- const sessions = node.tunnelSessions ? node.tunnelSessions.size : 0
356
+ const sessionsFromServer = node.tunnelServer && node.tunnelServer.sessions && typeof node.tunnelServer.sessions.size === 'number'
357
+ ? node.tunnelServer.sessions.size
358
+ : null
359
+ const sessionsFromEvents = node.tunnelSessions ? node.tunnelSessions.size : 0
360
+ const sessions = sessionsFromServer !== null ? sessionsFromServer : sessionsFromEvents
361
+ const adv = (node.tunnelServer && node.tunnelServer.options && node.tunnelServer.options.advertiseHost) ? node.tunnelServer.options.advertiseHost : ''
198
362
  const tail = extraText ? ` ${extraText}` : ''
199
- updateStatus({ fill: 'green', shape: 'dot', text: `Tunnel ${host}:${port} sessions:${sessions}${tail}` })
363
+ const advText = adv ? ` adv:${adv}` : ''
364
+ const rxText = node.tunnelRxCount ? ` rx:${node.tunnelRxCount}` : ''
365
+ updateStatus({ fill: 'green', shape: 'dot', text: `Tunnel ${host}:${port}${advText} sessions:${sessions}${rxText}${tail}` })
200
366
  }
201
367
 
202
368
  const startTunnelServerIfNeeded = () => {
@@ -213,7 +379,8 @@ module.exports = function (RED) {
213
379
 
214
380
  const listenHost = (config.tunnelListenHost && String(config.tunnelListenHost).trim()) || '0.0.0.0'
215
381
  const listenPort = safeNumber(config.tunnelListenPort, 3671)
216
- const advertiseHost = (config.tunnelAdvertiseHost && String(config.tunnelAdvertiseHost).trim()) || undefined
382
+ const advertiseHostConfigured = (config.tunnelAdvertiseHost && String(config.tunnelAdvertiseHost).trim()) || ''
383
+ const advertiseHost = advertiseHostConfigured || guessAdvertiseHost(listenHost)
217
384
  const maxSessions = Math.max(1, safeNumber(config.tunnelMaxSessions, 1))
218
385
 
219
386
  const loglevel = (node.serverKNX && node.serverKNX.loglevel) ? node.serverKNX.loglevel : 'error'
@@ -227,6 +394,26 @@ module.exports = function (RED) {
227
394
  loglevel
228
395
  })
229
396
 
397
+ // If the OS network was not ready at boot, periodically refresh auto-advertise host.
398
+ // This affects only new client connections (CONNECT_RESPONSE payload).
399
+ if (!advertiseHostConfigured) {
400
+ const refreshAdvertiseHost = () => {
401
+ try {
402
+ if (!node.tunnelServer || !node.tunnelServer.options) return
403
+ const next = guessAdvertiseHost(listenHost)
404
+ const cur = node.tunnelServer.options.advertiseHost
405
+ if (next && cur !== next) {
406
+ node.tunnelServer.options.advertiseHost = next
407
+ updateTunnelStatus('(adv host updated)')
408
+ }
409
+ } catch (e) { /* ignore */ }
410
+ }
411
+ try {
412
+ node.tunnelAdvertiseHostTimer = setInterval(refreshAdvertiseHost, 10000)
413
+ if (node.tunnelAdvertiseHostTimer && typeof node.tunnelAdvertiseHostTimer.unref === 'function') node.tunnelAdvertiseHostTimer.unref()
414
+ } catch (e) { /* ignore */ }
415
+ }
416
+
230
417
  node.tunnelServer.on('error', (err) => {
231
418
  updateStatus({ fill: 'red', shape: 'dot', text: `Tunnel error: ${err.message}` })
232
419
  node.error(err)
@@ -248,6 +435,8 @@ module.exports = function (RED) {
248
435
 
249
436
  node.tunnelServer.on('rawTelegram', (knx, info) => {
250
437
  try {
438
+ node.tunnelRxCount = (node.tunnelRxCount || 0) + 1
439
+ node.tunnelLastRxAt = Date.now()
251
440
  const msg = {
252
441
  topic: node.outputtopic || knx.destination,
253
442
  payload: {
@@ -259,12 +448,20 @@ module.exports = function (RED) {
259
448
  }
260
449
  }
261
450
  }
262
- node.send(msg)
451
+ const processed = applyRoutingCounterOnOutboundMsg(msg)
452
+ if (!processed) return
453
+ node.send(processed)
263
454
  } catch (error) {
264
455
  node.sysLogger?.error(`knxUltimateMultiRouting: tunnel rawTelegram output error: ${error.message}`)
265
456
  }
266
457
  })
267
458
 
459
+ // Periodic refresh to surface session count even if Node-RED doesn't show transient updates.
460
+ try {
461
+ node.tunnelStatusRefreshTimer = setInterval(() => updateTunnelStatus(''), 5000)
462
+ if (node.tunnelStatusRefreshTimer && typeof node.tunnelStatusRefreshTimer.unref === 'function') node.tunnelStatusRefreshTimer.unref()
463
+ } catch (e) { /* ignore */ }
464
+
268
465
  updateStatus({ fill: 'grey', shape: 'dot', text: 'Starting KNX/IP Server...' })
269
466
  Promise.resolve()
270
467
  .then(() => node.tunnelServer.start())
@@ -278,6 +475,23 @@ module.exports = function (RED) {
278
475
  node.on('input', function (msg) {
279
476
  try {
280
477
  const parsed = parseIncoming(msg)
478
+ if (node.respectRoutingCounter && parsed && parsed.hopCount === 0) {
479
+ node.setNodeStatus({ fill: 'grey', shape: 'ring', text: 'Dropped (routing counter 0)', payload: parsed.event || '', GA: parsed.destination, dpt: '', devicename: parsed.source || '' })
480
+ return
481
+ }
482
+
483
+ if (node.decrementRoutingCounter && parsed && parsed.cemiHex && parsed.hopCount !== null && parsed.hopCount > 0) {
484
+ const dec = decrementHopCountInCemiHex(parsed.cemiHex)
485
+ if (dec) {
486
+ if (node.respectRoutingCounter && dec.newHopCount === 0) {
487
+ node.setNodeStatus({ fill: 'grey', shape: 'ring', text: 'Dropped (routing counter 0)', payload: parsed.event || '', GA: parsed.destination, dpt: '', devicename: parsed.source || '' })
488
+ return
489
+ }
490
+ parsed.cemiHex = dec.cemiHex
491
+ parsed.hopCount = dec.newHopCount
492
+ }
493
+ }
494
+
281
495
  if (node.mode === 'server') {
282
496
  if (!canForwardToTunnel(parsed)) return
283
497
  forwardToTunnel(parsed)
@@ -298,6 +512,14 @@ module.exports = function (RED) {
298
512
  if (node.serverKNX) {
299
513
  try { node.serverKNX.removeClient(node) } catch (e) { /* ignore */ }
300
514
  }
515
+ if (node.tunnelAdvertiseHostTimer) {
516
+ try { clearInterval(node.tunnelAdvertiseHostTimer) } catch (e) { /* ignore */ }
517
+ node.tunnelAdvertiseHostTimer = null
518
+ }
519
+ if (node.tunnelStatusRefreshTimer) {
520
+ try { clearInterval(node.tunnelStatusRefreshTimer) } catch (e) { /* ignore */ }
521
+ node.tunnelStatusRefreshTimer = null
522
+ }
301
523
  if (node.tunnelServer) {
302
524
  try { await node.tunnelServer.stop() } catch (e) { /* ignore */ }
303
525
  node.tunnelServer = null
@@ -131,4 +131,4 @@
131
131
  <label for="node-input-srcRewriteRules"><i class="fa fa-random"></i> <span data-i18n="knxUltimateRouterFilter.properties.srcRewriteRules"></span></label>
132
132
  <textarea style="width:100%; height:90px;" id="node-input-srcRewriteRules" data-i18n="[placeholder]knxUltimateRouterFilter.placeholder.srcRewriteRules"></textarea>
133
133
  </div>
134
- </script>
134
+ </script>