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 +13 -26
- package/build/lib/state-manager.js.map +1 -1
- package/build/lib/websocket-client.js.map +2 -2
- package/io-package.json +82 -82
- package/package.json +3 -1
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 >=
|
|
34
|
-
- **ioBroker Admin >= 7.
|
|
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
|
-
-
|
|
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
|
-
-
|
|
178
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAsB;AACtB,oBAAyB;AACzB,oBAA8B;AAsBvB,MAAM,oBAAoB;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACT,KAAuB;AAAA,EACvB,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOpB,YAAY,IAAY,OAAe,WAAwB;AAC7D,SAAK,KAAK;AACV,SAAK,QAAQ;AACb,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,QAAQ;AAEb,UAAM,MAAM,SAAS,KAAK,EAAE;AAC5B,SAAK,UAAU,IAAI,MAAM,oBAAoB,GAAG,EAAE;AAElD,SAAK,KAAK,IAAI,UAAAA,QAAU,KAAK;AAAA,MAC3B,OAAO;AAAA,MACP,kBAAkB;AAAA,IACpB,CAAC;AAED,SAAK,GAAG,GAAG,QAAQ,MAAM;AACvB,WAAK,UAAU,IAAI,MAAM,cAAc,KAAK,EAAE,EAAE;AAAA,IAClD,CAAC;AAED,SAAK,GAAG,GAAG,WAAW,CAAC,QAA2B;AAChD,WAAK,cAAc,GAAG;AAAA,IACxB,CAAC;AAED,SAAK,GAAG,GAAG,SAAS,CAAC,MAAc,WAAmB;AACpD,WAAK,UAAU,IAAI,MAAM,cAAc,IAAI,IAAI,OAAO,SAAS,CAAC,EAAE;AAClE,WAAK,KAAK;AACV,UAAI,CAAC,KAAK,WAAW;AACnB,aAAK,UAAU,eAAe;AAAA,MAChC;AAAA,IACF,CAAC;AAED,SAAK,GAAG,GAAG,SAAS,CAAC,QAAe;AAClC,WAAK,UAAU,IAAI,MAAM,aAAa,IAAI,OAAO,EAAE;AAAA,IAErD,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,YAAY;AACjB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,IAAI,cAAuB;AAvF7B;AAwFI,aAAO,UAAK,OAAL,mBAAS,gBAAe,UAAAA,QAAU;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,cAAc,KAA8B;AAClD,UAAM,OAAO,OAAO,SAAS,GAAG,IAC5B,IAAI,SAAS,MAAM,IACnB,eAAe,cACb,OAAO,KAAK,GAAG,EAAE,SAAS,MAAM,IAChC,MAAM,QAAQ,GAAG,IACf,OAAO,OAAO,GAAG,EAAE,SAAS,MAAM,IAClC;AACR,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,IAAI;AAAA,IAC1B,QAAQ;AACN,WAAK,UAAU,IAAI,KAAK,oBAAoB,KAAK,UAAU,GAAG,GAAG,CAAC,EAAE;AACpE;AAAA,IACF;AAEA,QAAI,KAAC,6BAAc,MAAM,GAAG;AAC1B,WAAK,UAAU,IAAI,KAAK,0BAA0B,KAAK,UAAU,GAAG,GAAG,CAAC,EAAE;AAC1E;AAAA,IACF;AAEA,UAAM,OAAO,OAAO;AACpB,QAAI,OAAO,SAAS,UAAU;AAC5B,WAAK,UAAU,IAAI,KAAK,gCAAgC;AACxD;AAAA,IACF;AAEA,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,aAAK,UAAU,IAAI,MAAM,kCAAkC;AAC3D,aAAK,QAAQ,EAAE,MAAM,iBAAiB,MAAM,KAAK,MAAM,CAAC;AACxD;AAAA,MAEF,KAAK;AACH,aAAK,UAAU,IAAI,MAAM,2CAA2C;AACpE,aAAK,QAAQ,EAAE,MAAM,aAAa,MAAM,cAAc,CAAC;AACvD,aAAK,UAAU,YAAY;AAC3B;AAAA,MAEF,KAAK;AACH,gBAAI,6BAAc,OAAO,IAAI,GAAG;AAC9B,eAAK,UAAU,cAAc,OAAO,
|
|
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.
|
|
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": "
|
|
8
|
-
"de": "
|
|
9
|
-
"ru": "
|
|
10
|
-
"pt": "Limpeza
|
|
11
|
-
"nl": "
|
|
12
|
-
"fr": "Nettoyage
|
|
13
|
-
"it": "
|
|
14
|
-
"es": "Limpieza
|
|
15
|
-
"pl": "
|
|
16
|
-
"uk": "
|
|
17
|
-
"zh-cn": "
|
|
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": "
|
|
21
|
-
"de": "
|
|
22
|
-
"ru": "
|
|
23
|
-
"pt": "
|
|
24
|
-
"nl": "
|
|
25
|
-
"fr": "
|
|
26
|
-
"it": "
|
|
27
|
-
"es": "
|
|
28
|
-
"pl": "
|
|
29
|
-
"uk": "
|
|
30
|
-
"zh-cn": "
|
|
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": "
|
|
34
|
-
"de": "
|
|
35
|
-
"ru": "
|
|
36
|
-
"pt": "
|
|
37
|
-
"nl": "
|
|
38
|
-
"fr": "
|
|
39
|
-
"it": "
|
|
40
|
-
"es": "
|
|
41
|
-
"pl": "
|
|
42
|
-
"uk": "
|
|
43
|
-
"zh-cn": "
|
|
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": "
|
|
47
|
-
"de": "WebSocket- und REST-
|
|
48
|
-
"ru": "
|
|
49
|
-
"pt": "
|
|
50
|
-
"nl": "
|
|
51
|
-
"fr": "
|
|
52
|
-
"it": "
|
|
53
|
-
"es": "
|
|
54
|
-
"pl": "
|
|
55
|
-
"uk": "
|
|
56
|
-
"zh-cn": "
|
|
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
|
|
60
|
-
"de": "
|
|
61
|
-
"ru": "
|
|
62
|
-
"pt": "
|
|
63
|
-
"nl": "
|
|
64
|
-
"fr": "
|
|
65
|
-
"it": "
|
|
66
|
-
"es": "
|
|
67
|
-
"pl": "
|
|
68
|
-
"uk": "
|
|
69
|
-
"zh-cn": "
|
|
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": "
|
|
73
|
-
"de": "
|
|
74
|
-
"ru": "
|
|
75
|
-
"pt": "Limpeza
|
|
76
|
-
"nl": "
|
|
77
|
-
"fr": "Nettoyage
|
|
78
|
-
"it": "Pulizia
|
|
79
|
-
"es": "Limpieza
|
|
80
|
-
"pl": "
|
|
81
|
-
"uk": "
|
|
82
|
-
"zh-cn": "
|
|
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": ">=
|
|
160
|
+
"js-controller": ">=7.0.7"
|
|
161
161
|
}
|
|
162
162
|
],
|
|
163
163
|
"globalDependencies": [
|
|
164
164
|
{
|
|
165
|
-
"admin": ">=7.
|
|
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.
|
|
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": {
|