iobroker.homewizard 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -170,6 +170,14 @@ homewizard.0.
170
170
  Placeholder for the next version (at the beginning of the line):
171
171
  ### **WORK IN PROGRESS**
172
172
  -->
173
+ ### 0.8.0 (2026-05-17)
174
+
175
+ - Internal modernization. No user-facing changes. Requires Node.js 22+.
176
+
177
+ ### 0.7.8 (2026-05-13)
178
+
179
+ - Debug log now traces every HTTPS API call and device-state lifecycle — easier diagnostics for chronic bouncing or pairing/recovery issues.
180
+
173
181
  ### 0.7.7 (2026-05-13)
174
182
 
175
183
  - Devices with chronically bad WiFi no longer flood the log: max one warn per hour when the device drops out, one info when it comes back. Full timeline stays at debug level.
@@ -189,12 +197,6 @@ homewizard.0.
189
197
  - 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.
190
198
  - Various behind-the-scenes hardening — invisible if everything was already running fine, robustness if something is unstable.
191
199
 
192
- ### 0.7.4 (2026-05-09)
193
- - 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.
194
-
195
- ### 0.7.3 (2026-05-07)
196
- - Less log spam when a device stays offline for longer periods — the initial `device unreachable` warning is enough; mDNS recovery attempts and offline-retry status now log at debug level only.
197
-
198
200
  ### Support Development
199
201
 
200
202
  This adapter is free and open source. If you find it useful, consider buying me a coffee:
