iobroker.homewizard 0.12.0 → 0.12.2

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,21 @@ 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.2 (2026-06-11)
183
+
184
+ - Reboot and identify buttons reset themselves after the action, so they stay clickable in the admin UI
185
+ - Re-pairing a removed device no longer inherits the old device's log cooldown — its first connection warning shows up immediately again
186
+
187
+ ### 0.12.1 (2026-06-09)
188
+
189
+ - Internal refactoring. No user-facing changes.
190
+
180
191
  ### 0.12.0 (2026-06-07)
181
192
 
182
193
  - Added optional Sentry error reporting: crashes are sent to the developer so issues get fixed faster. Active only with ioBroker diagnostics enabled; anonymous.
@@ -192,15 +203,6 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
192
203
  - Two new data points: the WiFi network name the device is connected to, and the raw P1 telegram text (P1 meter only).
193
204
  - Removing a device now revokes its access token on the HomeWizard device, so no unused tokens are left behind.
194
205
 
195
- ### 0.9.3 (2026-05-23)
196
-
197
- - User-modified device names are no longer overwritten on adapter restart or IP recovery.
198
- - Improved timer management for ioBroker compact mode.
199
-
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_
@@ -18,6 +18,8 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var state_manager_exports = {};
20
20
  __export(state_manager_exports, {
21
+ MEASUREMENT_STATE_DEFS: () => MEASUREMENT_STATE_DEFS,
22
+ MOMENTARY_KEYS: () => MOMENTARY_KEYS,
21
23
  StateManager: () => StateManager
22
24
  });
23
25
  module.exports = __toCommonJS(state_manager_exports);
@@ -1011,6 +1013,8 @@ class StateManager {
1011
1013
  }
1012
1014
  // Annotate the CommonJS export names for ESM import in node:
1013
1015
  0 && (module.exports = {
1016
+ MEASUREMENT_STATE_DEFS,
1017
+ MOMENTARY_KEYS,
1014
1018
  StateManager
1015
1019
  });
1016
1020
  //# sourceMappingURL=state-manager.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/state-manager.ts"],
