iobroker.homewizard 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -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/i18n-states.js +11 -0
- package/build/lib/i18n-states.js.map +2 -2
- package/build/lib/state-manager.js +45 -11
- package/build/lib/state-manager.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 +109 -15
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +1 -1
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
|
|
@@ -169,6 +170,20 @@ 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.6 (2026-05-12)
|
|
174
|
+
|
|
175
|
+
- The battery mode dropdown and the tariff state no longer crash the admin with "Error in GUI" when opened.
|
|
176
|
+
|
|
177
|
+
### 0.7.5 (2026-05-10)
|
|
178
|
+
- Half-dead connections are now detected and torn down — fixes cases where the device stopped responding but the adapter still showed "connected" with stale measurement values.
|
|
179
|
+
- The auth handshake now has a 45-second timeout — devices that accept the TCP connection but never reply to the auth protocol no longer hang forever.
|
|
180
|
+
- IP recovery and manual re-pair after factory reset no longer leave a dangling connection from before — switching to a new IP just works.
|
|
181
|
+
- Battery endpoint errors are no longer fully swallowed: 404 stays silent (device has no battery), other errors are visible in the debug log instead of being silently dropped.
|
|
182
|
+
- Manual pairing IP is validated as IPv4 up front — invalid input fails fast with a warning instead of a silent 60-second pairing timeout.
|
|
183
|
+
- 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.
|
|
184
|
+
- Pairing supports multiple devices in one 60-second window: button-press additional devices and they are added one after the other instead of the session ending after the first.
|
|
185
|
+
- Various behind-the-scenes hardening — invisible if everything was already running fine, robustness if something is unstable.
|
|
186
|
+
|
|
172
187
|
### 0.7.4 (2026-05-09)
|
|
173
188
|
- 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
189
|
|
|
@@ -178,16 +193,6 @@ homewizard.0.
|
|
|
178
193
|
### 0.7.2 (2026-05-06)
|
|
179
194
|
- Internal hardening: stricter number parsing for sensor inputs, parallel state writes, code split for testability, 38 new tests covering the HTTPS client. No user-facing changes.
|
|
180
195
|
|
|
181
|
-
### 0.7.1 (2026-05-06)
|
|
182
|
-
- WiFi signal strength is now reported in dBm (was incorrectly labelled `dB`).
|
|
183
|
-
- 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
|
-
|
|
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:
|
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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;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,UAAM,SAAS,MAAM,KAAK,QAAyB,QAAQ,aAAa;AAAA,MACtE,MAAM;AAAA,IACR,CAAC;AAGD,QAAI,CAAC,UAAU,OAAO,OAAO,UAAU,YAAY,OAAO,MAAM,WAAW,GAAG;AAC5E,YAAM,IAAI,mBAAmB,KAAK,KAAK,UAAU,MAAM,GAAG,uCAAuC;AAAA,IACnG;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,iBAAuC;AAC3C,WAAO,KAAK,QAAqB,OAAO,kBAAkB;AAAA,EAC5D;AAAA;AAAA,EAGA,MAAM,YAAiC;AACrC,WAAO,KAAK,QAAoB,OAAO,aAAa;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAAU,UAAoD;AAClE,WAAO,KAAK,QAAoB,OAAO,eAAe,QAAQ;AAAA,EAChE;AAAA;AAAA,EAGA,MAAM,SAAwB;AAC5B,UAAM,KAAK,QAAQ,OAAO,oBAAoB;AAAA,EAChD;AAAA;AAAA,EAGA,MAAM,WAA0B;AAC9B,UAAM,KAAK,QAAQ,OAAO,sBAAsB;AAAA,EAClD;AAAA;AAAA,EAGA,MAAM,eAAwC;AAC5C,WAAO,KAAK,QAAwB,OAAO,gBAAgB;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,UAA4D;AAC7E,WAAO,KAAK,QAAwB,OAAO,kBAAkB,QAAQ;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,QAAW,QAAgB,MAAc,MAA4B;AAC3E,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,UAAU,OAAO,KAAK,UAAU,IAAI,IAAI;AAC9C,YAAM,UAAkC;AAAA,QACtC,iBAAiB;AAAA,MACnB;AAEA,UAAI,KAAK,OAAO;AACd,gBAAQ,gBAAgB,UAAU,KAAK,KAAK;AAAA,MAC9C;AACA,UAAI,SAAS;AACX,gBAAQ,cAAc,IAAI;AAC1B,gBAAQ,gBAAgB,IAAI,OAAO,WAAW,OAAO,EAAE,SAAS;AAAA,MAClE;AAEA,YAAM,MAAM,MAAM;AAAA,QAChB;AAAA,UACE,UAAU,KAAK;AAAA,UACf,MAAM,KAAK;AAAA,UACX;AAAA,UACA;AAAA,UACA;AAAA,UACA,OAAO,KAAK;AAAA,UACZ,SAAS;AAAA,QACX;AAAA,QACA,SAAO;AACL,gBAAM,SAAmB,CAAC;AAC1B,cAAI,GAAG,SAAS,MAAM;AACtB,cAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,cAAI,GAAG,OAAO,MAAM;AAzH9B;AA0HY,kBAAM,OAAO,OAAO,OAAO,MAAM,EAAE,SAAS;AAC5C,gBAAI,CAAC,IAAI,cAAc,IAAI,cAAc,KAAK;AAC5C,oBAAM,QAAQ,IAAI,oBAAmB,SAAI,eAAJ,YAAkB,GAAG,MAAM,GAAG,MAAM,IAAI,IAAI,EAAE;AACnF,qBAAO,KAAK;AACZ;AAAA,YACF;AACA,gBAAI,CAAC,MAAM;AACT,sBAAQ,MAAc;AACtB;AAAA,YACF;AACA,gBAAI;AACF,sBAAQ,KAAK,MAAM,IAAI,CAAM;AAAA,YAC/B,QAAQ;AACN,qBAAO,IAAI,MAAM,qBAAqB,MAAM,IAAI,IAAI,KAAK,KAAK,UAAU,GAAG,GAAG,CAAC,EAAE,CAAC;AAAA,YACpF;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAEA,UAAI,GAAG,SAAS,MAAM;AACtB,UAAI,GAAG,WAAW,MAAM;AACtB,YAAI,QAAQ,IAAI,MAAM,YAAY,MAAM,IAAI,IAAI,EAAE,CAAC;AAAA,MACrD,CAAC;AAED,UAAI,SAAS;AACX,YAAI,MAAM,OAAO;AAAA,MACnB;AACA,UAAI,IAAI;AAAA,IACV,CAAC;AAAA,EACH;AACF;AAGO,MAAM,2BAA2B,MAAM;AAAA,EACnC;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOT,YAAY,YAAoB,MAAc,SAAiB;AApKjE;AAqKI,QAAI,YAAY;AAChB,QAAI,cAAc;AAClB,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,mBAAY,wBAAO,UAAP,mBAAc,SAAd,YAAsB,OAAO,UAA7B,YAAsC;AAClD,qBAAc,wBAAO,UAAP,mBAAc,gBAAd,aAA6B,YAAO,UAAP,mBAAc,SAA3C,YAAmD;AAAA,IACnE,QAAQ;AAAA,IAER;AACA,UAAM,GAAG,OAAO,UAAU,UAAU,WAAM,WAAW,EAAE;AACvD,SAAK,OAAO;AACZ,SAAK,aAAa;AAClB,SAAK,YAAY;AAAA,EACnB;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/lib/i18n-states.js
CHANGED
|
@@ -21,6 +21,7 @@ __export(i18n_states_exports, {
|
|
|
21
21
|
STATE_DESCS: () => STATE_DESCS,
|
|
22
22
|
STATE_LABELS: () => STATE_LABELS,
|
|
23
23
|
STATE_NAMES: () => STATE_NAMES,
|
|
24
|
+
resolveLabel: () => resolveLabel,
|
|
24
25
|
tDesc: () => tDesc,
|
|
25
26
|
tLabel: () => tLabel,
|
|
26
27
|
tName: () => tName
|
|
@@ -1424,11 +1425,21 @@ function tDesc(key) {
|
|
|
1424
1425
|
function tLabel(key) {
|
|
1425
1426
|
return STATE_LABELS[key];
|
|
1426
1427
|
}
|
|
1428
|
+
function resolveLabel(key, lang) {
|
|
1429
|
+
var _a, _b;
|
|
1430
|
+
const obj = STATE_LABELS[key];
|
|
1431
|
+
if (typeof obj === "string") {
|
|
1432
|
+
return obj;
|
|
1433
|
+
}
|
|
1434
|
+
const dict = obj;
|
|
1435
|
+
return (_b = (_a = dict[lang]) != null ? _a : dict.en) != null ? _b : key;
|
|
1436
|
+
}
|
|
1427
1437
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1428
1438
|
0 && (module.exports = {
|
|
1429
1439
|
STATE_DESCS,
|
|
1430
1440
|
STATE_LABELS,
|
|
1431
1441
|
STATE_NAMES,
|
|
1442
|
+
resolveLabel,
|
|
1432
1443
|
tDesc,
|
|
1433
1444
|
tLabel,
|
|
1434
1445
|
tName
|