iobroker.openknx 1.1.13 → 1.2.0
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/README.md +24 -22
- package/admin/index_m.html +80 -1
- package/io-package.json +29 -29
- package/lib/projectImport.js +0 -3
- package/main.js +177 -29
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|

|
|
21
21
|

|
|
22
22
|
|
|
23
|
+
## Sentry
|
|
24
|
+
**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.**
|
|
25
|
+
For more details and for information how to disable the error reporting see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)! Sentry reporting is used starting with js-controller 3.0.
|
|
26
|
+
|
|
27
|
+
## Info
|
|
23
28
|
ioBroker adapter for KNX IP communication, powered by [KNXUltimate](https://github.com/Supergiovane/knxultimate).
|
|
24
29
|
|
|
25
30
|
- Native .knxproj import (ETS4/5/6, password-protected projects supported) -- import dialog accepts both .knxproj and .xml files
|
|
@@ -47,20 +52,20 @@ ioBroker adapter for KNX IP communication, powered by [KNXUltimate](https://gith
|
|
|
47
52
|
|
|
48
53
|
## MDT IP Interface / Router (SCN-IP100, SCN-IP000)
|
|
49
54
|
|
|
50
|
-
MDT IP
|
|
55
|
+
MDT IP gateways acknowledge tunnel frames more slowly than most competitors (around 2 telegrams per second). At higher throughput the device reacts with ACK timeouts or drops the tunnel connection. The adapter detects MDT automatically (deviceName match) and warns in the log if the settings do not fit.
|
|
56
|
+
|
|
57
|
+
Recommended order (start with the cheapest step, only escalate if needed):
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
1. **In ETS** raise the keep-alive timeout — biggest effect at zero adapter cost:
|
|
60
|
+
- **SCN-IP100 (Router):** parameter "Tunneling: longer monitoring interval" → "Yes"
|
|
61
|
+
- **SCN-IP000 (Interface):** parameter "Support slow tunneling connections" → "Yes"
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
3. **"Max Direct Link send rate"** auf 2 setzen — Coalescing-Queue throttelt Bursts im laufenden Betrieb.
|
|
61
|
-
4. **"Wait for ACK"** aktivieren — ACK-basierte Flow Control auf knxultimate-Ebene; allein nicht ausreichend, in Kombination sinnvoll.
|
|
62
|
-
5. **"linkedStateDebounce"** für chronisch flappende Quellen setzen — der Adapter weist im Fehlerfall direkt auf die betroffene GA hin.
|
|
63
|
-
6. **Last Resort: Protokoll auf "Multicast (Routing)" umstellen.** Stateless, umgeht den kompletten Tunnel-Session-Stack — löst hartnäckige Disconnects definitiv. Voraussetzungen: Multicast-fähiges Netzwerk (IGMP-Snooping korrekt konfiguriert). Trade-off: keine Per-Frame-Bestätigung mehr — verlorene Telegramme werden nicht auf Transport-Ebene erkannt.
|
|
63
|
+
Both raise the keep-alive timeout from ~10 s to 60 s. Short network glitches then no longer trigger a DISCONNECT_REQUEST.
|
|
64
|
+
2. **Disable autoread** — eliminates the startup burst (most common cause of the first disconnect).
|
|
65
|
+
3. **"Max Direct Link send rate"** set to 2 — the coalescing queue throttles bursts during normal operation.
|
|
66
|
+
4. **"Wait for ACK"** enable — ACK-based flow control at the knxultimate layer; not sufficient alone, useful in combination.
|
|
67
|
+
5. **"linkedStateDebounce"** set on chronically flapping sources — the adapter points to the affected GA in the error message.
|
|
68
|
+
6. **Last resort: switch protocol to "Multicast (Routing)".** Stateless, bypasses the entire tunnel session stack — resolves stubborn disconnects definitively. Requirements: a multicast-capable network (IGMP snooping configured correctly). Trade-off: no per-frame acknowledgement — lost telegrams are no longer detected at the transport layer.
|
|
64
69
|
|
|
65
70
|
## Changelog
|
|
66
71
|
|
|
@@ -68,6 +73,13 @@ Empfohlene Reihenfolge (mit dem günstigsten Schritt anfangen, nur eskalieren we
|
|
|
68
73
|
Placeholder for the next version (at the beginning of the line):
|
|
69
74
|
### **WORK IN PROGRESS**
|
|
70
75
|
-->
|
|
76
|
+
### 1.2.0 (2026-06-25)
|
|
77
|
+
- (copilot) Adapter requires node.js >= 22 now
|
|
78
|
+
|
|
79
|
+
### 1.1.14 (2026-06-11)
|
|
80
|
+
- improve reconnect logic
|
|
81
|
+
- improve project import
|
|
82
|
+
|
|
71
83
|
### 1.1.13 (2026-06-01)
|
|
72
84
|
|
|
73
85
|
- feat: improve disconnect handling on high bus load for MDT Gateways
|
|
@@ -80,16 +92,6 @@ Empfohlene Reihenfolge (mit dem günstigsten Schritt anfangen, nur eskalieren we
|
|
|
80
92
|
### 1.1.11 (2026-05-08)
|
|
81
93
|
- fix for UDP interface connection
|
|
82
94
|
|
|
83
|
-
### 1.1.10 (2026-05-05)
|
|
84
|
-
- add new DPTs and units
|
|
85
|
-
|
|
86
|
-
### 1.1.9 (2026-05-04)
|
|
87
|
-
- fix handling of unkown DPT
|
|
88
|
-
|
|
89
|
-
### initial version
|
|
90
|
-
|
|
91
|
-
- initial version from https://www.npmjs.com/package/iobroker.knx/v/0.8.3
|
|
92
|
-
|
|
93
95
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
94
96
|
|
|
95
97
|
## License
|
package/admin/index_m.html
CHANGED
|
@@ -41,6 +41,73 @@
|
|
|
41
41
|
background-color: #000;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/* Dark mode: body.dark is set by the inline script (reads ?react=dark). */
|
|
45
|
+
body.dark input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]),
|
|
46
|
+
body.dark textarea,
|
|
47
|
+
body.dark select,
|
|
48
|
+
body.dark .select-wrapper input.select-dropdown {
|
|
49
|
+
background-color: #2a2a2a !important;
|
|
50
|
+
color: #e0e0e0 !important;
|
|
51
|
+
border-bottom-color: #555 !important;
|
|
52
|
+
}
|
|
53
|
+
body.dark .dropdown-content {
|
|
54
|
+
background-color: #2a2a2a !important;
|
|
55
|
+
}
|
|
56
|
+
body.dark .dropdown-content li > span,
|
|
57
|
+
body.dark .dropdown-content li > a {
|
|
58
|
+
color: #e0e0e0 !important;
|
|
59
|
+
}
|
|
60
|
+
body.dark .dropdown-content li:hover,
|
|
61
|
+
body.dark .dropdown-content li.selected {
|
|
62
|
+
background-color: #444 !important;
|
|
63
|
+
}
|
|
64
|
+
/* Materialize buttons render dark grey on dark background — make them readable. */
|
|
65
|
+
body.dark .btn,
|
|
66
|
+
body.dark .btn-flat,
|
|
67
|
+
body.dark .btn-large,
|
|
68
|
+
body.dark .btn-small {
|
|
69
|
+
background-color: #1976d2 !important;
|
|
70
|
+
color: #ffffff !important;
|
|
71
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5) !important;
|
|
72
|
+
}
|
|
73
|
+
body.dark .btn:hover,
|
|
74
|
+
body.dark .btn-flat:hover,
|
|
75
|
+
body.dark .btn-large:hover,
|
|
76
|
+
body.dark .btn-small:hover {
|
|
77
|
+
background-color: #1e88e5 !important;
|
|
78
|
+
}
|
|
79
|
+
body.dark .btn[disabled],
|
|
80
|
+
body.dark .btn-flat[disabled] {
|
|
81
|
+
background-color: #2a2a2a !important;
|
|
82
|
+
color: #777 !important;
|
|
83
|
+
box-shadow: none !important;
|
|
84
|
+
}
|
|
85
|
+
/* Hint boxes (e.g. MDT recommended steps) */
|
|
86
|
+
.hint-box {
|
|
87
|
+
background: #f5f5f5;
|
|
88
|
+
color: #555;
|
|
89
|
+
}
|
|
90
|
+
body.dark .hint-box {
|
|
91
|
+
background: #2a2a2a !important;
|
|
92
|
+
color: #c0c0c0 !important;
|
|
93
|
+
}
|
|
94
|
+
/* GA-tree hover/selected on dark background */
|
|
95
|
+
body.dark .ga-tree .ga-leaf {
|
|
96
|
+
border-bottom-color: #333;
|
|
97
|
+
}
|
|
98
|
+
body.dark .ga-tree .ga-leaf:hover {
|
|
99
|
+
background-color: #2a3a4a;
|
|
100
|
+
}
|
|
101
|
+
body.dark .ga-tree .ga-leaf.selected {
|
|
102
|
+
background-color: #1e3a5f;
|
|
103
|
+
}
|
|
104
|
+
body.dark .ga-tree .ga-folder > span:hover {
|
|
105
|
+
background-color: #2a2a2a;
|
|
106
|
+
}
|
|
107
|
+
body.dark .ga-tree .ga-val {
|
|
108
|
+
color: #64b5f6;
|
|
109
|
+
}
|
|
110
|
+
|
|
44
111
|
/* GA-Tools tree styles */
|
|
45
112
|
.ga-tree { list-style: none; padding: 0 !important; margin: 0; }
|
|
46
113
|
.ga-tree ul { list-style: none; padding-left: 16px; margin: 0; }
|
|
@@ -1126,6 +1193,18 @@
|
|
|
1126
1193
|
</head>
|
|
1127
1194
|
|
|
1128
1195
|
<body>
|
|
1196
|
+
<script>
|
|
1197
|
+
// ioBroker.admin signals theme via the iframe URL (?react=dark|light). Mirror it
|
|
1198
|
+
// onto <body> so our `body.dark` CSS rules match.
|
|
1199
|
+
(function () {
|
|
1200
|
+
try {
|
|
1201
|
+
var m = (location.search || "").match(/[?&]react=([^&]+)/);
|
|
1202
|
+
if (m && decodeURIComponent(m[1]) === "dark") {
|
|
1203
|
+
document.body.classList.add("dark");
|
|
1204
|
+
}
|
|
1205
|
+
} catch (e) {}
|
|
1206
|
+
})();
|
|
1207
|
+
</script>
|
|
1129
1208
|
<!-- you have to put your config page in a div with id adapter-container -->
|
|
1130
1209
|
<div class="m adapter-container">
|
|
1131
1210
|
<nav>
|
|
@@ -1214,7 +1293,7 @@
|
|
|
1214
1293
|
>Max Direct Link send rate [tel/s] 0=Unlimited, MDT: 2</label>
|
|
1215
1294
|
</div>
|
|
1216
1295
|
<div class="col s12" style="margin-top:12px;">
|
|
1217
|
-
<div style="border-left:3px solid #888; padding:8px 12px;
|
|
1296
|
+
<div class="hint-box" style="border-left:3px solid #888; padding:8px 12px; font-size:0.9em;">
|
|
1218
1297
|
<div class="translate" style="font-weight:500; margin-bottom:6px;">MDT IP router — recommended steps:</div>
|
|
1219
1298
|
<ol style="margin:0; padding-left:20px;">
|
|
1220
1299
|
<li class="translate">ETS: set 60 s keep-alive (SCN-IP100: "Tunneling: longer monitoring interval", SCN-IP000: "Support slow tunneling connections")</li>
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "openknx",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.2.0": {
|
|
7
|
+
"en": "Adapter requires node.js >= 22 now",
|
|
8
|
+
"de": "Der Adapter benötigt jetzt Node.js >= 22.",
|
|
9
|
+
"ru": "Для работы адаптера требуется Node.js версии >= 22.",
|
|
10
|
+
"pt": "O adaptador agora requer Node.js >= 22.",
|
|
11
|
+
"nl": "De adapter vereist nu Node.js versie 22 of hoger.",
|
|
12
|
+
"fr": "L'adaptateur nécessite désormais Node.js >= 22.",
|
|
13
|
+
"it": "L'adattatore ora richiede Node.js >= 22",
|
|
14
|
+
"es": "El adaptador ahora requiere node.js >= 22.",
|
|
15
|
+
"pl": "Adapter wymaga teraz node.js >= 22",
|
|
16
|
+
"uk": "Адаптер тепер вимагає node.js >= 22",
|
|
17
|
+
"zh-cn": "适配器现在需要 node.js 版本 >= 22。"
|
|
18
|
+
},
|
|
19
|
+
"1.1.14": {
|
|
20
|
+
"en": "improve reconnect logic\nimprove project import",
|
|
21
|
+
"de": "reconnect logik verbessert\nprojektimport verbessert",
|
|
22
|
+
"ru": "улучшить логику подключения\nулучшение импорта проектов",
|
|
23
|
+
"pt": "melhorar a lógica de reconexão\nmelhorar a importação do projecto",
|
|
24
|
+
"nl": "de herverbindingslogica verbeteren\nprojectimport verbeteren",
|
|
25
|
+
"fr": "améliorer la logique de reconnect\naméliorer l'importation des projets",
|
|
26
|
+
"it": "migliorare la logica di riconnettere\nmigliorare l'importazione di progetti",
|
|
27
|
+
"es": "mejorar la lógica de reconexión\nmejoramiento de la importación de proyectos",
|
|
28
|
+
"pl": "poprawić logikę ponownego połączenia\npoprawa importu projektów",
|
|
29
|
+
"uk": "поліпшення логіки відключення\nвдосконалення імпорту проекту",
|
|
30
|
+
"zh-cn": "改进重联逻辑\n改进项目导入"
|
|
31
|
+
},
|
|
6
32
|
"1.1.13": {
|
|
7
33
|
"en": "feat: improve disconnect handling on high bus load for MDT Gateways\nnew options \"Wait for ACK\", \"Max Direct Link send rate\" (coalescing queue) and per-GA \"linkedStateDebounce\"; burst log with, hint at linkedStateDebounce GAs",
|
|
8
34
|
"de": "feat: Bessere Behandlung von Verbindungsabbrüchen bei hoher Buslast für MDT-Gateways\nNeue Optionen \"Wait for ACK\", \"Max Direct Link send rate\" (Coalescing-Queue) und pro GA \"linkedStateDebounce\"; Burst-Log bei Disconnect mit Hinweis auf störende GAs",
|
|
@@ -67,32 +93,6 @@
|
|
|
67
93
|
"pl": "fix handling of unkown DPT",
|
|
68
94
|
"uk": "фіксувати обробку unkown DPT",
|
|
69
95
|
"zh-cn": "固定处理未隐藏的 DPT"
|
|
70
|
-
},
|
|
71
|
-
"1.1.8": {
|
|
72
|
-
"en": "add compatiblity mode für raw DPT5.*\nreduce default delay to 25ms",
|
|
73
|
-
"de": "Kombatiblitäts-Modus für raw DPT5.*\nDefault-Delay auf 25m reduziert",
|
|
74
|
-
"ru": "добавить режим совместимости с сырым DPT5.*\nуменьшить задержку до 25 мс",
|
|
75
|
-
"pt": "adicionar o modo de compatibilidade für cru DPT5.*\nreduzir o atraso padrão para 25ms",
|
|
76
|
-
"nl": "compatiblity mode für raw DPT5.*\nde standaardvertraging verlagen naar 25ms",
|
|
77
|
-
"fr": "ajouter le mode de compatibilité für brut DPT5.*\nréduire le délai par défaut à 25ms",
|
|
78
|
-
"it": "aggiungere la modalità compatiblity für raw DPT5.*\nridurre il ritardo predefinito a 25ms",
|
|
79
|
-
"es": "añadir compatiblity mode für raw DPT5.*\nreducir el retraso predeterminado a 25 ms",
|
|
80
|
-
"pl": "dodać tryb kompatybilności für raw DPT5. *\nzmniejszyć domyślne opóźnienie do 25ms",
|
|
81
|
-
"uk": "додати режим сумісності für raw DPT5.*\nзменшити затримку за замовчуванням до 25ms",
|
|
82
|
-
"zh-cn": "添加相容性模式 für 原始 DPT5. *\n将默认延迟减少至 25 ms"
|
|
83
|
-
},
|
|
84
|
-
"1.1.7": {
|
|
85
|
-
"en": "improve Umlaute handling for DPT-16",
|
|
86
|
-
"de": "Verbesserung der Umlaute Handhabung für DPT-16",
|
|
87
|
-
"ru": "улучшение управляемости Umlaute для DPT-16",
|
|
88
|
-
"pt": "melhorar o manuseio de Umlaute para o DPT-16",
|
|
89
|
-
"nl": "umlautebehandeling verbeteren voor DPT-16",
|
|
90
|
-
"fr": "améliorer la manipulation Umlaute pour DPT-16",
|
|
91
|
-
"it": "migliorare la gestione Umlaute per DPT-16",
|
|
92
|
-
"es": "mejorar el manejo de Umlaute para DPT-16",
|
|
93
|
-
"pl": "poprawa obsługi Umlaute dla DPT- 16",
|
|
94
|
-
"uk": "поліпшити роботу Umlaute для DPT-16",
|
|
95
|
-
"zh-cn": "改进 DPT-16 的 Umlaute 处理"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"_id": "info.busload",
|
|
261
261
|
"type": "state",
|
|
262
262
|
"common": {
|
|
263
|
-
"role": "
|
|
263
|
+
"role": "value",
|
|
264
264
|
"name": {
|
|
265
265
|
"en": "Busload in the last 5 seconds",
|
|
266
266
|
"de": "Buslast in den letzten 5 Sekunden",
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"_id": "info.messagecount",
|
|
287
287
|
"type": "state",
|
|
288
288
|
"common": {
|
|
289
|
-
"role": "
|
|
289
|
+
"role": "value",
|
|
290
290
|
"name": {
|
|
291
291
|
"en": "Message count",
|
|
292
292
|
"de": "Meldungszahl",
|
package/lib/projectImport.js
CHANGED
|
@@ -300,14 +300,12 @@ module.exports = {
|
|
|
300
300
|
let commonWrite = true;
|
|
301
301
|
let autoread = !tools.isTriggerDPT(dpt);
|
|
302
302
|
|
|
303
|
-
let transmitFlag = false;
|
|
304
303
|
let updateFlag = false;
|
|
305
304
|
|
|
306
305
|
if (linkedCOs.length > 0) {
|
|
307
306
|
// Use CO flags: any CO with read flag -> common.read; any CO with write flag -> common.write
|
|
308
307
|
commonRead = linkedCOs.some(co => co.flags.read);
|
|
309
308
|
commonWrite = linkedCOs.some(co => co.flags.write);
|
|
310
|
-
transmitFlag = linkedCOs.some(co => co.flags.transmit);
|
|
311
309
|
updateFlag = linkedCOs.some(co => co.flags.update);
|
|
312
310
|
// Use read_on_init from COs if available, fallback to DPT-based heuristic
|
|
313
311
|
const hasReadOnInit = linkedCOs.some(co => co.flags.read_on_init);
|
|
@@ -339,7 +337,6 @@ module.exports = {
|
|
|
339
337
|
type: type,
|
|
340
338
|
unit: dptObj?.subtype?.unit || "",
|
|
341
339
|
write: commonWrite,
|
|
342
|
-
update: transmitFlag,
|
|
343
340
|
states: dptObj?.subtype?.enc,
|
|
344
341
|
...(range[0] != null && typeof range[0] == "number" && type == "number" && { min: range[0] }),
|
|
345
342
|
...(range[1] != null && typeof range[0] == "number" && type == "number" && { max: range[1] }),
|
package/main.js
CHANGED
|
@@ -376,6 +376,7 @@ class openknx extends utils.Adapter {
|
|
|
376
376
|
KNXClient.discoverInterfaces()
|
|
377
377
|
.then(interfaces => {
|
|
378
378
|
this.log.info(`Discovery found ${interfaces.length} interface(s)`);
|
|
379
|
+
this.log.info(`Discovery raw result: ${JSON.stringify(interfaces)}`);
|
|
379
380
|
if (obj.callback) {
|
|
380
381
|
if (interfaces.length === 0) {
|
|
381
382
|
this.sendTo(
|
|
@@ -504,7 +505,29 @@ class openknx extends utils.Adapter {
|
|
|
504
505
|
* remove knx elements that are not found in the current import file
|
|
505
506
|
*/
|
|
506
507
|
async removeUnusedObjects(importObjects, removeUnusedObjects) {
|
|
507
|
-
|
|
508
|
+
// In foreign-namespace mode (targetNamespace set), the adapter's own
|
|
509
|
+
// getAdapterObjectsAsync would return openknx.0.* — empty for our purposes.
|
|
510
|
+
// List from the configured target namespace instead and use the foreign API
|
|
511
|
+
// for deletion accordingly.
|
|
512
|
+
let objects;
|
|
513
|
+
if (this.isForeign) {
|
|
514
|
+
const list = await new Promise(resolve => {
|
|
515
|
+
this.getObjectList(
|
|
516
|
+
{ startkey: `${this.mynamespace}.`, endkey: `${this.mynamespace}.香` },
|
|
517
|
+
(err, result) => resolve(err ? null : result),
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
objects = {};
|
|
521
|
+
if (list && Array.isArray(list.rows)) {
|
|
522
|
+
for (const row of list.rows) {
|
|
523
|
+
objects[row.id] = row.value;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
objects = await this.getAdapterObjectsAsync();
|
|
528
|
+
}
|
|
529
|
+
const delFn = this.isForeign ? this.delForeignObject.bind(this) : this.delObject.bind(this);
|
|
530
|
+
let staleCount = 0;
|
|
508
531
|
|
|
509
532
|
Object.entries(objects).forEach(object => {
|
|
510
533
|
if (
|
|
@@ -517,33 +540,61 @@ class openknx extends utils.Adapter {
|
|
|
517
540
|
const found = importObjects.find(element => `${this.mynamespace}.${element._id}` === object[0]);
|
|
518
541
|
if (!found) {
|
|
519
542
|
// knx element in object tree not found in import file
|
|
520
|
-
|
|
521
|
-
`${removeUnusedObjects ? "deleting" : ""}
|
|
522
|
-
existing element in object tree not found in import file: ${object[0]}`,
|
|
523
|
-
);
|
|
543
|
+
staleCount++;
|
|
524
544
|
if (removeUnusedObjects) {
|
|
525
|
-
this.
|
|
545
|
+
this.log.info(`Deleting stale KNX object: ${object[0]}`);
|
|
546
|
+
delFn(object[0], err => {
|
|
526
547
|
if (err) {
|
|
527
548
|
this.log.warn(`could not delete object ${object[0]}`);
|
|
528
549
|
}
|
|
529
550
|
});
|
|
551
|
+
} else {
|
|
552
|
+
this.log.info(
|
|
553
|
+
`Stale KNX object kept: ${object[0]} (not in current import file). Enable "remove unused objects" in adapter settings to delete it automatically.`,
|
|
554
|
+
);
|
|
530
555
|
}
|
|
531
556
|
}
|
|
532
557
|
}
|
|
533
558
|
});
|
|
559
|
+
|
|
560
|
+
if (staleCount > 0 && !removeUnusedObjects) {
|
|
561
|
+
this.log.info(
|
|
562
|
+
`${staleCount} stale KNX object(s) kept (not in current import file). Enable "remove unused objects" in adapter settings if you want them deleted on import.`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
534
565
|
}
|
|
535
566
|
|
|
536
567
|
/*
|
|
537
568
|
* delete all existing KNX objects before a clean re-import
|
|
538
569
|
*/
|
|
539
570
|
async cleanImport() {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
571
|
+
if (this.isForeign) {
|
|
572
|
+
// foreign-namespace mode: KNX objects live under mynamespace, not the
|
|
573
|
+
// adapter's own namespace. delObjectAsync('') would only delete openknx.0.*
|
|
574
|
+
// (info.connection etc.) and miss the actual KNX states.
|
|
575
|
+
const list = await new Promise(resolve => {
|
|
576
|
+
this.getObjectList(
|
|
577
|
+
{ startkey: `${this.mynamespace}.`, endkey: `${this.mynamespace}.香` },
|
|
578
|
+
(err, result) => resolve(err ? null : result),
|
|
579
|
+
);
|
|
580
|
+
});
|
|
581
|
+
if (list && Array.isArray(list.rows)) {
|
|
582
|
+
for (const row of list.rows) {
|
|
583
|
+
if (row.id.indexOf(".info.") !== -1) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
await this.delForeignObjectAsync(row.id).catch(() => {});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
try {
|
|
591
|
+
await this.delObjectAsync("", { recursive: true });
|
|
592
|
+
} catch (e) {
|
|
593
|
+
this.log.warn(`delObjectAsync("") failed (${e.message}), falling back to manual deletion`);
|
|
594
|
+
const objects = await this.getAdapterObjectsAsync();
|
|
595
|
+
for (const id of Object.keys(objects)) {
|
|
596
|
+
await this.delObjectAsync(id).catch(() => {});
|
|
597
|
+
}
|
|
547
598
|
}
|
|
548
599
|
}
|
|
549
600
|
this.log.info("cleanImport: deleted all existing KNX objects");
|
|
@@ -563,8 +614,8 @@ class openknx extends utils.Adapter {
|
|
|
563
614
|
|
|
564
615
|
this.getObjectList(
|
|
565
616
|
{
|
|
566
|
-
startkey: this.
|
|
567
|
-
endkey: `${this.
|
|
617
|
+
startkey: this.mynamespace,
|
|
618
|
+
endkey: `${this.mynamespace}\u9999`,
|
|
568
619
|
},
|
|
569
620
|
(e, result) => {
|
|
570
621
|
const gas = [];
|
|
@@ -598,7 +649,11 @@ class openknx extends utils.Adapter {
|
|
|
598
649
|
if (onlyAddNewObjects) {
|
|
599
650
|
// if user setting Add only new Objects write only new objects
|
|
600
651
|
// extend object would overwrite user made element changes if known in the import, not intended
|
|
601
|
-
|
|
652
|
+
const id = `${this.mynamespace}.${objects[i]._id}`;
|
|
653
|
+
const setNotExists = this.isForeign
|
|
654
|
+
? this.setForeignObjectNotExists.bind(this)
|
|
655
|
+
: this.setObjectNotExists.bind(this);
|
|
656
|
+
setNotExists(id, objects[i], err => {
|
|
602
657
|
if (err) {
|
|
603
658
|
this.log.warn(`error store Object ${objects[i]._id} ${err ? ` ${err}` : ""}`);
|
|
604
659
|
}
|
|
@@ -613,7 +668,9 @@ class openknx extends utils.Adapter {
|
|
|
613
668
|
});
|
|
614
669
|
} else {
|
|
615
670
|
// setObjet to overwrite all existing settings, default
|
|
616
|
-
|
|
671
|
+
const id = `${this.mynamespace}.${objects[i]._id}`;
|
|
672
|
+
const setObj = this.isForeign ? this.setForeignObject.bind(this) : this.setObject.bind(this);
|
|
673
|
+
setObj(id, objects[i], err => {
|
|
617
674
|
if (err) {
|
|
618
675
|
this.log.warn(`error store Object ${objects[i]._id}${err ? ` ${err}` : ""}`);
|
|
619
676
|
}
|
|
@@ -1401,20 +1458,34 @@ class openknx extends utils.Adapter {
|
|
|
1401
1458
|
}
|
|
1402
1459
|
}
|
|
1403
1460
|
|
|
1404
|
-
// Reconnect
|
|
1405
|
-
|
|
1461
|
+
// Reconnect schedule: first 3 attempts after 10 s each, then unbounded retry
|
|
1462
|
+
// every 30 s. The adapter never gives up on its own — long outages (LTE down,
|
|
1463
|
+
// WireGuard down, gateway power-cycle) recover automatically once the path is
|
|
1464
|
+
// back, no external watchdog script needed.
|
|
1465
|
+
static reconnectInitialDelays = [10, 10, 10];
|
|
1466
|
+
static reconnectFollowUpDelay = 30;
|
|
1406
1467
|
|
|
1407
1468
|
scheduleReconnect() {
|
|
1408
|
-
if (this.stopping
|
|
1409
|
-
if (!this.stopping) {
|
|
1410
|
-
this.log.error(`Giving up after ${openknx.reconnectDelays.length} reconnect attempts`);
|
|
1411
|
-
}
|
|
1469
|
+
if (this.stopping) {
|
|
1412
1470
|
return;
|
|
1413
1471
|
}
|
|
1414
|
-
|
|
1472
|
+
// Drop any stale handle so concurrent paths (error event + disconnected event)
|
|
1473
|
+
// see a consistent "no pending timer" state. The timer below replaces it.
|
|
1474
|
+
if (this.reconnectTimer) {
|
|
1475
|
+
clearTimeout(this.reconnectTimer);
|
|
1476
|
+
this.reconnectTimer = undefined;
|
|
1477
|
+
}
|
|
1478
|
+
const initial = openknx.reconnectInitialDelays;
|
|
1479
|
+
const delay =
|
|
1480
|
+
this.reconnectCount < initial.length ? initial[this.reconnectCount] : openknx.reconnectFollowUpDelay;
|
|
1415
1481
|
this.reconnectCount++;
|
|
1416
|
-
|
|
1482
|
+
const label =
|
|
1483
|
+
this.reconnectCount <= initial.length
|
|
1484
|
+
? `${this.reconnectCount}/${initial.length}`
|
|
1485
|
+
: `${this.reconnectCount} (follow-up)`;
|
|
1486
|
+
this.log.info(`Reconnect attempt ${label} in ${delay}s...`);
|
|
1417
1487
|
this.reconnectTimer = setTimeout(() => {
|
|
1488
|
+
this.reconnectTimer = undefined;
|
|
1418
1489
|
try {
|
|
1419
1490
|
this.startKnxStack();
|
|
1420
1491
|
} catch (e) {
|
|
@@ -1456,7 +1527,7 @@ class openknx extends utils.Adapter {
|
|
|
1456
1527
|
let ifaceName = "";
|
|
1457
1528
|
let ifaceMatchedIp = "";
|
|
1458
1529
|
if (this.config.localInterface) {
|
|
1459
|
-
const interfaces = require("os").networkInterfaces();
|
|
1530
|
+
const interfaces = require("node:os").networkInterfaces();
|
|
1460
1531
|
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
1461
1532
|
if (addrs && addrs.some(a => a.address === this.config.localInterface)) {
|
|
1462
1533
|
ifaceName = name;
|
|
@@ -1476,7 +1547,7 @@ class openknx extends utils.Adapter {
|
|
|
1476
1547
|
// to localIPAddress, derived from the chosen network interface — wrong selection
|
|
1477
1548
|
// on multi-homed hosts (Docker, VPN, multiple NICs) is the most common cause of
|
|
1478
1549
|
// "v0.7 worked, v1.x doesn't" reports.
|
|
1479
|
-
const allIfs = require("os").networkInterfaces();
|
|
1550
|
+
const allIfs = require("node:os").networkInterfaces();
|
|
1480
1551
|
const ipv4Candidates = [];
|
|
1481
1552
|
for (const [name, addrs] of Object.entries(allIfs)) {
|
|
1482
1553
|
if (!addrs) {
|
|
@@ -1514,6 +1585,25 @@ class openknx extends utils.Adapter {
|
|
|
1514
1585
|
loglevel: this._knxultimateLogLevel(),
|
|
1515
1586
|
};
|
|
1516
1587
|
|
|
1588
|
+
// TunnelTCP precheck: knxultimate's TCP path always runs the KNX Secure
|
|
1589
|
+
// session handshake on connect — there is no plain (non-secure) TCP tunnel
|
|
1590
|
+
// implementation. Without credentials the connect either hangs or throws
|
|
1591
|
+
// an opaque error deep inside secureBuildSessionAuthenticate. Fail fast.
|
|
1592
|
+
if (hostProtocol === "TunnelTCP") {
|
|
1593
|
+
const hasKeyring = !!(this.config.knxKeysContent && this.config.knxKeysPassword);
|
|
1594
|
+
const hasManualPassword = !!this.config.tunnelUserPassword;
|
|
1595
|
+
if (!hasKeyring && !hasManualPassword) {
|
|
1596
|
+
this.log.error(
|
|
1597
|
+
"TunnelTCP requires KNX Secure credentials (keyring or tunnel password). " +
|
|
1598
|
+
"Plain TCP tunnel is not supported by knxultimate. " +
|
|
1599
|
+
"Either: (a) enable KNX Secure and provide a .knxkeys file + password from ETS, " +
|
|
1600
|
+
"(b) enable KNX Secure and provide Tunnel User Password + ID, " +
|
|
1601
|
+
"or (c) switch the protocol to TunnelUDP.",
|
|
1602
|
+
);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1517
1607
|
// KNX Secure options
|
|
1518
1608
|
if (this.config.isSecureKNXEnabled) {
|
|
1519
1609
|
// Validate that at least one complete authentication source is configured.
|
|
@@ -1717,12 +1807,70 @@ class openknx extends utils.Adapter {
|
|
|
1717
1807
|
this.stopQueueHealthMonitor();
|
|
1718
1808
|
this.stopLinkedWriteDrain();
|
|
1719
1809
|
this.linkedWriteQueue.clear();
|
|
1720
|
-
|
|
1810
|
+
// Idempotent reschedule: if the error event already queued a reconnect
|
|
1811
|
+
// for the same incident, skip — otherwise reconnectCount would double-
|
|
1812
|
+
// increment and the user-facing N/3 numbering loses meaning.
|
|
1813
|
+
if (!this.reconnectTimer) {
|
|
1814
|
+
this.scheduleReconnect();
|
|
1815
|
+
}
|
|
1721
1816
|
});
|
|
1722
1817
|
|
|
1723
1818
|
// Event: error
|
|
1819
|
+
// Event: error
|
|
1820
|
+
// KNXnet/IP CONNECT_RESPONSE status codes (per KNXnet/IP spec, values mirrored from
|
|
1821
|
+
// xknx/xknx/knxip/error_code.py). knxultimate surfaces them as "Connect response error N"
|
|
1822
|
+
// with N in decimal. Map known codes to their official name + standard meaning so users
|
|
1823
|
+
// get the documented error instead of a stack trace.
|
|
1824
|
+
const knxErrorCodes = {
|
|
1825
|
+
0x00: ["E_NO_ERROR", "Connection accepted (no error)."],
|
|
1826
|
+
0x01: ["E_HOST_PROTOCOL_TYPE", "The requested host protocol type is not supported."],
|
|
1827
|
+
0x02: ["E_VERSION_NOT_SUPPORTED", "The requested protocol version is not supported."],
|
|
1828
|
+
0x04: ["E_SEQUENCE_NUMBER", "The received sequence number is out of order."],
|
|
1829
|
+
0x0f: ["E_ERROR", "Generic error."],
|
|
1830
|
+
0x21: ["E_CONNECTION_ID", "No active data connection with the specified ID found."],
|
|
1831
|
+
0x22: ["E_CONNECTION_TYPE", "The requested connection type is not supported."],
|
|
1832
|
+
0x23: ["E_CONNECTION_OPTION", "One or more requested connection options are not supported."],
|
|
1833
|
+
0x24: ["E_NO_MORE_CONNECTIONS", "Server has reached its maximum number of concurrent data connections."],
|
|
1834
|
+
0x25: ["E_NO_MORE_UNIQUE_CONNECTIONS", "Requested individual address is already in use multiple times."],
|
|
1835
|
+
0x26: ["E_DATA_CONNECTION", "Server detected an error in the data connection with the specified ID."],
|
|
1836
|
+
0x27: ["E_KNX_CONNECTION", "Server detected an error in the KNX subnetwork connection with the specified ID."],
|
|
1837
|
+
0x28: ["E_AUTHORISATION_ERROR", "Authorisation error."],
|
|
1838
|
+
0x29: ["E_TUNNELLING_LAYER", "The requested tunnelling layer is not supported."],
|
|
1839
|
+
0x2d: ["E_NO_TUNNELLING_ADDRESS", "No tunnelling address available."],
|
|
1840
|
+
0x2e: ["E_CONNECTION_IN_USE", "Connection already in use."],
|
|
1841
|
+
};
|
|
1724
1842
|
this.knxConnection.on(KNXClientEvents.error, err => {
|
|
1725
|
-
|
|
1843
|
+
const msg = err?.message || String(err);
|
|
1844
|
+
const m = msg.match(/Connect response error (\d+)/i);
|
|
1845
|
+
if (m) {
|
|
1846
|
+
const code = parseInt(m[1], 10);
|
|
1847
|
+
const entry = knxErrorCodes[code];
|
|
1848
|
+
if (entry) {
|
|
1849
|
+
this.log.error(
|
|
1850
|
+
`KNX gateway rejected CONNECT_REQUEST: ${entry[0]} (status 0x${code.toString(16).padStart(2, "0")}). ${entry[1]}`,
|
|
1851
|
+
);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
this.log.warn(msg);
|
|
1856
|
+
// Any error that left knxultimate in a non-CONNECTED internal state means
|
|
1857
|
+
// we'll never see traffic again on this socket. The `disconnected` event is
|
|
1858
|
+
// not always emitted (Connect timeout, dead heartbeat on TCP — see knxultimate
|
|
1859
|
+
// closeSocket() bug). Single source of truth: knxConnection.isConnected().
|
|
1860
|
+
// If the lib says it's not connected and we have no reconnect pending, we
|
|
1861
|
+
// start one ourselves and reconcile our adapter-side state.
|
|
1862
|
+
if (!this.knxConnection?.isConnected?.() && !this.reconnectTimer && !this.stopping) {
|
|
1863
|
+
if (this.connected) {
|
|
1864
|
+
this.connected = false;
|
|
1865
|
+
this.setState("info.connection", false, true);
|
|
1866
|
+
this.setState("info.busload", 0, true);
|
|
1867
|
+
this.stopCyclicSending();
|
|
1868
|
+
this.stopQueueHealthMonitor();
|
|
1869
|
+
this.stopLinkedWriteDrain();
|
|
1870
|
+
this.linkedWriteQueue.clear();
|
|
1871
|
+
}
|
|
1872
|
+
this.scheduleReconnect();
|
|
1873
|
+
}
|
|
1726
1874
|
});
|
|
1727
1875
|
|
|
1728
1876
|
// Event: ackReceived - fires for both transport-level success (ack=true) and timeout (ack=false)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.openknx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "ioBroker knx Adapter",
|
|
5
5
|
"author": "boellner",
|
|
6
6
|
"contributors": [
|
|
@@ -31,27 +31,27 @@
|
|
|
31
31
|
"url": "https://github.com/iobroker-community-adapters/ioBroker.openknx"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=22"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@iobroker/adapter-core": "^3.
|
|
37
|
+
"@iobroker/adapter-core": "^3.4.1",
|
|
38
38
|
"@xmldom/xmldom": "^0.9.10",
|
|
39
39
|
"@zip.js/zip.js": "^2.8.26",
|
|
40
40
|
"ipaddr.js": "^2.4.0",
|
|
41
|
-
"knxultimate": "^5.5.
|
|
41
|
+
"knxultimate": "^5.5.9",
|
|
42
42
|
"sax": "^1.6.0",
|
|
43
43
|
"xpath": "^0.0.34"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@alcalzone/release-script": "^5.2.
|
|
46
|
+
"@alcalzone/release-script": "^5.2.1",
|
|
47
47
|
"@alcalzone/release-script-plugin-iobroker": "^5.2.0",
|
|
48
48
|
"@alcalzone/release-script-plugin-license": "^5.2.0",
|
|
49
49
|
"@alcalzone/release-script-plugin-manual-review": "^5.2.0",
|
|
50
50
|
"@iobroker/adapter-dev": "^1.5.0",
|
|
51
51
|
"@iobroker/eslint-config": "^2.3.4",
|
|
52
52
|
"@iobroker/testing": "^5.2.2",
|
|
53
|
-
"@tsconfig/
|
|
54
|
-
"@types/node": "^20.
|
|
53
|
+
"@tsconfig/node22": "^22.0.5",
|
|
54
|
+
"@types/node": "^22.20.0",
|
|
55
55
|
"typescript": "~5.9.3"
|
|
56
56
|
},
|
|
57
57
|
"main": "main.js",
|