iobroker.homewizard 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -13
- package/build/lib/types.js.map +1 -1
- package/build/main.js +135 -12
- package/build/main.js.map +2 -2
- package/io-package.json +25 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,13 +154,16 @@ homewizard.0.
|
|
|
154
154
|
|
|
155
155
|
## Changelog
|
|
156
156
|
|
|
157
|
+
### 0.3.5 (2026-04-05)
|
|
158
|
+
- Fix log spam: error deduplication by category, not by context
|
|
159
|
+
- REST fallback stops on network errors, system poll only for connected devices
|
|
160
|
+
|
|
157
161
|
### 0.3.4 (2026-04-05)
|
|
158
|
-
- mDNS only
|
|
159
|
-
-
|
|
162
|
+
- mDNS only during pairing, automatic IP recovery on connection loss
|
|
163
|
+
- Device marked offline when unreachable (no endless reconnect loop)
|
|
160
164
|
|
|
161
165
|
### 0.3.3 (2026-04-05)
|
|
162
166
|
- Fix mDNS pairing not finding devices (browser restart on pairing start)
|
|
163
|
-
- Improve log messages during pairing
|
|
164
167
|
|
|
165
168
|
### 0.3.2 (2026-04-05)
|
|
166
169
|
- Improve Admin UI and README with detailed configuration guide
|
|
@@ -170,19 +173,10 @@ homewizard.0.
|
|
|
170
173
|
|
|
171
174
|
### 0.3.0 (2026-04-05)
|
|
172
175
|
- Store device config in device objects (no adapter restart on pairing/remove)
|
|
173
|
-
- Fix datapoint issues (startPairing, pairingIp, button states)
|
|
174
176
|
- Reconnect workflow: IP change handling, auth backoff, error dedup
|
|
175
177
|
|
|
176
178
|
### 0.2.0 (2026-04-05)
|
|
177
|
-
- Fix WebSocket auth format and mDNS service type
|
|
178
|
-
- Add editable IP column in Admin UI (empty = mDNS, set = fixed IP)
|
|
179
|
-
- Add manual IP pairing for networks without mDNS
|
|
180
|
-
|
|
181
|
-
### 0.1.3 (2026-04-04)
|
|
182
|
-
- mDNS discovery runs permanently, automatic IP updates
|
|
183
|
-
|
|
184
|
-
### 0.1.2 (2026-04-04)
|
|
185
|
-
- Bundle HomeWizard CA certificate for proper TLS validation
|
|
179
|
+
- Fix WebSocket auth format and mDNS service type, manual IP pairing
|
|
186
180
|
|
|
187
181
|
Older changelog: [CHANGELOG.md](CHANGELOG.md)
|
|
188
182
|
|
package/build/lib/types.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/types.ts"],
|
|
4
|
-
"sourcesContent": ["import type { HomeWizardWebSocket } from \"./websocket-client\";\n\n/** Persisted config for a single paired device (stored in device object native) */\nexport interface DeviceConfig {\n /** Bearer token (encrypted via adapter.encrypt) */\n token: string;\n /** Product type (e.g. HWE-P1) */\n productType: string;\n /** Device serial number */\n serial: string;\n /** Human-readable product name */\n productName: string;\n /** Fixed IP address (only when manually set, empty = mDNS) */\n ip?: string;\n}\n\n/** Response from GET /api */\nexport interface DeviceInfo {\n /** Product name */\n product_name: string;\n /** Product type identifier */\n product_type: string;\n /** Device serial number */\n serial: string;\n /** Firmware version string */\n firmware_version: string;\n /** API version string */\n api_version: string;\n}\n\n/** Response from POST /api/user (pairing) */\nexport interface PairingResponse {\n /** Bearer token for API access */\n token: string;\n}\n\n/** Measurement data from GET /api/measurement or WebSocket push */\nexport interface Measurement {\n /** Unique meter identifier */\n unique_id?: string;\n /** Protocol version */\n protocol_version?: number;\n /** Meter model */\n meter_model?: string;\n /** Measurement timestamp */\n timestamp?: string;\n /** Active tariff number */\n tariff?: number;\n\n /** Total energy import in kWh */\n energy_import_kwh?: number;\n /** Energy import tariff 1 */\n energy_import_t1_kwh?: number;\n /** Energy import tariff 2 */\n energy_import_t2_kwh?: number;\n /** Energy import tariff 3 */\n energy_import_t3_kwh?: number;\n /** Energy import tariff 4 */\n energy_import_t4_kwh?: number;\n /** Total energy export in kWh */\n energy_export_kwh?: number;\n /** Energy export tariff 1 */\n energy_export_t1_kwh?: number;\n /** Energy export tariff 2 */\n energy_export_t2_kwh?: number;\n /** Energy export tariff 3 */\n energy_export_t3_kwh?: number;\n /** Energy export tariff 4 */\n energy_export_t4_kwh?: number;\n\n /** Total active power in W */\n power_w?: number;\n /** Active power phase 1 */\n power_l1_w?: number;\n /** Active power phase 2 */\n power_l2_w?: number;\n /** Active power phase 3 */\n power_l3_w?: number;\n\n /** Voltage (single phase) */\n voltage_v?: number;\n /** Voltage phase 1 */\n voltage_l1_v?: number;\n /** Voltage phase 2 */\n voltage_l2_v?: number;\n /** Voltage phase 3 */\n voltage_l3_v?: number;\n\n /** Current (single phase) */\n current_a?: number;\n /** Current phase 1 */\n current_l1_a?: number;\n /** Current phase 2 */\n current_l2_a?: number;\n /** Current phase 3 */\n current_l3_a?: number;\n\n /** Grid frequency in Hz */\n frequency_hz?: number;\n\n /** Voltage sag count phase 1 */\n voltage_sag_l1_count?: number;\n /** Voltage sag count phase 2 */\n voltage_sag_l2_count?: number;\n /** Voltage sag count phase 3 */\n voltage_sag_l3_count?: number;\n /** Voltage swell count phase 1 */\n voltage_swell_l1_count?: number;\n /** Voltage swell count phase 2 */\n voltage_swell_l2_count?: number;\n /** Voltage swell count phase 3 */\n voltage_swell_l3_count?: number;\n /** Any power fail count */\n any_power_fail_count?: number;\n /** Long power fail count */\n long_power_fail_count?: number;\n\n /** Average power over 15 min (Belgium) */\n average_power_15m_w?: number;\n /** Monthly power peak (Belgium) */\n monthly_power_peak_w?: number;\n /** Monthly power peak timestamp (Belgium) */\n monthly_power_peak_timestamp?: string;\n\n /** Apparent current */\n apparent_current_a?: number;\n /** Reactive current */\n reactive_current_a?: number;\n /** Apparent power in VA */\n apparent_power_va?: number;\n /** Reactive power in var */\n reactive_power_var?: number;\n /** Power factor */\n power_factor?: number;\n /** Apparent current phase 1 */\n apparent_current_l1_a?: number;\n /** Apparent current phase 2 */\n apparent_current_l2_a?: number;\n /** Apparent current phase 3 */\n apparent_current_l3_a?: number;\n /** Reactive current phase 1 */\n reactive_current_l1_a?: number;\n /** Reactive current phase 2 */\n reactive_current_l2_a?: number;\n /** Reactive current phase 3 */\n reactive_current_l3_a?: number;\n /** Apparent power phase 1 */\n apparent_power_l1_va?: number;\n /** Apparent power phase 2 */\n apparent_power_l2_va?: number;\n /** Apparent power phase 3 */\n apparent_power_l3_va?: number;\n /** Reactive power phase 1 */\n reactive_power_l1_var?: number;\n /** Reactive power phase 2 */\n reactive_power_l2_var?: number;\n /** Reactive power phase 3 */\n reactive_power_l3_var?: number;\n /** Power factor phase 1 */\n power_factor_l1?: number;\n /** Power factor phase 2 */\n power_factor_l2?: number;\n /** Power factor phase 3 */\n power_factor_l3?: number;\n\n /** Battery state of charge in percent */\n state_of_charge_pct?: number;\n /** Battery charge cycles */\n cycles?: number;\n\n /** External meters (gas, water, heat) */\n external?: ExternalMeter[];\n}\n\n/** External meter attached to P1 (gas, water, heat) */\nexport interface ExternalMeter {\n /** Unique meter identifier */\n unique_id: string;\n /** Meter type */\n type:\n | \"gas_meter\"\n | \"heat_meter\"\n | \"warm_water_meter\"\n | \"water_meter\"\n | \"inlet_heat_meter\";\n /** Last reading timestamp */\n timestamp: string;\n /** Meter reading value */\n value: number;\n /** Measurement unit */\n unit: string;\n}\n\n/** System info from GET /api/system */\nexport interface SystemInfo {\n /** WiFi SSID */\n wifi_ssid: string;\n /** WiFi signal strength in dB */\n wifi_rssi_db: number;\n /** Uptime in seconds */\n uptime_s: number;\n /** Cloud communication enabled */\n cloud_enabled: boolean;\n /** Status LED brightness 0-100% */\n status_led_brightness_pct: number;\n /** Legacy API v1 enabled */\n api_v1_enabled?: boolean;\n}\n\n/** Battery control from GET /api/batteries */\nexport interface BatteryControl {\n /** Battery mode */\n mode: \"zero\" | \"to_full\" | \"standby\";\n /** Battery permissions */\n permissions?: string[];\n /** Number of connected batteries */\n battery_count?: number;\n /** Current combined power in W */\n power_w?: number;\n /** Target power in W */\n target_power_w?: number;\n /** Maximum consumption in W */\n max_consumption_w?: number;\n /** Maximum production in W */\n max_production_w?: number;\n}\n\n/** WebSocket message envelope */\nexport interface WsMessage {\n /** Message type */\n type: string;\n /** Message data payload (string for auth/subscribe, object for measurement) */\n data?: unknown;\n}\n\n/** Device discovered via mDNS */\nexport interface DiscoveredDevice {\n /** Device IP address */\n ip: string;\n /** Product type from mDNS TXT record or device info */\n productType: string;\n /** Serial number from mDNS name */\n serial: string;\n /** Human-readable name */\n name: string;\n}\n\n/** Connection state for a single device */\nexport interface DeviceConnection {\n /** Device config */\n config: DeviceConfig;\n /** Current IP address (from mDNS or stored fixed IP) */\n ip: string;\n /** WebSocket client instance (if connected) */\n wsClient: HomeWizardWebSocket | null;\n /** Whether WS is authenticated */\n wsAuthenticated: boolean;\n /** REST fallback polling timer */\n pollTimer: ioBroker.Interval | undefined;\n /** Reconnect timer */\n reconnectTimer: ioBroker.Timeout | undefined;\n /** Consecutive WS failures for backoff */\n wsFailCount: number;\n /** Consecutive auth failures */\n authFailCount: number;\n /** Last error code for dedup */\n lastErrorCode: string;\n}\n"],
|
|
4
|
+
"sourcesContent": ["import type { HomeWizardWebSocket } from \"./websocket-client\";\n\n/** Persisted config for a single paired device (stored in device object native) */\nexport interface DeviceConfig {\n /** Bearer token (encrypted via adapter.encrypt) */\n token: string;\n /** Product type (e.g. HWE-P1) */\n productType: string;\n /** Device serial number */\n serial: string;\n /** Human-readable product name */\n productName: string;\n /** Fixed IP address (only when manually set, empty = mDNS) */\n ip?: string;\n}\n\n/** Response from GET /api */\nexport interface DeviceInfo {\n /** Product name */\n product_name: string;\n /** Product type identifier */\n product_type: string;\n /** Device serial number */\n serial: string;\n /** Firmware version string */\n firmware_version: string;\n /** API version string */\n api_version: string;\n}\n\n/** Response from POST /api/user (pairing) */\nexport interface PairingResponse {\n /** Bearer token for API access */\n token: string;\n}\n\n/** Measurement data from GET /api/measurement or WebSocket push */\nexport interface Measurement {\n /** Unique meter identifier */\n unique_id?: string;\n /** Protocol version */\n protocol_version?: number;\n /** Meter model */\n meter_model?: string;\n /** Measurement timestamp */\n timestamp?: string;\n /** Active tariff number */\n tariff?: number;\n\n /** Total energy import in kWh */\n energy_import_kwh?: number;\n /** Energy import tariff 1 */\n energy_import_t1_kwh?: number;\n /** Energy import tariff 2 */\n energy_import_t2_kwh?: number;\n /** Energy import tariff 3 */\n energy_import_t3_kwh?: number;\n /** Energy import tariff 4 */\n energy_import_t4_kwh?: number;\n /** Total energy export in kWh */\n energy_export_kwh?: number;\n /** Energy export tariff 1 */\n energy_export_t1_kwh?: number;\n /** Energy export tariff 2 */\n energy_export_t2_kwh?: number;\n /** Energy export tariff 3 */\n energy_export_t3_kwh?: number;\n /** Energy export tariff 4 */\n energy_export_t4_kwh?: number;\n\n /** Total active power in W */\n power_w?: number;\n /** Active power phase 1 */\n power_l1_w?: number;\n /** Active power phase 2 */\n power_l2_w?: number;\n /** Active power phase 3 */\n power_l3_w?: number;\n\n /** Voltage (single phase) */\n voltage_v?: number;\n /** Voltage phase 1 */\n voltage_l1_v?: number;\n /** Voltage phase 2 */\n voltage_l2_v?: number;\n /** Voltage phase 3 */\n voltage_l3_v?: number;\n\n /** Current (single phase) */\n current_a?: number;\n /** Current phase 1 */\n current_l1_a?: number;\n /** Current phase 2 */\n current_l2_a?: number;\n /** Current phase 3 */\n current_l3_a?: number;\n\n /** Grid frequency in Hz */\n frequency_hz?: number;\n\n /** Voltage sag count phase 1 */\n voltage_sag_l1_count?: number;\n /** Voltage sag count phase 2 */\n voltage_sag_l2_count?: number;\n /** Voltage sag count phase 3 */\n voltage_sag_l3_count?: number;\n /** Voltage swell count phase 1 */\n voltage_swell_l1_count?: number;\n /** Voltage swell count phase 2 */\n voltage_swell_l2_count?: number;\n /** Voltage swell count phase 3 */\n voltage_swell_l3_count?: number;\n /** Any power fail count */\n any_power_fail_count?: number;\n /** Long power fail count */\n long_power_fail_count?: number;\n\n /** Average power over 15 min (Belgium) */\n average_power_15m_w?: number;\n /** Monthly power peak (Belgium) */\n monthly_power_peak_w?: number;\n /** Monthly power peak timestamp (Belgium) */\n monthly_power_peak_timestamp?: string;\n\n /** Apparent current */\n apparent_current_a?: number;\n /** Reactive current */\n reactive_current_a?: number;\n /** Apparent power in VA */\n apparent_power_va?: number;\n /** Reactive power in var */\n reactive_power_var?: number;\n /** Power factor */\n power_factor?: number;\n /** Apparent current phase 1 */\n apparent_current_l1_a?: number;\n /** Apparent current phase 2 */\n apparent_current_l2_a?: number;\n /** Apparent current phase 3 */\n apparent_current_l3_a?: number;\n /** Reactive current phase 1 */\n reactive_current_l1_a?: number;\n /** Reactive current phase 2 */\n reactive_current_l2_a?: number;\n /** Reactive current phase 3 */\n reactive_current_l3_a?: number;\n /** Apparent power phase 1 */\n apparent_power_l1_va?: number;\n /** Apparent power phase 2 */\n apparent_power_l2_va?: number;\n /** Apparent power phase 3 */\n apparent_power_l3_va?: number;\n /** Reactive power phase 1 */\n reactive_power_l1_var?: number;\n /** Reactive power phase 2 */\n reactive_power_l2_var?: number;\n /** Reactive power phase 3 */\n reactive_power_l3_var?: number;\n /** Power factor phase 1 */\n power_factor_l1?: number;\n /** Power factor phase 2 */\n power_factor_l2?: number;\n /** Power factor phase 3 */\n power_factor_l3?: number;\n\n /** Battery state of charge in percent */\n state_of_charge_pct?: number;\n /** Battery charge cycles */\n cycles?: number;\n\n /** External meters (gas, water, heat) */\n external?: ExternalMeter[];\n}\n\n/** External meter attached to P1 (gas, water, heat) */\nexport interface ExternalMeter {\n /** Unique meter identifier */\n unique_id: string;\n /** Meter type */\n type:\n | \"gas_meter\"\n | \"heat_meter\"\n | \"warm_water_meter\"\n | \"water_meter\"\n | \"inlet_heat_meter\";\n /** Last reading timestamp */\n timestamp: string;\n /** Meter reading value */\n value: number;\n /** Measurement unit */\n unit: string;\n}\n\n/** System info from GET /api/system */\nexport interface SystemInfo {\n /** WiFi SSID */\n wifi_ssid: string;\n /** WiFi signal strength in dB */\n wifi_rssi_db: number;\n /** Uptime in seconds */\n uptime_s: number;\n /** Cloud communication enabled */\n cloud_enabled: boolean;\n /** Status LED brightness 0-100% */\n status_led_brightness_pct: number;\n /** Legacy API v1 enabled */\n api_v1_enabled?: boolean;\n}\n\n/** Battery control from GET /api/batteries */\nexport interface BatteryControl {\n /** Battery mode */\n mode: \"zero\" | \"to_full\" | \"standby\";\n /** Battery permissions */\n permissions?: string[];\n /** Number of connected batteries */\n battery_count?: number;\n /** Current combined power in W */\n power_w?: number;\n /** Target power in W */\n target_power_w?: number;\n /** Maximum consumption in W */\n max_consumption_w?: number;\n /** Maximum production in W */\n max_production_w?: number;\n}\n\n/** WebSocket message envelope */\nexport interface WsMessage {\n /** Message type */\n type: string;\n /** Message data payload (string for auth/subscribe, object for measurement) */\n data?: unknown;\n}\n\n/** Device discovered via mDNS */\nexport interface DiscoveredDevice {\n /** Device IP address */\n ip: string;\n /** Product type from mDNS TXT record or device info */\n productType: string;\n /** Serial number from mDNS name */\n serial: string;\n /** Human-readable name */\n name: string;\n}\n\n/** Connection state for a single device */\nexport interface DeviceConnection {\n /** Device config */\n config: DeviceConfig;\n /** Current IP address (from mDNS or stored fixed IP) */\n ip: string;\n /** WebSocket client instance (if connected) */\n wsClient: HomeWizardWebSocket | null;\n /** Whether WS is authenticated */\n wsAuthenticated: boolean;\n /** REST fallback polling timer */\n pollTimer: ioBroker.Interval | undefined;\n /** Reconnect timer */\n reconnectTimer: ioBroker.Timeout | undefined;\n /** Consecutive WS failures for backoff */\n wsFailCount: number;\n /** Consecutive auth failures */\n authFailCount: number;\n /** Last error code for dedup */\n lastErrorCode: string;\n /** Whether mDNS IP recovery was already attempted (prevents endless loop) */\n ipRecoveryDone: boolean;\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -33,6 +33,8 @@ const WS_RECONNECT_MAX_MS = 3e5;
|
|
|
33
33
|
const REST_POLL_MS = 1e4;
|
|
34
34
|
const SYSTEM_POLL_MS = 6e4;
|
|
35
35
|
const MAX_AUTH_FAILURES = 3;
|
|
36
|
+
const WS_FAILURES_BEFORE_MDNS = 3;
|
|
37
|
+
const IP_RECOVERY_TIMEOUT_MS = 6e4;
|
|
36
38
|
class HomeWizard extends utils.Adapter {
|
|
37
39
|
stateManager;
|
|
38
40
|
discovery = null;
|
|
@@ -40,6 +42,7 @@ class HomeWizard extends utils.Adapter {
|
|
|
40
42
|
pairingTimer = void 0;
|
|
41
43
|
pairingPollTimer = void 0;
|
|
42
44
|
systemPollTimer = void 0;
|
|
45
|
+
ipRecoveryTimer = void 0;
|
|
43
46
|
isPairing = false;
|
|
44
47
|
pairingManualIp = "";
|
|
45
48
|
discoveredDuringPairing = [];
|
|
@@ -95,7 +98,8 @@ class HomeWizard extends utils.Adapter {
|
|
|
95
98
|
reconnectTimer: void 0,
|
|
96
99
|
wsFailCount: 0,
|
|
97
100
|
authFailCount: 0,
|
|
98
|
-
lastErrorCode: ""
|
|
101
|
+
lastErrorCode: "",
|
|
102
|
+
ipRecoveryDone: false
|
|
99
103
|
};
|
|
100
104
|
this.connections.set(key, conn);
|
|
101
105
|
if (conn.ip) {
|
|
@@ -213,6 +217,9 @@ class HomeWizard extends utils.Adapter {
|
|
|
213
217
|
if (this.systemPollTimer) {
|
|
214
218
|
this.clearInterval(this.systemPollTimer);
|
|
215
219
|
}
|
|
220
|
+
if (this.ipRecoveryTimer) {
|
|
221
|
+
this.clearTimeout(this.ipRecoveryTimer);
|
|
222
|
+
}
|
|
216
223
|
(_a = this.discovery) == null ? void 0 : _a.stop();
|
|
217
224
|
for (const conn of this.connections.values()) {
|
|
218
225
|
(_b = conn.wsClient) == null ? void 0 : _b.close();
|
|
@@ -296,6 +303,7 @@ class HomeWizard extends utils.Adapter {
|
|
|
296
303
|
await this.setStateAsync("startPairing", { val: false, ack: true });
|
|
297
304
|
this.isPairing = true;
|
|
298
305
|
this.discoveredDuringPairing = [];
|
|
306
|
+
this.stopIpRecovery();
|
|
299
307
|
const ipState = await this.getStateAsync("pairingIp");
|
|
300
308
|
this.pairingManualIp = (ipState == null ? void 0 : ipState.val) ? String(ipState.val).trim() : "";
|
|
301
309
|
await this.setStateAsync("pairingIp", { val: "", ack: true });
|
|
@@ -360,7 +368,8 @@ class HomeWizard extends utils.Adapter {
|
|
|
360
368
|
reconnectTimer: void 0,
|
|
361
369
|
wsFailCount: 0,
|
|
362
370
|
authFailCount: 0,
|
|
363
|
-
lastErrorCode: ""
|
|
371
|
+
lastErrorCode: "",
|
|
372
|
+
ipRecoveryDone: false
|
|
364
373
|
};
|
|
365
374
|
this.connections.set(key, conn);
|
|
366
375
|
void this.initDevice(conn);
|
|
@@ -398,6 +407,72 @@ class HomeWizard extends utils.Adapter {
|
|
|
398
407
|
this.pairingTimer = void 0;
|
|
399
408
|
}
|
|
400
409
|
}
|
|
410
|
+
/** Start mDNS to find devices that changed IP */
|
|
411
|
+
startIpRecovery() {
|
|
412
|
+
if (this.discovery || this.isPairing) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.log.info("Device unreachable \u2014 searching for new IP via mDNS");
|
|
416
|
+
this.discovery = new import_discovery.HomeWizardDiscovery(this.log);
|
|
417
|
+
this.discovery.start((discovered) => {
|
|
418
|
+
for (const conn of this.connections.values()) {
|
|
419
|
+
if (conn.config.serial !== discovered.serial) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (discovered.ip === conn.ip || conn.wsAuthenticated) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
this.log.info(
|
|
426
|
+
`${conn.config.productName}: found at new IP ${discovered.ip} (was ${conn.ip})`
|
|
427
|
+
);
|
|
428
|
+
conn.ip = discovered.ip;
|
|
429
|
+
conn.config.ip = discovered.ip;
|
|
430
|
+
conn.wsFailCount = 0;
|
|
431
|
+
void this.saveDeviceToObject(conn.config);
|
|
432
|
+
if (conn.reconnectTimer) {
|
|
433
|
+
this.clearTimeout(conn.reconnectTimer);
|
|
434
|
+
conn.reconnectTimer = void 0;
|
|
435
|
+
}
|
|
436
|
+
if (conn.pollTimer) {
|
|
437
|
+
this.clearInterval(conn.pollTimer);
|
|
438
|
+
conn.pollTimer = void 0;
|
|
439
|
+
}
|
|
440
|
+
this.connectWebSocket(conn);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
this.ipRecoveryTimer = this.setTimeout(() => {
|
|
445
|
+
this.ipRecoveryTimer = void 0;
|
|
446
|
+
this.stopIpRecovery();
|
|
447
|
+
for (const conn of this.connections.values()) {
|
|
448
|
+
if (!conn.wsAuthenticated && conn.wsFailCount > 0) {
|
|
449
|
+
conn.ipRecoveryDone = true;
|
|
450
|
+
if (conn.reconnectTimer) {
|
|
451
|
+
this.clearTimeout(conn.reconnectTimer);
|
|
452
|
+
conn.reconnectTimer = void 0;
|
|
453
|
+
}
|
|
454
|
+
if (conn.pollTimer) {
|
|
455
|
+
this.clearInterval(conn.pollTimer);
|
|
456
|
+
conn.pollTimer = void 0;
|
|
457
|
+
}
|
|
458
|
+
this.log.warn(
|
|
459
|
+
`${conn.config.productName}: device offline \u2014 check network or re-pair with new IP`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}, IP_RECOVERY_TIMEOUT_MS);
|
|
464
|
+
}
|
|
465
|
+
/** Stop mDNS IP recovery */
|
|
466
|
+
stopIpRecovery() {
|
|
467
|
+
if (this.ipRecoveryTimer) {
|
|
468
|
+
this.clearTimeout(this.ipRecoveryTimer);
|
|
469
|
+
this.ipRecoveryTimer = void 0;
|
|
470
|
+
}
|
|
471
|
+
if (this.discovery && !this.isPairing) {
|
|
472
|
+
this.discovery.stop();
|
|
473
|
+
this.discovery = null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
401
476
|
/**
|
|
402
477
|
* Initialize a newly discovered device — fetch info and connect WebSocket
|
|
403
478
|
*
|
|
@@ -430,6 +505,9 @@ class HomeWizard extends utils.Adapter {
|
|
|
430
505
|
if (conn.authFailCount >= MAX_AUTH_FAILURES) {
|
|
431
506
|
return;
|
|
432
507
|
}
|
|
508
|
+
if (conn.wsFailCount >= WS_FAILURES_BEFORE_MDNS && !conn.ipRecoveryDone) {
|
|
509
|
+
this.startIpRecovery();
|
|
510
|
+
}
|
|
433
511
|
const key = this.stateManager.devicePrefix(conn.config);
|
|
434
512
|
const wsClient = new import_websocket_client.HomeWizardWebSocket(conn.ip, conn.config.token, {
|
|
435
513
|
onMeasurement: (data) => {
|
|
@@ -439,12 +517,21 @@ class HomeWizard extends utils.Adapter {
|
|
|
439
517
|
conn.wsAuthenticated = true;
|
|
440
518
|
conn.wsFailCount = 0;
|
|
441
519
|
conn.authFailCount = 0;
|
|
520
|
+
conn.ipRecoveryDone = false;
|
|
442
521
|
void this.stateManager.setDeviceConnected(conn.config, true);
|
|
443
522
|
this.updateGlobalConnection();
|
|
444
523
|
if (conn.pollTimer) {
|
|
445
524
|
this.clearInterval(conn.pollTimer);
|
|
446
525
|
conn.pollTimer = void 0;
|
|
447
526
|
}
|
|
527
|
+
if (this.discovery && !this.isPairing) {
|
|
528
|
+
const allConnected = Array.from(this.connections.values()).every(
|
|
529
|
+
(c) => c.wsAuthenticated
|
|
530
|
+
);
|
|
531
|
+
if (allConnected) {
|
|
532
|
+
this.stopIpRecovery();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
448
535
|
if (conn.lastErrorCode) {
|
|
449
536
|
this.log.info(`${conn.config.productName}: connection restored`);
|
|
450
537
|
conn.lastErrorCode = "";
|
|
@@ -490,7 +577,9 @@ class HomeWizard extends utils.Adapter {
|
|
|
490
577
|
wsClient.connect();
|
|
491
578
|
}
|
|
492
579
|
/**
|
|
493
|
-
* Start REST polling as fallback when WebSocket is down
|
|
580
|
+
* Start REST polling as fallback when WebSocket is down.
|
|
581
|
+
* Stops automatically on network errors (device unreachable) —
|
|
582
|
+
* WS reconnect + IP recovery handle the recovery path.
|
|
494
583
|
*
|
|
495
584
|
* @param conn Device connection
|
|
496
585
|
*/
|
|
@@ -507,6 +596,10 @@ class HomeWizard extends utils.Adapter {
|
|
|
507
596
|
} catch (err) {
|
|
508
597
|
this.logDeviceError(conn, "rest", err);
|
|
509
598
|
await this.stateManager.setDeviceConnected(conn.config, false);
|
|
599
|
+
if (this.classifyError(err) === "NETWORK" && conn.pollTimer) {
|
|
600
|
+
this.clearInterval(conn.pollTimer);
|
|
601
|
+
conn.pollTimer = void 0;
|
|
602
|
+
}
|
|
510
603
|
}
|
|
511
604
|
this.updateGlobalConnection();
|
|
512
605
|
}, REST_POLL_MS);
|
|
@@ -514,7 +607,7 @@ class HomeWizard extends utils.Adapter {
|
|
|
514
607
|
/** Poll system info for all connected devices */
|
|
515
608
|
async pollAllSystemInfo() {
|
|
516
609
|
for (const conn of this.connections.values()) {
|
|
517
|
-
if (conn.ip &&
|
|
610
|
+
if (conn.ip && conn.wsAuthenticated) {
|
|
518
611
|
await this.pollSystemInfo(conn);
|
|
519
612
|
}
|
|
520
613
|
}
|
|
@@ -595,23 +688,53 @@ class HomeWizard extends utils.Adapter {
|
|
|
595
688
|
return void 0;
|
|
596
689
|
}
|
|
597
690
|
/**
|
|
598
|
-
*
|
|
691
|
+
* Classify an error for deduplication and log-level decisions.
|
|
692
|
+
* Returns a stable category string regardless of error message details.
|
|
693
|
+
*
|
|
694
|
+
* @param err The error to classify
|
|
695
|
+
*/
|
|
696
|
+
classifyError(err) {
|
|
697
|
+
if (err instanceof import_homewizard_client.HomeWizardApiError) {
|
|
698
|
+
if (err.errorCode === "user:unauthorized") {
|
|
699
|
+
return "AUTH";
|
|
700
|
+
}
|
|
701
|
+
return `HTTP_${err.statusCode}`;
|
|
702
|
+
}
|
|
703
|
+
if (err instanceof Error) {
|
|
704
|
+
const code = err.code;
|
|
705
|
+
if (code === "ECONNREFUSED" || code === "EHOSTUNREACH" || code === "ENOTFOUND" || code === "ECONNRESET" || code === "ENETUNREACH" || code === "EAI_AGAIN") {
|
|
706
|
+
return "NETWORK";
|
|
707
|
+
}
|
|
708
|
+
if (code === "ETIMEDOUT" || err.message.includes("Timeout")) {
|
|
709
|
+
return "TIMEOUT";
|
|
710
|
+
}
|
|
711
|
+
return code || "UNKNOWN";
|
|
712
|
+
}
|
|
713
|
+
return "UNKNOWN";
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Log device error with deduplication (based on error category, not context).
|
|
717
|
+
* First occurrence of a new error category logs as warn, repeats as debug.
|
|
599
718
|
*
|
|
600
719
|
* @param conn Device connection
|
|
601
|
-
* @param context Error context
|
|
720
|
+
* @param context Error context (for debug messages only)
|
|
602
721
|
* @param err Error object
|
|
603
722
|
*/
|
|
604
723
|
logDeviceError(conn, context, err) {
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
|
|
724
|
+
const errorCode = this.classifyError(err);
|
|
725
|
+
const isRepeat = errorCode === conn.lastErrorCode;
|
|
726
|
+
conn.lastErrorCode = errorCode;
|
|
727
|
+
if (isRepeat) {
|
|
608
728
|
this.log.debug(
|
|
609
|
-
`${conn.config.productName}
|
|
729
|
+
`${conn.config.productName} ${context}: ${err instanceof Error ? err.message : String(err)}`
|
|
730
|
+
);
|
|
731
|
+
} else if (errorCode === "NETWORK") {
|
|
732
|
+
this.log.warn(
|
|
733
|
+
`${conn.config.productName}: device unreachable \u2014 will keep retrying`
|
|
610
734
|
);
|
|
611
735
|
} else {
|
|
612
|
-
conn.lastErrorCode = key;
|
|
613
736
|
this.log.warn(
|
|
614
|
-
`${conn.config.productName}
|
|
737
|
+
`${conn.config.productName} ${context}: ${err instanceof Error ? err.message : String(err)}`
|
|
615
738
|
);
|
|
616
739
|
}
|
|
617
740
|
}
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { HomeWizardDiscovery } from \"./lib/discovery\";\nimport { HomeWizardApiError, HomeWizardClient } from \"./lib/homewizard-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport type {\n DeviceConfig,\n DeviceConnection,\n DiscoveredDevice,\n Measurement,\n} from \"./lib/types\";\nimport { HomeWizardWebSocket } from \"./lib/websocket-client\";\n\n/** Pairing timeout in milliseconds (60 seconds) */\nconst PAIRING_TIMEOUT_MS = 60_000;\n/** Pairing poll interval in milliseconds */\nconst PAIRING_POLL_MS = 2_000;\n/** WebSocket reconnect base delay in milliseconds */\nconst WS_RECONNECT_BASE_MS = 5_000;\n/** Maximum WebSocket reconnect delay in milliseconds */\nconst WS_RECONNECT_MAX_MS = 300_000;\n/** REST fallback poll interval in milliseconds */\nconst REST_POLL_MS = 10_000;\n/** System info poll interval in milliseconds */\nconst SYSTEM_POLL_MS = 60_000;\n/** Max auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\n\nclass HomeWizard extends utils.Adapter {\n private stateManager!: StateManager;\n private discovery: HomeWizardDiscovery | null = null;\n private readonly connections = new Map<string, DeviceConnection>();\n private pairingTimer: ioBroker.Timeout | undefined = undefined;\n private pairingPollTimer: ioBroker.Interval | undefined = undefined;\n private systemPollTimer: ioBroker.Interval | undefined = undefined;\n private isPairing = false;\n private pairingManualIp = \"\";\n private discoveredDuringPairing: DiscoveredDevice[] = [];\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"homewizard\" });\n this.on(\"ready\", () => this.onReady());\n this.on(\"stateChange\", (id, state) => this.onStateChange(id, state));\n this.on(\"unload\", (callback) => this.onUnload(callback));\n }\n\n /** Adapter started */\n private async onReady(): Promise<void> {\n this.stateManager = new StateManager(this);\n\n // Create pairing states\n await this.extendObjectAsync(\"pairingIp\", {\n type: \"state\",\n common: {\n name: \"Device IP for manual pairing\",\n type: \"string\",\n role: \"text\",\n read: true,\n write: true,\n def: \"\",\n },\n native: {},\n });\n\n // Reset pairing states on start (in case previous run was killed mid-pairing)\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\n // Subscribe to pairing button and writable device states\n await this.subscribeStatesAsync(\"startPairing\");\n await this.subscribeStatesAsync(\"*.system.reboot\");\n await this.subscribeStatesAsync(\"*.system.identify\");\n await this.subscribeStatesAsync(\"*.system.cloud_enabled\");\n await this.subscribeStatesAsync(\"*.system.status_led_brightness_pct\");\n await this.subscribeStatesAsync(\"*.system.api_v1_enabled\");\n await this.subscribeStatesAsync(\"*.battery.mode\");\n await this.subscribeStatesAsync(\"*.battery.permissions\");\n await this.subscribeStatesAsync(\"*.remove\");\n\n // Load devices from device objects (not from adapter config)\n const devices = await this.loadDevicesFromObjects();\n if (devices.length === 0) {\n this.log.info(\n \"No devices configured \u2014 set 'startPairing' to true to add a device\",\n );\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n }\n\n // Create connection entries for all configured devices\n for (const device of devices) {\n const key = this.stateManager.devicePrefix(device);\n await this.stateManager.createDeviceStates(device);\n const conn: DeviceConnection = {\n config: device,\n ip: device.ip || \"\",\n wsClient: null,\n wsAuthenticated: false,\n pollTimer: undefined,\n reconnectTimer: undefined,\n wsFailCount: 0,\n authFailCount: 0,\n lastErrorCode: \"\",\n };\n this.connections.set(key, conn);\n\n // If we have a stored IP, connect immediately\n if (conn.ip) {\n this.log.debug(`Using stored IP ${conn.ip} for ${device.productName}`);\n void this.initDevice(conn);\n }\n }\n\n // Periodic system info poll\n this.systemPollTimer = this.setInterval(() => {\n void this.pollAllSystemInfo();\n }, SYSTEM_POLL_MS);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Load device configs from existing device objects\n * Tokens are stored encrypted in device object native\n */\n private async loadDevicesFromObjects(): Promise<DeviceConfig[]> {\n const devices: DeviceConfig[] = [];\n\n // Also migrate from old adapter config if devices exist there\n const oldDevices: DeviceConfig[] =\n ((this.config as Record<string, unknown>).devices as DeviceConfig[]) ||\n [];\n if (oldDevices.length > 0) {\n this.log.debug(\n `Migrating ${oldDevices.length} device(s) from adapter config to device objects`,\n );\n for (const device of oldDevices) {\n await this.saveDeviceToObject(device);\n }\n // Clear old config (this triggers one restart, but only during migration)\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, {\n native: { devices: [] },\n });\n return oldDevices;\n }\n\n // Read device objects from our namespace\n const objects = await this.getAdapterObjectsAsync();\n for (const [id, obj] of Object.entries(objects)) {\n if (obj.type !== \"device\") {\n continue;\n }\n const native = obj.native as Record<string, string> | undefined;\n if (!native?.encryptedToken || !native.serial) {\n continue;\n }\n const localId = id.replace(`${this.namespace}.`, \"\");\n this.log.debug(`Loading device from object: ${localId}`);\n const token = this.decrypt(native.encryptedToken);\n devices.push({\n token,\n productType: native.productType || \"unknown\",\n serial: native.serial,\n productName: native.productName || native.productType || \"unknown\",\n ...(native.ip ? { ip: native.ip } : {}),\n });\n }\n\n return devices;\n }\n\n /**\n * Save device config to its device object native (encrypted token)\n *\n * @param config Device configuration to save\n */\n private async saveDeviceToObject(config: DeviceConfig): Promise<void> {\n const prefix = this.stateManager.devicePrefix(config);\n const encryptedToken = this.encrypt(config.token);\n await this.extendObjectAsync(prefix, {\n type: \"device\",\n common: { name: config.productName || config.productType },\n native: {\n encryptedToken,\n productType: config.productType,\n serial: config.serial,\n productName: config.productName,\n ...(config.ip ? { ip: config.ip } : {}),\n },\n });\n }\n\n /**\n * Remove device config from its device object\n *\n * @param config Device configuration to remove\n */\n private async removeDeviceFromObject(config: DeviceConfig): Promise<void> {\n await this.stateManager.removeDevice(config);\n }\n\n /**\n * Handle a discovered device from mDNS (only active during pairing)\n *\n * @param discovered Discovered device info\n */\n private onDeviceDiscovered(discovered: DiscoveredDevice): void {\n // Skip already paired devices\n const existing = Array.from(this.connections.values()).find(\n (c) => c.config.serial === discovered.serial,\n );\n if (existing) {\n return;\n }\n\n // Skip duplicates\n if (\n this.discoveredDuringPairing.find((d) => d.serial === discovered.serial)\n ) {\n return;\n }\n\n this.discoveredDuringPairing.push(discovered);\n this.log.info(\n `Found ${discovered.name} (${discovered.productType}) at ${discovered.ip} \u2014 press the button on the device to pair`,\n );\n }\n\n /**\n * Adapter stopping \u2014 MUST be synchronous\n *\n * @param callback Completion callback\n */\n private onUnload(callback: () => void): void {\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n }\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n }\n if (this.systemPollTimer) {\n this.clearInterval(this.systemPollTimer);\n }\n\n this.discovery?.stop();\n\n for (const conn of this.connections.values()) {\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n }\n }\n this.connections.clear();\n\n void this.setState(\"info.connection\", { val: false, ack: true });\n callback();\n }\n\n /**\n * Handle state changes\n *\n * @param id State ID\n * @param state State value\n */\n private async onStateChange(\n id: string,\n state: ioBroker.State | null | undefined,\n ): Promise<void> {\n if (!state || state.ack) {\n return;\n }\n\n // Pairing button\n if (id.endsWith(\".startPairing\")) {\n if (state.val) {\n await this.startPairing();\n }\n return;\n }\n\n // Remove device button\n if (id.endsWith(\".remove\")) {\n if (state.val) {\n await this.removeDevice(id);\n }\n return;\n }\n\n // Find which device this state belongs to\n const conn = this.findConnectionForState(id);\n if (!conn || !conn.ip) {\n return;\n }\n\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n\n try {\n if (id.endsWith(\".system.reboot\")) {\n this.log.info(`Rebooting ${conn.config.productName} (${conn.ip})`);\n await client.reboot();\n } else if (id.endsWith(\".system.identify\")) {\n await client.identify();\n } else if (id.endsWith(\".system.cloud_enabled\")) {\n await client.setSystem({ cloud_enabled: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".system.status_led_brightness_pct\")) {\n await client.setSystem({\n status_led_brightness_pct: Number(state.val),\n });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".system.api_v1_enabled\")) {\n await client.setSystem({ api_v1_enabled: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.mode\")) {\n await client.setBatteries({\n mode: String(state.val) as \"zero\" | \"to_full\" | \"standby\",\n });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.permissions\")) {\n const perms = JSON.parse(String(state.val));\n await client.setBatteries({ permissions: perms });\n await this.setStateAsync(id, { val: state.val, ack: true });\n }\n } catch (err) {\n this.log.warn(\n `Failed to set ${id}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n /** Start pairing mode \u2014 discover devices and attempt to pair */\n private async startPairing(): Promise<void> {\n if (this.isPairing) {\n this.log.debug(\"Pairing already active\");\n return;\n }\n\n // Reset startPairing immediately so it doesn't survive a restart\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n\n this.isPairing = true;\n this.discoveredDuringPairing = [];\n\n // Check if manual IP is set, then clear pairingIp immediately\n const ipState = await this.getStateAsync(\"pairingIp\");\n this.pairingManualIp = ipState?.val ? String(ipState.val).trim() : \"\";\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\n if (this.pairingManualIp) {\n this.log.info(\n `Pairing mode enabled for ${this.pairingManualIp} \u2014 press the button on your HomeWizard device now (60 seconds timeout)`,\n );\n // Add as discovered device immediately\n this.discoveredDuringPairing.push({\n ip: this.pairingManualIp,\n productType: \"unknown\",\n serial: \"unknown\",\n name: this.pairingManualIp,\n });\n } else {\n this.log.info(\n \"Pairing mode enabled \u2014 searching for devices via mDNS, press the button on your HomeWizard device now (60 seconds timeout)\",\n );\n // Restart mDNS browser to trigger fresh query \u2014 already-cached devices\n // won't be re-announced otherwise and pairing would never find them\n if (!this.discovery) {\n this.discovery = new HomeWizardDiscovery(this.log);\n }\n this.discovery.start((discovered) => {\n this.onDeviceDiscovered(discovered);\n });\n }\n\n // Poll discovered devices for pairing\n this.pairingPollTimer = this.setInterval(() => {\n void this.pollPairing();\n }, PAIRING_POLL_MS);\n\n // Timeout pairing\n this.pairingTimer = this.setTimeout(() => {\n this.stopPairing();\n this.log.info(\n \"Pairing mode automatically disabled after 60 seconds timeout\",\n );\n }, PAIRING_TIMEOUT_MS);\n }\n\n /** Poll all discovered devices to attempt pairing */\n private async pollPairing(): Promise<void> {\n for (const device of this.discoveredDuringPairing) {\n try {\n const client = new HomeWizardClient(device.ip);\n const result = await client.requestPairing();\n\n // Success! Button was pressed\n this.log.info(\n `Successfully paired with ${device.name} (${device.productType}) at ${device.ip} \u2014 connecting...`,\n );\n\n // Get device info\n const authedClient = new HomeWizardClient(device.ip, result.token);\n const info = await authedClient.getDeviceInfo();\n\n const deviceConfig: DeviceConfig = {\n token: result.token,\n productType: info.product_type,\n serial: info.serial,\n productName: info.product_name,\n ip: device.ip,\n };\n\n // Save to device object (no adapter restart!)\n await this.saveDeviceToObject(deviceConfig);\n await this.stateManager.createDeviceStates(deviceConfig);\n\n // Create connection and connect\n const key = this.stateManager.devicePrefix(deviceConfig);\n const conn: DeviceConnection = {\n config: deviceConfig,\n ip: device.ip,\n wsClient: null,\n wsAuthenticated: false,\n pollTimer: undefined,\n reconnectTimer: undefined,\n wsFailCount: 0,\n authFailCount: 0,\n lastErrorCode: \"\",\n };\n this.connections.set(key, conn);\n void this.initDevice(conn);\n\n // Remove from discovery list\n this.discoveredDuringPairing = this.discoveredDuringPairing.filter(\n (d) => d.serial !== info.serial,\n );\n\n this.stopPairing();\n this.updateGlobalConnection();\n return;\n } catch (err) {\n // 403 = button not pressed yet \u2014 expected, keep polling\n if (err instanceof HomeWizardApiError && err.statusCode === 403) {\n continue;\n }\n this.log.debug(\n `Pairing poll error for ${device.ip}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n /** Stop pairing mode */\n private stopPairing(): void {\n this.isPairing = false;\n this.pairingManualIp = \"\";\n this.discoveredDuringPairing = [];\n\n // Stop mDNS \u2014 only needed during pairing\n if (this.discovery) {\n this.discovery.stop();\n this.discovery = null;\n }\n\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n this.pairingPollTimer = undefined;\n }\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n this.pairingTimer = undefined;\n }\n }\n\n /**\n * Initialize a newly discovered device \u2014 fetch info and connect WebSocket\n *\n * @param conn Device connection with IP set\n */\n private async initDevice(conn: DeviceConnection): Promise<void> {\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const info = await client.getDeviceInfo();\n const key = this.stateManager.devicePrefix(conn.config);\n await this.setStateAsync(`${key}.info.firmware`, {\n val: info.firmware_version,\n ack: true,\n });\n } catch (err) {\n this.logDeviceError(conn, \"init\", err);\n }\n\n this.connectWebSocket(conn);\n void this.pollSystemInfo(conn);\n }\n\n /**\n * Connect WebSocket for a device\n *\n * @param conn Device connection\n */\n private connectWebSocket(conn: DeviceConnection): void {\n if (!conn.ip) {\n return; // No IP yet \u2014 wait for mDNS\n }\n\n // Stop reconnecting if auth keeps failing\n if (conn.authFailCount >= MAX_AUTH_FAILURES) {\n return;\n }\n\n const key = this.stateManager.devicePrefix(conn.config);\n\n const wsClient = new HomeWizardWebSocket(conn.ip, conn.config.token, {\n onMeasurement: (data: Measurement) => {\n void this.stateManager.updateMeasurement(conn.config, data);\n },\n onConnected: () => {\n conn.wsAuthenticated = true;\n conn.wsFailCount = 0;\n conn.authFailCount = 0;\n void this.stateManager.setDeviceConnected(conn.config, true);\n this.updateGlobalConnection();\n\n // Stop REST fallback if active\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n\n // Log restoration if we had errors before\n if (conn.lastErrorCode) {\n this.log.info(`${conn.config.productName}: connection restored`);\n conn.lastErrorCode = \"\";\n }\n\n this.log.debug(\n `WebSocket connected to ${conn.config.productName} (${conn.ip})`,\n );\n },\n onDisconnected: (error?: Error) => {\n conn.wsAuthenticated = false;\n conn.wsClient = null;\n void this.stateManager.setDeviceConnected(conn.config, false);\n this.updateGlobalConnection();\n\n if (error) {\n this.logDeviceError(conn, \"ws\", error);\n }\n\n // Check if this was an auth failure\n if (\n error instanceof HomeWizardApiError &&\n error.errorCode === \"user:unauthorized\"\n ) {\n conn.authFailCount++;\n if (conn.authFailCount >= MAX_AUTH_FAILURES) {\n this.log.warn(\n `${conn.config.productName}: token invalid \u2014 re-pair device to fix`,\n );\n return;\n }\n }\n\n // Start REST fallback\n this.startRestFallback(conn);\n\n // Schedule reconnect with exponential backoff\n conn.wsFailCount++;\n const delay = Math.min(\n WS_RECONNECT_BASE_MS * Math.pow(2, conn.wsFailCount - 1),\n WS_RECONNECT_MAX_MS,\n );\n this.log.debug(\n `${key}: WS reconnect in ${delay / 1000}s (attempt ${conn.wsFailCount})`,\n );\n\n conn.reconnectTimer = this.setTimeout(() => {\n conn.reconnectTimer = undefined;\n this.connectWebSocket(conn);\n }, delay);\n },\n log: this.log,\n });\n\n conn.wsClient = wsClient;\n wsClient.connect();\n }\n\n /**\n * Start REST polling as fallback when WebSocket is down\n *\n * @param conn Device connection\n */\n private startRestFallback(conn: DeviceConnection): void {\n if (conn.pollTimer || !conn.ip) {\n return;\n }\n\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n\n conn.pollTimer = this.setInterval(async () => {\n try {\n const data = await client.getMeasurement();\n await this.stateManager.updateMeasurement(conn.config, data);\n await this.stateManager.setDeviceConnected(conn.config, true);\n } catch (err) {\n this.logDeviceError(conn, \"rest\", err);\n await this.stateManager.setDeviceConnected(conn.config, false);\n }\n this.updateGlobalConnection();\n }, REST_POLL_MS);\n }\n\n /** Poll system info for all connected devices */\n private async pollAllSystemInfo(): Promise<void> {\n for (const conn of this.connections.values()) {\n // Only poll devices that have an IP and are connected or at least reachable\n if (conn.ip && (conn.wsAuthenticated || conn.pollTimer)) {\n await this.pollSystemInfo(conn);\n }\n }\n }\n\n /**\n * Poll system info for a single device\n *\n * @param conn Device connection\n */\n private async pollSystemInfo(conn: DeviceConnection): Promise<void> {\n if (!conn.ip) {\n return;\n }\n\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const system = await client.getSystem();\n await this.stateManager.updateSystem(conn.config, system);\n\n // Also poll battery if device supports it\n try {\n const battery = await client.getBatteries();\n // Only create battery states if batteries are actually connected\n if (battery.battery_count && battery.battery_count > 0) {\n await this.stateManager.updateBattery(conn.config, battery);\n }\n } catch {\n // Device may not support batteries \u2014 that's fine\n }\n } catch (err) {\n this.logDeviceError(conn, \"system\", err);\n }\n }\n\n /** Update global info.connection based on all device states */\n private updateGlobalConnection(): void {\n const anyConnected = Array.from(this.connections.values()).some(\n (c) => c.wsAuthenticated,\n );\n void this.setStateAsync(\"info.connection\", {\n val: anyConnected,\n ack: true,\n });\n }\n\n /**\n * Remove a device \u2014 disconnect, delete states and object\n *\n * @param stateId The remove state ID\n */\n private async removeDevice(stateId: string): Promise<void> {\n const conn = this.findConnectionForState(stateId);\n if (!conn) {\n return;\n }\n\n const key = this.stateManager.devicePrefix(conn.config);\n this.log.info(\n `Removing device ${conn.config.productName} (${conn.config.serial})`,\n );\n\n // Disconnect\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n }\n this.connections.delete(key);\n\n // Delete device object and all states (no adapter restart!)\n await this.removeDeviceFromObject(conn.config);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Find connection for a state ID\n *\n * @param stateId Full state ID\n */\n private findConnectionForState(\n stateId: string,\n ): DeviceConnection | undefined {\n const localId = stateId.replace(`${this.namespace}.`, \"\");\n for (const conn of this.connections.values()) {\n const prefix = this.stateManager.devicePrefix(conn.config);\n if (localId.startsWith(`${prefix}.`)) {\n return conn;\n }\n }\n return undefined;\n }\n\n /**\n * Log device error with deduplication\n *\n * @param conn Device connection\n * @param context Error context\n * @param err Error object\n */\n private logDeviceError(\n conn: DeviceConnection,\n context: string,\n err: unknown,\n ): void {\n const code =\n err instanceof HomeWizardApiError\n ? err.errorCode\n : err instanceof Error\n ? err.message\n : \"unknown\";\n const key = `${context}:${code}`;\n\n if (conn.lastErrorCode === key) {\n // Same error as last time \u2014 debug only\n this.log.debug(\n `${conn.config.productName} (${conn.ip}) ${context}: ${code}`,\n );\n } else {\n conn.lastErrorCode = key;\n this.log.warn(\n `${conn.config.productName} (${conn.ip}) ${context}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) =>\n new HomeWizard(options);\n} else {\n (() => new HomeWizard())();\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { HomeWizardDiscovery } from \"./lib/discovery\";\nimport { HomeWizardApiError, HomeWizardClient } from \"./lib/homewizard-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport type {\n DeviceConfig,\n DeviceConnection,\n DiscoveredDevice,\n Measurement,\n} from \"./lib/types\";\nimport { HomeWizardWebSocket } from \"./lib/websocket-client\";\n\n/** Pairing timeout in milliseconds (60 seconds) */\nconst PAIRING_TIMEOUT_MS = 60_000;\n/** Pairing poll interval in milliseconds */\nconst PAIRING_POLL_MS = 2_000;\n/** WebSocket reconnect base delay in milliseconds */\nconst WS_RECONNECT_BASE_MS = 5_000;\n/** Maximum WebSocket reconnect delay in milliseconds */\nconst WS_RECONNECT_MAX_MS = 300_000;\n/** REST fallback poll interval in milliseconds */\nconst REST_POLL_MS = 10_000;\n/** System info poll interval in milliseconds */\nconst SYSTEM_POLL_MS = 60_000;\n/** Max auth failures before giving up */\nconst MAX_AUTH_FAILURES = 3;\n/** WS failures before starting mDNS IP recovery */\nconst WS_FAILURES_BEFORE_MDNS = 3;\n/** mDNS IP recovery timeout in milliseconds */\nconst IP_RECOVERY_TIMEOUT_MS = 60_000;\n\nclass HomeWizard extends utils.Adapter {\n private stateManager!: StateManager;\n private discovery: HomeWizardDiscovery | null = null;\n private readonly connections = new Map<string, DeviceConnection>();\n private pairingTimer: ioBroker.Timeout | undefined = undefined;\n private pairingPollTimer: ioBroker.Interval | undefined = undefined;\n private systemPollTimer: ioBroker.Interval | undefined = undefined;\n private ipRecoveryTimer: ioBroker.Timeout | undefined = undefined;\n private isPairing = false;\n private pairingManualIp = \"\";\n private discoveredDuringPairing: DiscoveredDevice[] = [];\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"homewizard\" });\n this.on(\"ready\", () => this.onReady());\n this.on(\"stateChange\", (id, state) => this.onStateChange(id, state));\n this.on(\"unload\", (callback) => this.onUnload(callback));\n }\n\n /** Adapter started */\n private async onReady(): Promise<void> {\n this.stateManager = new StateManager(this);\n\n // Create pairing states\n await this.extendObjectAsync(\"pairingIp\", {\n type: \"state\",\n common: {\n name: \"Device IP for manual pairing\",\n type: \"string\",\n role: \"text\",\n read: true,\n write: true,\n def: \"\",\n },\n native: {},\n });\n\n // Reset pairing states on start (in case previous run was killed mid-pairing)\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\n // Subscribe to pairing button and writable device states\n await this.subscribeStatesAsync(\"startPairing\");\n await this.subscribeStatesAsync(\"*.system.reboot\");\n await this.subscribeStatesAsync(\"*.system.identify\");\n await this.subscribeStatesAsync(\"*.system.cloud_enabled\");\n await this.subscribeStatesAsync(\"*.system.status_led_brightness_pct\");\n await this.subscribeStatesAsync(\"*.system.api_v1_enabled\");\n await this.subscribeStatesAsync(\"*.battery.mode\");\n await this.subscribeStatesAsync(\"*.battery.permissions\");\n await this.subscribeStatesAsync(\"*.remove\");\n\n // Load devices from device objects (not from adapter config)\n const devices = await this.loadDevicesFromObjects();\n if (devices.length === 0) {\n this.log.info(\n \"No devices configured \u2014 set 'startPairing' to true to add a device\",\n );\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n }\n\n // Create connection entries for all configured devices\n for (const device of devices) {\n const key = this.stateManager.devicePrefix(device);\n await this.stateManager.createDeviceStates(device);\n const conn: DeviceConnection = {\n config: device,\n ip: device.ip || \"\",\n wsClient: null,\n wsAuthenticated: false,\n pollTimer: undefined,\n reconnectTimer: undefined,\n wsFailCount: 0,\n authFailCount: 0,\n lastErrorCode: \"\",\n ipRecoveryDone: false,\n };\n this.connections.set(key, conn);\n\n // If we have a stored IP, connect immediately\n if (conn.ip) {\n this.log.debug(`Using stored IP ${conn.ip} for ${device.productName}`);\n void this.initDevice(conn);\n }\n }\n\n // Periodic system info poll\n this.systemPollTimer = this.setInterval(() => {\n void this.pollAllSystemInfo();\n }, SYSTEM_POLL_MS);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Load device configs from existing device objects\n * Tokens are stored encrypted in device object native\n */\n private async loadDevicesFromObjects(): Promise<DeviceConfig[]> {\n const devices: DeviceConfig[] = [];\n\n // Also migrate from old adapter config if devices exist there\n const oldDevices: DeviceConfig[] =\n ((this.config as Record<string, unknown>).devices as DeviceConfig[]) ||\n [];\n if (oldDevices.length > 0) {\n this.log.debug(\n `Migrating ${oldDevices.length} device(s) from adapter config to device objects`,\n );\n for (const device of oldDevices) {\n await this.saveDeviceToObject(device);\n }\n // Clear old config (this triggers one restart, but only during migration)\n await this.extendForeignObjectAsync(`system.adapter.${this.namespace}`, {\n native: { devices: [] },\n });\n return oldDevices;\n }\n\n // Read device objects from our namespace\n const objects = await this.getAdapterObjectsAsync();\n for (const [id, obj] of Object.entries(objects)) {\n if (obj.type !== \"device\") {\n continue;\n }\n const native = obj.native as Record<string, string> | undefined;\n if (!native?.encryptedToken || !native.serial) {\n continue;\n }\n const localId = id.replace(`${this.namespace}.`, \"\");\n this.log.debug(`Loading device from object: ${localId}`);\n const token = this.decrypt(native.encryptedToken);\n devices.push({\n token,\n productType: native.productType || \"unknown\",\n serial: native.serial,\n productName: native.productName || native.productType || \"unknown\",\n ...(native.ip ? { ip: native.ip } : {}),\n });\n }\n\n return devices;\n }\n\n /**\n * Save device config to its device object native (encrypted token)\n *\n * @param config Device configuration to save\n */\n private async saveDeviceToObject(config: DeviceConfig): Promise<void> {\n const prefix = this.stateManager.devicePrefix(config);\n const encryptedToken = this.encrypt(config.token);\n await this.extendObjectAsync(prefix, {\n type: \"device\",\n common: { name: config.productName || config.productType },\n native: {\n encryptedToken,\n productType: config.productType,\n serial: config.serial,\n productName: config.productName,\n ...(config.ip ? { ip: config.ip } : {}),\n },\n });\n }\n\n /**\n * Remove device config from its device object\n *\n * @param config Device configuration to remove\n */\n private async removeDeviceFromObject(config: DeviceConfig): Promise<void> {\n await this.stateManager.removeDevice(config);\n }\n\n /**\n * Handle a discovered device from mDNS (only active during pairing)\n *\n * @param discovered Discovered device info\n */\n private onDeviceDiscovered(discovered: DiscoveredDevice): void {\n // Skip already paired devices\n const existing = Array.from(this.connections.values()).find(\n (c) => c.config.serial === discovered.serial,\n );\n if (existing) {\n return;\n }\n\n // Skip duplicates\n if (\n this.discoveredDuringPairing.find((d) => d.serial === discovered.serial)\n ) {\n return;\n }\n\n this.discoveredDuringPairing.push(discovered);\n this.log.info(\n `Found ${discovered.name} (${discovered.productType}) at ${discovered.ip} \u2014 press the button on the device to pair`,\n );\n }\n\n /**\n * Adapter stopping \u2014 MUST be synchronous\n *\n * @param callback Completion callback\n */\n private onUnload(callback: () => void): void {\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n }\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n }\n if (this.systemPollTimer) {\n this.clearInterval(this.systemPollTimer);\n }\n if (this.ipRecoveryTimer) {\n this.clearTimeout(this.ipRecoveryTimer);\n }\n\n this.discovery?.stop();\n\n for (const conn of this.connections.values()) {\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n }\n }\n this.connections.clear();\n\n void this.setState(\"info.connection\", { val: false, ack: true });\n callback();\n }\n\n /**\n * Handle state changes\n *\n * @param id State ID\n * @param state State value\n */\n private async onStateChange(\n id: string,\n state: ioBroker.State | null | undefined,\n ): Promise<void> {\n if (!state || state.ack) {\n return;\n }\n\n // Pairing button\n if (id.endsWith(\".startPairing\")) {\n if (state.val) {\n await this.startPairing();\n }\n return;\n }\n\n // Remove device button\n if (id.endsWith(\".remove\")) {\n if (state.val) {\n await this.removeDevice(id);\n }\n return;\n }\n\n // Find which device this state belongs to\n const conn = this.findConnectionForState(id);\n if (!conn || !conn.ip) {\n return;\n }\n\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n\n try {\n if (id.endsWith(\".system.reboot\")) {\n this.log.info(`Rebooting ${conn.config.productName} (${conn.ip})`);\n await client.reboot();\n } else if (id.endsWith(\".system.identify\")) {\n await client.identify();\n } else if (id.endsWith(\".system.cloud_enabled\")) {\n await client.setSystem({ cloud_enabled: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".system.status_led_brightness_pct\")) {\n await client.setSystem({\n status_led_brightness_pct: Number(state.val),\n });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".system.api_v1_enabled\")) {\n await client.setSystem({ api_v1_enabled: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.mode\")) {\n await client.setBatteries({\n mode: String(state.val) as \"zero\" | \"to_full\" | \"standby\",\n });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.permissions\")) {\n const perms = JSON.parse(String(state.val));\n await client.setBatteries({ permissions: perms });\n await this.setStateAsync(id, { val: state.val, ack: true });\n }\n } catch (err) {\n this.log.warn(\n `Failed to set ${id}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n /** Start pairing mode \u2014 discover devices and attempt to pair */\n private async startPairing(): Promise<void> {\n if (this.isPairing) {\n this.log.debug(\"Pairing already active\");\n return;\n }\n\n // Reset startPairing immediately so it doesn't survive a restart\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n\n this.isPairing = true;\n this.discoveredDuringPairing = [];\n\n // Stop IP recovery if running \u2014 pairing takes priority\n this.stopIpRecovery();\n\n // Check if manual IP is set, then clear pairingIp immediately\n const ipState = await this.getStateAsync(\"pairingIp\");\n this.pairingManualIp = ipState?.val ? String(ipState.val).trim() : \"\";\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\n if (this.pairingManualIp) {\n this.log.info(\n `Pairing mode enabled for ${this.pairingManualIp} \u2014 press the button on your HomeWizard device now (60 seconds timeout)`,\n );\n // Add as discovered device immediately\n this.discoveredDuringPairing.push({\n ip: this.pairingManualIp,\n productType: \"unknown\",\n serial: \"unknown\",\n name: this.pairingManualIp,\n });\n } else {\n this.log.info(\n \"Pairing mode enabled \u2014 searching for devices via mDNS, press the button on your HomeWizard device now (60 seconds timeout)\",\n );\n // Restart mDNS browser to trigger fresh query \u2014 already-cached devices\n // won't be re-announced otherwise and pairing would never find them\n if (!this.discovery) {\n this.discovery = new HomeWizardDiscovery(this.log);\n }\n this.discovery.start((discovered) => {\n this.onDeviceDiscovered(discovered);\n });\n }\n\n // Poll discovered devices for pairing\n this.pairingPollTimer = this.setInterval(() => {\n void this.pollPairing();\n }, PAIRING_POLL_MS);\n\n // Timeout pairing\n this.pairingTimer = this.setTimeout(() => {\n this.stopPairing();\n this.log.info(\n \"Pairing mode automatically disabled after 60 seconds timeout\",\n );\n }, PAIRING_TIMEOUT_MS);\n }\n\n /** Poll all discovered devices to attempt pairing */\n private async pollPairing(): Promise<void> {\n for (const device of this.discoveredDuringPairing) {\n try {\n const client = new HomeWizardClient(device.ip);\n const result = await client.requestPairing();\n\n // Success! Button was pressed\n this.log.info(\n `Successfully paired with ${device.name} (${device.productType}) at ${device.ip} \u2014 connecting...`,\n );\n\n // Get device info\n const authedClient = new HomeWizardClient(device.ip, result.token);\n const info = await authedClient.getDeviceInfo();\n\n const deviceConfig: DeviceConfig = {\n token: result.token,\n productType: info.product_type,\n serial: info.serial,\n productName: info.product_name,\n ip: device.ip,\n };\n\n // Save to device object (no adapter restart!)\n await this.saveDeviceToObject(deviceConfig);\n await this.stateManager.createDeviceStates(deviceConfig);\n\n // Create connection and connect\n const key = this.stateManager.devicePrefix(deviceConfig);\n const conn: DeviceConnection = {\n config: deviceConfig,\n ip: device.ip,\n wsClient: null,\n wsAuthenticated: false,\n pollTimer: undefined,\n reconnectTimer: undefined,\n wsFailCount: 0,\n authFailCount: 0,\n lastErrorCode: \"\",\n ipRecoveryDone: false,\n };\n this.connections.set(key, conn);\n void this.initDevice(conn);\n\n // Remove from discovery list\n this.discoveredDuringPairing = this.discoveredDuringPairing.filter(\n (d) => d.serial !== info.serial,\n );\n\n this.stopPairing();\n this.updateGlobalConnection();\n return;\n } catch (err) {\n // 403 = button not pressed yet \u2014 expected, keep polling\n if (err instanceof HomeWizardApiError && err.statusCode === 403) {\n continue;\n }\n this.log.debug(\n `Pairing poll error for ${device.ip}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n }\n\n /** Stop pairing mode */\n private stopPairing(): void {\n this.isPairing = false;\n this.pairingManualIp = \"\";\n this.discoveredDuringPairing = [];\n\n // Stop mDNS \u2014 only needed during pairing\n if (this.discovery) {\n this.discovery.stop();\n this.discovery = null;\n }\n\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n this.pairingPollTimer = undefined;\n }\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n this.pairingTimer = undefined;\n }\n }\n\n /** Start mDNS to find devices that changed IP */\n private startIpRecovery(): void {\n // Don't start if already running or pairing\n if (this.discovery || this.isPairing) {\n return;\n }\n\n this.log.info(\"Device unreachable \u2014 searching for new IP via mDNS\");\n\n this.discovery = new HomeWizardDiscovery(this.log);\n this.discovery.start((discovered) => {\n // Match against disconnected devices\n for (const conn of this.connections.values()) {\n if (conn.config.serial !== discovered.serial) {\n continue;\n }\n if (discovered.ip === conn.ip || conn.wsAuthenticated) {\n return; // Same IP or already connected\n }\n\n this.log.info(\n `${conn.config.productName}: found at new IP ${discovered.ip} (was ${conn.ip})`,\n );\n\n // Update IP and persist\n conn.ip = discovered.ip;\n conn.config.ip = discovered.ip;\n conn.wsFailCount = 0;\n void this.saveDeviceToObject(conn.config);\n\n // Cancel pending reconnect and connect immediately\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n this.connectWebSocket(conn);\n return;\n }\n });\n\n // Stop after timeout \u2014 mark device offline, stop reconnecting\n this.ipRecoveryTimer = this.setTimeout(() => {\n this.ipRecoveryTimer = undefined;\n this.stopIpRecovery();\n\n // Mark all unreachable devices as offline\n for (const conn of this.connections.values()) {\n if (!conn.wsAuthenticated && conn.wsFailCount > 0) {\n conn.ipRecoveryDone = true;\n // Stop reconnect timer and REST fallback\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n this.log.warn(\n `${conn.config.productName}: device offline \u2014 check network or re-pair with new IP`,\n );\n }\n }\n }, IP_RECOVERY_TIMEOUT_MS);\n }\n\n /** Stop mDNS IP recovery */\n private stopIpRecovery(): void {\n if (this.ipRecoveryTimer) {\n this.clearTimeout(this.ipRecoveryTimer);\n this.ipRecoveryTimer = undefined;\n }\n if (this.discovery && !this.isPairing) {\n this.discovery.stop();\n this.discovery = null;\n }\n }\n\n /**\n * Initialize a newly discovered device \u2014 fetch info and connect WebSocket\n *\n * @param conn Device connection with IP set\n */\n private async initDevice(conn: DeviceConnection): Promise<void> {\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const info = await client.getDeviceInfo();\n const key = this.stateManager.devicePrefix(conn.config);\n await this.setStateAsync(`${key}.info.firmware`, {\n val: info.firmware_version,\n ack: true,\n });\n } catch (err) {\n this.logDeviceError(conn, \"init\", err);\n }\n\n this.connectWebSocket(conn);\n void this.pollSystemInfo(conn);\n }\n\n /**\n * Connect WebSocket for a device\n *\n * @param conn Device connection\n */\n private connectWebSocket(conn: DeviceConnection): void {\n if (!conn.ip) {\n return; // No IP yet \u2014 wait for mDNS\n }\n\n // Stop reconnecting if auth keeps failing\n if (conn.authFailCount >= MAX_AUTH_FAILURES) {\n return;\n }\n\n // After repeated failures, try mDNS once to find a new IP\n if (conn.wsFailCount >= WS_FAILURES_BEFORE_MDNS && !conn.ipRecoveryDone) {\n this.startIpRecovery();\n }\n\n const key = this.stateManager.devicePrefix(conn.config);\n\n const wsClient = new HomeWizardWebSocket(conn.ip, conn.config.token, {\n onMeasurement: (data: Measurement) => {\n void this.stateManager.updateMeasurement(conn.config, data);\n },\n onConnected: () => {\n conn.wsAuthenticated = true;\n conn.wsFailCount = 0;\n conn.authFailCount = 0;\n conn.ipRecoveryDone = false;\n void this.stateManager.setDeviceConnected(conn.config, true);\n this.updateGlobalConnection();\n\n // Stop REST fallback if active\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n\n // Stop IP recovery if all devices are connected\n if (this.discovery && !this.isPairing) {\n const allConnected = Array.from(this.connections.values()).every(\n (c) => c.wsAuthenticated,\n );\n if (allConnected) {\n this.stopIpRecovery();\n }\n }\n\n // Log restoration if we had errors before\n if (conn.lastErrorCode) {\n this.log.info(`${conn.config.productName}: connection restored`);\n conn.lastErrorCode = \"\";\n }\n\n this.log.debug(\n `WebSocket connected to ${conn.config.productName} (${conn.ip})`,\n );\n },\n onDisconnected: (error?: Error) => {\n conn.wsAuthenticated = false;\n conn.wsClient = null;\n void this.stateManager.setDeviceConnected(conn.config, false);\n this.updateGlobalConnection();\n\n if (error) {\n this.logDeviceError(conn, \"ws\", error);\n }\n\n // Check if this was an auth failure\n if (\n error instanceof HomeWizardApiError &&\n error.errorCode === \"user:unauthorized\"\n ) {\n conn.authFailCount++;\n if (conn.authFailCount >= MAX_AUTH_FAILURES) {\n this.log.warn(\n `${conn.config.productName}: token invalid \u2014 re-pair device to fix`,\n );\n return;\n }\n }\n\n // Start REST fallback\n this.startRestFallback(conn);\n\n // Schedule reconnect with exponential backoff\n conn.wsFailCount++;\n const delay = Math.min(\n WS_RECONNECT_BASE_MS * Math.pow(2, conn.wsFailCount - 1),\n WS_RECONNECT_MAX_MS,\n );\n this.log.debug(\n `${key}: WS reconnect in ${delay / 1000}s (attempt ${conn.wsFailCount})`,\n );\n\n conn.reconnectTimer = this.setTimeout(() => {\n conn.reconnectTimer = undefined;\n this.connectWebSocket(conn);\n }, delay);\n },\n log: this.log,\n });\n\n conn.wsClient = wsClient;\n wsClient.connect();\n }\n\n /**\n * Start REST polling as fallback when WebSocket is down.\n * Stops automatically on network errors (device unreachable) \u2014\n * WS reconnect + IP recovery handle the recovery path.\n *\n * @param conn Device connection\n */\n private startRestFallback(conn: DeviceConnection): void {\n if (conn.pollTimer || !conn.ip) {\n return;\n }\n\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n\n conn.pollTimer = this.setInterval(async () => {\n try {\n const data = await client.getMeasurement();\n await this.stateManager.updateMeasurement(conn.config, data);\n await this.stateManager.setDeviceConnected(conn.config, true);\n } catch (err) {\n this.logDeviceError(conn, \"rest\", err);\n await this.stateManager.setDeviceConnected(conn.config, false);\n\n // Stop REST polling on network errors \u2014 no point hammering unreachable device\n if (this.classifyError(err) === \"NETWORK\" && conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n }\n this.updateGlobalConnection();\n }, REST_POLL_MS);\n }\n\n /** Poll system info for all connected devices */\n private async pollAllSystemInfo(): Promise<void> {\n for (const conn of this.connections.values()) {\n if (conn.ip && conn.wsAuthenticated) {\n await this.pollSystemInfo(conn);\n }\n }\n }\n\n /**\n * Poll system info for a single device\n *\n * @param conn Device connection\n */\n private async pollSystemInfo(conn: DeviceConnection): Promise<void> {\n if (!conn.ip) {\n return;\n }\n\n try {\n const client = new HomeWizardClient(conn.ip, conn.config.token);\n const system = await client.getSystem();\n await this.stateManager.updateSystem(conn.config, system);\n\n // Also poll battery if device supports it\n try {\n const battery = await client.getBatteries();\n // Only create battery states if batteries are actually connected\n if (battery.battery_count && battery.battery_count > 0) {\n await this.stateManager.updateBattery(conn.config, battery);\n }\n } catch {\n // Device may not support batteries \u2014 that's fine\n }\n } catch (err) {\n this.logDeviceError(conn, \"system\", err);\n }\n }\n\n /** Update global info.connection based on all device states */\n private updateGlobalConnection(): void {\n const anyConnected = Array.from(this.connections.values()).some(\n (c) => c.wsAuthenticated,\n );\n void this.setStateAsync(\"info.connection\", {\n val: anyConnected,\n ack: true,\n });\n }\n\n /**\n * Remove a device \u2014 disconnect, delete states and object\n *\n * @param stateId The remove state ID\n */\n private async removeDevice(stateId: string): Promise<void> {\n const conn = this.findConnectionForState(stateId);\n if (!conn) {\n return;\n }\n\n const key = this.stateManager.devicePrefix(conn.config);\n this.log.info(\n `Removing device ${conn.config.productName} (${conn.config.serial})`,\n );\n\n // Disconnect\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n }\n this.connections.delete(key);\n\n // Delete device object and all states (no adapter restart!)\n await this.removeDeviceFromObject(conn.config);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Find connection for a state ID\n *\n * @param stateId Full state ID\n */\n private findConnectionForState(\n stateId: string,\n ): DeviceConnection | undefined {\n const localId = stateId.replace(`${this.namespace}.`, \"\");\n for (const conn of this.connections.values()) {\n const prefix = this.stateManager.devicePrefix(conn.config);\n if (localId.startsWith(`${prefix}.`)) {\n return conn;\n }\n }\n return undefined;\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 */\n private 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\n /**\n * Log device error with deduplication (based on error category, not context).\n * First occurrence of a new error category logs as warn, repeats as debug.\n *\n * @param conn Device connection\n * @param context Error context (for debug messages only)\n * @param err Error object\n */\n private logDeviceError(\n conn: DeviceConnection,\n context: string,\n err: unknown,\n ): void {\n const errorCode = this.classifyError(err);\n const isRepeat = errorCode === conn.lastErrorCode;\n conn.lastErrorCode = errorCode;\n\n if (isRepeat) {\n this.log.debug(\n `${conn.config.productName} ${context}: ${err instanceof Error ? err.message : String(err)}`,\n );\n } else if (errorCode === \"NETWORK\") {\n this.log.warn(\n `${conn.config.productName}: device unreachable \u2014 will keep retrying`,\n );\n } else {\n this.log.warn(\n `${conn.config.productName} ${context}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) =>\n new HomeWizard(options);\n} else {\n (() => new HomeWizard())();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "homewizard",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.5",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.3.5": {
|
|
7
|
+
"en": "Fix log spam: error deduplication by category (not context), REST fallback stops on network errors, system poll only for connected devices",
|
|
8
|
+
"de": "Log-Spam behoben: Fehler-Deduplizierung nach Kategorie (nicht Kontext), REST-Fallback stoppt bei Netzwerkfehlern, System-Poll nur für verbundene Geräte",
|
|
9
|
+
"ru": "Исправлен спам в логах: дедупликация ошибок по категории (не контексту), REST-фоллбэк останавливается при сетевых ошибках, системный опрос только для подключённых устройств",
|
|
10
|
+
"pt": "Corrigido spam de log: deduplicação de erros por categoria (não contexto), REST fallback para em erros de rede, poll de sistema apenas para dispositivos conectados",
|
|
11
|
+
"nl": "Log-spam opgelost: foutdeduplicatie op categorie (niet context), REST-fallback stopt bij netwerkfouten, systeempoll alleen voor verbonden apparaten",
|
|
12
|
+
"fr": "Correction du spam de log: déduplication des erreurs par catégorie (pas contexte), REST fallback s arrête sur erreurs réseau, poll système uniquement pour appareils connectés",
|
|
13
|
+
"it": "Corretto spam dei log: deduplicazione errori per categoria (non contesto), REST fallback si ferma su errori di rete, poll sistema solo per dispositivi connessi",
|
|
14
|
+
"es": "Corregido spam de log: deduplicación de errores por categoría (no contexto), REST fallback se detiene en errores de red, poll de sistema solo para dispositivos conectados",
|
|
15
|
+
"pl": "Naprawiono spam w logach: deduplikacja błędów według kategorii (nie kontekstu), REST fallback zatrzymuje się przy błędach sieciowych, poll systemowy tylko dla podłączonych urządzeń",
|
|
16
|
+
"uk": "Виправлено спам у логах: дедуплікація помилок за категорією (не контекстом), REST fallback зупиняється при мережевих помилках, системний опитування тільки для підключених пристроїв",
|
|
17
|
+
"zh-cn": "修复日志刷屏:按类别(非上下文)去重错误,REST 回退在网络错误时停止,系统轮询仅针对已连接设备"
|
|
18
|
+
},
|
|
6
19
|
"0.3.4": {
|
|
7
|
-
"en": "mDNS only
|
|
8
|
-
"de": "mDNS nur beim Pairing
|
|
9
|
-
"ru": "mDNS
|
|
10
|
-
"pt": "mDNS
|
|
11
|
-
"nl": "mDNS alleen
|
|
12
|
-
"fr": "mDNS
|
|
13
|
-
"it": "mDNS
|
|
14
|
-
"es": "mDNS solo
|
|
15
|
-
"pl": "mDNS
|
|
16
|
-
"uk": "mDNS
|
|
17
|
-
"zh-cn": "mDNS
|
|
20
|
+
"en": "mDNS only during pairing, automatic IP recovery on connection loss, offline detection",
|
|
21
|
+
"de": "mDNS nur beim Pairing, automatische IP-Wiederherstellung bei Verbindungsverlust, Offline-Erkennung",
|
|
22
|
+
"ru": "mDNS только при сопряжении, автоматическое восстановление IP при потере связи, обнаружение офлайн",
|
|
23
|
+
"pt": "mDNS apenas durante emparelhamento, recuperacao automatica de IP na perda de conexao, deteccao offline",
|
|
24
|
+
"nl": "mDNS alleen tijdens koppelen, automatisch IP-herstel bij verbindingsverlies, offline-detectie",
|
|
25
|
+
"fr": "mDNS uniquement lors de l appairage, recuperation IP automatique en cas de perte de connexion, detection hors ligne",
|
|
26
|
+
"it": "mDNS solo durante associazione, recupero IP automatico alla perdita di connessione, rilevamento offline",
|
|
27
|
+
"es": "mDNS solo durante emparejamiento, recuperacion automatica de IP al perder conexion, deteccion offline",
|
|
28
|
+
"pl": "mDNS tylko podczas parowania, automatyczne odzyskiwanie IP przy utracie polaczenia, wykrywanie offline",
|
|
29
|
+
"uk": "mDNS лише пiд час сполучення, автоматичне вiдновлення IP при втратi з єднання, виявлення офлайн",
|
|
30
|
+
"zh-cn": "mDNS 仅在配对时活跃,连接丢失时自动恢复 IP,离线检测"
|
|
18
31
|
},
|
|
19
32
|
"0.3.3": {
|
|
20
33
|
"en": "Fix mDNS pairing not finding devices, improve log messages during pairing",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Poprawiono autoryzację WebSocket i typ usługi mDNS, edytowalne pole IP w Admin UI",
|
|
81
94
|
"uk": "Виправлено авторизацію WebSocket та тип сервісу mDNS, редаговане поле IP в Admin UI",
|
|
82
95
|
"zh-cn": "修复 WebSocket 认证和 mDNS 服务类型,Admin UI 中添加可编辑的 IP 字段"
|
|
83
|
-
},
|
|
84
|
-
"0.1.3": {
|
|
85
|
-
"en": "Remove IP from config — devices are now discovered via mDNS at runtime",
|
|
86
|
-
"de": "IP aus Konfiguration entfernt — Geräte werden jetzt per mDNS zur Laufzeit gefunden",
|
|
87
|
-
"ru": "IP удалён из конфигурации — устройства теперь обнаруживаются через mDNS во время работы",
|
|
88
|
-
"pt": "IP removido da configuração — dispositivos agora são descobertos via mDNS em tempo de execução",
|
|
89
|
-
"nl": "IP uit configuratie verwijderd — apparaten worden nu via mDNS tijdens runtime gevonden",
|
|
90
|
-
"fr": "IP supprimée de la configuration — les appareils sont maintenant découverts via mDNS à l exécution",
|
|
91
|
-
"it": "IP rimosso dalla configurazione — i dispositivi vengono ora scoperti tramite mDNS a runtime",
|
|
92
|
-
"es": "IP eliminada de la configuración — los dispositivos se descubren ahora vía mDNS en tiempo de ejecución",
|
|
93
|
-
"pl": "IP usunięto z konfiguracji — urządzenia są teraz wykrywane przez mDNS w czasie działania",
|
|
94
|
-
"uk": "IP видалено з конфігурації — пристрої тепер виявляються через mDNS під час роботи",
|
|
95
|
-
"zh-cn": "从配置中移除 IP — 设备现在通过 mDNS 在运行时发现"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
package/package.json
CHANGED