4
- "sourcesContent": ["import type * as utils from \"@iobroker/adapter-core\";\nimport { coerceBoolean, coerceFiniteNumber, coerceString, isPlainObject } from \"./coerce\";\nimport type { I18nKey } from \"./i18n\";\nimport { resolveLabel, tName } from \"./i18n\";\nimport type { BatteryControl, DeviceConfig, Measurement, SystemInfo } from \"./types\";\n\n/** Measurement field to state definition mapping */\ninterface MeasurementStateDef {\n /** Measurement field key */\n key: string;\n /** ioBroker state ID suffix */\n id: string;\n /** Translation key for `common.name` (resolved via {@link tName}) */\n nameKey: I18nKey;\n /** Optional translation key for `common.desc` (resolved via {@link tName}) */\n descKey?: I18nKey;\n /** State value type */\n type: ioBroker.CommonType;\n /** ioBroker role */\n role: string;\n /** Unit string */\n unit?: string;\n}\n\n/** Options for {@link StateManager.createState} (avoids long positional argument lists). */\ninterface StateDef {\n /** Full state ID */\n id: string;\n /** State name (translation object or device-identifier string) */\n name: ioBroker.StringOrTranslated;\n /** Value type */\n type: ioBroker.CommonType;\n /** ioBroker role */\n role: string;\n /** Whether the state is writable (default false) */\n write?: boolean;\n /** Optional unit */\n unit?: string;\n /** Optional `common.desc` */\n desc?: ioBroker.StringOrTranslated;\n /** Optional `common.states` map (plain-string values) */\n states?: Record<string, string>;\n}\n\n/** Options for {@link StateManager.ensureAndSet} \u2014 a {@link StateDef} plus the value to write. */\ninterface StateSet extends StateDef {\n /** Value to write */\n value: ioBroker.StateValue;\n /** Use setStateChangedAsync (skip redundant writes) instead of setStateAsync */\n changedOnly?: boolean;\n}\n\n/**\n * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).\n *\n * @param str Raw string to sanitize\n */\nfunction sanitize(str: string): string {\n return str.replace(/[^a-zA-Z0-9_-]/g, \"_\").toLowerCase();\n}\n\nconst MEASUREMENT_STATE_DEFS: MeasurementStateDef[] = [\n // Power\n { key: \"power_w\", id: \"power_w\", nameKey: \"powerTotal\", type: \"number\", role: \"value.power\", unit: \"W\" },\n { key: \"power_l1_w\", id: \"power_l1_w\", nameKey: \"powerL1\", type: \"number\", role: \"value.power\", unit: \"W\" },\n { key: \"power_l2_w\", id: \"power_l2_w\", nameKey: \"powerL2\", type: \"number\", role: \"value.power\", unit: \"W\" },\n { key: \"power_l3_w\", id: \"power_l3_w\", nameKey: \"powerL3\", type: \"number\", role: \"value.power\", unit: \"W\" },\n // Voltage\n { key: \"voltage_v\", id: \"voltage_v\", nameKey: \"voltage\", type: \"number\", role: \"value.voltage\", unit: \"V\" },\n { key: \"voltage_l1_v\", id: \"voltage_l1_v\", nameKey: \"voltageL1\", type: \"number\", role: \"value.voltage\", unit: \"V\" },\n { key: \"voltage_l2_v\", id: \"voltage_l2_v\", nameKey: \"voltageL2\", type: \"number\", role: \"value.voltage\", unit: \"V\" },\n { key: \"voltage_l3_v\", id: \"voltage_l3_v\", nameKey: \"voltageL3\", type: \"number\", role: \"value.voltage\", unit: \"V\" },\n // Current\n { key: \"current_a\", id: \"current_a\", nameKey: \"current\", type: \"number\", role: \"value.current\", unit: \"A\" },\n { key: \"current_l1_a\", id: \"current_l1_a\", nameKey: \"currentL1\", type: \"number\", role: \"value.current\", unit: \"A\" },\n { key: \"current_l2_a\", id: \"current_l2_a\", nameKey: \"currentL2\", type: \"number\", role: \"value.current\", unit: \"A\" },\n { key: \"current_l3_a\", id: \"current_l3_a\", nameKey: \"currentL3\", type: \"number\", role: \"value.current\", unit: \"A\" },\n // Frequency\n { key: \"frequency_hz\", id: \"frequency_hz\", nameKey: \"frequency\", type: \"number\", role: \"value\", unit: \"Hz\" },\n // Energy import\n {\n key: \"energy_import_kwh\",\n id: \"energy_import_kwh\",\n nameKey: \"energyImportTotal\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_import_t1_kwh\",\n id: \"energy_import_t1_kwh\",\n nameKey: \"energyImportT1\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_import_t2_kwh\",\n id: \"energy_import_t2_kwh\",\n nameKey: \"energyImportT2\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_import_t3_kwh\",\n id: \"energy_import_t3_kwh\",\n nameKey: \"energyImportT3\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_import_t4_kwh\",\n id: \"energy_import_t4_kwh\",\n nameKey: \"energyImportT4\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n // Energy export\n {\n key: \"energy_export_kwh\",\n id: \"energy_export_kwh\",\n nameKey: \"energyExportTotal\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_export_t1_kwh\",\n id: \"energy_export_t1_kwh\",\n nameKey: \"energyExportT1\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_export_t2_kwh\",\n id: \"energy_export_t2_kwh\",\n nameKey: \"energyExportT2\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_export_t3_kwh\",\n id: \"energy_export_t3_kwh\",\n nameKey: \"energyExportT3\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_export_t4_kwh\",\n id: \"energy_export_t4_kwh\",\n nameKey: \"energyExportT4\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n // Tariff (common.states applied separately in updateMeasurement for translation labels)\n { key: \"tariff\", id: \"tariff\", nameKey: \"tariff\", type: \"number\", role: \"value\" },\n // Power quality\n {\n key: \"voltage_sag_l1_count\",\n id: \"quality.voltage_sag_l1_count\",\n nameKey: \"voltageSagL1\",\n descKey: \"voltageSag\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_sag_l2_count\",\n id: \"quality.voltage_sag_l2_count\",\n nameKey: \"voltageSagL2\",\n descKey: \"voltageSag\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_sag_l3_count\",\n id: \"quality.voltage_sag_l3_count\",\n nameKey: \"voltageSagL3\",\n descKey: \"voltageSag\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l1_count\",\n id: \"quality.voltage_swell_l1_count\",\n nameKey: \"voltageSwellL1\",\n descKey: \"voltageSwell\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l2_count\",\n id: \"quality.voltage_swell_l2_count\",\n nameKey: \"voltageSwellL2\",\n descKey: \"voltageSwell\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l3_count\",\n id: \"quality.voltage_swell_l3_count\",\n nameKey: \"voltageSwellL3\",\n descKey: \"voltageSwell\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"any_power_fail_count\",\n id: \"quality.power_fail_count\",\n nameKey: \"powerFailCount\",\n descKey: \"powerFailCountDesc\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"long_power_fail_count\",\n id: \"quality.long_power_fail_count\",\n nameKey: \"longPowerFailCount\",\n descKey: \"longPowerFailCountDesc\",\n type: \"number\",\n role: \"value\",\n },\n // Capacity tariff (Belgium)\n {\n key: \"average_power_15m_w\",\n id: \"average_power_15m_w\",\n nameKey: \"avgPower15m\",\n descKey: \"belgiumCapacityTariff\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"monthly_power_peak_w\",\n id: \"monthly_power_peak_w\",\n nameKey: \"monthlyPowerPeak\",\n descKey: \"belgiumCapacityTariff\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"monthly_power_peak_timestamp\",\n id: \"monthly_power_peak_timestamp\",\n nameKey: \"monthlyPowerPeakTimestamp\",\n descKey: \"belgiumCapacityTariff\",\n type: \"string\",\n role: \"date\",\n },\n // kWh meter specifics \u2014 apparent / reactive\n {\n key: \"apparent_current_a\",\n id: \"apparent_current_a\",\n nameKey: \"apparentCurrent\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_current_l1_a\",\n id: \"apparent_current_l1_a\",\n nameKey: \"apparentCurrentL1\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_current_l2_a\",\n id: \"apparent_current_l2_a\",\n nameKey: \"apparentCurrentL2\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_current_l3_a\",\n id: \"apparent_current_l3_a\",\n nameKey: \"apparentCurrentL3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_a\",\n id: \"reactive_current_a\",\n nameKey: \"reactiveCurrent\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_l1_a\",\n id: \"reactive_current_l1_a\",\n nameKey: \"reactiveCurrentL1\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_l2_a\",\n id: \"reactive_current_l2_a\",\n nameKey: \"reactiveCurrentL2\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_l3_a\",\n id: \"reactive_current_l3_a\",\n nameKey: \"reactiveCurrentL3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_power_va\",\n id: \"apparent_power_va\",\n nameKey: \"apparentPower\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"apparent_power_l1_va\",\n id: \"apparent_power_l1_va\",\n nameKey: \"apparentPowerL1\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"apparent_power_l2_va\",\n id: \"apparent_power_l2_va\",\n nameKey: \"apparentPowerL2\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"apparent_power_l3_va\",\n id: \"apparent_power_l3_va\",\n nameKey: \"apparentPowerL3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"reactive_power_var\",\n id: \"reactive_power_var\",\n nameKey: \"reactivePower\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"reactive_power_l1_var\",\n id: \"reactive_power_l1_var\",\n nameKey: \"reactivePowerL1\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"reactive_power_l2_var\",\n id: \"reactive_power_l2_var\",\n nameKey: \"reactivePowerL2\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"reactive_power_l3_var\",\n id: \"reactive_power_l3_var\",\n nameKey: \"reactivePowerL3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"power_factor\",\n id: \"power_factor\",\n nameKey: \"powerFactor\",\n descKey: \"powerFactorDesc\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l1\",\n id: \"power_factor_l1\",\n nameKey: \"powerFactorL1\",\n descKey: \"powerFactorDesc\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l2\",\n id: \"power_factor_l2\",\n nameKey: \"powerFactorL2\",\n descKey: \"powerFactorDesc\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l3\",\n id: \"power_factor_l3\",\n nameKey: \"powerFactorL3\",\n descKey: \"powerFactorDesc\",\n type: \"number\",\n role: \"value\",\n },\n // Battery specifics\n {\n key: \"state_of_charge_pct\",\n id: \"state_of_charge_pct\",\n nameKey: \"stateOfCharge\",\n type: \"number\",\n role: \"value.battery\",\n unit: \"%\",\n },\n { key: \"cycles\", id: \"cycles\", nameKey: \"cycles\", type: \"number\", role: \"value\" },\n // Metadata\n { key: \"meter_model\", id: \"meter_model\", nameKey: \"meterModel\", type: \"string\", role: \"text\" },\n { key: \"timestamp\", id: \"timestamp\", nameKey: \"measurementTimestamp\", type: \"string\", role: \"date\" },\n];\n\n// Instantaneous electrical values \u2014 change on (almost) every ~1/s push, so a setStateChanged\n// read-compare buys nothing. These stay on setStateAsync; every other measurement field\n// (energy totals, tariff, power-quality counts, capacity tariff, SoC/cycles, model/timestamp)\n// is slow/static and uses setStateChangedAsync to skip redundant 1/s writes.\nconst MOMENTARY_KEYS = new Set<string>([\n \"power_w\",\n \"power_l1_w\",\n \"power_l2_w\",\n \"power_l3_w\",\n \"voltage_v\",\n \"voltage_l1_v\",\n \"voltage_l2_v\",\n \"voltage_l3_v\",\n \"current_a\",\n \"current_l1_a\",\n \"current_l2_a\",\n \"current_l3_a\",\n \"frequency_hz\",\n \"apparent_current_a\",\n \"apparent_current_l1_a\",\n \"apparent_current_l2_a\",\n \"apparent_current_l3_a\",\n \"reactive_current_a\",\n \"reactive_current_l1_a\",\n \"reactive_current_l2_a\",\n \"reactive_current_l3_a\",\n \"apparent_power_va\",\n \"apparent_power_l1_va\",\n \"apparent_power_l2_va\",\n \"apparent_power_l3_va\",\n \"reactive_power_var\",\n \"reactive_power_l1_var\",\n \"reactive_power_l2_var\",\n \"reactive_power_l3_var\",\n \"power_factor\",\n \"power_factor_l1\",\n \"power_factor_l2\",\n \"power_factor_l3\",\n]);\n\n/**\n * Build a `common.states` map for tariff (T1-T4) with plain-string labels.\n *\n * **VALUES MUST be plain-string** \u2014 Admin renders states-values as React\n * children. Translation objects trigger React Error #31 \u2192 fatal \"Error in GUI\"\n * on dropdown open (verified hassemu v1.28.4, 2026-05-12).\n *\n */\n// Cached after first build \u2014 the system language is fixed for the adapter run\n// (I18n.init runs once in onReady), so these label maps never change at runtime.\n// Avoids rebuilding the object on every ~1/s measurement push.\nlet tariffStatesCache: Record<string, string> | null = null;\nfunction tariffStates(): Record<string, string> {\n return (tariffStatesCache ??= {\n 1: resolveLabel(\"tariff1\"),\n 2: resolveLabel(\"tariff2\"),\n 3: resolveLabel(\"tariff3\"),\n 4: resolveLabel(\"tariff4\"),\n });\n}\n\n/**\n * Build a `common.states` map for HWE-BAT battery.mode with plain-string labels.\n * Same constraint + same memoization as {@link tariffStates}. `predictive` since API 2.3.0.\n */\nlet batteryModeStatesCache: Record<string, string> | null = null;\nfunction batteryModeStates(): Record<string, string> {\n return (batteryModeStatesCache ??= {\n zero: resolveLabel(\"modeZero\"),\n to_full: resolveLabel(\"modeToFull\"),\n standby: resolveLabel(\"modeStandby\"),\n predictive: resolveLabel(\"modePredictive\"),\n });\n}\n\n/** Manages ioBroker state creation and updates for HomeWizard devices */\nexport class StateManager {\n private readonly adapter: utils.AdapterInstance;\n /**\n * Cache of state / channel IDs that have already passed\n * `setObjectNotExistsAsync`. Skips repeat DB lookups on the hot path \u2014\n * a P1 meter pushes ~1 measurement/s with up to ~30 active fields, which\n * otherwise meant ~30 Redis lookups per second just to ask \u201Edoes it\n * exist\". On `removeDevice(prefix)` all `prefix.*` IDs are dropped.\n */\n private readonly createdIds = new Set<string>();\n\n /** @param adapter The ioBroker adapter instance */\n constructor(adapter: utils.AdapterInstance) {\n this.adapter = adapter;\n }\n\n /**\n * Create device channel and info states\n *\n * @param config Device configuration\n */\n async createDeviceStates(config: DeviceConfig): Promise<void> {\n const prefix = this.devicePrefix(config);\n\n this.adapter.log.debug(`state-manager: createDeviceStates ${prefix} (productType=${config.productType})`);\n\n // Device-Object: common.name keeps the user-supplied product name (or product type as fallback) \u2014\n // these are device-specific identifiers, NOT translatable.\n await this.adapter.extendObjectAsync(\n prefix,\n {\n type: \"device\",\n common: {\n name: config.productName || config.productType,\n statusStates: {\n onlineId: `${this.adapter.namespace}.${prefix}.info.connected`,\n },\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n\n await this.adapter.extendObjectAsync(\n `${prefix}.info`,\n {\n type: \"channel\",\n common: { name: tName(\"deviceInformation\") },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n\n await this.createState({\n id: `${prefix}.info.productName`,\n name: tName(\"productName\"),\n type: \"string\",\n role: \"text\",\n });\n await this.createState({\n id: `${prefix}.info.productType`,\n name: tName(\"productType\"),\n type: \"string\",\n role: \"text\",\n });\n await this.createState({ id: `${prefix}.info.firmware`, name: tName(\"firmware\"), type: \"string\", role: \"text\" });\n await this.createState({\n id: `${prefix}.info.connected`,\n name: tName(\"connected\"),\n type: \"boolean\",\n role: \"indicator.reachable\",\n });\n await this.createState({ id: `${prefix}.info.wifi_ssid`, name: tName(\"wifiSsid\"), type: \"string\", role: \"text\" });\n await this.createState({\n id: `${prefix}.info.wifi_rssi_db`,\n name: tName(\"wifiRssi\"),\n type: \"number\",\n role: \"value\",\n unit: \"dBm\",\n });\n await this.createState({\n id: `${prefix}.info.uptime_s`,\n name: tName(\"uptime\"),\n type: \"number\",\n role: \"value\",\n unit: \"s\",\n });\n\n // Remove device button\n await this.createButton(`${prefix}.remove`, tName(\"removeDevice\"), tName(\"removeDeviceDesc\"));\n\n // Set initial info values\n await this.adapter.setStateAsync(`${prefix}.info.productName`, {\n val: config.productName,\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.info.productType`, {\n val: config.productType,\n ack: true,\n });\n }\n\n /**\n * Update measurement states \u2014 only creates states that have values\n *\n * @param config Device configuration\n * @param data Measurement data\n */\n async updateMeasurement(config: DeviceConfig, data: Measurement): Promise<void> {\n if (!isPlainObject(data)) {\n return;\n }\n const prefix = this.devicePrefix(config);\n const mPrefix = `${prefix}.measurement`;\n\n // Ensure measurement channel exists (cached after first call per device)\n await this.ensureChannel(mPrefix, tName(\"measurement\"));\n\n // Main measurement values \u2014 coerce per declared type. Once a state's object\n // is in the cache, ensureAndSet only does one setStateAsync per field \u2014 those\n // are independent and run in parallel via Promise.all instead of sequentially.\n const record = data;\n const writes: Promise<void>[] = [];\n for (const def of MEASUREMENT_STATE_DEFS) {\n const raw = record[def.key];\n let coerced: number | string | null = null;\n if (def.type === \"number\") {\n coerced = coerceFiniteNumber(raw);\n } else if (def.type === \"string\") {\n coerced = coerceString(raw);\n }\n if (coerced !== null) {\n writes.push(\n this.ensureAndSet({\n id: `${mPrefix}.${def.id}`,\n name: tName(def.nameKey),\n type: def.type,\n role: def.role,\n value: coerced,\n unit: def.unit,\n desc: def.descKey ? tName(def.descKey) : undefined,\n states: def.key === \"tariff\" ? tariffStates() : undefined,\n changedOnly: !MOMENTARY_KEYS.has(def.key),\n }),\n );\n }\n }\n await Promise.all(writes);\n\n // External meters (P1 gas/water/heat) \u2014 channel-create paths must run sequentially\n // because the parent `external` channel must exist before the per-meter channel\n // and the per-meter value/unit/timestamp states. Inside one meter, the three\n // value/unit/timestamp writes are independent and run in parallel.\n const external = record.external;\n if (Array.isArray(external) && external.length > 0) {\n for (const rawExt of external) {\n if (!isPlainObject(rawExt)) {\n continue;\n }\n const type = coerceString(rawExt.type);\n const uniqueId = coerceString(rawExt.unique_id);\n if (!type || !uniqueId) {\n continue;\n }\n\n const value = coerceFiniteNumber(rawExt.value);\n const unit = coerceString(rawExt.unit);\n const timestamp = coerceString(rawExt.timestamp);\n\n await this.ensureChannel(`${mPrefix}.external`, tName(\"externalMeters\"));\n\n const extId = `${mPrefix}.external.${sanitize(type)}_${sanitize(uniqueId)}`;\n // External meter channel keeps the device-supplied type (e.g. \"gas_meter\")\n // as channel name \u2014 identifies the physical meter, not localizable.\n await this.ensureChannel(extId, type);\n\n const extWrites: Promise<void>[] = [];\n if (value !== null) {\n extWrites.push(\n this.ensureAndSet({\n id: `${extId}.value`,\n name: tName(\"externalValue\"),\n type: \"number\",\n role: \"value\",\n value,\n unit: unit ?? undefined,\n changedOnly: true,\n }),\n );\n }\n if (unit) {\n extWrites.push(\n this.ensureAndSet({\n id: `${extId}.unit`,\n name: tName(\"externalUnit\"),\n type: \"string\",\n role: \"text\",\n value: unit,\n changedOnly: true,\n }),\n );\n }\n if (timestamp) {\n extWrites.push(\n this.ensureAndSet({\n id: `${extId}.timestamp`,\n name: tName(\"externalTimestamp\"),\n type: \"string\",\n role: \"date\",\n value: timestamp,\n changedOnly: true,\n }),\n );\n }\n await Promise.all(extWrites);\n }\n }\n }\n\n /**\n * Update system states\n *\n * @param config Device configuration\n * @param system System info data\n */\n async updateSystem(config: DeviceConfig, system: SystemInfo): Promise<void> {\n if (!isPlainObject(system)) {\n return;\n }\n const prefix = this.devicePrefix(config);\n const record = system as Record<string, unknown>;\n\n // WiFi SSID/RSSI + uptime in info channel \u2014 slow-changing \u2192 changedOnly.\n const ssid = coerceString(record.wifi_ssid);\n if (ssid !== null) {\n await this.ensureAndSet({\n id: `${prefix}.info.wifi_ssid`,\n name: tName(\"wifiSsid\"),\n type: \"string\",\n role: \"text\",\n value: ssid,\n changedOnly: true,\n });\n }\n const rssi = coerceFiniteNumber(record.wifi_rssi_db);\n if (rssi !== null) {\n await this.ensureAndSet({\n id: `${prefix}.info.wifi_rssi_db`,\n name: tName(\"wifiRssi\"),\n type: \"number\",\n role: \"value\",\n value: rssi,\n unit: \"dBm\",\n changedOnly: true,\n });\n }\n const uptime = coerceFiniteNumber(record.uptime_s);\n if (uptime !== null) {\n await this.ensureAndSet({\n id: `${prefix}.info.uptime_s`,\n name: tName(\"uptime\"),\n type: \"number\",\n role: \"value\",\n value: uptime,\n unit: \"s\",\n changedOnly: true,\n });\n }\n\n // System control channel (cached after first call per device)\n await this.ensureChannel(`${prefix}.system`, tName(\"systemSettings\"));\n\n // HWE-BAT: cloud_enabled is read-only (always true) and reboot is unsupported.\n const isBattery = config.productType === \"HWE-BAT\";\n\n const cloudEnabled = coerceBoolean(record.cloud_enabled);\n if (cloudEnabled !== null) {\n await this.ensureAndSet({\n id: `${prefix}.system.cloud_enabled`,\n name: tName(\"cloudEnabled\"),\n type: \"boolean\",\n role: \"switch\",\n value: cloudEnabled,\n write: !isBattery,\n changedOnly: true,\n });\n }\n const ledPct = coerceFiniteNumber(record.status_led_brightness_pct);\n if (ledPct !== null) {\n await this.ensureAndSet({\n id: `${prefix}.system.status_led_brightness_pct`,\n name: tName(\"ledBrightness\"),\n type: \"number\",\n role: \"level\",\n value: ledPct,\n unit: \"%\",\n write: true,\n changedOnly: true,\n });\n }\n\n const apiV1 = coerceBoolean(record.api_v1_enabled);\n if (apiV1 !== null) {\n await this.ensureAndSet({\n id: `${prefix}.system.api_v1_enabled`,\n name: tName(\"apiV1Enabled\"),\n type: \"boolean\",\n role: \"switch\",\n value: apiV1,\n write: true,\n changedOnly: true,\n });\n }\n\n // Action buttons (reboot is unsupported on the Plug-In Battery)\n if (!isBattery) {\n await this.createButton(`${prefix}.system.reboot`, tName(\"rebootDevice\"));\n }\n await this.createButton(`${prefix}.system.identify`, tName(\"identify\"));\n }\n\n /**\n * Update battery control states\n *\n * @param config Device configuration\n * @param battery Battery control data\n */\n async updateBattery(config: DeviceConfig, battery: BatteryControl): Promise<void> {\n if (!isPlainObject(battery)) {\n return;\n }\n const prefix = this.devicePrefix(config);\n const record = battery as Record<string, unknown>;\n\n await this.ensureChannel(`${prefix}.battery`, tName(\"batteryControl\"));\n\n const mode = coerceString(record.mode);\n if (mode) {\n await this.ensureAndSet({\n id: `${prefix}.battery.mode`,\n name: tName(\"batteryMode\"),\n type: \"string\",\n role: \"text\",\n value: mode,\n write: true,\n desc: tName(\"batteryModeDesc\"),\n states: batteryModeStates(),\n changedOnly: true,\n });\n }\n if (Array.isArray(record.permissions)) {\n await this.ensureAndSet({\n id: `${prefix}.battery.permissions`,\n name: tName(\"batteryPermissions\"),\n type: \"string\",\n role: \"json\",\n value: JSON.stringify(record.permissions),\n write: true,\n changedOnly: true,\n });\n }\n // charge_to_full (API 2.3.0) \u2014 writable switch: charge all batteries to 100%.\n const chargeToFull = coerceBoolean(record.charge_to_full);\n if (chargeToFull !== null) {\n await this.ensureAndSet({\n id: `${prefix}.battery.charge_to_full`,\n name: tName(\"batteryChargeToFull\"),\n type: \"boolean\",\n role: \"switch\",\n value: chargeToFull,\n write: true,\n changedOnly: true,\n });\n }\n\n const numberFields: Array<{\n key: string;\n id: string;\n nameKey: I18nKey;\n role: string;\n unit?: string;\n }> = [\n { key: \"battery_count\", id: \"battery_count\", nameKey: \"batteryCount\", role: \"value\" },\n { key: \"power_w\", id: \"power_w\", nameKey: \"batteryPower\", role: \"value.power\", unit: \"W\" },\n { key: \"target_power_w\", id: \"target_power_w\", nameKey: \"batteryTargetPower\", role: \"value.power\", unit: \"W\" },\n {\n key: \"max_consumption_w\",\n id: \"max_consumption_w\",\n nameKey: \"batteryMaxConsumption\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"max_production_w\",\n id: \"max_production_w\",\n nameKey: \"batteryMaxProduction\",\n role: \"value.power\",\n unit: \"W\",\n },\n ];\n for (const field of numberFields) {\n const coerced = coerceFiniteNumber(record[field.key]);\n if (coerced !== null) {\n await this.ensureAndSet({\n id: `${prefix}.battery.${field.id}`,\n name: tName(field.nameKey),\n type: \"number\",\n role: field.role,\n value: coerced,\n unit: field.unit,\n changedOnly: true,\n });\n }\n }\n }\n\n /**\n * Set device connected state\n *\n * @param config Device configuration\n * @param connected Connection status\n */\n async setDeviceConnected(config: DeviceConfig, connected: boolean): Promise<void> {\n const prefix = this.devicePrefix(config);\n await this.adapter.setStateChangedAsync(`${prefix}.info.connected`, {\n val: connected,\n ack: true,\n });\n }\n\n /**\n * Remove all states for a device\n *\n * @param config Device configuration\n */\n async removeDevice(config: DeviceConfig): Promise<void> {\n const prefix = this.devicePrefix(config);\n this.adapter.log.debug(`state-manager: removeDevice ${prefix}`);\n await this.adapter.delObjectAsync(prefix, { recursive: true });\n // Drop cache entries belonging to this device \u2014 re-pairing the same\n // device must re-create channels/states from scratch.\n let dropped = 0;\n for (const id of this.createdIds) {\n if (id === prefix || id.startsWith(`${prefix}.`)) {\n this.createdIds.delete(id);\n dropped++;\n }\n }\n this.adapter.log.debug(`state-manager: removeDevice ${prefix} done (dropped ${dropped} cached IDs)`);\n }\n\n /**\n * Remove obsolete states: pre-v0.4.0 device-root paths (now under measurement/) plus\n * states retired in later versions (v0.11.0: raw P1 telegram).\n *\n * @param config Device configuration\n */\n async cleanupMovedStates(config: DeviceConfig): Promise<void> {\n const prefix = this.devicePrefix(config);\n this.adapter.log.debug(`state-manager: cleanupMovedStates ${prefix} (scanning pre-v0.4.0 paths)`);\n\n // Old paths: states were at device root, now under measurement/\n const oldIds: string[] = [];\n for (const def of MEASUREMENT_STATE_DEFS) {\n oldIds.push(`${prefix}.${def.id}`);\n }\n // External was at device root too\n oldIds.push(`${prefix}.external`);\n // Retired in v0.11.0: raw P1 telegram (DSMR passthrough, not part of the v2 data model)\n oldIds.push(`${prefix}.measurement.telegram`);\n\n let removed = 0;\n for (const id of oldIds) {\n if (await this.adapter.getObjectAsync(id)) {\n await this.adapter.delObjectAsync(id, { recursive: true });\n this.adapter.log.debug(`Removed obsolete state: ${id}`);\n removed++;\n }\n }\n if (removed > 0) {\n this.adapter.log.debug(`state-manager: cleanupMovedStates ${prefix} done (removed ${removed} obsolete paths)`);\n }\n }\n\n /**\n * Get device object ID prefix\n *\n * @param config Device configuration\n */\n devicePrefix(config: DeviceConfig): string {\n return `${sanitize(config.productType)}_${sanitize(config.serial)}`;\n }\n\n /**\n * Ensure a channel object exists. Skips the DB lookup once `id` is in the\n * cache \u2014 channels are static after first creation per device.\n *\n * @param id Full channel ID (`<prefix>.<channelName>`).\n * @param name Display name (translation object or device-supplied string).\n */\n private async ensureChannel(id: string, name: ioBroker.StringOrTranslated): Promise<void> {\n if (this.createdIds.has(id)) {\n return;\n }\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"channel\",\n common: { name },\n native: {},\n });\n this.createdIds.add(id);\n }\n\n /**\n * Create a state if it doesn't exist.\n *\n * @param def State definition (options object \u2014 avoids long positional argument lists).\n */\n private async createState(def: StateDef): Promise<void> {\n if (this.createdIds.has(def.id)) {\n return;\n }\n const common: Partial<ioBroker.StateCommon> = {\n name: def.name,\n type: def.type,\n role: def.role,\n read: true,\n write: def.write ?? false,\n };\n if (def.unit) {\n common.unit = def.unit;\n }\n if (def.desc) {\n common.desc = def.desc;\n }\n if (def.states) {\n common.states = def.states;\n }\n await this.adapter.setObjectNotExistsAsync(def.id, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {},\n });\n if (def.states) {\n // Existing datapoints from earlier releases may carry translation-object\n // VALUES in `common.states` (v0.7.0 introduced tLabel-as-string casts).\n // setObjectNotExistsAsync is a no-op for those \u2014 actively replace if any\n // value is not plain-string. Admin renders states-values as React child:\n // an object triggers React Error #31 \u2192 fatal \"Error in GUI\" on dropdown.\n await this.repairCommonStatesIfBuggy(def.id, def.states);\n }\n this.createdIds.add(def.id);\n }\n\n /**\n * If the persisted object at `id` has `common.states` values that are not\n * plain-string (= translation objects from older releases), replace\n * `common.states` with the fresh map via `setObjectAsync`. Otherwise no-op.\n *\n * `extendObjectAsync` deep-merges and CANNOT replace an object-value with\n * a string \u2014 only a full `setObjectAsync` replaces. Pattern proven in\n * hassemu v1.27.2 (URL-dropdown) and v1.28.4 (mode-dropdown).\n *\n * @param id State ID to repair.\n * @param fresh Plain-string `common.states` map to write.\n */\n private async repairCommonStatesIfBuggy(id: string, fresh: Record<string, string>): Promise<void> {\n const existing = await this.adapter.getObjectAsync(id);\n if (!existing) {\n return;\n }\n const states = existing.common?.states;\n if (!states || typeof states !== \"object\") {\n return;\n }\n const buggy = Object.values(states as Record<string, unknown>).some(v => typeof v !== \"string\");\n if (!buggy) {\n return;\n }\n existing.common = { ...existing.common, states: fresh } as ioBroker.StateCommon;\n await this.adapter.setObjectAsync(id, existing);\n }\n\n /**\n * Create a button state (read: false, write: true) with initial value false\n *\n * @param id State ID\n * @param name Button label (translation object)\n * @param desc Optional translation object for `common.desc`\n */\n private async createButton(\n id: string,\n name: ioBroker.StringOrTranslated,\n desc?: ioBroker.StringOrTranslated,\n ): Promise<void> {\n if (this.createdIds.has(id)) {\n return;\n }\n const common: Partial<ioBroker.StateCommon> = {\n name: name,\n type: \"boolean\",\n role: \"button\",\n read: false,\n write: true,\n };\n if (desc) {\n common.desc = desc;\n }\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {},\n });\n await this.adapter.setStateAsync(id, { val: false, ack: true });\n this.createdIds.add(id);\n }\n\n /**\n * Ensure a state exists and set its value.\n *\n * `changedOnly` routes through `setStateChangedAsync` (skips the write when the value is\n * unchanged) \u2014 used for slow/static fields (energy totals, system, battery control) so the\n * ~1/s push doesn't churn the DB. Momentary 1 Hz values (power/voltage/current/\u2026) stay on\n * `setStateAsync`. `changedOnly` also prevents double-writes when REST poll + WS push the\n * same field.\n *\n * @param def State definition + value + optional `changedOnly` flag.\n */\n private async ensureAndSet(def: StateSet): Promise<void> {\n await this.createState(def);\n if (def.changedOnly) {\n await this.adapter.setStateChangedAsync(def.id, { val: def.value, ack: true });\n } else {\n await this.adapter.setStateAsync(def.id, { val: def.value, ack: true });\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type * as utils from \"@iobroker/adapter-core\";\nimport { coerceBoolean, coerceFiniteNumber, coerceString, isPlainObject } from \"./coerce\";\nimport type { I18nKey } from \"./i18n\";\nimport { resolveLabel, tName } from \"./i18n\";\nimport type { BatteryControl, DeviceConfig, Measurement, SystemInfo } from \"./types\";\n\n/** Measurement field to state definition mapping */\ninterface MeasurementStateDef {\n /** Measurement field key */\n key: string;\n /** ioBroker state ID suffix */\n id: string;\n /** Translation key for `common.name` (resolved via {@link tName}) */\n nameKey: I18nKey;\n /** Optional translation key for `common.desc` (resolved via {@link tName}) */\n descKey?: I18nKey;\n /** State value type */\n type: ioBroker.CommonType;\n /** ioBroker role */\n role: string;\n /** Unit string */\n unit?: string;\n}\n\n/** Options for {@link StateManager.createState} (avoids long positional argument lists). */\ninterface StateDef {\n /** Full state ID */\n id: string;\n /** State name (translation object or device-identifier string) */\n name: ioBroker.StringOrTranslated;\n /** Value type */\n type: ioBroker.CommonType;\n /** ioBroker role */\n role: string;\n /** Whether the state is writable (default false) */\n write?: boolean;\n /** Optional unit */\n unit?: string;\n /** Optional `common.desc` */\n desc?: ioBroker.StringOrTranslated;\n /** Optional `common.states` map (plain-string values) */\n states?: Record<string, string>;\n}\n\n/** Options for {@link StateManager.ensureAndSet} \u2014 a {@link StateDef} plus the value to write. */\ninterface StateSet extends StateDef {\n /** Value to write */\n value: ioBroker.StateValue;\n /** Use setStateChangedAsync (skip redundant writes) instead of setStateAsync */\n changedOnly?: boolean;\n}\n\n/**\n * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).\n *\n * @param str Raw string to sanitize\n */\nfunction sanitize(str: string): string {\n return str.replace(/[^a-zA-Z0-9_-]/g, \"_\").toLowerCase();\n}\n\n// Exported for unit-tests only (invariant lock: every MOMENTARY_KEYS entry must\n// reference an existing def key \u2014 a typo would silently demote the field to\n// changed-only writes). Production code uses these via StateManager methods.\nexport const MEASUREMENT_STATE_DEFS: MeasurementStateDef[] = [\n // Power\n { key: \"power_w\", id: \"power_w\", nameKey: \"powerTotal\", type: \"number\", role: \"value.power\", unit: \"W\" },\n { key: \"power_l1_w\", id: \"power_l1_w\", nameKey: \"powerL1\", type: \"number\", role: \"value.power\", unit: \"W\" },\n { key: \"power_l2_w\", id: \"power_l2_w\", nameKey: \"powerL2\", type: \"number\", role: \"value.power\", unit: \"W\" },\n { key: \"power_l3_w\", id: \"power_l3_w\", nameKey: \"powerL3\", type: \"number\", role: \"value.power\", unit: \"W\" },\n // Voltage\n { key: \"voltage_v\", id: \"voltage_v\", nameKey: \"voltage\", type: \"number\", role: \"value.voltage\", unit: \"V\" },\n { key: \"voltage_l1_v\", id: \"voltage_l1_v\", nameKey: \"voltageL1\", type: \"number\", role: \"value.voltage\", unit: \"V\" },\n { key: \"voltage_l2_v\", id: \"voltage_l2_v\", nameKey: \"voltageL2\", type: \"number\", role: \"value.voltage\", unit: \"V\" },\n { key: \"voltage_l3_v\", id: \"voltage_l3_v\", nameKey: \"voltageL3\", type: \"number\", role: \"value.voltage\", unit: \"V\" },\n // Current\n { key: \"current_a\", id: \"current_a\", nameKey: \"current\", type: \"number\", role: \"value.current\", unit: \"A\" },\n { key: \"current_l1_a\", id: \"current_l1_a\", nameKey: \"currentL1\", type: \"number\", role: \"value.current\", unit: \"A\" },\n { key: \"current_l2_a\", id: \"current_l2_a\", nameKey: \"currentL2\", type: \"number\", role: \"value.current\", unit: \"A\" },\n { key: \"current_l3_a\", id: \"current_l3_a\", nameKey: \"currentL3\", type: \"number\", role: \"value.current\", unit: \"A\" },\n // Frequency\n { key: \"frequency_hz\", id: \"frequency_hz\", nameKey: \"frequency\", type: \"number\", role: \"value\", unit: \"Hz\" },\n // Energy import\n {\n key: \"energy_import_kwh\",\n id: \"energy_import_kwh\",\n nameKey: \"energyImportTotal\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_import_t1_kwh\",\n id: \"energy_import_t1_kwh\",\n nameKey: \"energyImportT1\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_import_t2_kwh\",\n id: \"energy_import_t2_kwh\",\n nameKey: \"energyImportT2\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_import_t3_kwh\",\n id: \"energy_import_t3_kwh\",\n nameKey: \"energyImportT3\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_import_t4_kwh\",\n id: \"energy_import_t4_kwh\",\n nameKey: \"energyImportT4\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n // Energy export\n {\n key: \"energy_export_kwh\",\n id: \"energy_export_kwh\",\n nameKey: \"energyExportTotal\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_export_t1_kwh\",\n id: \"energy_export_t1_kwh\",\n nameKey: \"energyExportT1\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_export_t2_kwh\",\n id: \"energy_export_t2_kwh\",\n nameKey: \"energyExportT2\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_export_t3_kwh\",\n id: \"energy_export_t3_kwh\",\n nameKey: \"energyExportT3\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n {\n key: \"energy_export_t4_kwh\",\n id: \"energy_export_t4_kwh\",\n nameKey: \"energyExportT4\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n // Tariff (common.states applied separately in updateMeasurement for translation labels)\n { key: \"tariff\", id: \"tariff\", nameKey: \"tariff\", type: \"number\", role: \"value\" },\n // Power quality\n {\n key: \"voltage_sag_l1_count\",\n id: \"quality.voltage_sag_l1_count\",\n nameKey: \"voltageSagL1\",\n descKey: \"voltageSag\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_sag_l2_count\",\n id: \"quality.voltage_sag_l2_count\",\n nameKey: \"voltageSagL2\",\n descKey: \"voltageSag\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_sag_l3_count\",\n id: \"quality.voltage_sag_l3_count\",\n nameKey: \"voltageSagL3\",\n descKey: \"voltageSag\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l1_count\",\n id: \"quality.voltage_swell_l1_count\",\n nameKey: \"voltageSwellL1\",\n descKey: \"voltageSwell\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l2_count\",\n id: \"quality.voltage_swell_l2_count\",\n nameKey: \"voltageSwellL2\",\n descKey: \"voltageSwell\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l3_count\",\n id: \"quality.voltage_swell_l3_count\",\n nameKey: \"voltageSwellL3\",\n descKey: \"voltageSwell\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"any_power_fail_count\",\n id: \"quality.power_fail_count\",\n nameKey: \"powerFailCount\",\n descKey: \"powerFailCountDesc\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"long_power_fail_count\",\n id: \"quality.long_power_fail_count\",\n nameKey: \"longPowerFailCount\",\n descKey: \"longPowerFailCountDesc\",\n type: \"number\",\n role: \"value\",\n },\n // Capacity tariff (Belgium)\n {\n key: \"average_power_15m_w\",\n id: \"average_power_15m_w\",\n nameKey: \"avgPower15m\",\n descKey: \"belgiumCapacityTariff\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"monthly_power_peak_w\",\n id: \"monthly_power_peak_w\",\n nameKey: \"monthlyPowerPeak\",\n descKey: \"belgiumCapacityTariff\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"monthly_power_peak_timestamp\",\n id: \"monthly_power_peak_timestamp\",\n nameKey: \"monthlyPowerPeakTimestamp\",\n descKey: \"belgiumCapacityTariff\",\n type: \"string\",\n role: \"date\",\n },\n // kWh meter specifics \u2014 apparent / reactive\n {\n key: \"apparent_current_a\",\n id: \"apparent_current_a\",\n nameKey: \"apparentCurrent\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_current_l1_a\",\n id: \"apparent_current_l1_a\",\n nameKey: \"apparentCurrentL1\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_current_l2_a\",\n id: \"apparent_current_l2_a\",\n nameKey: \"apparentCurrentL2\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_current_l3_a\",\n id: \"apparent_current_l3_a\",\n nameKey: \"apparentCurrentL3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_a\",\n id: \"reactive_current_a\",\n nameKey: \"reactiveCurrent\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_l1_a\",\n id: \"reactive_current_l1_a\",\n nameKey: \"reactiveCurrentL1\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_l2_a\",\n id: \"reactive_current_l2_a\",\n nameKey: \"reactiveCurrentL2\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_l3_a\",\n id: \"reactive_current_l3_a\",\n nameKey: \"reactiveCurrentL3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_power_va\",\n id: \"apparent_power_va\",\n nameKey: \"apparentPower\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"apparent_power_l1_va\",\n id: \"apparent_power_l1_va\",\n nameKey: \"apparentPowerL1\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"apparent_power_l2_va\",\n id: \"apparent_power_l2_va\",\n nameKey: \"apparentPowerL2\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"apparent_power_l3_va\",\n id: \"apparent_power_l3_va\",\n nameKey: \"apparentPowerL3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"reactive_power_var\",\n id: \"reactive_power_var\",\n nameKey: \"reactivePower\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"reactive_power_l1_var\",\n id: \"reactive_power_l1_var\",\n nameKey: \"reactivePowerL1\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"reactive_power_l2_var\",\n id: \"reactive_power_l2_var\",\n nameKey: \"reactivePowerL2\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"reactive_power_l3_var\",\n id: \"reactive_power_l3_var\",\n nameKey: \"reactivePowerL3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"power_factor\",\n id: \"power_factor\",\n nameKey: \"powerFactor\",\n descKey: \"powerFactorDesc\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l1\",\n id: \"power_factor_l1\",\n nameKey: \"powerFactorL1\",\n descKey: \"powerFactorDesc\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l2\",\n id: \"power_factor_l2\",\n nameKey: \"powerFactorL2\",\n descKey: \"powerFactorDesc\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l3\",\n id: \"power_factor_l3\",\n nameKey: \"powerFactorL3\",\n descKey: \"powerFactorDesc\",\n type: \"number\",\n role: \"value\",\n },\n // Battery specifics\n {\n key: \"state_of_charge_pct\",\n id: \"state_of_charge_pct\",\n nameKey: \"stateOfCharge\",\n type: \"number\",\n role: \"value.battery\",\n unit: \"%\",\n },\n { key: \"cycles\", id: \"cycles\", nameKey: \"cycles\", type: \"number\", role: \"value\" },\n // Metadata\n { key: \"meter_model\", id: \"meter_model\", nameKey: \"meterModel\", type: \"string\", role: \"text\" },\n { key: \"timestamp\", id: \"timestamp\", nameKey: \"measurementTimestamp\", type: \"string\", role: \"date\" },\n];\n\n// Instantaneous electrical values \u2014 change on (almost) every ~1/s push, so a setStateChanged\n// read-compare buys nothing. These stay on setStateAsync; every other measurement field\n// (energy totals, tariff, power-quality counts, capacity tariff, SoC/cycles, model/timestamp)\n// is slow/static and uses setStateChangedAsync to skip redundant 1/s writes.\n// Exported for unit-tests only (subset-invariant against MEASUREMENT_STATE_DEFS).\nexport const MOMENTARY_KEYS = new Set<string>([\n \"power_w\",\n \"power_l1_w\",\n \"power_l2_w\",\n \"power_l3_w\",\n \"voltage_v\",\n \"voltage_l1_v\",\n \"voltage_l2_v\",\n \"voltage_l3_v\",\n \"current_a\",\n \"current_l1_a\",\n \"current_l2_a\",\n \"current_l3_a\",\n \"frequency_hz\",\n \"apparent_current_a\",\n \"apparent_current_l1_a\",\n \"apparent_current_l2_a\",\n \"apparent_current_l3_a\",\n \"reactive_current_a\",\n \"reactive_current_l1_a\",\n \"reactive_current_l2_a\",\n \"reactive_current_l3_a\",\n \"apparent_power_va\",\n \"apparent_power_l1_va\",\n \"apparent_power_l2_va\",\n \"apparent_power_l3_va\",\n \"reactive_power_var\",\n \"reactive_power_l1_var\",\n \"reactive_power_l2_var\",\n \"reactive_power_l3_var\",\n \"power_factor\",\n \"power_factor_l1\",\n \"power_factor_l2\",\n \"power_factor_l3\",\n]);\n\n/**\n * Build a `common.states` map for tariff (T1-T4) with plain-string labels.\n *\n * **VALUES MUST be plain-string** \u2014 Admin renders states-values as React\n * children. Translation objects trigger React Error #31 \u2192 fatal \"Error in GUI\"\n * on dropdown open (verified hassemu v1.28.4, 2026-05-12).\n *\n */\n// Cached after first build \u2014 the system language is fixed for the adapter run\n// (I18n.init runs once in onReady), so these label maps never change at runtime.\n// Avoids rebuilding the object on every ~1/s measurement push.\nlet tariffStatesCache: Record<string, string> | null = null;\nfunction tariffStates(): Record<string, string> {\n return (tariffStatesCache ??= {\n 1: resolveLabel(\"tariff1\"),\n 2: resolveLabel(\"tariff2\"),\n 3: resolveLabel(\"tariff3\"),\n 4: resolveLabel(\"tariff4\"),\n });\n}\n\n/**\n * Build a `common.states` map for HWE-BAT battery.mode with plain-string labels.\n * Same constraint + same memoization as {@link tariffStates}. `predictive` since API 2.3.0.\n */\nlet batteryModeStatesCache: Record<string, string> | null = null;\nfunction batteryModeStates(): Record<string, string> {\n return (batteryModeStatesCache ??= {\n zero: resolveLabel(\"modeZero\"),\n to_full: resolveLabel(\"modeToFull\"),\n standby: resolveLabel(\"modeStandby\"),\n predictive: resolveLabel(\"modePredictive\"),\n });\n}\n\n/** Manages ioBroker state creation and updates for HomeWizard devices */\nexport class StateManager {\n private readonly adapter: utils.AdapterInstance;\n /**\n * Cache of state / channel IDs that have already passed\n * `setObjectNotExistsAsync`. Skips repeat DB lookups on the hot path \u2014\n * a P1 meter pushes ~1 measurement/s with up to ~30 active fields, which\n * otherwise meant ~30 Redis lookups per second just to ask \u201Edoes it\n * exist\". On `removeDevice(prefix)` all `prefix.*` IDs are dropped.\n */\n private readonly createdIds = new Set<string>();\n\n /** @param adapter The ioBroker adapter instance */\n constructor(adapter: utils.AdapterInstance) {\n this.adapter = adapter;\n }\n\n /**\n * Create device channel and info states\n *\n * @param config Device configuration\n */\n async createDeviceStates(config: DeviceConfig): Promise<void> {\n const prefix = this.devicePrefix(config);\n\n this.adapter.log.debug(`state-manager: createDeviceStates ${prefix} (productType=${config.productType})`);\n\n // Device-Object: common.name keeps the user-supplied product name (or product type as fallback) \u2014\n // these are device-specific identifiers, NOT translatable.\n await this.adapter.extendObjectAsync(\n prefix,\n {\n type: \"device\",\n common: {\n name: config.productName || config.productType,\n statusStates: {\n onlineId: `${this.adapter.namespace}.${prefix}.info.connected`,\n },\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n\n await this.adapter.extendObjectAsync(\n `${prefix}.info`,\n {\n type: \"channel\",\n common: { name: tName(\"deviceInformation\") },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n\n await this.createState({\n id: `${prefix}.info.productName`,\n name: tName(\"productName\"),\n type: \"string\",\n role: \"text\",\n });\n await this.createState({\n id: `${prefix}.info.productType`,\n name: tName(\"productType\"),\n type: \"string\",\n role: \"text\",\n });\n await this.createState({ id: `${prefix}.info.firmware`, name: tName(\"firmware\"), type: \"string\", role: \"text\" });\n await this.createState({\n id: `${prefix}.info.connected`,\n name: tName(\"connected\"),\n type: \"boolean\",\n role: \"indicator.reachable\",\n });\n await this.createState({ id: `${prefix}.info.wifi_ssid`, name: tName(\"wifiSsid\"), type: \"string\", role: \"text\" });\n await this.createState({\n id: `${prefix}.info.wifi_rssi_db`,\n name: tName(\"wifiRssi\"),\n type: \"number\",\n role: \"value\",\n unit: \"dBm\",\n });\n await this.createState({\n id: `${prefix}.info.uptime_s`,\n name: tName(\"uptime\"),\n type: \"number\",\n role: \"value\",\n unit: \"s\",\n });\n\n // Remove device button\n await this.createButton(`${prefix}.remove`, tName(\"removeDevice\"), tName(\"removeDeviceDesc\"));\n\n // Set initial info values\n await this.adapter.setStateAsync(`${prefix}.info.productName`, {\n val: config.productName,\n ack: true,\n });\n await this.adapter.setStateAsync(`${prefix}.info.productType`, {\n val: config.productType,\n ack: true,\n });\n }\n\n /**\n * Update measurement states \u2014 only creates states that have values\n *\n * @param config Device configuration\n * @param data Measurement data\n */\n async updateMeasurement(config: DeviceConfig, data: Measurement): Promise<void> {\n if (!isPlainObject(data)) {\n return;\n }\n const prefix = this.devicePrefix(config);\n const mPrefix = `${prefix}.measurement`;\n\n // Ensure measurement channel exists (cached after first call per device)\n await this.ensureChannel(mPrefix, tName(\"measurement\"));\n\n // Main measurement values \u2014 coerce per declared type. Once a state's object\n // is in the cache, ensureAndSet only does one setStateAsync per field \u2014 those\n // are independent and run in parallel via Promise.all instead of sequentially.\n const record = data;\n const writes: Promise<void>[] = [];\n for (const def of MEASUREMENT_STATE_DEFS) {\n const raw = record[def.key];\n let coerced: number | string | null = null;\n if (def.type === \"number\") {\n coerced = coerceFiniteNumber(raw);\n } else if (def.type === \"string\") {\n coerced = coerceString(raw);\n }\n if (coerced !== null) {\n writes.push(\n this.ensureAndSet({\n id: `${mPrefix}.${def.id}`,\n name: tName(def.nameKey),\n type: def.type,\n role: def.role,\n value: coerced,\n unit: def.unit,\n desc: def.descKey ? tName(def.descKey) : undefined,\n states: def.key === \"tariff\" ? tariffStates() : undefined,\n changedOnly: !MOMENTARY_KEYS.has(def.key),\n }),\n );\n }\n }\n await Promise.all(writes);\n\n // External meters (P1 gas/water/heat) \u2014 channel-create paths must run sequentially\n // because the parent `external` channel must exist before the per-meter channel\n // and the per-meter value/unit/timestamp states. Inside one meter, the three\n // value/unit/timestamp writes are independent and run in parallel.\n const external = record.external;\n if (Array.isArray(external) && external.length > 0) {\n for (const rawExt of external) {\n if (!isPlainObject(rawExt)) {\n continue;\n }\n const type = coerceString(rawExt.type);\n const uniqueId = coerceString(rawExt.unique_id);\n if (!type || !uniqueId) {\n continue;\n }\n\n const value = coerceFiniteNumber(rawExt.value);\n const unit = coerceString(rawExt.unit);\n const timestamp = coerceString(rawExt.timestamp);\n\n await this.ensureChannel(`${mPrefix}.external`, tName(\"externalMeters\"));\n\n const extId = `${mPrefix}.external.${sanitize(type)}_${sanitize(uniqueId)}`;\n // External meter channel keeps the device-supplied type (e.g. \"gas_meter\")\n // as channel name \u2014 identifies the physical meter, not localizable.\n await this.ensureChannel(extId, type);\n\n const extWrites: Promise<void>[] = [];\n if (value !== null) {\n extWrites.push(\n this.ensureAndSet({\n id: `${extId}.value`,\n name: tName(\"externalValue\"),\n type: \"number\",\n role: \"value\",\n value,\n unit: unit ?? undefined,\n changedOnly: true,\n }),\n );\n }\n if (unit) {\n extWrites.push(\n this.ensureAndSet({\n id: `${extId}.unit`,\n name: tName(\"externalUnit\"),\n type: \"string\",\n role: \"text\",\n value: unit,\n changedOnly: true,\n }),\n );\n }\n if (timestamp) {\n extWrites.push(\n this.ensureAndSet({\n id: `${extId}.timestamp`,\n name: tName(\"externalTimestamp\"),\n type: \"string\",\n role: \"date\",\n value: timestamp,\n changedOnly: true,\n }),\n );\n }\n await Promise.all(extWrites);\n }\n }\n }\n\n /**\n * Update system states\n *\n * @param config Device configuration\n * @param system System info data\n */\n async updateSystem(config: DeviceConfig, system: SystemInfo): Promise<void> {\n if (!isPlainObject(system)) {\n return;\n }\n const prefix = this.devicePrefix(config);\n const record = system as Record<string, unknown>;\n\n // WiFi SSID/RSSI + uptime in info channel \u2014 slow-changing \u2192 changedOnly.\n const ssid = coerceString(record.wifi_ssid);\n if (ssid !== null) {\n await this.ensureAndSet({\n id: `${prefix}.info.wifi_ssid`,\n name: tName(\"wifiSsid\"),\n type: \"string\",\n role: \"text\",\n value: ssid,\n changedOnly: true,\n });\n }\n const rssi = coerceFiniteNumber(record.wifi_rssi_db);\n if (rssi !== null) {\n await this.ensureAndSet({\n id: `${prefix}.info.wifi_rssi_db`,\n name: tName(\"wifiRssi\"),\n type: \"number\",\n role: \"value\",\n value: rssi,\n unit: \"dBm\",\n changedOnly: true,\n });\n }\n const uptime = coerceFiniteNumber(record.uptime_s);\n if (uptime !== null) {\n await this.ensureAndSet({\n id: `${prefix}.info.uptime_s`,\n name: tName(\"uptime\"),\n type: \"number\",\n role: \"value\",\n value: uptime,\n unit: \"s\",\n changedOnly: true,\n });\n }\n\n // System control channel (cached after first call per device)\n await this.ensureChannel(`${prefix}.system`, tName(\"systemSettings\"));\n\n // HWE-BAT: cloud_enabled is read-only (always true) and reboot is unsupported.\n const isBattery = config.productType === \"HWE-BAT\";\n\n const cloudEnabled = coerceBoolean(record.cloud_enabled);\n if (cloudEnabled !== null) {\n await this.ensureAndSet({\n id: `${prefix}.system.cloud_enabled`,\n name: tName(\"cloudEnabled\"),\n type: \"boolean\",\n role: \"switch\",\n value: cloudEnabled,\n write: !isBattery,\n changedOnly: true,\n });\n }\n const ledPct = coerceFiniteNumber(record.status_led_brightness_pct);\n if (ledPct !== null) {\n await this.ensureAndSet({\n id: `${prefix}.system.status_led_brightness_pct`,\n name: tName(\"ledBrightness\"),\n type: \"number\",\n role: \"level\",\n value: ledPct,\n unit: \"%\",\n write: true,\n changedOnly: true,\n });\n }\n\n const apiV1 = coerceBoolean(record.api_v1_enabled);\n if (apiV1 !== null) {\n await this.ensureAndSet({\n id: `${prefix}.system.api_v1_enabled`,\n name: tName(\"apiV1Enabled\"),\n type: \"boolean\",\n role: \"switch\",\n value: apiV1,\n write: true,\n changedOnly: true,\n });\n }\n\n // Action buttons (reboot is unsupported on the Plug-In Battery)\n if (!isBattery) {\n await this.createButton(`${prefix}.system.reboot`, tName(\"rebootDevice\"));\n }\n await this.createButton(`${prefix}.system.identify`, tName(\"identify\"));\n }\n\n /**\n * Update battery control states\n *\n * @param config Device configuration\n * @param battery Battery control data\n */\n async updateBattery(config: DeviceConfig, battery: BatteryControl): Promise<void> {\n if (!isPlainObject(battery)) {\n return;\n }\n const prefix = this.devicePrefix(config);\n const record = battery as Record<string, unknown>;\n\n await this.ensureChannel(`${prefix}.battery`, tName(\"batteryControl\"));\n\n const mode = coerceString(record.mode);\n if (mode) {\n await this.ensureAndSet({\n id: `${prefix}.battery.mode`,\n name: tName(\"batteryMode\"),\n type: \"string\",\n role: \"text\",\n value: mode,\n write: true,\n desc: tName(\"batteryModeDesc\"),\n states: batteryModeStates(),\n changedOnly: true,\n });\n }\n if (Array.isArray(record.permissions)) {\n await this.ensureAndSet({\n id: `${prefix}.battery.permissions`,\n name: tName(\"batteryPermissions\"),\n type: \"string\",\n role: \"json\",\n value: JSON.stringify(record.permissions),\n write: true,\n changedOnly: true,\n });\n }\n // charge_to_full (API 2.3.0) \u2014 writable switch: charge all batteries to 100%.\n const chargeToFull = coerceBoolean(record.charge_to_full);\n if (chargeToFull !== null) {\n await this.ensureAndSet({\n id: `${prefix}.battery.charge_to_full`,\n name: tName(\"batteryChargeToFull\"),\n type: \"boolean\",\n role: \"switch\",\n value: chargeToFull,\n write: true,\n changedOnly: true,\n });\n }\n\n const numberFields: Array<{\n key: string;\n id: string;\n nameKey: I18nKey;\n role: string;\n unit?: string;\n }> = [\n { key: \"battery_count\", id: \"battery_count\", nameKey: \"batteryCount\", role: \"value\" },\n { key: \"power_w\", id: \"power_w\", nameKey: \"batteryPower\", role: \"value.power\", unit: \"W\" },\n { key: \"target_power_w\", id: \"target_power_w\", nameKey: \"batteryTargetPower\", role: \"value.power\", unit: \"W\" },\n {\n key: \"max_consumption_w\",\n id: \"max_consumption_w\",\n nameKey: \"batteryMaxConsumption\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"max_production_w\",\n id: \"max_production_w\",\n nameKey: \"batteryMaxProduction\",\n role: \"value.power\",\n unit: \"W\",\n },\n ];\n for (const field of numberFields) {\n const coerced = coerceFiniteNumber(record[field.key]);\n if (coerced !== null) {\n await this.ensureAndSet({\n id: `${prefix}.battery.${field.id}`,\n name: tName(field.nameKey),\n type: \"number\",\n role: field.role,\n value: coerced,\n unit: field.unit,\n changedOnly: true,\n });\n }\n }\n }\n\n /**\n * Set device connected state\n *\n * @param config Device configuration\n * @param connected Connection status\n */\n async setDeviceConnected(config: DeviceConfig, connected: boolean): Promise<void> {\n const prefix = this.devicePrefix(config);\n await this.adapter.setStateChangedAsync(`${prefix}.info.connected`, {\n val: connected,\n ack: true,\n });\n }\n\n /**\n * Remove all states for a device\n *\n * @param config Device configuration\n */\n async removeDevice(config: DeviceConfig): Promise<void> {\n const prefix = this.devicePrefix(config);\n this.adapter.log.debug(`state-manager: removeDevice ${prefix}`);\n await this.adapter.delObjectAsync(prefix, { recursive: true });\n // Drop cache entries belonging to this device \u2014 re-pairing the same\n // device must re-create channels/states from scratch.\n let dropped = 0;\n for (const id of this.createdIds) {\n if (id === prefix || id.startsWith(`${prefix}.`)) {\n this.createdIds.delete(id);\n dropped++;\n }\n }\n this.adapter.log.debug(`state-manager: removeDevice ${prefix} done (dropped ${dropped} cached IDs)`);\n }\n\n /**\n * Remove obsolete states: pre-v0.4.0 device-root paths (now under measurement/) plus\n * states retired in later versions (v0.11.0: raw P1 telegram).\n *\n * @param config Device configuration\n */\n async cleanupMovedStates(config: DeviceConfig): Promise<void> {\n const prefix = this.devicePrefix(config);\n this.adapter.log.debug(`state-manager: cleanupMovedStates ${prefix} (scanning pre-v0.4.0 paths)`);\n\n // Old paths: states were at device root, now under measurement/\n const oldIds: string[] = [];\n for (const def of MEASUREMENT_STATE_DEFS) {\n oldIds.push(`${prefix}.${def.id}`);\n }\n // External was at device root too\n oldIds.push(`${prefix}.external`);\n // Retired in v0.11.0: raw P1 telegram (DSMR passthrough, not part of the v2 data model)\n oldIds.push(`${prefix}.measurement.telegram`);\n\n let removed = 0;\n for (const id of oldIds) {\n if (await this.adapter.getObjectAsync(id)) {\n await this.adapter.delObjectAsync(id, { recursive: true });\n this.adapter.log.debug(`Removed obsolete state: ${id}`);\n removed++;\n }\n }\n if (removed > 0) {\n this.adapter.log.debug(`state-manager: cleanupMovedStates ${prefix} done (removed ${removed} obsolete paths)`);\n }\n }\n\n /**\n * Get device object ID prefix\n *\n * @param config Device configuration\n */\n devicePrefix(config: DeviceConfig): string {\n return `${sanitize(config.productType)}_${sanitize(config.serial)}`;\n }\n\n /**\n * Ensure a channel object exists. Skips the DB lookup once `id` is in the\n * cache \u2014 channels are static after first creation per device.\n *\n * @param id Full channel ID (`<prefix>.<channelName>`).\n * @param name Display name (translation object or device-supplied string).\n */\n private async ensureChannel(id: string, name: ioBroker.StringOrTranslated): Promise<void> {\n if (this.createdIds.has(id)) {\n return;\n }\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"channel\",\n common: { name },\n native: {},\n });\n this.createdIds.add(id);\n }\n\n /**\n * Create a state if it doesn't exist.\n *\n * @param def State definition (options object \u2014 avoids long positional argument lists).\n */\n private async createState(def: StateDef): Promise<void> {\n if (this.createdIds.has(def.id)) {\n return;\n }\n const common: Partial<ioBroker.StateCommon> = {\n name: def.name,\n type: def.type,\n role: def.role,\n read: true,\n write: def.write ?? false,\n };\n if (def.unit) {\n common.unit = def.unit;\n }\n if (def.desc) {\n common.desc = def.desc;\n }\n if (def.states) {\n common.states = def.states;\n }\n await this.adapter.setObjectNotExistsAsync(def.id, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {},\n });\n if (def.states) {\n // Existing datapoints from earlier releases may carry translation-object\n // VALUES in `common.states` (v0.7.0 introduced tLabel-as-string casts).\n // setObjectNotExistsAsync is a no-op for those \u2014 actively replace if any\n // value is not plain-string. Admin renders states-values as React child:\n // an object triggers React Error #31 \u2192 fatal \"Error in GUI\" on dropdown.\n await this.repairCommonStatesIfBuggy(def.id, def.states);\n }\n this.createdIds.add(def.id);\n }\n\n /**\n * If the persisted object at `id` has `common.states` values that are not\n * plain-string (= translation objects from older releases), replace\n * `common.states` with the fresh map via `setObjectAsync`. Otherwise no-op.\n *\n * `extendObjectAsync` deep-merges and CANNOT replace an object-value with\n * a string \u2014 only a full `setObjectAsync` replaces. Pattern proven in\n * hassemu v1.27.2 (URL-dropdown) and v1.28.4 (mode-dropdown).\n *\n * @param id State ID to repair.\n * @param fresh Plain-string `common.states` map to write.\n */\n private async repairCommonStatesIfBuggy(id: string, fresh: Record<string, string>): Promise<void> {\n const existing = await this.adapter.getObjectAsync(id);\n if (!existing) {\n return;\n }\n const states = existing.common?.states;\n if (!states || typeof states !== \"object\") {\n return;\n }\n const buggy = Object.values(states as Record<string, unknown>).some(v => typeof v !== \"string\");\n if (!buggy) {\n return;\n }\n existing.common = { ...existing.common, states: fresh } as ioBroker.StateCommon;\n await this.adapter.setObjectAsync(id, existing);\n }\n\n /**\n * Create a button state (read: false, write: true) with initial value false\n *\n * @param id State ID\n * @param name Button label (translation object)\n * @param desc Optional translation object for `common.desc`\n */\n private async createButton(\n id: string,\n name: ioBroker.StringOrTranslated,\n desc?: ioBroker.StringOrTranslated,\n ): Promise<void> {\n if (this.createdIds.has(id)) {\n return;\n }\n const common: Partial<ioBroker.StateCommon> = {\n name: name,\n type: \"boolean\",\n role: \"button\",\n read: false,\n write: true,\n };\n if (desc) {\n common.desc = desc;\n }\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {},\n });\n await this.adapter.setStateAsync(id, { val: false, ack: true });\n this.createdIds.add(id);\n }\n\n /**\n * Ensure a state exists and set its value.\n *\n * `changedOnly` routes through `setStateChangedAsync` (skips the write when the value is\n * unchanged) \u2014 used for slow/static fields (energy totals, system, battery control) so the\n * ~1/s push doesn't churn the DB. Momentary 1 Hz values (power/voltage/current/\u2026) stay on\n * `setStateAsync`. `changedOnly` also prevents double-writes when REST poll + WS push the\n * same field.\n *\n * @param def State definition + value + optional `changedOnly` flag.\n */\n private async ensureAndSet(def: StateSet): Promise<void> {\n await this.createState(def);\n if (def.changedOnly) {\n await this.adapter.setStateChangedAsync(def.id, { val: def.value, ack: true });\n } else {\n await this.adapter.setStateAsync(def.id, { val: def.value, ack: true });\n }\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAA+E;AAE/E,kBAAoC;AAsDpC,SAAS,SAAS,KAAqB;AACrC,SAAO,IAAI,QAAQ,mBAAmB,GAAG,EAAE,YAAY;AACzD;AAKO,MAAM,yBAAgD;AAAA;AAAA,EAE3D,EAAE,KAAK,WAAW,IAAI,WAAW,SAAS,cAAc,MAAM,UAAU,MAAM,eAAe,MAAM,IAAI;AAAA,EACvG,EAAE,KAAK,cAAc,IAAI,cAAc,SAAS,WAAW,MAAM,UAAU,MAAM,eAAe,MAAM,IAAI;AAAA,EAC1G,EAAE,KAAK,cAAc,IAAI,cAAc,SAAS,WAAW,MAAM,UAAU,MAAM,eAAe,MAAM,IAAI;AAAA,EAC1G,EAAE,KAAK,cAAc,IAAI,cAAc,SAAS,WAAW,MAAM,UAAU,MAAM,eAAe,MAAM,IAAI;AAAA;AAAA,EAE1G,EAAE,KAAK,aAAa,IAAI,aAAa,SAAS,WAAW,MAAM,UAAU,MAAM,iBAAiB,MAAM,IAAI;AAAA,EAC1G,EAAE,KAAK,gBAAgB,IAAI,gBAAgB,SAAS,aAAa,MAAM,UAAU,MAAM,iBAAiB,MAAM,IAAI;AAAA,EAClH,EAAE,KAAK,gBAAgB,IAAI,gBAAgB,SAAS,aAAa,MAAM,UAAU,MAAM,iBAAiB,MAAM,IAAI;AAAA,EAClH,EAAE,KAAK,gBAAgB,IAAI,gBAAgB,SAAS,aAAa,MAAM,UAAU,MAAM,iBAAiB,MAAM,IAAI;AAAA;AAAA,EAElH,EAAE,KAAK,aAAa,IAAI,aAAa,SAAS,WAAW,MAAM,UAAU,MAAM,iBAAiB,MAAM,IAAI;AAAA,EAC1G,EAAE,KAAK,gBAAgB,IAAI,gBAAgB,SAAS,aAAa,MAAM,UAAU,MAAM,iBAAiB,MAAM,IAAI;AAAA,EAClH,EAAE,KAAK,gBAAgB,IAAI,gBAAgB,SAAS,aAAa,MAAM,UAAU,MAAM,iBAAiB,MAAM,IAAI;AAAA,EAClH,EAAE,KAAK,gBAAgB,IAAI,gBAAgB,SAAS,aAAa,MAAM,UAAU,MAAM,iBAAiB,MAAM,IAAI;AAAA;AAAA,EAElH,EAAE,KAAK,gBAAgB,IAAI,gBAAgB,SAAS,aAAa,MAAM,UAAU,MAAM,SAAS,MAAM,KAAK;AAAA;AAAA,EAE3G;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA;AAAA,EAEA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA;AAAA,EAEA,EAAE,KAAK,UAAU,IAAI,UAAU,SAAS,UAAU,MAAM,UAAU,MAAM,QAAQ;AAAA;AAAA,EAEhF;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA;AAAA,EAEA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA;AAAA,EAEA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA;AAAA,EAEA;AAAA,IACE,KAAK;AAAA,IACL,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,EAAE,KAAK,UAAU,IAAI,UAAU,SAAS,UAAU,MAAM,UAAU,MAAM,QAAQ;AAAA;AAAA,EAEhF,EAAE,KAAK,eAAe,IAAI,eAAe,SAAS,cAAc,MAAM,UAAU,MAAM,OAAO;AAAA,EAC7F,EAAE,KAAK,aAAa,IAAI,aAAa,SAAS,wBAAwB,MAAM,UAAU,MAAM,OAAO;AACrG;AAOO,MAAM,iBAAiB,oBAAI,IAAY;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAaD,IAAI,oBAAmD;AACvD,SAAS,eAAuC;AAC9C,SAAQ,oEAAsB;AAAA,IAC5B,OAAG,0BAAa,SAAS;AAAA,IACzB,OAAG,0BAAa,SAAS;AAAA,IACzB,OAAG,0BAAa,SAAS;AAAA,IACzB,OAAG,0BAAa,SAAS;AAAA,EAC3B;AACF;AAMA,IAAI,yBAAwD;AAC5D,SAAS,oBAA4C;AACnD,SAAQ,mFAA2B;AAAA,IACjC,UAAM,0BAAa,UAAU;AAAA,IAC7B,aAAS,0BAAa,YAAY;AAAA,IAClC,aAAS,0BAAa,aAAa;AAAA,IACnC,gBAAY,0BAAa,gBAAgB;AAAA,EAC3C;AACF;AAGO,MAAM,aAAa;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAa,oBAAI,IAAY;AAAA;AAAA,EAG9C,YAAY,SAAgC;AAC1C,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,mBAAmB,QAAqC;AAC5D,UAAM,SAAS,KAAK,aAAa,MAAM;AAEvC,SAAK,QAAQ,IAAI,MAAM,qCAAqC,MAAM,iBAAiB,OAAO,WAAW,GAAG;AAIxG,UAAM,KAAK,QAAQ;AAAA,MACjB;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,QAAQ;AAAA,UACN,MAAM,OAAO,eAAe,OAAO;AAAA,UACnC,cAAc;AAAA,YACZ,UAAU,GAAG,KAAK,QAAQ,SAAS,IAAI,MAAM;AAAA,UAC/C;AAAA,QACF;AAAA,QACA,QAAQ,CAAC;AAAA,MACX;AAAA,MACA,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE;AAAA,IACnC;AAEA,UAAM,KAAK,QAAQ;AAAA,MACjB,GAAG,MAAM;AAAA,MACT;AAAA,QACE,MAAM;AAAA,QACN,QAAQ,EAAE,UAAM,mBAAM,mBAAmB,EAAE;AAAA,QAC3C,QAAQ,CAAC;AAAA,MACX;AAAA,MACA,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE;AAAA,IACnC;AAEA,UAAM,KAAK,YAAY;AAAA,MACrB,IAAI,GAAG,MAAM;AAAA,MACb,UAAM,mBAAM,aAAa;AAAA,MACzB,MAAM;AAAA,MACN,MAAM;AAAA,IACR,CAAC;AACD,UAAM,KAAK,YAAY;AAAA,MACrB,IAAI,GAAG,MAAM;AAAA,MACb,UAAM,mBAAM,aAAa;AAAA,MACzB,MAAM;AAAA,MACN,MAAM;AAAA,IACR,CAAC;AACD,UAAM,KAAK,YAAY,EAAE,IAAI,GAAG,MAAM,kBAAkB,UAAM,mBAAM,UAAU,GAAG,MAAM,UAAU,MAAM,OAAO,CAAC;AAC/G,UAAM,KAAK,YAAY;AAAA,MACrB,IAAI,GAAG,MAAM;AAAA,MACb,UAAM,mBAAM,WAAW;AAAA,MACvB,MAAM;AAAA,MACN,MAAM;AAAA,IACR,CAAC;AACD,UAAM,KAAK,YAAY,EAAE,IAAI,GAAG,MAAM,mBAAmB,UAAM,mBAAM,UAAU,GAAG,MAAM,UAAU,MAAM,OAAO,CAAC;AAChH,UAAM,KAAK,YAAY;AAAA,MACrB,IAAI,GAAG,MAAM;AAAA,MACb,UAAM,mBAAM,UAAU;AAAA,MACtB,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,IACR,CAAC;AACD,UAAM,KAAK,YAAY;AAAA,MACrB,IAAI,GAAG,MAAM;AAAA,MACb,UAAM,mBAAM,QAAQ;AAAA,MACpB,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,IACR,CAAC;AAGD,UAAM,KAAK,aAAa,GAAG,MAAM,eAAW,mBAAM,cAAc,OAAG,mBAAM,kBAAkB,CAAC;AAG5F,UAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,qBAAqB;AAAA,MAC7D,KAAK,OAAO;AAAA,MACZ,KAAK;AAAA,IACP,CAAC;AACD,UAAM,KAAK,QAAQ,cAAc,GAAG,MAAM,qBAAqB;AAAA,MAC7D,KAAK,OAAO;AAAA,MACZ,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,kBAAkB,QAAsB,MAAkC;AAC9E,QAAI,KAAC,6BAAc,IAAI,GAAG;AACxB;AAAA,IACF;AACA,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,UAAM,UAAU,GAAG,MAAM;AAGzB,UAAM,KAAK,cAAc,aAAS,mBAAM,aAAa,CAAC;AAKtD,UAAM,SAAS;AACf,UAAM,SAA0B,CAAC;AACjC,eAAW,OAAO,wBAAwB;AACxC,YAAM,MAAM,OAAO,IAAI,GAAG;AAC1B,UAAI,UAAkC;AACtC,UAAI,IAAI,SAAS,UAAU;AACzB,sBAAU,kCAAmB,GAAG;AAAA,MAClC,WAAW,IAAI,SAAS,UAAU;AAChC,sBAAU,4BAAa,GAAG;AAAA,MAC5B;AACA,UAAI,YAAY,MAAM;AACpB,eAAO;AAAA,UACL,KAAK,aAAa;AAAA,YAChB,IAAI,GAAG,OAAO,IAAI,IAAI,EAAE;AAAA,YACxB,UAAM,mBAAM,IAAI,OAAO;AAAA,YACvB,MAAM,IAAI;AAAA,YACV,MAAM,IAAI;AAAA,YACV,OAAO;AAAA,YACP,MAAM,IAAI;AAAA,YACV,MAAM,IAAI,cAAU,mBAAM,IAAI,OAAO,IAAI;AAAA,YACzC,QAAQ,IAAI,QAAQ,WAAW,aAAa,IAAI;AAAA,YAChD,aAAa,CAAC,eAAe,IAAI,IAAI,GAAG;AAAA,UAC1C,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AACA,UAAM,QAAQ,IAAI,MAAM;AAMxB,UAAM,WAAW,OAAO;AACxB,QAAI,MAAM,QAAQ,QAAQ,KAAK,SAAS,SAAS,GAAG;AAClD,iBAAW,UAAU,UAAU;AAC7B,YAAI,KAAC,6BAAc,MAAM,GAAG;AAC1B;AAAA,QACF;AACA,cAAM,WAAO,4BAAa,OAAO,IAAI;AACrC,cAAM,eAAW,4BAAa,OAAO,SAAS;AAC9C,YAAI,CAAC,QAAQ,CAAC,UAAU;AACtB;AAAA,QACF;AAEA,cAAM,YAAQ,kCAAmB,OAAO,KAAK;AAC7C,cAAM,WAAO,4BAAa,OAAO,IAAI;AACrC,cAAM,gBAAY,4BAAa,OAAO,SAAS;AAE/C,cAAM,KAAK,cAAc,GAAG,OAAO,iBAAa,mBAAM,gBAAgB,CAAC;AAEvE,cAAM,QAAQ,GAAG,OAAO,aAAa,SAAS,IAAI,CAAC,IAAI,SAAS,QAAQ,CAAC;AAGzE,cAAM,KAAK,cAAc,OAAO,IAAI;AAEpC,cAAM,YAA6B,CAAC;AACpC,YAAI,UAAU,MAAM;AAClB,oBAAU;AAAA,YACR,KAAK,aAAa;AAAA,cAChB,IAAI,GAAG,KAAK;AAAA,cACZ,UAAM,mBAAM,eAAe;AAAA,cAC3B,MAAM;AAAA,cACN,MAAM;AAAA,cACN;AAAA,cACA,MAAM,sBAAQ;AAAA,cACd,aAAa;AAAA,YACf,CAAC;AAAA,UACH;AAAA,QACF;AACA,YAAI,MAAM;AACR,oBAAU;AAAA,YACR,KAAK,aAAa;AAAA,cAChB,IAAI,GAAG,KAAK;AAAA,cACZ,UAAM,mBAAM,cAAc;AAAA,cAC1B,MAAM;AAAA,cACN,MAAM;AAAA,cACN,OAAO;AAAA,cACP,aAAa;AAAA,YACf,CAAC;AAAA,UACH;AAAA,QACF;AACA,YAAI,WAAW;AACb,oBAAU;AAAA,YACR,KAAK,aAAa;AAAA,cAChB,IAAI,GAAG,KAAK;AAAA,cACZ,UAAM,mBAAM,mBAAmB;AAAA,cAC/B,MAAM;AAAA,cACN,MAAM;AAAA,cACN,OAAO;AAAA,cACP,aAAa;AAAA,YACf,CAAC;AAAA,UACH;AAAA,QACF;AACA,cAAM,QAAQ,IAAI,SAAS;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,aAAa,QAAsB,QAAmC;AAC1E,QAAI,KAAC,6BAAc,MAAM,GAAG;AAC1B;AAAA,IACF;AACA,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,UAAM,SAAS;AAGf,UAAM,WAAO,4BAAa,OAAO,SAAS;AAC1C,QAAI,SAAS,MAAM;AACjB,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,UAAU;AAAA,QACtB,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AACA,UAAM,WAAO,kCAAmB,OAAO,YAAY;AACnD,QAAI,SAAS,MAAM;AACjB,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,UAAU;AAAA,QACtB,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,QACN,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AACA,UAAM,aAAS,kCAAmB,OAAO,QAAQ;AACjD,QAAI,WAAW,MAAM;AACnB,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,QAAQ;AAAA,QACpB,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,QACN,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AAGA,UAAM,KAAK,cAAc,GAAG,MAAM,eAAW,mBAAM,gBAAgB,CAAC;AAGpE,UAAM,YAAY,OAAO,gBAAgB;AAEzC,UAAM,mBAAe,6BAAc,OAAO,aAAa;AACvD,QAAI,iBAAiB,MAAM;AACzB,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,cAAc;AAAA,QAC1B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,OAAO,CAAC;AAAA,QACR,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AACA,UAAM,aAAS,kCAAmB,OAAO,yBAAyB;AAClE,QAAI,WAAW,MAAM;AACnB,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,eAAe;AAAA,QAC3B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,QACN,OAAO;AAAA,QACP,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AAEA,UAAM,YAAQ,6BAAc,OAAO,cAAc;AACjD,QAAI,UAAU,MAAM;AAClB,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,cAAc;AAAA,QAC1B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,OAAO;AAAA,QACP,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AAGA,QAAI,CAAC,WAAW;AACd,YAAM,KAAK,aAAa,GAAG,MAAM,sBAAkB,mBAAM,cAAc,CAAC;AAAA,IAC1E;AACA,UAAM,KAAK,aAAa,GAAG,MAAM,wBAAoB,mBAAM,UAAU,CAAC;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,cAAc,QAAsB,SAAwC;AAChF,QAAI,KAAC,6BAAc,OAAO,GAAG;AAC3B;AAAA,IACF;AACA,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,UAAM,SAAS;AAEf,UAAM,KAAK,cAAc,GAAG,MAAM,gBAAY,mBAAM,gBAAgB,CAAC;AAErE,UAAM,WAAO,4BAAa,OAAO,IAAI;AACrC,QAAI,MAAM;AACR,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,aAAa;AAAA,QACzB,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,OAAO;AAAA,QACP,UAAM,mBAAM,iBAAiB;AAAA,QAC7B,QAAQ,kBAAkB;AAAA,QAC1B,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AACA,QAAI,MAAM,QAAQ,OAAO,WAAW,GAAG;AACrC,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,oBAAoB;AAAA,QAChC,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO,KAAK,UAAU,OAAO,WAAW;AAAA,QACxC,OAAO;AAAA,QACP,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AAEA,UAAM,mBAAe,6BAAc,OAAO,cAAc;AACxD,QAAI,iBAAiB,MAAM;AACzB,YAAM,KAAK,aAAa;AAAA,QACtB,IAAI,GAAG,MAAM;AAAA,QACb,UAAM,mBAAM,qBAAqB;AAAA,QACjC,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,OAAO;AAAA,QACP,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AAEA,UAAM,eAMD;AAAA,MACH,EAAE,KAAK,iBAAiB,IAAI,iBAAiB,SAAS,gBAAgB,MAAM,QAAQ;AAAA,MACpF,EAAE,KAAK,WAAW,IAAI,WAAW,SAAS,gBAAgB,MAAM,eAAe,MAAM,IAAI;AAAA,MACzF,EAAE,KAAK,kBAAkB,IAAI,kBAAkB,SAAS,sBAAsB,MAAM,eAAe,MAAM,IAAI;AAAA,MAC7G;AAAA,QACE,KAAK;AAAA,QACL,IAAI;AAAA,QACJ,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,IAAI;AAAA,QACJ,SAAS;AAAA,QACT,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AAAA,IACF;AACA,eAAW,SAAS,cAAc;AAChC,YAAM,cAAU,kCAAmB,OAAO,MAAM,GAAG,CAAC;AACpD,UAAI,YAAY,MAAM;AACpB,cAAM,KAAK,aAAa;AAAA,UACtB,IAAI,GAAG,MAAM,YAAY,MAAM,EAAE;AAAA,UACjC,UAAM,mBAAM,MAAM,OAAO;AAAA,UACzB,MAAM;AAAA,UACN,MAAM,MAAM;AAAA,UACZ,OAAO;AAAA,UACP,MAAM,MAAM;AAAA,UACZ,aAAa;AAAA,QACf,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,mBAAmB,QAAsB,WAAmC;AAChF,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,UAAM,KAAK,QAAQ,qBAAqB,GAAG,MAAM,mBAAmB;AAAA,MAClE,KAAK;AAAA,MACL,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,QAAqC;AACtD,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,SAAK,QAAQ,IAAI,MAAM,+BAA+B,MAAM,EAAE;AAC9D,UAAM,KAAK,QAAQ,eAAe,QAAQ,EAAE,WAAW,KAAK,CAAC;AAG7D,QAAI,UAAU;AACd,eAAW,MAAM,KAAK,YAAY;AAChC,UAAI,OAAO,UAAU,GAAG,WAAW,GAAG,MAAM,GAAG,GAAG;AAChD,aAAK,WAAW,OAAO,EAAE;AACzB;AAAA,MACF;AAAA,IACF;AACA,SAAK,QAAQ,IAAI,MAAM,+BAA+B,MAAM,kBAAkB,OAAO,cAAc;AAAA,EACrG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,mBAAmB,QAAqC;AAC5D,UAAM,SAAS,KAAK,aAAa,MAAM;AACvC,SAAK,QAAQ,IAAI,MAAM,qCAAqC,MAAM,8BAA8B;AAGhG,UAAM,SAAmB,CAAC;AAC1B,eAAW,OAAO,wBAAwB;AACxC,aAAO,KAAK,GAAG,MAAM,IAAI,IAAI,EAAE,EAAE;AAAA,IACnC;AAEA,WAAO,KAAK,GAAG,MAAM,WAAW;AAEhC,WAAO,KAAK,GAAG,MAAM,uBAAuB;AAE5C,QAAI,UAAU;AACd,eAAW,MAAM,QAAQ;AACvB,UAAI,MAAM,KAAK,QAAQ,eAAe,EAAE,GAAG;AACzC,cAAM,KAAK,QAAQ,eAAe,IAAI,EAAE,WAAW,KAAK,CAAC;AACzD,aAAK,QAAQ,IAAI,MAAM,2BAA2B,EAAE,EAAE;AACtD;AAAA,MACF;AAAA,IACF;AACA,QAAI,UAAU,GAAG;AACf,WAAK,QAAQ,IAAI,MAAM,qCAAqC,MAAM,kBAAkB,OAAO,kBAAkB;AAAA,IAC/G;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAa,QAA8B;AACzC,WAAO,GAAG,SAAS,OAAO,WAAW,CAAC,IAAI,SAAS,OAAO,MAAM,CAAC;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,cAAc,IAAY,MAAkD;AACxF,QAAI,KAAK,WAAW,IAAI,EAAE,GAAG;AAC3B;AAAA,IACF;AACA,UAAM,KAAK,QAAQ,wBAAwB,IAAI;AAAA,MAC7C,MAAM;AAAA,MACN,QAAQ,EAAE,KAAK;AAAA,MACf,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,SAAK,WAAW,IAAI,EAAE;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,YAAY,KAA8B;AApgC1D;AAqgCI,QAAI,KAAK,WAAW,IAAI,IAAI,EAAE,GAAG;AAC/B;AAAA,IACF;AACA,UAAM,SAAwC;AAAA,MAC5C,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,MAAM;AAAA,MACN,QAAO,SAAI,UAAJ,YAAa;AAAA,IACtB;AACA,QAAI,IAAI,MAAM;AACZ,aAAO,OAAO,IAAI;AAAA,IACpB;AACA,QAAI,IAAI,MAAM;AACZ,aAAO,OAAO,IAAI;AAAA,IACpB;AACA,QAAI,IAAI,QAAQ;AACd,aAAO,SAAS,IAAI;AAAA,IACtB;AACA,UAAM,KAAK,QAAQ,wBAAwB,IAAI,IAAI;AAAA,MACjD,MAAM;AAAA,MACN;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,QAAI,IAAI,QAAQ;AAMd,YAAM,KAAK,0BAA0B,IAAI,IAAI,IAAI,MAAM;AAAA,IACzD;AACA,SAAK,WAAW,IAAI,IAAI,EAAE;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAc,0BAA0B,IAAY,OAA8C;AApjCpG;AAqjCI,UAAM,WAAW,MAAM,KAAK,QAAQ,eAAe,EAAE;AACrD,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AACA,UAAM,UAAS,cAAS,WAAT,mBAAiB;AAChC,QAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC;AAAA,IACF;AACA,UAAM,QAAQ,OAAO,OAAO,MAAiC,EAAE,KAAK,OAAK,OAAO,MAAM,QAAQ;AAC9F,QAAI,CAAC,OAAO;AACV;AAAA,IACF;AACA,aAAS,SAAS,EAAE,GAAG,SAAS,QAAQ,QAAQ,MAAM;AACtD,UAAM,KAAK,QAAQ,eAAe,IAAI,QAAQ;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,aACZ,IACA,MACA,MACe;AACf,QAAI,KAAK,WAAW,IAAI,EAAE,GAAG;AAC3B;AAAA,IACF;AACA,UAAM,SAAwC;AAAA,MAC5C;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,IACT;AACA,QAAI,MAAM;AACR,aAAO,OAAO;AAAA,IAChB;AACA,UAAM,KAAK,QAAQ,wBAAwB,IAAI;AAAA,MAC7C,MAAM;AAAA,MACN;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,CAAC;AACD,UAAM,KAAK,QAAQ,cAAc,IAAI,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAC9D,SAAK,WAAW,IAAI,EAAE;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,aAAa,KAA8B;AACvD,UAAM,KAAK,YAAY,GAAG;AAC1B,QAAI,IAAI,aAAa;AACnB,YAAM,KAAK,QAAQ,qBAAqB,IAAI,IAAI,EAAE,KAAK,IAAI,OAAO,KAAK,KAAK,CAAC;AAAA,IAC/E,OAAO;AACL,YAAM,KAAK,QAAQ,cAAc,IAAI,IAAI,EAAE,KAAK,IAAI,OAAO,KAAK,KAAK,CAAC;AAAA,IACxE;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -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/build/main.js CHANGED
@@ -76,6 +76,13 @@ class HomeWizard extends utils.Adapter {
76
76
  systemPollTimer = void 0;
77
77
  ipRecoveryTimer = void 0;
78
78
  isPairing = false;
79
+ /**
80
+ * In-flight guard for {@link pollPairing}: the poll runs every 2 s, but a
81
+ * single device's requestPairing can hang up to the 10 s HTTP timeout —
82
+ * without the guard, overlapping polls would fire concurrent POST /api/user
83
+ * against the same device.
84
+ */
85
+ pairingPollBusy = false;
79
86
  pairingManualIp = "";
80
87
  discoveredDuringPairing = [];
81
88
  /** Set during onUnload — async paths bail before further setStateAsync calls. */
@@ -90,6 +97,7 @@ class HomeWizard extends utils.Adapter {
90
97
  */
91
98
  makeClient = (ip, token) => new import_homewizard_client.HomeWizardClient(ip, token, { log: this.log });
92
99
  makeWebSocket = (ip, token, callbacks, timers) => new import_websocket_client.HomeWizardWebSocket(ip, token, callbacks, timers);
100
+ makeDiscovery = () => new import_discovery.HomeWizardDiscovery(this.log);
93
101
  /**
94
102
  * Close a connection's WebSocket and clear its poll + reconnect timers.
95
103
  *
@@ -257,15 +265,19 @@ class HomeWizard extends utils.Adapter {
257
265
  try {
258
266
  if (this.pairingTimer) {
259
267
  this.clearTimeout(this.pairingTimer);
268
+ this.pairingTimer = void 0;
260
269
  }
261
270
  if (this.pairingPollTimer) {
262
271
  this.clearInterval(this.pairingPollTimer);
272
+ this.pairingPollTimer = void 0;
263
273
  }
264
274
  if (this.systemPollTimer) {
265
275
  this.clearInterval(this.systemPollTimer);
276
+ this.systemPollTimer = void 0;
266
277
  }
267
278
  if (this.ipRecoveryTimer) {
268
279
  this.clearTimeout(this.ipRecoveryTimer);
280
+ this.ipRecoveryTimer = void 0;
269
281
  }
270
282
  (_a = this.discovery) == null ? void 0 : _a.stop();
271
283
  for (const conn of this.connections.values()) {
@@ -296,6 +308,7 @@ class HomeWizard extends utils.Adapter {
296
308
  }
297
309
  const conn = this.findConnectionForState(id);
298
310
  if (!conn || !conn.ip) {
311
+ this.log.debug(`stateChange ${id}: no matching connected device \u2014 ignored`);
299
312
  return;
300
313
  }
301
314
  const client = this.makeClient(conn.ip, conn.config.token);
@@ -303,8 +316,10 @@ class HomeWizard extends utils.Adapter {
303
316
  if (id.endsWith(".system.reboot")) {
304
317
  this.log.info(`Rebooting ${conn.config.productName} (${conn.ip})`);
305
318
  await client.reboot();
319
+ await this.setStateAsync(id, { val: false, ack: true });
306
320
  } else if (id.endsWith(".system.identify")) {
307
321
  await client.identify();
322
+ await this.setStateAsync(id, { val: false, ack: true });
308
323
  } else if (id.endsWith(".system.cloud_enabled")) {
309
324
  await client.setSystem({ cloud_enabled: !!state.val });
310
325
  await this.setStateAsync(id, { val: state.val, ack: true });
@@ -381,7 +396,7 @@ class HomeWizard extends utils.Adapter {
381
396
  `Pairing mode enabled \u2014 searching for devices via mDNS, press the button on your HomeWizard device now (60 seconds timeout)`
382
397
  );
383
398
  if (!this.discovery) {
384
- this.discovery = new import_discovery.HomeWizardDiscovery(this.log);
399
+ this.discovery = this.makeDiscovery();
385
400
  }
386
401
  this.discovery.start((discovered) => {
387
402
  this.onDeviceDiscovered(discovered);
@@ -397,6 +412,18 @@ class HomeWizard extends utils.Adapter {
397
412
  }
398
413
  /** Poll all discovered devices to attempt pairing */
399
414
  async pollPairing() {
415
+ if (this.pairingPollBusy) {
416
+ return;
417
+ }
418
+ this.pairingPollBusy = true;
419
+ try {
420
+ await this.pollPairingDevices();
421
+ } finally {
422
+ this.pairingPollBusy = false;
423
+ }
424
+ }
425
+ /** One pairing-poll pass over all discovered devices. */
426
+ async pollPairingDevices() {
400
427
  for (const device of this.discoveredDuringPairing) {
401
428
  try {
402
429
  const client = this.makeClient(device.ip, "");
@@ -461,7 +488,7 @@ class HomeWizard extends utils.Adapter {
461
488
  return;
462
489
  }
463
490
  this.log.debug(`Device unreachable \u2014 searching for new IP via mDNS`);
464
- this.discovery = new import_discovery.HomeWizardDiscovery(this.log);
491
+ this.discovery = this.makeDiscovery();
465
492
  this.discovery.start((discovered) => {
466
493
  for (const conn of this.connections.values()) {
467
494
  if (conn.config.serial !== discovered.serial) {
@@ -859,6 +886,8 @@ class HomeWizard extends utils.Adapter {
859
886
  }
860
887
  this.teardownConnection(conn);
861
888
  this.connections.delete(key);
889
+ this.lastWarnAt.delete(conn.config.serial);
890
+ this.lastInfoAt.delete(conn.config.serial);
862
891
  await this.stateManager.removeDevice(conn.config);
863
892
  this.updateGlobalConnection();
864
893
  }
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 { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { errText, isValidIpv4, parseBatteryPermissions, validateBatteryMode } from \"./lib/coerce\";\nimport { classifyError, createDeviceConnection, UNSTABLE_DISCONNECT_THRESHOLD } from \"./lib/connection-utils\";\nimport { HomeWizardDiscovery } from \"./lib/discovery\";\nimport { HomeWizardApiError, HomeWizardClient } from \"./lib/homewizard-client\";\nimport {\n computeReconnectDelay,\n decideUnstableTransition,\n findConnectionForState as resolveConnectionForState,\n pickRestPollInterval,\n shouldEmitAfterCooldown,\n shouldStartIpRecovery,\n} from \"./lib/main-helpers\";\nimport { StateManager } from \"./lib/state-manager\";\nimport type {\n BatteryControl,\n DeviceConfig,\n DeviceConnection,\n DiscoveredDevice,\n Measurement,\n SystemInfo,\n} from \"./lib/types\";\nimport { HomeWizardWebSocket, type TimerDeps, type WsCallbacks } 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/** Retry mDNS every N WS failures after first attempt (~1 hour at 5 min cap) */\nconst MDNS_RETRY_EVERY = 12;\n/** Connection must last this long to count as \"stable\" */\nconst STABLE_THRESHOLD_MS = 600_000;\n/** Max reconnect delay for unstable devices */\nconst WS_RECONNECT_MAX_UNSTABLE_MS = 60_000;\n/** REST fallback interval for unstable devices (slower, not stopped) */\nconst REST_POLL_UNSTABLE_MS = 30_000;\n/**\n * Cooldown window for `device unreachable` warns. Per-device, category-\n * spanning: bouncing hardware should produce max 1\u00D7 warn per window, regardless\n * of whether each cycle's failure was TIMEOUT, NETWORK, or HTTP_503. Survives\n * the lastErrorCode-reset on recovery so chronic bouncing doesn't flap warn /\n * debug at every cycle.\n */\nconst WARN_COOLDOWN_MS = 60 * 60 * 1000;\n/** Cooldown window for `connection restored` infos \u2014 analog to warn cooldown. */\nconst INFO_COOLDOWN_MS = 60 * 60 * 1000;\n\n/**\n * HomeWizard adapter \u2014 manages multiple devices over API v2 (HTTPS + WebSocket):\n * pairing, real-time push, REST fallback, reconnect/recovery and state mapping.\n * Exported so the orchestration unit tests can drive its handlers directly.\n */\nexport class HomeWizard extends utils.Adapter {\n private stateManager!: StateManager;\n private discovery: HomeWizardDiscovery | null = null;\n private readonly connections = new Map<string, DeviceConnection>();\n /**\n * Per-device last-warn timestamp for chronic-bouncing cooldown. Key =\n * `conn.config.serial` (kategorien\u00FCbergreifend). The classifyError-based\n * `lastErrorCode`-Dedup in {@link logDeviceError} resets on every recovery,\n * so on chronic bouncing a new disconnect counts as \"first occurrence\"\n * \u2192 wieder warn. This cooldown stamp persists across recoveries so the user\n * sees max one warn per WARN_COOLDOWN_MS per device.\n */\n private readonly lastWarnAt = new Map<string, number>();\n /** Per-device last-info timestamp for `connection restored`. Analog cooldown. */\n private readonly lastInfoAt = new Map<string, number>();\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 /** Set during onUnload \u2014 async paths bail before further setStateAsync calls. */\n private unloading = false;\n /**\n * Factories for the REST/WS clients \u2014 default to the real constructors. Test seams:\n * a unit test can replace these with fakes to exercise the orchestration (initDevice,\n * onWsConnected/onWsDisconnected, onStateChange) without real network.\n *\n * @param ip Device IP address\n * @param token Bearer token (empty string for pairing requests)\n */\n private makeClient: (ip: string, token: string) => HomeWizardClient = (ip, token) =>\n new HomeWizardClient(ip, token, { log: this.log });\n private makeWebSocket: (ip: string, token: string, callbacks: WsCallbacks, timers: TimerDeps) => HomeWizardWebSocket =\n (ip, token, callbacks, timers) => new HomeWizardWebSocket(ip, token, callbacks, timers);\n\n /**\n * Close a connection's WebSocket and clear its poll + reconnect timers.\n *\n * @param conn Device connection to tear down\n */\n private teardownConnection(conn: DeviceConnection): void {\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n }\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"homewizard\" });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"stateChange\", this.onStateChange.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n // No process-level unhandledRejection/uncaughtException handlers: in compact mode they\n // are process-wide and cross-adapter-harmful. Every handler has .bind+try/catch and\n // fire-and-forget paths use .catch (Fleet pattern \u2014 hueemu/parcelapp/nut).\n }\n\n private async onReady(): Promise<void> {\n try {\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.stateManager = new StateManager(this);\n\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\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(\"*.battery.charge_to_full\");\n await this.subscribeStatesAsync(\"*.remove\");\n\n const devices = await this.loadDevicesFromObjects();\n if (devices.length === 0) {\n this.log.info(`No devices configured \u2014 set 'startPairing' to true to add a device`);\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true });\n }\n\n for (const device of devices) {\n const key = this.stateManager.devicePrefix(device);\n await this.stateManager.cleanupMovedStates(device);\n await this.stateManager.createDeviceStates(device);\n const conn = createDeviceConnection(device, device.ip || \"\");\n this.connections.set(key, conn);\n\n if (conn.ip) {\n this.log.debug(`Using stored IP ${conn.ip} for ${device.productName}`);\n void this.initDevice(conn).catch((err: unknown) =>\n this.log.error(`initDevice failed for ${conn.config.productName}: ${errText(err)}`),\n );\n }\n }\n\n this.systemPollTimer = this.setInterval(() => {\n void this.pollAllSystemInfo();\n }, SYSTEM_POLL_MS);\n\n this.updateGlobalConnection();\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n }\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 // One-shot legacy migration: v0.1/0.2 stored devices in adapter `native.devices`;\n // v0.3.0 moved them to per-device objects. Any install that ran v0.3.0+ has already\n // migrated (native.devices cleared below), but removal is low-reward / non-zero-risk\n // for an install that has been dormant since v0.2 \u2014 keep until at least v1.0.0.\n // Defensive: native.devices could be a non-array if a previous version\n // wrote a different shape, or if the user edited it manually.\n const rawOldDevices = (this.config as Record<string, unknown>).devices;\n const oldDevices: DeviceConfig[] = Array.isArray(rawOldDevices) ? (rawOldDevices as DeviceConfig[]) : [];\n if (oldDevices.length > 0) {\n this.log.debug(`Migrating ${oldDevices.length} device(s) from adapter config to device objects`);\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. A corrupted encryptedToken\n // (e.g. after secret rotation, crypto-lib changes, manual DB edits) must\n // not take down the whole adapter \u2014 skip the broken device, keep the rest.\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 let token: string;\n try {\n token = this.decrypt(native.encryptedToken);\n } catch (err) {\n this.log.warn(\n `Cannot decrypt token for ${localId} \u2014 re-pair the device. ` +\n `(${errText(err)}). Other devices remain unaffected.`,\n );\n continue;\n }\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(\n prefix,\n {\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 { preserve: { common: [\"name\"] } },\n );\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(c => c.config.serial === discovered.serial);\n if (existing) {\n return;\n }\n\n // Skip duplicates\n if (this.discoveredDuringPairing.find(d => d.serial === discovered.serial)) {\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 // Set first, before any clearTimeout \u2014 in-flight async paths\n // (REST poll, getMeasurement, getSystem) check this after each await\n // and bail out before further setStateAsync on a tearing-down adapter.\n this.unloading = true;\n try {\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 this.teardownConnection(conn);\n }\n this.connections.clear();\n\n void this.setState(\"info.connection\", { val: false, ack: true });\n } finally {\n callback();\n }\n }\n\n private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n try {\n if (!state || state.ack || this.unloading) {\n return;\n }\n\n if (id.endsWith(\".startPairing\")) {\n if (state.val) {\n await this.startPairing();\n }\n return;\n }\n\n if (id.endsWith(\".remove\")) {\n if (state.val) {\n await this.removeDevice(id);\n }\n return;\n }\n\n const conn = this.findConnectionForState(id);\n if (!conn || !conn.ip) {\n return;\n }\n\n const client = this.makeClient(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 const mode = validateBatteryMode(String(state.val));\n if (!mode) {\n this.log.warn(\n `Invalid battery.mode value: '${String(state.val)}' \u2014 expected one of: zero, to_full, standby, predictive`,\n );\n return;\n }\n await client.setBatteries({ mode });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.permissions\")) {\n const result = parseBatteryPermissions(String(state.val));\n if (!result.ok) {\n this.log.warn(\n `Invalid JSON for battery.permissions: ${result.reason} \u2014 expected array, got: ${result.sample}`,\n );\n return;\n }\n await client.setBatteries({ permissions: result.perms });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.charge_to_full\")) {\n await client.setBatteries({ charge_to_full: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n }\n } catch (err) {\n this.log.warn(`Failed to set ${id}: ${errText(err)}`);\n }\n } catch (err: unknown) {\n this.log.error(`stateChange failed: ${errText(err)}`);\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 // Validate manual-IP up front \u2014 better to fail fast than wait 60s while\n // requestPairing keeps timing out against a malformed input.\n if (!isValidIpv4(this.pairingManualIp)) {\n this.log.warn(`Invalid pairing IP '${this.pairingManualIp}' \u2014 expected IPv4 (e.g. 192.168.1.42)`);\n this.isPairing = false;\n this.pairingManualIp = \"\";\n return;\n }\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(`Pairing mode automatically disabled after 60 seconds timeout`);\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 = this.makeClient(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 = this.makeClient(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 // Re-pair of an existing device (e.g. after factory reset): close the\n // old connection's wsClient + timers before overwriting the map entry,\n // otherwise the old WS keeps running as a zombie until restart.\n const key = this.stateManager.devicePrefix(deviceConfig);\n const previous = this.connections.get(key);\n if (previous) {\n this.log.debug(`Re-pair: closing previous connection for ${deviceConfig.productName}`);\n this.teardownConnection(previous);\n }\n\n // Create connection and connect\n const conn = createDeviceConnection(deviceConfig, device.ip);\n this.connections.set(key, conn);\n void this.initDevice(conn).catch((err: unknown) =>\n this.log.error(`initDevice failed for ${conn.config.productName}: ${errText(err)}`),\n );\n\n // Remove from discovery list \u2014 but keep pairing window open so the\n // user can button-press additional devices in the same session.\n this.discoveredDuringPairing = this.discoveredDuringPairing.filter(d => d.serial !== info.serial);\n\n this.updateGlobalConnection();\n // Do NOT call stopPairing() here \u2014 pairingTimer (60 s) closes the\n // window naturally; meanwhile the user can pair more devices.\n continue;\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(`Pairing poll error for ${device.ip}: ${errText(err)}`);\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 // Internal recovery \u2014 debug only. The initial disconnect already produced\n // one warn via logDeviceError; repeating that hourly while a device stays\n // offline is just spam.\n this.log.debug(`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 // Multiple mDNS broadcasts can arrive within one recovery window\n // (e.g. AP roam). Skip if a connect cycle is already in flight.\n if (conn.recovering) {\n return;\n }\n\n this.log.info(`${conn.config.productName}: found at new IP ${discovered.ip} (was ${conn.ip})`);\n\n // Update IP and persist \u2014 reset stability (new network conditions)\n conn.ip = discovered.ip;\n conn.config.ip = discovered.ip;\n conn.wsFailCount = 0;\n conn.recentDisconnects = 0;\n // Surface persist-failures (e.g. js-controller hiccup) instead of\n // swallowing them \u2014 the user otherwise sees \"new IP\" log but the\n // change is lost on next restart.\n this.saveDeviceToObject(conn.config).catch((err: unknown) =>\n this.log.debug(`Failed to persist new IP for ${conn.config.productName}: ${errText(err)}`),\n );\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 mDNS after timeout \u2014 WS reconnect continues with exponential\n // backoff. Don't log per-device warns here: the initial disconnect already\n // produced a `deviceUnreachable` warn via logDeviceError; spamming the\n // user hourly while the device stays offline adds zero information. If\n // someone needs to see retry cadence they can enable debug logging.\n this.ipRecoveryTimer = this.setTimeout(() => {\n this.ipRecoveryTimer = undefined;\n this.stopIpRecovery();\n\n for (const conn of this.connections.values()) {\n if (!conn.wsAuthenticated && conn.wsFailCount > 0) {\n this.log.debug(\n `${conn.config.productName}: device offline \u2014 will keep retrying every ${WS_RECONNECT_MAX_MS / 1000}s`,\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 if (this.unloading || conn.removed) {\n return;\n }\n try {\n const client = this.makeClient(conn.ip, conn.config.token);\n const info = await client.getDeviceInfo();\n if (this.unloading || conn.removed) {\n return;\n }\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 if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"init\", err);\n }\n\n if (this.unloading || conn.removed) {\n return;\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 // Mark as recovering so concurrent triggers (mDNS broadcast race,\n // overlapping reconnect timer) don't spawn a second wsClient.\n conn.recovering = true;\n\n // Close any existing wsClient before creating a new one. The normal\n // disconnect path nulls conn.wsClient, but IP-recovery jumps in directly\n // and would otherwise leak the old socket.\n if (conn.wsClient) {\n conn.wsClient.close();\n conn.wsClient = null;\n }\n\n // After repeated failures, try mDNS periodically to find a new IP\n if (shouldStartIpRecovery(conn.wsFailCount, WS_FAILURES_BEFORE_MDNS, MDNS_RETRY_EVERY)) {\n this.startIpRecovery();\n }\n\n // Thin callbacks delegating to instance methods (extracted for readability + unit-testability).\n const wsClient = this.makeWebSocket(\n conn.ip,\n conn.config.token,\n {\n onMeasurement: data => this.onWsMeasurement(conn, data),\n onSystem: data => this.onWsSystem(conn, data),\n onBattery: data => this.onWsBattery(conn, data),\n onConnected: () => this.onWsConnected(conn),\n onDisconnected: error => this.onWsDisconnected(conn, error),\n log: this.log,\n },\n {\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: h => {\n this.clearTimeout(h as ioBroker.Timeout);\n },\n scheduleRepeating: (cb, ms) => this.setInterval(cb, ms),\n cancelRepeating: h => {\n this.clearInterval(h as ioBroker.Interval);\n },\n },\n );\n\n conn.wsClient = wsClient;\n wsClient.connect();\n }\n\n /**\n * Handle a measurement push.\n *\n * @param conn Device connection\n * @param data Measurement payload\n */\n private onWsMeasurement(conn: DeviceConnection, data: Measurement): void {\n // Skip updates for devices removed mid-flight (frame can race delObjectAsync) + teardown.\n if (conn.removed || this.unloading) {\n return;\n }\n // Defensive .catch \u2014 writes inside updateMeasurement may reject on transient Redis hiccups;\n // we want a debug-log, not an unhandled rejection.\n this.stateManager.updateMeasurement(conn.config, data).catch((err: unknown) => {\n this.log.debug(`updateMeasurement failed for ${conn.config.productName}: ${errText(err)}`);\n });\n }\n\n /**\n * Handle a real-time system push (cloud/led changes etc.).\n *\n * @param conn Device connection\n * @param data System payload\n */\n private onWsSystem(conn: DeviceConnection, data: SystemInfo): void {\n if (conn.removed || this.unloading) {\n return;\n }\n this.stateManager.updateSystem(conn.config, data).catch((err: unknown) => {\n this.log.debug(`updateSystem (ws) failed for ${conn.config.productName}: ${errText(err)}`);\n });\n }\n\n /**\n * Handle a real-time battery-group push (mode/permissions/target power).\n *\n * @param conn Device connection\n * @param data Battery-control payload\n */\n private onWsBattery(conn: DeviceConnection, data: BatteryControl): void {\n if (conn.removed || this.unloading) {\n return;\n }\n // Only surface battery states when batteries are actually connected.\n if (!data.battery_count || data.battery_count <= 0) {\n return;\n }\n this.stateManager.updateBattery(conn.config, data).catch((err: unknown) => {\n this.log.debug(`updateBattery (ws) failed for ${conn.config.productName}: ${errText(err)}`);\n });\n }\n\n /**\n * WebSocket authenticated \u2014 mark connected, stop REST fallback, log recovery (cooldowned).\n *\n * @param conn Device connection\n */\n private onWsConnected(conn: DeviceConnection): void {\n conn.wsAuthenticated = true;\n conn.wsFailCount = 0;\n conn.authFailCount = 0;\n conn.lastConnectedAt = Date.now();\n conn.recovering = 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(c => c.wsAuthenticated);\n if (allConnected) {\n this.stopIpRecovery();\n }\n }\n\n // Log restoration if we had errors before. Per-device cooldown so chronic bouncing\n // doesn't emit one info per cycle \u2014 repeats go to debug.\n if (conn.lastErrorCode) {\n const now = Date.now();\n const lastInfo = this.lastInfoAt.get(conn.config.serial) ?? 0;\n const msg = this.isUnstable(conn)\n ? `${conn.config.productName}: connection restored (unstable mode)`\n : `${conn.config.productName}: connection restored`;\n if (shouldEmitAfterCooldown(lastInfo, now, INFO_COOLDOWN_MS)) {\n this.lastInfoAt.set(conn.config.serial, now);\n this.log.info(msg);\n } else {\n this.log.debug(`${msg} (cooldown)`);\n }\n conn.lastErrorCode = \"\";\n }\n\n this.log.debug(`WebSocket connected to ${conn.config.productName} (${conn.ip})`);\n }\n\n /**\n * WebSocket disconnected \u2014 track stability, start REST fallback, schedule backed-off reconnect\n * (unless an auth failure stops the loop).\n *\n * @param conn Device connection\n * @param error Disconnect error, if any\n */\n private onWsDisconnected(conn: DeviceConnection, error?: Error): void {\n // Auth failures are not a connectivity-stability signal \u2014 they mean the token is bad,\n // not the WiFi. Counting them as short connections would flip the device into unstable mode.\n const isAuthError = error instanceof HomeWizardApiError && error.errorCode === \"user:unauthorized\";\n\n // Track connection stability \u2014 pure decision in main-helpers, side-effects here.\n if (conn.lastConnectedAt > 0 && !isAuthError) {\n const duration = Date.now() - conn.lastConnectedAt;\n const transition = decideUnstableTransition(\n conn.recentDisconnects,\n duration,\n STABLE_THRESHOLD_MS,\n UNSTABLE_DISCONNECT_THRESHOLD,\n );\n if (duration < STABLE_THRESHOLD_MS) {\n conn.recentDisconnects++;\n } else {\n conn.recentDisconnects = 0;\n }\n // Hysterese-transitions are internal reconnect-strategy adjustments \u2192 debug, not info.\n if (transition === \"becameUnstable\") {\n this.log.debug(`${conn.config.productName}: unstable connection detected \u2014 using faster reconnect`);\n } else if (transition === \"stabilized\") {\n this.log.debug(`${conn.config.productName}: connection stabilized \u2014 using normal reconnect`);\n }\n }\n\n conn.wsAuthenticated = false;\n conn.wsClient = null;\n conn.recovering = false;\n void this.stateManager.setDeviceConnected(conn.config, false);\n this.updateGlobalConnection();\n\n if (error) {\n this.logDeviceError(conn, \"ws\", error);\n }\n\n // Auth failure \u2192 stop the reconnect path.\n if (!this.handleAuthFailure(conn, error, /* cleanupTimers */ false)) {\n return;\n }\n\n // Start REST fallback\n this.startRestFallback(conn);\n\n // Schedule reconnect with exponential backoff (faster for unstable devices).\n conn.wsFailCount++;\n const maxDelay = this.isUnstable(conn) ? WS_RECONNECT_MAX_UNSTABLE_MS : WS_RECONNECT_MAX_MS;\n const delay = computeReconnectDelay(conn.wsFailCount, WS_RECONNECT_BASE_MS, maxDelay);\n const key = this.stateManager.devicePrefix(conn.config);\n this.log.debug(`${key}: WS reconnect in ${delay / 1000}s (attempt ${conn.wsFailCount})`);\n\n conn.reconnectTimer = this.setTimeout(() => {\n conn.reconnectTimer = undefined;\n this.connectWebSocket(conn);\n }, delay);\n }\n\n /**\n * Start REST polling as fallback when WebSocket is down.\n * For stable devices: stops on network errors (WS reconnect handles recovery).\n * For unstable devices: slows down instead of stopping to minimize data gaps.\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 unstable = this.isUnstable(conn);\n const interval = pickRestPollInterval(unstable, REST_POLL_MS, REST_POLL_UNSTABLE_MS);\n const client = this.makeClient(conn.ip, conn.config.token);\n\n conn.pollTimer = this.setInterval(async () => {\n // Bail out if device was removed or adapter is shutting down \u2014 the\n // setStateAsync chain inside updateMeasurement would otherwise either\n // recreate deleted objects or hit a torn-down adapter.\n if (conn.removed || this.unloading) {\n return;\n }\n try {\n const data = await client.getMeasurement();\n if (conn.removed || this.unloading) {\n return;\n }\n await this.stateManager.updateMeasurement(conn.config, data);\n } catch (err) {\n if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"rest\", err);\n\n // Auth failures: stop everything \u2014 token is bad, re-pair required.\n if (err instanceof HomeWizardApiError && err.errorCode === \"user:unauthorized\") {\n this.handleAuthFailure(conn, err, /* cleanupTimers */ true);\n return;\n }\n\n // Stop REST polling on network errors for stable devices.\n // Unstable devices keep polling (slower) to minimize data gaps.\n if (!unstable && classifyError(err) === \"NETWORK\" && conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n }\n }, interval);\n }\n\n /** Poll system info for all connected devices in parallel */\n private async pollAllSystemInfo(): Promise<void> {\n if (this.unloading) {\n return;\n }\n const tasks = Array.from(this.connections.values())\n .filter(c => c.ip && c.wsAuthenticated && !c.removed)\n .map(c => this.pollSystemInfo(c));\n await Promise.all(tasks);\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 || conn.removed || this.unloading) {\n return;\n }\n\n try {\n const client = this.makeClient(conn.ip, conn.config.token);\n const system = await client.getSystem();\n if (conn.removed || this.unloading) {\n return;\n }\n await this.stateManager.updateSystem(conn.config, system);\n\n // Sync productName drift: if the user renamed the device in the\n // HomeWizard app (or a firmware update changed the product_name), pick\n // up the new value once per system-poll instead of staying stale until\n // re-pair. Cheap \u2014 only writes on actual change.\n try {\n const info = await client.getDeviceInfo();\n if (!conn.removed && !this.unloading && info.product_name && info.product_name !== conn.config.productName) {\n this.log.info(`${conn.config.productName}: name changed to '${info.product_name}' \u2014 updating object`);\n conn.config.productName = info.product_name;\n await this.saveDeviceToObject(conn.config);\n }\n } catch {\n // device-info is best-effort here; the system-poll log already\n // surfaces real connectivity issues.\n }\n\n // Also poll battery if device supports it. 404 = no battery \u2014 silent.\n // Other errors (500, timeout, malformed body) used to be swallowed\n // entirely; now they surface at debug so post-mortem diagnosis is\n // possible without losing any normal-flow logging.\n if (conn.removed || this.unloading) {\n return;\n }\n try {\n const battery = await client.getBatteries();\n if (conn.removed || this.unloading) {\n return;\n }\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 (err) {\n if (err instanceof HomeWizardApiError && err.statusCode === 404) {\n return; // device doesn't support batteries \u2014 expected\n }\n this.log.debug(`${conn.config.productName} batteries: ${errText(err)}`);\n }\n } catch (err) {\n if (this.unloading) {\n return;\n }\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(c => c.wsAuthenticated);\n // setStateChanged: flips rarely (connect/disconnect), called on every WS event \u2014 skip no-op writes.\n void this.setStateChangedAsync(\"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(`Removing device ${conn.config.productName} (${conn.config.serial})`);\n\n // Mark as removed FIRST \u2014 async tasks (in-flight WS frames, REST polls,\n // outstanding pollSystemInfo) check this flag after each await and bail\n // out before recreating just-deleted objects via setStateAsync.\n conn.removed = true;\n\n // Best-effort token revoke on the device (DELETE /api/user) so the local/iobroker user\n // doesn't linger across pair/unpair cycles. Fire-and-forget \u2014 never block removal on a\n // (possibly offline) device's 10s timeout.\n if (conn.ip && conn.config.token) {\n void this.makeClient(conn.ip, conn.config.token)\n .deleteUser()\n .catch((err: unknown) => this.log.debug(`Token revoke failed for ${conn.config.productName}: ${errText(err)}`));\n }\n\n // Disconnect\n this.teardownConnection(conn);\n this.connections.delete(key);\n\n // Delete device object and all states (no adapter restart!)\n await this.stateManager.removeDevice(conn.config);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Find connection for a state ID. Delegates to the pure helper so the\n * lookup math is unit-tested separately (`lib/main-helpers.test.ts`).\n *\n * @param stateId Full state ID\n */\n private findConnectionForState(stateId: string): DeviceConnection | undefined {\n return resolveConnectionForState(stateId, this.namespace, this.connections);\n }\n\n /**\n * Whether a device has unstable connectivity (frequent short-lived connections).\n * Unstable devices get faster reconnect and persistent REST fallback.\n *\n * @param conn Device connection\n */\n private isUnstable(conn: DeviceConnection): boolean {\n return conn.recentDisconnects >= UNSTABLE_DISCONNECT_THRESHOLD;\n }\n\n /**\n * Handle a possible auth failure on a device connection. Counts failures and,\n * once `MAX_AUTH_FAILURES` is reached, warns the user and (optionally) tears\n * down active timers and the WebSocket \u2014 stops bombarding the device with a\n * known-bad token.\n *\n * @param conn Device connection.\n * @param error The error from the failing call (any error type accepted).\n * @param cleanupTimers If `true`, clears poll/reconnect timers and closes the WS\n * on threshold reach. Used by REST-fallback paths where\n * the WS would otherwise keep retrying indefinitely. The\n * WS-disconnect path passes `false` because the caller\n * decides the next step itself.\n * @returns `true` if the caller should continue normal flow (no auth-stop),\n * `false` if the auth-stop fired and the caller should bail out.\n */\n private handleAuthFailure(conn: DeviceConnection, error: unknown, cleanupTimers: boolean): boolean {\n if (!(error instanceof HomeWizardApiError) || error.errorCode !== \"user:unauthorized\") {\n return true;\n }\n conn.authFailCount++;\n if (conn.authFailCount < MAX_AUTH_FAILURES) {\n return true;\n }\n this.log.warn(`${conn.config.productName}: token invalid \u2014 re-pair device to fix`);\n if (cleanupTimers) {\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n conn.wsClient?.close();\n }\n return false;\n }\n\n /**\n * Log device error with deduplication.\n *\n * Two-stage dedup:\n * 1. `lastErrorCode` per connection \u2014 repeats of the same error category go\n * to debug. Resets on recovery (`onConnected` clears it) so a new category\n * after recovery surfaces as warn again. Designtechnisch correct for\n * \u201Enew failure mode\" but blind to chronic bouncing.\n * 2. {@link lastWarnAt} per device serial \u2014 survives recovery. Even if the\n * `lastErrorCode`-Dedup says \u201Efirst occurrence\", the cooldown stamp keeps\n * the warn-emit suppressed if we've warned for this device within\n * {@link WARN_COOLDOWN_MS}. Chronic bouncing produces at most 1\u00D7 warn per\n * hour per device.\n *\n * Cooldown key is the device serial \u2014 category-spanning. A flapping P1 that\n * cycles TIMEOUT\u2192NETWORK\u2192TIMEOUT is one phenomenon, one warn-budget.\n *\n * @param conn Device connection\n * @param context Error context (for debug messages only)\n * @param err Error object\n */\n private logDeviceError(conn: DeviceConnection, context: string, err: unknown): void {\n const errorCode = classifyError(err);\n const isRepeat = errorCode === conn.lastErrorCode;\n conn.lastErrorCode = errorCode;\n\n if (isRepeat) {\n this.log.debug(`${conn.config.productName} ${context}: ${errText(err)}`);\n return;\n }\n\n // New category \u2014 apply per-device cooldown so chronic bouncing doesn't\n // emit warn at every cycle just because each cycle's first failure is\n // a fresh `lastErrorCode`.\n const now = Date.now();\n const lastWarn = this.lastWarnAt.get(conn.config.serial) ?? 0;\n if (!shouldEmitAfterCooldown(lastWarn, now, WARN_COOLDOWN_MS)) {\n this.log.debug(`${conn.config.productName} ${context} (cooldown): ${errText(err)}`);\n return;\n }\n\n this.lastWarnAt.set(conn.config.serial, now);\n if (errorCode === \"NETWORK\") {\n this.log.warn(`${conn.config.productName}: device unreachable \u2014 will keep retrying`);\n } else {\n this.log.warn(`${conn.config.productName} ${context}: ${errText(err)}`);\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new HomeWizard(options);\n} else {\n (() => new HomeWizard())();\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;AACvB,0BAAqB;AACrB,uBAAqB;AACrB,oBAAmF;AACnF,8BAAqF;AACrF,uBAAoC;AACpC,+BAAqD;AACrD,0BAOO;AACP,2BAA6B;AAS7B,8BAAsE;AAGtE,MAAM,qBAAqB;AAE3B,MAAM,kBAAkB;AAExB,MAAM,uBAAuB;AAE7B,MAAM,sBAAsB;AAE5B,MAAM,eAAe;AAErB,MAAM,iBAAiB;AAEvB,MAAM,oBAAoB;AAE1B,MAAM,0BAA0B;AAEhC,MAAM,yBAAyB;AAE/B,MAAM,mBAAmB;AAEzB,MAAM,sBAAsB;AAE5B,MAAM,+BAA+B;AAErC,MAAM,wBAAwB;AAQ9B,MAAM,mBAAmB,KAAK,KAAK;AAEnC,MAAM,mBAAmB,KAAK,KAAK;AAO5B,MAAM,mBAAmB,MAAM,QAAQ;AAAA,EACpC;AAAA,EACA,YAAwC;AAAA,EAC/B,cAAc,oBAAI,IAA8B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAShD,aAAa,oBAAI,IAAoB;AAAA;AAAA,EAErC,aAAa,oBAAI,IAAoB;AAAA,EAC9C,eAA6C;AAAA,EAC7C,mBAAkD;AAAA,EAClD,kBAAiD;AAAA,EACjD,kBAAgD;AAAA,EAChD,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,0BAA8C,CAAC;AAAA;AAAA,EAE/C,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASZ,aAA8D,CAAC,IAAI,UACzE,IAAI,0CAAiB,IAAI,OAAO,EAAE,KAAK,KAAK,IAAI,CAAC;AAAA,EAC3C,gBACN,CAAC,IAAI,OAAO,WAAW,WAAW,IAAI,4CAAoB,IAAI,OAAO,WAAW,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOhF,mBAAmB,MAA8B;AA9G3D;AA+GI,eAAK,aAAL,mBAAe;AACf,QAAI,KAAK,WAAW;AAClB,WAAK,cAAc,KAAK,SAAS;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,QAAI,KAAK,gBAAgB;AACvB,WAAK,aAAa,KAAK,cAAc;AACrC,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA;AAAA,EAGO,YAAY,UAAyC,CAAC,GAAG;AAC9D,UAAM,EAAE,GAAG,SAAS,MAAM,aAAa,CAAC;AACxC,SAAK,GAAG,SAAS,KAAK,QAAQ,KAAK,IAAI,CAAC;AACxC,SAAK,GAAG,eAAe,KAAK,cAAc,KAAK,IAAI,CAAC;AACpD,SAAK,GAAG,UAAU,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,EAI5C;AAAA,EAEA,MAAc,UAAyB;AACrC,QAAI;AACF,YAAM,yBAAK,SAAK,uBAAK,KAAK,YAAY,OAAO,GAAG,IAAI;AACpD,WAAK,eAAe,IAAI,kCAAa,IAAI;AAEzC,YAAM,KAAK,cAAc,gBAAgB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAClE,YAAM,KAAK,cAAc,aAAa,EAAE,KAAK,IAAI,KAAK,KAAK,CAAC;AAE5D,YAAM,KAAK,qBAAqB,cAAc;AAC9C,YAAM,KAAK,qBAAqB,iBAAiB;AACjD,YAAM,KAAK,qBAAqB,mBAAmB;AACnD,YAAM,KAAK,qBAAqB,wBAAwB;AACxD,YAAM,KAAK,qBAAqB,oCAAoC;AACpE,YAAM,KAAK,qBAAqB,yBAAyB;AACzD,YAAM,KAAK,qBAAqB,gBAAgB;AAChD,YAAM,KAAK,qBAAqB,uBAAuB;AACvD,YAAM,KAAK,qBAAqB,0BAA0B;AAC1D,YAAM,KAAK,qBAAqB,UAAU;AAE1C,YAAM,UAAU,MAAM,KAAK,uBAAuB;AAClD,UAAI,QAAQ,WAAW,GAAG;AACxB,aAAK,IAAI,KAAK,yEAAoE;AAClF,cAAM,KAAK,qBAAqB,mBAAmB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,MAC9E;AAEA,iBAAW,UAAU,SAAS;AAC5B,cAAM,MAAM,KAAK,aAAa,aAAa,MAAM;AACjD,cAAM,KAAK,aAAa,mBAAmB,MAAM;AACjD,cAAM,KAAK,aAAa,mBAAmB,MAAM;AACjD,cAAM,WAAO,gDAAuB,QAAQ,OAAO,MAAM,EAAE;AAC3D,aAAK,YAAY,IAAI,KAAK,IAAI;AAE9B,YAAI,KAAK,IAAI;AACX,eAAK,IAAI,MAAM,mBAAmB,KAAK,EAAE,QAAQ,OAAO,WAAW,EAAE;AACrE,eAAK,KAAK,WAAW,IAAI,EAAE;AAAA,YAAM,CAAC,QAChC,KAAK,IAAI,MAAM,yBAAyB,KAAK,OAAO,WAAW,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,UACpF;AAAA,QACF;AAAA,MACF;AAEA,WAAK,kBAAkB,KAAK,YAAY,MAAM;AAC5C,aAAK,KAAK,kBAAkB;AAAA,MAC9B,GAAG,cAAc;AAEjB,WAAK,uBAAuB;AAAA,IAC9B,SAAS,KAAc;AACrB,WAAK,IAAI,MAAM,uBAAmB,uBAAQ,GAAG,CAAC,EAAE;AAAA,IAClD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,yBAAkD;AAC9D,UAAM,UAA0B,CAAC;AAQjC,UAAM,gBAAiB,KAAK,OAAmC;AAC/D,UAAM,aAA6B,MAAM,QAAQ,aAAa,IAAK,gBAAmC,CAAC;AACvG,QAAI,WAAW,SAAS,GAAG;AACzB,WAAK,IAAI,MAAM,aAAa,WAAW,MAAM,kDAAkD;AAC/F,iBAAW,UAAU,YAAY;AAC/B,cAAM,KAAK,mBAAmB,MAAM;AAAA,MACtC;AAEA,YAAM,KAAK,yBAAyB,kBAAkB,KAAK,SAAS,IAAI;AAAA,QACtE,QAAQ,EAAE,SAAS,CAAC,EAAE;AAAA,MACxB,CAAC;AACD,aAAO;AAAA,IACT;AAKA,UAAM,UAAU,MAAM,KAAK,uBAAuB;AAClD,eAAW,CAAC,IAAI,GAAG,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC/C,UAAI,IAAI,SAAS,UAAU;AACzB;AAAA,MACF;AACA,YAAM,SAAS,IAAI;AACnB,UAAI,EAAC,iCAAQ,mBAAkB,CAAC,OAAO,QAAQ;AAC7C;AAAA,MACF;AACA,YAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,SAAS,KAAK,EAAE;AACnD,WAAK,IAAI,MAAM,+BAA+B,OAAO,EAAE;AACvD,UAAI;AACJ,UAAI;AACF,gBAAQ,KAAK,QAAQ,OAAO,cAAc;AAAA,MAC5C,SAAS,KAAK;AACZ,aAAK,IAAI;AAAA,UACP,4BAA4B,OAAO,oCAC7B,uBAAQ,GAAG,CAAC;AAAA,QACpB;AACA;AAAA,MACF;AACA,cAAQ,KAAK;AAAA,QACX;AAAA,QACA,aAAa,OAAO,eAAe;AAAA,QACnC,QAAQ,OAAO;AAAA,QACf,aAAa,OAAO,eAAe,OAAO,eAAe;AAAA,QACzD,GAAI,OAAO,KAAK,EAAE,IAAI,OAAO,GAAG,IAAI,CAAC;AAAA,MACvC,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,mBAAmB,QAAqC;AACpE,UAAM,SAAS,KAAK,aAAa,aAAa,MAAM;AACpD,UAAM,iBAAiB,KAAK,QAAQ,OAAO,KAAK;AAChD,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,QAAQ,EAAE,MAAM,OAAO,eAAe,OAAO,YAAY;AAAA,QACzD,QAAQ;AAAA,UACN;AAAA,UACA,aAAa,OAAO;AAAA,UACpB,QAAQ,OAAO;AAAA,UACf,aAAa,OAAO;AAAA,UACpB,GAAI,OAAO,KAAK,EAAE,IAAI,OAAO,GAAG,IAAI,CAAC;AAAA,QACvC;AAAA,MACF;AAAA,MACA,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE;AAAA,IACnC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,mBAAmB,YAAoC;AAE7D,UAAM,WAAW,MAAM,KAAK,KAAK,YAAY,OAAO,CAAC,EAAE,KAAK,OAAK,EAAE,OAAO,WAAW,WAAW,MAAM;AACtG,QAAI,UAAU;AACZ;AAAA,IACF;AAGA,QAAI,KAAK,wBAAwB,KAAK,OAAK,EAAE,WAAW,WAAW,MAAM,GAAG;AAC1E;AAAA,IACF;AAEA,SAAK,wBAAwB,KAAK,UAAU;AAC5C,SAAK,IAAI;AAAA,MACP,SAAS,WAAW,IAAI,KAAK,WAAW,WAAW,QAAQ,WAAW,EAAE;AAAA,IAC1E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,SAAS,UAA4B;AA3S/C;AA+SI,SAAK,YAAY;AACjB,QAAI;AACF,UAAI,KAAK,cAAc;AACrB,aAAK,aAAa,KAAK,YAAY;AAAA,MACrC;AACA,UAAI,KAAK,kBAAkB;AACzB,aAAK,cAAc,KAAK,gBAAgB;AAAA,MAC1C;AACA,UAAI,KAAK,iBAAiB;AACxB,aAAK,cAAc,KAAK,eAAe;AAAA,MACzC;AACA,UAAI,KAAK,iBAAiB;AACxB,aAAK,aAAa,KAAK,eAAe;AAAA,MACxC;AAEA,iBAAK,cAAL,mBAAgB;AAEhB,iBAAW,QAAQ,KAAK,YAAY,OAAO,GAAG;AAC5C,aAAK,mBAAmB,IAAI;AAAA,MAC9B;AACA,WAAK,YAAY,MAAM;AAEvB,WAAK,KAAK,SAAS,mBAAmB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,IACjE,UAAE;AACA,eAAS;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAc,cAAc,IAAY,OAAyD;AAC/F,QAAI;AACF,UAAI,CAAC,SAAS,MAAM,OAAO,KAAK,WAAW;AACzC;AAAA,MACF;AAEA,UAAI,GAAG,SAAS,eAAe,GAAG;AAChC,YAAI,MAAM,KAAK;AACb,gBAAM,KAAK,aAAa;AAAA,QAC1B;AACA;AAAA,MACF;AAEA,UAAI,GAAG,SAAS,SAAS,GAAG;AAC1B,YAAI,MAAM,KAAK;AACb,gBAAM,KAAK,aAAa,EAAE;AAAA,QAC5B;AACA;AAAA,MACF;AAEA,YAAM,OAAO,KAAK,uBAAuB,EAAE;AAC3C,UAAI,CAAC,QAAQ,CAAC,KAAK,IAAI;AACrB;AAAA,MACF;AAEA,YAAM,SAAS,KAAK,WAAW,KAAK,IAAI,KAAK,OAAO,KAAK;AAEzD,UAAI;AACF,YAAI,GAAG,SAAS,gBAAgB,GAAG;AACjC,eAAK,IAAI,KAAK,aAAa,KAAK,OAAO,WAAW,KAAK,KAAK,EAAE,GAAG;AACjE,gBAAM,OAAO,OAAO;AAAA,QACtB,WAAW,GAAG,SAAS,kBAAkB,GAAG;AAC1C,gBAAM,OAAO,SAAS;AAAA,QACxB,WAAW,GAAG,SAAS,uBAAuB,GAAG;AAC/C,gBAAM,OAAO,UAAU,EAAE,eAAe,CAAC,CAAC,MAAM,IAAI,CAAC;AACrD,gBAAM,KAAK,cAAc,IAAI,EAAE,KAAK,MAAM,KAAK,KAAK,KAAK,CAAC;AAAA,QAC5D,WAAW,GAAG,SAAS,mCAAmC,GAAG;AAC3D,gBAAM,OAAO,UAAU;AAAA,YACrB,2BAA2B,OAAO,MAAM,GAAG;AAAA,UAC7C,CAAC;AACD,gBAAM,KAAK,cAAc,IAAI,EAAE,KAAK,MAAM,KAAK,KAAK,KAAK,CAAC;AAAA,QAC5D,WAAW,GAAG,SAAS,wBAAwB,GAAG;AAChD,gBAAM,OAAO,UAAU,EAAE,gBAAgB,CAAC,CAAC,MAAM,IAAI,CAAC;AACtD,gBAAM,KAAK,cAAc,IAAI,EAAE,KAAK,MAAM,KAAK,KAAK,KAAK,CAAC;AAAA,QAC5D,WAAW,GAAG,SAAS,eAAe,GAAG;AACvC,gBAAM,WAAO,mCAAoB,OAAO,MAAM,GAAG,CAAC;AAClD,cAAI,CAAC,MAAM;AACT,iBAAK,IAAI;AAAA,cACP,gCAAgC,OAAO,MAAM,GAAG,CAAC;AAAA,YACnD;AACA;AAAA,UACF;AACA,gBAAM,OAAO,aAAa,EAAE,KAAK,CAAC;AAClC,gBAAM,KAAK,cAAc,IAAI,EAAE,KAAK,MAAM,KAAK,KAAK,KAAK,CAAC;AAAA,QAC5D,WAAW,GAAG,SAAS,sBAAsB,GAAG;AAC9C,gBAAM,aAAS,uCAAwB,OAAO,MAAM,GAAG,CAAC;AACxD,cAAI,CAAC,OAAO,IAAI;AACd,iBAAK,IAAI;AAAA,cACP,yCAAyC,OAAO,MAAM,gCAA2B,OAAO,MAAM;AAAA,YAChG;AACA;AAAA,UACF;AACA,gBAAM,OAAO,aAAa,EAAE,aAAa,OAAO,MAAM,CAAC;AACvD,gBAAM,KAAK,cAAc,IAAI,EAAE,KAAK,MAAM,KAAK,KAAK,KAAK,CAAC;AAAA,QAC5D,WAAW,GAAG,SAAS,yBAAyB,GAAG;AACjD,gBAAM,OAAO,aAAa,EAAE,gBAAgB,CAAC,CAAC,MAAM,IAAI,CAAC;AACzD,gBAAM,KAAK,cAAc,IAAI,EAAE,KAAK,MAAM,KAAK,KAAK,KAAK,CAAC;AAAA,QAC5D;AAAA,MACF,SAAS,KAAK;AACZ,aAAK,IAAI,KAAK,iBAAiB,EAAE,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,MACtD;AAAA,IACF,SAAS,KAAc;AACrB,WAAK,IAAI,MAAM,2BAAuB,uBAAQ,GAAG,CAAC,EAAE;AAAA,IACtD;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,eAA8B;AAC1C,QAAI,KAAK,WAAW;AAClB,WAAK,IAAI,MAAM,wBAAwB;AACvC;AAAA,IACF;AAGA,UAAM,KAAK,cAAc,gBAAgB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAElE,SAAK,YAAY;AACjB,SAAK,0BAA0B,CAAC;AAGhC,SAAK,eAAe;AAGpB,UAAM,UAAU,MAAM,KAAK,cAAc,WAAW;AACpD,SAAK,mBAAkB,mCAAS,OAAM,OAAO,QAAQ,GAAG,EAAE,KAAK,IAAI;AACnE,UAAM,KAAK,cAAc,aAAa,EAAE,KAAK,IAAI,KAAK,KAAK,CAAC;AAE5D,QAAI,KAAK,iBAAiB;AAGxB,UAAI,KAAC,2BAAY,KAAK,eAAe,GAAG;AACtC,aAAK,IAAI,KAAK,uBAAuB,KAAK,eAAe,4CAAuC;AAChG,aAAK,YAAY;AACjB,aAAK,kBAAkB;AACvB;AAAA,MACF;AACA,WAAK,IAAI;AAAA,QACP,4BAA4B,KAAK,eAAe;AAAA,MAClD;AAEA,WAAK,wBAAwB,KAAK;AAAA,QAChC,IAAI,KAAK;AAAA,QACT,aAAa;AAAA,QACb,QAAQ;AAAA,QACR,MAAM,KAAK;AAAA,MACb,CAAC;AAAA,IACH,OAAO;AACL,WAAK,IAAI;AAAA,QACP;AAAA,MACF;AAGA,UAAI,CAAC,KAAK,WAAW;AACnB,aAAK,YAAY,IAAI,qCAAoB,KAAK,GAAG;AAAA,MACnD;AACA,WAAK,UAAU,MAAM,gBAAc;AACjC,aAAK,mBAAmB,UAAU;AAAA,MACpC,CAAC;AAAA,IACH;AAGA,SAAK,mBAAmB,KAAK,YAAY,MAAM;AAC7C,WAAK,KAAK,YAAY;AAAA,IACxB,GAAG,eAAe;AAGlB,SAAK,eAAe,KAAK,WAAW,MAAM;AACxC,WAAK,YAAY;AACjB,WAAK,IAAI,KAAK,8DAA8D;AAAA,IAC9E,GAAG,kBAAkB;AAAA,EACvB;AAAA;AAAA,EAGA,MAAc,cAA6B;AACzC,eAAW,UAAU,KAAK,yBAAyB;AACjD,UAAI;AACF,cAAM,SAAS,KAAK,WAAW,OAAO,IAAI,EAAE;AAC5C,cAAM,SAAS,MAAM,OAAO,eAAe;AAG3C,aAAK,IAAI;AAAA,UACP,4BAA4B,OAAO,IAAI,KAAK,OAAO,WAAW,QAAQ,OAAO,EAAE;AAAA,QACjF;AAGA,cAAM,eAAe,KAAK,WAAW,OAAO,IAAI,OAAO,KAAK;AAC5D,cAAM,OAAO,MAAM,aAAa,cAAc;AAE9C,cAAM,eAA6B;AAAA,UACjC,OAAO,OAAO;AAAA,UACd,aAAa,KAAK;AAAA,UAClB,QAAQ,KAAK;AAAA,UACb,aAAa,KAAK;AAAA,UAClB,IAAI,OAAO;AAAA,QACb;AAGA,cAAM,KAAK,mBAAmB,YAAY;AAC1C,cAAM,KAAK,aAAa,mBAAmB,YAAY;AAKvD,cAAM,MAAM,KAAK,aAAa,aAAa,YAAY;AACvD,cAAM,WAAW,KAAK,YAAY,IAAI,GAAG;AACzC,YAAI,UAAU;AACZ,eAAK,IAAI,MAAM,4CAA4C,aAAa,WAAW,EAAE;AACrF,eAAK,mBAAmB,QAAQ;AAAA,QAClC;AAGA,cAAM,WAAO,gDAAuB,cAAc,OAAO,EAAE;AAC3D,aAAK,YAAY,IAAI,KAAK,IAAI;AAC9B,aAAK,KAAK,WAAW,IAAI,EAAE;AAAA,UAAM,CAAC,QAChC,KAAK,IAAI,MAAM,yBAAyB,KAAK,OAAO,WAAW,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,QACpF;AAIA,aAAK,0BAA0B,KAAK,wBAAwB,OAAO,OAAK,EAAE,WAAW,KAAK,MAAM;AAEhG,aAAK,uBAAuB;AAG5B;AAAA,MACF,SAAS,KAAK;AAEZ,YAAI,eAAe,+CAAsB,IAAI,eAAe,KAAK;AAC/D;AAAA,QACF;AACA,aAAK,IAAI,MAAM,0BAA0B,OAAO,EAAE,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGQ,cAAoB;AAC1B,SAAK,YAAY;AACjB,SAAK,kBAAkB;AACvB,SAAK,0BAA0B,CAAC;AAGhC,QAAI,KAAK,WAAW;AAClB,WAAK,UAAU,KAAK;AACpB,WAAK,YAAY;AAAA,IACnB;AAEA,QAAI,KAAK,kBAAkB;AACzB,WAAK,cAAc,KAAK,gBAAgB;AACxC,WAAK,mBAAmB;AAAA,IAC1B;AACA,QAAI,KAAK,cAAc;AACrB,WAAK,aAAa,KAAK,YAAY;AACnC,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA;AAAA,EAGQ,kBAAwB;AAE9B,QAAI,KAAK,aAAa,KAAK,WAAW;AACpC;AAAA,IACF;AAKA,SAAK,IAAI,MAAM,yDAAoD;AAEnE,SAAK,YAAY,IAAI,qCAAoB,KAAK,GAAG;AACjD,SAAK,UAAU,MAAM,gBAAc;AAEjC,iBAAW,QAAQ,KAAK,YAAY,OAAO,GAAG;AAC5C,YAAI,KAAK,OAAO,WAAW,WAAW,QAAQ;AAC5C;AAAA,QACF;AACA,YAAI,WAAW,OAAO,KAAK,MAAM,KAAK,iBAAiB;AACrD;AAAA,QACF;AAGA,YAAI,KAAK,YAAY;AACnB;AAAA,QACF;AAEA,aAAK,IAAI,KAAK,GAAG,KAAK,OAAO,WAAW,qBAAqB,WAAW,EAAE,SAAS,KAAK,EAAE,GAAG;AAG7F,aAAK,KAAK,WAAW;AACrB,aAAK,OAAO,KAAK,WAAW;AAC5B,aAAK,cAAc;AACnB,aAAK,oBAAoB;AAIzB,aAAK,mBAAmB,KAAK,MAAM,EAAE;AAAA,UAAM,CAAC,QAC1C,KAAK,IAAI,MAAM,gCAAgC,KAAK,OAAO,WAAW,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,QAC3F;AAGA,YAAI,KAAK,gBAAgB;AACvB,eAAK,aAAa,KAAK,cAAc;AACrC,eAAK,iBAAiB;AAAA,QACxB;AACA,YAAI,KAAK,WAAW;AAClB,eAAK,cAAc,KAAK,SAAS;AACjC,eAAK,YAAY;AAAA,QACnB;AACA,aAAK,iBAAiB,IAAI;AAC1B;AAAA,MACF;AAAA,IACF,CAAC;AAOD,SAAK,kBAAkB,KAAK,WAAW,MAAM;AAC3C,WAAK,kBAAkB;AACvB,WAAK,eAAe;AAEpB,iBAAW,QAAQ,KAAK,YAAY,OAAO,GAAG;AAC5C,YAAI,CAAC,KAAK,mBAAmB,KAAK,cAAc,GAAG;AACjD,eAAK,IAAI;AAAA,YACP,GAAG,KAAK,OAAO,WAAW,oDAA+C,sBAAsB,GAAI;AAAA,UACrG;AAAA,QACF;AAAA,MACF;AAAA,IACF,GAAG,sBAAsB;AAAA,EAC3B;AAAA;AAAA,EAGQ,iBAAuB;AAC7B,QAAI,KAAK,iBAAiB;AACxB,WAAK,aAAa,KAAK,eAAe;AACtC,WAAK,kBAAkB;AAAA,IACzB;AACA,QAAI,KAAK,aAAa,CAAC,KAAK,WAAW;AACrC,WAAK,UAAU,KAAK;AACpB,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,WAAW,MAAuC;AAC9D,QAAI,KAAK,aAAa,KAAK,SAAS;AAClC;AAAA,IACF;AACA,QAAI;AACF,YAAM,SAAS,KAAK,WAAW,KAAK,IAAI,KAAK,OAAO,KAAK;AACzD,YAAM,OAAO,MAAM,OAAO,cAAc;AACxC,UAAI,KAAK,aAAa,KAAK,SAAS;AAClC;AAAA,MACF;AACA,YAAM,MAAM,KAAK,aAAa,aAAa,KAAK,MAAM;AACtD,YAAM,KAAK,cAAc,GAAG,GAAG,kBAAkB;AAAA,QAC/C,KAAK,KAAK;AAAA,QACV,KAAK;AAAA,MACP,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,KAAK,WAAW;AAClB;AAAA,MACF;AACA,WAAK,eAAe,MAAM,QAAQ,GAAG;AAAA,IACvC;AAEA,QAAI,KAAK,aAAa,KAAK,SAAS;AAClC;AAAA,IACF;AACA,SAAK,iBAAiB,IAAI;AAC1B,SAAK,KAAK,eAAe,IAAI;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,iBAAiB,MAA8B;AACrD,QAAI,CAAC,KAAK,IAAI;AACZ;AAAA,IACF;AAGA,QAAI,KAAK,iBAAiB,mBAAmB;AAC3C;AAAA,IACF;AAIA,SAAK,aAAa;AAKlB,QAAI,KAAK,UAAU;AACjB,WAAK,SAAS,MAAM;AACpB,WAAK,WAAW;AAAA,IAClB;AAGA,YAAI,2CAAsB,KAAK,aAAa,yBAAyB,gBAAgB,GAAG;AACtF,WAAK,gBAAgB;AAAA,IACvB;AAGA,UAAM,WAAW,KAAK;AAAA,MACpB,KAAK;AAAA,MACL,KAAK,OAAO;AAAA,MACZ;AAAA,QACE,eAAe,UAAQ,KAAK,gBAAgB,MAAM,IAAI;AAAA,QACtD,UAAU,UAAQ,KAAK,WAAW,MAAM,IAAI;AAAA,QAC5C,WAAW,UAAQ,KAAK,YAAY,MAAM,IAAI;AAAA,QAC9C,aAAa,MAAM,KAAK,cAAc,IAAI;AAAA,QAC1C,gBAAgB,WAAS,KAAK,iBAAiB,MAAM,KAAK;AAAA,QAC1D,KAAK,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,QACE,UAAU,CAAC,IAAI,OAAO,KAAK,WAAW,IAAI,EAAE;AAAA,QAC5C,QAAQ,OAAK;AACX,eAAK,aAAa,CAAqB;AAAA,QACzC;AAAA,QACA,mBAAmB,CAAC,IAAI,OAAO,KAAK,YAAY,IAAI,EAAE;AAAA,QACtD,iBAAiB,OAAK;AACpB,eAAK,cAAc,CAAsB;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAEA,SAAK,WAAW;AAChB,aAAS,QAAQ;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,gBAAgB,MAAwB,MAAyB;AAEvE,QAAI,KAAK,WAAW,KAAK,WAAW;AAClC;AAAA,IACF;AAGA,SAAK,aAAa,kBAAkB,KAAK,QAAQ,IAAI,EAAE,MAAM,CAAC,QAAiB;AAC7E,WAAK,IAAI,MAAM,gCAAgC,KAAK,OAAO,WAAW,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,IAC3F,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,WAAW,MAAwB,MAAwB;AACjE,QAAI,KAAK,WAAW,KAAK,WAAW;AAClC;AAAA,IACF;AACA,SAAK,aAAa,aAAa,KAAK,QAAQ,IAAI,EAAE,MAAM,CAAC,QAAiB;AACxE,WAAK,IAAI,MAAM,gCAAgC,KAAK,OAAO,WAAW,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,IAC3F,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,YAAY,MAAwB,MAA4B;AACtE,QAAI,KAAK,WAAW,KAAK,WAAW;AAClC;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,iBAAiB,KAAK,iBAAiB,GAAG;AAClD;AAAA,IACF;AACA,SAAK,aAAa,cAAc,KAAK,QAAQ,IAAI,EAAE,MAAM,CAAC,QAAiB;AACzE,WAAK,IAAI,MAAM,iCAAiC,KAAK,OAAO,WAAW,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,IAC5F,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,cAAc,MAA8B;AA5xBtD;AA6xBI,SAAK,kBAAkB;AACvB,SAAK,cAAc;AACnB,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,KAAK,IAAI;AAChC,SAAK,aAAa;AAClB,SAAK,KAAK,aAAa,mBAAmB,KAAK,QAAQ,IAAI;AAC3D,SAAK,uBAAuB;AAG5B,QAAI,KAAK,WAAW;AAClB,WAAK,cAAc,KAAK,SAAS;AACjC,WAAK,YAAY;AAAA,IACnB;AAGA,QAAI,KAAK,aAAa,CAAC,KAAK,WAAW;AACrC,YAAM,eAAe,MAAM,KAAK,KAAK,YAAY,OAAO,CAAC,EAAE,MAAM,OAAK,EAAE,eAAe;AACvF,UAAI,cAAc;AAChB,aAAK,eAAe;AAAA,MACtB;AAAA,IACF;AAIA,QAAI,KAAK,eAAe;AACtB,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,YAAW,UAAK,WAAW,IAAI,KAAK,OAAO,MAAM,MAAtC,YAA2C;AAC5D,YAAM,MAAM,KAAK,WAAW,IAAI,IAC5B,GAAG,KAAK,OAAO,WAAW,0CAC1B,GAAG,KAAK,OAAO,WAAW;AAC9B,cAAI,6CAAwB,UAAU,KAAK,gBAAgB,GAAG;AAC5D,aAAK,WAAW,IAAI,KAAK,OAAO,QAAQ,GAAG;AAC3C,aAAK,IAAI,KAAK,GAAG;AAAA,MACnB,OAAO;AACL,aAAK,IAAI,MAAM,GAAG,GAAG,aAAa;AAAA,MACpC;AACA,WAAK,gBAAgB;AAAA,IACvB;AAEA,SAAK,IAAI,MAAM,0BAA0B,KAAK,OAAO,WAAW,KAAK,KAAK,EAAE,GAAG;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,iBAAiB,MAAwB,OAAqB;AAGpE,UAAM,cAAc,iBAAiB,+CAAsB,MAAM,cAAc;AAG/E,QAAI,KAAK,kBAAkB,KAAK,CAAC,aAAa;AAC5C,YAAM,WAAW,KAAK,IAAI,IAAI,KAAK;AACnC,YAAM,iBAAa;AAAA,QACjB,KAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,UAAI,WAAW,qBAAqB;AAClC,aAAK;AAAA,MACP,OAAO;AACL,aAAK,oBAAoB;AAAA,MAC3B;AAEA,UAAI,eAAe,kBAAkB;AACnC,aAAK,IAAI,MAAM,GAAG,KAAK,OAAO,WAAW,8DAAyD;AAAA,MACpG,WAAW,eAAe,cAAc;AACtC,aAAK,IAAI,MAAM,GAAG,KAAK,OAAO,WAAW,uDAAkD;AAAA,MAC7F;AAAA,IACF;AAEA,SAAK,kBAAkB;AACvB,SAAK,WAAW;AAChB,SAAK,aAAa;AAClB,SAAK,KAAK,aAAa,mBAAmB,KAAK,QAAQ,KAAK;AAC5D,SAAK,uBAAuB;AAE5B,QAAI,OAAO;AACT,WAAK,eAAe,MAAM,MAAM,KAAK;AAAA,IACvC;AAGA,QAAI,CAAC,KAAK;AAAA,MAAkB;AAAA,MAAM;AAAA;AAAA,MAA2B;AAAA,IAAK,GAAG;AACnE;AAAA,IACF;AAGA,SAAK,kBAAkB,IAAI;AAG3B,SAAK;AACL,UAAM,WAAW,KAAK,WAAW,IAAI,IAAI,+BAA+B;AACxE,UAAM,YAAQ,2CAAsB,KAAK,aAAa,sBAAsB,QAAQ;AACpF,UAAM,MAAM,KAAK,aAAa,aAAa,KAAK,MAAM;AACtD,SAAK,IAAI,MAAM,GAAG,GAAG,qBAAqB,QAAQ,GAAI,cAAc,KAAK,WAAW,GAAG;AAEvF,SAAK,iBAAiB,KAAK,WAAW,MAAM;AAC1C,WAAK,iBAAiB;AACtB,WAAK,iBAAiB,IAAI;AAAA,IAC5B,GAAG,KAAK;AAAA,EACV;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,kBAAkB,MAA8B;AACtD,QAAI,KAAK,aAAa,CAAC,KAAK,IAAI;AAC9B;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,WAAW,IAAI;AACrC,UAAM,eAAW,0CAAqB,UAAU,cAAc,qBAAqB;AACnF,UAAM,SAAS,KAAK,WAAW,KAAK,IAAI,KAAK,OAAO,KAAK;AAEzD,SAAK,YAAY,KAAK,YAAY,YAAY;AAI5C,UAAI,KAAK,WAAW,KAAK,WAAW;AAClC;AAAA,MACF;AACA,UAAI;AACF,cAAM,OAAO,MAAM,OAAO,eAAe;AACzC,YAAI,KAAK,WAAW,KAAK,WAAW;AAClC;AAAA,QACF;AACA,cAAM,KAAK,aAAa,kBAAkB,KAAK,QAAQ,IAAI;AAAA,MAC7D,SAAS,KAAK;AACZ,YAAI,KAAK,WAAW;AAClB;AAAA,QACF;AACA,aAAK,eAAe,MAAM,QAAQ,GAAG;AAGrC,YAAI,eAAe,+CAAsB,IAAI,cAAc,qBAAqB;AAC9E,eAAK;AAAA,YAAkB;AAAA,YAAM;AAAA;AAAA,YAAyB;AAAA,UAAI;AAC1D;AAAA,QACF;AAIA,YAAI,CAAC,gBAAY,uCAAc,GAAG,MAAM,aAAa,KAAK,WAAW;AACnE,eAAK,cAAc,KAAK,SAAS;AACjC,eAAK,YAAY;AAAA,QACnB;AAAA,MACF;AAAA,IACF,GAAG,QAAQ;AAAA,EACb;AAAA;AAAA,EAGA,MAAc,oBAAmC;AAC/C,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AACA,UAAM,QAAQ,MAAM,KAAK,KAAK,YAAY,OAAO,CAAC,EAC/C,OAAO,OAAK,EAAE,MAAM,EAAE,mBAAmB,CAAC,EAAE,OAAO,EACnD,IAAI,OAAK,KAAK,eAAe,CAAC,CAAC;AAClC,UAAM,QAAQ,IAAI,KAAK;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,eAAe,MAAuC;AAClE,QAAI,CAAC,KAAK,MAAM,KAAK,WAAW,KAAK,WAAW;AAC9C;AAAA,IACF;AAEA,QAAI;AACF,YAAM,SAAS,KAAK,WAAW,KAAK,IAAI,KAAK,OAAO,KAAK;AACzD,YAAM,SAAS,MAAM,OAAO,UAAU;AACtC,UAAI,KAAK,WAAW,KAAK,WAAW;AAClC;AAAA,MACF;AACA,YAAM,KAAK,aAAa,aAAa,KAAK,QAAQ,MAAM;AAMxD,UAAI;AACF,cAAM,OAAO,MAAM,OAAO,cAAc;AACxC,YAAI,CAAC,KAAK,WAAW,CAAC,KAAK,aAAa,KAAK,gBAAgB,KAAK,iBAAiB,KAAK,OAAO,aAAa;AAC1G,eAAK,IAAI,KAAK,GAAG,KAAK,OAAO,WAAW,sBAAsB,KAAK,YAAY,0BAAqB;AACpG,eAAK,OAAO,cAAc,KAAK;AAC/B,gBAAM,KAAK,mBAAmB,KAAK,MAAM;AAAA,QAC3C;AAAA,MACF,QAAQ;AAAA,MAGR;AAMA,UAAI,KAAK,WAAW,KAAK,WAAW;AAClC;AAAA,MACF;AACA,UAAI;AACF,cAAM,UAAU,MAAM,OAAO,aAAa;AAC1C,YAAI,KAAK,WAAW,KAAK,WAAW;AAClC;AAAA,QACF;AAEA,YAAI,QAAQ,iBAAiB,QAAQ,gBAAgB,GAAG;AACtD,gBAAM,KAAK,aAAa,cAAc,KAAK,QAAQ,OAAO;AAAA,QAC5D;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,eAAe,+CAAsB,IAAI,eAAe,KAAK;AAC/D;AAAA,QACF;AACA,aAAK,IAAI,MAAM,GAAG,KAAK,OAAO,WAAW,mBAAe,uBAAQ,GAAG,CAAC,EAAE;AAAA,MACxE;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,KAAK,WAAW;AAClB;AAAA,MACF;AACA,WAAK,eAAe,MAAM,UAAU,GAAG;AAAA,IACzC;AAAA,EACF;AAAA;AAAA,EAGQ,yBAA+B;AACrC,UAAM,eAAe,MAAM,KAAK,KAAK,YAAY,OAAO,CAAC,EAAE,KAAK,OAAK,EAAE,eAAe;AAEtF,SAAK,KAAK,qBAAqB,mBAAmB;AAAA,MAChD,KAAK;AAAA,MACL,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,aAAa,SAAgC;AACzD,UAAM,OAAO,KAAK,uBAAuB,OAAO;AAChD,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AAEA,UAAM,MAAM,KAAK,aAAa,aAAa,KAAK,MAAM;AACtD,SAAK,IAAI,KAAK,mBAAmB,KAAK,OAAO,WAAW,KAAK,KAAK,OAAO,MAAM,GAAG;AAKlF,SAAK,UAAU;AAKf,QAAI,KAAK,MAAM,KAAK,OAAO,OAAO;AAChC,WAAK,KAAK,WAAW,KAAK,IAAI,KAAK,OAAO,KAAK,EAC5C,WAAW,EACX,MAAM,CAAC,QAAiB,KAAK,IAAI,MAAM,2BAA2B,KAAK,OAAO,WAAW,SAAK,uBAAQ,GAAG,CAAC,EAAE,CAAC;AAAA,IAClH;AAGA,SAAK,mBAAmB,IAAI;AAC5B,SAAK,YAAY,OAAO,GAAG;AAG3B,UAAM,KAAK,aAAa,aAAa,KAAK,MAAM;AAEhD,SAAK,uBAAuB;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,uBAAuB,SAA+C;AAC5E,eAAO,oBAAAA,wBAA0B,SAAS,KAAK,WAAW,KAAK,WAAW;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,WAAW,MAAiC;AAClD,WAAO,KAAK,qBAAqB;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBQ,kBAAkB,MAAwB,OAAgB,eAAiC;AA1lCrG;AA2lCI,QAAI,EAAE,iBAAiB,gDAAuB,MAAM,cAAc,qBAAqB;AACrF,aAAO;AAAA,IACT;AACA,SAAK;AACL,QAAI,KAAK,gBAAgB,mBAAmB;AAC1C,aAAO;AAAA,IACT;AACA,SAAK,IAAI,KAAK,GAAG,KAAK,OAAO,WAAW,8CAAyC;AACjF,QAAI,eAAe;AACjB,UAAI,KAAK,WAAW;AAClB,aAAK,cAAc,KAAK,SAAS;AACjC,aAAK,YAAY;AAAA,MACnB;AACA,UAAI,KAAK,gBAAgB;AACvB,aAAK,aAAa,KAAK,cAAc;AACrC,aAAK,iBAAiB;AAAA,MACxB;AACA,iBAAK,aAAL,mBAAe;AAAA,IACjB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBQ,eAAe,MAAwB,SAAiB,KAAoB;AAtoCtF;AAuoCI,UAAM,gBAAY,uCAAc,GAAG;AACnC,UAAM,WAAW,cAAc,KAAK;AACpC,SAAK,gBAAgB;AAErB,QAAI,UAAU;AACZ,WAAK,IAAI,MAAM,GAAG,KAAK,OAAO,WAAW,IAAI,OAAO,SAAK,uBAAQ,GAAG,CAAC,EAAE;AACvE;AAAA,IACF;AAKA,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,YAAW,UAAK,WAAW,IAAI,KAAK,OAAO,MAAM,MAAtC,YAA2C;AAC5D,QAAI,KAAC,6CAAwB,UAAU,KAAK,gBAAgB,GAAG;AAC7D,WAAK,IAAI,MAAM,GAAG,KAAK,OAAO,WAAW,IAAI,OAAO,oBAAgB,uBAAQ,GAAG,CAAC,EAAE;AAClF;AAAA,IACF;AAEA,SAAK,WAAW,IAAI,KAAK,OAAO,QAAQ,GAAG;AAC3C,QAAI,cAAc,WAAW;AAC3B,WAAK,IAAI,KAAK,GAAG,KAAK,OAAO,WAAW,gDAA2C;AAAA,IACrF,OAAO;AACL,WAAK,IAAI,KAAK,GAAG,KAAK,OAAO,WAAW,IAAI,OAAO,SAAK,uBAAQ,GAAG,CAAC,EAAE;AAAA,IACxE;AAAA,EACF;AACF;AAEA,IAAI,QAAQ,SAAS,QAAQ;AAC3B,SAAO,UAAU,CAAC,YAAuD,IAAI,WAAW,OAAO;AACjG,OAAO;AACL,GAAC,MAAM,IAAI,WAAW,GAAG;AAC3B;",
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { errText, isValidIpv4, parseBatteryPermissions, validateBatteryMode } from \"./lib/coerce\";\nimport { classifyError, createDeviceConnection, UNSTABLE_DISCONNECT_THRESHOLD } from \"./lib/connection-utils\";\nimport { HomeWizardDiscovery } from \"./lib/discovery\";\nimport { HomeWizardApiError, HomeWizardClient } from \"./lib/homewizard-client\";\nimport {\n computeReconnectDelay,\n decideUnstableTransition,\n findConnectionForState as resolveConnectionForState,\n pickRestPollInterval,\n shouldEmitAfterCooldown,\n shouldStartIpRecovery,\n} from \"./lib/main-helpers\";\nimport { StateManager } from \"./lib/state-manager\";\nimport type {\n BatteryControl,\n DeviceConfig,\n DeviceConnection,\n DiscoveredDevice,\n Measurement,\n SystemInfo,\n} from \"./lib/types\";\nimport { HomeWizardWebSocket, type TimerDeps, type WsCallbacks } 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/** Retry mDNS every N WS failures after first attempt (~1 hour at 5 min cap) */\nconst MDNS_RETRY_EVERY = 12;\n/** Connection must last this long to count as \"stable\" */\nconst STABLE_THRESHOLD_MS = 600_000;\n/** Max reconnect delay for unstable devices */\nconst WS_RECONNECT_MAX_UNSTABLE_MS = 60_000;\n/** REST fallback interval for unstable devices (slower, not stopped) */\nconst REST_POLL_UNSTABLE_MS = 30_000;\n/**\n * Cooldown window for `device unreachable` warns. Per-device, category-\n * spanning: bouncing hardware should produce max 1\u00D7 warn per window, regardless\n * of whether each cycle's failure was TIMEOUT, NETWORK, or HTTP_503. Survives\n * the lastErrorCode-reset on recovery so chronic bouncing doesn't flap warn /\n * debug at every cycle.\n */\nconst WARN_COOLDOWN_MS = 60 * 60 * 1000;\n/** Cooldown window for `connection restored` infos \u2014 analog to warn cooldown. */\nconst INFO_COOLDOWN_MS = 60 * 60 * 1000;\n\n/**\n * HomeWizard adapter \u2014 manages multiple devices over API v2 (HTTPS + WebSocket):\n * pairing, real-time push, REST fallback, reconnect/recovery and state mapping.\n * Exported so the orchestration unit tests can drive its handlers directly.\n */\nexport class HomeWizard extends utils.Adapter {\n private stateManager!: StateManager;\n private discovery: HomeWizardDiscovery | null = null;\n private readonly connections = new Map<string, DeviceConnection>();\n /**\n * Per-device last-warn timestamp for chronic-bouncing cooldown. Key =\n * `conn.config.serial` (kategorien\u00FCbergreifend). The classifyError-based\n * `lastErrorCode`-Dedup in {@link logDeviceError} resets on every recovery,\n * so on chronic bouncing a new disconnect counts as \"first occurrence\"\n * \u2192 wieder warn. This cooldown stamp persists across recoveries so the user\n * sees max one warn per WARN_COOLDOWN_MS per device.\n */\n private readonly lastWarnAt = new Map<string, number>();\n /** Per-device last-info timestamp for `connection restored`. Analog cooldown. */\n private readonly lastInfoAt = new Map<string, number>();\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 /**\n * In-flight guard for {@link pollPairing}: the poll runs every 2 s, but a\n * single device's requestPairing can hang up to the 10 s HTTP timeout \u2014\n * without the guard, overlapping polls would fire concurrent POST /api/user\n * against the same device.\n */\n private pairingPollBusy = false;\n private pairingManualIp = \"\";\n private discoveredDuringPairing: DiscoveredDevice[] = [];\n /** Set during onUnload \u2014 async paths bail before further setStateAsync calls. */\n private unloading = false;\n /**\n * Factories for the REST/WS clients \u2014 default to the real constructors. Test seams:\n * a unit test can replace these with fakes to exercise the orchestration (initDevice,\n * onWsConnected/onWsDisconnected, onStateChange) without real network.\n *\n * @param ip Device IP address\n * @param token Bearer token (empty string for pairing requests)\n */\n private makeClient: (ip: string, token: string) => HomeWizardClient = (ip, token) =>\n new HomeWizardClient(ip, token, { log: this.log });\n private makeWebSocket: (ip: string, token: string, callbacks: WsCallbacks, timers: TimerDeps) => HomeWizardWebSocket =\n (ip, token, callbacks, timers) => new HomeWizardWebSocket(ip, token, callbacks, timers);\n private makeDiscovery: () => HomeWizardDiscovery = () => new HomeWizardDiscovery(this.log);\n\n /**\n * Close a connection's WebSocket and clear its poll + reconnect timers.\n *\n * @param conn Device connection to tear down\n */\n private teardownConnection(conn: DeviceConnection): void {\n conn.wsClient?.close();\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n }\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"homewizard\" });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"stateChange\", this.onStateChange.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n // No process-level unhandledRejection/uncaughtException handlers: in compact mode they\n // are process-wide and cross-adapter-harmful. Every handler has .bind+try/catch and\n // fire-and-forget paths use .catch (Fleet pattern \u2014 hueemu/parcelapp/nut).\n }\n\n private async onReady(): Promise<void> {\n try {\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.stateManager = new StateManager(this);\n\n await this.setStateAsync(\"startPairing\", { val: false, ack: true });\n await this.setStateAsync(\"pairingIp\", { val: \"\", ack: true });\n\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(\"*.battery.charge_to_full\");\n await this.subscribeStatesAsync(\"*.remove\");\n\n const devices = await this.loadDevicesFromObjects();\n if (devices.length === 0) {\n this.log.info(`No devices configured \u2014 set 'startPairing' to true to add a device`);\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true });\n }\n\n for (const device of devices) {\n const key = this.stateManager.devicePrefix(device);\n await this.stateManager.cleanupMovedStates(device);\n await this.stateManager.createDeviceStates(device);\n const conn = createDeviceConnection(device, device.ip || \"\");\n this.connections.set(key, conn);\n\n if (conn.ip) {\n this.log.debug(`Using stored IP ${conn.ip} for ${device.productName}`);\n void this.initDevice(conn).catch((err: unknown) =>\n this.log.error(`initDevice failed for ${conn.config.productName}: ${errText(err)}`),\n );\n }\n }\n\n this.systemPollTimer = this.setInterval(() => {\n void this.pollAllSystemInfo();\n }, SYSTEM_POLL_MS);\n\n this.updateGlobalConnection();\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n }\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 // One-shot legacy migration: v0.1/0.2 stored devices in adapter `native.devices`;\n // v0.3.0 moved them to per-device objects. Any install that ran v0.3.0+ has already\n // migrated (native.devices cleared below), but removal is low-reward / non-zero-risk\n // for an install that has been dormant since v0.2 \u2014 keep until at least v1.0.0.\n // Defensive: native.devices could be a non-array if a previous version\n // wrote a different shape, or if the user edited it manually.\n const rawOldDevices = (this.config as Record<string, unknown>).devices;\n const oldDevices: DeviceConfig[] = Array.isArray(rawOldDevices) ? (rawOldDevices as DeviceConfig[]) : [];\n if (oldDevices.length > 0) {\n this.log.debug(`Migrating ${oldDevices.length} device(s) from adapter config to device objects`);\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. A corrupted encryptedToken\n // (e.g. after secret rotation, crypto-lib changes, manual DB edits) must\n // not take down the whole adapter \u2014 skip the broken device, keep the rest.\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 let token: string;\n try {\n token = this.decrypt(native.encryptedToken);\n } catch (err) {\n this.log.warn(\n `Cannot decrypt token for ${localId} \u2014 re-pair the device. ` +\n `(${errText(err)}). Other devices remain unaffected.`,\n );\n continue;\n }\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(\n prefix,\n {\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 { preserve: { common: [\"name\"] } },\n );\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(c => c.config.serial === discovered.serial);\n if (existing) {\n return;\n }\n\n // Skip duplicates\n if (this.discoveredDuringPairing.find(d => d.serial === discovered.serial)) {\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 // Set first, before any clearTimeout \u2014 in-flight async paths\n // (REST poll, getMeasurement, getSystem) check this after each await\n // and bail out before further setStateAsync on a tearing-down adapter.\n this.unloading = true;\n try {\n if (this.pairingTimer) {\n this.clearTimeout(this.pairingTimer);\n this.pairingTimer = undefined;\n }\n if (this.pairingPollTimer) {\n this.clearInterval(this.pairingPollTimer);\n this.pairingPollTimer = undefined;\n }\n if (this.systemPollTimer) {\n this.clearInterval(this.systemPollTimer);\n this.systemPollTimer = undefined;\n }\n if (this.ipRecoveryTimer) {\n this.clearTimeout(this.ipRecoveryTimer);\n this.ipRecoveryTimer = undefined;\n }\n\n this.discovery?.stop();\n\n for (const conn of this.connections.values()) {\n this.teardownConnection(conn);\n }\n this.connections.clear();\n\n void this.setState(\"info.connection\", { val: false, ack: true });\n } finally {\n callback();\n }\n }\n\n private async onStateChange(id: string, state: ioBroker.State | null | undefined): Promise<void> {\n try {\n if (!state || state.ack || this.unloading) {\n return;\n }\n\n if (id.endsWith(\".startPairing\")) {\n if (state.val) {\n await this.startPairing();\n }\n return;\n }\n\n if (id.endsWith(\".remove\")) {\n if (state.val) {\n await this.removeDevice(id);\n }\n return;\n }\n\n const conn = this.findConnectionForState(id);\n if (!conn || !conn.ip) {\n // Orphaned state (device removed but state written) or device without\n // IP yet \u2014 surface at debug so a user-side diagnosis is possible.\n this.log.debug(`stateChange ${id}: no matching connected device \u2014 ignored`);\n return;\n }\n\n const client = this.makeClient(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 // Reset the button so it is clickable again in Admin (fleet pattern,\n // same as startPairing above \u2014 a button must not stay `true, ack=false`).\n await this.setStateAsync(id, { val: false, ack: true });\n } else if (id.endsWith(\".system.identify\")) {\n await client.identify();\n await this.setStateAsync(id, { val: false, ack: true });\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 const mode = validateBatteryMode(String(state.val));\n if (!mode) {\n this.log.warn(\n `Invalid battery.mode value: '${String(state.val)}' \u2014 expected one of: zero, to_full, standby, predictive`,\n );\n return;\n }\n await client.setBatteries({ mode });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.permissions\")) {\n const result = parseBatteryPermissions(String(state.val));\n if (!result.ok) {\n this.log.warn(\n `Invalid JSON for battery.permissions: ${result.reason} \u2014 expected array, got: ${result.sample}`,\n );\n return;\n }\n await client.setBatteries({ permissions: result.perms });\n await this.setStateAsync(id, { val: state.val, ack: true });\n } else if (id.endsWith(\".battery.charge_to_full\")) {\n await client.setBatteries({ charge_to_full: !!state.val });\n await this.setStateAsync(id, { val: state.val, ack: true });\n }\n } catch (err) {\n this.log.warn(`Failed to set ${id}: ${errText(err)}`);\n }\n } catch (err: unknown) {\n this.log.error(`stateChange failed: ${errText(err)}`);\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 // Validate manual-IP up front \u2014 better to fail fast than wait 60s while\n // requestPairing keeps timing out against a malformed input.\n if (!isValidIpv4(this.pairingManualIp)) {\n this.log.warn(`Invalid pairing IP '${this.pairingManualIp}' \u2014 expected IPv4 (e.g. 192.168.1.42)`);\n this.isPairing = false;\n this.pairingManualIp = \"\";\n return;\n }\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 = this.makeDiscovery();\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(`Pairing mode automatically disabled after 60 seconds timeout`);\n }, PAIRING_TIMEOUT_MS);\n }\n\n /** Poll all discovered devices to attempt pairing */\n private async pollPairing(): Promise<void> {\n if (this.pairingPollBusy) {\n return;\n }\n this.pairingPollBusy = true;\n try {\n await this.pollPairingDevices();\n } finally {\n this.pairingPollBusy = false;\n }\n }\n\n /** One pairing-poll pass over all discovered devices. */\n private async pollPairingDevices(): Promise<void> {\n for (const device of this.discoveredDuringPairing) {\n try {\n const client = this.makeClient(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 = this.makeClient(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 // Re-pair of an existing device (e.g. after factory reset): close the\n // old connection's wsClient + timers before overwriting the map entry,\n // otherwise the old WS keeps running as a zombie until restart.\n const key = this.stateManager.devicePrefix(deviceConfig);\n const previous = this.connections.get(key);\n if (previous) {\n this.log.debug(`Re-pair: closing previous connection for ${deviceConfig.productName}`);\n this.teardownConnection(previous);\n }\n\n // Create connection and connect\n const conn = createDeviceConnection(deviceConfig, device.ip);\n this.connections.set(key, conn);\n void this.initDevice(conn).catch((err: unknown) =>\n this.log.error(`initDevice failed for ${conn.config.productName}: ${errText(err)}`),\n );\n\n // Remove from discovery list \u2014 but keep pairing window open so the\n // user can button-press additional devices in the same session.\n this.discoveredDuringPairing = this.discoveredDuringPairing.filter(d => d.serial !== info.serial);\n\n this.updateGlobalConnection();\n // Do NOT call stopPairing() here \u2014 pairingTimer (60 s) closes the\n // window naturally; meanwhile the user can pair more devices.\n continue;\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(`Pairing poll error for ${device.ip}: ${errText(err)}`);\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 // Internal recovery \u2014 debug only. The initial disconnect already produced\n // one warn via logDeviceError; repeating that hourly while a device stays\n // offline is just spam.\n this.log.debug(`Device unreachable \u2014 searching for new IP via mDNS`);\n\n this.discovery = this.makeDiscovery();\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 // Multiple mDNS broadcasts can arrive within one recovery window\n // (e.g. AP roam). Skip if a connect cycle is already in flight.\n if (conn.recovering) {\n return;\n }\n\n this.log.info(`${conn.config.productName}: found at new IP ${discovered.ip} (was ${conn.ip})`);\n\n // Update IP and persist \u2014 reset stability (new network conditions)\n conn.ip = discovered.ip;\n conn.config.ip = discovered.ip;\n conn.wsFailCount = 0;\n conn.recentDisconnects = 0;\n // Surface persist-failures (e.g. js-controller hiccup) instead of\n // swallowing them \u2014 the user otherwise sees \"new IP\" log but the\n // change is lost on next restart.\n this.saveDeviceToObject(conn.config).catch((err: unknown) =>\n this.log.debug(`Failed to persist new IP for ${conn.config.productName}: ${errText(err)}`),\n );\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 mDNS after timeout \u2014 WS reconnect continues with exponential\n // backoff. Don't log per-device warns here: the initial disconnect already\n // produced a `deviceUnreachable` warn via logDeviceError; spamming the\n // user hourly while the device stays offline adds zero information. If\n // someone needs to see retry cadence they can enable debug logging.\n this.ipRecoveryTimer = this.setTimeout(() => {\n this.ipRecoveryTimer = undefined;\n this.stopIpRecovery();\n\n for (const conn of this.connections.values()) {\n if (!conn.wsAuthenticated && conn.wsFailCount > 0) {\n this.log.debug(\n `${conn.config.productName}: device offline \u2014 will keep retrying every ${WS_RECONNECT_MAX_MS / 1000}s`,\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 if (this.unloading || conn.removed) {\n return;\n }\n try {\n const client = this.makeClient(conn.ip, conn.config.token);\n const info = await client.getDeviceInfo();\n if (this.unloading || conn.removed) {\n return;\n }\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 if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"init\", err);\n }\n\n if (this.unloading || conn.removed) {\n return;\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 // Mark as recovering so concurrent triggers (mDNS broadcast race,\n // overlapping reconnect timer) don't spawn a second wsClient.\n conn.recovering = true;\n\n // Close any existing wsClient before creating a new one. The normal\n // disconnect path nulls conn.wsClient, but IP-recovery jumps in directly\n // and would otherwise leak the old socket.\n if (conn.wsClient) {\n conn.wsClient.close();\n conn.wsClient = null;\n }\n\n // After repeated failures, try mDNS periodically to find a new IP\n if (shouldStartIpRecovery(conn.wsFailCount, WS_FAILURES_BEFORE_MDNS, MDNS_RETRY_EVERY)) {\n this.startIpRecovery();\n }\n\n // Thin callbacks delegating to instance methods (extracted for readability + unit-testability).\n const wsClient = this.makeWebSocket(\n conn.ip,\n conn.config.token,\n {\n onMeasurement: data => this.onWsMeasurement(conn, data),\n onSystem: data => this.onWsSystem(conn, data),\n onBattery: data => this.onWsBattery(conn, data),\n onConnected: () => this.onWsConnected(conn),\n onDisconnected: error => this.onWsDisconnected(conn, error),\n log: this.log,\n },\n {\n schedule: (cb, ms) => this.setTimeout(cb, ms),\n cancel: h => {\n this.clearTimeout(h as ioBroker.Timeout);\n },\n scheduleRepeating: (cb, ms) => this.setInterval(cb, ms),\n cancelRepeating: h => {\n this.clearInterval(h as ioBroker.Interval);\n },\n },\n );\n\n conn.wsClient = wsClient;\n wsClient.connect();\n }\n\n /**\n * Handle a measurement push.\n *\n * @param conn Device connection\n * @param data Measurement payload\n */\n private onWsMeasurement(conn: DeviceConnection, data: Measurement): void {\n // Skip updates for devices removed mid-flight (frame can race delObjectAsync) + teardown.\n if (conn.removed || this.unloading) {\n return;\n }\n // Defensive .catch \u2014 writes inside updateMeasurement may reject on transient Redis hiccups;\n // we want a debug-log, not an unhandled rejection.\n this.stateManager.updateMeasurement(conn.config, data).catch((err: unknown) => {\n this.log.debug(`updateMeasurement failed for ${conn.config.productName}: ${errText(err)}`);\n });\n }\n\n /**\n * Handle a real-time system push (cloud/led changes etc.).\n *\n * @param conn Device connection\n * @param data System payload\n */\n private onWsSystem(conn: DeviceConnection, data: SystemInfo): void {\n if (conn.removed || this.unloading) {\n return;\n }\n this.stateManager.updateSystem(conn.config, data).catch((err: unknown) => {\n this.log.debug(`updateSystem (ws) failed for ${conn.config.productName}: ${errText(err)}`);\n });\n }\n\n /**\n * Handle a real-time battery-group push (mode/permissions/target power).\n *\n * @param conn Device connection\n * @param data Battery-control payload\n */\n private onWsBattery(conn: DeviceConnection, data: BatteryControl): void {\n if (conn.removed || this.unloading) {\n return;\n }\n // Only surface battery states when batteries are actually connected.\n if (!data.battery_count || data.battery_count <= 0) {\n return;\n }\n this.stateManager.updateBattery(conn.config, data).catch((err: unknown) => {\n this.log.debug(`updateBattery (ws) failed for ${conn.config.productName}: ${errText(err)}`);\n });\n }\n\n /**\n * WebSocket authenticated \u2014 mark connected, stop REST fallback, log recovery (cooldowned).\n *\n * @param conn Device connection\n */\n private onWsConnected(conn: DeviceConnection): void {\n conn.wsAuthenticated = true;\n conn.wsFailCount = 0;\n conn.authFailCount = 0;\n conn.lastConnectedAt = Date.now();\n conn.recovering = 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(c => c.wsAuthenticated);\n if (allConnected) {\n this.stopIpRecovery();\n }\n }\n\n // Log restoration if we had errors before. Per-device cooldown so chronic bouncing\n // doesn't emit one info per cycle \u2014 repeats go to debug.\n if (conn.lastErrorCode) {\n const now = Date.now();\n const lastInfo = this.lastInfoAt.get(conn.config.serial) ?? 0;\n const msg = this.isUnstable(conn)\n ? `${conn.config.productName}: connection restored (unstable mode)`\n : `${conn.config.productName}: connection restored`;\n if (shouldEmitAfterCooldown(lastInfo, now, INFO_COOLDOWN_MS)) {\n this.lastInfoAt.set(conn.config.serial, now);\n this.log.info(msg);\n } else {\n this.log.debug(`${msg} (cooldown)`);\n }\n conn.lastErrorCode = \"\";\n }\n\n this.log.debug(`WebSocket connected to ${conn.config.productName} (${conn.ip})`);\n }\n\n /**\n * WebSocket disconnected \u2014 track stability, start REST fallback, schedule backed-off reconnect\n * (unless an auth failure stops the loop).\n *\n * @param conn Device connection\n * @param error Disconnect error, if any\n */\n private onWsDisconnected(conn: DeviceConnection, error?: Error): void {\n // Auth failures are not a connectivity-stability signal \u2014 they mean the token is bad,\n // not the WiFi. Counting them as short connections would flip the device into unstable mode.\n const isAuthError = error instanceof HomeWizardApiError && error.errorCode === \"user:unauthorized\";\n\n // Track connection stability \u2014 pure decision in main-helpers, side-effects here.\n if (conn.lastConnectedAt > 0 && !isAuthError) {\n const duration = Date.now() - conn.lastConnectedAt;\n const transition = decideUnstableTransition(\n conn.recentDisconnects,\n duration,\n STABLE_THRESHOLD_MS,\n UNSTABLE_DISCONNECT_THRESHOLD,\n );\n if (duration < STABLE_THRESHOLD_MS) {\n conn.recentDisconnects++;\n } else {\n conn.recentDisconnects = 0;\n }\n // Hysterese-transitions are internal reconnect-strategy adjustments \u2192 debug, not info.\n if (transition === \"becameUnstable\") {\n this.log.debug(`${conn.config.productName}: unstable connection detected \u2014 using faster reconnect`);\n } else if (transition === \"stabilized\") {\n this.log.debug(`${conn.config.productName}: connection stabilized \u2014 using normal reconnect`);\n }\n }\n\n conn.wsAuthenticated = false;\n conn.wsClient = null;\n conn.recovering = false;\n void this.stateManager.setDeviceConnected(conn.config, false);\n this.updateGlobalConnection();\n\n if (error) {\n this.logDeviceError(conn, \"ws\", error);\n }\n\n // Auth failure \u2192 stop the reconnect path.\n if (!this.handleAuthFailure(conn, error, /* cleanupTimers */ false)) {\n return;\n }\n\n // Start REST fallback\n this.startRestFallback(conn);\n\n // Schedule reconnect with exponential backoff (faster for unstable devices).\n conn.wsFailCount++;\n const maxDelay = this.isUnstable(conn) ? WS_RECONNECT_MAX_UNSTABLE_MS : WS_RECONNECT_MAX_MS;\n const delay = computeReconnectDelay(conn.wsFailCount, WS_RECONNECT_BASE_MS, maxDelay);\n const key = this.stateManager.devicePrefix(conn.config);\n this.log.debug(`${key}: WS reconnect in ${delay / 1000}s (attempt ${conn.wsFailCount})`);\n\n conn.reconnectTimer = this.setTimeout(() => {\n conn.reconnectTimer = undefined;\n this.connectWebSocket(conn);\n }, delay);\n }\n\n /**\n * Start REST polling as fallback when WebSocket is down.\n * For stable devices: stops on network errors (WS reconnect handles recovery).\n * For unstable devices: slows down instead of stopping to minimize data gaps.\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 unstable = this.isUnstable(conn);\n const interval = pickRestPollInterval(unstable, REST_POLL_MS, REST_POLL_UNSTABLE_MS);\n const client = this.makeClient(conn.ip, conn.config.token);\n\n conn.pollTimer = this.setInterval(async () => {\n // Bail out if device was removed or adapter is shutting down \u2014 the\n // setStateAsync chain inside updateMeasurement would otherwise either\n // recreate deleted objects or hit a torn-down adapter.\n if (conn.removed || this.unloading) {\n return;\n }\n try {\n const data = await client.getMeasurement();\n if (conn.removed || this.unloading) {\n return;\n }\n await this.stateManager.updateMeasurement(conn.config, data);\n } catch (err) {\n if (this.unloading) {\n return;\n }\n this.logDeviceError(conn, \"rest\", err);\n\n // Auth failures: stop everything \u2014 token is bad, re-pair required.\n if (err instanceof HomeWizardApiError && err.errorCode === \"user:unauthorized\") {\n this.handleAuthFailure(conn, err, /* cleanupTimers */ true);\n return;\n }\n\n // Stop REST polling on network errors for stable devices.\n // Unstable devices keep polling (slower) to minimize data gaps.\n if (!unstable && classifyError(err) === \"NETWORK\" && conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n }\n }, interval);\n }\n\n /** Poll system info for all connected devices in parallel */\n private async pollAllSystemInfo(): Promise<void> {\n if (this.unloading) {\n return;\n }\n const tasks = Array.from(this.connections.values())\n .filter(c => c.ip && c.wsAuthenticated && !c.removed)\n .map(c => this.pollSystemInfo(c));\n await Promise.all(tasks);\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 || conn.removed || this.unloading) {\n return;\n }\n\n try {\n const client = this.makeClient(conn.ip, conn.config.token);\n const system = await client.getSystem();\n if (conn.removed || this.unloading) {\n return;\n }\n await this.stateManager.updateSystem(conn.config, system);\n\n // Sync productName drift: if the user renamed the device in the\n // HomeWizard app (or a firmware update changed the product_name), pick\n // up the new value once per system-poll instead of staying stale until\n // re-pair. Cheap \u2014 only writes on actual change.\n try {\n const info = await client.getDeviceInfo();\n if (!conn.removed && !this.unloading && info.product_name && info.product_name !== conn.config.productName) {\n this.log.info(`${conn.config.productName}: name changed to '${info.product_name}' \u2014 updating object`);\n conn.config.productName = info.product_name;\n await this.saveDeviceToObject(conn.config);\n }\n } catch {\n // device-info is best-effort here; the system-poll log already\n // surfaces real connectivity issues.\n }\n\n // Also poll battery if device supports it. 404 = no battery \u2014 silent.\n // Other errors (500, timeout, malformed body) used to be swallowed\n // entirely; now they surface at debug so post-mortem diagnosis is\n // possible without losing any normal-flow logging.\n if (conn.removed || this.unloading) {\n return;\n }\n try {\n const battery = await client.getBatteries();\n if (conn.removed || this.unloading) {\n return;\n }\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 (err) {\n if (err instanceof HomeWizardApiError && err.statusCode === 404) {\n return; // device doesn't support batteries \u2014 expected\n }\n this.log.debug(`${conn.config.productName} batteries: ${errText(err)}`);\n }\n } catch (err) {\n if (this.unloading) {\n return;\n }\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(c => c.wsAuthenticated);\n // setStateChanged: flips rarely (connect/disconnect), called on every WS event \u2014 skip no-op writes.\n void this.setStateChangedAsync(\"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(`Removing device ${conn.config.productName} (${conn.config.serial})`);\n\n // Mark as removed FIRST \u2014 async tasks (in-flight WS frames, REST polls,\n // outstanding pollSystemInfo) check this flag after each await and bail\n // out before recreating just-deleted objects via setStateAsync.\n conn.removed = true;\n\n // Best-effort token revoke on the device (DELETE /api/user) so the local/iobroker user\n // doesn't linger across pair/unpair cycles. Fire-and-forget \u2014 never block removal on a\n // (possibly offline) device's 10s timeout.\n if (conn.ip && conn.config.token) {\n void this.makeClient(conn.ip, conn.config.token)\n .deleteUser()\n .catch((err: unknown) => this.log.debug(`Token revoke failed for ${conn.config.productName}: ${errText(err)}`));\n }\n\n // Disconnect\n this.teardownConnection(conn);\n this.connections.delete(key);\n // Drop the per-device cooldown stamps \u2014 otherwise a re-pair of the same\n // serial within the cooldown window inherits the old device's stamp and\n // its first warn/info is silently suppressed (and the maps grow forever\n // across pair/remove cycles).\n this.lastWarnAt.delete(conn.config.serial);\n this.lastInfoAt.delete(conn.config.serial);\n\n // Delete device object and all states (no adapter restart!)\n await this.stateManager.removeDevice(conn.config);\n\n this.updateGlobalConnection();\n }\n\n /**\n * Find connection for a state ID. Delegates to the pure helper so the\n * lookup math is unit-tested separately (`lib/main-helpers.test.ts`).\n *\n * @param stateId Full state ID\n */\n private findConnectionForState(stateId: string): DeviceConnection | undefined {\n return resolveConnectionForState(stateId, this.namespace, this.connections);\n }\n\n /**\n * Whether a device has unstable connectivity (frequent short-lived connections).\n * Unstable devices get faster reconnect and persistent REST fallback.\n *\n * @param conn Device connection\n */\n private isUnstable(conn: DeviceConnection): boolean {\n return conn.recentDisconnects >= UNSTABLE_DISCONNECT_THRESHOLD;\n }\n\n /**\n * Handle a possible auth failure on a device connection. Counts failures and,\n * once `MAX_AUTH_FAILURES` is reached, warns the user and (optionally) tears\n * down active timers and the WebSocket \u2014 stops bombarding the device with a\n * known-bad token.\n *\n * @param conn Device connection.\n * @param error The error from the failing call (any error type accepted).\n * @param cleanupTimers If `true`, clears poll/reconnect timers and closes the WS\n * on threshold reach. Used by REST-fallback paths where\n * the WS would otherwise keep retrying indefinitely. The\n * WS-disconnect path passes `false` because the caller\n * decides the next step itself.\n * @returns `true` if the caller should continue normal flow (no auth-stop),\n * `false` if the auth-stop fired and the caller should bail out.\n */\n private handleAuthFailure(conn: DeviceConnection, error: unknown, cleanupTimers: boolean): boolean {\n if (!(error instanceof HomeWizardApiError) || error.errorCode !== \"user:unauthorized\") {\n return true;\n }\n conn.authFailCount++;\n if (conn.authFailCount < MAX_AUTH_FAILURES) {\n return true;\n }\n this.log.warn(`${conn.config.productName}: token invalid \u2014 re-pair device to fix`);\n if (cleanupTimers) {\n if (conn.pollTimer) {\n this.clearInterval(conn.pollTimer);\n conn.pollTimer = undefined;\n }\n if (conn.reconnectTimer) {\n this.clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = undefined;\n }\n conn.wsClient?.close();\n }\n return false;\n }\n\n /**\n * Log device error with deduplication.\n *\n * Two-stage dedup:\n * 1. `lastErrorCode` per connection \u2014 repeats of the same error category go\n * to debug. Resets on recovery (`onConnected` clears it) so a new category\n * after recovery surfaces as warn again. Designtechnisch correct for\n * \u201Enew failure mode\" but blind to chronic bouncing.\n * 2. {@link lastWarnAt} per device serial \u2014 survives recovery. Even if the\n * `lastErrorCode`-Dedup says \u201Efirst occurrence\", the cooldown stamp keeps\n * the warn-emit suppressed if we've warned for this device within\n * {@link WARN_COOLDOWN_MS}. Chronic bouncing produces at most 1\u00D7 warn per\n * hour per device.\n *\n * Cooldown key is the device serial \u2014 category-spanning. A flapping P1 that\n * cycles TIMEOUT\u2192NETWORK\u2192TIMEOUT is one phenomenon, one warn-budget.\n *\n * @param conn Device connection\n * @param context Error context (for debug messages only)\n * @param err Error object\n */\n private logDeviceError(conn: DeviceConnection, context: string, err: unknown): void {\n const errorCode = classifyError(err);\n const isRepeat = errorCode === conn.lastErrorCode;\n conn.lastErrorCode = errorCode;\n\n if (isRepeat) {\n this.log.debug(`${conn.config.productName} ${context}: ${errText(err)}`);\n return;\n }\n\n // New category \u2014 apply per-device cooldown so chronic bouncing doesn't\n // emit warn at every cycle just because each cycle's first failure is\n // a fresh `lastErrorCode`.\n const now = Date.now();\n const lastWarn = this.lastWarnAt.get(conn.config.serial) ?? 0;\n if (!shouldEmitAfterCooldown(lastWarn, now, WARN_COOLDOWN_MS)) {\n this.log.debug(`${conn.config.productName} ${context} (cooldown): ${errText(err)}`);\n return;\n }\n\n this.lastWarnAt.set(conn.config.serial, now);\n if (errorCode === \"NETWORK\") {\n this.log.warn(`${conn.config.productName}: device unreachable \u2014 will keep retrying`);\n } else {\n this.log.warn(`${conn.config.productName} ${context}: ${errText(err)}`);\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new HomeWizard(options);\n} else {\n (() => new HomeWizard())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["resolveConnectionForState"]
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "homewizard",
4
- "version": "0.12.0",
4
+ "version": "0.12.2",
5
5
  "news": {
6
+ "0.12.2": {
7
+ "en": "Reboot and identify buttons reset themselves after the action and stay clickable. Re-paired devices show their first connection warning again right away.",
8
+ "de": "Neustart- und Identifizieren-Buttons setzen sich nach der Aktion zurück und bleiben klickbar. Neu gekoppelte Geräte zeigen ihre erste Verbindungswarnung wieder sofort.",
9
+ "ru": "Кнопки перезагрузки и идентификации сбрасываются после действия и остаются кликабельными. Повторно сопряжённые устройства снова сразу показывают первое предупреждение о соединении.",
10
+ "pt": "Os botões de reiniciar e identificar são redefinidos após a ação e continuam clicáveis. Dispositivos emparelhados novamente mostram o primeiro aviso de conexão de imediato.",
11
+ "nl": "Herstart- en identificatieknoppen resetten zichzelf na de actie en blijven klikbaar. Opnieuw gekoppelde apparaten tonen hun eerste verbindingswaarschuwing weer direct.",
12
+ "fr": "Les boutons redémarrer et identifier se réinitialisent après l'action et restent cliquables. Les appareils ré-appairés affichent de nouveau leur premier avertissement de connexion.",
13
+ "it": "I pulsanti di riavvio e identificazione si ripristinano dopo l'azione e restano cliccabili. I dispositivi riaccoppiati mostrano di nuovo subito il primo avviso di connessione.",
14
+ "es": "Los botones de reinicio e identificación se restablecen tras la acción y siguen siendo clicables. Los dispositivos emparejados de nuevo muestran su primera advertencia de conexión de inmediato.",
15
+ "pl": "Przyciski restartu i identyfikacji resetują się po akcji i pozostają klikalne. Ponownie sparowane urządzenia od razu pokazują pierwsze ostrzeżenie o połączeniu.",
16
+ "uk": "Кнопки перезавантаження та ідентифікації скидаються після дії та залишаються клікабельними. Повторно з'єднані пристрої знову одразу показують перше попередження про з'єднання.",
17
+ "zh-cn": "重启和识别按钮在操作后会自行复位并保持可点击。重新配对的设备会立即再次显示首次连接警告。"
18
+ },
19
+ "0.12.1": {
20
+ "en": "Internal refactoring. No user-facing changes.",
21
+ "de": "Interne Überarbeitung. Keine für Nutzer sichtbaren Änderungen.",
22
+ "ru": "Внутренняя переработка. Никаких изменений для пользователей.",
23
+ "pt": "Refatoração interna. Sem alterações visíveis para o utilizador.",
24
+ "nl": "Interne herstructurering. Geen zichtbare wijzigingen voor gebruikers.",
25
+ "fr": "Refactorisation interne. Aucun changement visible pour l'utilisateur.",
26
+ "it": "Refactoring interno. Nessuna modifica visibile per l'utente.",
27
+ "es": "Refactorización interna. Sin cambios visibles para el usuario.",
28
+ "pl": "Wewnętrzna refaktoryzacja. Brak zmian widocznych dla użytkownika.",
29
+ "uk": "Внутрішня переробка. Жодних змін для користувачів.",
30
+ "zh-cn": "内部重构。对用户无可见变化。"
31
+ },
6
32
  "0.12.0": {
7
33
  "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
34
  "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.",
@@ -67,32 +93,6 @@
67
93
  "pl": "Dziennik zmian przepisany w stylu zorientowanym na użytkownika we wszystkich wersjach.",
68
94
  "uk": "Журнал змін переписано у стилі орієнтованому на користувача для всіх версій.",
69
95
  "zh-cn": "所有版本的更新日志已改写为面向用户的风格。"
70
- },
71
- "0.9.1": {
72
- "en": "Internal cleanup. No user-facing changes.",
73
- "de": "Interne Bereinigung. Keine sichtbaren Änderungen.",
74
- "ru": "Внутренняя очистка. Нет видимых изменений для пользователей.",
75
- "pt": "Limpeza interna. Sem alterações visíveis para o utilizador.",
76
- "nl": "Interne opruiming. Geen zichtbare wijzigingen voor gebruikers.",
77
- "fr": "Nettoyage interne. Aucun changement visible pour l utilisateur.",
78
- "it": "Pulizia interna. Nessuna modifica visibile per l utente.",
79
- "es": "Limpieza interna. Sin cambios visibles para el usuario.",
80
- "pl": "Wewnętrzne porządki. Brak widocznych zmian dla użytkownika.",
81
- "uk": "Внутрішнє прибирання. Без видимих змін для користувача.",
82
- "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.2",
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",