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 +4 -2
- package/examples/KNX Multi Routing - KNXIP Server.json +133 -0
- package/nodes/knxUltimateAI.js +131 -85
- package/nodes/knxUltimateMultiRouting.html +63 -3
- package/nodes/knxUltimateMultiRouting.js +167 -27
- package/nodes/locales/de/knxUltimateMultiRouting.json +14 -1
- package/nodes/locales/en/knxUltimateMultiRouting.html +4 -0
- package/nodes/locales/en/knxUltimateMultiRouting.json +14 -1
- package/nodes/locales/es/knxUltimateMultiRouting.json +14 -1
- package/nodes/locales/fr/knxUltimateMultiRouting.json +14 -1
- package/nodes/locales/it/knxUltimateMultiRouting.html +4 -0
- package/nodes/locales/it/knxUltimateMultiRouting.json +14 -1
- package/nodes/locales/zh-CN/knxUltimateMultiRouting.json +14 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
-
**Version 4.1.
|
|
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
|
|
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
|
+
]
|
package/nodes/knxUltimateAI.js
CHANGED
|
@@ -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
|
-
|
|
729
|
-
provider.applyStatusUpdate
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1146
|
+
if (cmd === 'summary' || cmd === 'stats' || cmd === 'top' || cmd === '') {
|
|
1147
|
+
emitSummary()
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
1136
1150
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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"> <span data-i18n="knxUltimateMultiRouting.properties.dropIfSameGateway"></span></label>
|
|
54
84
|
</div>
|
|
55
|
-
|
|
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.
|
|
25
|
-
|
|
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
|
-
|
|
56
|
-
provider.applyStatusUpdate
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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 &&
|
|
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 (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
166
|
-
node.serverKNX
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|