iobroker.homewizard 0.12.0 → 0.12.1

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
@@ -21,10 +21,18 @@ Real-time energy monitoring for [HomeWizard](https://www.homewizard.com) Energy
21
21
 
22
22
  ---
23
23
 
24
+ ## Sentry / Error reporting
25
+
26
+ **This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** Reporting only happens if you have enabled error reporting in the ioBroker diagnostics (**System settings → Diagnostics and error reporting**). Only an anonymous installation ID is transmitted — no name, e-mail address or IP address.
27
+
28
+ For details and how to disable it, see the [Sentry plugin documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry). Error reporting requires js-controller 3.0 or newer.
29
+
30
+ ---
31
+
24
32
  ## Requirements
25
33
 
26
34
  - **Node.js >= 22**
27
- - **ioBroker js-controller >= 7.0.7**
35
+ - **ioBroker js-controller >= 7.1.2**
28
36
  - **ioBroker Admin >= 7.8.23**
29
37
  - **HomeWizard device with API v2 support** (firmware 4.x+ with local API enabled)
30
38
 
@@ -165,18 +173,16 @@ homewizard.0.
165
173
 
166
174
  ---
167
175
 
168
- ## Sentry / Error reporting
169
-
170
- This adapter uses [Sentry](https://sentry.io) to automatically report exceptions and errors to the developer, so problems can be found and fixed quickly. Reporting only happens if you have enabled error reporting in the ioBroker diagnostics (**System settings → Diagnostics and error reporting**). Only an anonymous installation ID is transmitted — no name, e-mail address or IP address.
171
-
172
- For details and how to disable it, see the [Sentry plugin documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry). Error reporting requires js-controller 3.0 or newer.
173
-
174
176
  ## Changelog
175
177
 
176
178
  <!--
177
179
  Placeholder for the next version (at the beginning of the line):
178
180
  ### **WORK IN PROGRESS**
179
181
  -->
182
+ ### 0.12.1 (2026-06-09)
183
+
184
+ - Internal refactoring. No user-facing changes.
185
+
180
186
  ### 0.12.0 (2026-06-07)
181
187
 
182
188
  - Added optional Sentry error reporting: crashes are sent to the developer so issues get fixed faster. Active only with ioBroker diagnostics enabled; anonymous.
@@ -197,10 +203,6 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
197
203
  - User-modified device names are no longer overwritten on adapter restart or IP recovery.
198
204
  - Improved timer management for ioBroker compact mode.
199
205
 
200
- ### 0.9.2 (2026-05-23)
201
-
202
- - Changelog rewritten in user-centric style across all versions.
203
-
204
206
  [Older changelogs can be found there](CHANGELOG_OLD.md)
205
207
 
206
208
  ## Support Development
@@ -238,4 +240,4 @@ SOFTWARE.
238
240
 
239
241
  ---
240
242
 
241
- *Developed with assistance from Claude.ai*
243
+ _Developed with assistance from Claude.ai_
@@ -117,11 +117,6 @@ class HomeWizardWebSocket {
117
117
  this.destroyed = true;
118
118
  this.cleanup();
119
119
  }
120
- /** Whether the WebSocket is currently open */
121
- get isConnected() {
122
- var _a;
123
- return ((_a = this.ws) == null ? void 0 : _a.readyState) === import_ws.default.OPEN;
124
- }
125
120
  /**
126
121
  * Handle incoming WebSocket message
127
122
  *
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/websocket-client.ts"],
4
- "sourcesContent": ["import type * as https from \"node:https\";\nimport WebSocket from \"ws\";\nimport { HW_AGENT } from \"./cacert\";\nimport { isPlainObject } from \"./coerce\";\nimport type { BatteryControl, Measurement, SystemInfo } 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/** Timer dependency injection \u2014 allows adapter-managed timers instead of native ones. */\nexport interface TimerDeps {\n /** Schedule a one-shot callback */\n schedule(cb: () => void, ms: number): unknown;\n /** Cancel a one-shot timer */\n cancel(handle: unknown): void;\n /** Schedule a recurring callback */\n scheduleRepeating(cb: () => void, ms: number): unknown;\n /** Cancel a recurring timer */\n cancelRepeating(handle: unknown): void;\n}\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 a real-time system push is received (cloud/led changes etc.). Optional. */\n onSystem?: (data: SystemInfo) => void;\n /** Called when a real-time battery-group push is received (mode/permissions/target power). Optional. */\n onBattery?: (data: BatteryControl) => 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 readonly timers: TimerDeps;\n private readonly agent: https.Agent;\n /** Override target port \u2014 only used by tests against a local wss stub-server. */\n private readonly port: number;\n private ws: WebSocket | null = null;\n private destroyed = false;\n private authTimer: unknown = null;\n private pingInterval: unknown = null;\n private pongTimer: unknown = null;\n\n /**\n * @param ip Device IP address\n * @param token Bearer token\n * @param callbacks Event callbacks\n * @param timers Timer functions (use adapter-managed timers in production)\n * @param options Optional overrides \u2014 primarily for unit tests against a local wss 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(\n ip: string,\n token: string,\n callbacks: WsCallbacks,\n timers: TimerDeps,\n options: { agent?: https.Agent; port?: number } = {},\n ) {\n this.ip = ip;\n this.token = token;\n this.callbacks = callbacks;\n this.timers = timers;\n this.agent = options.agent ?? HW_AGENT;\n this.port = options.port ?? 443;\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 portSeg = this.port !== 443 ? `:${this.port}` : \"\";\n const url = `wss://${this.ip}${portSeg}/api/ws`;\n this.callbacks.log.debug(`WS connecting to ${url}`);\n\n this.ws = new WebSocket(url, {\n agent: this.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 = this.timers.schedule(() => {\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 != null) {\n this.timers.cancel(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 // Subscribe to the three real-time topics this adapter consumes (explicit, not \"*\",\n // to avoid device/user-topic noise). system/batteries push control-state changes;\n // measurement is the ~1/s data feed.\n this.callbacks.log.debug(\"WS authorized, subscribing to measurement + system + batteries\");\n this.sendRaw({ type: \"subscribe\", data: \"measurement\" });\n this.sendRaw({ type: \"subscribe\", data: \"system\" });\n this.sendRaw({ type: \"subscribe\", data: \"batteries\" });\n // Auth complete \u2014 clear auth-watchdog and start the heartbeat.\n if (this.authTimer != null) {\n this.timers.cancel(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 case \"system\":\n // isPlainObject-guarded WS payload; updateSystem re-validates every field, so the\n // boundary cast is safe (the typed shape is aspirational, not trusted).\n if (isPlainObject(parsed.data)) {\n this.callbacks.onSystem?.(parsed.data as unknown as SystemInfo);\n }\n break;\n\n case \"batteries\":\n if (isPlainObject(parsed.data)) {\n this.callbacks.onBattery?.(parsed.data as unknown as BatteryControl);\n }\n break;\n\n case \"error\": {\n const detail =\n isPlainObject(parsed.data) && typeof parsed.data.message === \"string\"\n ? parsed.data.message\n : text.substring(0, 200);\n this.callbacks.log.warn(`WS error: ${detail}`);\n break;\n }\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 = this.timers.scheduleRepeating(() => {\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 = this.timers.schedule(() => {\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 != null) {\n this.timers.cancel(this.authTimer);\n this.authTimer = null;\n }\n if (this.pingInterval != null) {\n this.timers.cancelRepeating(this.pingInterval);\n this.pingInterval = null;\n }\n if (this.pongTimer != null) {\n this.timers.cancel(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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",
4
+ "sourcesContent": ["import type * as https from \"node:https\";\nimport WebSocket from \"ws\";\nimport { HW_AGENT } from \"./cacert\";\nimport { isPlainObject } from \"./coerce\";\nimport type { BatteryControl, Measurement, SystemInfo } 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/** Timer dependency injection \u2014 allows adapter-managed timers instead of native ones. */\nexport interface TimerDeps {\n /** Schedule a one-shot callback */\n schedule(cb: () => void, ms: number): unknown;\n /** Cancel a one-shot timer */\n cancel(handle: unknown): void;\n /** Schedule a recurring callback */\n scheduleRepeating(cb: () => void, ms: number): unknown;\n /** Cancel a recurring timer */\n cancelRepeating(handle: unknown): void;\n}\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 a real-time system push is received (cloud/led changes etc.). Optional. */\n onSystem?: (data: SystemInfo) => void;\n /** Called when a real-time battery-group push is received (mode/permissions/target power). Optional. */\n onBattery?: (data: BatteryControl) => 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 readonly timers: TimerDeps;\n private readonly agent: https.Agent;\n /** Override target port \u2014 only used by tests against a local wss stub-server. */\n private readonly port: number;\n private ws: WebSocket | null = null;\n private destroyed = false;\n private authTimer: unknown = null;\n private pingInterval: unknown = null;\n private pongTimer: unknown = null;\n\n /**\n * @param ip Device IP address\n * @param token Bearer token\n * @param callbacks Event callbacks\n * @param timers Timer functions (use adapter-managed timers in production)\n * @param options Optional overrides \u2014 primarily for unit tests against a local wss 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(\n ip: string,\n token: string,\n callbacks: WsCallbacks,\n timers: TimerDeps,\n options: { agent?: https.Agent; port?: number } = {},\n ) {\n this.ip = ip;\n this.token = token;\n this.callbacks = callbacks;\n this.timers = timers;\n this.agent = options.agent ?? HW_AGENT;\n this.port = options.port ?? 443;\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 portSeg = this.port !== 443 ? `:${this.port}` : \"\";\n const url = `wss://${this.ip}${portSeg}/api/ws`;\n this.callbacks.log.debug(`WS connecting to ${url}`);\n\n this.ws = new WebSocket(url, {\n agent: this.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 = this.timers.schedule(() => {\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 != null) {\n this.timers.cancel(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 /**\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 // Subscribe to the three real-time topics this adapter consumes (explicit, not \"*\",\n // to avoid device/user-topic noise). system/batteries push control-state changes;\n // measurement is the ~1/s data feed.\n this.callbacks.log.debug(\"WS authorized, subscribing to measurement + system + batteries\");\n this.sendRaw({ type: \"subscribe\", data: \"measurement\" });\n this.sendRaw({ type: \"subscribe\", data: \"system\" });\n this.sendRaw({ type: \"subscribe\", data: \"batteries\" });\n // Auth complete \u2014 clear auth-watchdog and start the heartbeat.\n if (this.authTimer != null) {\n this.timers.cancel(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 case \"system\":\n // isPlainObject-guarded WS payload; updateSystem re-validates every field, so the\n // boundary cast is safe (the typed shape is aspirational, not trusted).\n if (isPlainObject(parsed.data)) {\n this.callbacks.onSystem?.(parsed.data as unknown as SystemInfo);\n }\n break;\n\n case \"batteries\":\n if (isPlainObject(parsed.data)) {\n this.callbacks.onBattery?.(parsed.data as unknown as BatteryControl);\n }\n break;\n\n case \"error\": {\n const detail =\n isPlainObject(parsed.data) && typeof parsed.data.message === \"string\"\n ? parsed.data.message\n : text.substring(0, 200);\n this.callbacks.log.warn(`WS error: ${detail}`);\n break;\n }\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 = this.timers.scheduleRepeating(() => {\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 = this.timers.schedule(() => {\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 != null) {\n this.timers.cancel(this.authTimer);\n this.authTimer = null;\n }\n if (this.pingInterval != null) {\n this.timers.cancelRepeating(this.pingInterval);\n this.pingInterval = null;\n }\n if (this.pongTimer != null) {\n this.timers.cancel(this.pongTimer);\n this.pongTimer = null;\n }\n }\n\n /** Close WebSocket without triggering reconnect */\n private cleanup(): void {\n this.clearTimers();\n if (this.ws) {\n this.ws.removeAllListeners();\n // Prevent uncaught errors from frames received during close\n this.ws.on(\"error\", () => {});\n this.ws.terminate();\n this.ws = null;\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["WebSocket"]
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "homewizard",
4
- "version": "0.12.0",
4
+ "version": "0.12.1",
5
5
  "news": {
6
+ "0.12.1": {
7
+ "en": "Internal refactoring. No user-facing changes.",
8
+ "de": "Interne Überarbeitung. Keine für Nutzer sichtbaren Änderungen.",
9
+ "ru": "Внутренняя переработка. Никаких изменений для пользователей.",
10
+ "pt": "Refatoração interna. Sem alterações visíveis para o utilizador.",
11
+ "nl": "Interne herstructurering. Geen zichtbare wijzigingen voor gebruikers.",
12
+ "fr": "Refactorisation interne. Aucun changement visible pour l'utilisateur.",
13
+ "it": "Refactoring interno. Nessuna modifica visibile per l'utente.",
14
+ "es": "Refactorización interna. Sin cambios visibles para el usuario.",
15
+ "pl": "Wewnętrzna refaktoryzacja. Brak zmian widocznych dla użytkownika.",
16
+ "uk": "Внутрішня переробка. Жодних змін для користувачів.",
17
+ "zh-cn": "内部重构。对用户无可见变化。"
18
+ },
6
19
  "0.12.0": {
7
20
  "en": "Added optional Sentry error reporting: crashes are sent to the developer so issues get fixed faster. Active only with ioBroker diagnostics enabled; anonymous.",
8
21
  "de": "Optionale Fehlermeldung über Sentry hinzugefügt: Abstürze werden an den Entwickler gesendet, damit Probleme schneller behoben werden. Nur aktiv bei eingeschalteter ioBroker-Diagnose; anonym.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Wewnętrzne porządki. Brak widocznych zmian dla użytkownika.",
81
94
  "uk": "Внутрішнє прибирання. Без видимих змін для користувача.",
82
95
  "zh-cn": "内部清理。无用户可见的更改。"
83
- },
84
- "0.9.0": {
85
- "en": "User-modified state names are no longer overwritten on adapter restart.",
86
- "de": "Vom Benutzer geänderte Datenpunktnamen werden beim Neustart nicht mehr überschrieben.",
87
- "ru": "Пользовательские имена состояний больше не перезаписываются при перезапуске адаптера.",
88
- "pt": "Nomes de estados modificados pelo utilizador já não são substituídos ao reiniciar o adaptador.",
89
- "nl": "Door gebruiker gewijzigde statusnamen worden niet meer overschreven bij herstart van de adapter.",
90
- "fr": "Les noms d états modifiés par l utilisateur ne sont plus écrasés au redémarrage de l adaptateur.",
91
- "it": "I nomi degli stati modificati dall utente non vengono più sovrascritti al riavvio dell adattatore.",
92
- "es": "Los nombres de estados modificados por el usuario ya no se sobrescriben al reiniciar el adaptador.",
93
- "pl": "Nazwy stanów zmienione przez użytkownika nie są już nadpisywane przy restarcie adaptera.",
94
- "uk": "Змінені користувачем назви станів більше не перезаписуються при перезапуску адаптера.",
95
- "zh-cn": "用户修改的状态名称在适配器重启时不再被覆盖。"
96
96
  }
97
97
  },
98
98
  "plugins": {
@@ -162,7 +162,7 @@
162
162
  },
163
163
  "dependencies": [
164
164
  {
165
- "js-controller": ">=7.0.7"
165
+ "js-controller": ">=7.1.2"
166
166
  }
167
167
  ],
168
168
  "globalDependencies": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.homewizard",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "ioBroker adapter for HomeWizard Energy devices with API v2",
5
5
  "author": {
6
6
  "name": "krobi",
@@ -46,10 +46,10 @@
46
46
  "@types/iobroker": "npm:@iobroker/types@^7.1.2",
47
47
  "@types/node": "^22.19.17",
48
48
  "@types/ws": "^8.18.1",
49
- "@vitest/coverage-v8": "^4.1.6",
49
+ "@vitest/coverage-v8": "^4.1.8",
50
50
  "rimraf": "^6.1.3",
51
51
  "typescript": "~6.0.3",
52
- "vitest": "^4.1.6"
52
+ "vitest": "^4.1.8"
53
53
  },
54
54
  "files": [
55
55
  "admin",