iobroker.homewizard 0.7.3 → 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 +19 -11
- package/build/lib/coerce.js +24 -0
- package/build/lib/coerce.js.map +2 -2
- package/build/lib/connection-utils.js +3 -1
- package/build/lib/connection-utils.js.map +2 -2
- package/build/lib/discovery.js +20 -8
- package/build/lib/discovery.js.map +2 -2
- package/build/lib/homewizard-client.js +5 -1
- package/build/lib/homewizard-client.js.map +2 -2
- package/build/lib/types.js.map +1 -1
- package/build/lib/websocket-client.js +78 -2
- package/build/lib/websocket-client.js.map +2 -2
- package/build/main.js +137 -82
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +1 -1
- package/build/lib/i18n-logs.js +0 -384
- package/build/lib/i18n-logs.js.map +0 -7
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` —
|
|
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
|
|
@@ -165,6 +166,23 @@ homewizard.0.
|
|
|
165
166
|
---
|
|
166
167
|
|
|
167
168
|
## Changelog
|
|
169
|
+
<!--
|
|
170
|
+
Placeholder for the next version (at the beginning of the line):
|
|
171
|
+
### **WORK IN PROGRESS**
|
|
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
|
+
|
|
183
|
+
### 0.7.4 (2026-05-09)
|
|
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.
|
|
185
|
+
|
|
168
186
|
### 0.7.3 (2026-05-07)
|
|
169
187
|
- Less log spam when a device stays offline for longer periods — the initial `device unreachable` warning is enough; mDNS recovery attempts and offline-retry status now log at debug level only.
|
|
170
188
|
|
|
@@ -175,16 +193,6 @@ homewizard.0.
|
|
|
175
193
|
- WiFi signal strength is now reported in dBm (was incorrectly labelled `dB`).
|
|
176
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.
|
|
177
195
|
|
|
178
|
-
### 0.7.0 (2026-05-06)
|
|
179
|
-
- Adapter texts now follow your ioBroker system language: datapoint names, descriptions, dropdown values for `tariff` and `battery.mode`, and user-visible logs in 11 languages (EN, DE, RU, PT, NL, FR, IT, ES, PL, UK, ZH-CN).
|
|
180
|
-
- Power-quality and Belgian capacity-tariff datapoints carry inline descriptions — hover in admin to see what each one means.
|
|
181
|
-
- Battery inputs are checked up-front: an unknown `battery.mode` or malformed `battery.permissions` JSON gives a clear warning instead of a cryptic error.
|
|
182
|
-
- Minimum requirements: Node.js 22 and ioBroker Admin 7.8.23.
|
|
183
|
-
|
|
184
|
-
### 0.6.7 (2026-05-01)
|
|
185
|
-
- Internal cleanup. No user-facing changes.
|
|
186
|
-
- Documentation: rewrote release notes for v0.6.0–v0.6.6 in user-friendly style across all languages.
|
|
187
|
-
|
|
188
196
|
### Support Development
|
|
189
197
|
|
|
190
198
|
This adapter is free and open source. If you find it useful, consider buying me a coffee:
|
package/build/lib/coerce.js
CHANGED
|
@@ -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
|
});
|
package/build/lib/coerce.js.map
CHANGED
|
@@ -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
|
}
|
|
@@ -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,
|
|
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
|
}
|
package/build/lib/discovery.js
CHANGED
|
@@ -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 = (
|
|
89
|
-
const serial = (
|
|
90
|
-
const name = (
|
|
91
|
-
const apiVersion = txt
|
|
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,
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAsC;
|
|
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
|
-
|
|
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
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;AACvB,oBAAyB;AAIlB,MAAM,iBAAiB;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASjB,YAAY,IAAY,QAAgB,IAAI,UAAkD,CAAC,GAAG;AAnBpG;AAoBI,SAAK,KAAK;AACV,SAAK,QAAQ;AACb,SAAK,SAAQ,aAAQ,UAAR,YAAiB;AAC9B,SAAK,QAAO,aAAQ,SAAR,YAAgB;AAAA,EAC9B;AAAA;AAAA,EAGA,MAAM,gBAAqC;AACzC,WAAO,KAAK,QAAoB,OAAO,MAAM;AAAA,EAC/C;AAAA;AAAA,EAGA,MAAM,iBAA2C;AAC/C,
|
|
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
|
}
|
package/build/lib/types.js.map
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAsB;AACtB,oBAAyB;AACzB,oBAA8B;
|
|
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
|
}
|