iobroker.homewizard 0.7.6 → 0.7.7

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
@@ -170,6 +170,11 @@ homewizard.0.
170
170
  Placeholder for the next version (at the beginning of the line):
171
171
  ### **WORK IN PROGRESS**
172
172
  -->
173
+ ### 0.7.7 (2026-05-13)
174
+
175
+ - Devices with chronically bad WiFi no longer flood the log: max one warn per hour when the device drops out, one info when it comes back. Full timeline stays at debug level.
176
+ - Internal reconnect-strategy adjustments (unstable / normal mode switches) moved from info to debug — not user-actionable.
177
+
173
178
  ### 0.7.6 (2026-05-12)
174
179
 
175
180
  - The battery mode dropdown and the tariff state no longer crash the admin with "Error in GUI" when opened.
@@ -190,9 +195,6 @@ homewizard.0.
190
195
  ### 0.7.3 (2026-05-07)
191
196
  - Less log spam when a device stays offline for longer periods — the initial `device unreachable` warning is enough; mDNS recovery attempts and offline-retry status now log at debug level only.
192
197
 
193
- ### 0.7.2 (2026-05-06)
194
- - Internal hardening: stricter number parsing for sensor inputs, parallel state writes, code split for testability, 38 new tests covering the HTTPS client. No user-facing changes.
195
-
196
198
  ### Support Development
197
199
 
198
200
  This adapter is free and open source. If you find it useful, consider buying me a coffee:
@@ -22,6 +22,7 @@ __export(main_helpers_exports, {
22
22
  decideUnstableTransition: () => decideUnstableTransition,
23
23
  findConnectionForState: () => findConnectionForState,
24
24
  pickRestPollInterval: () => pickRestPollInterval,
25
+ shouldEmitAfterCooldown: () => shouldEmitAfterCooldown,
25
26
  shouldStartIpRecovery: () => shouldStartIpRecovery,
26
27
  stripNamespace: () => stripNamespace
27
28
  });
@@ -61,12 +62,19 @@ function findConnectionForState(stateId, namespace, connections) {
61
62
  }
62
63
  return void 0;
63
64
  }
65
+ function shouldEmitAfterCooldown(lastMs, now, cooldownMs) {
66
+ if (lastMs === 0) {
67
+ return true;
68
+ }
69
+ return now - lastMs >= cooldownMs;
70
+ }
64
71
  // Annotate the CommonJS export names for ESM import in node:
