iobroker.openknx 1.1.13 → 1.1.14
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 +16 -15
- package/admin/index_m.html +80 -1
- package/io-package.json +14 -14
- package/main.js +175 -27
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -47,20 +47,20 @@ ioBroker adapter for KNX IP communication, powered by [KNXUltimate](https://gith
|
|
|
47
47
|
|
|
48
48
|
## MDT IP Interface / Router (SCN-IP100, SCN-IP000)
|
|
49
49
|
|
|
50
|
-
MDT IP
|
|
50
|
+
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.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Recommended order (start with the cheapest step, only escalate if needed):
|
|
53
53
|
|
|
54
|
-
1. **In
|
|
55
|
-
- **SCN-IP100 (Router):**
|
|
56
|
-
- **SCN-IP000 (Interface):**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
2. **
|
|
60
|
-
3. **"Max Direct Link send rate"**
|
|
61
|
-
4. **"Wait for ACK"**
|
|
62
|
-
5. **"linkedStateDebounce"**
|
|
63
|
-
6. **Last
|
|
54
|
+
1. **In ETS** raise the keep-alive timeout — biggest effect at zero adapter cost:
|
|
55
|
+
- **SCN-IP100 (Router):** parameter "Tunneling: longer monitoring interval" → "Yes"
|
|
56
|
+
- **SCN-IP000 (Interface):** parameter "Support slow tunneling connections" → "Yes"
|
|
57
|
+
|
|
58
|
+
Both raise the keep-alive timeout from ~10 s to 60 s. Short network glitches then no longer trigger a DISCONNECT_REQUEST.
|
|
59
|
+
2. **Disable autoread** — eliminates the startup burst (most common cause of the first disconnect).
|
|
60
|
+
3. **"Max Direct Link send rate"** set to 2 — the coalescing queue throttles bursts during normal operation.
|
|
61
|
+
4. **"Wait for ACK"** enable — ACK-based flow control at the knxultimate layer; not sufficient alone, useful in combination.
|
|
62
|
+
5. **"linkedStateDebounce"** set on chronically flapping sources — the adapter points to the affected GA in the error message.
|
|
63
|
+
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
64
|
|
|
65
65
|
## Changelog
|
|
66
66
|
|
|
@@ -68,6 +68,10 @@ Empfohlene Reihenfolge (mit dem günstigsten Schritt anfangen, nur eskalieren we
|
|
|
68
68
|
Placeholder for the next version (at the beginning of the line):
|
|
69
69
|
### **WORK IN PROGRESS**
|
|
70
70
|
-->
|
|
71
|
+
### 1.1.14 (2026-06-11)
|
|
72
|
+
- improve reconnect logic
|
|
73
|
+
- improve project import
|
|
74
|
+
|
|
71
75
|
### 1.1.13 (2026-06-01)
|
|
72
76
|
|
|
73
77
|
- feat: improve disconnect handling on high bus load for MDT Gateways
|
|
@@ -83,9 +87,6 @@ Empfohlene Reihenfolge (mit dem günstigsten Schritt anfangen, nur eskalieren we
|
|
|
83
87
|
### 1.1.10 (2026-05-05)
|
|
84
88
|
- add new DPTs and units
|
|
85
89
|
|
|
86
|
-
### 1.1.9 (2026-05-04)
|
|
87
|
-
- fix handling of unkown DPT
|
|
88
|
-
|
|
89
90
|
### initial version
|
|
90
91
|
|
|
91
92
|
- initial version from https://www.npmjs.com/package/iobroker.knx/v/0.8.3
|
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,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "openknx",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.14",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.1.14": {
|
|
7
|
+
"en": "improve reconnect logic\nimprove project import",
|
|
8
|
+
"de": "reconnect logik verbessert\nprojektimport verbessert",
|
|
9
|
+
"ru": "улучшить логику подключения\nулучшение импорта проектов",
|
|
10
|
+
"pt": "melhorar a lógica de reconexão\nmelhorar a importação do projecto",
|
|
11
|
+
"nl": "de herverbindingslogica verbeteren\nprojectimport verbeteren",
|
|
12
|
+
"fr": "améliorer la logique de reconnect\naméliorer l'importation des projets",
|
|
13
|
+
"it": "migliorare la logica di riconnettere\nmigliorare l'importazione di progetti",
|
|
14
|
+
"es": "mejorar la lógica de reconexión\nmejoramiento de la importación de proyectos",
|
|
15
|
+
"pl": "poprawić logikę ponownego połączenia\npoprawa importu projektów",
|
|
16
|
+
"uk": "поліпшення логіки відключення\nвдосконалення імпорту проекту",
|
|
17
|
+
"zh-cn": "改进重联逻辑\n改进项目导入"
|
|
18
|
+
},
|
|
6
19
|
"1.1.13": {
|
|
7
20
|
"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
21
|
"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",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "dodać tryb kompatybilności für raw DPT5. *\nzmniejszyć domyślne opóźnienie do 25ms",
|
|
81
94
|
"uk": "додати режим сумісності für raw DPT5.*\nзменшити затримку за замовчуванням до 25ms",
|
|
82
95
|
"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": {
|
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) {
|
|
@@ -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.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "ioBroker knx Adapter",
|
|
5
5
|
"author": "boellner",
|
|
6
6
|
"contributors": [
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"node": ">=20"
|
|
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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@iobroker/eslint-config": "^2.3.4",
|
|
52
52
|
"@iobroker/testing": "^5.2.2",
|
|
53
53
|
"@tsconfig/node20": "^20.1.9",
|
|
54
|
-
"@types/node": "^20.19.
|
|
54
|
+
"@types/node": "^20.19.43",
|
|
55
55
|
"typescript": "~5.9.3"
|
|
56
56
|
},
|
|
57
57
|
"main": "main.js",
|