iobroker.homewizard 0.7.4 → 0.7.5

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
@@ -155,8 +155,9 @@ homewizard.0.
155
155
  - Check that multicast/mDNS traffic is not blocked by your router/firewall
156
156
 
157
157
  ### WebSocket keeps disconnecting
158
- - Check `info.wifi_rssi_db` — anything above -75 dBm is fine, weaker than -85 dBm explains frequent drops
158
+ - Check `info.wifi_rssi_db` — above -75 dBm is comfortable, weaker than -85 dBm explains frequent drops
159
159
  - For devices with weak WiFi the adapter switches to a faster reconnect interval (60 s instead of 5 min) and keeps REST polling in the background so you don't lose data
160
+ - A WebSocket-layer ping/pong heartbeat (~30 s ping, 10 s pong window) catches half-dead links where the TCP stream is buffered but the device has stopped responding. Such links are torn down and reconnected automatically — you no longer end up with a stale "connected" status while measurement values stop updating.
160
161
  - IP changes are picked up via mDNS — no manual reconfiguration needed
161
162
 
162
163
  ### Token invalid after factory reset
@@ -169,6 +170,16 @@ homewizard.0.
169
170
  Placeholder for the next version (at the beginning of the line):
170
171
  ### **WORK IN PROGRESS**
171
172
  -->
173
+ ### 0.7.5 (2026-05-10)
174
+ - Half-dead WebSocket connections are now detected via WS-layer ping/pong (30 s ping, 10 s pong window) and torn down — fixes cases where the device stopped responding but the adapter still showed "connected" with stale measurement values.
175
+ - WebSocket auth handshake now has a 45 s timeout (was unbounded) — devices that accept the TCP connection but never reply to the auth protocol no longer hang forever.
176
+ - IP recovery, manual re-pair after factory reset, and parallel mDNS broadcasts no longer leak the previous WebSocket — reconnects are now race-free.
177
+ - Battery endpoint errors are no longer fully swallowed: 404 stays silent (device has no battery), other errors log at debug level so post-mortem diagnosis is possible.
178
+ - Manual pairing IP is validated as IPv4 up front — invalid input fails fast with a warn instead of a silent 60 s pairing timeout.
179
+ - A single corrupted device token can no longer take down the whole adapter — affected device is skipped with a re-pair hint, the others come up normally.
180
+ - Pairing supports multiple devices in one 60 s window: button-press additional devices and they are added one after the other instead of the session ending after the first.
181
+ - Internal robustness: parallel system polling for multi-device setups, productName drift sync after firmware updates, race protection on adapter unload, defensive guards on TXT records and adapter config shape.
182
+
172
183
  ### 0.7.4 (2026-05-09)
173
184
  - Adapter log messages are now English only, in line with the ioBroker community standard. Localized state names, descriptions and dropdown labels (11 languages) are unchanged.
174
185
 
@@ -182,12 +193,6 @@ homewizard.0.
182
193
  - WiFi signal strength is now reported in dBm (was incorrectly labelled `dB`).
183
194
  - Faster state updates: existence checks for datapoints are cached after first creation, saving ~30 Redis lookups per second on a P1 Meter pushing 1 measurement/second.
184
195
 
185
- ### 0.7.0 (2026-05-06)
186
- - Adapter texts now follow your ioBroker system language: datapoint names, descriptions, and dropdown values for `tariff` and `battery.mode` in 11 languages (EN, DE, RU, PT, NL, FR, IT, ES, PL, UK, ZH-CN).
187
- - Power-quality and Belgian capacity-tariff datapoints carry inline descriptions — hover in admin to see what each one means.
188
- - Battery inputs are checked up-front: an unknown `battery.mode` or malformed `battery.permissions` JSON gives a clear warning instead of a cryptic error.
189
- - Minimum requirements: Node.js 22 and ioBroker Admin 7.8.23.
190
-
191
196
  ### Support Development
192
197
 
193
198
  This adapter is free and open source. If you find it useful, consider buying me a coffee:
@@ -24,6 +24,7 @@ __export(coerce_exports, {
24
24
  coerceString: () => coerceString,
25
25
  errText: () => errText,
26
26
  isPlainObject: () => isPlainObject,
27
+ isValidIpv4: () => isValidIpv4,
27
28
  parseBatteryPermissions: () => parseBatteryPermissions,
28
29
  validateBatteryMode: () => validateBatteryMode
29
30
  });
