iobroker.homewizard 0.6.6 → 0.6.7

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
@@ -30,8 +30,8 @@ Real-time energy monitoring from [HomeWizard](https://www.homewizard.com) Energy
30
30
  ## Requirements
31
31
 
32
32
  - **Node.js >= 20**
33
- - **ioBroker js-controller >= 6.0.11**
34
- - **ioBroker Admin >= 7.6.20**
33
+ - **ioBroker js-controller >= 7.0.7**
34
+ - **ioBroker Admin >= 7.7.22**
35
35
  - **HomeWizard device with API v2 support** (firmware 4.x+ with local API enabled)
36
36
 
37
37
  ---
@@ -164,37 +164,24 @@ homewizard.0.
164
164
  ---
165
165
 
166
166
  ## Changelog
167
+ ### 0.6.7 (2026-05-01)
168
+ - Internal cleanup. No user-facing changes.
169
+ - Documentation: rewrote release notes for v0.6.0–v0.6.6 in user-friendly style across all languages.
170
+
167
171
  ### 0.6.6 (2026-04-28)
168
- - Audit cleanup against the upstream `ioBroker.example/TypeScript` full standard:
169
- - Test setup migrated: tests now live next to source as `src/**/*.test.ts` and run directly via `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`
170
- - `@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: ">=20"`
171
- - Dependabot now ignores major bumps for `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`
172
- - `nyc` config + `coverage` script added
173
- - `prettier.config.mjs` made explicit with project-style overrides (Spaces 2-wide, double quotes)
174
- - Orphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)
172
+ - Internal cleanup. No user-facing changes.
175
173
 
176
174
  ### 0.6.5 (2026-04-26)
177
- - Process-level `unhandledRejection` / `uncaughtException` handlers added as last-line-of-defence against fire-and-forget rejections.
178
- - Stop shipping the `manual-review` release-script plugin adapter-only consequence.
179
- - Audit-driven boilerplate sync with the other krobi adapters (`.vscode` json5 schemas, `tsconfig.test` looser test rules).
180
- - Min js-controller correction: was `>=7.0.0`, restored to repochecker-recommended `>=6.0.11` (Source: `ioBroker.repochecker/lib/M1000_IOPackageJson.js`).
181
- - `@types/iobroker` bumped to `^7.1.1`.
175
+ - Crash defense: process-level error handlers.
176
+ - Min `js-controller` restored to `>=6.0.11` (was incorrectly `>=7.0.0`).
182
177
 
183
178
  ### 0.6.4 (2026-04-23)
184
- - Separate test-build output (`build-test/`) from production `build/`, so `npm test` no longer risks leaving duplicated `build/src` + `build/test` trees in the published package.
185
- - Wrap async `onReady` and `onStateChange` handlers with `.catch()` to prevent unhandled promise rejections from SIGKILLing the adapter.
186
- - Declare `pairingIp` as an instance object (11-language name) instead of creating it dynamically in `onReady`.
179
+ - Internal hardening. No user-facing changes.
187
180
 
188
181
  ### 0.6.3 (2026-04-18)
189
- - Harden WebSocket and REST input handling against unexpected API responses
190
- - Stop endless reconnect when the device token is invalid (fires once after 3 failed auth attempts)
191
- - Avoid creating an empty `external/` channel when a device reports no external meters
192
-
193
- ### 0.6.2 (2026-04-13)
194
- - Fix hanging promise when response stream errors mid-transfer (`res.on("error")`)
195
- - Fix onUnload: wrap in try/finally so callback always fires (prevents adapter hang on shutdown)
196
- - Optimize state creation hot path: use `setObjectNotExistsAsync` instead of `extendObjectAsync` (~50 fewer object writes per second per device)
197
- - Remove unnecessary `removeDeviceFromObject` wrapper (DRY)
182
+ - WebSocket and REST input hardening. Stops endless reconnect when the device token is invalid.
183
+
184
+ Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
198
185
 
199
186
  ## Support
200
187
 
