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 +10 -0
- package/nodes/knxUltimateMultiRouting.html +12 -0
- package/nodes/knxUltimateMultiRouting.js +229 -7
- package/nodes/knxUltimateRouterFilter.html +1 -1
- package/nodes/knxUltimateRouterFilter.js +215 -9
- package/nodes/locales/de/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/de/knxUltimateMultiRouting.json +3 -2
- package/nodes/locales/de/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/en/knxUltimateMultiRouting.html +12 -0
- package/nodes/locales/en/knxUltimateMultiRouting.json +3 -2
- package/nodes/locales/en/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/es/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/es/knxUltimateMultiRouting.json +3 -2
- package/nodes/locales/es/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/fr/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/fr/knxUltimateMultiRouting.json +3 -2
- package/nodes/locales/fr/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/it/knxUltimateMultiRouting.html +12 -0
- package/nodes/locales/it/knxUltimateMultiRouting.json +3 -2
- package/nodes/locales/it/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/zh-CN/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/zh-CN/knxUltimateMultiRouting.json +3 -2
- package/nodes/locales/zh-CN/knxUltimateRouterFilter.html +39 -1
- package/package.json +1 -1
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"> <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"> <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"> <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
|
-
|
|
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 } =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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>
|