@@ -54,6 +55,28 @@ function coerceBoolean(value) {
54
55
  function isPlainObject(value) {
55
56
  return typeof value === "object" && value !== null && !Array.isArray(value);
56
57
  }
58
+ function isValidIpv4(value) {
59
+ if (typeof value !== "string") {
60
+ return false;
61
+ }
62
+ const parts = value.split(".");
63
+ if (parts.length !== 4) {
64
+ return false;
65
+ }
66
+ for (const part of parts) {
67
+ if (!/^\d+$/.test(part)) {
68
+ return false;
69
+ }
70
+ const n = Number(part);
71
+ if (n < 0 || n > 255) {
72
+ return false;
73
+ }
74
+ if (part.length > 1 && part.startsWith("0")) {
75
+ return false;
76
+ }
77
+ }
78
+ return true;
79
+ }
57
80
  const BATTERY_MODES = ["zero", "to_full", "standby"];
58
81
  function validateBatteryMode(value) {
59
82
  return typeof value === "string" && BATTERY_MODES.includes(value) ? value : null;
@@ -111,6 +134,7 @@ function errText(err) {
111
134
  coerceString,
112
135
  errText,
113
136
  isPlainObject,
137
+ isValidIpv4,
114
138
  parseBatteryPermissions,
115
139
  validateBatteryMode
116
140
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/coerce.ts"],
4
- "sourcesContent": ["/**\n * Boundary coercion helpers for external API data (REST + WebSocket).\n * HomeWizard API v2 is well-documented but field types still drift in practice\n * (firmware bugs, future additions, null values). These helpers guard against\n * NaN/Infinity/non-string values reaching ioBroker states.\n */\n\n// Strict decimal regex \u2014 only optional minus sign + digits + optional fractional part.\n// Rejects HEX (`0x...`), exponential (`1e3`), Infinity, NaN, leading/trailing whitespace.\n// hassemu (E8 in v1.9.0) hardened the same coerce-helper this way; consistency item D8.\nconst DECIMAL_NUMBER_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Coerce to a finite number or null.\n * Accepts numbers directly; parses strict decimal strings; rejects NaN, Infinity,\n * HEX (`0x...`) and exponential notation (`1e3`).\n *\n * @param value Unknown external value\n */\nexport function coerceFiniteNumber(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n if (typeof value === \"string\" && DECIMAL_NUMBER_RE.test(value)) {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/**\n * Coerce to a non-empty string, or null.\n *\n * @param value Unknown external value\n */\nexport function coerceString(value: unknown): string | null {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n return null;\n}\n\n/**\n * Coerce to a boolean (only `true`/`false` accepted \u2014 no truthy/falsy JS rules).\n *\n * @param value Unknown external value\n */\nexport function coerceBoolean(value: unknown): boolean | null {\n if (typeof value === \"boolean\") {\n return value;\n }\n return null;\n}\n\n/**\n * Guard for plain objects (not arrays, not null).\n *\n * @param value Unknown external value\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Allowed values for `battery.mode` per HomeWizard API v2. */\nexport const BATTERY_MODES = [\"zero\", \"to_full\", \"standby\"] as const;\nexport type BatteryMode = (typeof BATTERY_MODES)[number];\n\n/**\n * Validate user input for `battery.mode` against the API-allowed enum. Returns\n * the typed mode on success, or `null` if the input is not in the whitelist.\n *\n * @param value Raw user input (`String(state.val)`).\n */\nexport function validateBatteryMode(value: unknown): BatteryMode | null {\n return typeof value === \"string\" && (BATTERY_MODES as readonly string[]).includes(value)\n ? (value as BatteryMode)\n : null;\n}\n\n/** Outcome of {@link parseBatteryPermissions} \u2014 either a parsed array or a diagnostic. */\nexport type BatteryPermissionsResult = { ok: true; perms: string[] } | { ok: false; reason: string; sample: string };\n\n/**\n * Parse a JSON string for `battery.permissions`. Expected shape: `string[]`.\n * Wraps `JSON.parse` so a malformed user input becomes a typed warning instead\n * of a thrown exception, and rejects non-array results explicitly.\n *\n * @param raw Raw user input (`String(state.val)`).\n */\nexport function parseBatteryPermissions(raw: string): BatteryPermissionsResult {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n return { ok: false, reason: errText(err), sample: raw.slice(0, 200) };\n }\n if (!Array.isArray(parsed)) {\n return { ok: false, reason: \"expected JSON array\", sample: raw.slice(0, 200) };\n }\n // permissions are documented as string array \u2014 coerce defensively.\n const perms: string[] = [];\n for (const item of parsed) {\n if (typeof item !== \"string\") {\n return {\n ok: false,\n reason: `non-string entry: ${typeof item}`,\n sample: raw.slice(0, 200),\n };\n }\n perms.push(item);\n }\n return { ok: true, perms };\n}\n\n/**\n * Extract a log-friendly message from a thrown / rejected value. Centralizes the\n * `err instanceof Error ? err.message : String(err)` pattern that otherwise\n * gets repeated at every catch-site. Plain objects are JSON-stringified so a\n * `[object Object]` log is avoided when adapters throw bag-of-fields.\n *\n * @param err Caught value of unknown shape (Error, string, undefined, ...).\n */\nexport function errText(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n if (err === null) {\n return \"null\";\n }\n if (err === undefined) {\n return \"undefined\";\n }\n if (typeof err === \"string\") {\n return err;\n }\n if (typeof err === \"number\" || typeof err === \"boolean\" || typeof err === \"bigint\") {\n return String(err);\n }\n // Plain objects + symbols would otherwise stringify to \"[object Object]\" / fail.\n // Prefer JSON for the common case so the log is at least diagnosable.\n try {\n return JSON.stringify(err);\n } catch {\n return Object.prototype.toString.call(err);\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,MAAM,oBAAoB;AASnB,SAAS,mBAAmB,OAA+B;AAChE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,EAC1C;AACA,MAAI,OAAO,UAAU,YAAY,kBAAkB,KAAK,KAAK,GAAG;AAC9D,UAAM,IAAI,OAAO,KAAK;AACtB,WAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAOO,SAAS,aAAa,OAA+B;AAC1D,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,cAAc,OAAgC;AAC5D,MAAI,OAAO,UAAU,WAAW;AAC9B,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,cAAc,OAAkD;AAC9E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAGO,MAAM,gBAAgB,CAAC,QAAQ,WAAW,SAAS;AASnD,SAAS,oBAAoB,OAAoC;AACtE,SAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK,IAClF,QACD;AACN;AAYO,SAAS,wBAAwB,KAAuC;AAC7E,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,GAAG;AAAA,EACzB,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,QAAQ,QAAQ,GAAG,GAAG,QAAQ,IAAI,MAAM,GAAG,GAAG,EAAE;AAAA,EACtE;AACA,MAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,WAAO,EAAE,IAAI,OAAO,QAAQ,uBAAuB,QAAQ,IAAI,MAAM,GAAG,GAAG,EAAE;AAAA,EAC/E;AAEA,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,QAAQ;AACzB,QAAI,OAAO,SAAS,UAAU;AAC5B,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,qBAAqB,OAAO,IAAI;AAAA,QACxC,QAAQ,IAAI,MAAM,GAAG,GAAG;AAAA,MAC1B;AAAA,IACF;AACA,UAAM,KAAK,IAAI;AAAA,EACjB;AACA,SAAO,EAAE,IAAI,MAAM,MAAM;AAC3B;AAUO,SAAS,QAAQ,KAAsB;AAC5C,MAAI,eAAe,OAAO;AACxB,WAAO,IAAI;AAAA,EACb;AACA,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,aAAa,OAAO,QAAQ,UAAU;AAClF,WAAO,OAAO,GAAG;AAAA,EACnB;AAGA,MAAI;AACF,WAAO,KAAK,UAAU,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO,OAAO,UAAU,SAAS,KAAK,GAAG;AAAA,EAC3C;AACF;",
4
+ "sourcesContent": ["/**\n * Boundary coercion helpers for external API data (REST + WebSocket).\n * HomeWizard API v2 is well-documented but field types still drift in practice\n * (firmware bugs, future additions, null values). These helpers guard against\n * NaN/Infinity/non-string values reaching ioBroker states.\n */\n\n// Strict decimal regex \u2014 only optional minus sign + digits + optional fractional part.\n// Rejects HEX (`0x...`), exponential (`1e3`), Infinity, NaN, leading/trailing whitespace.\n// hassemu (E8 in v1.9.0) hardened the same coerce-helper this way; consistency item D8.\nconst DECIMAL_NUMBER_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Coerce to a finite number or null.\n * Accepts numbers directly; parses strict decimal strings; rejects NaN, Infinity,\n * HEX (`0x...`) and exponential notation (`1e3`).\n *\n * @param value Unknown external value\n */\nexport function coerceFiniteNumber(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n if (typeof value === \"string\" && DECIMAL_NUMBER_RE.test(value)) {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/**\n * Coerce to a non-empty string, or null.\n *\n * @param value Unknown external value\n */\nexport function coerceString(value: unknown): string | null {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n return null;\n}\n\n/**\n * Coerce to a boolean (only `true`/`false` accepted \u2014 no truthy/falsy JS rules).\n *\n * @param value Unknown external value\n */\nexport function coerceBoolean(value: unknown): boolean | null {\n if (typeof value === \"boolean\") {\n return value;\n }\n return null;\n}\n\n/**\n * Guard for plain objects (not arrays, not null).\n *\n * @param value Unknown external value\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Validate that a string is an IPv4 address (octets 0-255, exactly 4 parts).\n * Used to fail manual-pairing input fast instead of waiting on a 60s timeout.\n *\n * @param value Raw user input.\n */\nexport function isValidIpv4(value: unknown): boolean {\n if (typeof value !== \"string\") {\n return false;\n }\n const parts = value.split(\".\");\n if (parts.length !== 4) {\n return false;\n }\n for (const part of parts) {\n if (!/^\\d+$/.test(part)) {\n return false;\n }\n const n = Number(part);\n if (n < 0 || n > 255) {\n return false;\n }\n // Reject leading zeros: \"01\" / \"001\" \u2014 ambiguous, may be parsed as octal elsewhere.\n if (part.length > 1 && part.startsWith(\"0\")) {\n return false;\n }\n }\n return true;\n}\n\n/** Allowed values for `battery.mode` per HomeWizard API v2. */\nexport const BATTERY_MODES = [\"zero\", \"to_full\", \"standby\"] as const;\nexport type BatteryMode = (typeof BATTERY_MODES)[number];\n\n/**\n * Validate user input for `battery.mode` against the API-allowed enum. Returns\n * the typed mode on success, or `null` if the input is not in the whitelist.\n *\n * @param value Raw user input (`String(state.val)`).\n */\nexport function validateBatteryMode(value: unknown): BatteryMode | null {\n return typeof value === \"string\" && (BATTERY_MODES as readonly string[]).includes(value)\n ? (value as BatteryMode)\n : null;\n}\n\n/** Outcome of {@link parseBatteryPermissions} \u2014 either a parsed array or a diagnostic. */\nexport type BatteryPermissionsResult = { ok: true; perms: string[] } | { ok: false; reason: string; sample: string };\n\n/**\n * Parse a JSON string for `battery.permissions`. Expected shape: `string[]`.\n * Wraps `JSON.parse` so a malformed user input becomes a typed warning instead\n * of a thrown exception, and rejects non-array results explicitly.\n *\n * @param raw Raw user input (`String(state.val)`).\n */\nexport function parseBatteryPermissions(raw: string): BatteryPermissionsResult {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n return { ok: false, reason: errText(err), sample: raw.slice(0, 200) };\n }\n if (!Array.isArray(parsed)) {\n return { ok: false, reason: \"expected JSON array\", sample: raw.slice(0, 200) };\n }\n // permissions are documented as string array \u2014 coerce defensively.\n const perms: string[] = [];\n for (const item of parsed) {\n if (typeof item !== \"string\") {\n return {\n ok: false,\n reason: `non-string entry: ${typeof item}`,\n sample: raw.slice(0, 200),\n };\n }\n perms.push(item);\n }\n return { ok: true, perms };\n}\n\n/**\n * Extract a log-friendly message from a thrown / rejected value. Centralizes the\n * `err instanceof Error ? err.message : String(err)` pattern that otherwise\n * gets repeated at every catch-site. Plain objects are JSON-stringified so a\n * `[object Object]` log is avoided when adapters throw bag-of-fields.\n *\n * @param err Caught value of unknown shape (Error, string, undefined, ...).\n */\nexport function errText(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n if (err === null) {\n return \"null\";\n }\n if (err === undefined) {\n return \"undefined\";\n }\n if (typeof err === \"string\") {\n return err;\n }\n if (typeof err === \"number\" || typeof err === \"boolean\" || typeof err === \"bigint\") {\n return String(err);\n }\n // Plain objects + symbols would otherwise stringify to \"[object Object]\" / fail.\n // Prefer JSON for the common case so the log is at least diagnosable.\n try {\n return JSON.stringify(err);\n } catch {\n return Object.prototype.toString.call(err);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -37,7 +37,9 @@ function createDeviceConnection(config, ip) {
37
37
  authFailCount: 0,
38
38
  lastErrorCode: "",
39
39
  lastConnectedAt: 0,
40
- recentDisconnects: 0
40
+ recentDisconnects: 0,
41
+ recovering: false,
42
+ removed: false
41
43
  };
42
44
  }
43
45
  function classifyError(err) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/connection-utils.ts"],
4
- "sourcesContent": ["import { HomeWizardApiError } from \"./homewizard-client\";\nimport type { DeviceConfig, DeviceConnection } from \"./types\";\n\n/** After this many short-lived connections, switch to unstable mode */\nexport const UNSTABLE_DISCONNECT_THRESHOLD = 3;\n\n/**\n * Create a fresh DeviceConnection with default values.\n *\n * @param config device configuration\n * @param ip device IP address\n */\nexport function createDeviceConnection(config: DeviceConfig, ip: string): DeviceConnection {\n return {\n config,\n ip,\n wsClient: null,\n wsAuthenticated: false,\n pollTimer: undefined,\n reconnectTimer: undefined,\n wsFailCount: 0,\n authFailCount: 0,\n lastErrorCode: \"\",\n lastConnectedAt: 0,\n recentDisconnects: 0,\n };\n}\n\n/**\n * Classify an error for deduplication and log-level decisions.\n * Returns a stable category string regardless of error message details.\n *\n * @param err the error to classify\n */\nexport function classifyError(err: unknown): string {\n if (err instanceof HomeWizardApiError) {\n if (err.errorCode === \"user:unauthorized\") {\n return \"AUTH\";\n }\n return `HTTP_${err.statusCode}`;\n }\n if (err instanceof Error) {\n const code = (err as NodeJS.ErrnoException).code;\n if (\n code === \"ECONNREFUSED\" ||\n code === \"EHOSTUNREACH\" ||\n code === \"ENOTFOUND\" ||\n code === \"ECONNRESET\" ||\n code === \"ENETUNREACH\" ||\n code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (code === \"ETIMEDOUT\" || err.message.includes(\"Timeout\")) {\n return \"TIMEOUT\";\n }\n return code || \"UNKNOWN\";\n }\n return \"UNKNOWN\";\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+BAAmC;AAI5B,MAAM,gCAAgC;AAQtC,SAAS,uBAAuB,QAAsB,IAA8B;AACzF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,iBAAiB;AAAA,IACjB,WAAW;AAAA,IACX,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,eAAe;AAAA,IACf,eAAe;AAAA,IACf,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,EACrB;AACF;AAQO,SAAS,cAAc,KAAsB;AAClD,MAAI,eAAe,6CAAoB;AACrC,QAAI,IAAI,cAAc,qBAAqB;AACzC,aAAO;AAAA,IACT;AACA,WAAO,QAAQ,IAAI,UAAU;AAAA,EAC/B;AACA,MAAI,eAAe,OAAO;AACxB,UAAM,OAAQ,IAA8B;AAC5C,QACE,SAAS,kBACT,SAAS,kBACT,SAAS,eACT,SAAS,gBACT,SAAS,iBACT,SAAS,aACT;AACA,aAAO;AAAA,IACT;AACA,QAAI,SAAS,eAAe,IAAI,QAAQ,SAAS,SAAS,GAAG;AAC3D,aAAO;AAAA,IACT;AACA,WAAO,QAAQ;AAAA,EACjB;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["import { HomeWizardApiError } from \"./homewizard-client\";\nimport type { DeviceConfig, DeviceConnection } from \"./types\";\n\n/** After this many short-lived connections, switch to unstable mode */\nexport const UNSTABLE_DISCONNECT_THRESHOLD = 3;\n\n/**\n * Create a fresh DeviceConnection with default values.\n *\n * @param config device configuration\n * @param ip device IP address\n */\nexport function createDeviceConnection(config: DeviceConfig, ip: string): DeviceConnection {\n return {\n config,\n ip,\n wsClient: null,\n wsAuthenticated: false,\n pollTimer: undefined,\n reconnectTimer: undefined,\n wsFailCount: 0,\n authFailCount: 0,\n lastErrorCode: \"\",\n lastConnectedAt: 0,\n recentDisconnects: 0,\n recovering: false,\n removed: false,\n };\n}\n\n/**\n * Classify an error for deduplication and log-level decisions.\n * Returns a stable category string regardless of error message details.\n *\n * @param err the error to classify\n */\nexport function classifyError(err: unknown): string {\n if (err instanceof HomeWizardApiError) {\n if (err.errorCode === \"user:unauthorized\") {\n return \"AUTH\";\n }\n return `HTTP_${err.statusCode}`;\n }\n if (err instanceof Error) {\n const code = (err as NodeJS.ErrnoException).code;\n if (\n code === \"ECONNREFUSED\" ||\n code === \"EHOSTUNREACH\" ||\n code === \"ENOTFOUND\" ||\n code === \"ECONNRESET\" ||\n code === \"ENETUNREACH\" ||\n code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (code === \"ETIMEDOUT\" || err.message.includes(\"Timeout\")) {\n return \"TIMEOUT\";\n }\n return code || \"UNKNOWN\";\n }\n return \"UNKNOWN\";\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+BAAmC;AAI5B,MAAM,gCAAgC;AAQtC,SAAS,uBAAuB,QAAsB,IAA8B;AACzF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,iBAAiB;AAAA,IACjB,WAAW;AAAA,IACX,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,eAAe;AAAA,IACf,eAAe;AAAA,IACf,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,IACnB,YAAY;AAAA,IACZ,SAAS;AAAA,EACX;AACF;AAQO,SAAS,cAAc,KAAsB;AAClD,MAAI,eAAe,6CAAoB;AACrC,QAAI,IAAI,cAAc,qBAAqB;AACzC,aAAO;AAAA,IACT;AACA,WAAO,QAAQ,IAAI,UAAU;AAAA,EAC/B;AACA,MAAI,eAAe,OAAO;AACxB,UAAM,OAAQ,IAA8B;AAC5C,QACE,SAAS,kBACT,SAAS,kBACT,SAAS,eACT,SAAS,gBACT,SAAS,iBACT,SAAS,aACT;AACA,aAAO;AAAA,IACT;AACA,QAAI,SAAS,eAAe,IAAI,QAAQ,SAAS,SAAS,GAAG;AAC3D,aAAO;AAAA,IACT;AACA,WAAO,QAAQ;AAAA,EACjB;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -28,10 +28,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var discovery_exports = {};
30
30
  __export(discovery_exports, {
31
- HomeWizardDiscovery: () => HomeWizardDiscovery
31
+ HomeWizardDiscovery: () => HomeWizardDiscovery,
32
+ coerceTxtValue: () => coerceTxtValue
32
33
  });
33
34
  module.exports = __toCommonJS(discovery_exports);
34
35
  var import_bonjour_service = __toESM(require("bonjour-service"));
36
+ function coerceTxtValue(value) {
37
+ if (typeof value === "string" && value.length > 0) {
38
+ return value;
39
+ }
40
+ if (Buffer.isBuffer(value)) {
41
+ const decoded = value.toString("utf8");
42
+ return decoded.length > 0 ? decoded : void 0;
43
+ }
44
+ return void 0;
45
+ }
35
46
  class HomeWizardDiscovery {
36
47
  bonjour = null;
37
48
  browser = null;
@@ -78,17 +89,17 @@ class HomeWizardDiscovery {
78
89
  * @param service Bonjour service record
79
90
  */
80
91
  parseService(service) {
81
- var _a, _b, _c, _d, _e, _f;
92
+ var _a, _b, _c, _d, _e, _f, _g;
82
93
  const ip = (_a = service.addresses) == null ? void 0 : _a.find((addr) => addr.includes("."));
83
94
  if (!ip) {
84
95
  this.log.debug(`mDNS: no IPv4 address for ${service.name}`);
85
96
  return null;
86
97
  }
87
- const txt = service.txt;
88
- const productType = (_b = txt == null ? void 0 : txt.product_type) != null ? _b : "unknown";
89
- const serial = (_d = (_c = txt == null ? void 0 : txt.serial) != null ? _c : service.name) != null ? _d : "unknown";
90
- const name = (_f = (_e = txt == null ? void 0 : txt.product_name) != null ? _e : service.name) != null ? _f : productType;
91
- const apiVersion = txt == null ? void 0 : txt.api_version;
98
+ const txt = (_b = service.txt) != null ? _b : {};
99
+ const productType = (_c = coerceTxtValue(txt.product_type)) != null ? _c : "unknown";
100
+ const serial = (_e = (_d = coerceTxtValue(txt.serial)) != null ? _d : service.name) != null ? _e : "unknown";
101
+ const name = (_g = (_f = coerceTxtValue(txt.product_name)) != null ? _f : service.name) != null ? _g : productType;
102
+ const apiVersion = coerceTxtValue(txt.api_version);
92
103
  if (apiVersion) {
93
104
  this.log.debug(`mDNS: TXT api_version=${apiVersion} serial=${serial}`);
94
105
  }
@@ -97,6 +108,7 @@ class HomeWizardDiscovery {
97
108
  }
98
109
  // Annotate the CommonJS export names for ESM import in node:
99
110
  0 && (module.exports = {
100
- HomeWizardDiscovery
111
+ HomeWizardDiscovery,
112
+ coerceTxtValue
101
113
  });
102
114
  //# sourceMappingURL=discovery.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/discovery.ts"],
4
- "sourcesContent": ["import Bonjour, { type Service } from \"bonjour-service\";\nimport type { DiscoveredDevice } from \"./types\";\n\n/** Callback for discovered devices */\nexport type DiscoveryCallback = (device: DiscoveredDevice) => void;\n\n/**\n * mDNS discovery for HomeWizard Energy devices.\n * Browses for `_hwenergy._tcp` services on the local network.\n */\nexport class HomeWizardDiscovery {\n private bonjour: Bonjour | null = null;\n private browser: ReturnType<Bonjour[\"find\"]> | null = null;\n private readonly log: {\n debug: (msg: string) => void;\n warn: (msg: string) => void;\n };\n\n /**\n * @param log Logger interface\n * @param log.debug Debug log function\n * @param log.warn Warning log function\n */\n constructor(log: { debug: (msg: string) => void; warn: (msg: string) => void }) {\n this.log = log;\n }\n\n /**\n * Start scanning for HomeWizard devices\n *\n * @param callback Called for each discovered device\n */\n start(callback: DiscoveryCallback): void {\n this.stop();\n\n this.bonjour = new Bonjour();\n this.log.debug(\"mDNS: browsing for _homewizard._tcp (v2)\");\n\n this.browser = this.bonjour.find({ type: \"homewizard\", protocol: \"tcp\" }, (service: Service) => {\n const device = this.parseService(service);\n if (device) {\n this.log.debug(`mDNS: found ${device.name} (${device.productType}) at ${device.ip}`);\n callback(device);\n }\n });\n }\n\n /** Stop scanning */\n stop(): void {\n if (this.browser) {\n this.browser.stop();\n this.browser = null;\n }\n if (this.bonjour) {\n this.bonjour.destroy();\n this.bonjour = null;\n }\n }\n\n /**\n * Parse a Bonjour service into a DiscoveredDevice\n *\n * @param service Bonjour service record\n */\n private parseService(service: Service): DiscoveredDevice | null {\n // IPv4 address\n const ip = service.addresses?.find(addr => addr.includes(\".\"));\n if (!ip) {\n this.log.debug(`mDNS: no IPv4 address for ${service.name}`);\n return null;\n }\n\n // TXT records contain product_type, serial, etc.\n const txt = service.txt as Record<string, string> | undefined;\n const productType = txt?.product_type ?? \"unknown\";\n const serial = txt?.serial ?? service.name ?? \"unknown\";\n const name = txt?.product_name ?? service.name ?? productType;\n const apiVersion = txt?.api_version;\n\n if (apiVersion) {\n this.log.debug(`mDNS: TXT api_version=${apiVersion} serial=${serial}`);\n }\n\n return { ip, productType, serial, name };\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAsC;AAU/B,MAAM,oBAAoB;AAAA,EACvB,UAA0B;AAAA,EAC1B,UAA8C;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUjB,YAAY,KAAoE;AAC9E,SAAK,MAAM;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAAmC;AACvC,SAAK,KAAK;AAEV,SAAK,UAAU,IAAI,uBAAAA,QAAQ;AAC3B,SAAK,IAAI,MAAM,0CAA0C;AAEzD,SAAK,UAAU,KAAK,QAAQ,KAAK,EAAE,MAAM,cAAc,UAAU,MAAM,GAAG,CAAC,YAAqB;AAC9F,YAAM,SAAS,KAAK,aAAa,OAAO;AACxC,UAAI,QAAQ;AACV,aAAK,IAAI,MAAM,eAAe,OAAO,IAAI,KAAK,OAAO,WAAW,QAAQ,OAAO,EAAE,EAAE;AACnF,iBAAS,MAAM;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,OAAa;AACX,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,KAAK;AAClB,WAAK,UAAU;AAAA,IACjB;AACA,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,QAAQ;AACrB,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,aAAa,SAA2C;AAhElE;AAkEI,UAAM,MAAK,aAAQ,cAAR,mBAAmB,KAAK,UAAQ,KAAK,SAAS,GAAG;AAC5D,QAAI,CAAC,IAAI;AACP,WAAK,IAAI,MAAM,6BAA6B,QAAQ,IAAI,EAAE;AAC1D,aAAO;AAAA,IACT;AAGA,UAAM,MAAM,QAAQ;AACpB,UAAM,eAAc,gCAAK,iBAAL,YAAqB;AACzC,UAAM,UAAS,sCAAK,WAAL,YAAe,QAAQ,SAAvB,YAA+B;AAC9C,UAAM,QAAO,sCAAK,iBAAL,YAAqB,QAAQ,SAA7B,YAAqC;AAClD,UAAM,aAAa,2BAAK;AAExB,QAAI,YAAY;AACd,WAAK,IAAI,MAAM,yBAAyB,UAAU,WAAW,MAAM,EAAE;AAAA,IACvE;AAEA,WAAO,EAAE,IAAI,aAAa,QAAQ,KAAK;AAAA,EACzC;AACF;",
4
+ "sourcesContent": ["import Bonjour, { type Service } from \"bonjour-service\";\nimport type { DiscoveredDevice } from \"./types\";\n\n/**\n * Coerce a raw Bonjour TXT-record value to a string. The library returns\n * either string, Buffer, or undefined depending on encoding \u2014 we normalize\n * here so downstream code sees one shape. Exported for unit-tests; the\n * production path uses it via {@link HomeWizardDiscovery#parseService}.\n *\n * @param value Raw TXT-record value.\n */\nexport function coerceTxtValue(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n if (Buffer.isBuffer(value)) {\n const decoded = value.toString(\"utf8\");\n return decoded.length > 0 ? decoded : undefined;\n }\n return undefined;\n}\n\n/** Callback for discovered devices */\nexport type DiscoveryCallback = (device: DiscoveredDevice) => void;\n\n/**\n * mDNS discovery for HomeWizard Energy devices.\n * Browses for `_hwenergy._tcp` services on the local network.\n */\nexport class HomeWizardDiscovery {\n private bonjour: Bonjour | null = null;\n private browser: ReturnType<Bonjour[\"find\"]> | null = null;\n private readonly log: {\n debug: (msg: string) => void;\n warn: (msg: string) => void;\n };\n\n /**\n * @param log Logger interface\n * @param log.debug Debug log function\n * @param log.warn Warning log function\n */\n constructor(log: { debug: (msg: string) => void; warn: (msg: string) => void }) {\n this.log = log;\n }\n\n /**\n * Start scanning for HomeWizard devices\n *\n * @param callback Called for each discovered device\n */\n start(callback: DiscoveryCallback): void {\n this.stop();\n\n this.bonjour = new Bonjour();\n this.log.debug(\"mDNS: browsing for _homewizard._tcp (v2)\");\n\n this.browser = this.bonjour.find({ type: \"homewizard\", protocol: \"tcp\" }, (service: Service) => {\n const device = this.parseService(service);\n if (device) {\n this.log.debug(`mDNS: found ${device.name} (${device.productType}) at ${device.ip}`);\n callback(device);\n }\n });\n }\n\n /** Stop scanning */\n stop(): void {\n if (this.browser) {\n this.browser.stop();\n this.browser = null;\n }\n if (this.bonjour) {\n this.bonjour.destroy();\n this.bonjour = null;\n }\n }\n\n /**\n * Parse a Bonjour service into a DiscoveredDevice\n *\n * @param service Bonjour service record\n */\n private parseService(service: Service): DiscoveredDevice | null {\n // IPv4 address\n const ip = service.addresses?.find(addr => addr.includes(\".\"));\n if (!ip) {\n this.log.debug(`mDNS: no IPv4 address for ${service.name}`);\n return null;\n }\n\n // TXT records contain product_type, serial, etc. Library may hand us\n // strings or Buffers \u2014 coerce defensively before use.\n const txt = (service.txt ?? {}) as Record<string, unknown>;\n const productType = coerceTxtValue(txt.product_type) ?? \"unknown\";\n const serial = coerceTxtValue(txt.serial) ?? service.name ?? \"unknown\";\n const name = coerceTxtValue(txt.product_name) ?? service.name ?? productType;\n const apiVersion = coerceTxtValue(txt.api_version);\n\n if (apiVersion) {\n this.log.debug(`mDNS: TXT api_version=${apiVersion} serial=${serial}`);\n }\n\n return { ip, productType, serial, name };\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAsC;AAW/B,SAAS,eAAe,OAAoC;AACjE,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,WAAO;AAAA,EACT;AACA,MAAI,OAAO,SAAS,KAAK,GAAG;AAC1B,UAAM,UAAU,MAAM,SAAS,MAAM;AACrC,WAAO,QAAQ,SAAS,IAAI,UAAU;AAAA,EACxC;AACA,SAAO;AACT;AASO,MAAM,oBAAoB;AAAA,EACvB,UAA0B;AAAA,EAC1B,UAA8C;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUjB,YAAY,KAAoE;AAC9E,SAAK,MAAM;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAAmC;AACvC,SAAK,KAAK;AAEV,SAAK,UAAU,IAAI,uBAAAA,QAAQ;AAC3B,SAAK,IAAI,MAAM,0CAA0C;AAEzD,SAAK,UAAU,KAAK,QAAQ,KAAK,EAAE,MAAM,cAAc,UAAU,MAAM,GAAG,CAAC,YAAqB;AAC9F,YAAM,SAAS,KAAK,aAAa,OAAO;AACxC,UAAI,QAAQ;AACV,aAAK,IAAI,MAAM,eAAe,OAAO,IAAI,KAAK,OAAO,WAAW,QAAQ,OAAO,EAAE,EAAE;AACnF,iBAAS,MAAM;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,OAAa;AACX,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,KAAK;AAClB,WAAK,UAAU;AAAA,IACjB;AACA,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,QAAQ;AACrB,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,aAAa,SAA2C;AAnFlE;AAqFI,UAAM,MAAK,aAAQ,cAAR,mBAAmB,KAAK,UAAQ,KAAK,SAAS,GAAG;AAC5D,QAAI,CAAC,IAAI;AACP,WAAK,IAAI,MAAM,6BAA6B,QAAQ,IAAI,EAAE;AAC1D,aAAO;AAAA,IACT;AAIA,UAAM,OAAO,aAAQ,QAAR,YAAe,CAAC;AAC7B,UAAM,eAAc,oBAAe,IAAI,YAAY,MAA/B,YAAoC;AACxD,UAAM,UAAS,0BAAe,IAAI,MAAM,MAAzB,YAA8B,QAAQ,SAAtC,YAA8C;AAC7D,UAAM,QAAO,0BAAe,IAAI,YAAY,MAA/B,YAAoC,QAAQ,SAA5C,YAAoD;AACjE,UAAM,aAAa,eAAe,IAAI,WAAW;AAEjD,QAAI,YAAY;AACd,WAAK,IAAI,MAAM,yBAAyB,UAAU,WAAW,MAAM,EAAE;AAAA,IACvE;AAEA,WAAO,EAAE,IAAI,aAAa,QAAQ,KAAK;AAAA,EACzC;AACF;",
6
6
  "names": ["Bonjour"]
7
7
  }
@@ -60,9 +60,13 @@ class HomeWizardClient {
60
60
  }
61
61
  /** Request pairing token (POST /api/user) — 403 until button pressed */
62
62
  async requestPairing() {
63
- return this.request("POST", "/api/user", {
63
+ const result = await this.request("POST", "/api/user", {
64
64
  name: "local/iobroker"
65
65
  });
66
+ if (!result || typeof result.token !== "string" || result.token.length === 0) {
67
+ throw new HomeWizardApiError(200, JSON.stringify(result), "POST /api/user (no token in response)");
68
+ }
69
+ return result;
66
70
  }
67
71
  /** Get current measurement (REST fallback) */
68
72
  async getMeasurement() {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/homewizard-client.ts"],
4
- "sourcesContent": ["import * as https from \"node:https\";\nimport { HW_AGENT } from \"./cacert\";\nimport type { BatteryControl, DeviceInfo, Measurement, PairingResponse, SystemInfo } from \"./types\";\n\n/** HTTPS client for HomeWizard API v2 */\nexport class HomeWizardClient {\n private readonly ip: string;\n private readonly token: string;\n private readonly agent: https.Agent;\n /** Override target port \u2014 only used by tests against a local stub-server. */\n private readonly port: number;\n\n /**\n * @param ip Device IP address\n * @param token Bearer token (empty string for pairing requests)\n * @param options Optional overrides \u2014 primarily for unit tests against a local TLS stub.\n * @param options.agent HTTPS agent to use; defaults to {@link HW_AGENT} (with HomeWizard CA pinning).\n * @param options.port Target port; defaults to 443.\n */\n constructor(ip: string, token: string = \"\", options: { agent?: https.Agent; port?: number } = {}) {\n this.ip = ip;\n this.token = token;\n this.agent = options.agent ?? HW_AGENT;\n this.port = options.port ?? 443;\n }\n\n /** Get device info (GET /api) */\n async getDeviceInfo(): Promise<DeviceInfo> {\n return this.request<DeviceInfo>(\"GET\", \"/api\");\n }\n\n /** Request pairing token (POST /api/user) \u2014 403 until button pressed */\n async requestPairing(): Promise<PairingResponse> {\n return this.request<PairingResponse>(\"POST\", \"/api/user\", {\n name: \"local/iobroker\",\n });\n }\n\n /** Get current measurement (REST fallback) */\n async getMeasurement(): Promise<Measurement> {\n return this.request<Measurement>(\"GET\", \"/api/measurement\");\n }\n\n /** Get system info */\n async getSystem(): Promise<SystemInfo> {\n return this.request<SystemInfo>(\"GET\", \"/api/system\");\n }\n\n /**\n * Update system settings\n *\n * @param settings System settings to update\n */\n async setSystem(settings: Partial<SystemInfo>): Promise<SystemInfo> {\n return this.request<SystemInfo>(\"PUT\", \"/api/system\", settings);\n }\n\n /** Reboot device */\n async reboot(): Promise<void> {\n await this.request(\"PUT\", \"/api/system/reboot\");\n }\n\n /** Identify device (blink LED) */\n async identify(): Promise<void> {\n await this.request(\"PUT\", \"/api/system/identify\");\n }\n\n /** Get battery control status */\n async getBatteries(): Promise<BatteryControl> {\n return this.request<BatteryControl>(\"GET\", \"/api/batteries\");\n }\n\n /**\n * Set battery control\n *\n * @param settings Battery control settings to update\n */\n async setBatteries(settings: Partial<BatteryControl>): Promise<BatteryControl> {\n return this.request<BatteryControl>(\"PUT\", \"/api/batteries\", settings);\n }\n\n /**\n * @param method HTTP method\n * @param path API path\n * @param body Optional request body\n */\n private request<T>(method: string, path: string, body?: unknown): Promise<T> {\n return new Promise((resolve, reject) => {\n const bodyStr = body ? JSON.stringify(body) : undefined;\n const headers: Record<string, string> = {\n \"X-Api-Version\": \"2\",\n };\n\n if (this.token) {\n headers.Authorization = `Bearer ${this.token}`;\n }\n if (bodyStr) {\n headers[\"Content-Type\"] = \"application/json\";\n headers[\"Content-Length\"] = Buffer.byteLength(bodyStr).toString();\n }\n\n const req = https.request(\n {\n hostname: this.ip,\n port: this.port,\n path,\n method,\n headers,\n agent: this.agent,\n timeout: 10_000,\n },\n res => {\n const chunks: Buffer[] = [];\n res.on(\"error\", reject);\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"end\", () => {\n const data = Buffer.concat(chunks).toString();\n if (!res.statusCode || res.statusCode >= 400) {\n const error = new HomeWizardApiError(res.statusCode ?? 0, data, `${method} ${path}`);\n reject(error);\n return;\n }\n if (!data) {\n resolve(undefined as T);\n return;\n }\n try {\n resolve(JSON.parse(data) as T);\n } catch {\n reject(new Error(`Invalid JSON from ${method} ${path}: ${data.substring(0, 200)}`));\n }\n });\n },\n );\n\n req.on(\"error\", reject);\n req.on(\"timeout\", () => {\n req.destroy(new Error(`Timeout: ${method} ${path}`));\n });\n\n if (bodyStr) {\n req.write(bodyStr);\n }\n req.end();\n });\n }\n}\n\n/** API error with status code and parsed error body */\nexport class HomeWizardApiError extends Error {\n readonly statusCode: number;\n readonly errorCode: string;\n\n /**\n * @param statusCode HTTP status code\n * @param body Response body\n * @param context Request context for error message\n */\n constructor(statusCode: number, body: string, context: string) {\n let errorCode = \"unknown\";\n let description = body;\n try {\n const parsed = JSON.parse(body);\n errorCode = parsed.error?.code ?? parsed.error ?? \"unknown\";\n description = parsed.error?.description ?? parsed.error?.code ?? body;\n } catch {\n // body is not JSON\n }\n super(`${context}: HTTP ${statusCode} \u2014 ${description}`);\n this.name = \"HomeWizardApiError\";\n this.statusCode = statusCode;\n this.errorCode = errorCode;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as https from \"node:https\";\nimport { HW_AGENT } from \"./cacert\";\nimport type { BatteryControl, DeviceInfo, Measurement, PairingResponse, SystemInfo } from \"./types\";\n\n/** HTTPS client for HomeWizard API v2 */\nexport class HomeWizardClient {\n private readonly ip: string;\n private readonly token: string;\n private readonly agent: https.Agent;\n /** Override target port \u2014 only used by tests against a local stub-server. */\n private readonly port: number;\n\n /**\n * @param ip Device IP address\n * @param token Bearer token (empty string for pairing requests)\n * @param options Optional overrides \u2014 primarily for unit tests against a local TLS stub.\n * @param options.agent HTTPS agent to use; defaults to {@link HW_AGENT} (with HomeWizard CA pinning).\n * @param options.port Target port; defaults to 443.\n */\n constructor(ip: string, token: string = \"\", options: { agent?: https.Agent; port?: number } = {}) {\n this.ip = ip;\n this.token = token;\n this.agent = options.agent ?? HW_AGENT;\n this.port = options.port ?? 443;\n }\n\n /** Get device info (GET /api) */\n async getDeviceInfo(): Promise<DeviceInfo> {\n return this.request<DeviceInfo>(\"GET\", \"/api\");\n }\n\n /** Request pairing token (POST /api/user) \u2014 403 until button pressed */\n async requestPairing(): Promise<PairingResponse> {\n const result = await this.request<PairingResponse>(\"POST\", \"/api/user\", {\n name: \"local/iobroker\",\n });\n // Server returned 200 but we still validate the shape \u2014 a malformed or\n // missing token would otherwise crash later in this.encrypt(undefined).\n if (!result || typeof result.token !== \"string\" || result.token.length === 0) {\n throw new HomeWizardApiError(200, JSON.stringify(result), \"POST /api/user (no token in response)\");\n }\n return result;\n }\n\n /** Get current measurement (REST fallback) */\n async getMeasurement(): Promise<Measurement> {\n return this.request<Measurement>(\"GET\", \"/api/measurement\");\n }\n\n /** Get system info */\n async getSystem(): Promise<SystemInfo> {\n return this.request<SystemInfo>(\"GET\", \"/api/system\");\n }\n\n /**\n * Update system settings\n *\n * @param settings System settings to update\n */\n async setSystem(settings: Partial<SystemInfo>): Promise<SystemInfo> {\n return this.request<SystemInfo>(\"PUT\", \"/api/system\", settings);\n }\n\n /** Reboot device */\n async reboot(): Promise<void> {\n await this.request(\"PUT\", \"/api/system/reboot\");\n }\n\n /** Identify device (blink LED) */\n async identify(): Promise<void> {\n await this.request(\"PUT\", \"/api/system/identify\");\n }\n\n /** Get battery control status */\n async getBatteries(): Promise<BatteryControl> {\n return this.request<BatteryControl>(\"GET\", \"/api/batteries\");\n }\n\n /**\n * Set battery control\n *\n * @param settings Battery control settings to update\n */\n async setBatteries(settings: Partial<BatteryControl>): Promise<BatteryControl> {\n return this.request<BatteryControl>(\"PUT\", \"/api/batteries\", settings);\n }\n\n /**\n * @param method HTTP method\n * @param path API path\n * @param body Optional request body\n */\n private request<T>(method: string, path: string, body?: unknown): Promise<T> {\n return new Promise((resolve, reject) => {\n const bodyStr = body ? JSON.stringify(body) : undefined;\n const headers: Record<string, string> = {\n \"X-Api-Version\": \"2\",\n };\n\n if (this.token) {\n headers.Authorization = `Bearer ${this.token}`;\n }\n if (bodyStr) {\n headers[\"Content-Type\"] = \"application/json\";\n headers[\"Content-Length\"] = Buffer.byteLength(bodyStr).toString();\n }\n\n const req = https.request(\n {\n hostname: this.ip,\n port: this.port,\n path,\n method,\n headers,\n agent: this.agent,\n timeout: 10_000,\n },\n res => {\n const chunks: Buffer[] = [];\n res.on(\"error\", reject);\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"end\", () => {\n const data = Buffer.concat(chunks).toString();\n if (!res.statusCode || res.statusCode >= 400) {\n const error = new HomeWizardApiError(res.statusCode ?? 0, data, `${method} ${path}`);\n reject(error);\n return;\n }\n if (!data) {\n resolve(undefined as T);\n return;\n }\n try {\n resolve(JSON.parse(data) as T);\n } catch {\n reject(new Error(`Invalid JSON from ${method} ${path}: ${data.substring(0, 200)}`));\n }\n });\n },\n );\n\n req.on(\"error\", reject);\n req.on(\"timeout\", () => {\n req.destroy(new Error(`Timeout: ${method} ${path}`));\n });\n\n if (bodyStr) {\n req.write(bodyStr);\n }\n req.end();\n });\n }\n}\n\n/** API error with status code and parsed error body */\nexport class HomeWizardApiError extends Error {\n readonly statusCode: number;\n readonly errorCode: string;\n\n /**\n * @param statusCode HTTP status code\n * @param body Response body\n * @param context Request context for error message\n */\n constructor(statusCode: number, body: string, context: string) {\n let errorCode = \"unknown\";\n let description = body;\n try {\n const parsed = JSON.parse(body);\n errorCode = parsed.error?.code ?? parsed.error ?? \"unknown\";\n description = parsed.error?.description ?? parsed.error?.code ?? body;\n } catch {\n // body is not JSON\n }\n super(`${context}: HTTP ${statusCode} \u2014 ${description}`);\n this.name = \"HomeWizardApiError\";\n this.statusCode = statusCode;\n this.errorCode = errorCode;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/types.ts"],
4
- "sourcesContent": ["import type { HomeWizardWebSocket } from \"./websocket-client\";\n\n/** Persisted config for a single paired device (stored in device object native) */\nexport interface DeviceConfig {\n /** Bearer token (encrypted via adapter.encrypt) */\n token: string;\n /** Product type (e.g. HWE-P1) */\n productType: string;\n /** Device serial number */\n serial: string;\n /** Human-readable product name */\n productName: string;\n /** Fixed IP address (only when manually set, empty = mDNS) */\n ip?: string;\n}\n\n/** Response from GET /api */\nexport interface DeviceInfo {\n /** Product name */\n product_name: string;\n /** Product type identifier */\n product_type: string;\n /** Device serial number */\n serial: string;\n /** Firmware version string */\n firmware_version: string;\n /** API version string */\n api_version: string;\n}\n\n/** Response from POST /api/user (pairing) */\nexport interface PairingResponse {\n /** Bearer token for API access */\n token: string;\n}\n\n/** Measurement data from GET /api/measurement or WebSocket push */\nexport interface Measurement {\n /** Unique meter identifier */\n unique_id?: string;\n /** Protocol version */\n protocol_version?: number;\n /** Meter model */\n meter_model?: string;\n /** Measurement timestamp */\n timestamp?: string;\n /** Active tariff number */\n tariff?: number;\n\n /** Total energy import in kWh */\n energy_import_kwh?: number;\n /** Energy import tariff 1 */\n energy_import_t1_kwh?: number;\n /** Energy import tariff 2 */\n energy_import_t2_kwh?: number;\n /** Energy import tariff 3 */\n energy_import_t3_kwh?: number;\n /** Energy import tariff 4 */\n energy_import_t4_kwh?: number;\n /** Total energy export in kWh */\n energy_export_kwh?: number;\n /** Energy export tariff 1 */\n energy_export_t1_kwh?: number;\n /** Energy export tariff 2 */\n energy_export_t2_kwh?: number;\n /** Energy export tariff 3 */\n energy_export_t3_kwh?: number;\n /** Energy export tariff 4 */\n energy_export_t4_kwh?: number;\n\n /** Total active power in W */\n power_w?: number;\n /** Active power phase 1 */\n power_l1_w?: number;\n /** Active power phase 2 */\n power_l2_w?: number;\n /** Active power phase 3 */\n power_l3_w?: number;\n\n /** Voltage (single phase) */\n voltage_v?: number;\n /** Voltage phase 1 */\n voltage_l1_v?: number;\n /** Voltage phase 2 */\n voltage_l2_v?: number;\n /** Voltage phase 3 */\n voltage_l3_v?: number;\n\n /** Current (single phase) */\n current_a?: number;\n /** Current phase 1 */\n current_l1_a?: number;\n /** Current phase 2 */\n current_l2_a?: number;\n /** Current phase 3 */\n current_l3_a?: number;\n\n /** Grid frequency in Hz */\n frequency_hz?: number;\n\n /** Voltage sag count phase 1 */\n voltage_sag_l1_count?: number;\n /** Voltage sag count phase 2 */\n voltage_sag_l2_count?: number;\n /** Voltage sag count phase 3 */\n voltage_sag_l3_count?: number;\n /** Voltage swell count phase 1 */\n voltage_swell_l1_count?: number;\n /** Voltage swell count phase 2 */\n voltage_swell_l2_count?: number;\n /** Voltage swell count phase 3 */\n voltage_swell_l3_count?: number;\n /** Any power fail count */\n any_power_fail_count?: number;\n /** Long power fail count */\n long_power_fail_count?: number;\n\n /** Average power over 15 min (Belgium) */\n average_power_15m_w?: number;\n /** Monthly power peak (Belgium) */\n monthly_power_peak_w?: number;\n /** Monthly power peak timestamp (Belgium) */\n monthly_power_peak_timestamp?: string;\n\n /** Apparent current */\n apparent_current_a?: number;\n /** Reactive current */\n reactive_current_a?: number;\n /** Apparent power in VA */\n apparent_power_va?: number;\n /** Reactive power in var */\n reactive_power_var?: number;\n /** Power factor */\n power_factor?: number;\n /** Apparent current phase 1 */\n apparent_current_l1_a?: number;\n /** Apparent current phase 2 */\n apparent_current_l2_a?: number;\n /** Apparent current phase 3 */\n apparent_current_l3_a?: number;\n /** Reactive current phase 1 */\n reactive_current_l1_a?: number;\n /** Reactive current phase 2 */\n reactive_current_l2_a?: number;\n /** Reactive current phase 3 */\n reactive_current_l3_a?: number;\n /** Apparent power phase 1 */\n apparent_power_l1_va?: number;\n /** Apparent power phase 2 */\n apparent_power_l2_va?: number;\n /** Apparent power phase 3 */\n apparent_power_l3_va?: number;\n /** Reactive power phase 1 */\n reactive_power_l1_var?: number;\n /** Reactive power phase 2 */\n reactive_power_l2_var?: number;\n /** Reactive power phase 3 */\n reactive_power_l3_var?: number;\n /** Power factor phase 1 */\n power_factor_l1?: number;\n /** Power factor phase 2 */\n power_factor_l2?: number;\n /** Power factor phase 3 */\n power_factor_l3?: number;\n\n /** Battery state of charge in percent */\n state_of_charge_pct?: number;\n /** Battery charge cycles */\n cycles?: number;\n\n /** External meters (gas, water, heat) */\n external?: ExternalMeter[];\n}\n\n/** External meter attached to P1 (gas, water, heat) */\nexport interface ExternalMeter {\n /** Unique meter identifier */\n unique_id: string;\n /** Meter type */\n type: \"gas_meter\" | \"heat_meter\" | \"warm_water_meter\" | \"water_meter\" | \"inlet_heat_meter\";\n /** Last reading timestamp */\n timestamp: string;\n /** Meter reading value */\n value: number;\n /** Measurement unit */\n unit: string;\n}\n\n/** System info from GET /api/system */\nexport interface SystemInfo {\n /** WiFi SSID */\n wifi_ssid: string;\n /** WiFi signal strength in dB */\n wifi_rssi_db: number;\n /** Uptime in seconds */\n uptime_s: number;\n /** Cloud communication enabled */\n cloud_enabled: boolean;\n /** Status LED brightness 0-100% */\n status_led_brightness_pct: number;\n /** Legacy API v1 enabled */\n api_v1_enabled?: boolean;\n}\n\n/** Battery control from GET /api/batteries */\nexport interface BatteryControl {\n /** Battery mode */\n mode: \"zero\" | \"to_full\" | \"standby\";\n /** Battery permissions */\n permissions?: string[];\n /** Number of connected batteries */\n battery_count?: number;\n /** Current combined power in W */\n power_w?: number;\n /** Target power in W */\n target_power_w?: number;\n /** Maximum consumption in W */\n max_consumption_w?: number;\n /** Maximum production in W */\n max_production_w?: number;\n}\n\n/** WebSocket message envelope */\nexport interface WsMessage {\n /** Message type */\n type: string;\n /** Message data payload (string for auth/subscribe, object for measurement) */\n data?: unknown;\n}\n\n/** Device discovered via mDNS */\nexport interface DiscoveredDevice {\n /** Device IP address */\n ip: string;\n /** Product type from mDNS TXT record or device info */\n productType: string;\n /** Serial number from mDNS name */\n serial: string;\n /** Human-readable name */\n name: string;\n}\n\n/** Connection state for a single device */\nexport interface DeviceConnection {\n /** Device config */\n config: DeviceConfig;\n /** Current IP address (from mDNS or stored fixed IP) */\n ip: string;\n /** WebSocket client instance (if connected) */\n wsClient: HomeWizardWebSocket | null;\n /** Whether WS is authenticated */\n wsAuthenticated: boolean;\n /** REST fallback polling timer */\n pollTimer: ioBroker.Interval | undefined;\n /** Reconnect timer */\n reconnectTimer: ioBroker.Timeout | undefined;\n /** Consecutive WS failures for backoff */\n wsFailCount: number;\n /** Consecutive auth failures */\n authFailCount: number;\n /** Last error code for dedup */\n lastErrorCode: string;\n /** Timestamp when WS last connected (for stability tracking) */\n lastConnectedAt: number;\n /** Count of short-lived connections (< STABLE_THRESHOLD) */\n recentDisconnects: number;\n}\n"],
4
+ "sourcesContent": ["import type { HomeWizardWebSocket } from \"./websocket-client\";\n\n/** Persisted config for a single paired device (stored in device object native) */\nexport interface DeviceConfig {\n /** Bearer token (encrypted via adapter.encrypt) */\n token: string;\n /** Product type (e.g. HWE-P1) */\n productType: string;\n /** Device serial number */\n serial: string;\n /** Human-readable product name */\n productName: string;\n /** Fixed IP address (only when manually set, empty = mDNS) */\n ip?: string;\n}\n\n/** Response from GET /api */\nexport interface DeviceInfo {\n /** Product name */\n product_name: string;\n /** Product type identifier */\n product_type: string;\n /** Device serial number */\n serial: string;\n /** Firmware version string */\n firmware_version: string;\n /** API version string */\n api_version: string;\n}\n\n/** Response from POST /api/user (pairing) */\nexport interface PairingResponse {\n /** Bearer token for API access */\n token: string;\n}\n\n/** Measurement data from GET /api/measurement or WebSocket push */\nexport interface Measurement {\n /** Unique meter identifier */\n unique_id?: string;\n /** Protocol version */\n protocol_version?: number;\n /** Meter model */\n meter_model?: string;\n /** Measurement timestamp */\n timestamp?: string;\n /** Active tariff number */\n tariff?: number;\n\n /** Total energy import in kWh */\n energy_import_kwh?: number;\n /** Energy import tariff 1 */\n energy_import_t1_kwh?: number;\n /** Energy import tariff 2 */\n energy_import_t2_kwh?: number;\n /** Energy import tariff 3 */\n energy_import_t3_kwh?: number;\n /** Energy import tariff 4 */\n energy_import_t4_kwh?: number;\n /** Total energy export in kWh */\n energy_export_kwh?: number;\n /** Energy export tariff 1 */\n energy_export_t1_kwh?: number;\n /** Energy export tariff 2 */\n energy_export_t2_kwh?: number;\n /** Energy export tariff 3 */\n energy_export_t3_kwh?: number;\n /** Energy export tariff 4 */\n energy_export_t4_kwh?: number;\n\n /** Total active power in W */\n power_w?: number;\n /** Active power phase 1 */\n power_l1_w?: number;\n /** Active power phase 2 */\n power_l2_w?: number;\n /** Active power phase 3 */\n power_l3_w?: number;\n\n /** Voltage (single phase) */\n voltage_v?: number;\n /** Voltage phase 1 */\n voltage_l1_v?: number;\n /** Voltage phase 2 */\n voltage_l2_v?: number;\n /** Voltage phase 3 */\n voltage_l3_v?: number;\n\n /** Current (single phase) */\n current_a?: number;\n /** Current phase 1 */\n current_l1_a?: number;\n /** Current phase 2 */\n current_l2_a?: number;\n /** Current phase 3 */\n current_l3_a?: number;\n\n /** Grid frequency in Hz */\n frequency_hz?: number;\n\n /** Voltage sag count phase 1 */\n voltage_sag_l1_count?: number;\n /** Voltage sag count phase 2 */\n voltage_sag_l2_count?: number;\n /** Voltage sag count phase 3 */\n voltage_sag_l3_count?: number;\n /** Voltage swell count phase 1 */\n voltage_swell_l1_count?: number;\n /** Voltage swell count phase 2 */\n voltage_swell_l2_count?: number;\n /** Voltage swell count phase 3 */\n voltage_swell_l3_count?: number;\n /** Any power fail count */\n any_power_fail_count?: number;\n /** Long power fail count */\n long_power_fail_count?: number;\n\n /** Average power over 15 min (Belgium) */\n average_power_15m_w?: number;\n /** Monthly power peak (Belgium) */\n monthly_power_peak_w?: number;\n /** Monthly power peak timestamp (Belgium) */\n monthly_power_peak_timestamp?: string;\n\n /** Apparent current */\n apparent_current_a?: number;\n /** Reactive current */\n reactive_current_a?: number;\n /** Apparent power in VA */\n apparent_power_va?: number;\n /** Reactive power in var */\n reactive_power_var?: number;\n /** Power factor */\n power_factor?: number;\n /** Apparent current phase 1 */\n apparent_current_l1_a?: number;\n /** Apparent current phase 2 */\n apparent_current_l2_a?: number;\n /** Apparent current phase 3 */\n apparent_current_l3_a?: number;\n /** Reactive current phase 1 */\n reactive_current_l1_a?: number;\n /** Reactive current phase 2 */\n reactive_current_l2_a?: number;\n /** Reactive current phase 3 */\n reactive_current_l3_a?: number;\n /** Apparent power phase 1 */\n apparent_power_l1_va?: number;\n /** Apparent power phase 2 */\n apparent_power_l2_va?: number;\n /** Apparent power phase 3 */\n apparent_power_l3_va?: number;\n /** Reactive power phase 1 */\n reactive_power_l1_var?: number;\n /** Reactive power phase 2 */\n reactive_power_l2_var?: number;\n /** Reactive power phase 3 */\n reactive_power_l3_var?: number;\n /** Power factor phase 1 */\n power_factor_l1?: number;\n /** Power factor phase 2 */\n power_factor_l2?: number;\n /** Power factor phase 3 */\n power_factor_l3?: number;\n\n /** Battery state of charge in percent */\n state_of_charge_pct?: number;\n /** Battery charge cycles */\n cycles?: number;\n\n /** External meters (gas, water, heat) */\n external?: ExternalMeter[];\n}\n\n/** External meter attached to P1 (gas, water, heat) */\nexport interface ExternalMeter {\n /** Unique meter identifier */\n unique_id: string;\n /** Meter type */\n type: \"gas_meter\" | \"heat_meter\" | \"warm_water_meter\" | \"water_meter\" | \"inlet_heat_meter\";\n /** Last reading timestamp */\n timestamp: string;\n /** Meter reading value */\n value: number;\n /** Measurement unit */\n unit: string;\n}\n\n/** System info from GET /api/system */\nexport interface SystemInfo {\n /** WiFi SSID */\n wifi_ssid: string;\n /** WiFi signal strength in dB */\n wifi_rssi_db: number;\n /** Uptime in seconds */\n uptime_s: number;\n /** Cloud communication enabled */\n cloud_enabled: boolean;\n /** Status LED brightness 0-100% */\n status_led_brightness_pct: number;\n /** Legacy API v1 enabled */\n api_v1_enabled?: boolean;\n}\n\n/** Battery control from GET /api/batteries */\nexport interface BatteryControl {\n /** Battery mode */\n mode: \"zero\" | \"to_full\" | \"standby\";\n /** Battery permissions */\n permissions?: string[];\n /** Number of connected batteries */\n battery_count?: number;\n /** Current combined power in W */\n power_w?: number;\n /** Target power in W */\n target_power_w?: number;\n /** Maximum consumption in W */\n max_consumption_w?: number;\n /** Maximum production in W */\n max_production_w?: number;\n}\n\n/** WebSocket message envelope */\nexport interface WsMessage {\n /** Message type */\n type: string;\n /** Message data payload (string for auth/subscribe, object for measurement) */\n data?: unknown;\n}\n\n/** Device discovered via mDNS */\nexport interface DiscoveredDevice {\n /** Device IP address */\n ip: string;\n /** Product type from mDNS TXT record or device info */\n productType: string;\n /** Serial number from mDNS name */\n serial: string;\n /** Human-readable name */\n name: string;\n}\n\n/** Connection state for a single device */\nexport interface DeviceConnection {\n /** Device config */\n config: DeviceConfig;\n /** Current IP address (from mDNS or stored fixed IP) */\n ip: string;\n /** WebSocket client instance (if connected) */\n wsClient: HomeWizardWebSocket | null;\n /** Whether WS is authenticated */\n wsAuthenticated: boolean;\n /** REST fallback polling timer */\n pollTimer: ioBroker.Interval | undefined;\n /** Reconnect timer */\n reconnectTimer: ioBroker.Timeout | undefined;\n /** Consecutive WS failures for backoff */\n wsFailCount: number;\n /** Consecutive auth failures */\n authFailCount: number;\n /** Last error code for dedup */\n lastErrorCode: string;\n /** Timestamp when WS last connected (for stability tracking) */\n lastConnectedAt: number;\n /** Count of short-lived connections (< STABLE_THRESHOLD) */\n recentDisconnects: number;\n /** True while a connect/IP-recovery cycle is in flight \u2014 guards against duplicate connect attempts triggered by repeated mDNS broadcasts. */\n recovering: boolean;\n /** True after the device was removed \u2014 async tasks (in-flight REST/WS) check this before writing further state. */\n removed: boolean;\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
@@ -28,18 +28,27 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var websocket_client_exports = {};
30
30
  __export(websocket_client_exports, {
31
- HomeWizardWebSocket: () => HomeWizardWebSocket
31
+ AUTH_TIMEOUT_MS: () => AUTH_TIMEOUT_MS,
32
+ HomeWizardWebSocket: () => HomeWizardWebSocket,
33
+ PING_INTERVAL_MS: () => PING_INTERVAL_MS,
34
+ PONG_TIMEOUT_MS: () => PONG_TIMEOUT_MS
32
35
  });
33
36
  module.exports = __toCommonJS(websocket_client_exports);
34
37
  var import_ws = __toESM(require("ws"));
35
38
  var import_cacert = require("./cacert");
36
39
  var import_coerce = require("./coerce");
40
+ const AUTH_TIMEOUT_MS = 45e3;
41
+ const PING_INTERVAL_MS = 3e4;
42
+ const PONG_TIMEOUT_MS = 1e4;
37
43
  class HomeWizardWebSocket {
38
44
  ip;
39
45
  token;
40
46
  callbacks;
41
47
  ws = null;
42
48
  destroyed = false;
49
+ authTimer = null;
50
+ pingInterval = null;
51
+ pongTimer = null;
43
52
  /**
44
53
  * @param ip Device IP address
45
54
  * @param token Bearer token
@@ -62,14 +71,25 @@ class HomeWizardWebSocket {
62
71
  agent: import_cacert.HW_AGENT,
63
72
  handshakeTimeout: 1e4
64
73
  });
74
+ this.authTimer = setTimeout(() => {
75
+ this.callbacks.log.debug(`WS auth-timeout (${AUTH_TIMEOUT_MS}ms) \u2014 terminating`);
76
+ this.forceDisconnect();
77
+ }, AUTH_TIMEOUT_MS);
65
78
  this.ws.on("open", () => {
66
79
  this.callbacks.log.debug(`WS open to ${this.ip}`);
67
80
  });
68
81
  this.ws.on("message", (raw) => {
69
82
  this.handleMessage(raw);
70
83
  });
84
+ this.ws.on("pong", () => {
85
+ if (this.pongTimer) {
86
+ clearTimeout(this.pongTimer);
87
+ this.pongTimer = null;
88
+ }
89
+ });
71
90
  this.ws.on("close", (code, reason) => {
72
91
  this.callbacks.log.debug(`WS closed: ${code} ${reason.toString()}`);
92
+ this.clearTimers();
73
93
  this.ws = null;
74
94
  if (!this.destroyed) {
75
95
  this.callbacks.onDisconnected();
@@ -120,6 +140,11 @@ class HomeWizardWebSocket {
120
140
  case "authorized":
121
141
  this.callbacks.log.debug("WS authorized, subscribing to measurement");
122
142
  this.sendRaw({ type: "subscribe", data: "measurement" });
143
+ if (this.authTimer) {
144
+ clearTimeout(this.authTimer);
145
+ this.authTimer = null;
146
+ }
147
+ this.startHeartbeat();
123
148
  this.callbacks.onConnected();
124
149
  break;
125
150
  case "measurement":
@@ -147,8 +172,56 @@ class HomeWizardWebSocket {
147
172
  this.ws.send(JSON.stringify(msg));
148
173
  }
149
174
  }
175
+ /**
176
+ * Start the ping/pong heartbeat. Sends a WS-layer ping every
177
+ * PING_INTERVAL_MS and arms a pong-timer; a missing pong terminates.
178
+ * This catches half-dead links where the TCP stream is buffered but the
179
+ * device has stopped responding (the documented "API-Lockup" mode).
180
+ */
181
+ startHeartbeat() {
182
+ this.pingInterval = setInterval(() => {
183
+ if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) {
184
+ return;
185
+ }
186
+ this.pongTimer = setTimeout(() => {
187
+ this.callbacks.log.debug(`WS pong-timeout (${PONG_TIMEOUT_MS}ms) \u2014 terminating`);
188
+ this.forceDisconnect();
189
+ }, PONG_TIMEOUT_MS);
190
+ try {
191
+ this.ws.ping();
192
+ } catch (err) {
193
+ this.callbacks.log.debug(`WS ping send failed: ${err instanceof Error ? err.message : String(err)}`);
194
+ }
195
+ }, PING_INTERVAL_MS);
196
+ }
197
+ /** Terminate the socket — triggers close-event → onDisconnected → reconnect. */
198
+ forceDisconnect() {
199
+ if (!this.ws) {
200
+ return;
201
+ }
202
+ try {
203
+ this.ws.terminate();
204
+ } catch {
205
+ }
206
+ }
207
+ /** Clear all timers. Called on close, cleanup, and from the close-event. */
208
+ clearTimers() {
209
+ if (this.authTimer) {
210
+ clearTimeout(this.authTimer);
211
+ this.authTimer = null;
212
+ }
213
+ if (this.pingInterval) {
214
+ clearInterval(this.pingInterval);
215
+ this.pingInterval = null;
216
+ }
217
+ if (this.pongTimer) {
218
+ clearTimeout(this.pongTimer);
219
+ this.pongTimer = null;
220
+ }
221
+ }
150
222
  /** Close WebSocket without triggering reconnect */
151
223
  cleanup() {
224
+ this.clearTimers();
152
225
  if (this.ws) {
153
226
  this.ws.removeAllListeners();
154
227
  this.ws.on("error", () => {
@@ -160,6 +233,9 @@ class HomeWizardWebSocket {
160
233
  }
161
234
  // Annotate the CommonJS export names for ESM import in node:
162
235
  0 && (module.exports = {
163
- HomeWizardWebSocket
236
+ AUTH_TIMEOUT_MS,
237
+ HomeWizardWebSocket,
238
+ PING_INTERVAL_MS,
239
+ PONG_TIMEOUT_MS
164
240
  });
165
241
  //# sourceMappingURL=websocket-client.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/websocket-client.ts"],
4
- "sourcesContent": ["import WebSocket from \"ws\";\nimport { HW_AGENT } from \"./cacert\";\nimport { isPlainObject } from \"./coerce\";\nimport type { Measurement } from \"./types\";\n\n/** Callback interface for WebSocket events */\nexport interface WsCallbacks {\n /** Called when measurement data is received */\n onMeasurement: (data: Measurement) => void;\n /** Called when connection is established and authenticated */\n onConnected: () => void;\n /** Called when connection is lost */\n onDisconnected: (error?: Error) => void;\n /** Log functions */\n log: {\n debug: (msg: string) => void;\n warn: (msg: string) => void;\n };\n}\n\n/**\n * WebSocket client for HomeWizard real-time measurement push.\n * Handles auth handshake, subscription, and auto-reconnect.\n */\nexport class HomeWizardWebSocket {\n private readonly ip: string;\n private readonly token: string;\n private readonly callbacks: WsCallbacks;\n private ws: WebSocket | null = null;\n private destroyed = false;\n\n /**\n * @param ip Device IP address\n * @param token Bearer token\n * @param callbacks Event callbacks\n */\n constructor(ip: string, token: string, callbacks: WsCallbacks) {\n this.ip = ip;\n this.token = token;\n this.callbacks = callbacks;\n }\n\n /** Connect to WebSocket and start auth handshake */\n connect(): void {\n if (this.destroyed) {\n return;\n }\n\n this.cleanup();\n\n const url = `wss://${this.ip}/api/ws`;\n this.callbacks.log.debug(`WS connecting to ${url}`);\n\n this.ws = new WebSocket(url, {\n agent: HW_AGENT,\n handshakeTimeout: 10_000,\n });\n\n this.ws.on(\"open\", () => {\n this.callbacks.log.debug(`WS open to ${this.ip}`);\n });\n\n this.ws.on(\"message\", (raw: WebSocket.RawData) => {\n this.handleMessage(raw);\n });\n\n this.ws.on(\"close\", (code: number, reason: Buffer) => {\n this.callbacks.log.debug(`WS closed: ${code} ${reason.toString()}`);\n this.ws = null;\n if (!this.destroyed) {\n this.callbacks.onDisconnected();\n }\n });\n\n this.ws.on(\"error\", (err: Error) => {\n this.callbacks.log.debug(`WS error: ${err.message}`);\n // close event will follow\n });\n }\n\n /** Gracefully close connection */\n close(): void {\n this.destroyed = true;\n this.cleanup();\n }\n\n /** Whether the WebSocket is currently open */\n get isConnected(): boolean {\n return this.ws?.readyState === WebSocket.OPEN;\n }\n\n /**\n * Handle incoming WebSocket message\n *\n * @param raw Raw message data\n */\n private handleMessage(raw: WebSocket.RawData): void {\n const text = Buffer.isBuffer(raw)\n ? raw.toString(\"utf8\")\n : raw instanceof ArrayBuffer\n ? Buffer.from(raw).toString(\"utf8\")\n : Array.isArray(raw)\n ? Buffer.concat(raw).toString(\"utf8\")\n : \"\";\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n this.callbacks.log.warn(`WS invalid JSON: ${text.substring(0, 200)}`);\n return;\n }\n\n if (!isPlainObject(parsed)) {\n this.callbacks.log.warn(`WS non-object message: ${text.substring(0, 200)}`);\n return;\n }\n\n const type = parsed.type;\n if (typeof type !== \"string\") {\n this.callbacks.log.warn(`WS message without string type`);\n return;\n }\n\n switch (type) {\n case \"authorization_requested\":\n this.callbacks.log.debug(\"WS auth requested, sending token\");\n this.sendRaw({ type: \"authorization\", data: this.token });\n break;\n\n case \"authorized\":\n this.callbacks.log.debug(\"WS authorized, subscribing to measurement\");\n this.sendRaw({ type: \"subscribe\", data: \"measurement\" });\n this.callbacks.onConnected();\n break;\n\n case \"measurement\":\n if (isPlainObject(parsed.data)) {\n this.callbacks.onMeasurement(parsed.data);\n } else {\n this.callbacks.log.warn(`WS measurement without object payload`);\n }\n break;\n\n default:\n this.callbacks.log.debug(`WS message type: ${type}`);\n break;\n }\n }\n\n /**\n * Send a message over WebSocket\n *\n * @param msg Message envelope\n * @param msg.type Message type identifier\n * @param msg.data Optional payload\n */\n private sendRaw(msg: { type: string; data?: unknown }): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n /** Close WebSocket without triggering reconnect */\n private cleanup(): void {\n if (this.ws) {\n this.ws.removeAllListeners();\n // Prevent uncaught errors from frames received during close\n this.ws.on(\"error\", () => {});\n this.ws.terminate();\n this.ws = null;\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import WebSocket from \"ws\";\nimport { HW_AGENT } from \"./cacert\";\nimport { isPlainObject } from \"./coerce\";\nimport type { Measurement } from \"./types\";\n\n/** Auth handshake must complete within this window (Doku says 40s, +5s slack). */\nexport const AUTH_TIMEOUT_MS = 45_000;\n/** WS-layer ping interval after `authorized`. */\nexport const PING_INTERVAL_MS = 30_000;\n/** Max time to wait for a pong reply before declaring the link dead. */\nexport const PONG_TIMEOUT_MS = 10_000;\n\n/** Callback interface for WebSocket events */\nexport interface WsCallbacks {\n /** Called when measurement data is received */\n onMeasurement: (data: Measurement) => void;\n /** Called when connection is established and authenticated */\n onConnected: () => void;\n /** Called when connection is lost */\n onDisconnected: (error?: Error) => void;\n /** Log functions */\n log: {\n debug: (msg: string) => void;\n warn: (msg: string) => void;\n };\n}\n\n/**\n * WebSocket client for HomeWizard real-time measurement push.\n * Handles auth handshake, subscription, heartbeat (WS-layer ping/pong),\n * and termination of half-dead connections (TCP open, no traffic).\n *\n * The push is event-driven (P1 Power ~1/s, Gas ~5min, Battery undocumented),\n * so we cannot rely on measurement frames as a liveness signal. Instead we\n * use the WS-layer ping/pong frames, which the device must answer regardless\n * of data activity.\n */\nexport class HomeWizardWebSocket {\n private readonly ip: string;\n private readonly token: string;\n private readonly callbacks: WsCallbacks;\n private ws: WebSocket | null = null;\n private destroyed = false;\n private authTimer: NodeJS.Timeout | null = null;\n private pingInterval: NodeJS.Timeout | null = null;\n private pongTimer: NodeJS.Timeout | null = null;\n\n /**\n * @param ip Device IP address\n * @param token Bearer token\n * @param callbacks Event callbacks\n */\n constructor(ip: string, token: string, callbacks: WsCallbacks) {\n this.ip = ip;\n this.token = token;\n this.callbacks = callbacks;\n }\n\n /** Connect to WebSocket and start auth handshake */\n connect(): void {\n if (this.destroyed) {\n return;\n }\n\n this.cleanup();\n\n const url = `wss://${this.ip}/api/ws`;\n this.callbacks.log.debug(`WS connecting to ${url}`);\n\n this.ws = new WebSocket(url, {\n agent: HW_AGENT,\n handshakeTimeout: 10_000,\n });\n\n // Auth-watchdog: server must finish the auth handshake within\n // AUTH_TIMEOUT_MS or we declare the link dead. Doku timeout is 40s.\n this.authTimer = setTimeout(() => {\n this.callbacks.log.debug(`WS auth-timeout (${AUTH_TIMEOUT_MS}ms) \u2014 terminating`);\n this.forceDisconnect();\n }, AUTH_TIMEOUT_MS);\n\n this.ws.on(\"open\", () => {\n this.callbacks.log.debug(`WS open to ${this.ip}`);\n });\n\n this.ws.on(\"message\", (raw: WebSocket.RawData) => {\n this.handleMessage(raw);\n });\n\n this.ws.on(\"pong\", () => {\n // Pong arrived in time \u2014 clear pending pong-timer.\n if (this.pongTimer) {\n clearTimeout(this.pongTimer);\n this.pongTimer = null;\n }\n });\n\n this.ws.on(\"close\", (code: number, reason: Buffer) => {\n this.callbacks.log.debug(`WS closed: ${code} ${reason.toString()}`);\n this.clearTimers();\n this.ws = null;\n if (!this.destroyed) {\n this.callbacks.onDisconnected();\n }\n });\n\n this.ws.on(\"error\", (err: Error) => {\n this.callbacks.log.debug(`WS error: ${err.message}`);\n // close event will follow\n });\n }\n\n /** Gracefully close connection */\n close(): void {\n this.destroyed = true;\n this.cleanup();\n }\n\n /** Whether the WebSocket is currently open */\n get isConnected(): boolean {\n return this.ws?.readyState === WebSocket.OPEN;\n }\n\n /**\n * Handle incoming WebSocket message\n *\n * @param raw Raw message data\n */\n private handleMessage(raw: WebSocket.RawData): void {\n const text = Buffer.isBuffer(raw)\n ? raw.toString(\"utf8\")\n : raw instanceof ArrayBuffer\n ? Buffer.from(raw).toString(\"utf8\")\n : Array.isArray(raw)\n ? Buffer.concat(raw).toString(\"utf8\")\n : \"\";\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n this.callbacks.log.warn(`WS invalid JSON: ${text.substring(0, 200)}`);\n return;\n }\n\n if (!isPlainObject(parsed)) {\n this.callbacks.log.warn(`WS non-object message: ${text.substring(0, 200)}`);\n return;\n }\n\n const type = parsed.type;\n if (typeof type !== \"string\") {\n this.callbacks.log.warn(`WS message without string type`);\n return;\n }\n\n switch (type) {\n case \"authorization_requested\":\n this.callbacks.log.debug(\"WS auth requested, sending token\");\n this.sendRaw({ type: \"authorization\", data: this.token });\n break;\n\n case \"authorized\":\n this.callbacks.log.debug(\"WS authorized, subscribing to measurement\");\n this.sendRaw({ type: \"subscribe\", data: \"measurement\" });\n // Auth complete \u2014 clear auth-watchdog and start the heartbeat.\n if (this.authTimer) {\n clearTimeout(this.authTimer);\n this.authTimer = null;\n }\n this.startHeartbeat();\n this.callbacks.onConnected();\n break;\n\n case \"measurement\":\n if (isPlainObject(parsed.data)) {\n this.callbacks.onMeasurement(parsed.data);\n } else {\n this.callbacks.log.warn(`WS measurement without object payload`);\n }\n break;\n\n default:\n this.callbacks.log.debug(`WS message type: ${type}`);\n break;\n }\n }\n\n /**\n * Send a message over WebSocket\n *\n * @param msg Message envelope\n * @param msg.type Message type identifier\n * @param msg.data Optional payload\n */\n private sendRaw(msg: { type: string; data?: unknown }): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n /**\n * Start the ping/pong heartbeat. Sends a WS-layer ping every\n * PING_INTERVAL_MS and arms a pong-timer; a missing pong terminates.\n * This catches half-dead links where the TCP stream is buffered but the\n * device has stopped responding (the documented \"API-Lockup\" mode).\n */\n private startHeartbeat(): void {\n this.pingInterval = setInterval(() => {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n return;\n }\n // Arm the pong-timer first, then ping. If pong arrives, the pong\n // handler clears it; if it doesn't, we terminate.\n this.pongTimer = setTimeout(() => {\n this.callbacks.log.debug(`WS pong-timeout (${PONG_TIMEOUT_MS}ms) \u2014 terminating`);\n this.forceDisconnect();\n }, PONG_TIMEOUT_MS);\n try {\n this.ws.ping();\n } catch (err) {\n this.callbacks.log.debug(`WS ping send failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }, PING_INTERVAL_MS);\n }\n\n /** Terminate the socket \u2014 triggers close-event \u2192 onDisconnected \u2192 reconnect. */\n private forceDisconnect(): void {\n if (!this.ws) {\n return;\n }\n try {\n this.ws.terminate();\n } catch {\n // ignore \u2014 already closed\n }\n }\n\n /** Clear all timers. Called on close, cleanup, and from the close-event. */\n private clearTimers(): void {\n if (this.authTimer) {\n clearTimeout(this.authTimer);\n this.authTimer = null;\n }\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n if (this.pongTimer) {\n clearTimeout(this.pongTimer);\n this.pongTimer = null;\n }\n }\n\n /** Close WebSocket without triggering reconnect */\n private cleanup(): void {\n this.clearTimers();\n if (this.ws) {\n this.ws.removeAllListeners();\n // Prevent uncaught errors from frames received during close\n this.ws.on(\"error\", () => {});\n this.ws.terminate();\n this.ws = null;\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["WebSocket"]
7
7
  }
package/build/main.js CHANGED
@@ -55,6 +55,8 @@ class HomeWizard extends utils.Adapter {
55
55
  discoveredDuringPairing = [];
56
56
  unhandledRejectionHandler = null;
57
57
  uncaughtExceptionHandler = null;
58
+ /** Set during onUnload — async paths bail before further setStateAsync calls. */
59
+ unloading = false;
58
60
  /** @param options Adapter options */
59
61
  constructor(options = {}) {
60
62
  super({ ...options, name: "homewizard" });
@@ -115,7 +117,8 @@ class HomeWizard extends utils.Adapter {
115
117
  */
116
118
  async loadDevicesFromObjects() {
117
119
  const devices = [];
118
- const oldDevices = this.config.devices || [];
120
+ const rawOldDevices = this.config.devices;
121
+ const oldDevices = Array.isArray(rawOldDevices) ? rawOldDevices : [];
119
122
  if (oldDevices.length > 0) {
120
123
  this.log.debug(`Migrating ${oldDevices.length} device(s) from adapter config to device objects`);
121
124
  for (const device of oldDevices) {
@@ -137,7 +140,15 @@ class HomeWizard extends utils.Adapter {
137
140
  }
138
141
  const localId = id.replace(`${this.namespace}.`, "");
139
142
  this.log.debug(`Loading device from object: ${localId}`);
140
- const token = this.decrypt(native.encryptedToken);
143
+ let token;
144
+ try {
145
+ token = this.decrypt(native.encryptedToken);
146
+ } catch (err) {
147
+ this.log.warn(
148
+ `Cannot decrypt token for ${localId} \u2014 re-pair the device. (${(0, import_coerce.errText)(err)}). Other devices remain unaffected.`
149
+ );
150
+ continue;
151
+ }
141
152
  devices.push({
142
153
  token,
143
154
  productType: native.productType || "unknown",
@@ -193,6 +204,7 @@ class HomeWizard extends utils.Adapter {
193
204
  */
194
205
  onUnload(callback) {
195
206
  var _a, _b;
207
+ this.unloading = true;
196
208
  try {
197
209
  if (this.pairingTimer) {
198
210
  this.clearTimeout(this.pairingTimer);
@@ -237,7 +249,7 @@ class HomeWizard extends utils.Adapter {
237
249
  * @param state State value
238
250
  */
239
251
  async onStateChange(id, state) {
240
- if (!state || state.ack) {
252
+ if (!state || state.ack || this.unloading) {
241
253
  return;
242
254
  }
243
255
  if (id.endsWith(".startPairing")) {
@@ -311,6 +323,12 @@ class HomeWizard extends utils.Adapter {
311
323
  this.pairingManualIp = (ipState == null ? void 0 : ipState.val) ? String(ipState.val).trim() : "";
312
324
  await this.setStateAsync("pairingIp", { val: "", ack: true });
313
325
  if (this.pairingManualIp) {
326
+ if (!(0, import_coerce.isValidIpv4)(this.pairingManualIp)) {
327
+ this.log.warn(`Invalid pairing IP '${this.pairingManualIp}' \u2014 expected IPv4 (e.g. 192.168.1.42)`);
328
+ this.isPairing = false;
329
+ this.pairingManualIp = "";
330
+ return;
331
+ }
314
332
  this.log.info(
315
333
  `Pairing mode enabled for ${this.pairingManualIp} \u2014 press the button on your HomeWizard device now (60 seconds timeout)`
316
334
  );
@@ -341,6 +359,7 @@ class HomeWizard extends utils.Adapter {
341
359
  }
342
360
  /** Poll all discovered devices to attempt pairing */
343
361
  async pollPairing() {
362
+ var _a;
344
363
  for (const device of this.discoveredDuringPairing) {
345
364
  try {
346
365
  const client = new import_homewizard_client.HomeWizardClient(device.ip);
@@ -360,13 +379,23 @@ class HomeWizard extends utils.Adapter {
360
379
  await this.saveDeviceToObject(deviceConfig);
361
380
  await this.stateManager.createDeviceStates(deviceConfig);
362
381
  const key = this.stateManager.devicePrefix(deviceConfig);
382
+ const previous = this.connections.get(key);
383
+ if (previous) {
384
+ this.log.debug(`Re-pair: closing previous connection for ${deviceConfig.productName}`);
385
+ (_a = previous.wsClient) == null ? void 0 : _a.close();
386
+ if (previous.pollTimer) {
387
+ this.clearInterval(previous.pollTimer);
388
+ }
389
+ if (previous.reconnectTimer) {
390
+ this.clearTimeout(previous.reconnectTimer);
391
+ }
392
+ }
363
393
  const conn = (0, import_connection_utils.createDeviceConnection)(deviceConfig, device.ip);
364
394
  this.connections.set(key, conn);
365
395
  void this.initDevice(conn);
366
396
  this.discoveredDuringPairing = this.discoveredDuringPairing.filter((d) => d.serial !== info.serial);
367
- this.stopPairing();
368
397
  this.updateGlobalConnection();
369
- return;
398
+ continue;
370
399
  } catch (err) {
371
400
  if (err instanceof import_homewizard_client.HomeWizardApiError && err.statusCode === 403) {
372
401
  continue;
@@ -408,12 +437,17 @@ class HomeWizard extends utils.Adapter {
408
437
  if (discovered.ip === conn.ip || conn.wsAuthenticated) {
409
438
  return;
410
439
  }
440
+ if (conn.recovering) {
441
+ return;
442
+ }
411
443
  this.log.info(`${conn.config.productName}: found at new IP ${discovered.ip} (was ${conn.ip})`);
412
444
  conn.ip = discovered.ip;
413
445
  conn.config.ip = discovered.ip;
414
446
  conn.wsFailCount = 0;
415
447
  conn.recentDisconnects = 0;
416
- void this.saveDeviceToObject(conn.config);
448
+ this.saveDeviceToObject(conn.config).catch(
449
+ (err) => this.log.debug(`Failed to persist new IP for ${conn.config.productName}: ${(0, import_coerce.errText)(err)}`)
450
+ );
417
451
  if (conn.reconnectTimer) {
418
452
  this.clearTimeout(conn.reconnectTimer);
419
453
  conn.reconnectTimer = void 0;
@@ -455,17 +489,29 @@ class HomeWizard extends utils.Adapter {
455
489
  * @param conn Device connection with IP set
456
490
  */
457
491
  async initDevice(conn) {
492
+ if (this.unloading || conn.removed) {
493
+ return;
494
+ }
458
495
  try {
459
496
  const client = new import_homewizard_client.HomeWizardClient(conn.ip, conn.config.token);
460
497
  const info = await client.getDeviceInfo();
498
+ if (this.unloading || conn.removed) {
499
+ return;
500
+ }
461
501
  const key = this.stateManager.devicePrefix(conn.config);
462
502
  await this.setStateAsync(`${key}.info.firmware`, {
463
503
  val: info.firmware_version,
464
504
  ack: true
465
505
  });
466
506
  } catch (err) {
507
+ if (this.unloading) {
508
+ return;
509
+ }
467
510
  this.logDeviceError(conn, "init", err);
468
511
  }
512
+ if (this.unloading || conn.removed) {
513
+ return;
514
+ }
469
515
  this.connectWebSocket(conn);
470
516
  void this.pollSystemInfo(conn);
471
517
  }
@@ -481,19 +527,30 @@ class HomeWizard extends utils.Adapter {
481
527
  if (conn.authFailCount >= MAX_AUTH_FAILURES) {
482
528
  return;
483
529
  }
530
+ conn.recovering = true;
531
+ if (conn.wsClient) {
532
+ conn.wsClient.close();
533
+ conn.wsClient = null;
534
+ }
484
535
  if ((0, import_main_helpers.shouldStartIpRecovery)(conn.wsFailCount, WS_FAILURES_BEFORE_MDNS, MDNS_RETRY_EVERY)) {
485
536
  this.startIpRecovery();
486
537
  }
487
538
  const key = this.stateManager.devicePrefix(conn.config);
488
539
  const wsClient = new import_websocket_client.HomeWizardWebSocket(conn.ip, conn.config.token, {
489
540
  onMeasurement: (data) => {
490
- void this.stateManager.updateMeasurement(conn.config, data);
541
+ if (conn.removed || this.unloading) {
542
+ return;
543
+ }
544
+ this.stateManager.updateMeasurement(conn.config, data).catch((err) => {
545
+ this.log.debug(`updateMeasurement failed for ${conn.config.productName}: ${(0, import_coerce.errText)(err)}`);
546
+ });
491
547
  },
492
548
  onConnected: () => {
493
549
  conn.wsAuthenticated = true;
494
550
  conn.wsFailCount = 0;
495
551
  conn.authFailCount = 0;
496
552
  conn.lastConnectedAt = Date.now();
553
+ conn.recovering = false;
497
554
  void this.stateManager.setDeviceConnected(conn.config, true);
498
555
  this.updateGlobalConnection();
499
556
  if (conn.pollTimer) {
@@ -515,7 +572,8 @@ class HomeWizard extends utils.Adapter {
515
572
  this.log.debug(`WebSocket connected to ${conn.config.productName} (${conn.ip})`);
516
573
  },
517
574
  onDisconnected: (error) => {
518
- if (conn.lastConnectedAt > 0) {
575
+ const isAuthError = error instanceof import_homewizard_client.HomeWizardApiError && error.errorCode === "user:unauthorized";
576
+ if (conn.lastConnectedAt > 0 && !isAuthError) {
519
577
  const duration = Date.now() - conn.lastConnectedAt;
520
578
  const transition = (0, import_main_helpers.decideUnstableTransition)(
521
579
  conn.recentDisconnects,
@@ -536,6 +594,7 @@ class HomeWizard extends utils.Adapter {
536
594
  }
537
595
  conn.wsAuthenticated = false;
538
596
  conn.wsClient = null;
597
+ conn.recovering = false;
539
598
  void this.stateManager.setDeviceConnected(conn.config, false);
540
599
  this.updateGlobalConnection();
541
600
  if (error) {
@@ -579,10 +638,19 @@ class HomeWizard extends utils.Adapter {
579
638
  const interval = (0, import_main_helpers.pickRestPollInterval)(unstable, REST_POLL_MS, REST_POLL_UNSTABLE_MS);
580
639
  const client = new import_homewizard_client.HomeWizardClient(conn.ip, conn.config.token);
581
640
  conn.pollTimer = this.setInterval(async () => {
641
+ if (conn.removed || this.unloading) {
642
+ return;
643
+ }
582
644
  try {
583
645
  const data = await client.getMeasurement();
646
+ if (conn.removed || this.unloading) {
647
+ return;
648
+ }
584
649
  await this.stateManager.updateMeasurement(conn.config, data);
585
650
  } catch (err) {
651
+ if (this.unloading) {
652
+ return;
653
+ }
586
654
  this.logDeviceError(conn, "rest", err);
587
655
  if (err instanceof import_homewizard_client.HomeWizardApiError && err.errorCode === "user:unauthorized") {
588
656
  this.handleAuthFailure(
@@ -600,13 +668,13 @@ class HomeWizard extends utils.Adapter {
600
668
  }
601
669
  }, interval);
602
670
  }
603
- /** Poll system info for all connected devices */
671
+ /** Poll system info for all connected devices in parallel */
604
672
  async pollAllSystemInfo() {
605
- for (const conn of this.connections.values()) {
606
- if (conn.ip && conn.wsAuthenticated) {
607
- await this.pollSystemInfo(conn);
608
- }
673
+ if (this.unloading) {
674
+ return;
609
675
  }
676
+ const tasks = Array.from(this.connections.values()).filter((c) => c.ip && c.wsAuthenticated && !c.removed).map((c) => this.pollSystemInfo(c));
677
+ await Promise.all(tasks);
610
678
  }
611
679
  /**
612
680
  * Poll system info for a single device
@@ -614,21 +682,46 @@ class HomeWizard extends utils.Adapter {
614
682
  * @param conn Device connection
615
683
  */
616
684
  async pollSystemInfo(conn) {
617
- if (!conn.ip) {
685
+ if (!conn.ip || conn.removed || this.unloading) {
618
686
  return;
619
687
  }
620
688
  try {
621
689
  const client = new import_homewizard_client.HomeWizardClient(conn.ip, conn.config.token);
622
690
  const system = await client.getSystem();
691
+ if (conn.removed || this.unloading) {
692
+ return;
693
+ }
623
694
  await this.stateManager.updateSystem(conn.config, system);
695
+ try {
696
+ const info = await client.getDeviceInfo();
697
+ if (!conn.removed && !this.unloading && info.product_name && info.product_name !== conn.config.productName) {
698
+ this.log.info(`${conn.config.productName}: name changed to '${info.product_name}' \u2014 updating object`);
699
+ conn.config.productName = info.product_name;
700
+ await this.saveDeviceToObject(conn.config);
701
+ }
702
+ } catch {
703
+ }
704
+ if (conn.removed || this.unloading) {
705
+ return;
706
+ }
624
707
  try {
625
708
  const battery = await client.getBatteries();
709
+ if (conn.removed || this.unloading) {
710
+ return;
711
+ }
626
712
  if (battery.battery_count && battery.battery_count > 0) {
627
713
  await this.stateManager.updateBattery(conn.config, battery);
628
714
  }
629
- } catch {
715
+ } catch (err) {
716
+ if (err instanceof import_homewizard_client.HomeWizardApiError && err.statusCode === 404) {
717
+ return;
718
+ }
719
+ this.log.debug(`${conn.config.productName} batteries: ${(0, import_coerce.errText)(err)}`);
630
720
  }
631
721
  } catch (err) {
722
+ if (this.unloading) {
723
+ return;
724
+ }
632
725
  this.logDeviceError(conn, "system", err);
633
726
  }
634
727
  }
@@ -653,6 +746,7 @@ class HomeWizard extends utils.Adapter {
653
746
  }
654
747
  const key = this.stateManager.devicePrefix(conn.config);
655
748
  this.log.info(`Removing device ${conn.config.productName} (${conn.config.serial})`);
749
+ conn.removed = true;
656
750
  (_a = conn.wsClient) == null ? void 0 : _a.close();
657
751
  if (conn.pollTimer) {
658
752
  this.clearInterval(conn.pollTimer);
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, 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\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 const oldDevices: DeviceConfig[] = ((this.config as Record<string, unknown>).devices 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\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 const token = this.decrypt(native.encryptedToken);\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 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) {\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 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 // Create connection and connect\n const key = this.stateManager.devicePrefix(deviceConfig);\n const conn = createDeviceConnection(deviceConfig, device.ip);\n this.connections.set(key, conn);\n void this.initDevice(conn);\n\n // Remove from discovery list\n this.discoveredDuringPairing = this.discoveredDuringPairing.filter(d => d.serial !== info.serial);\n\n this.stopPairing();\n this.updateGlobalConnection();\n return;\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\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 void this.saveDeviceToObject(conn.config);\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 try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const info = await client.getDeviceInfo();\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 this.logDeviceError(conn, \"init\", err);\n }\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 // 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 void this.stateManager.updateMeasurement(conn.config, data);\n },\n onConnected: () => {\n conn.wsAuthenticated = true;\n conn.wsFailCount = 0;\n conn.authFailCount = 0;\n conn.lastConnectedAt = Date.now();\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 // Track connection stability \u2014 pure decision in main-helpers, side-effects here\n if (conn.lastConnectedAt > 0) {\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 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 try {\n const data = await client.getMeasurement();\n await this.stateManager.updateMeasurement(conn.config, data);\n } catch (err) {\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 */\n private async pollAllSystemInfo(): Promise<void> {\n for (const conn of this.connections.values()) {\n if (conn.ip && conn.wsAuthenticated) {\n await this.pollSystemInfo(conn);\n }\n }\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) {\n return;\n }\n\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const system = await client.getSystem();\n await this.stateManager.updateSystem(conn.config, system);\n\n // Also poll battery if device supports it\n try {\n const battery = await client.getBatteries();\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 {\n // Device may not support batteries \u2014 that's fine\n }\n } catch (err) {\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 // 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 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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",
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.4",
4
+ "version": "0.7.5",
5
5
  "news": {
6
+ "0.7.5": {
7
+ "en": "Robustness: WebSocket heartbeat detects half-dead connections, auth handshake has a 45 s timeout, IP recovery and re-pair leak no sockets, one bad token no longer blocks the whole adapter.",
8
+ "de": "Robustheit: WebSocket-Heartbeat erkennt halb-tote Verbindungen, Auth-Handshake mit 45 s Timeout, IP-Recovery und Re-Pair leaken keine Sockets, defekter Token blockiert nicht mehr den Adapter.",
9
+ "ru": "Надёжность: heartbeat WebSocket видит наполовину мёртвые соединения, тайм-аут авторизации 45 с, восстановление IP и повторное сопряжение без утечек, плохой токен не блокирует адаптер.",
10
+ "pt": "Robustez: heartbeat WebSocket deteta ligações semi-mortas, autenticação com timeout de 45 s, recuperação de IP e novo emparelhamento sem fugas, token corrompido não bloqueia o adaptador.",
11
+ "nl": "Robuustheid: WebSocket-heartbeat ziet half-dode verbindingen, auth-handshake met 45 s time-out, IP-herstel en opnieuw koppelen lekken geen sockets, kapot token blokkeert de adapter niet meer.",
12
+ "fr": "Robustesse : heartbeat WebSocket détecte les connexions à moitié mortes, timeout auth 45 s, récupération IP et ré-appairage sans fuite, un token corrompu ne bloque plus tout l'adaptateur.",
13
+ "it": "Robustezza: heartbeat WebSocket vede connessioni semi-morte, handshake auth con timeout 45 s, recupero IP e riaccoppiamento senza fughe, un token corrotto non blocca più l'adattatore.",
14
+ "es": "Robustez: heartbeat WebSocket ve conexiones medio-muertas, timeout de auth 45 s, recuperación de IP y reemparejamiento sin fugas, un token corrupto ya no bloquea todo el adaptador.",
15
+ "pl": "Stabilność: heartbeat WebSocket wykrywa półmartwe połączenia, limit auth 45 s, odzyskiwanie IP i ponowne parowanie bez wycieków, uszkodzony token nie blokuje już adaptera.",
16
+ "uk": "Надійність: heartbeat WebSocket бачить напівмертві з'єднання, тайм-аут авторизації 45 с, відновлення IP та повторне сполучення без витоків, поганий токен не блокує адаптер.",
17
+ "zh-cn": "稳健性:WebSocket 心跳识别半死链接,认证握手 45 秒超时,IP 恢复与重新配对不再泄漏 socket,单个损坏令牌不再阻塞整个适配器。"
18
+ },
6
19
  "0.7.4": {
7
20
  "en": "Adapter log messages are now English only, in line with the ioBroker community standard. Localized state names, descriptions and dropdown labels (11 languages) remain unchanged.",
8
21
  "de": "Adapter-Logs sind jetzt nur noch auf Englisch, gemäß ioBroker-Community-Standard. Lokalisierte Datenpunkt-Namen, Beschreibungen und Dropdown-Labels (11 Sprachen) bleiben erhalten.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Dokumentacja: notatki wydania przepisane w przyjaznym dla użytkownika stylu we wszystkich językach.",
81
94
  "uk": "Документація: нотатки до релізу переписано у дружньому до користувача стилі усіма мовами.",
82
95
  "zh-cn": "文档:所有语言的发布说明已重写为用户友好的风格。"
83
- },
84
- "0.6.6": {
85
- "en": "Internal cleanup. No user-facing changes.",
86
- "de": "Interne Bereinigung. Keine User-sichtbaren Änderungen.",
87
- "ru": "Внутренняя очистка. Без видимых изменений для пользователя.",
88
- "pt": "Limpeza interna. Sem alterações visíveis para o usuário.",
89
- "nl": "Interne opschoning. Geen wijzigingen voor de gebruiker.",
90
- "fr": "Nettoyage interne. Aucune modification visible pour l'utilisateur.",
91
- "it": "Pulizia interna. Nessuna modifica visibile all'utente.",
92
- "es": "Limpieza interna. Sin cambios visibles para el usuario.",
93
- "pl": "Wewnętrzne porządki. Brak zmian widocznych dla użytkownika.",
94
- "uk": "Внутрішнє прибирання. Без помітних для користувача змін.",
95
- "zh-cn": "内部清理。对用户无可见变化。"
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.4",
3
+ "version": "0.7.5",
4
4
  "description": "ioBroker adapter for HomeWizard Energy devices with API v2",
5
5
  "author": {
6
6
  "name": "krobi",