@@ -40,19 +40,23 @@ class HomeWizardClient {
40
40
  agent;
41
41
  /** Override target port — only used by tests against a local stub-server. */
42
42
  port;
43
+ /** Optional logger for per-call debug-trace (request entry + response success/fail). */
44
+ log;
43
45
  /**
44
46
  * @param ip Device IP address
45
47
  * @param token Bearer token (empty string for pairing requests)
46
48
  * @param options Optional overrides — primarily for unit tests against a local TLS stub.
47
49
  * @param options.agent HTTPS agent to use; defaults to {@link HW_AGENT} (with HomeWizard CA pinning).
48
50
  * @param options.port Target port; defaults to 443.
51
+ * @param options.log Optional logger for per-call debug-trace (request/success/fail).
49
52
  */
50
53
  constructor(ip, token = "", options = {}) {
51
- var _a, _b;
54
+ var _a, _b, _c;
52
55
  this.ip = ip;
53
56
  this.token = token;
54
57
  this.agent = (_a = options.agent) != null ? _a : import_cacert.HW_AGENT;
55
58
  this.port = (_b = options.port) != null ? _b : 443;
59
+ this.log = (_c = options.log) != null ? _c : null;
56
60
  }
57
61
  /** Get device info (GET /api) */
58
62
  async getDeviceInfo() {
@@ -111,6 +115,7 @@ class HomeWizardClient {
111
115
  */
112
116
  request(method, path, body) {
113
117
  return new Promise((resolve, reject) => {
118
+ var _a;
114
119
  const bodyStr = body ? JSON.stringify(body) : void 0;
115
120
  const headers = {
116
121
  "X-Api-Version": "2"
@@ -122,6 +127,8 @@ class HomeWizardClient {
122
127
  headers["Content-Type"] = "application/json";
123
128
  headers["Content-Length"] = Buffer.byteLength(bodyStr).toString();
124
129
  }
130
+ const startMs = Date.now();
131
+ (_a = this.log) == null ? void 0 : _a.debug(`HTTPS ${method} ${path} ip=${this.ip} auth=${this.token ? "bearer" : "none"}`);
125
132
  const req = https.request(
126
133
  {
127
134
  hostname: this.ip,
@@ -137,13 +144,20 @@ class HomeWizardClient {
137
144
  res.on("error", reject);
138
145
  res.on("data", (chunk) => chunks.push(chunk));
139
146
  res.on("end", () => {
140
- var _a;
147
+ var _a2, _b, _c;
141
148
  const data = Buffer.concat(chunks).toString();
142
- if (!res.statusCode || res.statusCode >= 400) {
143
- const error = new HomeWizardApiError((_a = res.statusCode) != null ? _a : 0, data, `${method} ${path}`);
149
+ const elapsedMs = Date.now() - startMs;
150
+ const statusCode = (_a2 = res.statusCode) != null ? _a2 : 0;
151
+ if (!statusCode || statusCode >= 400) {
152
+ const snippet = data.length > 200 ? `${data.slice(0, 200)}\u2026` : data;
153
+ (_b = this.log) == null ? void 0 : _b.debug(`HTTPS ${method} ${path}: status=${statusCode} elapsed=${elapsedMs}ms body="${snippet}"`);
154
+ const error = new HomeWizardApiError(statusCode, data, `${method} ${path}`);
144
155
  reject(error);
145
156
  return;
146
157
  }
158
+ (_c = this.log) == null ? void 0 : _c.debug(
159
+ `HTTPS ${method} ${path}: status=${statusCode} elapsed=${elapsedMs}ms bytes=${data.length}`
160
+ );
147
161
  if (!data) {
148
162
  resolve(void 0);
149
163
  return;
@@ -156,7 +170,11 @@ class HomeWizardClient {
156
170
  });
157
171
  }
158
172
  );
159
- req.on("error", reject);
173
+ req.on("error", (err) => {
174
+ var _a2;
175
+ (_a2 = this.log) == null ? void 0 : _a2.debug(`HTTPS ${method} ${path}: error="${err.message}" elapsed=${Date.now() - startMs}ms`);
176
+ reject(err);
177
+ });
160
178
  req.on("timeout", () => {
161
179
  req.destroy(new Error(`Timeout: ${method} ${path}`));
162
180
  });
@@ -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 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
- "names": []
4
+ "sourcesContent": ["import * as https from \"node:https\";\nimport { HW_AGENT } from \"./cacert\";\nimport type { BatteryControl, DeviceInfo, Measurement, PairingResponse, SystemInfo } from \"./types\";\n\n/** Minimal logger surface \u2014 only debug needed for HTTPS-trace. */\nexport interface HomeWizardClientLogger {\n /**\n * Debug-log delegate.\n *\n * @param msg Log message\n */\n debug(msg: string): void;\n}\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 /** Optional logger for per-call debug-trace (request entry + response success/fail). */\n private readonly log: HomeWizardClientLogger | null;\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 * @param options.log Optional logger for per-call debug-trace (request/success/fail).\n */\n constructor(\n ip: string,\n token: string = \"\",\n options: { agent?: https.Agent; port?: number; log?: HomeWizardClientLogger } = {},\n ) {\n this.ip = ip;\n this.token = token;\n this.agent = options.agent ?? HW_AGENT;\n this.port = options.port ?? 443;\n this.log = options.log ?? null;\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 // Per mcm-Linie + reference_iobroker_logging_levels: every API call gets\n // an entry-trace so a user-debug-log shows what the adapter actually\n // tried \u2014 not just what failed. auth=bearer/none discloses presence,\n // never the token itself.\n const startMs = Date.now();\n this.log?.debug(`HTTPS ${method} ${path} ip=${this.ip} auth=${this.token ? \"bearer\" : \"none\"}`);\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 const elapsedMs = Date.now() - startMs;\n const statusCode = res.statusCode ?? 0;\n if (!statusCode || statusCode >= 400) {\n // Fail: emit detailed trace BEFORE constructing the error so the\n // body snippet is in the log even when the caller catches the\n // throw silently. 200-char snippet cap mirrors the HomeWizardApiError\n // constructor's parsing depth.\n const snippet = data.length > 200 ? `${data.slice(0, 200)}\u2026` : data;\n this.log?.debug(`HTTPS ${method} ${path}: status=${statusCode} elapsed=${elapsedMs}ms body=\"${snippet}\"`);\n const error = new HomeWizardApiError(statusCode, data, `${method} ${path}`);\n reject(error);\n return;\n }\n // Success: status + size + timing. Body itself stays out \u2014 too\n // big to log on debug, available at silly if ever wired.\n this.log?.debug(\n `HTTPS ${method} ${path}: status=${statusCode} elapsed=${elapsedMs}ms bytes=${data.length}`,\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\", err => {\n // Pre-response errors (DNS, TCP reset, TLS): log endpoint + elapsed so\n // chronic-bouncing patterns are correlatable in the per-device trace.\n this.log?.debug(`HTTPS ${method} ${path}: error=\"${err.message}\" elapsed=${Date.now() - startMs}ms`);\n reject(err);\n });\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
+ "names": ["_a"]
7
7
  }
@@ -68,6 +68,19 @@ const STATE_NAMES = {
68
68
  uk: "\u0417\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043C \u0441\u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043D\u044F",
69
69
  "zh-cn": "\u542F\u52A8\u914D\u5BF9\u6A21\u5F0F"
70
70
  },
71
+ pairingIp: {
72
+ en: "Device IP for manual pairing",
73
+ de: "Ger\xE4te-IP f\xFCr manuelles Pairing",
74
+ ru: "IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0430 \u0434\u043B\u044F \u0440\u0443\u0447\u043D\u043E\u0433\u043E \u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u044F",
75
+ pt: "IP do dispositivo para emparelhamento manual",
76
+ nl: "Apparaat-IP voor handmatig koppelen",
77
+ fr: "IP de l'appareil pour l'appairage manuel",
78
+ it: "IP del dispositivo per l'accoppiamento manuale",
79
+ es: "IP del dispositivo para emparejamiento manual",
80
+ pl: "IP urz\u0105dzenia do r\u0119cznego parowania",
81
+ uk: "IP \u043F\u0440\u0438\u0441\u0442\u0440\u043E\u044E \u0434\u043B\u044F \u0440\u0443\u0447\u043D\u043E\u0433\u043E \u0441\u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043D\u044F",
82
+ "zh-cn": "\u7528\u4E8E\u624B\u52A8\u914D\u5BF9\u7684\u8BBE\u5907 IP"
83
+ },
71
84
  // ──────── Channel names ────────
72
85
  deviceInformation: {
73
86
  en: "Device Information",