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 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-Gateways bestätigen Tunnel-Frames langsamer als die meisten Wettbewerber (ca. 2 Telegramme/Sekunde). Bei höherem Durchsatz reagiert das Gerät mit ACK-Timeouts oder dropt die Tunnel-Verbindung. Der Adapter erkennt MDT automatisch (deviceName-Match) und warnt im Log, falls die Einstellungen nicht passen.
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
- Empfohlene Reihenfolge (mit dem günstigsten Schritt anfangen, nur eskalieren wenn nötig):
52
+ Recommended order (start with the cheapest step, only escalate if needed):
53
53
 
54
- 1. **In der ETS** Keep-alive-Timeout erhöhenGrößter Effekt bei null Adapter-Aufwand:
55
- - **SCN-IP100 (Router):** Parameter "Tunneling: längeres Überwachungsintervall" → auf "Ja"
56
- - **SCN-IP000 (Interface):** Parameter "Langsame Tunneling-Verbindungen unterstützen" → auf "Ja"
57
-
58
- Beide setzen den Keep-alive-Timeout von ~10 s auf 60 s. Kurze Netzwackler lösen dann keinen DISCONNECT_REQUEST mehr aus.
59
- 2. **Autoread deaktivieren** — eliminiert den Startup-Burst (häufigster Auslöser des ersten Disconnects).
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 setzender 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-Stacklöst hartnäckige Disconnects definitiv. Voraussetzungen: Multicast-fähiges Netzwerk (IGMP-Snooping korrekt konfiguriert). Trade-off: keine Per-Frame-Bestätigung mehrverlorene Telegramme werden nicht auf Transport-Ebene erkannt.
54
+ 1. **In ETS** raise the keep-alive timeoutbiggest 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 sourcesthe 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 acknowledgementlost 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
@@ -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; background:#f5f5f5; color:#555; font-size:0.9em;">
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.13",
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
- const objects = await this.getAdapterObjectsAsync();
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
- this.log.info(
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.delObject(object[0], err => {
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
- try {
541
- await this.delObjectAsync("", { recursive: true });
542
- } catch (e) {
543
- this.log.warn(`delObjectAsync("") failed (${e.message}), falling back to manual deletion`);
544
- const objects = await this.getAdapterObjectsAsync();
545
- for (const id of Object.keys(objects)) {
546
- await this.delObjectAsync(id).catch(() => {});
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.namespace,
567
- endkey: `${this.namespace}\u9999`,
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
- this.setObjectNotExists(`${this.mynamespace}.${objects[i]._id}`, objects[i], err => {
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
- this.setObject(`${this.mynamespace}.${objects[i]._id}`, objects[i], err => {
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 delays in seconds: 10, 30, 60, 120, 120, 120, 120
1405
- static reconnectDelays = [10, 30, 60, 120, 120, 120, 120];
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 || this.reconnectCount >= openknx.reconnectDelays.length) {
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
- const delay = openknx.reconnectDelays[this.reconnectCount];
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
- this.log.info(`Reconnect attempt ${this.reconnectCount}/${openknx.reconnectDelays.length} in ${delay}s...`);
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
- this.scheduleReconnect();
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
- this.log.warn(err.message || String(err));
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.13",
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.3.2",
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.8",
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.0",
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.41",
54
+ "@types/node": "^20.19.43",
55
55
  "typescript": "~5.9.3"
56
56
  },
57
57
  "main": "main.js",