@@ -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 { 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 /** State display name */\n name: string;\n /** State value type */\n type: ioBroker.CommonType;\n /** ioBroker role */\n role: string;\n /** Unit string */\n unit?: string;\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 {\n key: \"power_w\",\n id: \"power_w\",\n name: \"Total power\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"power_l1_w\",\n id: \"power_l1_w\",\n name: \"Power L1\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"power_l2_w\",\n id: \"power_l2_w\",\n name: \"Power L2\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"power_l3_w\",\n id: \"power_l3_w\",\n name: \"Power L3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n // Voltage\n {\n key: \"voltage_v\",\n id: \"voltage_v\",\n name: \"Voltage\",\n type: \"number\",\n role: \"value.voltage\",\n unit: \"V\",\n },\n {\n key: \"voltage_l1_v\",\n id: \"voltage_l1_v\",\n name: \"Voltage L1\",\n type: \"number\",\n role: \"value.voltage\",\n unit: \"V\",\n },\n {\n key: \"voltage_l2_v\",\n id: \"voltage_l2_v\",\n name: \"Voltage L2\",\n type: \"number\",\n role: \"value.voltage\",\n unit: \"V\",\n },\n {\n key: \"voltage_l3_v\",\n id: \"voltage_l3_v\",\n name: \"Voltage L3\",\n type: \"number\",\n role: \"value.voltage\",\n unit: \"V\",\n },\n // Current\n {\n key: \"current_a\",\n id: \"current_a\",\n name: \"Current\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"current_l1_a\",\n id: \"current_l1_a\",\n name: \"Current L1\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"current_l2_a\",\n id: \"current_l2_a\",\n name: \"Current L2\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"current_l3_a\",\n id: \"current_l3_a\",\n name: \"Current L3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n // Frequency\n {\n key: \"frequency_hz\",\n id: \"frequency_hz\",\n name: \"Frequency\",\n type: \"number\",\n role: \"value\",\n unit: \"Hz\",\n },\n // Energy import\n {\n key: \"energy_import_kwh\",\n id: \"energy_import_kwh\",\n name: \"Energy import total\",\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 name: \"Energy import T1\",\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 name: \"Energy import T2\",\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 name: \"Energy import T3\",\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 name: \"Energy import T4\",\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 name: \"Energy export total\",\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 name: \"Energy export T1\",\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 name: \"Energy export T2\",\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 name: \"Energy export T3\",\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 name: \"Energy export T4\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n // Tariff\n {\n key: \"tariff\",\n id: \"tariff\",\n name: \"Active tariff\",\n type: \"number\",\n role: \"value\",\n },\n // Power quality\n {\n key: \"voltage_sag_l1_count\",\n id: \"quality.voltage_sag_l1_count\",\n name: \"Voltage sag L1\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_sag_l2_count\",\n id: \"quality.voltage_sag_l2_count\",\n name: \"Voltage sag L2\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_sag_l3_count\",\n id: \"quality.voltage_sag_l3_count\",\n name: \"Voltage sag L3\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l1_count\",\n id: \"quality.voltage_swell_l1_count\",\n name: \"Voltage swell L1\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l2_count\",\n id: \"quality.voltage_swell_l2_count\",\n name: \"Voltage swell L2\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l3_count\",\n id: \"quality.voltage_swell_l3_count\",\n name: \"Voltage swell L3\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"any_power_fail_count\",\n id: \"quality.power_fail_count\",\n name: \"Power fail count\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"long_power_fail_count\",\n id: \"quality.long_power_fail_count\",\n name: \"Long power fail count\",\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 name: \"Average power 15min\",\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 name: \"Monthly power peak\",\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 name: \"Monthly power peak time\",\n type: \"string\",\n role: \"date\",\n },\n // kWh meter specifics\n {\n key: \"apparent_current_a\",\n id: \"apparent_current_a\",\n name: \"Apparent current\",\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 name: \"Apparent current L1\",\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 name: \"Apparent current L2\",\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 name: \"Apparent current L3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_a\",\n id: \"reactive_current_a\",\n name: \"Reactive current\",\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 name: \"Reactive current L1\",\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 name: \"Reactive current L2\",\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 name: \"Reactive current L3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_power_va\",\n id: \"apparent_power_va\",\n name: \"Apparent power\",\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 name: \"Apparent power L1\",\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 name: \"Apparent power L2\",\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 name: \"Apparent power L3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"reactive_power_var\",\n id: \"reactive_power_var\",\n name: \"Reactive power\",\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 name: \"Reactive power L1\",\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 name: \"Reactive power L2\",\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 name: \"Reactive power L3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"power_factor\",\n id: \"power_factor\",\n name: \"Power factor\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l1\",\n id: \"power_factor_l1\",\n name: \"Power factor L1\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l2\",\n id: \"power_factor_l2\",\n name: \"Power factor L2\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l3\",\n id: \"power_factor_l3\",\n name: \"Power factor L3\",\n type: \"number\",\n role: \"value\",\n },\n // Battery specifics\n {\n key: \"state_of_charge_pct\",\n id: \"state_of_charge_pct\",\n name: \"State of charge\",\n type: \"number\",\n role: \"value.battery\",\n unit: \"%\",\n },\n {\n key: \"cycles\",\n id: \"cycles\",\n name: \"Charge cycles\",\n type: \"number\",\n role: \"value\",\n },\n // Metadata\n {\n key: \"meter_model\",\n id: \"meter_model\",\n name: \"Meter model\",\n type: \"string\",\n role: \"text\",\n },\n {\n key: \"timestamp\",\n id: \"timestamp\",\n name: \"Measurement timestamp\",\n type: \"string\",\n role: \"date\",\n },\n];\n\n/** Manages ioBroker state creation and updates for HomeWizard devices */\nexport class StateManager {\n private readonly adapter: utils.AdapterInstance;\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 await this.adapter.extendObjectAsync(prefix, {\n type: \"device\",\n common: {\n name: config.productName || config.productType,\n statusStates: {\n onlineId: `${this.adapter.namespace}.${prefix}.info.connected`,\n },\n } as ioBroker.DeviceCommon,\n native: {},\n });\n\n await this.adapter.extendObjectAsync(`${prefix}.info`, {\n type: \"channel\",\n common: { name: \"Device Information\" },\n native: {},\n });\n\n await this.createState(`${prefix}.info.productName`, \"Product name\", \"string\", \"text\", false);\n await this.createState(`${prefix}.info.productType`, \"Product type\", \"string\", \"text\", false);\n await this.createState(`${prefix}.info.firmware`, \"Firmware version\", \"string\", \"text\", false);\n await this.createState(`${prefix}.info.connected`, \"Device connected\", \"boolean\", \"indicator.reachable\", false);\n await this.createState(`${prefix}.info.wifi_rssi_db`, \"WiFi signal strength\", \"number\", \"value\", false, \"dB\");\n await this.createState(`${prefix}.info.uptime_s`, \"Uptime\", \"number\", \"value\", false, \"s\");\n\n // Remove device button\n await this.createButton(`${prefix}.remove`, \"Remove device\");\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\n await this.adapter.setObjectNotExistsAsync(mPrefix, {\n type: \"channel\",\n common: { name: \"Measurement\" },\n native: {},\n });\n\n // Main measurement values \u2014 coerce per declared type\n const record = data;\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 await this.ensureAndSet(`${mPrefix}.${def.id}`, def.name, def.type, def.role, coerced, def.unit);\n }\n }\n\n // External meters (P1 gas/water/heat)\n const external = record.external;\n if (Array.isArray(external) && external.length > 0) {\n let extChannelEnsured = false;\n\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 if (!extChannelEnsured) {\n await this.adapter.setObjectNotExistsAsync(`${mPrefix}.external`, {\n type: \"channel\",\n common: { name: \"External Meters\" },\n native: {},\n });\n extChannelEnsured = true;\n }\n\n const extId = `${mPrefix}.external.${sanitize(type)}_${sanitize(uniqueId)}`;\n await this.adapter.setObjectNotExistsAsync(extId, {\n type: \"channel\",\n common: { name: type },\n native: {},\n });\n if (value !== null) {\n await this.ensureAndSet(`${extId}.value`, \"Value\", \"number\", \"value\", value, unit ?? undefined);\n }\n if (unit) {\n await this.ensureAndSet(`${extId}.unit`, \"Unit\", \"string\", \"text\", unit);\n }\n if (timestamp) {\n await this.ensureAndSet(`${extId}.timestamp`, \"Timestamp\", \"string\", \"date\", timestamp);\n }\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/uptime in info channel\n const rssi = coerceFiniteNumber(record.wifi_rssi_db);\n if (rssi !== null) {\n await this.ensureAndSet(`${prefix}.info.wifi_rssi_db`, \"WiFi signal strength\", \"number\", \"value\", rssi, \"dB\");\n }\n const uptime = coerceFiniteNumber(record.uptime_s);\n if (uptime !== null) {\n await this.ensureAndSet(`${prefix}.info.uptime_s`, \"Uptime\", \"number\", \"value\", uptime, \"s\");\n }\n\n // System control channel\n await this.adapter.setObjectNotExistsAsync(`${prefix}.system`, {\n type: \"channel\",\n common: { name: \"System Settings\" },\n native: {},\n });\n\n const cloudEnabled = coerceBoolean(record.cloud_enabled);\n if (cloudEnabled !== null) {\n await this.ensureAndSet(\n `${prefix}.system.cloud_enabled`,\n \"Cloud enabled\",\n \"boolean\",\n \"switch\",\n cloudEnabled,\n undefined,\n true,\n );\n }\n const ledPct = coerceFiniteNumber(record.status_led_brightness_pct);\n if (ledPct !== null) {\n await this.ensureAndSet(\n `${prefix}.system.status_led_brightness_pct`,\n \"LED brightness\",\n \"number\",\n \"level\",\n ledPct,\n \"%\",\n true,\n );\n }\n\n const apiV1 = coerceBoolean(record.api_v1_enabled);\n if (apiV1 !== null) {\n await this.ensureAndSet(\n `${prefix}.system.api_v1_enabled`,\n \"API v1 enabled\",\n \"boolean\",\n \"switch\",\n apiV1,\n undefined,\n true,\n );\n }\n\n // Action buttons\n await this.createButton(`${prefix}.system.reboot`, \"Reboot device\");\n await this.createButton(`${prefix}.system.identify`, \"Identify (blink LED)\");\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.adapter.setObjectNotExistsAsync(`${prefix}.battery`, {\n type: \"channel\",\n common: { name: \"Battery Control\" },\n native: {},\n });\n\n const mode = coerceString(record.mode);\n if (mode) {\n await this.ensureAndSet(`${prefix}.battery.mode`, \"Battery mode\", \"string\", \"text\", mode, undefined, true);\n }\n if (Array.isArray(record.permissions)) {\n await this.ensureAndSet(\n `${prefix}.battery.permissions`,\n \"Battery permissions\",\n \"string\",\n \"json\",\n JSON.stringify(record.permissions),\n undefined,\n true,\n );\n }\n\n const numberFields: Array<{\n key: string;\n id: string;\n name: string;\n role: string;\n unit?: string;\n }> = [\n {\n key: \"battery_count\",\n id: \"battery_count\",\n name: \"Connected batteries\",\n role: \"value\",\n },\n {\n key: \"power_w\",\n id: \"power_w\",\n name: \"Battery power\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"target_power_w\",\n id: \"target_power_w\",\n name: \"Target power\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"max_consumption_w\",\n id: \"max_consumption_w\",\n name: \"Max consumption\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"max_production_w\",\n id: \"max_production_w\",\n name: \"Max production\",\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(`${prefix}.battery.${field.id}`, field.name, \"number\", field.role, coerced, field.unit);\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.setStateAsync(`${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 await this.adapter.delObjectAsync(prefix, { recursive: true });\n }\n\n /**\n * Remove measurement states from old locations (pre-v0.4.0: device root instead of measurement/ channel)\n *\n * @param config Device configuration\n */\n async cleanupMovedStates(config: DeviceConfig): Promise<void> {\n const prefix = this.devicePrefix(config);\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\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 }\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 * Create a state if it doesn't exist\n *\n * @param id State ID\n * @param name State name\n * @param type Value type\n * @param role ioBroker role\n * @param write Whether state is writable\n * @param unit Optional unit\n */\n private async createState(\n id: string,\n name: string,\n type: ioBroker.CommonType,\n role: string,\n write: boolean,\n unit?: string,\n ): Promise<void> {\n const common: Partial<ioBroker.StateCommon> = {\n name,\n type,\n role,\n read: true,\n write,\n };\n if (unit) {\n common.unit = unit;\n }\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {},\n });\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 State name\n */\n private async createButton(id: string, name: string): Promise<void> {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: {\n name,\n type: \"boolean\",\n role: \"button\",\n read: false,\n write: true,\n } as ioBroker.StateCommon,\n native: {},\n });\n await this.adapter.setStateAsync(id, { val: false, ack: true });\n }\n\n /**\n * Ensure state exists and set value\n *\n * @param id State ID\n * @param name State name\n * @param type Value type\n * @param role ioBroker role\n * @param value State value\n * @param unit Optional unit\n * @param write Whether state is writable\n */\n private async ensureAndSet(\n id: string,\n name: string,\n type: ioBroker.CommonType,\n role: string,\n value: ioBroker.StateValue,\n unit?: string,\n write?: boolean,\n ): Promise<void> {\n await this.createState(id, name, type, role, write ?? false, unit);\n await this.adapter.setStateAsync(id, { val: value, ack: true });\n }\n}\n"],
4
+ "sourcesContent": ["import type * as utils from \"@iobroker/adapter-core\";\nimport { coerceBoolean, coerceFiniteNumber, coerceString, isPlainObject } from \"./coerce\";\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 /** State display name */\n name: string;\n /** State value type */\n type: ioBroker.CommonType;\n /** ioBroker role */\n role: string;\n /** Unit string */\n unit?: string;\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 {\n key: \"power_w\",\n id: \"power_w\",\n name: \"Total power\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"power_l1_w\",\n id: \"power_l1_w\",\n name: \"Power L1\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"power_l2_w\",\n id: \"power_l2_w\",\n name: \"Power L2\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"power_l3_w\",\n id: \"power_l3_w\",\n name: \"Power L3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"W\",\n },\n // Voltage\n {\n key: \"voltage_v\",\n id: \"voltage_v\",\n name: \"Voltage\",\n type: \"number\",\n role: \"value.voltage\",\n unit: \"V\",\n },\n {\n key: \"voltage_l1_v\",\n id: \"voltage_l1_v\",\n name: \"Voltage L1\",\n type: \"number\",\n role: \"value.voltage\",\n unit: \"V\",\n },\n {\n key: \"voltage_l2_v\",\n id: \"voltage_l2_v\",\n name: \"Voltage L2\",\n type: \"number\",\n role: \"value.voltage\",\n unit: \"V\",\n },\n {\n key: \"voltage_l3_v\",\n id: \"voltage_l3_v\",\n name: \"Voltage L3\",\n type: \"number\",\n role: \"value.voltage\",\n unit: \"V\",\n },\n // Current\n {\n key: \"current_a\",\n id: \"current_a\",\n name: \"Current\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"current_l1_a\",\n id: \"current_l1_a\",\n name: \"Current L1\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"current_l2_a\",\n id: \"current_l2_a\",\n name: \"Current L2\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"current_l3_a\",\n id: \"current_l3_a\",\n name: \"Current L3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n // Frequency\n {\n key: \"frequency_hz\",\n id: \"frequency_hz\",\n name: \"Frequency\",\n type: \"number\",\n role: \"value\",\n unit: \"Hz\",\n },\n // Energy import\n {\n key: \"energy_import_kwh\",\n id: \"energy_import_kwh\",\n name: \"Energy import total\",\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 name: \"Energy import T1\",\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 name: \"Energy import T2\",\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 name: \"Energy import T3\",\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 name: \"Energy import T4\",\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 name: \"Energy export total\",\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 name: \"Energy export T1\",\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 name: \"Energy export T2\",\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 name: \"Energy export T3\",\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 name: \"Energy export T4\",\n type: \"number\",\n role: \"value.energy\",\n unit: \"kWh\",\n },\n // Tariff\n {\n key: \"tariff\",\n id: \"tariff\",\n name: \"Active tariff\",\n type: \"number\",\n role: \"value\",\n },\n // Power quality\n {\n key: \"voltage_sag_l1_count\",\n id: \"quality.voltage_sag_l1_count\",\n name: \"Voltage sag L1\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_sag_l2_count\",\n id: \"quality.voltage_sag_l2_count\",\n name: \"Voltage sag L2\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_sag_l3_count\",\n id: \"quality.voltage_sag_l3_count\",\n name: \"Voltage sag L3\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l1_count\",\n id: \"quality.voltage_swell_l1_count\",\n name: \"Voltage swell L1\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l2_count\",\n id: \"quality.voltage_swell_l2_count\",\n name: \"Voltage swell L2\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"voltage_swell_l3_count\",\n id: \"quality.voltage_swell_l3_count\",\n name: \"Voltage swell L3\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"any_power_fail_count\",\n id: \"quality.power_fail_count\",\n name: \"Power fail count\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"long_power_fail_count\",\n id: \"quality.long_power_fail_count\",\n name: \"Long power fail count\",\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 name: \"Average power 15min\",\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 name: \"Monthly power peak\",\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 name: \"Monthly power peak time\",\n type: \"string\",\n role: \"date\",\n },\n // kWh meter specifics\n {\n key: \"apparent_current_a\",\n id: \"apparent_current_a\",\n name: \"Apparent current\",\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 name: \"Apparent current L1\",\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 name: \"Apparent current L2\",\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 name: \"Apparent current L3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"reactive_current_a\",\n id: \"reactive_current_a\",\n name: \"Reactive current\",\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 name: \"Reactive current L1\",\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 name: \"Reactive current L2\",\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 name: \"Reactive current L3\",\n type: \"number\",\n role: \"value.current\",\n unit: \"A\",\n },\n {\n key: \"apparent_power_va\",\n id: \"apparent_power_va\",\n name: \"Apparent power\",\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 name: \"Apparent power L1\",\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 name: \"Apparent power L2\",\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 name: \"Apparent power L3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"VA\",\n },\n {\n key: \"reactive_power_var\",\n id: \"reactive_power_var\",\n name: \"Reactive power\",\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 name: \"Reactive power L1\",\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 name: \"Reactive power L2\",\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 name: \"Reactive power L3\",\n type: \"number\",\n role: \"value.power\",\n unit: \"var\",\n },\n {\n key: \"power_factor\",\n id: \"power_factor\",\n name: \"Power factor\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l1\",\n id: \"power_factor_l1\",\n name: \"Power factor L1\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l2\",\n id: \"power_factor_l2\",\n name: \"Power factor L2\",\n type: \"number\",\n role: \"value\",\n },\n {\n key: \"power_factor_l3\",\n id: \"power_factor_l3\",\n name: \"Power factor L3\",\n type: \"number\",\n role: \"value\",\n },\n // Battery specifics\n {\n key: \"state_of_charge_pct\",\n id: \"state_of_charge_pct\",\n name: \"State of charge\",\n type: \"number\",\n role: \"value.battery\",\n unit: \"%\",\n },\n {\n key: \"cycles\",\n id: \"cycles\",\n name: \"Charge cycles\",\n type: \"number\",\n role: \"value\",\n },\n // Metadata\n {\n key: \"meter_model\",\n id: \"meter_model\",\n name: \"Meter model\",\n type: \"string\",\n role: \"text\",\n },\n {\n key: \"timestamp\",\n id: \"timestamp\",\n name: \"Measurement timestamp\",\n type: \"string\",\n role: \"date\",\n },\n];\n\n/** Manages ioBroker state creation and updates for HomeWizard devices */\nexport class StateManager {\n private readonly adapter: utils.AdapterInstance;\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 await this.adapter.extendObjectAsync(prefix, {\n type: \"device\",\n common: {\n name: config.productName || config.productType,\n statusStates: {\n onlineId: `${this.adapter.namespace}.${prefix}.info.connected`,\n },\n } as ioBroker.DeviceCommon,\n native: {},\n });\n\n await this.adapter.extendObjectAsync(`${prefix}.info`, {\n type: \"channel\",\n common: { name: \"Device Information\" },\n native: {},\n });\n\n await this.createState(`${prefix}.info.productName`, \"Product name\", \"string\", \"text\", false);\n await this.createState(`${prefix}.info.productType`, \"Product type\", \"string\", \"text\", false);\n await this.createState(`${prefix}.info.firmware`, \"Firmware version\", \"string\", \"text\", false);\n await this.createState(`${prefix}.info.connected`, \"Device connected\", \"boolean\", \"indicator.reachable\", false);\n await this.createState(`${prefix}.info.wifi_rssi_db`, \"WiFi signal strength\", \"number\", \"value\", false, \"dB\");\n await this.createState(`${prefix}.info.uptime_s`, \"Uptime\", \"number\", \"value\", false, \"s\");\n\n // Remove device button\n await this.createButton(`${prefix}.remove`, \"Remove device\");\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\n await this.adapter.setObjectNotExistsAsync(mPrefix, {\n type: \"channel\",\n common: { name: \"Measurement\" },\n native: {},\n });\n\n // Main measurement values \u2014 coerce per declared type\n const record = data;\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 await this.ensureAndSet(`${mPrefix}.${def.id}`, def.name, def.type, def.role, coerced, def.unit);\n }\n }\n\n // External meters (P1 gas/water/heat)\n const external = record.external;\n if (Array.isArray(external) && external.length > 0) {\n let extChannelEnsured = false;\n\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 if (!extChannelEnsured) {\n await this.adapter.setObjectNotExistsAsync(`${mPrefix}.external`, {\n type: \"channel\",\n common: { name: \"External Meters\" },\n native: {},\n });\n extChannelEnsured = true;\n }\n\n const extId = `${mPrefix}.external.${sanitize(type)}_${sanitize(uniqueId)}`;\n await this.adapter.setObjectNotExistsAsync(extId, {\n type: \"channel\",\n common: { name: type },\n native: {},\n });\n if (value !== null) {\n await this.ensureAndSet(`${extId}.value`, \"Value\", \"number\", \"value\", value, unit ?? undefined);\n }\n if (unit) {\n await this.ensureAndSet(`${extId}.unit`, \"Unit\", \"string\", \"text\", unit);\n }\n if (timestamp) {\n await this.ensureAndSet(`${extId}.timestamp`, \"Timestamp\", \"string\", \"date\", timestamp);\n }\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/uptime in info channel\n const rssi = coerceFiniteNumber(record.wifi_rssi_db);\n if (rssi !== null) {\n await this.ensureAndSet(`${prefix}.info.wifi_rssi_db`, \"WiFi signal strength\", \"number\", \"value\", rssi, \"dB\");\n }\n const uptime = coerceFiniteNumber(record.uptime_s);\n if (uptime !== null) {\n await this.ensureAndSet(`${prefix}.info.uptime_s`, \"Uptime\", \"number\", \"value\", uptime, \"s\");\n }\n\n // System control channel\n await this.adapter.setObjectNotExistsAsync(`${prefix}.system`, {\n type: \"channel\",\n common: { name: \"System Settings\" },\n native: {},\n });\n\n const cloudEnabled = coerceBoolean(record.cloud_enabled);\n if (cloudEnabled !== null) {\n await this.ensureAndSet(\n `${prefix}.system.cloud_enabled`,\n \"Cloud enabled\",\n \"boolean\",\n \"switch\",\n cloudEnabled,\n undefined,\n true,\n );\n }\n const ledPct = coerceFiniteNumber(record.status_led_brightness_pct);\n if (ledPct !== null) {\n await this.ensureAndSet(\n `${prefix}.system.status_led_brightness_pct`,\n \"LED brightness\",\n \"number\",\n \"level\",\n ledPct,\n \"%\",\n true,\n );\n }\n\n const apiV1 = coerceBoolean(record.api_v1_enabled);\n if (apiV1 !== null) {\n await this.ensureAndSet(\n `${prefix}.system.api_v1_enabled`,\n \"API v1 enabled\",\n \"boolean\",\n \"switch\",\n apiV1,\n undefined,\n true,\n );\n }\n\n // Action buttons\n await this.createButton(`${prefix}.system.reboot`, \"Reboot device\");\n await this.createButton(`${prefix}.system.identify`, \"Identify (blink LED)\");\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.adapter.setObjectNotExistsAsync(`${prefix}.battery`, {\n type: \"channel\",\n common: { name: \"Battery Control\" },\n native: {},\n });\n\n const mode = coerceString(record.mode);\n if (mode) {\n await this.ensureAndSet(`${prefix}.battery.mode`, \"Battery mode\", \"string\", \"text\", mode, undefined, true);\n }\n if (Array.isArray(record.permissions)) {\n await this.ensureAndSet(\n `${prefix}.battery.permissions`,\n \"Battery permissions\",\n \"string\",\n \"json\",\n JSON.stringify(record.permissions),\n undefined,\n true,\n );\n }\n\n const numberFields: Array<{\n key: string;\n id: string;\n name: string;\n role: string;\n unit?: string;\n }> = [\n {\n key: \"battery_count\",\n id: \"battery_count\",\n name: \"Connected batteries\",\n role: \"value\",\n },\n {\n key: \"power_w\",\n id: \"power_w\",\n name: \"Battery power\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"target_power_w\",\n id: \"target_power_w\",\n name: \"Target power\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"max_consumption_w\",\n id: \"max_consumption_w\",\n name: \"Max consumption\",\n role: \"value.power\",\n unit: \"W\",\n },\n {\n key: \"max_production_w\",\n id: \"max_production_w\",\n name: \"Max production\",\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(`${prefix}.battery.${field.id}`, field.name, \"number\", field.role, coerced, field.unit);\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.setStateAsync(`${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 await this.adapter.delObjectAsync(prefix, { recursive: true });\n }\n\n /**\n * Remove measurement states from old locations (pre-v0.4.0: device root instead of measurement/ channel)\n *\n * @param config Device configuration\n */\n async cleanupMovedStates(config: DeviceConfig): Promise<void> {\n const prefix = this.devicePrefix(config);\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\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 }\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 * Create a state if it doesn't exist\n *\n * @param id State ID\n * @param name State name\n * @param type Value type\n * @param role ioBroker role\n * @param write Whether state is writable\n * @param unit Optional unit\n */\n private async createState(\n id: string,\n name: string,\n type: ioBroker.CommonType,\n role: string,\n write: boolean,\n unit?: string,\n ): Promise<void> {\n const common: Partial<ioBroker.StateCommon> = {\n name,\n type,\n role,\n read: true,\n write,\n };\n if (unit) {\n common.unit = unit;\n }\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: common as ioBroker.StateCommon,\n native: {},\n });\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 State name\n */\n private async createButton(id: string, name: string): Promise<void> {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: {\n name,\n type: \"boolean\",\n role: \"button\",\n read: false,\n write: true,\n },\n native: {},\n });\n await this.adapter.setStateAsync(id, { val: false, ack: true });\n }\n\n /**\n * Ensure state exists and set value\n *\n * @param id State ID\n * @param name State name\n * @param type Value type\n * @param role ioBroker role\n * @param value State value\n * @param unit Optional unit\n * @param write Whether state is writable\n */\n private async ensureAndSet(\n id: string,\n name: string,\n type: ioBroker.CommonType,\n role: string,\n value: ioBroker.StateValue,\n unit?: string,\n write?: boolean,\n ): Promise<void> {\n await this.createState(id, name, type, role, write ?? false, unit);\n await this.adapter.setStateAsync(id, { val: value, ack: true });\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/websocket-client.ts"],
4
- "sourcesContent": ["import WebSocket from \"ws\";\nimport { HW_AGENT } from \"./cacert\";\nimport { isPlainObject } from \"./coerce\";\nimport type { Measurement } from \"./types\";\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 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, and auto-reconnect.\n */\nexport class HomeWizardWebSocket {\n private readonly ip: string;\n private readonly token: string;\n private readonly callbacks: WsCallbacks;\n private ws: WebSocket | null = null;\n private destroyed = false;\n\n /**\n * @param ip Device IP address\n * @param token Bearer token\n * @param callbacks Event callbacks\n */\n constructor(ip: string, token: string, callbacks: WsCallbacks) {\n this.ip = ip;\n this.token = token;\n this.callbacks = callbacks;\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 url = `wss://${this.ip}/api/ws`;\n this.callbacks.log.debug(`WS connecting to ${url}`);\n\n this.ws = new WebSocket(url, {\n agent: HW_AGENT,\n handshakeTimeout: 10_000,\n });\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(\"close\", (code: number, reason: Buffer) => {\n this.callbacks.log.debug(`WS closed: ${code} ${reason.toString()}`);\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 this.callbacks.log.debug(\"WS authorized, subscribing to measurement\");\n this.sendRaw({ type: \"subscribe\", data: \"measurement\" });\n this.callbacks.onConnected();\n break;\n\n case \"measurement\":\n if (isPlainObject(parsed.data)) {\n this.callbacks.onMeasurement(parsed.data as Measurement);\n } else {\n this.callbacks.log.warn(`WS measurement without object payload`);\n }\n break;\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 /** Close WebSocket without triggering reconnect */\n private cleanup(): void {\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 WebSocket from \"ws\";\nimport { HW_AGENT } from \"./cacert\";\nimport { isPlainObject } from \"./coerce\";\nimport type { Measurement } from \"./types\";\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 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, and auto-reconnect.\n */\nexport class HomeWizardWebSocket {\n private readonly ip: string;\n private readonly token: string;\n private readonly callbacks: WsCallbacks;\n private ws: WebSocket | null = null;\n private destroyed = false;\n\n /**\n * @param ip Device IP address\n * @param token Bearer token\n * @param callbacks Event callbacks\n */\n constructor(ip: string, token: string, callbacks: WsCallbacks) {\n this.ip = ip;\n this.token = token;\n this.callbacks = callbacks;\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 url = `wss://${this.ip}/api/ws`;\n this.callbacks.log.debug(`WS connecting to ${url}`);\n\n this.ws = new WebSocket(url, {\n agent: HW_AGENT,\n handshakeTimeout: 10_000,\n });\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(\"close\", (code: number, reason: Buffer) => {\n this.callbacks.log.debug(`WS closed: ${code} ${reason.toString()}`);\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 this.callbacks.log.debug(\"WS authorized, subscribing to measurement\");\n this.sendRaw({ type: \"subscribe\", data: \"measurement\" });\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 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 /** Close WebSocket without triggering reconnect */\n private cleanup(): void {\n if (this.ws) {\n this.ws.removeAllListeners();\n // Prevent uncaught errors from frames received during close\n this.ws.on(\"error\", () => {});\n this.ws.terminate();\n this.ws = null;\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["WebSocket"]
7
7
  }
package/io-package.json CHANGED
@@ -1,98 +1,98 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "homewizard",
4
- "version": "0.6.6",
4
+ "version": "0.6.7",
5
5
  "news": {
6
+ "0.6.7": {
7
+ "en": "Documentation: rewrote release notes in user-friendly style across all languages.",
8
+ "de": "Dokumentation: Release-Notes in alle Sprachen User-orientiert neu geschrieben.",
9
+ "ru": "Документация: примечания к релизу переписаны в дружественном к пользователю стиле на всех языках.",
10
+ "pt": "Documentação: notas de release reescritas em estilo amigável ao usuário em todos os idiomas.",
11
+ "nl": "Documentatie: release-notes herschreven in gebruikersvriendelijke stijl in alle talen.",
12
+ "fr": "Documentation : notes de version réécrites dans un style adapté à l'utilisateur dans toutes les langues.",
13
+ "it": "Documentazione: note di release riscritte in stile user-friendly in tutte le lingue.",
14
+ "es": "Documentación: notas de versión reescritas en estilo amigable para el usuario en todos los idiomas.",
15
+ "pl": "Dokumentacja: notatki wydania przepisane w przyjaznym dla użytkownika stylu we wszystkich językach.",
16
+ "uk": "Документація: нотатки до релізу переписано у дружньому до користувача стилі усіма мовами.",
17
+ "zh-cn": "文档:所有语言的发布说明已重写为用户友好的风格。"
18
+ },
6
19
  "0.6.6": {
7
- "en": "Audit cleanup against the upstream `ioBroker.example/TypeScript` full standard:\nTest setup migrated: tests now live next to source as `src/**/*.test.ts` and run directly via `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: \">=20\"`\nDependabot now ignores major bumps for `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + `coverage` script added\n`prettier.config.mjs` made explicit with project-style overrides (Spaces 2-wide, double quotes)\nOrphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)",
8
- "de": "Audit-Aufräumung gegen den vorgeschalteten `ioBroker.example/TypeScript` Vollstandard:\nTest-Setup migriert: Tests leben jetzt neben der Quelle als `src/**/*.test.ts` und laufen direkt über `ts-node/register`. Entfernen `tsconfig.test.json` + `build-test/`, add `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` rollte zurück von `^25.6.0` zu `^20.19.24` so type defs match `engines.node: \">=20\"``\nDependabot ignoriert nun wichtige Beulen für `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + `coverage` skript hinzugefügt\n`prettier.config.mjs` explizit mit Projekt-Stil Overrides (Spaces 2-wide, doppelte Zitate)\nOrphan `.github/auto-merge.yml` entfernt (aktiver Workflow ist `automerge-dependabot.yml` mit `gh pr merge`)",
9
- "ru": "Очистка аудита по полному стандарту «ioBroker.example/TypeScript»:\nНастройка тестирования мигрировала: тесты теперь живут рядом с источником как «src/**/*.test.ts» и запускаются непосредственно через «ts-узел/регистр». Снято \"tsconfig.test.json\" + \"build-test/\", добавлено \"test/mocharc.custom.json\" + \"test/mocha.setup.js\" + \"test/tsconfig.json\" + \"test/.eslintrc.json\"\n'@types/node' откатился от '^25.6.0' к '^20.19.24', так что type defs соответствуют 'engines.node: '>=20'\nDependabot теперь игнорирует основные проблемы для «@types/node», «typescript», «eslint», «actions/checkout», «actions/setup-node»\n«nyc» config + скрипт «coverage»\n«prettier.config.mjs» в прямом эфире с надстройками в стиле проекта (пространства 2, двойные цитаты)\nOrphan '.github/auto-merge.yml' removed (активный рабочий процесс - 'automerge-dependabot.yml' using 'gh pr merge')",
10
- "pt": "Limpeza de auditoria contra o padrão completo 'ioBroker.example/TypeScript` upstream:\nConfiguração de teste migrada: testes agora ao vivo ao lado do código fonte como `src/**/*.test.ts` e executado diretamente via `ts-node/register'. Removido `tsconfig.test.json` + `build-test/`, adicionado `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json'\n`@types/node` rolled back from `^25.6.0` to `^20.19.24` então tipo defs match `engines.node: \">=20\"`\nDependabot agora ignora grandes saliências para `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node'\n'nyc' config + 'coverage' script adicionado\n`prettier.config.mjs` explicitado com sobreposições de estilo de projeto (Espaços 2-largura, aspas duplas)\nÓrfão `.github/auto-merge.yml` removido (fluxo de trabalho ativo é `automerge-dependabot.yml` usando `gh pr merge`)",
11
- "nl": "Audit cleanup tegen de upstream .example/TypeScript\nTestopstelling gemigreerd: de tests leven nu naast de broncode als Verwijderd \nhet type defs komt overeen met de motors.node: \">=20\"\nDependabot negeert nu de grote hobbels voor \n`nyc` config + `coverage` script added\n`prettier.config.mjs` made explicit with project-style overrides (Spaces 2-wide, double quotes)\nOrphan ",
12
- "fr": "Nettoyage de l'audit par rapport à la norme en amont `ioBroker.example/TypeScript`:\nConfiguration de test migrée: teste maintenant en direct à côté de source comme `src/**/*.test.ts` et fonctionne directement via `ts-node/register`. Supprimé `tsconfig.test.json` + `build-test/`, ajouté `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.estintrc.json`\n`@types/node` retourné de `^25.6.0` à `^20.19.24` de sorte que le type defs correspond `moteurs.node: \">=20\"`\nDependabot ignore maintenant les principales bosses pour `@types/node`, `typescript`, `estint`, `actions/checkout`, `actions/setup-node`\nscript `nyc` config + `coverage` ajouté\n`prettier.config.mjs` fait explicite avec des redéfinitions de type projet (espaces 2-large, guillemets doubles)\nOrphan `.github/auto-merge.yml` supprimé (le workflow actif est `automerge-dependabot.yml` en utilisant `gh pr merge`)",
13
- "it": "Audit cleanup contro il `ioBroker.example/TypeScript` standard completo:\nConfigurazione del test migrata: i test ora vivono accanto alla sorgente come `src/**/*.test.ts` e vengono eseguiti direttamente tramite `ts-node/register`. `tsconfig.test.json` + `build-test/`, aggiunto `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` rotolato indietro da `^25.6.0` a `^20.19.24` così tipo defs match `engines.node: \">=20\"`\nDependabot ora ignora urti principali per `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + `coverage` script aggiunto\n`prettier.config.mjs` fatto esplicito con overrides di progetto (Spaces 2-wide, doppie citazioni)\nOrphan `.github/auto-merge.yml` rimosso (il flusso di lavoro attivo è `automerge-dependabot.yml` utilizzando `gh pr merge`)",
14
- "es": "Limpieza de auditorías contra el estándar completo del `ioBroker.example/TypeScript`:\nSe migraron las pruebas: las pruebas ahora viven al lado de la fuente como `src/**/*.test.ts` y se ejecutan directamente a través de `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: \"consign=20\"\nDependabot hace caso omiso de los principales golpes de `@tipos/nodo`, `tiposcript`, `eslint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + `coverage` script añadido\n`prettier.config.mjs` hecho explícito con anulas de estilo de proyecto (Spaces 2-wide, citas dobles)\nOrphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)",
15
- "pl": "Oczyszczanie audytów w stosunku do pełnego standardu 'iBroker.example / TypeScript':\nUstawienie testowe migrowane: testy są teraz dostępne obok źródła jako 'src / */* .test.ts' i uruchamiane bezpośrednio przez 'ts- node / register'. Usunięto 'tsconfig.test.json' + 'build- test /', dodano 'test / mocharc.customi.json' + 'test / mocha.setup.js' + 'test / tsconfig.json' + 'test / .eslintrc.json' \n'@ types / node' rolled back from '^ 25.6.0' to '^ 20.19.24' so type defs match 'engins.node: \"> = 20\"'\nDependabot ignoruje teraz główne przeszkody dla '@ types / node', 'typescript', 'eslint', 'actions / checkout', 'actions / setup-node'\ndodano skrypt 'nyc' config + 'cover'\n'prettier.config.mjs' jest wyraźna z nadwoziem w stylu project- style (Spaces 2- szerokie, podwójne cytaty)\nUsunięty Orphan '.github / auto- merge.yml' (aktywny przepływ pracy to 'automaterge- dependiabot.yml' przy użyciu 'gh pr merge')",
16
- "uk": "Аудиторське очищення від потоку `ioBroker.example/TypeScript`\nТестові налаштування мігровані: тести тепер живуть поруч з джерелом як `src/**/**.test.ts` і запустіть безпосередньо через `ts-node/register`. Вилучено `tsconfig.test.json` + `build-test/``, додано `test/mocharc.custom.json` + `test/mocha.setup.js` +`test/tsconfig.json` + `test/.eslintrc.json`\n`@types/node` від `^25.6.0`` до `^20.19.24` так типу defs match `engines.node: \">=20\"`\nВ залежності від того, як `slint`, `slint`, `actions/checkout`, `actions/setup-node`\n`nyc` config + скрипт `coverage` додано\n`prettier.config.mjs` зробив явний з перенаряддями проекту (Косміка 2-широка, подвійні котирування)\nOrphan `.github/auto-merge.yml` вилучено (активний робочий процес `automerge-залежніabot.yml` за допомогою `gh pr злиття`)",
17
- "zh-cn": "对照上游`ioBroker.example/TypeScript ' 全面标准进行审计清理:\n测试设置迁移:测试现在作为`src/**/*.test.ts ' 在源旁进行,直接通过`ts-node/register ' 运行。 删除`tsconfig.test.json ' +`building-test/ ' ,增加`test/mocharc.custom.json ' +`test/mocha.setup.js ' +`test/tsconfig.json ' +`test/.eslintrac.json'\n`@型/节点`从`^25.6.0`回滚至`^20.19.24`,因此 type型与 型相同\n现在,依赖忽略了`Q ' 类型/节点 ' 、`字典 ' 、`slint ' 、`行动/检查 ' 、`行动/设置节点 ' 的重大颠峰\n添加了 \" nyc \" 配置 + \" 覆盖 \" 文字\n`prettier.config.mjs'明确了项目式的重置(2宽空间,双引号)\n孤儿`.github/自动合并.yml ' 被删除(使用`gh pr 合并 ' 的主动工作流程为`自动合并-依赖.yml')"
20
+ "en": "Internal cleanup. No user-facing changes.",
21
+ "de": "Interne Bereinigung. Keine User-sichtbaren Änderungen.",
22
+ "ru": "Внутренняя очистка. Без видимых изменений для пользователя.",
23
+ "pt": "Limpeza interna. Sem alterações visíveis para o usuário.",
24
+ "nl": "Interne opschoning. Geen wijzigingen voor de gebruiker.",
25
+ "fr": "Nettoyage interne. Aucune modification visible pour l'utilisateur.",
26
+ "it": "Pulizia interna. Nessuna modifica visibile all'utente.",
27
+ "es": "Limpieza interna. Sin cambios visibles para el usuario.",
28
+ "pl": "Wewnętrzne porządki. Brak zmian widocznych dla użytkownika.",
29
+ "uk": "Внутрішнє прибирання. Без помітних для користувача змін.",
30
+ "zh-cn": "内部清理。对用户无可见变化。"
18
31
  },
19
32
  "0.6.5": {
20
- "en": "Process-level unhandledRejection/uncaughtException handlers added as last-line-of-defence. Stop shipping the manual-review release-script plugin. Audit-driven boilerplate sync with the other krobi adapters. Min js-controller correction: was >=7.0.0, restored to repochecker-recommended >=6.0.11 (Source: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker bumped to ^7.1.1.",
21
- "de": "Process-level unhandledRejection/uncaughtException-Handler als Last-Line-of-Defence hinzugefügt. Das manual-review-Release-Script-Plugin entfällt. Audit-getriebener Boilerplate-Sync gegenüber den anderen krobi-Adaptern. js-controller-Korrektur: war >=7.0.0, korrigiert auf Repochecker-recommended >=6.0.11 (Quelle: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker auf ^7.1.1 angehoben.",
22
- "ru": "Process-level обробники unhandledRejection/uncaughtException додано як last-line-of-defence. Manual-review release-script plugin більше не постачається. Audit-керована синхронізація boilerplate з іншими krobi-адаптерами. Корекція js-controller: було >=7.0.0, відновлено на repochecker-recommended >=6.0.11 (Джерело: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker оновлено до ^7.1.1.",
23
- "pt": "Handlers process-level unhandledRejection/uncaughtException adicionados como last-line-of-defence. O plugin manual-review do release-script foi removido. Sincronização de boilerplate orientada por audit com os outros adaptadores krobi. Correção js-controller: era >=7.0.0, restaurada para repochecker-recommended >=6.0.11 (Fonte: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker atualizado para ^7.1.1.",
24
- "nl": "Process-level unhandledRejection/uncaughtException-handlers toegevoegd als last-line-of-defence. Het manual-review release-script plugin wordt niet meer meegeleverd. Audit-gestuurde boilerplate-sync met de andere krobi-adapters. js-controller-correctie: was >=7.0.0, hersteld naar repochecker-recommended >=6.0.11 (Bron: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker verhoogd naar ^7.1.1.",
25
- "fr": "Handlers process-level unhandledRejection/uncaughtException ajoutés en last-line-of-defence. Le plugin manual-review du release-script n'est plus livré. Synchronisation boilerplate audit-driven avec les autres adaptateurs krobi. Correction js-controller: était >=7.0.0, restaurée à repochecker-recommended >=6.0.11 (Source: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker porté à ^7.1.1.",
26
- "it": "Handler process-level unhandledRejection/uncaughtException aggiunti come last-line-of-defence. Il plugin manual-review del release-script non viene più distribuito. Sincronizzazione boilerplate audit-driven con gli altri adapter krobi. Correzione js-controller: era >=7.0.0, ripristinata a repochecker-recommended >=6.0.11 (Fonte: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker alzato a ^7.1.1.",
27
- "es": "Handlers process-level unhandledRejection/uncaughtException añadidos como last-line-of-defence. El plugin manual-review del release-script ya no se incluye. Sincronización de boilerplate guiada por audit con los demás adaptadores krobi. Corrección js-controller: era >=7.0.0, restaurada a repochecker-recommended >=6.0.11 (Fuente: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker elevado a ^7.1.1.",
28
- "pl": "Handlery process-level unhandledRejection/uncaughtException dodane jako last-line-of-defence. Plugin manual-review release-script nie jest już dostarczany. Synchronizacja boilerplate'u audit-driven z pozostałymi adapterami krobi. Korekcja js-controller: było >=7.0.0, przywrócono do repochecker-recommended >=6.0.11 (Źródło: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker podniesione do ^7.1.1.",
29
- "uk": "Process-level обробники unhandledRejection/uncaughtException додано як last-line-of-defence. Manual-review release-script plugin більше не постачається. Audit-керована синхронізація boilerplate з іншими krobi-адаптерами. js-controller-корекція: було >=7.0.0, відновлено на repochecker-recommended >=6.0.11 (Джерело: ioBroker.repochecker/lib/M1000_IOPackageJson.js). @types/iobroker оновлено до ^7.1.1.",
30
- "zh-cn": "新增进程级 unhandledRejection/uncaughtException 处理器作为 last-line-of-defence。manual-review release-script 插件不再随包发布。与其他 krobi 适配器进行 audit 驱动的 boilerplate 同步。js-controller 更正:从 >=7.0.0 恢复为 repochecker 推荐的 >=6.0.11(来源:ioBroker.repochecker/lib/M1000_IOPackageJson.js)。@types/iobroker 升级至 ^7.1.1。"
33
+ "en": "Crash defense: process-level error handlers. Min js-controller restored to >=6.0.11.",
34
+ "de": "Crash-Schutz: process-level Error-Handler. Min js-controller zurück auf >=6.0.11.",
35
+ "ru": "Защита от сбоев: process-level обработчики ошибок. Мин. js-controller восстановлен на >=6.0.11.",
36
+ "pt": "Proteção contra falhas: handlers de erro process-level. Mín. js-controller restaurado para >=6.0.11.",
37
+ "nl": "Crashbescherming: process-level error-handlers. Min. js-controller hersteld op >=6.0.11.",
38
+ "fr": "Défense contre les crashs : handlers d'erreur process-level. Min js-controller rétabli à >=6.0.11.",
39
+ "it": "Difesa anti-crash: handler di errore process-level. Min js-controller ripristinato a >=6.0.11.",
40
+ "es": "Defensa contra fallos: handlers de error process-level. Mín. js-controller restaurado a >=6.0.11.",
41
+ "pl": "Ochrona przed crashem: handlery błędów process-level. Min. js-controller przywrócony do >=6.0.11.",
42
+ "uk": "Захист від збоїв: process-level обробники помилок. Мін. js-controller повернено на >=6.0.11.",
43
+ "zh-cn": "崩溃防护:process-level 错误处理器。最低 js-controller 恢复为 >=6.0.11。"
31
44
  },
32
45
  "0.6.4": {
33
- "en": "Proactive sync with parcelapp v0.2.14 learnings: tsconfig.test.json emits to build-test/ (prevents build/src + build/test duplicates), wrap async onReady and onStateChange with .catch(), declare pairingIp as instance object (11 languages) instead of dynamic creation in onReady",
34
- "de": "Proaktive Übernahme der parcelapp v0.2.14 Learnings: tsconfig.test.json schreibt nach build-test/ (verhindert build/src + build/test Duplikate), async onReady und onStateChange mit .catch() gehärtet, pairingIp als instance object deklariert (11 Sprachen) statt dynamisch in onReady",
35
- "ru": "Проактивная синхронизация с parcelapp v0.2.14: tsconfig.test.json собирает в build-test/ (предотвращает дубли build/src + build/test), async onReady и onStateChange обёрнуты в .catch(), pairingIp объявлен как instance object (11 языков) вместо динамического создания в onReady",
36
- "pt": "Sincronização proativa com parcelapp v0.2.14: tsconfig.test.json emite para build-test/ (evita duplicados build/src + build/test), onReady e onStateChange assíncronos envolvidos em .catch(), pairingIp declarado como instance object (11 línguas) em vez de criação dinâmica em onReady",
37
- "nl": "Proactieve synchronisatie met parcelapp v0.2.14: tsconfig.test.json schrijft naar build-test/ (voorkomt build/src + build/test duplicaten), async onReady en onStateChange omhuld met .catch(), pairingIp als instance object gedeclareerd (11 talen) in plaats van dynamische creatie in onReady",
38
- "fr": "Synchronisation proactive avec parcelapp v0.2.14 : tsconfig.test.json émet vers build-test/ (évite les doublons build/src + build/test), onReady et onStateChange asynchrones protégés par .catch(), pairingIp déclaré comme instance object (11 langues) au lieu d'une création dynamique dans onReady",
39
- "it": "Sincronizzazione proattiva con parcelapp v0.2.14: tsconfig.test.json emette in build-test/ (previene duplicati build/src + build/test), onReady e onStateChange asincroni protetti con .catch(), pairingIp dichiarato come instance object (11 lingue) invece della creazione dinamica in onReady",
40
- "es": "Sincronización proactiva con parcelapp v0.2.14: tsconfig.test.json escribe en build-test/ (evita duplicados build/src + build/test), onReady y onStateChange asíncronos protegidos con .catch(), pairingIp declarado como instance object (11 idiomas) en lugar de creación dinámica en onReady",
41
- "pl": "Proaktywna synchronizacja z parcelapp v0.2.14: tsconfig.test.json zapisuje do build-test/ (zapobiega duplikatom build/src + build/test), async onReady i onStateChange zabezpieczone .catch(), pairingIp zadeklarowany jako instance object (11 języków) zamiast dynamicznego tworzenia w onReady",
42
- "uk": "Проактивна синхронізація з parcelapp v0.2.14: tsconfig.test.json збирає у build-test/ (запобігає дублям build/src + build/test), async onReady та onStateChange захищено .catch(), pairingIp оголошено як instance object (11 мов) замість динамічного створення в onReady",
43
- "zh-cn": "主动采纳 parcelapp v0.2.14 经验:tsconfig.test.json 输出到 build-test/(避免 build/src + build/test 重复),异步 onReady 和 onStateChange 使用 .catch() 加固,pairingIp 声明为 instance object(11 种语言)而非在 onReady 中动态创建"
46
+ "en": "Internal hardening. No user-facing changes.",
47
+ "de": "Interne Härtung. Keine User-sichtbaren Änderungen.",
48
+ "ru": "Внутреннее усиление. Без видимых изменений для пользователя.",
49
+ "pt": "Endurecimento interno. Sem alterações visíveis para o usuário.",
50
+ "nl": "Interne verharding. Geen wijzigingen voor de gebruiker.",
51
+ "fr": "Durcissement interne. Aucune modification visible pour l'utilisateur.",
52
+ "it": "Hardening interno. Nessuna modifica visibile all'utente.",
53
+ "es": "Endurecimiento interno. Sin cambios visibles para el usuario.",
54
+ "pl": "Wewnętrzne wzmocnienie. Brak zmian widocznych dla użytkownika.",
55
+ "uk": "Внутрішнє зміцнення. Без помітних для користувача змін.",
56
+ "zh-cn": "内部加固。对用户无可见变化。"
44
57
  },
45
58
  "0.6.3": {
46
- "en": "Harden WebSocket and REST input handling, stop endless reconnect on invalid token, avoid empty external channel",
47
- "de": "WebSocket- und REST-Eingaben gehärtet, Endlos-Reconnect bei ungültigem Token gestoppt, leeren External-Channel vermieden",
48
- "ru": "Усиленная обработка входных данных WebSocket и REST, остановка бесконечного переподключения при недействительном токене, избежание пустого external-канала",
49
- "pt": "Tratamento reforçado de entradas WebSocket e REST, reconexão infinita com token inválido interrompida, canal external vazio evitado",
50
- "nl": "Verstevigde afhandeling van WebSocket- en REST-invoer, eindeloze herverbinding bij ongeldig token gestopt, leeg external-kanaal voorkomen",
51
- "fr": "Traitement renforcé des entrées WebSocket et REST, arrêt de la reconnexion infinie en cas de jeton invalide, canal external vide évité",
52
- "it": "Gestione rafforzata degli input WebSocket e REST, riconnessione infinita su token non valido interrotta, canale external vuoto evitato",
53
- "es": "Manejo reforzado de entradas WebSocket y REST, reconexión infinita con token inválido detenida, canal external vacío evitado",
54
- "pl": "Wzmocnione przetwarzanie wejść WebSocket i REST, zatrzymano nieskończone ponowne łączenie przy nieprawidłowym tokenie, unikanie pustego kanału external",
55
- "uk": "Посилена обробка вхідних даних WebSocket та REST, зупинено нескінченне перепідключення при недійсному токені, уникнення порожнього каналу external",
56
- "zh-cn": "加固 WebSocket 和 REST 输入处理,令牌无效时停止无限重连,避免创建空的 external 通道"
59
+ "en": "WebSocket and REST input hardening. Stops endless reconnect when the device token is invalid.",
60
+ "de": "WebSocket- und REST-Input-Härtung. Beendet Endlos-Reconnect bei ungültigem Geräte-Token.",
61
+ "ru": "Усиление input WebSocket и REST. Прекращает бесконечный reconnect при недействительном токене устройства.",
62
+ "pt": "Endurecimento de entrada WebSocket e REST. Para a reconexão infinita quando o token do dispositivo é inválido.",
63
+ "nl": "WebSocket- en REST-input-verharding. Stopt eindeloze reconnect bij ongeldig apparaat-token.",
64
+ "fr": "Durcissement des entrées WebSocket et REST. Arrête la reconnexion infinie en cas de token d'appareil invalide.",
65
+ "it": "Hardening input WebSocket e REST. Ferma riconnessione infinita su token dispositivo non valido.",
66
+ "es": "Endurecimiento de entrada WebSocket y REST. Detiene la reconexión infinita con token de dispositivo inválido.",
67
+ "pl": "Wzmocnienie input WebSocket i REST. Zatrzymuje nieskończony reconnect przy nieprawidłowym tokenie urządzenia.",
68
+ "uk": "Зміцнення input WebSocket та REST. Припиняє нескінченний reconnect при недійсному токені пристрою.",
69
+ "zh-cn": "WebSocket 和 REST 输入加固。设备 token 无效时停止无限重连。"
57
70
  },
58
71
  "0.6.2": {
59
- "en": "Fix hanging promise on response stream error, safe onUnload with try/finally, optimize state creation hot path (setObjectNotExistsAsync), remove dead code",
60
- "de": "Hängendes Promise bei Response-Stream-Fehler behoben, sicheres onUnload mit try/finally, State-Erstellung im Hot Path optimiert (setObjectNotExistsAsync), toten Code entfernt",
61
- "ru": "Исправлен зависший промис при ошибке потока ответа, безопасный onUnload с try/finally, оптимизация создания состояний (setObjectNotExistsAsync), удалён мёртвый код",
62
- "pt": "Corrigida promessa pendente em erro de stream de resposta, onUnload seguro com try/finally, otimização da criação de estados (setObjectNotExistsAsync), código morto removido",
63
- "nl": "Vastlopende promise bij response stream fout opgelost, veilige onUnload met try/finally, state-creatie hot path geoptimaliseerd (setObjectNotExistsAsync), dode code verwijderd",
64
- "fr": "Correction de la promesse bloquée lors d'une erreur de flux de réponse, onUnload sécurisé avec try/finally, optimisation du chemin critique de création d'états (setObjectNotExistsAsync), code mort supprimé",
65
- "it": "Corretta promessa bloccata su errore stream di risposta, onUnload sicuro con try/finally, ottimizzazione percorso critico creazione stati (setObjectNotExistsAsync), codice morto rimosso",
66
- "es": "Corregida promesa colgada en error de flujo de respuesta, onUnload seguro con try/finally, optimización de ruta crítica de creación de estados (setObjectNotExistsAsync), código muerto eliminado",
67
- "pl": "Naprawiono zawieszającą się obietnicę przy błędzie strumienia odpowiedzi, bezpieczne onUnload z try/finally, optymalizacja ścieżki krytycznej tworzenia stanów (setObjectNotExistsAsync), usunięto martwy kod",
68
- "uk": "Виправлено завислий проміс при помилці потоку відповіді, безпечний onUnload з try/finally, оптимізація критичного шляху створення станів (setObjectNotExistsAsync), видалено мертвий код",
69
- "zh-cn": "修复响应流错误时挂起的Promise,使用try/finally保护onUnload,优化状态创建热路径(setObjectNotExistsAsync),移除死代码"
72
+ "en": "Fix: hanging promise on response stream errors. Safer adapter shutdown.",
73
+ "de": "Fix: hängender Promise bei Response-Stream-Fehlern. Sichereres Adapter-Shutdown.",
74
+ "ru": "Fix: зависший promise при ошибках response-stream. Более безопасное завершение работы адаптера.",
75
+ "pt": "Fix: promise pendurada em erros de response stream. Encerramento mais seguro do adaptador.",
76
+ "nl": "Fix: hangende promise bij response-stream-fouten. Veiliger afsluiten van de adapter.",
77
+ "fr": "Fix : promesse bloquée sur erreurs de response stream. Arrêt plus sûr de l'adaptateur.",
78
+ "it": "Fix: promise sospesa su errori response stream. Spegnimento più sicuro dell'adattatore.",
79
+ "es": "Fix: promise colgada en errores de response stream. Apagado más seguro del adaptador.",
80
+ "pl": "Fix: zawieszony promise przy błędach response stream. Bezpieczniejsze zamykanie adaptera.",
81
+ "uk": "Fix: завислий promise при помилках response-stream. Безпечніше завершення роботи адаптера.",
82
+ "zh-cn": "修复:response stream 错误时的挂起 promise。更安全的适配器关闭。"
70
83
  },
71
84
  "0.6.1": {
72
- "en": "Code cleanup: extract testable connection-utils module, add 20 unit tests, fix ESLint warnings, remove unused dependencies",
73
- "de": "Code-Bereinigung: testbares connection-utils Modul extrahiert, 20 Unit-Tests hinzugefügt, ESLint-Warnungen behoben, ungenutzte Abhängigkeiten entfernt",
74
- "ru": "Очистка кода: выделен тестируемый модуль connection-utils, добавлено 20 юнит-тестов, исправлены предупреждения ESLint, удалены неиспользуемые зависимости",
75
- "pt": "Limpeza de código: módulo connection-utils testável extraído, 20 testes unitários adicionados, avisos ESLint corrigidos, dependências não utilizadas removidas",
76
- "nl": "Code-opschoning: testbare connection-utils module geëxtraheerd, 20 unit tests toegevoegd, ESLint-waarschuwingen opgelost, ongebruikte afhankelijkheden verwijderd",
77
- "fr": "Nettoyage du code: module connection-utils testable extrait, 20 tests unitaires ajoutés, avertissements ESLint corrigés, dépendances inutilisées supprimées",
78
- "it": "Pulizia del codice: modulo connection-utils testabile estratto, 20 test unitari aggiunti, avvisi ESLint corretti, dipendenze inutilizzate rimosse",
79
- "es": "Limpieza de código: módulo connection-utils testable extraído, 20 pruebas unitarias añadidas, advertencias ESLint corregidas, dependencias no utilizadas eliminadas",
80
- "pl": "Czyszczenie kodu: wyodrębniono testowalny moduł connection-utils, dodano 20 testów jednostkowych, naprawiono ostrzeżenia ESLint, usunięto nieużywane zależności",
81
- "uk": "Очищення коду: виділено тестований модуль connection-utils, додано 20 юніт-тестів, виправлено попередження ESLint, видалено невикористані залежності",
82
- "zh-cn": "代码清理:提取可测试的connection-utils模块,添加20个单元测试,修复ESLint警告,移除未使用的依赖"
83
- },
84
- "0.6.0": {
85
- "en": "Adaptive unstable mode: auto-detect devices with bad WiFi, faster reconnect (60s vs 5min), persistent REST fallback",
86
- "de": "Adaptiver Unstable-Modus: Geräte mit schlechtem WLAN automatisch erkennen, schnellerer Reconnect (60s statt 5min), persistenter REST-Fallback",
87
- "ru": "Адаптивный нестабильный режим: автоопределение устройств с плохим WiFi, быстрое переподключение (60с вместо 5мин), постоянный REST-фоллбэк",
88
- "pt": "Modo instável adaptativo: detecção automática de dispositivos com WiFi fraco, reconexão mais rápida (60s vs 5min), REST fallback persistente",
89
- "nl": "Adaptieve instabiele modus: automatisch apparaten met slecht WiFi detecteren, snellere herverbinding (60s vs 5min), persistent REST-fallback",
90
- "fr": "Mode instable adaptatif: détection auto des appareils avec mauvais WiFi, reconnexion plus rapide (60s vs 5min), REST fallback persistant",
91
- "it": "Modalità instabile adattiva: rilevamento automatico dispositivi con WiFi scarso, riconnessione più rapida (60s vs 5min), REST fallback persistente",
92
- "es": "Modo inestable adaptativo: detección automática de dispositivos con WiFi débil, reconexión más rápida (60s vs 5min), REST fallback persistente",
93
- "pl": "Adaptacyjny tryb niestabilny: automatyczne wykrywanie urządzeń ze słabym WiFi, szybsze ponowne połączenie (60s vs 5min), trwały REST fallback",
94
- "uk": "Адаптивний нестабільний режим: автовизначення ��ристроїв з поганим WiFi, швидше перепідключення (60с замість 5хв), постійний REST-фоллбек",
95
- "zh-cn": "自适应不稳定模式:自动检测WiFi信号差��设备,更快重连(60秒vs5分钟),持久REST回退"
85
+ "en": "Internal cleanup. No user-facing changes.",
86
+ "de": "Interne Bereinigung. Keine User-sichtbaren Änderungen.",
87
+ "ru": "Внутренняя очистка. Без видимых изменений для пользователя.",
88
+ "pt": "Limpeza interna. Sem alterações visíveis para o usuário.",
89
+ "nl": "Interne opschoning. Geen wijzigingen voor de gebruiker.",
90
+ "fr": "Nettoyage interne. Aucune modification visible pour l'utilisateur.",
91
+ "it": "Pulizia interna. Nessuna modifica visibile all'utente.",
92
+ "es": "Limpieza interna. Sin cambios visibles para el usuario.",
93
+ "pl": "Wewnętrzne porządki. Brak zmian widocznych dla użytkownika.",
94
+ "uk": "Внутрішнє прибирання. Без помітних для користувача змін.",
95
+ "zh-cn": "内部清理。对用户无可见变化。"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -157,12 +157,12 @@
157
157
  },
158
158
  "dependencies": [
159
159
  {
160
- "js-controller": ">=6.0.11"
160
+ "js-controller": ">=7.0.7"
161
161
  }
162
162
  ],
163
163
  "globalDependencies": [
164
164
  {
165
- "admin": ">=7.6.20"
165
+ "admin": ">=7.7.22"
166
166
  }
167
167
  ]
168
168
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.homewizard",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "ioBroker adapter for HomeWizard Energy devices — real-time energy monitoring via API v2 with WebSocket push",
5
5
  "author": {
6
6
  "name": "krobi",
@@ -73,6 +73,8 @@
73
73
  "translate": "translate-adapter",
74
74
  "lint": "eslint",
75
75
  "lint:fix": "eslint --fix",
76
+ "format": "prettier --write .",
77
+ "format:check": "prettier --check .",
76
78
  "release": "release-script"
77
79
  },
78
80
  "nyc": {