65
72
  0 && (module.exports = {
66
73
  computeReconnectDelay,
67
74
  decideUnstableTransition,
68
75
  findConnectionForState,
69
76
  pickRestPollInterval,
77
+ shouldEmitAfterCooldown,
70
78
  shouldStartIpRecovery,
71
79
  stripNamespace
72
80
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/main-helpers.ts"],
4
- "sourcesContent": ["/**\n * Pure decision helpers for the adapter lifecycle. Extracted from `main.ts` so\n * the math/branching can be unit-tested without spinning up an adapter mock.\n *\n * Nothing in here touches `this.adapter` / `this.log` / timers \u2014 caller wires\n * the result back to the actual side-effects.\n */\n\nimport type { DeviceConnection } from \"./types\";\n\n/** Outcome of {@link decideUnstableTransition}. */\nexport type UnstableTransition = \"becameUnstable\" | \"stabilized\" | \"noChange\";\n\n/**\n * Decide whether a device just crossed into \"unstable\" mode (too many short\n * connections in a row) or back to \"stable\". Pure function over the connection\n * counters \u2014 caller updates `recentDisconnects` and emits the info-log.\n *\n * @param prevDisconnects `conn.recentDisconnects` before this disconnect.\n * @param durationMs How long the connection lived this time (`now - lastConnectedAt`).\n * @param stableThresholdMs `STABLE_THRESHOLD_MS` constant.\n * @param unstableThreshold `UNSTABLE_DISCONNECT_THRESHOLD` constant.\n */\nexport function decideUnstableTransition(\n prevDisconnects: number,\n durationMs: number,\n stableThresholdMs: number,\n unstableThreshold: number,\n): UnstableTransition {\n if (durationMs < stableThresholdMs) {\n // Disconnect happened within the stable window \u2192 counter goes up.\n const next = prevDisconnects + 1;\n return next === unstableThreshold ? \"becameUnstable\" : \"noChange\";\n }\n // Connection survived the stable window \u2192 counter resets.\n return prevDisconnects >= unstableThreshold ? \"stabilized\" : \"noChange\";\n}\n\n/**\n * Compute exponential-backoff delay for the next WebSocket reconnect attempt.\n *\n * @param failCount Consecutive failures (`conn.wsFailCount`, already incremented for this attempt).\n * @param baseMs `WS_RECONNECT_BASE_MS`.\n * @param maxMs `WS_RECONNECT_MAX_MS` (stable) or `WS_RECONNECT_MAX_UNSTABLE_MS`.\n */\nexport function computeReconnectDelay(failCount: number, baseMs: number, maxMs: number): number {\n if (failCount <= 0) {\n return baseMs;\n }\n return Math.min(baseMs * Math.pow(2, failCount - 1), maxMs);\n}\n\n/**\n * Decide whether mDNS IP-recovery should kick off on this WS-failure tick.\n * After `beforeMdns` failures, recovery runs once; thereafter it retries every\n * `retryEvery` failures (~hourly given the 5-minute cap).\n *\n * @param failCount Consecutive failures (post-increment).\n * @param beforeMdns `WS_FAILURES_BEFORE_MDNS`.\n * @param retryEvery `MDNS_RETRY_EVERY`.\n */\nexport function shouldStartIpRecovery(failCount: number, beforeMdns: number, retryEvery: number): boolean {\n if (failCount < beforeMdns) {\n return false;\n }\n return (failCount - beforeMdns) % retryEvery === 0;\n}\n\n/**\n * Pick the REST-fallback poll interval based on connection stability. Unstable\n * devices poll faster to keep the data flowing while WS reconnect is still\n * trying.\n *\n * @param unstable Whether the device is currently in unstable mode.\n * @param stableIntervalMs `REST_POLL_MS`.\n * @param unstableIntervalMs `REST_POLL_UNSTABLE_MS`.\n */\nexport function pickRestPollInterval(unstable: boolean, stableIntervalMs: number, unstableIntervalMs: number): number {\n return unstable ? unstableIntervalMs : stableIntervalMs;\n}\n\n/**\n * Strip the adapter namespace prefix from a state-ID. Only strips when the\n * prefix matches at the start \u2014 defensive against unexpected IDs.\n *\n * @param stateId Full state-ID (`<namespace>.<localId>`).\n * @param namespace Adapter namespace (e.g. `homewizard.0`).\n */\nexport function stripNamespace(stateId: string, namespace: string): string {\n const prefix = `${namespace}.`;\n return stateId.startsWith(prefix) ? stateId.slice(prefix.length) : stateId;\n}\n\n/**\n * Find the device connection that owns a given state-ID. Pure linear lookup\n * over the connection map \u2014 fine for the typical 1\u201310 paired devices.\n *\n * @param stateId Full state-ID written by the user.\n * @param namespace Adapter namespace.\n * @param connections Iterable of `(prefix, connection)` pairs (`map.entries()`-shape).\n */\nexport function findConnectionForState<T extends DeviceConnection>(\n stateId: string,\n namespace: string,\n connections: Iterable<[string, T]>,\n): T | undefined {\n const localId = stripNamespace(stateId, namespace);\n for (const [prefix, conn] of connections) {\n if (localId.startsWith(`${prefix}.`)) {\n return conn;\n }\n }\n return undefined;\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBO,SAAS,yBACd,iBACA,YACA,mBACA,mBACoB;AACpB,MAAI,aAAa,mBAAmB;AAElC,UAAM,OAAO,kBAAkB;AAC/B,WAAO,SAAS,oBAAoB,mBAAmB;AAAA,EACzD;AAEA,SAAO,mBAAmB,oBAAoB,eAAe;AAC/D;AASO,SAAS,sBAAsB,WAAmB,QAAgB,OAAuB;AAC9F,MAAI,aAAa,GAAG;AAClB,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,SAAS,KAAK,IAAI,GAAG,YAAY,CAAC,GAAG,KAAK;AAC5D;AAWO,SAAS,sBAAsB,WAAmB,YAAoB,YAA6B;AACxG,MAAI,YAAY,YAAY;AAC1B,WAAO;AAAA,EACT;AACA,UAAQ,YAAY,cAAc,eAAe;AACnD;AAWO,SAAS,qBAAqB,UAAmB,kBAA0B,oBAAoC;AACpH,SAAO,WAAW,qBAAqB;AACzC;AASO,SAAS,eAAe,SAAiB,WAA2B;AACzE,QAAM,SAAS,GAAG,SAAS;AAC3B,SAAO,QAAQ,WAAW,MAAM,IAAI,QAAQ,MAAM,OAAO,MAAM,IAAI;AACrE;AAUO,SAAS,uBACd,SACA,WACA,aACe;AACf,QAAM,UAAU,eAAe,SAAS,SAAS;AACjD,aAAW,CAAC,QAAQ,IAAI,KAAK,aAAa;AACxC,QAAI,QAAQ,WAAW,GAAG,MAAM,GAAG,GAAG;AACpC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["/**\n * Pure decision helpers for the adapter lifecycle. Extracted from `main.ts` so\n * the math/branching can be unit-tested without spinning up an adapter mock.\n *\n * Nothing in here touches `this.adapter` / `this.log` / timers \u2014 caller wires\n * the result back to the actual side-effects.\n */\n\nimport type { DeviceConnection } from \"./types\";\n\n/** Outcome of {@link decideUnstableTransition}. */\nexport type UnstableTransition = \"becameUnstable\" | \"stabilized\" | \"noChange\";\n\n/**\n * Decide whether a device just crossed into \"unstable\" mode (too many short\n * connections in a row) or back to \"stable\". Pure function over the connection\n * counters \u2014 caller updates `recentDisconnects` and emits the info-log.\n *\n * @param prevDisconnects `conn.recentDisconnects` before this disconnect.\n * @param durationMs How long the connection lived this time (`now - lastConnectedAt`).\n * @param stableThresholdMs `STABLE_THRESHOLD_MS` constant.\n * @param unstableThreshold `UNSTABLE_DISCONNECT_THRESHOLD` constant.\n */\nexport function decideUnstableTransition(\n prevDisconnects: number,\n durationMs: number,\n stableThresholdMs: number,\n unstableThreshold: number,\n): UnstableTransition {\n if (durationMs < stableThresholdMs) {\n // Disconnect happened within the stable window \u2192 counter goes up.\n const next = prevDisconnects + 1;\n return next === unstableThreshold ? \"becameUnstable\" : \"noChange\";\n }\n // Connection survived the stable window \u2192 counter resets.\n return prevDisconnects >= unstableThreshold ? \"stabilized\" : \"noChange\";\n}\n\n/**\n * Compute exponential-backoff delay for the next WebSocket reconnect attempt.\n *\n * @param failCount Consecutive failures (`conn.wsFailCount`, already incremented for this attempt).\n * @param baseMs `WS_RECONNECT_BASE_MS`.\n * @param maxMs `WS_RECONNECT_MAX_MS` (stable) or `WS_RECONNECT_MAX_UNSTABLE_MS`.\n */\nexport function computeReconnectDelay(failCount: number, baseMs: number, maxMs: number): number {\n if (failCount <= 0) {\n return baseMs;\n }\n return Math.min(baseMs * Math.pow(2, failCount - 1), maxMs);\n}\n\n/**\n * Decide whether mDNS IP-recovery should kick off on this WS-failure tick.\n * After `beforeMdns` failures, recovery runs once; thereafter it retries every\n * `retryEvery` failures (~hourly given the 5-minute cap).\n *\n * @param failCount Consecutive failures (post-increment).\n * @param beforeMdns `WS_FAILURES_BEFORE_MDNS`.\n * @param retryEvery `MDNS_RETRY_EVERY`.\n */\nexport function shouldStartIpRecovery(failCount: number, beforeMdns: number, retryEvery: number): boolean {\n if (failCount < beforeMdns) {\n return false;\n }\n return (failCount - beforeMdns) % retryEvery === 0;\n}\n\n/**\n * Pick the REST-fallback poll interval based on connection stability. Unstable\n * devices poll faster to keep the data flowing while WS reconnect is still\n * trying.\n *\n * @param unstable Whether the device is currently in unstable mode.\n * @param stableIntervalMs `REST_POLL_MS`.\n * @param unstableIntervalMs `REST_POLL_UNSTABLE_MS`.\n */\nexport function pickRestPollInterval(unstable: boolean, stableIntervalMs: number, unstableIntervalMs: number): number {\n return unstable ? unstableIntervalMs : stableIntervalMs;\n}\n\n/**\n * Strip the adapter namespace prefix from a state-ID. Only strips when the\n * prefix matches at the start \u2014 defensive against unexpected IDs.\n *\n * @param stateId Full state-ID (`<namespace>.<localId>`).\n * @param namespace Adapter namespace (e.g. `homewizard.0`).\n */\nexport function stripNamespace(stateId: string, namespace: string): string {\n const prefix = `${namespace}.`;\n return stateId.startsWith(prefix) ? stateId.slice(prefix.length) : stateId;\n}\n\n/**\n * Find the device connection that owns a given state-ID. Pure linear lookup\n * over the connection map \u2014 fine for the typical 1\u201310 paired devices.\n *\n * @param stateId Full state-ID written by the user.\n * @param namespace Adapter namespace.\n * @param connections Iterable of `(prefix, connection)` pairs (`map.entries()`-shape).\n */\nexport function findConnectionForState<T extends DeviceConnection>(\n stateId: string,\n namespace: string,\n connections: Iterable<[string, T]>,\n): T | undefined {\n const localId = stripNamespace(stateId, namespace);\n for (const [prefix, conn] of connections) {\n if (localId.startsWith(`${prefix}.`)) {\n return conn;\n }\n }\n return undefined;\n}\n\n/**\n * Cooldown gate: whether a warn/info should be emitted right now, given the\n * last-emit timestamp for the same key.\n *\n * - `lastMs === 0` (never emitted) \u2192 true (emit, set stamp).\n * - `now - lastMs >= cooldownMs` \u2192 true (window expired, emit, refresh stamp).\n * - otherwise \u2192 false (caller demotes to debug).\n *\n * Used per-device in main.ts: `logDeviceError` warn-path + `onConnected`\n * recovery-info-path. Caller owns the timestamp map and updates it iff this\n * returns true.\n *\n * @param lastMs Last-emit timestamp (ms) or 0 if never.\n * @param now Current timestamp (ms) \u2014 caller-controlled for test determinism.\n * @param cooldownMs Cooldown window in ms.\n */\nexport function shouldEmitAfterCooldown(lastMs: number, now: number, cooldownMs: number): boolean {\n if (lastMs === 0) {\n return true;\n }\n return now - lastMs >= cooldownMs;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBO,SAAS,yBACd,iBACA,YACA,mBACA,mBACoB;AACpB,MAAI,aAAa,mBAAmB;AAElC,UAAM,OAAO,kBAAkB;AAC/B,WAAO,SAAS,oBAAoB,mBAAmB;AAAA,EACzD;AAEA,SAAO,mBAAmB,oBAAoB,eAAe;AAC/D;AASO,SAAS,sBAAsB,WAAmB,QAAgB,OAAuB;AAC9F,MAAI,aAAa,GAAG;AAClB,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,SAAS,KAAK,IAAI,GAAG,YAAY,CAAC,GAAG,KAAK;AAC5D;AAWO,SAAS,sBAAsB,WAAmB,YAAoB,YAA6B;AACxG,MAAI,YAAY,YAAY;AAC1B,WAAO;AAAA,EACT;AACA,UAAQ,YAAY,cAAc,eAAe;AACnD;AAWO,SAAS,qBAAqB,UAAmB,kBAA0B,oBAAoC;AACpH,SAAO,WAAW,qBAAqB;AACzC;AASO,SAAS,eAAe,SAAiB,WAA2B;AACzE,QAAM,SAAS,GAAG,SAAS;AAC3B,SAAO,QAAQ,WAAW,MAAM,IAAI,QAAQ,MAAM,OAAO,MAAM,IAAI;AACrE;AAUO,SAAS,uBACd,SACA,WACA,aACe;AACf,QAAM,UAAU,eAAe,SAAS,SAAS;AACjD,aAAW,CAAC,QAAQ,IAAI,KAAK,aAAa;AACxC,QAAI,QAAQ,WAAW,GAAG,MAAM,GAAG,GAAG;AACpC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAkBO,SAAS,wBAAwB,QAAgB,KAAa,YAA6B;AAChG,MAAI,WAAW,GAAG;AAChB,WAAO;AAAA,EACT;AACA,SAAO,MAAM,UAAU;AACzB;",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -42,10 +42,23 @@ const MDNS_RETRY_EVERY = 12;
42
42
  const STABLE_THRESHOLD_MS = 6e5;
43
43
  const WS_RECONNECT_MAX_UNSTABLE_MS = 6e4;
44
44
  const REST_POLL_UNSTABLE_MS = 3e4;
45
+ const WARN_COOLDOWN_MS = 60 * 60 * 1e3;
46
+ const INFO_COOLDOWN_MS = 60 * 60 * 1e3;
45
47
  class HomeWizard extends utils.Adapter {
46
48
  stateManager;
47
49
  discovery = null;
48
50
  connections = /* @__PURE__ */ new Map();
51
+ /**
52
+ * Per-device last-warn timestamp for chronic-bouncing cooldown. Key =
53
+ * `conn.config.serial` (kategorienübergreifend). The classifyError-based
54
+ * `lastErrorCode`-Dedup in {@link logDeviceError} resets on every recovery,
55
+ * so on chronic bouncing a new disconnect counts as "first occurrence"
56
+ * → wieder warn. This cooldown stamp persists across recoveries so the user
57
+ * sees max one warn per WARN_COOLDOWN_MS per device.
58
+ */
59
+ lastWarnAt = /* @__PURE__ */ new Map();
60
+ /** Per-device last-info timestamp for `connection restored`. Analog cooldown. */
61
+ lastInfoAt = /* @__PURE__ */ new Map();
49
62
  pairingTimer = void 0;
50
63
  pairingPollTimer = void 0;
51
64
  systemPollTimer = void 0;
@@ -546,6 +559,7 @@ class HomeWizard extends utils.Adapter {
546
559
  });
547
560
  },
548
561
  onConnected: () => {
562
+ var _a;
549
563
  conn.wsAuthenticated = true;
550
564
  conn.wsFailCount = 0;
551
565
  conn.authFailCount = 0;
@@ -564,9 +578,15 @@ class HomeWizard extends utils.Adapter {
564
578
  }
565
579
  }
566
580
  if (conn.lastErrorCode) {
567
- this.log.info(
568
- this.isUnstable(conn) ? `${conn.config.productName}: connection restored (unstable mode)` : `${conn.config.productName}: connection restored`
569
- );
581
+ const now = Date.now();
582
+ const lastInfo = (_a = this.lastInfoAt.get(conn.config.serial)) != null ? _a : 0;
583
+ const msg = this.isUnstable(conn) ? `${conn.config.productName}: connection restored (unstable mode)` : `${conn.config.productName}: connection restored`;
584
+ if ((0, import_main_helpers.shouldEmitAfterCooldown)(lastInfo, now, INFO_COOLDOWN_MS)) {
585
+ this.lastInfoAt.set(conn.config.serial, now);
586
+ this.log.info(msg);
587
+ } else {
588
+ this.log.debug(`${msg} (cooldown)`);
589
+ }
570
590
  conn.lastErrorCode = "";
571
591
  }
572
592
  this.log.debug(`WebSocket connected to ${conn.config.productName} (${conn.ip})`);
@@ -587,9 +607,9 @@ class HomeWizard extends utils.Adapter {
587
607
  conn.recentDisconnects = 0;
588
608
  }
589
609
  if (transition === "becameUnstable") {
590
- this.log.info(`${conn.config.productName}: unstable connection detected \u2014 using faster reconnect`);
610
+ this.log.debug(`${conn.config.productName}: unstable connection detected \u2014 using faster reconnect`);
591
611
  } else if (transition === "stabilized") {
592
- this.log.info(`${conn.config.productName}: connection stabilized \u2014 using normal reconnect`);
612
+ this.log.debug(`${conn.config.productName}: connection stabilized \u2014 using normal reconnect`);
593
613
  }
594
614
  }
595
615
  conn.wsAuthenticated = false;
@@ -816,20 +836,43 @@ class HomeWizard extends utils.Adapter {
816
836
  return false;
817
837
  }
818
838
  /**
819
- * Log device error with deduplication (based on error category, not context).
820
- * First occurrence of a new error category logs as warn, repeats as debug.
839
+ * Log device error with deduplication.
840
+ *
841
+ * Two-stage dedup:
842
+ * 1. `lastErrorCode` per connection — repeats of the same error category go
843
+ * to debug. Resets on recovery (`onConnected` clears it) so a new category
844
+ * after recovery surfaces as warn again. Designtechnisch correct for
845
+ * „new failure mode" but blind to chronic bouncing.
846
+ * 2. {@link lastWarnAt} per device serial — survives recovery. Even if the
847
+ * `lastErrorCode`-Dedup says „first occurrence", the cooldown stamp keeps
848
+ * the warn-emit suppressed if we've warned for this device within
849
+ * {@link WARN_COOLDOWN_MS}. Chronic bouncing produces at most 1× warn per
850
+ * hour per device.
851
+ *
852
+ * Cooldown key is the device serial — category-spanning. A flapping P1 that
853
+ * cycles TIMEOUT→NETWORK→TIMEOUT is one phenomenon, one warn-budget.
821
854
  *
822
855
  * @param conn Device connection
823
856
  * @param context Error context (for debug messages only)
824
857
  * @param err Error object
825
858
  */
826
859
  logDeviceError(conn, context, err) {
860
+ var _a;
827
861
  const errorCode = (0, import_connection_utils.classifyError)(err);
828
862
  const isRepeat = errorCode === conn.lastErrorCode;
829
863
  conn.lastErrorCode = errorCode;
830
864
  if (isRepeat) {
831
865
  this.log.debug(`${conn.config.productName} ${context}: ${(0, import_coerce.errText)(err)}`);
832
- } else if (errorCode === "NETWORK") {
866
+ return;
867
+ }
868
+ const now = Date.now();
869
+ const lastWarn = (_a = this.lastWarnAt.get(conn.config.serial)) != null ? _a : 0;
870
+ if (!(0, import_main_helpers.shouldEmitAfterCooldown)(lastWarn, now, WARN_COOLDOWN_MS)) {
871
+ this.log.debug(`${conn.config.productName} ${context} (cooldown): ${(0, import_coerce.errText)(err)}`);
872
+ return;
873
+ }
874
+ this.lastWarnAt.set(conn.config.serial, now);
875
+ if (errorCode === "NETWORK") {
833
876
  this.log.warn(`${conn.config.productName}: device unreachable \u2014 will keep retrying`);
834
877
  } else {
835
878
  this.log.warn(`${conn.config.productName} ${context}: ${(0, import_coerce.errText)(err)}`);
package/build/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { errText, isValidIpv4, parseBatteryPermissions, validateBatteryMode } from \"./lib/coerce\";\nimport { classifyError, createDeviceConnection, UNSTABLE_DISCONNECT_THRESHOLD } from \"./lib/connection-utils\";\nimport { HomeWizardDiscovery } from \"./lib/discovery\";\nimport { HomeWizardApiError, HomeWizardClient } from \"./lib/homewizard-client\";\nimport {\n computeReconnectDelay,\n decideUnstableTransition,\n findConnectionForState as resolveConnectionForState,\n pickRestPollInterval,\n shouldStartIpRecovery,\n} from \"./lib/main-helpers\";\nimport { StateManager } from \"./lib/state-manager\";\nimport type { DeviceConfig, DeviceConnection, DiscoveredDevice, Measurement } from \"./lib/types\";\nimport { HomeWizardWebSocket } from \"./lib/websocket-client\";\n\n/** Pairing timeout in milliseconds (60 seconds) */\nconst PAIRING_TIMEOUT_MS = 60_000;\n/** Pairing poll interval in milliseconds */\nconst PAIRING_POLL_MS = 2_000;\n/** WebSocket reconnect base delay in milliseconds */\nconst WS_RECONNECT_BASE_MS = 5_000;\n/** Maximum WebSocket reconnect delay in milliseconds */\nconst WS_RECONNECT_MAX_MS = 300_000;\n/** REST fallback poll interval in milliseconds */\nconst REST_POLL_MS = 10_000;\n/** System info poll interval in milliseconds */\nconst SYSTEM_POLL_MS = 60_000;\n/** Max auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\n/** WS failures before starting mDNS IP recovery */\nconst WS_FAILURES_BEFORE_MDNS = 3;\n/** mDNS IP recovery timeout in milliseconds */\nconst IP_RECOVERY_TIMEOUT_MS = 60_000;\n/** Retry mDNS every N WS failures after first attempt (~1 hour at 5 min cap) */\nconst MDNS_RETRY_EVERY = 12;\n/** Connection must last this long to count as \"stable\" */\nconst STABLE_THRESHOLD_MS = 600_000;\n/** Max reconnect delay for unstable devices */\nconst WS_RECONNECT_MAX_UNSTABLE_MS = 60_000;\n/** REST fallback interval for unstable devices (slower, not stopped) */\nconst REST_POLL_UNSTABLE_MS = 30_000;\n\nclass HomeWizard extends utils.Adapter {\n private stateManager!: StateManager;\n private discovery: HomeWizardDiscovery | null = null;\n private readonly connections = new Map<string, DeviceConnection>();\n private pairingTimer: ioBroker.Timeout | undefined = undefined;\n private pairingPollTimer: ioBroker.Interval | undefined = undefined;\n private systemPollTimer: ioBroker.Interval | undefined = undefined;\n private ipRecoveryTimer: ioBroker.Timeout | undefined = undefined;\n private isPairing = false;\n private pairingManualIp = \"\";\n private discoveredDuringPairing: DiscoveredDevice[] = [];\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n /** Set during onUnload \u2014 async paths bail before further setStateAsync calls. */\n private unloading = false;\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"homewizard\" });\n // Wrap async handlers with .catch() so a rejection can never become an\n // unhandled promise rejection (\u2192 SIGKILL \u2192 js-controller restart loop).\n this.on(\"ready\", () => {\n this.onReady().catch((err: unknown) => this.log.error(`onReady failed: ${errText(err)}`));\n });\n this.on(\"stateChange\", (id, state) => {\n this.onStateChange(id, state).catch((err: unknown) => this.log.error(`stateChange failed: ${errText(err)}`));\n });\n this.on(\"unload\", callback => this.onUnload(callback));\n\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths. The per-handler wrappers cover documented async\n // paths; this catches anything that slips past during refactors.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(`Unhandled rejection: ${errText(reason)}`);\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${err.message}`);\n };\n process.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n process.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n }\n\n /** Adapter started */\n private async onReady(): Promise<void> {\n this.stateManager = new StateManager(this);\n\n // `pairingIp` is declared in io-package.json instanceObjects \u2014 just reset state.\n // Reset pairing states on start (in case previous run was killed mid-pairing)\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\n // Subscribe to pairing button and writable device states\n await this.subscribeStatesAsync(\"startPairing\");\n await this.subscribeStatesAsync(\"*.system.reboot\");\n await this.subscribeStatesAsync(\"*.system.identify\");\n await this.subscribeStatesAsync(\"*.system.cloud_enabled\");\n await this.subscribeStatesAsync(\"*.system.status_led_brightness_pct\");\n await this.subscribeStatesAsync(\"*.system.api_v1_enabled\");\n await this.subscribeStatesAsync(\"*.battery.mode\");\n await this.subscribeStatesAsync(\"*.battery.permissions\");\n await this.subscribeStatesAsync(\"*.remove\");\n\n // Load devices from device objects (not from adapter config)\n const devices = await this.loadDevicesFromObjects();\n if (devices.length === 0) {\n this.log.info(`No devices configured \u2014 set 'startPairing' to true to add a device`);\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n }\n\n // Create connection entries for all configured devices\n for (const device of devices) {\n const key = this.stateManager.devicePrefix(device);\n await this.stateManager.cleanupMovedStates(device);\n await this.stateManager.createDeviceStates(device);\n const conn = createDeviceConnection(device, device.ip || \"\");\n this.connections.set(key, conn);\n\n // If we have a stored IP, connect immediately\n if (conn.ip) {\n this.log.debug(`Using stored IP ${conn.ip} for ${device.productName}`);\n void this.initDevice(conn);\n }\n }\n\n // Periodic system info poll\n this.systemPollTimer = this.setInterval(() => {\n void this.pollAllSystemInfo();\n }, SYSTEM_POLL_MS);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Load device configs from existing device objects\n * Tokens are stored encrypted in device object native\n */\n private async loadDevicesFromObjects(): Promise<DeviceConfig[]> {\n const devices: DeviceConfig[] = [];\n\n // Also migrate from old adapter config if devices exist there.\n // Defensive: native.devices could be a non-array if a previous version\n // wrote a different shape, or if the user edited it manually.\n const rawOldDevices = (this.config as Record<string, unknown>).devices;\n const oldDevices: DeviceConfig[] = Array.isArray(rawOldDevices) ? (rawOldDevices as DeviceConfig[]) : [];\n if (oldDevices.length > 0) {\n this.log.debug(`Migrating ${oldDevices.length} device(s) from adapter config to device objects`);\n for (const device of oldDevices) {\n await this.saveDeviceToObject(device);\n }\n // Clear old config (this triggers one restart, but only during migration)\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, {\n native: { devices: [] },\n });\n return oldDevices;\n }\n\n // Read device objects from our namespace. A corrupted encryptedToken\n // (e.g. after secret rotation, crypto-lib changes, manual DB edits) must\n // not take down the whole adapter \u2014 skip the broken device, keep the rest.\n const objects = await this.getAdapterObjectsAsync();\n for (const [id, obj] of Object.entries(objects)) {\n if (obj.type !== \"device\") {\n continue;\n }\n const native = obj.native as Record<string, string> | undefined;\n if (!native?.encryptedToken || !native.serial) {\n continue;\n }\n const localId = id.replace(`${this.namespace}.`, \"\");\n this.log.debug(`Loading device from object: ${localId}`);\n let token: string;\n try {\n token = this.decrypt(native.encryptedToken);\n } catch (err) {\n this.log.warn(\n `Cannot decrypt token for ${localId} \u2014 re-pair the device. ` +\n `(${errText(err)}). Other devices remain unaffected.`,\n );\n continue;\n }\n devices.push({\n token,\n productType: native.productType || \"unknown\",\n serial: native.serial,\n productName: native.productName || native.productType || \"unknown\",\n ...(native.ip ? { ip: native.ip } : {}),\n });\n }\n\n return devices;\n }\n\n /**\n * Save device config to its device object native (encrypted token)\n *\n * @param config Device configuration to save\n */\n private async saveDeviceToObject(config: DeviceConfig): Promise<void> {\n const prefix = this.stateManager.devicePrefix(config);\n const encryptedToken = this.encrypt(config.token);\n await this.extendObjectAsync(prefix, {\n type: \"device\",\n common: { name: config.productName || config.productType },\n native: {\n encryptedToken,\n productType: config.productType,\n serial: config.serial,\n productName: config.productName,\n ...(config.ip ? { ip: config.ip } : {}),\n },\n });\n }\n\n /**\n * Handle a discovered device from mDNS (only active during pairing)\n *\n * @param discovered Discovered device info\n */\n private onDeviceDiscovered(discovered: DiscoveredDevice): void {\n // Skip already paired devices\n const existing = Array.from(this.connections.values()).find(c => c.config.serial === discovered.serial);\n if (existing) {\n return;\n }\n\n // Skip duplicates\n if (this.discoveredDuringPairing.find(d => d.serial === discovered.serial)) {\n return;\n }\n\n this.discoveredDuringPairing.push(discovered);\n this.log.info(\n `Found ${discovered.name} (${discovered.productType}) at ${discovered.ip} \u2014 press the button on the device to pair`,\n );\n }\n\n /**\n * Adapter stopping \u2014 MUST be synchronous\n *\n * @param callback Completion callback\n */\n private onUnload(callback: () => void): void {\n // Set first, before any clearTimeout \u2014 in-flight async paths\n // (REST poll, getMeasurement, getSystem) check this after each await\n // and bail out before further setStateAsync on a tearing-down adapter.\n this.unloading = true;\n try {\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n }\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n }\n if (this.systemPollTimer) {\n this.clearInterval(this.systemPollTimer);\n }\n if (this.ipRecoveryTimer) {\n this.clearTimeout(this.ipRecoveryTimer);\n }\n\n this.discovery?.stop();\n\n for (const conn of this.connections.values()) {\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n }\n }\n this.connections.clear();\n\n // Detach process-level last-line-of-defence handlers\n if (this.unhandledRejectionHandler) {\n process.off(\"unhandledRejection\", this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off(\"uncaughtException\", this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n\n void this.setState(\"info.connection\", { val: false, ack: true });\n } finally {\n callback();\n }\n }\n\n /**\n * Handle state changes\n *\n * @param id State ID\n * @param state State value\n */\n private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n if (!state || state.ack || this.unloading) {\n return;\n }\n\n // Pairing button\n if (id.endsWith(\".startPairing\")) {\n if (state.val) {\n await this.startPairing();\n }\n return;\n }\n\n // Remove device button\n if (id.endsWith(\".remove\")) {\n if (state.val) {\n await this.removeDevice(id);\n }\n return;\n }\n\n // Find which device this state belongs to\n const conn = this.findConnectionForState(id);\n if (!conn || !conn.ip) {\n return;\n }\n\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n\n try {\n if (id.endsWith(\".system.reboot\")) {\n this.log.info(`Rebooting ${conn.config.productName} (${conn.ip})`);\n await client.reboot();\n } else if (id.endsWith(\".system.identify\")) {\n await client.identify();\n } else if (id.endsWith(\".system.cloud_enabled\")) {\n await client.setSystem({ cloud_enabled: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".system.status_led_brightness_pct\")) {\n await client.setSystem({\n status_led_brightness_pct: Number(state.val),\n });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".system.api_v1_enabled\")) {\n await client.setSystem({ api_v1_enabled: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.mode\")) {\n const mode = validateBatteryMode(String(state.val));\n if (!mode) {\n this.log.warn(`Invalid battery.mode value: '${String(state.val)}' \u2014 expected one of: zero, to_full, standby`);\n return;\n }\n await client.setBatteries({ mode });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.permissions\")) {\n const result = parseBatteryPermissions(String(state.val));\n if (!result.ok) {\n this.log.warn(\n `Invalid JSON for battery.permissions: ${result.reason} \u2014 expected array, got: ${result.sample}`,\n );\n return;\n }\n await client.setBatteries({ permissions: result.perms });\n await this.setStateAsync(id, { val: state.val, ack: true });\n }\n } catch (err) {\n this.log.warn(`Failed to set ${id}: ${errText(err)}`);\n }\n }\n\n /** Start pairing mode \u2014 discover devices and attempt to pair */\n private async startPairing(): Promise<void> {\n if (this.isPairing) {\n this.log.debug(\"Pairing already active\");\n return;\n }\n\n // Reset startPairing immediately so it doesn't survive a restart\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n\n this.isPairing = true;\n this.discoveredDuringPairing = [];\n\n // Stop IP recovery if running \u2014 pairing takes priority\n this.stopIpRecovery();\n\n // Check if manual IP is set, then clear pairingIp immediately\n const ipState = await this.getStateAsync(\"pairingIp\");\n this.pairingManualIp = ipState?.val ? String(ipState.val).trim() : \"\";\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\n if (this.pairingManualIp) {\n // Validate manual-IP up front \u2014 better to fail fast than wait 60s while\n // requestPairing keeps timing out against a malformed input.\n if (!isValidIpv4(this.pairingManualIp)) {\n this.log.warn(`Invalid pairing IP '${this.pairingManualIp}' \u2014 expected IPv4 (e.g. 192.168.1.42)`);\n this.isPairing = false;\n this.pairingManualIp = \"\";\n return;\n }\n this.log.info(\n `Pairing mode enabled for ${this.pairingManualIp} \u2014 press the button on your HomeWizard device now (60 seconds timeout)`,\n );\n // Add as discovered device immediately\n this.discoveredDuringPairing.push({\n ip: this.pairingManualIp,\n productType: \"unknown\",\n serial: \"unknown\",\n name: this.pairingManualIp,\n });\n } else {\n this.log.info(\n `Pairing mode enabled \u2014 searching for devices via mDNS, press the button on your HomeWizard device now (60 seconds timeout)`,\n );\n // Restart mDNS browser to trigger fresh query \u2014 already-cached devices\n // won't be re-announced otherwise and pairing would never find them\n if (!this.discovery) {\n this.discovery = new HomeWizardDiscovery(this.log);\n }\n this.discovery.start(discovered => {\n this.onDeviceDiscovered(discovered);\n });\n }\n\n // Poll discovered devices for pairing\n this.pairingPollTimer = this.setInterval(() => {\n void this.pollPairing();\n }, PAIRING_POLL_MS);\n\n // Timeout pairing\n this.pairingTimer = this.setTimeout(() => {\n this.stopPairing();\n this.log.info(`Pairing mode automatically disabled after 60 seconds timeout`);\n }, PAIRING_TIMEOUT_MS);\n }\n\n /** Poll all discovered devices to attempt pairing */\n private async pollPairing(): Promise<void> {\n for (const device of this.discoveredDuringPairing) {\n try {\n const client = new HomeWizardClient(device.ip);\n const result = await client.requestPairing();\n\n // Success! Button was pressed\n this.log.info(\n `Successfully paired with ${device.name} (${device.productType}) at ${device.ip} \u2014 connecting...`,\n );\n\n // Get device info\n const authedClient = new HomeWizardClient(device.ip, result.token);\n const info = await authedClient.getDeviceInfo();\n\n const deviceConfig: DeviceConfig = {\n token: result.token,\n productType: info.product_type,\n serial: info.serial,\n productName: info.product_name,\n ip: device.ip,\n };\n\n // Save to device object (no adapter restart!)\n await this.saveDeviceToObject(deviceConfig);\n await this.stateManager.createDeviceStates(deviceConfig);\n\n // Re-pair of an existing device (e.g. after factory reset): close the\n // old connection's wsClient + timers before overwriting the map entry,\n // otherwise the old WS keeps running as a zombie until restart.\n const key = this.stateManager.devicePrefix(deviceConfig);\n const previous = this.connections.get(key);\n if (previous) {\n this.log.debug(`Re-pair: closing previous connection for ${deviceConfig.productName}`);\n previous.wsClient?.close();\n if (previous.pollTimer) {\n this.clearInterval(previous.pollTimer);\n }\n if (previous.reconnectTimer) {\n this.clearTimeout(previous.reconnectTimer);\n }\n }\n\n // Create connection and connect\n const conn = createDeviceConnection(deviceConfig, device.ip);\n this.connections.set(key, conn);\n void this.initDevice(conn);\n\n // Remove from discovery list \u2014 but keep pairing window open so the\n // user can button-press additional devices in the same session.\n this.discoveredDuringPairing = this.discoveredDuringPairing.filter(d => d.serial !== info.serial);\n\n this.updateGlobalConnection();\n // Do NOT call stopPairing() here \u2014 pairingTimer (60 s) closes the\n // window naturally; meanwhile the user can pair more devices.\n continue;\n } catch (err) {\n // 403 = button not pressed yet \u2014 expected, keep polling\n if (err instanceof HomeWizardApiError && err.statusCode === 403) {\n continue;\n }\n this.log.debug(`Pairing poll error for ${device.ip}: ${errText(err)}`);\n }\n }\n }\n\n /** Stop pairing mode */\n private stopPairing(): void {\n this.isPairing = false;\n this.pairingManualIp = \"\";\n this.discoveredDuringPairing = [];\n\n // Stop mDNS \u2014 only needed during pairing\n if (this.discovery) {\n this.discovery.stop();\n this.discovery = null;\n }\n\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n this.pairingPollTimer = undefined;\n }\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n this.pairingTimer = undefined;\n }\n }\n\n /** Start mDNS to find devices that changed IP */\n private startIpRecovery(): void {\n // Don't start if already running or pairing\n if (this.discovery || this.isPairing) {\n return;\n }\n\n // Internal recovery \u2014 debug only. The initial disconnect already produced\n // one warn via logDeviceError; repeating that hourly while a device stays\n // offline is just spam.\n this.log.debug(`Device unreachable \u2014 searching for new IP via mDNS`);\n\n this.discovery = new HomeWizardDiscovery(this.log);\n this.discovery.start(discovered => {\n // Match against disconnected devices\n for (const conn of this.connections.values()) {\n if (conn.config.serial !== discovered.serial) {\n continue;\n }\n if (discovered.ip === conn.ip || conn.wsAuthenticated) {\n return; // Same IP or already connected\n }\n // Multiple mDNS broadcasts can arrive within one recovery window\n // (e.g. AP roam). Skip if a connect cycle is already in flight.\n if (conn.recovering) {\n return;\n }\n\n this.log.info(`${conn.config.productName}: found at new IP ${discovered.ip} (was ${conn.ip})`);\n\n // Update IP and persist \u2014 reset stability (new network conditions)\n conn.ip = discovered.ip;\n conn.config.ip = discovered.ip;\n conn.wsFailCount = 0;\n conn.recentDisconnects = 0;\n // Surface persist-failures (e.g. js-controller hiccup) instead of\n // swallowing them \u2014 the user otherwise sees \"new IP\" log but the\n // change is lost on next restart.\n this.saveDeviceToObject(conn.config).catch((err: unknown) =>\n this.log.debug(`Failed to persist new IP for ${conn.config.productName}: ${errText(err)}`),\n );\n\n // Cancel pending reconnect and connect immediately\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n this.connectWebSocket(conn);\n return;\n }\n });\n\n // Stop mDNS after timeout \u2014 WS reconnect continues with exponential\n // backoff. Don't log per-device warns here: the initial disconnect already\n // produced a `deviceUnreachable` warn via logDeviceError; spamming the\n // user hourly while the device stays offline adds zero information. If\n // someone needs to see retry cadence they can enable debug logging.\n this.ipRecoveryTimer = this.setTimeout(() => {\n this.ipRecoveryTimer = undefined;\n this.stopIpRecovery();\n\n for (const conn of this.connections.values()) {\n if (!conn.wsAuthenticated && conn.wsFailCount > 0) {\n this.log.debug(\n `${conn.config.productName}: device offline \u2014 will keep retrying every ${WS_RECONNECT_MAX_MS / 1000}s`,\n );\n }\n }\n }, IP_RECOVERY_TIMEOUT_MS);\n }\n\n /** Stop mDNS IP recovery */\n private stopIpRecovery(): void {\n if (this.ipRecoveryTimer) {\n this.clearTimeout(this.ipRecoveryTimer);\n this.ipRecoveryTimer = undefined;\n }\n if (this.discovery && !this.isPairing) {\n this.discovery.stop();\n this.discovery = null;\n }\n }\n\n /**\n * Initialize a newly discovered device \u2014 fetch info and connect WebSocket\n *\n * @param conn Device connection with IP set\n */\n private async initDevice(conn: DeviceConnection): Promise<void> {\n if (this.unloading || conn.removed) {\n return;\n }\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const info = await client.getDeviceInfo();\n if (this.unloading || conn.removed) {\n return;\n }\n const key = this.stateManager.devicePrefix(conn.config);\n await this.setStateAsync(`${key}.info.firmware`, {\n val: info.firmware_version,\n ack: true,\n });\n } catch (err) {\n if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"init\", err);\n }\n\n if (this.unloading || conn.removed) {\n return;\n }\n this.connectWebSocket(conn);\n void this.pollSystemInfo(conn);\n }\n\n /**\n * Connect WebSocket for a device\n *\n * @param conn Device connection\n */\n private connectWebSocket(conn: DeviceConnection): void {\n if (!conn.ip) {\n return; // No IP yet \u2014 wait for mDNS\n }\n\n // Stop reconnecting if auth keeps failing\n if (conn.authFailCount >= MAX_AUTH_FAILURES) {\n return;\n }\n\n // Mark as recovering so concurrent triggers (mDNS broadcast race,\n // overlapping reconnect timer) don't spawn a second wsClient.\n conn.recovering = true;\n\n // Close any existing wsClient before creating a new one. The normal\n // disconnect path nulls conn.wsClient, but IP-recovery jumps in directly\n // and would otherwise leak the old socket.\n if (conn.wsClient) {\n conn.wsClient.close();\n conn.wsClient = null;\n }\n\n // After repeated failures, try mDNS periodically to find a new IP\n if (shouldStartIpRecovery(conn.wsFailCount, WS_FAILURES_BEFORE_MDNS, MDNS_RETRY_EVERY)) {\n this.startIpRecovery();\n }\n\n const key = this.stateManager.devicePrefix(conn.config);\n\n const wsClient = new HomeWizardWebSocket(conn.ip, conn.config.token, {\n onMeasurement: (data: Measurement) => {\n // Skip updates for devices removed mid-flight (frame can race\n // delObjectAsync), and for adapter teardown.\n if (conn.removed || this.unloading) {\n return;\n }\n // Defensive .catch \u2014 Promise.all writes inside updateMeasurement may\n // reject on transient Redis hiccups; we want a debug-log, not an\n // unhandled rejection that bubbles to the process-level handler.\n this.stateManager.updateMeasurement(conn.config, data).catch((err: unknown) => {\n this.log.debug(`updateMeasurement failed for ${conn.config.productName}: ${errText(err)}`);\n });\n },\n onConnected: () => {\n conn.wsAuthenticated = true;\n conn.wsFailCount = 0;\n conn.authFailCount = 0;\n conn.lastConnectedAt = Date.now();\n conn.recovering = false;\n void this.stateManager.setDeviceConnected(conn.config, true);\n this.updateGlobalConnection();\n\n // Stop REST fallback if active\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n\n // Stop IP recovery if all devices are connected\n if (this.discovery && !this.isPairing) {\n const allConnected = Array.from(this.connections.values()).every(c => c.wsAuthenticated);\n if (allConnected) {\n this.stopIpRecovery();\n }\n }\n\n // Log restoration if we had errors before\n if (conn.lastErrorCode) {\n this.log.info(\n this.isUnstable(conn)\n ? `${conn.config.productName}: connection restored (unstable mode)`\n : `${conn.config.productName}: connection restored`,\n );\n conn.lastErrorCode = \"\";\n }\n\n this.log.debug(`WebSocket connected to ${conn.config.productName} (${conn.ip})`);\n },\n onDisconnected: (error?: Error) => {\n // Auth failures are not a connectivity-stability signal \u2014 they mean\n // the token is bad, not the WiFi. Counting them as short connections\n // would flip the device into unstable mode (faster reconnect spam,\n // misleading \"unstable\" log) on what is purely an auth issue.\n const isAuthError = error instanceof HomeWizardApiError && error.errorCode === \"user:unauthorized\";\n\n // Track connection stability \u2014 pure decision in main-helpers, side-effects here\n if (conn.lastConnectedAt > 0 && !isAuthError) {\n const duration = Date.now() - conn.lastConnectedAt;\n const transition = decideUnstableTransition(\n conn.recentDisconnects,\n duration,\n STABLE_THRESHOLD_MS,\n UNSTABLE_DISCONNECT_THRESHOLD,\n );\n if (duration < STABLE_THRESHOLD_MS) {\n conn.recentDisconnects++;\n } else {\n conn.recentDisconnects = 0;\n }\n if (transition === \"becameUnstable\") {\n this.log.info(`${conn.config.productName}: unstable connection detected \u2014 using faster reconnect`);\n } else if (transition === \"stabilized\") {\n this.log.info(`${conn.config.productName}: connection stabilized \u2014 using normal reconnect`);\n }\n }\n\n conn.wsAuthenticated = false;\n conn.wsClient = null;\n conn.recovering = false;\n void this.stateManager.setDeviceConnected(conn.config, false);\n this.updateGlobalConnection();\n\n if (error) {\n this.logDeviceError(conn, \"ws\", error);\n }\n\n // Check if this was an auth failure (returns false \u2192 stop reconnect path)\n if (!this.handleAuthFailure(conn, error, /* cleanupTimers */ false)) {\n return;\n }\n\n // Start REST fallback\n this.startRestFallback(conn);\n\n // Schedule reconnect with exponential backoff (faster for unstable devices)\n conn.wsFailCount++;\n const maxDelay = this.isUnstable(conn) ? WS_RECONNECT_MAX_UNSTABLE_MS : WS_RECONNECT_MAX_MS;\n const delay = computeReconnectDelay(conn.wsFailCount, WS_RECONNECT_BASE_MS, maxDelay);\n this.log.debug(`${key}: WS reconnect in ${delay / 1000}s (attempt ${conn.wsFailCount})`);\n\n conn.reconnectTimer = this.setTimeout(() => {\n conn.reconnectTimer = undefined;\n this.connectWebSocket(conn);\n }, delay);\n },\n log: this.log,\n });\n\n conn.wsClient = wsClient;\n wsClient.connect();\n }\n\n /**\n * Start REST polling as fallback when WebSocket is down.\n * For stable devices: stops on network errors (WS reconnect handles recovery).\n * For unstable devices: slows down instead of stopping to minimize data gaps.\n *\n * @param conn Device connection\n */\n private startRestFallback(conn: DeviceConnection): void {\n if (conn.pollTimer || !conn.ip) {\n return;\n }\n\n const unstable = this.isUnstable(conn);\n const interval = pickRestPollInterval(unstable, REST_POLL_MS, REST_POLL_UNSTABLE_MS);\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n\n conn.pollTimer = this.setInterval(async () => {\n // Bail out if device was removed or adapter is shutting down \u2014 the\n // setStateAsync chain inside updateMeasurement would otherwise either\n // recreate deleted objects or hit a torn-down adapter.\n if (conn.removed || this.unloading) {\n return;\n }\n try {\n const data = await client.getMeasurement();\n if (conn.removed || this.unloading) {\n return;\n }\n await this.stateManager.updateMeasurement(conn.config, data);\n } catch (err) {\n if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"rest\", err);\n\n // Auth failures: stop everything \u2014 token is bad, re-pair required.\n if (err instanceof HomeWizardApiError && err.errorCode === \"user:unauthorized\") {\n this.handleAuthFailure(conn, err, /* cleanupTimers */ true);\n return;\n }\n\n // Stop REST polling on network errors for stable devices.\n // Unstable devices keep polling (slower) to minimize data gaps.\n if (!unstable && classifyError(err) === \"NETWORK\" && conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n }\n }, interval);\n }\n\n /** Poll system info for all connected devices in parallel */\n private async pollAllSystemInfo(): Promise<void> {\n if (this.unloading) {\n return;\n }\n const tasks = Array.from(this.connections.values())\n .filter(c => c.ip && c.wsAuthenticated && !c.removed)\n .map(c => this.pollSystemInfo(c));\n await Promise.all(tasks);\n }\n\n /**\n * Poll system info for a single device\n *\n * @param conn Device connection\n */\n private async pollSystemInfo(conn: DeviceConnection): Promise<void> {\n if (!conn.ip || conn.removed || this.unloading) {\n return;\n }\n\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const system = await client.getSystem();\n if (conn.removed || this.unloading) {\n return;\n }\n await this.stateManager.updateSystem(conn.config, system);\n\n // Sync productName drift: if the user renamed the device in the\n // HomeWizard app (or a firmware update changed the product_name), pick\n // up the new value once per system-poll instead of staying stale until\n // re-pair. Cheap \u2014 only writes on actual change.\n try {\n const info = await client.getDeviceInfo();\n if (!conn.removed && !this.unloading && info.product_name && info.product_name !== conn.config.productName) {\n this.log.info(`${conn.config.productName}: name changed to '${info.product_name}' \u2014 updating object`);\n conn.config.productName = info.product_name;\n await this.saveDeviceToObject(conn.config);\n }\n } catch {\n // device-info is best-effort here; the system-poll log already\n // surfaces real connectivity issues.\n }\n\n // Also poll battery if device supports it. 404 = no battery \u2014 silent.\n // Other errors (500, timeout, malformed body) used to be swallowed\n // entirely; now they surface at debug so post-mortem diagnosis is\n // possible without losing any normal-flow logging.\n if (conn.removed || this.unloading) {\n return;\n }\n try {\n const battery = await client.getBatteries();\n if (conn.removed || this.unloading) {\n return;\n }\n // Only create battery states if batteries are actually connected\n if (battery.battery_count && battery.battery_count > 0) {\n await this.stateManager.updateBattery(conn.config, battery);\n }\n } catch (err) {\n if (err instanceof HomeWizardApiError && err.statusCode === 404) {\n return; // device doesn't support batteries \u2014 expected\n }\n this.log.debug(`${conn.config.productName} batteries: ${errText(err)}`);\n }\n } catch (err) {\n if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"system\", err);\n }\n }\n\n /** Update global info.connection based on all device states */\n private updateGlobalConnection(): void {\n const anyConnected = Array.from(this.connections.values()).some(c => c.wsAuthenticated);\n void this.setStateAsync(\"info.connection\", {\n val: anyConnected,\n ack: true,\n });\n }\n\n /**\n * Remove a device \u2014 disconnect, delete states and object\n *\n * @param stateId The remove state ID\n */\n private async removeDevice(stateId: string): Promise<void> {\n const conn = this.findConnectionForState(stateId);\n if (!conn) {\n return;\n }\n\n const key = this.stateManager.devicePrefix(conn.config);\n this.log.info(`Removing device ${conn.config.productName} (${conn.config.serial})`);\n\n // Mark as removed FIRST \u2014 async tasks (in-flight WS frames, REST polls,\n // outstanding pollSystemInfo) check this flag after each await and bail\n // out before recreating just-deleted objects via setStateAsync.\n conn.removed = true;\n\n // Disconnect\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n }\n this.connections.delete(key);\n\n // Delete device object and all states (no adapter restart!)\n await this.stateManager.removeDevice(conn.config);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Find connection for a state ID. Delegates to the pure helper so the\n * lookup math is unit-tested separately (`lib/main-helpers.test.ts`).\n *\n * @param stateId Full state ID\n */\n private findConnectionForState(stateId: string): DeviceConnection | undefined {\n return resolveConnectionForState(stateId, this.namespace, this.connections);\n }\n\n /**\n * Whether a device has unstable connectivity (frequent short-lived connections).\n * Unstable devices get faster reconnect and persistent REST fallback.\n *\n * @param conn Device connection\n */\n private isUnstable(conn: DeviceConnection): boolean {\n return conn.recentDisconnects >= UNSTABLE_DISCONNECT_THRESHOLD;\n }\n\n /**\n * Handle a possible auth failure on a device connection. Counts failures and,\n * once `MAX_AUTH_FAILURES` is reached, warns the user and (optionally) tears\n * down active timers and the WebSocket \u2014 stops bombarding the device with a\n * known-bad token.\n *\n * @param conn Device connection.\n * @param error The error from the failing call (any error type accepted).\n * @param cleanupTimers If `true`, clears poll/reconnect timers and closes the WS\n * on threshold reach. Used by REST-fallback paths where\n * the WS would otherwise keep retrying indefinitely. The\n * WS-disconnect path passes `false` because the caller\n * decides the next step itself.\n * @returns `true` if the caller should continue normal flow (no auth-stop),\n * `false` if the auth-stop fired and the caller should bail out.\n */\n private handleAuthFailure(conn: DeviceConnection, error: unknown, cleanupTimers: boolean): boolean {\n if (!(error instanceof HomeWizardApiError) || error.errorCode !== \"user:unauthorized\") {\n return true;\n }\n conn.authFailCount++;\n if (conn.authFailCount < MAX_AUTH_FAILURES) {\n return true;\n }\n this.log.warn(`${conn.config.productName}: token invalid \u2014 re-pair device to fix`);\n if (cleanupTimers) {\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n conn.wsClient?.close();\n }\n return false;\n }\n\n /**\n * Log device error with deduplication (based on error category, not context).\n * First occurrence of a new error category logs as warn, repeats as debug.\n *\n * @param conn Device connection\n * @param context Error context (for debug messages only)\n * @param err Error object\n */\n private logDeviceError(conn: DeviceConnection, context: string, err: unknown): void {\n const errorCode = classifyError(err);\n const isRepeat = errorCode === conn.lastErrorCode;\n conn.lastErrorCode = errorCode;\n\n if (isRepeat) {\n this.log.debug(`${conn.config.productName} ${context}: ${errText(err)}`);\n } else if (errorCode === \"NETWORK\") {\n this.log.warn(`${conn.config.productName}: device unreachable \u2014 will keep retrying`);\n } else {\n this.log.warn(`${conn.config.productName} ${context}: ${errText(err)}`);\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new HomeWizard(options);\n} else {\n (() => new HomeWizard())();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { errText, isValidIpv4, parseBatteryPermissions, validateBatteryMode } from \"./lib/coerce\";\nimport { classifyError, createDeviceConnection, UNSTABLE_DISCONNECT_THRESHOLD } from \"./lib/connection-utils\";\nimport { HomeWizardDiscovery } from \"./lib/discovery\";\nimport { HomeWizardApiError, HomeWizardClient } from \"./lib/homewizard-client\";\nimport {\n computeReconnectDelay,\n decideUnstableTransition,\n findConnectionForState as resolveConnectionForState,\n pickRestPollInterval,\n shouldEmitAfterCooldown,\n shouldStartIpRecovery,\n} from \"./lib/main-helpers\";\nimport { StateManager } from \"./lib/state-manager\";\nimport type { DeviceConfig, DeviceConnection, DiscoveredDevice, Measurement } from \"./lib/types\";\nimport { HomeWizardWebSocket } from \"./lib/websocket-client\";\n\n/** Pairing timeout in milliseconds (60 seconds) */\nconst PAIRING_TIMEOUT_MS = 60_000;\n/** Pairing poll interval in milliseconds */\nconst PAIRING_POLL_MS = 2_000;\n/** WebSocket reconnect base delay in milliseconds */\nconst WS_RECONNECT_BASE_MS = 5_000;\n/** Maximum WebSocket reconnect delay in milliseconds */\nconst WS_RECONNECT_MAX_MS = 300_000;\n/** REST fallback poll interval in milliseconds */\nconst REST_POLL_MS = 10_000;\n/** System info poll interval in milliseconds */\nconst SYSTEM_POLL_MS = 60_000;\n/** Max auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\n/** WS failures before starting mDNS IP recovery */\nconst WS_FAILURES_BEFORE_MDNS = 3;\n/** mDNS IP recovery timeout in milliseconds */\nconst IP_RECOVERY_TIMEOUT_MS = 60_000;\n/** Retry mDNS every N WS failures after first attempt (~1 hour at 5 min cap) */\nconst MDNS_RETRY_EVERY = 12;\n/** Connection must last this long to count as \"stable\" */\nconst STABLE_THRESHOLD_MS = 600_000;\n/** Max reconnect delay for unstable devices */\nconst WS_RECONNECT_MAX_UNSTABLE_MS = 60_000;\n/** REST fallback interval for unstable devices (slower, not stopped) */\nconst REST_POLL_UNSTABLE_MS = 30_000;\n/**\n * Cooldown window for `device unreachable` warns. Per-device, category-\n * spanning: bouncing hardware should produce max 1\u00D7 warn per window, regardless\n * of whether each cycle's failure was TIMEOUT, NETWORK, or HTTP_503. Survives\n * the lastErrorCode-reset on recovery so chronic bouncing doesn't flap warn /\n * debug at every cycle.\n */\nconst WARN_COOLDOWN_MS = 60 * 60 * 1000;\n/** Cooldown window for `connection restored` infos \u2014 analog to warn cooldown. */\nconst INFO_COOLDOWN_MS = 60 * 60 * 1000;\n\nclass HomeWizard extends utils.Adapter {\n private stateManager!: StateManager;\n private discovery: HomeWizardDiscovery | null = null;\n private readonly connections = new Map<string, DeviceConnection>();\n /**\n * Per-device last-warn timestamp for chronic-bouncing cooldown. Key =\n * `conn.config.serial` (kategorien\u00FCbergreifend). The classifyError-based\n * `lastErrorCode`-Dedup in {@link logDeviceError} resets on every recovery,\n * so on chronic bouncing a new disconnect counts as \"first occurrence\"\n * \u2192 wieder warn. This cooldown stamp persists across recoveries so the user\n * sees max one warn per WARN_COOLDOWN_MS per device.\n */\n private readonly lastWarnAt = new Map<string, number>();\n /** Per-device last-info timestamp for `connection restored`. Analog cooldown. */\n private readonly lastInfoAt = new Map<string, number>();\n private pairingTimer: ioBroker.Timeout | undefined = undefined;\n private pairingPollTimer: ioBroker.Interval | undefined = undefined;\n private systemPollTimer: ioBroker.Interval | undefined = undefined;\n private ipRecoveryTimer: ioBroker.Timeout | undefined = undefined;\n private isPairing = false;\n private pairingManualIp = \"\";\n private discoveredDuringPairing: DiscoveredDevice[] = [];\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n /** Set during onUnload \u2014 async paths bail before further setStateAsync calls. */\n private unloading = false;\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"homewizard\" });\n // Wrap async handlers with .catch() so a rejection can never become an\n // unhandled promise rejection (\u2192 SIGKILL \u2192 js-controller restart loop).\n this.on(\"ready\", () => {\n this.onReady().catch((err: unknown) => this.log.error(`onReady failed: ${errText(err)}`));\n });\n this.on(\"stateChange\", (id, state) => {\n this.onStateChange(id, state).catch((err: unknown) => this.log.error(`stateChange failed: ${errText(err)}`));\n });\n this.on(\"unload\", callback => this.onUnload(callback));\n\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths. The per-handler wrappers cover documented async\n // paths; this catches anything that slips past during refactors.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(`Unhandled rejection: ${errText(reason)}`);\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${err.message}`);\n };\n process.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n process.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n }\n\n /** Adapter started */\n private async onReady(): Promise<void> {\n this.stateManager = new StateManager(this);\n\n // `pairingIp` is declared in io-package.json instanceObjects \u2014 just reset state.\n // Reset pairing states on start (in case previous run was killed mid-pairing)\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\n // Subscribe to pairing button and writable device states\n await this.subscribeStatesAsync(\"startPairing\");\n await this.subscribeStatesAsync(\"*.system.reboot\");\n await this.subscribeStatesAsync(\"*.system.identify\");\n await this.subscribeStatesAsync(\"*.system.cloud_enabled\");\n await this.subscribeStatesAsync(\"*.system.status_led_brightness_pct\");\n await this.subscribeStatesAsync(\"*.system.api_v1_enabled\");\n await this.subscribeStatesAsync(\"*.battery.mode\");\n await this.subscribeStatesAsync(\"*.battery.permissions\");\n await this.subscribeStatesAsync(\"*.remove\");\n\n // Load devices from device objects (not from adapter config)\n const devices = await this.loadDevicesFromObjects();\n if (devices.length === 0) {\n this.log.info(`No devices configured \u2014 set 'startPairing' to true to add a device`);\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n }\n\n // Create connection entries for all configured devices\n for (const device of devices) {\n const key = this.stateManager.devicePrefix(device);\n await this.stateManager.cleanupMovedStates(device);\n await this.stateManager.createDeviceStates(device);\n const conn = createDeviceConnection(device, device.ip || \"\");\n this.connections.set(key, conn);\n\n // If we have a stored IP, connect immediately\n if (conn.ip) {\n this.log.debug(`Using stored IP ${conn.ip} for ${device.productName}`);\n void this.initDevice(conn);\n }\n }\n\n // Periodic system info poll\n this.systemPollTimer = this.setInterval(() => {\n void this.pollAllSystemInfo();\n }, SYSTEM_POLL_MS);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Load device configs from existing device objects\n * Tokens are stored encrypted in device object native\n */\n private async loadDevicesFromObjects(): Promise<DeviceConfig[]> {\n const devices: DeviceConfig[] = [];\n\n // Also migrate from old adapter config if devices exist there.\n // Defensive: native.devices could be a non-array if a previous version\n // wrote a different shape, or if the user edited it manually.\n const rawOldDevices = (this.config as Record<string, unknown>).devices;\n const oldDevices: DeviceConfig[] = Array.isArray(rawOldDevices) ? (rawOldDevices as DeviceConfig[]) : [];\n if (oldDevices.length > 0) {\n this.log.debug(`Migrating ${oldDevices.length} device(s) from adapter config to device objects`);\n for (const device of oldDevices) {\n await this.saveDeviceToObject(device);\n }\n // Clear old config (this triggers one restart, but only during migration)\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, {\n native: { devices: [] },\n });\n return oldDevices;\n }\n\n // Read device objects from our namespace. A corrupted encryptedToken\n // (e.g. after secret rotation, crypto-lib changes, manual DB edits) must\n // not take down the whole adapter \u2014 skip the broken device, keep the rest.\n const objects = await this.getAdapterObjectsAsync();\n for (const [id, obj] of Object.entries(objects)) {\n if (obj.type !== \"device\") {\n continue;\n }\n const native = obj.native as Record<string, string> | undefined;\n if (!native?.encryptedToken || !native.serial) {\n continue;\n }\n const localId = id.replace(`${this.namespace}.`, \"\");\n this.log.debug(`Loading device from object: ${localId}`);\n let token: string;\n try {\n token = this.decrypt(native.encryptedToken);\n } catch (err) {\n this.log.warn(\n `Cannot decrypt token for ${localId} \u2014 re-pair the device. ` +\n `(${errText(err)}). Other devices remain unaffected.`,\n );\n continue;\n }\n devices.push({\n token,\n productType: native.productType || \"unknown\",\n serial: native.serial,\n productName: native.productName || native.productType || \"unknown\",\n ...(native.ip ? { ip: native.ip } : {}),\n });\n }\n\n return devices;\n }\n\n /**\n * Save device config to its device object native (encrypted token)\n *\n * @param config Device configuration to save\n */\n private async saveDeviceToObject(config: DeviceConfig): Promise<void> {\n const prefix = this.stateManager.devicePrefix(config);\n const encryptedToken = this.encrypt(config.token);\n await this.extendObjectAsync(prefix, {\n type: \"device\",\n common: { name: config.productName || config.productType },\n native: {\n encryptedToken,\n productType: config.productType,\n serial: config.serial,\n productName: config.productName,\n ...(config.ip ? { ip: config.ip } : {}),\n },\n });\n }\n\n /**\n * Handle a discovered device from mDNS (only active during pairing)\n *\n * @param discovered Discovered device info\n */\n private onDeviceDiscovered(discovered: DiscoveredDevice): void {\n // Skip already paired devices\n const existing = Array.from(this.connections.values()).find(c => c.config.serial === discovered.serial);\n if (existing) {\n return;\n }\n\n // Skip duplicates\n if (this.discoveredDuringPairing.find(d => d.serial === discovered.serial)) {\n return;\n }\n\n this.discoveredDuringPairing.push(discovered);\n this.log.info(\n `Found ${discovered.name} (${discovered.productType}) at ${discovered.ip} \u2014 press the button on the device to pair`,\n );\n }\n\n /**\n * Adapter stopping \u2014 MUST be synchronous\n *\n * @param callback Completion callback\n */\n private onUnload(callback: () => void): void {\n // Set first, before any clearTimeout \u2014 in-flight async paths\n // (REST poll, getMeasurement, getSystem) check this after each await\n // and bail out before further setStateAsync on a tearing-down adapter.\n this.unloading = true;\n try {\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n }\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n }\n if (this.systemPollTimer) {\n this.clearInterval(this.systemPollTimer);\n }\n if (this.ipRecoveryTimer) {\n this.clearTimeout(this.ipRecoveryTimer);\n }\n\n this.discovery?.stop();\n\n for (const conn of this.connections.values()) {\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n }\n }\n this.connections.clear();\n\n // Detach process-level last-line-of-defence handlers\n if (this.unhandledRejectionHandler) {\n process.off(\"unhandledRejection\", this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off(\"uncaughtException\", this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n\n void this.setState(\"info.connection\", { val: false, ack: true });\n } finally {\n callback();\n }\n }\n\n /**\n * Handle state changes\n *\n * @param id State ID\n * @param state State value\n */\n private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n if (!state || state.ack || this.unloading) {\n return;\n }\n\n // Pairing button\n if (id.endsWith(\".startPairing\")) {\n if (state.val) {\n await this.startPairing();\n }\n return;\n }\n\n // Remove device button\n if (id.endsWith(\".remove\")) {\n if (state.val) {\n await this.removeDevice(id);\n }\n return;\n }\n\n // Find which device this state belongs to\n const conn = this.findConnectionForState(id);\n if (!conn || !conn.ip) {\n return;\n }\n\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n\n try {\n if (id.endsWith(\".system.reboot\")) {\n this.log.info(`Rebooting ${conn.config.productName} (${conn.ip})`);\n await client.reboot();\n } else if (id.endsWith(\".system.identify\")) {\n await client.identify();\n } else if (id.endsWith(\".system.cloud_enabled\")) {\n await client.setSystem({ cloud_enabled: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".system.status_led_brightness_pct\")) {\n await client.setSystem({\n status_led_brightness_pct: Number(state.val),\n });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".system.api_v1_enabled\")) {\n await client.setSystem({ api_v1_enabled: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.mode\")) {\n const mode = validateBatteryMode(String(state.val));\n if (!mode) {\n this.log.warn(`Invalid battery.mode value: '${String(state.val)}' \u2014 expected one of: zero, to_full, standby`);\n return;\n }\n await client.setBatteries({ mode });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.permissions\")) {\n const result = parseBatteryPermissions(String(state.val));\n if (!result.ok) {\n this.log.warn(\n `Invalid JSON for battery.permissions: ${result.reason} \u2014 expected array, got: ${result.sample}`,\n );\n return;\n }\n await client.setBatteries({ permissions: result.perms });\n await this.setStateAsync(id, { val: state.val, ack: true });\n }\n } catch (err) {\n this.log.warn(`Failed to set ${id}: ${errText(err)}`);\n }\n }\n\n /** Start pairing mode \u2014 discover devices and attempt to pair */\n private async startPairing(): Promise<void> {\n if (this.isPairing) {\n this.log.debug(\"Pairing already active\");\n return;\n }\n\n // Reset startPairing immediately so it doesn't survive a restart\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n\n this.isPairing = true;\n this.discoveredDuringPairing = [];\n\n // Stop IP recovery if running \u2014 pairing takes priority\n this.stopIpRecovery();\n\n // Check if manual IP is set, then clear pairingIp immediately\n const ipState = await this.getStateAsync(\"pairingIp\");\n this.pairingManualIp = ipState?.val ? String(ipState.val).trim() : \"\";\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\n if (this.pairingManualIp) {\n // Validate manual-IP up front \u2014 better to fail fast than wait 60s while\n // requestPairing keeps timing out against a malformed input.\n if (!isValidIpv4(this.pairingManualIp)) {\n this.log.warn(`Invalid pairing IP '${this.pairingManualIp}' \u2014 expected IPv4 (e.g. 192.168.1.42)`);\n this.isPairing = false;\n this.pairingManualIp = \"\";\n return;\n }\n this.log.info(\n `Pairing mode enabled for ${this.pairingManualIp} \u2014 press the button on your HomeWizard device now (60 seconds timeout)`,\n );\n // Add as discovered device immediately\n this.discoveredDuringPairing.push({\n ip: this.pairingManualIp,\n productType: \"unknown\",\n serial: \"unknown\",\n name: this.pairingManualIp,\n });\n } else {\n this.log.info(\n `Pairing mode enabled \u2014 searching for devices via mDNS, press the button on your HomeWizard device now (60 seconds timeout)`,\n );\n // Restart mDNS browser to trigger fresh query \u2014 already-cached devices\n // won't be re-announced otherwise and pairing would never find them\n if (!this.discovery) {\n this.discovery = new HomeWizardDiscovery(this.log);\n }\n this.discovery.start(discovered => {\n this.onDeviceDiscovered(discovered);\n });\n }\n\n // Poll discovered devices for pairing\n this.pairingPollTimer = this.setInterval(() => {\n void this.pollPairing();\n }, PAIRING_POLL_MS);\n\n // Timeout pairing\n this.pairingTimer = this.setTimeout(() => {\n this.stopPairing();\n this.log.info(`Pairing mode automatically disabled after 60 seconds timeout`);\n }, PAIRING_TIMEOUT_MS);\n }\n\n /** Poll all discovered devices to attempt pairing */\n private async pollPairing(): Promise<void> {\n for (const device of this.discoveredDuringPairing) {\n try {\n const client = new HomeWizardClient(device.ip);\n const result = await client.requestPairing();\n\n // Success! Button was pressed\n this.log.info(\n `Successfully paired with ${device.name} (${device.productType}) at ${device.ip} \u2014 connecting...`,\n );\n\n // Get device info\n const authedClient = new HomeWizardClient(device.ip, result.token);\n const info = await authedClient.getDeviceInfo();\n\n const deviceConfig: DeviceConfig = {\n token: result.token,\n productType: info.product_type,\n serial: info.serial,\n productName: info.product_name,\n ip: device.ip,\n };\n\n // Save to device object (no adapter restart!)\n await this.saveDeviceToObject(deviceConfig);\n await this.stateManager.createDeviceStates(deviceConfig);\n\n // Re-pair of an existing device (e.g. after factory reset): close the\n // old connection's wsClient + timers before overwriting the map entry,\n // otherwise the old WS keeps running as a zombie until restart.\n const key = this.stateManager.devicePrefix(deviceConfig);\n const previous = this.connections.get(key);\n if (previous) {\n this.log.debug(`Re-pair: closing previous connection for ${deviceConfig.productName}`);\n previous.wsClient?.close();\n if (previous.pollTimer) {\n this.clearInterval(previous.pollTimer);\n }\n if (previous.reconnectTimer) {\n this.clearTimeout(previous.reconnectTimer);\n }\n }\n\n // Create connection and connect\n const conn = createDeviceConnection(deviceConfig, device.ip);\n this.connections.set(key, conn);\n void this.initDevice(conn);\n\n // Remove from discovery list \u2014 but keep pairing window open so the\n // user can button-press additional devices in the same session.\n this.discoveredDuringPairing = this.discoveredDuringPairing.filter(d => d.serial !== info.serial);\n\n this.updateGlobalConnection();\n // Do NOT call stopPairing() here \u2014 pairingTimer (60 s) closes the\n // window naturally; meanwhile the user can pair more devices.\n continue;\n } catch (err) {\n // 403 = button not pressed yet \u2014 expected, keep polling\n if (err instanceof HomeWizardApiError && err.statusCode === 403) {\n continue;\n }\n this.log.debug(`Pairing poll error for ${device.ip}: ${errText(err)}`);\n }\n }\n }\n\n /** Stop pairing mode */\n private stopPairing(): void {\n this.isPairing = false;\n this.pairingManualIp = \"\";\n this.discoveredDuringPairing = [];\n\n // Stop mDNS \u2014 only needed during pairing\n if (this.discovery) {\n this.discovery.stop();\n this.discovery = null;\n }\n\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n this.pairingPollTimer = undefined;\n }\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n this.pairingTimer = undefined;\n }\n }\n\n /** Start mDNS to find devices that changed IP */\n private startIpRecovery(): void {\n // Don't start if already running or pairing\n if (this.discovery || this.isPairing) {\n return;\n }\n\n // Internal recovery \u2014 debug only. The initial disconnect already produced\n // one warn via logDeviceError; repeating that hourly while a device stays\n // offline is just spam.\n this.log.debug(`Device unreachable \u2014 searching for new IP via mDNS`);\n\n this.discovery = new HomeWizardDiscovery(this.log);\n this.discovery.start(discovered => {\n // Match against disconnected devices\n for (const conn of this.connections.values()) {\n if (conn.config.serial !== discovered.serial) {\n continue;\n }\n if (discovered.ip === conn.ip || conn.wsAuthenticated) {\n return; // Same IP or already connected\n }\n // Multiple mDNS broadcasts can arrive within one recovery window\n // (e.g. AP roam). Skip if a connect cycle is already in flight.\n if (conn.recovering) {\n return;\n }\n\n this.log.info(`${conn.config.productName}: found at new IP ${discovered.ip} (was ${conn.ip})`);\n\n // Update IP and persist \u2014 reset stability (new network conditions)\n conn.ip = discovered.ip;\n conn.config.ip = discovered.ip;\n conn.wsFailCount = 0;\n conn.recentDisconnects = 0;\n // Surface persist-failures (e.g. js-controller hiccup) instead of\n // swallowing them \u2014 the user otherwise sees \"new IP\" log but the\n // change is lost on next restart.\n this.saveDeviceToObject(conn.config).catch((err: unknown) =>\n this.log.debug(`Failed to persist new IP for ${conn.config.productName}: ${errText(err)}`),\n );\n\n // Cancel pending reconnect and connect immediately\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n this.connectWebSocket(conn);\n return;\n }\n });\n\n // Stop mDNS after timeout \u2014 WS reconnect continues with exponential\n // backoff. Don't log per-device warns here: the initial disconnect already\n // produced a `deviceUnreachable` warn via logDeviceError; spamming the\n // user hourly while the device stays offline adds zero information. If\n // someone needs to see retry cadence they can enable debug logging.\n this.ipRecoveryTimer = this.setTimeout(() => {\n this.ipRecoveryTimer = undefined;\n this.stopIpRecovery();\n\n for (const conn of this.connections.values()) {\n if (!conn.wsAuthenticated && conn.wsFailCount > 0) {\n this.log.debug(\n `${conn.config.productName}: device offline \u2014 will keep retrying every ${WS_RECONNECT_MAX_MS / 1000}s`,\n );\n }\n }\n }, IP_RECOVERY_TIMEOUT_MS);\n }\n\n /** Stop mDNS IP recovery */\n private stopIpRecovery(): void {\n if (this.ipRecoveryTimer) {\n this.clearTimeout(this.ipRecoveryTimer);\n this.ipRecoveryTimer = undefined;\n }\n if (this.discovery && !this.isPairing) {\n this.discovery.stop();\n this.discovery = null;\n }\n }\n\n /**\n * Initialize a newly discovered device \u2014 fetch info and connect WebSocket\n *\n * @param conn Device connection with IP set\n */\n private async initDevice(conn: DeviceConnection): Promise<void> {\n if (this.unloading || conn.removed) {\n return;\n }\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const info = await client.getDeviceInfo();\n if (this.unloading || conn.removed) {\n return;\n }\n const key = this.stateManager.devicePrefix(conn.config);\n await this.setStateAsync(`${key}.info.firmware`, {\n val: info.firmware_version,\n ack: true,\n });\n } catch (err) {\n if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"init\", err);\n }\n\n if (this.unloading || conn.removed) {\n return;\n }\n this.connectWebSocket(conn);\n void this.pollSystemInfo(conn);\n }\n\n /**\n * Connect WebSocket for a device\n *\n * @param conn Device connection\n */\n private connectWebSocket(conn: DeviceConnection): void {\n if (!conn.ip) {\n return; // No IP yet \u2014 wait for mDNS\n }\n\n // Stop reconnecting if auth keeps failing\n if (conn.authFailCount >= MAX_AUTH_FAILURES) {\n return;\n }\n\n // Mark as recovering so concurrent triggers (mDNS broadcast race,\n // overlapping reconnect timer) don't spawn a second wsClient.\n conn.recovering = true;\n\n // Close any existing wsClient before creating a new one. The normal\n // disconnect path nulls conn.wsClient, but IP-recovery jumps in directly\n // and would otherwise leak the old socket.\n if (conn.wsClient) {\n conn.wsClient.close();\n conn.wsClient = null;\n }\n\n // After repeated failures, try mDNS periodically to find a new IP\n if (shouldStartIpRecovery(conn.wsFailCount, WS_FAILURES_BEFORE_MDNS, MDNS_RETRY_EVERY)) {\n this.startIpRecovery();\n }\n\n const key = this.stateManager.devicePrefix(conn.config);\n\n const wsClient = new HomeWizardWebSocket(conn.ip, conn.config.token, {\n onMeasurement: (data: Measurement) => {\n // Skip updates for devices removed mid-flight (frame can race\n // delObjectAsync), and for adapter teardown.\n if (conn.removed || this.unloading) {\n return;\n }\n // Defensive .catch \u2014 Promise.all writes inside updateMeasurement may\n // reject on transient Redis hiccups; we want a debug-log, not an\n // unhandled rejection that bubbles to the process-level handler.\n this.stateManager.updateMeasurement(conn.config, data).catch((err: unknown) => {\n this.log.debug(`updateMeasurement failed for ${conn.config.productName}: ${errText(err)}`);\n });\n },\n onConnected: () => {\n conn.wsAuthenticated = true;\n conn.wsFailCount = 0;\n conn.authFailCount = 0;\n conn.lastConnectedAt = Date.now();\n conn.recovering = false;\n void this.stateManager.setDeviceConnected(conn.config, true);\n this.updateGlobalConnection();\n\n // Stop REST fallback if active\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n\n // Stop IP recovery if all devices are connected\n if (this.discovery && !this.isPairing) {\n const allConnected = Array.from(this.connections.values()).every(c => c.wsAuthenticated);\n if (allConnected) {\n this.stopIpRecovery();\n }\n }\n\n // Log restoration if we had errors before. Per-device cooldown\n // (analog to logDeviceError) so chronic bouncing doesn't emit one\n // info per cycle \u2014 bouncing hardware is one phenomenon and one\n // restoration-info per hour is enough. Repeats go to debug.\n if (conn.lastErrorCode) {\n const now = Date.now();\n const lastInfo = this.lastInfoAt.get(conn.config.serial) ?? 0;\n const msg = this.isUnstable(conn)\n ? `${conn.config.productName}: connection restored (unstable mode)`\n : `${conn.config.productName}: connection restored`;\n if (shouldEmitAfterCooldown(lastInfo, now, INFO_COOLDOWN_MS)) {\n this.lastInfoAt.set(conn.config.serial, now);\n this.log.info(msg);\n } else {\n this.log.debug(`${msg} (cooldown)`);\n }\n conn.lastErrorCode = \"\";\n }\n\n this.log.debug(`WebSocket connected to ${conn.config.productName} (${conn.ip})`);\n },\n onDisconnected: (error?: Error) => {\n // Auth failures are not a connectivity-stability signal \u2014 they mean\n // the token is bad, not the WiFi. Counting them as short connections\n // would flip the device into unstable mode (faster reconnect spam,\n // misleading \"unstable\" log) on what is purely an auth issue.\n const isAuthError = error instanceof HomeWizardApiError && error.errorCode === \"user:unauthorized\";\n\n // Track connection stability \u2014 pure decision in main-helpers, side-effects here\n if (conn.lastConnectedAt > 0 && !isAuthError) {\n const duration = Date.now() - conn.lastConnectedAt;\n const transition = decideUnstableTransition(\n conn.recentDisconnects,\n duration,\n STABLE_THRESHOLD_MS,\n UNSTABLE_DISCONNECT_THRESHOLD,\n );\n if (duration < STABLE_THRESHOLD_MS) {\n conn.recentDisconnects++;\n } else {\n conn.recentDisconnects = 0;\n }\n // Hysterese-transitions are internal reconnect-strategy adjustments,\n // not user-actionable events. Per the gesch\u00E4rfte mcm-Linie\n // (reference_iobroker_logging_levels HART-block): diagnostische\n // Prefixe / interne Hysterese-State geh\u00F6ren auf debug, nicht info.\n if (transition === \"becameUnstable\") {\n this.log.debug(`${conn.config.productName}: unstable connection detected \u2014 using faster reconnect`);\n } else if (transition === \"stabilized\") {\n this.log.debug(`${conn.config.productName}: connection stabilized \u2014 using normal reconnect`);\n }\n }\n\n conn.wsAuthenticated = false;\n conn.wsClient = null;\n conn.recovering = false;\n void this.stateManager.setDeviceConnected(conn.config, false);\n this.updateGlobalConnection();\n\n if (error) {\n this.logDeviceError(conn, \"ws\", error);\n }\n\n // Check if this was an auth failure (returns false \u2192 stop reconnect path)\n if (!this.handleAuthFailure(conn, error, /* cleanupTimers */ false)) {\n return;\n }\n\n // Start REST fallback\n this.startRestFallback(conn);\n\n // Schedule reconnect with exponential backoff (faster for unstable devices)\n conn.wsFailCount++;\n const maxDelay = this.isUnstable(conn) ? WS_RECONNECT_MAX_UNSTABLE_MS : WS_RECONNECT_MAX_MS;\n const delay = computeReconnectDelay(conn.wsFailCount, WS_RECONNECT_BASE_MS, maxDelay);\n this.log.debug(`${key}: WS reconnect in ${delay / 1000}s (attempt ${conn.wsFailCount})`);\n\n conn.reconnectTimer = this.setTimeout(() => {\n conn.reconnectTimer = undefined;\n this.connectWebSocket(conn);\n }, delay);\n },\n log: this.log,\n });\n\n conn.wsClient = wsClient;\n wsClient.connect();\n }\n\n /**\n * Start REST polling as fallback when WebSocket is down.\n * For stable devices: stops on network errors (WS reconnect handles recovery).\n * For unstable devices: slows down instead of stopping to minimize data gaps.\n *\n * @param conn Device connection\n */\n private startRestFallback(conn: DeviceConnection): void {\n if (conn.pollTimer || !conn.ip) {\n return;\n }\n\n const unstable = this.isUnstable(conn);\n const interval = pickRestPollInterval(unstable, REST_POLL_MS, REST_POLL_UNSTABLE_MS);\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n\n conn.pollTimer = this.setInterval(async () => {\n // Bail out if device was removed or adapter is shutting down \u2014 the\n // setStateAsync chain inside updateMeasurement would otherwise either\n // recreate deleted objects or hit a torn-down adapter.\n if (conn.removed || this.unloading) {\n return;\n }\n try {\n const data = await client.getMeasurement();\n if (conn.removed || this.unloading) {\n return;\n }\n await this.stateManager.updateMeasurement(conn.config, data);\n } catch (err) {\n if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"rest\", err);\n\n // Auth failures: stop everything \u2014 token is bad, re-pair required.\n if (err instanceof HomeWizardApiError && err.errorCode === \"user:unauthorized\") {\n this.handleAuthFailure(conn, err, /* cleanupTimers */ true);\n return;\n }\n\n // Stop REST polling on network errors for stable devices.\n // Unstable devices keep polling (slower) to minimize data gaps.\n if (!unstable && classifyError(err) === \"NETWORK\" && conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n }\n }, interval);\n }\n\n /** Poll system info for all connected devices in parallel */\n private async pollAllSystemInfo(): Promise<void> {\n if (this.unloading) {\n return;\n }\n const tasks = Array.from(this.connections.values())\n .filter(c => c.ip && c.wsAuthenticated && !c.removed)\n .map(c => this.pollSystemInfo(c));\n await Promise.all(tasks);\n }\n\n /**\n * Poll system info for a single device\n *\n * @param conn Device connection\n */\n private async pollSystemInfo(conn: DeviceConnection): Promise<void> {\n if (!conn.ip || conn.removed || this.unloading) {\n return;\n }\n\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const system = await client.getSystem();\n if (conn.removed || this.unloading) {\n return;\n }\n await this.stateManager.updateSystem(conn.config, system);\n\n // Sync productName drift: if the user renamed the device in the\n // HomeWizard app (or a firmware update changed the product_name), pick\n // up the new value once per system-poll instead of staying stale until\n // re-pair. Cheap \u2014 only writes on actual change.\n try {\n const info = await client.getDeviceInfo();\n if (!conn.removed && !this.unloading && info.product_name && info.product_name !== conn.config.productName) {\n this.log.info(`${conn.config.productName}: name changed to '${info.product_name}' \u2014 updating object`);\n conn.config.productName = info.product_name;\n await this.saveDeviceToObject(conn.config);\n }\n } catch {\n // device-info is best-effort here; the system-poll log already\n // surfaces real connectivity issues.\n }\n\n // Also poll battery if device supports it. 404 = no battery \u2014 silent.\n // Other errors (500, timeout, malformed body) used to be swallowed\n // entirely; now they surface at debug so post-mortem diagnosis is\n // possible without losing any normal-flow logging.\n if (conn.removed || this.unloading) {\n return;\n }\n try {\n const battery = await client.getBatteries();\n if (conn.removed || this.unloading) {\n return;\n }\n // Only create battery states if batteries are actually connected\n if (battery.battery_count && battery.battery_count > 0) {\n await this.stateManager.updateBattery(conn.config, battery);\n }\n } catch (err) {\n if (err instanceof HomeWizardApiError && err.statusCode === 404) {\n return; // device doesn't support batteries \u2014 expected\n }\n this.log.debug(`${conn.config.productName} batteries: ${errText(err)}`);\n }\n } catch (err) {\n if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"system\", err);\n }\n }\n\n /** Update global info.connection based on all device states */\n private updateGlobalConnection(): void {\n const anyConnected = Array.from(this.connections.values()).some(c => c.wsAuthenticated);\n void this.setStateAsync(\"info.connection\", {\n val: anyConnected,\n ack: true,\n });\n }\n\n /**\n * Remove a device \u2014 disconnect, delete states and object\n *\n * @param stateId The remove state ID\n */\n private async removeDevice(stateId: string): Promise<void> {\n const conn = this.findConnectionForState(stateId);\n if (!conn) {\n return;\n }\n\n const key = this.stateManager.devicePrefix(conn.config);\n this.log.info(`Removing device ${conn.config.productName} (${conn.config.serial})`);\n\n // Mark as removed FIRST \u2014 async tasks (in-flight WS frames, REST polls,\n // outstanding pollSystemInfo) check this flag after each await and bail\n // out before recreating just-deleted objects via setStateAsync.\n conn.removed = true;\n\n // Disconnect\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n }\n this.connections.delete(key);\n\n // Delete device object and all states (no adapter restart!)\n await this.stateManager.removeDevice(conn.config);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Find connection for a state ID. Delegates to the pure helper so the\n * lookup math is unit-tested separately (`lib/main-helpers.test.ts`).\n *\n * @param stateId Full state ID\n */\n private findConnectionForState(stateId: string): DeviceConnection | undefined {\n return resolveConnectionForState(stateId, this.namespace, this.connections);\n }\n\n /**\n * Whether a device has unstable connectivity (frequent short-lived connections).\n * Unstable devices get faster reconnect and persistent REST fallback.\n *\n * @param conn Device connection\n */\n private isUnstable(conn: DeviceConnection): boolean {\n return conn.recentDisconnects >= UNSTABLE_DISCONNECT_THRESHOLD;\n }\n\n /**\n * Handle a possible auth failure on a device connection. Counts failures and,\n * once `MAX_AUTH_FAILURES` is reached, warns the user and (optionally) tears\n * down active timers and the WebSocket \u2014 stops bombarding the device with a\n * known-bad token.\n *\n * @param conn Device connection.\n * @param error The error from the failing call (any error type accepted).\n * @param cleanupTimers If `true`, clears poll/reconnect timers and closes the WS\n * on threshold reach. Used by REST-fallback paths where\n * the WS would otherwise keep retrying indefinitely. The\n * WS-disconnect path passes `false` because the caller\n * decides the next step itself.\n * @returns `true` if the caller should continue normal flow (no auth-stop),\n * `false` if the auth-stop fired and the caller should bail out.\n */\n private handleAuthFailure(conn: DeviceConnection, error: unknown, cleanupTimers: boolean): boolean {\n if (!(error instanceof HomeWizardApiError) || error.errorCode !== \"user:unauthorized\") {\n return true;\n }\n conn.authFailCount++;\n if (conn.authFailCount < MAX_AUTH_FAILURES) {\n return true;\n }\n this.log.warn(`${conn.config.productName}: token invalid \u2014 re-pair device to fix`);\n if (cleanupTimers) {\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n conn.wsClient?.close();\n }\n return false;\n }\n\n /**\n * Log device error with deduplication.\n *\n * Two-stage dedup:\n * 1. `lastErrorCode` per connection \u2014 repeats of the same error category go\n * to debug. Resets on recovery (`onConnected` clears it) so a new category\n * after recovery surfaces as warn again. Designtechnisch correct for\n * \u201Enew failure mode\" but blind to chronic bouncing.\n * 2. {@link lastWarnAt} per device serial \u2014 survives recovery. Even if the\n * `lastErrorCode`-Dedup says \u201Efirst occurrence\", the cooldown stamp keeps\n * the warn-emit suppressed if we've warned for this device within\n * {@link WARN_COOLDOWN_MS}. Chronic bouncing produces at most 1\u00D7 warn per\n * hour per device.\n *\n * Cooldown key is the device serial \u2014 category-spanning. A flapping P1 that\n * cycles TIMEOUT\u2192NETWORK\u2192TIMEOUT is one phenomenon, one warn-budget.\n *\n * @param conn Device connection\n * @param context Error context (for debug messages only)\n * @param err Error object\n */\n private logDeviceError(conn: DeviceConnection, context: string, err: unknown): void {\n const errorCode = classifyError(err);\n const isRepeat = errorCode === conn.lastErrorCode;\n conn.lastErrorCode = errorCode;\n\n if (isRepeat) {\n this.log.debug(`${conn.config.productName} ${context}: ${errText(err)}`);\n return;\n }\n\n // New category \u2014 apply per-device cooldown so chronic bouncing doesn't\n // emit warn at every cycle just because each cycle's first failure is\n // a fresh `lastErrorCode`.\n const now = Date.now();\n const lastWarn = this.lastWarnAt.get(conn.config.serial) ?? 0;\n if (!shouldEmitAfterCooldown(lastWarn, now, WARN_COOLDOWN_MS)) {\n this.log.debug(`${conn.config.productName} ${context} (cooldown): ${errText(err)}`);\n return;\n }\n\n this.lastWarnAt.set(conn.config.serial, now);\n if (errorCode === \"NETWORK\") {\n this.log.warn(`${conn.config.productName}: device unreachable \u2014 will keep retrying`);\n } else {\n this.log.warn(`${conn.config.productName} ${context}: ${errText(err)}`);\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new HomeWizard(options);\n} else {\n (() => new HomeWizard())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["resolveConnectionForState"]
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "homewizard",
4
- "version": "0.7.6",
4
+ "version": "0.7.7",
5
5
  "news": {
6
+ "0.7.7": {
7
+ "en": "Devices with chronic WiFi drop-outs no longer flood the log — max 1 warn/h per device, full timeline at debug level. Internal mode-switches moved to debug.",
8
+ "de": "Geräte mit dauerhaft schlechtem WLAN spammen das Log nicht mehr — max 1 Warnung pro Stunde pro Gerät, voller Verlauf im Debug. Interne Modus-Wechsel auf Debug verschoben.",
9
+ "ru": "Устройства с постоянными WiFi-обрывами больше не засоряют лог — макс. 1 предупреждение/час на устройство, полный лог на debug. Внутренние смены режима теперь на debug.",
10
+ "pt": "Dispositivos com quedas constantes de WiFi já não enchem o log — máx. 1 aviso/hora por dispositivo, histórico completo em debug. Mudanças internas de modo movidas para debug.",
11
+ "nl": "Apparaten met chronisch slechte WiFi vullen het log niet meer — max 1 waarschuwing/uur per apparaat, volledige tijdlijn op debug. Interne modus-wisselingen naar debug.",
12
+ "fr": "Les appareils avec un WiFi chroniquement instable n'inondent plus le journal — max 1 avertissement/h par appareil, historique complet en debug. Les changements de mode internes passent en debug.",
13
+ "it": "I dispositivi con WiFi cronicamente instabile non riempiono più il log — max 1 avviso/ora per dispositivo, cronologia completa in debug. Cambi di modalità interni spostati su debug.",
14
+ "es": "Los dispositivos con WiFi crónicamente malo ya no inundan el log — máx. 1 aviso/h por dispositivo, historial completo en debug. Cambios de modo internos movidos a debug.",
15
+ "pl": "Urządzenia z chronicznie słabym WiFi nie zalewają już logu — maks. 1 ostrzeżenie/h na urządzenie, pełna oś czasu na poziomie debug. Wewnętrzne zmiany trybu przeniesione na debug.",
16
+ "uk": "Пристрої з хронічно поганим WiFi більше не заповнюють лог — максимум 1 попередження/год на пристрій, повний журнал на debug. Внутрішні зміни режиму перенесено на debug.",
17
+ "zh-cn": "WiFi 长期不稳定的设备不再淹没日志——每台设备每小时至多 1 条警告,完整时间线保留在 debug 级别。内部模式切换改到 debug。"
18
+ },
6
19
  "0.7.6": {
7
20
  "en": "The battery mode dropdown and the tariff state no longer crash the admin with \"Error in GUI\" when opened.",
8
21
  "de": "Das Battery-Mode-Dropdown und der Tariff-State stürzen beim Öffnen im Admin nicht mehr mit \"Error in GUI\" ab.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Wydajność: sprawdzanie istnienia datapointów jest cache'owane po pierwszym utworzeniu — oszczędza ~30 zapytań redis na sekundę przy P1 Meter. Jednostka sygnału WiFi to dBm.",
81
94
  "uk": "Продуктивність: перевірка існування датапоінтів кешується після першого створення — економить ~30 redis-запитів на секунду на P1 Meter. Одиниця сигналу WiFi — dBm.",
82
95
  "zh-cn": "性能:数据点存在检查在首次创建后会缓存——P1 Meter 每秒节省约 30 次 Redis 查询。WiFi 信号单位修正为 dBm。"
83
- },
84
- "0.7.0": {
85
- "en": "Multi-language: state names, descriptions, dropdown values (tariff, battery.mode) and user logs in your ioBroker system language (11 languages). Battery inputs validated. Min Node 22, Admin 7.8.23.",
86
- "de": "Mehrsprachig: Datenpunkt-Namen, -Beschreibungen, Dropdown-Werte (tariff, battery.mode) und User-Logs in der ioBroker-Systemsprache (11 Sprachen). Batterie-Eingaben werden geprüft. Min Node 22, Admin 7.8.23.",
87
- "ru": "Многоязычность: имена и описания датчиков, значения списков (tariff, battery.mode) и пользовательские логи в системном языке ioBroker (11 языков). Проверка ввода для батареи. Мин. Node 22, Admin 7.8.23.",
88
- "pt": "Multilíngue: nomes e descrições de datapoints, valores de dropdown (tariff, battery.mode) e logs no idioma do sistema ioBroker (11 idiomas). Validação de entradas de bateria. Mín. Node 22, Admin 7.8.23.",
89
- "nl": "Meertalig: datapoint-namen, beschrijvingen, dropdown-waarden (tariff, battery.mode) en gebruikerslogs in de ioBroker-systeemtaal (11 talen). Batterij-invoer wordt gecontroleerd. Min. Node 22, Admin 7.8.23.",
90
- "fr": "Multilingue : noms et descriptions des datapoints, valeurs déroulantes (tariff, battery.mode) et logs dans la langue système ioBroker (11 langues). Saisies batterie validées. Min Node 22, Admin 7.8.23.",
91
- "it": "Multilingua: nomi e descrizioni dei datapoint, valori dei menu (tariff, battery.mode) e log utente nella lingua di sistema ioBroker (11 lingue). Input batteria validati. Min Node 22, Admin 7.8.23.",
92
- "es": "Multilingüe: nombres y descripciones de datapoints, valores de menús (tariff, battery.mode) y logs en el idioma del sistema ioBroker (11 idiomas). Entradas de batería validadas. Mín. Node 22, Admin 7.8.23.",
93
- "pl": "Wiele języków: nazwy i opisy datapointów, wartości menu (tariff, battery.mode) i logi w języku systemu ioBroker (11 języków). Walidacja wprowadzania baterii. Min Node 22, Admin 7.8.23.",
94
- "uk": "Багатомовність: імена та описи датапоінтів, значення меню (tariff, battery.mode) та логи мовою системи ioBroker (11 мов). Перевірка вводу для батареї. Мін. Node 22, Admin 7.8.23.",
95
- "zh-cn": "多语言:数据点名称、描述、下拉值(tariff、battery.mode)和用户日志使用 ioBroker 系统语言(11 种)。电池输入会被校验。最低 Node 22、Admin 7.8.23。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.homewizard",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "ioBroker adapter for HomeWizard Energy devices with API v2",
5
5
  "author": {
6
6
  "name": "krobi",