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 +14 -2
- package/examples/KNX Multi Routing - KNXIP Server.json +133 -0
- package/nodes/knxUltimateAI.js +131 -85
- package/nodes/knxUltimateMultiRouting.html +75 -3
- package/nodes/knxUltimateMultiRouting.js +390 -28
- 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 +15 -1
- package/nodes/locales/de/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/en/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/en/knxUltimateMultiRouting.json +15 -1
- package/nodes/locales/en/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/es/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/es/knxUltimateMultiRouting.json +15 -1
- package/nodes/locales/es/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/fr/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/fr/knxUltimateMultiRouting.json +15 -1
- package/nodes/locales/fr/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/it/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/it/knxUltimateMultiRouting.json +15 -1
- package/nodes/locales/it/knxUltimateRouterFilter.html +39 -1
- package/nodes/locales/zh-CN/knxUltimateMultiRouting.html +16 -0
- package/nodes/locales/zh-CN/knxUltimateMultiRouting.json +15 -1
- package/nodes/locales/zh-CN/knxUltimateRouterFilter.html +39 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
-
**Version 4.1.
|
|
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
|
|
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
|
+
]
|
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,21 @@
|
|
|
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
|
+
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"> <span data-i18n="knxUltimateMultiRouting.properties.dropIfSameGateway"></span></label>
|
|
54
86
|
</div>
|
|
55
|
-
|
|
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
|
+
|
|
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>
|