diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e3083..a7b9417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,24 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html ### Fixed +- **`fronius_smart_meter` 2.1.2** — `read_f32` returns nil on a failed / given-up register instead of fabricating 0; when total AC power (40098) is missing the poll emits neither meter nor meter metrics. Optional phase/energy fields omit nil rather than coercing to 0. Declares `read_only = true`. +- **`tesla_vehicle` 0.2.2** — `emit_last` sets `soc_fresh = false` so a cached SoC replay is not treated as a fresh observation by the FTW host. +- **`zuidwijk_p1` 1.1.1** — emit `p1_crc_errors` only after a valid frame (silence no longer keeps driver health alive on a CRC metric alone); skip the meter emit when both import and export power OBIS (`1.7.0` / `2.7.0`) are missing. Declares `read_only = true`; manifest `control` corrected to false. +- **`pixii` 2.1.4** — serial probe in `driver_poll` no longer gates on an unset `ok` local (the branch was permanently false, so `host.set_sn` never ran). +- **`easee_cloud` 1.3.1** — declares `config_secrets = { "password" }`; `driver_default_mode` now pauses charging and writes `dynamicChargerCurrent = 0` so FTW loss of steer stands the charger down. +- **`ferroamp` 2.1.2** — when `config.serial` is a non-empty string, call `host.set_sn`; MQTT telemetry still carries no hub serial, so identity otherwise falls back to the endpoint. + +### Changed + +- **Read-only hybrids / meters declare `read_only = true`:** `sonnen` 2.0.3 (plus no-op `driver_command` / `driver_default_mode`), `goodwe` 2.1.2, `growatt` 2.1.2, `sofar` 2.1.2, `kostal` 2.1.2, `sma` 2.1.2, `victron` 2.1.2, `fronius` 2.1.2, `pixii_pv` 0.3.1, `solis_string` 1.1.2, `tibber` 1.1.2 (also `http_hosts = { "api.tibber.com" }`). + +### Added + +- **`zaptec_cloud` 0.1.0** — Zaptec Go / Go 2 / Pro via Zaptec Cloud REST API, promoted from FTW testdata. `read_only = true` (EV commands accepted as no-op success so the planner does not mark failed): review before merge found the control path's `ev_set_current` computed the requested current from the phase count *before* applying a same-call phase change, and no live charger has exercised pause/resume/current end-to-end. Re-add control once that ordering is fixed and hardware-verified per `AGENTS.md`. +- **`tesla_wall_connector` 0.1.0** — Tesla Wall Connector Gen 3 local HTTP observation driver, promoted from FTW testdata. `read_only = true` (EV commands accepted as no-op success so the planner does not mark failed). + +### Fixed + - **The channel build now carries `config_secrets` into the generated header, so a box no longer masks nothing.** `tools/ftw_repository.py` prepends a generated `DRIVER = { … }` block ahead of the source's own, and FTW core parses only the first block it finds (`extractDriverBlock`). The generated block never copied `config_secrets`, so any driver declaring it — `myuplink`, `nibe_local`, `sonnen`, `tibber` — published a catalog entry with nothing to mask: `GET /api/config` on a box running the channel build returned `myuplink`'s `client_secret` and `refresh_token` in clear text, confirmed on a live installation. The source block further down still declared the field correctly, which is exactly why nothing caught this in a source-level review. `test_config_secrets_reach_the_generated_header` holds the generated header to it. Fixes #106. - **myuplink** 1.2.2, **nibe_local** 1.1.4, **sonnen** 2.0.2, **tibber** 1.1.1 — patch bump only. The `config_secrets` fix above changes these four drivers' published artifact bytes (the signed channel now emits their `config_secrets` list in the generated header), so the signed channel's own version rule requires a new version to publish it under. No Lua source changed. diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index 0a8f30a..4fbd2b7 100644 --- a/SUPPORT_STATUS.md +++ b/SUPPORT_STATUS.md @@ -46,36 +46,36 @@ Catalog source is not proof that a target can install or run a driver. | deye | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | easee | 1.0.4 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | easee | 1.0.4 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| easee_cloud | 1.3.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| easee_cloud | 1.3.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| easee_cloud | 1.3.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| easee_cloud | 1.3.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | esphome-dsmr | 1.0.3 | ftw-core | not_assessed | 1.0.2 | — | not_recorded | — | not_assessed | no | | esphome-dsmr | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | esphome_dsmr | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | esphome_dsmr | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | etrel | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | etrel | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| ferroamp | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| ferroamp | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| ferroamp | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| ferroamp | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | ferroamp_dc2_v2x | 2.1.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | ferroamp_dc2_v2x | 2.1.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | ferroamp_modbus | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | ferroamp_modbus | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | foxess_h3_smart | 0.9.5 | ftw-core | not_assessed | 0.9.5 | — | not_recorded | — | not_assessed | yes | | foxess_h3_smart | 0.9.5 | blixt-l1 | not_assessed | 0.9.5 | — | not_recorded | — | not_assessed | yes | -| fronius | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| fronius | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| fronius | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| fronius | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | fronius_api | 1.0.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | fronius_api | 1.0.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| fronius_smart_meter | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| fronius_smart_meter | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| fronius_smart_meter | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| fronius_smart_meter | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | goe | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | goe | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | goe_http | 1.0.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | goe_http | 1.0.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| goodwe | 2.1.1 | ftw-core | contract_passed | — | — | gw8kn_et_hk3000_required | — | profile_selected | no | -| goodwe | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| growatt | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| growatt | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| goodwe | 2.1.2 | ftw-core | contract_passed | — | — | gw8kn_et_hk3000_required | — | profile_selected | no | +| goodwe | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| growatt | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| growatt | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | hardybarth | 1.0.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | hardybarth | 1.0.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | heishamon | 0.5.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | @@ -90,8 +90,8 @@ Catalog source is not proof that a target can install or run a driver. | keba | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | konja-261-svk | 0.3.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | konja-261-svk | 0.3.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| kostal | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| kostal | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| kostal | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| kostal | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | kstar | 1.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | kstar | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | mennekes | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | @@ -114,10 +114,10 @@ Catalog source is not proof that a target can install or run a driver. | p1_hdlc | 2.0.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | p1_meter | 2.0.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | p1_meter | 2.0.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| pixii | 2.1.3 | ftw-core | staged | 1.2.5 | — | required | — | blocked | no | -| pixii | 2.1.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| pixii_pv | 0.3.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| pixii_pv | 0.3.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| pixii | 2.1.4 | ftw-core | staged | 1.2.5 | — | required | — | blocked | no | +| pixii | 2.1.4 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| pixii_pv | 0.3.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| pixii_pv | 0.3.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | saj | 1.2.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | saj | 1.2.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | schneider_meter | 1.0.5 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | @@ -132,14 +132,14 @@ Catalog source is not proof that a target can install or run a driver. | siemens_pac | 1.0.5 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | sigenergy | 1.1.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | sigenergy | 1.1.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| sma | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| sma | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| sma | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| sma | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | sma_pv | 1.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | sma_pv | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | socomec | 1.0.4 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | socomec | 1.0.4 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| sofar | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| sofar | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| sofar | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| sofar | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | solaredge | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | solaredge | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | solaredge_legacy | 0.3.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | @@ -152,20 +152,22 @@ Catalog source is not proof that a target can install or run a driver. | solinteg | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | solis | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | solis | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| solis_string | 1.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| solis_string | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| sonnen | 2.0.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| sonnen | 2.0.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| solis_string | 1.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| solis_string | 1.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| sonnen | 2.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| sonnen | 2.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | sungrow | 1.5.8 | ftw-core | observe_only_profile_blocked | 1.3.3 | 1.3.0 | exact_model_firmware_required | — | not_assessed | no | | sungrow | 1.5.8 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| tesla_vehicle | 0.2.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| tesla_vehicle | 0.2.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| tibber | 1.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| tibber | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| tesla_vehicle | 0.2.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| tesla_vehicle | 0.2.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| tesla_wall_connector | 0.1.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| tesla_wall_connector | 0.1.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| tibber | 1.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| tibber | 1.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | varta | 1.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | varta | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| victron | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| victron | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| victron | 2.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| victron | 2.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | victron_mqtt | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | victron_mqtt | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | wallbox | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | @@ -174,5 +176,7 @@ Catalog source is not proof that a target can install or run a driver. | zap | 3.1.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | zaptec | 1.0.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | zaptec | 1.0.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| zuidwijk_p1 | 1.1.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| zuidwijk_p1 | 1.1.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| zaptec_cloud | 0.1.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| zaptec_cloud | 0.1.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| zuidwijk_p1 | 1.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| zuidwijk_p1 | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | diff --git a/devices.yaml b/devices.yaml index 804b0b9..937502a 100644 --- a/devices.yaml +++ b/devices.yaml @@ -349,7 +349,7 @@ manufacturers: protocols: - protocol: http driver: "easee_cloud" - version: "1.3.0" + version: "1.3.1" ders: [ev] control: true firmware_versions: "" @@ -399,7 +399,7 @@ manufacturers: protocols: - protocol: mqtt driver: "ferroamp" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: true - protocol: modbus @@ -441,7 +441,7 @@ manufacturers: protocols: - protocol: modbus driver: "fronius" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -452,7 +452,7 @@ manufacturers: protocols: - protocol: modbus driver: "fronius" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -463,7 +463,7 @@ manufacturers: protocols: - protocol: modbus driver: "fronius" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -474,7 +474,7 @@ manufacturers: protocols: - protocol: modbus driver: "fronius" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -485,7 +485,7 @@ manufacturers: protocols: - protocol: modbus driver: "fronius" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -496,7 +496,7 @@ manufacturers: protocols: - protocol: modbus driver: "fronius_smart_meter" - version: "2.1.1" + version: "2.1.2" ders: [meter] control: false firmware_versions: "" @@ -518,7 +518,7 @@ manufacturers: protocols: - protocol: modbus driver: "fronius" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -531,7 +531,7 @@ manufacturers: protocols: - protocol: modbus driver: "solis_string" - version: "1.1.1" + version: "1.1.2" ders: [pv] control: false firmware_versions: "" @@ -544,7 +544,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -555,7 +555,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -566,7 +566,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -577,7 +577,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -588,7 +588,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -599,7 +599,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -873,7 +873,7 @@ manufacturers: protocols: - protocol: modbus driver: "kostal" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -884,7 +884,7 @@ manufacturers: protocols: - protocol: modbus driver: "kostal" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -895,7 +895,7 @@ manufacturers: protocols: - protocol: modbus driver: "kostal" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -906,7 +906,7 @@ manufacturers: protocols: - protocol: modbus driver: "kostal" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -1032,7 +1032,7 @@ manufacturers: protocols: - protocol: mqtt driver: "pixii_pv" - version: "0.3.0" + version: "0.3.1" ders: [pv, meter] control: false firmware_versions: "" @@ -1043,7 +1043,7 @@ manufacturers: protocols: - protocol: modbus driver: "pixii" - version: "2.1.3" + version: "2.1.4" ders: [battery, meter] control: false firmware_versions: "" @@ -1091,7 +1091,7 @@ manufacturers: protocols: - protocol: modbus driver: "sma" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -1113,7 +1113,7 @@ manufacturers: protocols: - protocol: modbus driver: "sma" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -1124,7 +1124,7 @@ manufacturers: protocols: - protocol: modbus driver: "sma" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -1135,7 +1135,7 @@ manufacturers: protocols: - protocol: modbus driver: "sma" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -1146,7 +1146,7 @@ manufacturers: protocols: - protocol: modbus driver: "sma" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -1157,7 +1157,7 @@ manufacturers: protocols: - protocol: modbus driver: "sma" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -1168,7 +1168,7 @@ manufacturers: protocols: - protocol: modbus driver: "sma" - version: "2.1.1" + version: "2.1.2" ders: [battery, meter, pv] control: false firmware_versions: "" @@ -1450,7 +1450,7 @@ manufacturers: protocols: - protocol: modbus driver: "sofar" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -1461,7 +1461,7 @@ manufacturers: protocols: - protocol: modbus driver: "sofar" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -1472,7 +1472,7 @@ manufacturers: protocols: - protocol: modbus driver: "sofar" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -1804,11 +1804,22 @@ manufacturers: protocols: - protocol: http driver: "tesla_vehicle" - version: "0.2.1" + version: "0.2.2" ders: [vehicle] control: true firmware_versions: "" notes: "Read-only vehicle SoC + charge limit via Tesla API-compatible HTTP endpoint (e.g. TeslaBLEProxy)." + - name: "Tesla Wall Connector" + variants: [Wall Connector Gen 3] + regions: [] + protocols: + - protocol: http + driver: "tesla_wall_connector" + version: "0.1.0" + ders: [ev] + control: false + firmware_versions: "" + notes: "Tesla Wall Connector Gen 3 on the LAN. Observation only; no local current setpoint." - name: "Tibber" model_families: - name: "Tibber Pulse" @@ -1817,7 +1828,7 @@ manufacturers: protocols: - protocol: http driver: "tibber" - version: "1.1.1" + version: "1.1.2" ders: [meter] control: false firmware_versions: "" @@ -1947,7 +1958,7 @@ manufacturers: protocols: - protocol: modbus driver: "victron" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false - protocol: mqtt @@ -1963,7 +1974,7 @@ manufacturers: protocols: - protocol: modbus driver: "victron" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false - protocol: mqtt @@ -1979,7 +1990,7 @@ manufacturers: protocols: - protocol: modbus driver: "victron" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false - protocol: mqtt @@ -1995,7 +2006,7 @@ manufacturers: protocols: - protocol: modbus driver: "victron" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false - protocol: mqtt @@ -2011,7 +2022,7 @@ manufacturers: protocols: - protocol: modbus driver: "victron" - version: "2.1.1" + version: "2.1.2" ders: [pv, battery, meter] control: false - protocol: mqtt @@ -2047,6 +2058,17 @@ manufacturers: control: false firmware_versions: "" notes: "Community driver, untested" + - name: "Zaptec Cloud" + variants: [Go, Go 2, Pro] + regions: [] + protocols: + - protocol: http + driver: "zaptec_cloud" + version: "0.1.0" + ders: [ev] + control: false + firmware_versions: "" + notes: "Zaptec Go / Go 2 / Pro via Zaptec Cloud REST API. Email + password; optional charger serial." - name: "Zuidwijk" model_families: - name: "Zuidwijk P1 Reader Ethernet" @@ -2055,9 +2077,9 @@ manufacturers: protocols: - protocol: modbus driver: "zuidwijk_p1" - version: "1.1.0" + version: "1.1.1" ders: [meter] - control: true + control: false firmware_versions: "" notes: "Dutch DSMR P1 smart-meter via Zuidwijk Serial-to-Ethernet bridge (raw TCP, port 23)." - name: "go-e" @@ -2092,7 +2114,7 @@ manufacturers: protocols: - protocol: http driver: "sonnen" - version: "2.0.2" + version: "2.0.3" ders: [battery, meter] control: false firmware_versions: "" diff --git a/drivers/lua/easee_cloud.lua b/drivers/lua/easee_cloud.lua index 5eb44a2..4db4761 100644 --- a/drivers/lua/easee_cloud.lua +++ b/drivers/lua/easee_cloud.lua @@ -25,7 +25,7 @@ DRIVER = { id = "easee-cloud", name = "Easee Cloud", manufacturer = "Easee", - version = "1.3.0", + version = "1.3.1", protocols = { "http" }, capabilities = { "ev" }, description = "Easee Home/Charge via Cloud REST API. No local protocol needed.", @@ -37,6 +37,7 @@ DRIVER = { verified_by = { "frahlg@homelab-rpi:2d", "erikarenhill@fortytwo:1d" }, verified_at = "2026-04-18", verification_notes = "Observations API + lifecycle commands exercised against an Easee Home charger. Session state, op_mode labels, charge/pause/resume all verified.", + config_secrets = { "password" }, } PROTOCOL = "http" @@ -814,7 +815,25 @@ function driver_command(action, power_w, cmd) end function driver_default_mode() - -- No-op — cloud charger manages itself. + -- Stand-down when FTW can no longer steer: pause and clamp offered + -- current to 0 A. Missing serial/auth is not an error — nothing to + -- release yet. + if not charger_serial or not email or not password then + return + end + if not ensure_auth(email, password) then + return + end + if post_command("/commands/pause_charging") then + paused_state = true + end + local err = write_setting(charger_serial, {dynamicChargerCurrent = 0}) + if err == nil then + last_amps_set = 0 + else + host.log("warn", "Easee: default_mode current clamp failed: " .. + redact_http_err(err)) + end end function driver_cleanup() diff --git a/drivers/lua/ferroamp.lua b/drivers/lua/ferroamp.lua index f7339ca..c0c71d0 100644 --- a/drivers/lua/ferroamp.lua +++ b/drivers/lua/ferroamp.lua @@ -8,7 +8,7 @@ DRIVER = { id = "ferroamp", name = "Ferroamp EnergyHub", manufacturer = "Ferroamp", - version = "1.0.0", + version = "2.1.2", protocols = { "mqtt" }, capabilities = { "meter", "pv", "battery" }, description = "Ferroamp EnergyHub with ESO battery + SSO solar strings (3-phase).", @@ -344,6 +344,11 @@ end function driver_init(config) host.set_make("Ferroamp") + -- MQTT telemetry does not carry a hub serial, so identity otherwise + -- falls back to the endpoint. Operators can pin one via config.serial. + if config and type(config.serial) == "string" and config.serial ~= "" then + host.set_sn(config.serial) + end -- Honour the `skip_battery` config knob if set — the driver stays -- otherwise unchanged, but host.emit("battery", …) is skipped so diff --git a/drivers/lua/fronius.lua b/drivers/lua/fronius.lua index ca497ef..c9e1d00 100644 --- a/drivers/lua/fronius.lua +++ b/drivers/lua/fronius.lua @@ -15,9 +15,10 @@ DRIVER = { id = "fronius", name = "Fronius GEN24", manufacturer = "Fronius", - version = "2.1.1", + version = "2.1.2", protocols = { "modbus" }, capabilities = { "pv", "battery" }, + read_only = true, description = "Fronius Symo / Primo GEN24 hybrid inverters via Modbus TCP (SunSpec).", homepage = "https://www.fronius.com", authors = { "FTW contributors" }, diff --git a/drivers/lua/fronius_smart_meter.lua b/drivers/lua/fronius_smart_meter.lua index 14d6e80..285a771 100644 --- a/drivers/lua/fronius_smart_meter.lua +++ b/drivers/lua/fronius_smart_meter.lua @@ -14,9 +14,10 @@ DRIVER = { id = "fronius-smart-meter", name = "Fronius Smart Meter", manufacturer = "Fronius", - version = "2.1.1", + version = "2.1.2", protocols = { "modbus" }, capabilities = { "meter" }, + read_only = true, description = "Fronius Smart Meter three-phase energy meter via Modbus TCP (SunSpec).", homepage = "https://www.fronius.com", authors = { "FTW contributors" }, @@ -110,15 +111,14 @@ local function probe_read(addr, count, kind) end -- Helper: read a contiguous F32 BE pair at `addr` and return the decoded --- float. On Modbus error, returns 0 so the poll cycle still emits a --- well-shaped table. Every read in this driver goes through here, so --- bounding it bounds the whole driver. +-- float. On Modbus error / give-up, returns nil so callers can omit the +-- field or skip the emit rather than publishing a fabricated 0 W. local function read_f32(addr) local regs = probe_read(addr, 2, "holding") if regs then return decode_f32_be(regs[1], regs[2]) end - return 0 + return nil end ---------------------------------------------------------------------------- @@ -136,57 +136,53 @@ end ---------------------------------------------------------------------------- function driver_poll() - -- Per-phase current (A) + -- Total AC power (W) — Fronius: positive = import, matches site convention. + -- Without it there is no trustworthy site-meter reading; do not emit a + -- fabricated 0 W after the register has been given up. + local total_w = read_f32(40098) + if total_w == nil then + return 5000 + end + + -- Optional phase / energy fields: omit nil rather than coercing to 0. local l1_a = read_f32(40074) local l2_a = read_f32(40076) local l3_a = read_f32(40078) - - -- Per-phase voltage (V) local l1_v = read_f32(40082) local l2_v = read_f32(40084) local l3_v = read_f32(40086) - - -- Grid frequency (Hz) local hz = read_f32(40096) - - -- Total AC power (W) — Fronius: positive = import, matches site convention - local total_w = read_f32(40098) - - -- Per-phase AC power (W) local l1_w = read_f32(40100) local l2_w = read_f32(40102) local l3_w = read_f32(40104) - - -- Lifetime energy counters (Wh) local export_wh = read_f32(40130) local import_wh = read_f32(40138) - host.emit("meter", { - w = total_w, - l1_w = l1_w, - l2_w = l2_w, - l3_w = l3_w, - l1_v = l1_v, - l2_v = l2_v, - l3_v = l3_v, - l1_a = l1_a, - l2_a = l2_a, - l3_a = l3_a, - hz = hz, - import_wh = import_wh, - export_wh = export_wh, - }) - -- Diagnostics: long-format TS DB - host.emit_metric("meter_l1_w", l1_w) - host.emit_metric("meter_l2_w", l2_w) - host.emit_metric("meter_l3_w", l3_w) - host.emit_metric("meter_l1_v", l1_v) - host.emit_metric("meter_l2_v", l2_v) - host.emit_metric("meter_l3_v", l3_v) - host.emit_metric("meter_l1_a", l1_a) - host.emit_metric("meter_l2_a", l2_a) - host.emit_metric("meter_l3_a", l3_a) - host.emit_metric("grid_hz", hz) + local meter = { w = total_w } + if l1_w ~= nil then meter.l1_w = l1_w end + if l2_w ~= nil then meter.l2_w = l2_w end + if l3_w ~= nil then meter.l3_w = l3_w end + if l1_v ~= nil then meter.l1_v = l1_v end + if l2_v ~= nil then meter.l2_v = l2_v end + if l3_v ~= nil then meter.l3_v = l3_v end + if l1_a ~= nil then meter.l1_a = l1_a end + if l2_a ~= nil then meter.l2_a = l2_a end + if l3_a ~= nil then meter.l3_a = l3_a end + if hz ~= nil then meter.hz = hz end + if import_wh ~= nil then meter.import_wh = import_wh end + if export_wh ~= nil then meter.export_wh = export_wh end + + host.emit("meter", meter) + if l1_w ~= nil then host.emit_metric("meter_l1_w", l1_w) end + if l2_w ~= nil then host.emit_metric("meter_l2_w", l2_w) end + if l3_w ~= nil then host.emit_metric("meter_l3_w", l3_w) end + if l1_v ~= nil then host.emit_metric("meter_l1_v", l1_v) end + if l2_v ~= nil then host.emit_metric("meter_l2_v", l2_v) end + if l3_v ~= nil then host.emit_metric("meter_l3_v", l3_v) end + if l1_a ~= nil then host.emit_metric("meter_l1_a", l1_a) end + if l2_a ~= nil then host.emit_metric("meter_l2_a", l2_a) end + if l3_a ~= nil then host.emit_metric("meter_l3_a", l3_a) end + if hz ~= nil then host.emit_metric("grid_hz", hz) end return 5000 end diff --git a/drivers/lua/goodwe.lua b/drivers/lua/goodwe.lua index b9f106a..d7b0e03 100644 --- a/drivers/lua/goodwe.lua +++ b/drivers/lua/goodwe.lua @@ -13,9 +13,10 @@ DRIVER = { id = "goodwe", name = "GoodWe hybrid inverter", manufacturer = "GoodWe", - version = "2.1.1", + version = "2.1.2", protocols = { "modbus" }, capabilities = { "meter", "pv", "battery" }, + read_only = true, description = "GoodWe ET-Plus / EH series hybrid inverters via Modbus TCP.", homepage = "https://en.goodwe.com", authors = { "FTW contributors" }, diff --git a/drivers/lua/growatt.lua b/drivers/lua/growatt.lua index 3c96cd9..afc8789 100644 --- a/drivers/lua/growatt.lua +++ b/drivers/lua/growatt.lua @@ -21,9 +21,10 @@ DRIVER = { id = "growatt", name = "Growatt hybrid inverter", manufacturer = "Growatt", - version = "2.1.1", + version = "2.1.2", protocols = { "modbus" }, capabilities = { "meter", "pv", "battery" }, + read_only = true, description = "Growatt SPH / MOD hybrid inverters via Modbus TCP.", homepage = "https://www.growatt.com", authors = { "FTW contributors" }, diff --git a/drivers/lua/kostal.lua b/drivers/lua/kostal.lua index 1c86184..df2763e 100644 --- a/drivers/lua/kostal.lua +++ b/drivers/lua/kostal.lua @@ -10,9 +10,10 @@ DRIVER = { id = "kostal", name = "Kostal Plenticore", manufacturer = "Kostal", - version = "2.1.1", + version = "2.1.2", protocols = { "modbus" }, capabilities = { "meter", "pv", "battery" }, + read_only = true, description = "Kostal Plenticore Plus and Piko IQ via Modbus TCP (SunSpec plus Kostal custom map).", homepage = "https://www.kostal-solar-electric.com", authors = { "FTW contributors" }, diff --git a/drivers/lua/pixii.lua b/drivers/lua/pixii.lua index 041f7a9..568ff88 100644 --- a/drivers/lua/pixii.lua +++ b/drivers/lua/pixii.lua @@ -29,7 +29,7 @@ DRIVER = { id = "pixii", name = "Pixii PowerShaper", manufacturer = "Pixii", - version = "2.1.3", + version = "2.1.4", protocols = { "modbus" }, capabilities = { "battery", "meter" }, description = "Pixii PowerShaper commercial battery storage via Modbus TCP.", @@ -352,7 +352,7 @@ function driver_poll() -- the common block → absolute 40052, 16 regs ASCII). if not sn_read then local sn_regs = probe_read(40052, 16, "holding") - if ok and sn_regs then + if sn_regs then local sn = decode_ascii(sn_regs, 16) if string.len(sn) > 0 then host.set_sn(sn) diff --git a/drivers/lua/pixii_pv.lua b/drivers/lua/pixii_pv.lua index 05f8456..3bb6466 100644 --- a/drivers/lua/pixii_pv.lua +++ b/drivers/lua/pixii_pv.lua @@ -14,9 +14,10 @@ DRIVER = { id = "pixii-pv", name = "Pixii PowerShaper (PV + meter)", manufacturer = "Pixii", - version = "0.2.0", + version = "0.3.1", protocols = { "mqtt" }, capabilities = { "pv", "meter" }, + read_only = true, description = "Read-only Pixii telemetry: PV (external CTs) + grid meter (meter_w). Battery / SoC comes from the Modbus driver, not this one.", homepage = "https://pixii.com", authors = { "FTW contributors" }, @@ -114,13 +115,13 @@ function driver_poll() local w1 = tonumber(pv_payload.meter_w1) or 0 local w2 = tonumber(pv_payload.meter_w2) or 0 local w3 = tonumber(pv_payload.meter_w3) or 0 - local pv_w = -(w1 + w2 + w3) - local pv = { w = pv_w } + local pv = {} + pv.w = -(w1 + w2 + w3) local kwh_imp = tonumber(pv_payload.meter_kwh_imp) if kwh_imp then pv.generation_wh = kwh_imp * 1000.0 end host.emit("pv", pv) - host.emit_metric("pv_w_total", pv_w) + host.emit_metric("pv_w_total", pv.w) host.emit_metric("pv_l1_w", -w1) host.emit_metric("pv_l2_w", -w2) host.emit_metric("pv_l3_w", -w3) diff --git a/drivers/lua/sma.lua b/drivers/lua/sma.lua index c6a4c65..c33664c 100644 --- a/drivers/lua/sma.lua +++ b/drivers/lua/sma.lua @@ -13,9 +13,10 @@ DRIVER = { id = "sma", name = "SMA hybrid inverter", manufacturer = "SMA", - version = "2.1.1", + version = "2.1.2", protocols = { "modbus" }, capabilities = { "meter", "pv", "battery" }, + read_only = true, description = "SMA Sunny Tripower / Sunny Boy Storage via Modbus TCP (SunSpec).", homepage = "https://www.sma.de", authors = { "FTW contributors" }, diff --git a/drivers/lua/sofar.lua b/drivers/lua/sofar.lua index 07a77a1..58cf870 100644 --- a/drivers/lua/sofar.lua +++ b/drivers/lua/sofar.lua @@ -26,9 +26,10 @@ DRIVER = { id = "sofar", name = "Sofar hybrid inverter", manufacturer = "Sofar Solar", - version = "2.1.1", + version = "2.1.2", protocols = { "modbus" }, capabilities = { "meter", "pv", "battery" }, + read_only = true, description = "Sofar Solar HYD-ES / HYD-EP hybrid inverters via Modbus TCP.", homepage = "https://www.sofarsolar.com", authors = { "FTW contributors" }, diff --git a/drivers/lua/solis_string.lua b/drivers/lua/solis_string.lua index 4a17a38..eb573b0 100644 --- a/drivers/lua/solis_string.lua +++ b/drivers/lua/solis_string.lua @@ -11,9 +11,10 @@ DRIVER = { id = "solis-string", name = "Solis string inverter", manufacturer = "Ginlong Solis", - version = "1.1.1", + version = "1.1.2", protocols = { "modbus" }, capabilities = { "pv" }, + read_only = true, description = "Solis non-hybrid PV inverters via Modbus TCP.", homepage = "https://www.ginlong.com", authors = { "FTW contributors" }, diff --git a/drivers/lua/sonnen.lua b/drivers/lua/sonnen.lua index 4f1bade..003cec3 100644 --- a/drivers/lua/sonnen.lua +++ b/drivers/lua/sonnen.lua @@ -30,9 +30,10 @@ DRIVER = { id = "sonnen", name = "sonnenBatterie (local API)", manufacturer = "sonnen", - version = "1.0.0", + version = "2.0.3", protocols = { "http" }, capabilities = { "battery" }, + read_only = true, description = "sonnenBatterie local JSON API v2: SoC + charge/discharge power. Read-only.", homepage = "https://sonnen.de", authors = { "FTW contributors" }, @@ -87,6 +88,19 @@ local function in_backoff() return (host.millis() - last_attempt) < backoff_ms end +local function safe_http_get(url, headers) + local ok, resp, err = pcall(host.http_get, url, headers) + if not ok then return nil, tostring(resp) end + return resp, err +end + +local function safe_json_decode(body) + if body == nil then return nil end + local ok, data = pcall(host.json_decode, body) + if not ok then return nil end + return data +end + -- clamp drops obviously-broken values (NaN, sentinel huge numbers from a -- partial JSON parse) so a single bad poll doesn't poison the battery -- model with a 2 GW reading. @@ -138,7 +152,7 @@ function driver_poll() local url = base_url() .. "/api/v2/latestdata" local headers = { ["Auth-Token"] = api_token } - local body, err = host.http_get(url, headers) + local body, err = safe_http_get(url, headers) if err then bump_backoff() @@ -147,7 +161,7 @@ function driver_poll() return 1000 end - local data = host.json_decode(body) + local data = safe_json_decode(body) if not data then bump_backoff() host.log("warn", "sonnen: JSON decode failed") @@ -189,6 +203,13 @@ function driver_poll() return 1000 end +function driver_command(...) + return false +end + +function driver_default_mode() +end + function driver_cleanup() clear_backoff() end diff --git a/drivers/lua/tesla_vehicle.lua b/drivers/lua/tesla_vehicle.lua index c81a344..ffe9ef7 100644 --- a/drivers/lua/tesla_vehicle.lua +++ b/drivers/lua/tesla_vehicle.lua @@ -29,7 +29,7 @@ DRIVER = { id = "tesla-vehicle", name = "Tesla Vehicle (BLE Proxy)", manufacturer = "Tesla", - version = "0.1.0", + version = "0.2.2", protocols = { "http" }, capabilities = { "vehicle" }, description = "Read-only vehicle SoC + charge limit via Tesla API-compatible HTTP endpoint (e.g. TeslaBLEProxy).", @@ -131,6 +131,25 @@ local function safe_http_err(err) return tostring(err):match("^(HTTP %d+)") or "request failed" end +local function safe_http_get(url, headers) + local ok, resp, err = pcall(host.http_get, url, headers) + if not ok then return nil, tostring(resp) end + return resp, err +end + +local function safe_http_post(url, body, headers) + local ok, resp, err = pcall(host.http_post, url, body, headers) + if not ok then return nil, tostring(resp) end + return resp, err +end + +local function safe_json_decode(body) + if body == nil then return nil end + local ok, data = pcall(host.json_decode, body) + if not ok then return nil, tostring(data) end + return data, nil +end + function driver_init(config) if not config then host.log("error", "tesla: config required (ip + vin)") @@ -201,6 +220,9 @@ local function emit_last() charge_amps = last.charge_amps, charger_actual_current = last.charger_actual_current, stale = stale, + -- Cached replay is not a fresh observation. FTW treats a missing + -- soc_fresh as fresh, so mark it false whenever we re-emit last. + soc_fresh = false, }) end @@ -293,7 +315,7 @@ function driver_poll() -- first return is the body directly, NOT a table with .body. The -- earlier tesla_vehicle iterations treated it as a table and got -- length 0 on every poll, which silently emit_last()'d the driver. - local body, err = host.http_get(url, auth_headers()) + local body, err = safe_http_get(url, auth_headers()) if err ~= nil then -- 503 "Command Disallowed" or 408 timeouts mean the proxy's -- BLE radio is busy (usually because we just poked or the car @@ -335,7 +357,7 @@ function driver_poll() return steady_ms end - local decoded, derr = host.json_decode(body) + local decoded, derr = safe_json_decode(body) if derr or not decoded then host.log("warn", "tesla: json decode failed: " .. tostring(derr)) emit_last() @@ -443,7 +465,7 @@ function driver_command(action, _, _) return false end local url = base_url .. "/api/1/vehicles/" .. vin .. "/command/wake_up" - local body, err = host.http_post(url, "{}", auth_headers()) + local body, err = safe_http_post(url, "{}", auth_headers()) if err then local es = tostring(err) if es:match("HTTP 503") or es:match("HTTP 408") then @@ -477,7 +499,7 @@ function driver_command(action, _, _) -- Empty JSON object body — TeslaBLEProxy's command endpoints -- accept GET-ish POSTs; some Tesla SDKs send `{}` for parity -- with the cloud API. Either form works on the proxy. - local body, err = host.http_post(url, "{}", auth_headers()) + local body, err = safe_http_post(url, "{}", auth_headers()) if err then local es = tostring(err) -- 503 / "Command Disallowed" means the proxy's BLE radio is diff --git a/drivers/lua/tesla_wall_connector.lua b/drivers/lua/tesla_wall_connector.lua new file mode 100644 index 0000000..28c1a3e --- /dev/null +++ b/drivers/lua/tesla_wall_connector.lua @@ -0,0 +1,261 @@ +-- Tesla Wall Connector Gen 3 (local HTTP). +-- +-- Protocol: unauthenticated GET on the LAN. +-- /api/1/vitals live state, currents, voltages, session energy +-- /api/1/version serial / part / firmware (once) +-- Canonical copy belongs in srcfl/device-drivers as tesla_wall_connector.lua; +-- this testdata file is the in-tree source until that pin lands. Operators +-- can drop the same file in the user-drivers directory in the meantime. +-- +-- The wall connector has no local current setpoint. ev_set_current, +-- ev_pause and ev_resume are accepted so the planner does not mark the +-- driver failed; they do not write hardware. Steer a Tesla through +-- tesla_vehicle. Sign convention: positive W is charging. +-- +-- Config: +-- host LAN hostname or IP (no scheme) +-- base_url optional origin override (tests) +-- split_phase true for North-American split-phase power (grid_v × vehicle_a) +-- +-- verification_status is experimental until a live Gen 3 has been +-- exercised end-to-end. + +DRIVER = { + host_api_min = 1, + host_api_max = 1, + id = "tesla-wall-connector", + name = "Tesla Wall Connector", + manufacturer = "Tesla", + version = "0.1.0", + protocols = { "http" }, + capabilities = { "ev" }, + read_only = true, + description = "Tesla Wall Connector Gen 3 on the LAN. Observation only; no cloud account.", + homepage = "https://www.tesla.com/support/charging/wall-connector", + authors = { "FTW contributors" }, + tested_models = { "Wall Connector Gen 3" }, + verification_status = "experimental", +} + +PROTOCOL = "http" + +-- EVSE state codes documented by the ioBroker tesla-wallconnector3 adapter +-- (public field notes on /api/1/vitals). 3, 5, 7, 12 are unused. +local EVSE_IDLE = 1 +local EVSE_CONNECTED = 2 +local EVSE_READY = 4 +local EVSE_HANDSHAKE = 6 +local EVSE_FINISHED = 8 +local EVSE_WAIT_CAR = 9 +local EVSE_CHARGE_LOW = 10 +local EVSE_CHARGE_FULL = 11 + +local BASE_URL = nil +local split_phase = false +local warned_readonly = false + +local function as_number(v) + if type(v) == "number" then return v end + if v == nil then return nil end + return tonumber(v) +end + +local function pick(t, ...) + if type(t) ~= "table" then return nil end + for i = 1, select("#", ...) do + local v = t[select(i, ...)] + if v ~= nil then return v end + end + return nil +end + +-- Some Tesla firmware emits bare `nan` or drops the closing brace. +-- Repair before json_decode so a single bad sample does not stall poll. +local function repair_json(raw) + if raw == nil then return "" end + raw = tostring(raw) + raw = raw:gsub("[Nn][Aa][Nn]", "null") + local opens = 0 + local closes = 0 + for i = 1, #raw do + local c = raw:sub(i, i) + if c == "{" then opens = opens + 1 + elseif c == "}" then closes = closes + 1 + end + end + while closes < opens do + raw = raw .. "}" + closes = closes + 1 + end + return raw +end + +local function decode_json(raw) + if raw == nil or raw == "" then return nil, "empty body" end + local ok, data = pcall(host.json_decode, repair_json(raw)) + if not ok then return nil, tostring(data) end + if data == nil then return nil, "decode failed" end + return data, nil +end + +local function safe_http_get(url, headers) + local ok, resp, err = pcall(host.http_get, url, headers) + if not ok then return nil, tostring(resp) end + return resp, err +end + +local function origin_from_config(config) + if config.base_url and tostring(config.base_url) ~= "" then + return tostring(config.base_url):gsub("/$", "") + end + local host = config.host or config.ip + if not host or host == "" then return nil end + host = tostring(host) + if host:find("://", 1, true) then + return host:gsub("/$", "") + end + -- Optional config.port; scheme-only URL uses HTTP default 80. + local port = tonumber(config.port) + if port and port > 0 and port ~= 80 then + return "http://" .. host .. ":" .. tostring(port) + end + return "http://" .. host +end + +local function phase_power(v, a) + v = as_number(v) or 0 + a = as_number(a) or 0 + -- Ghost voltages (~1–3 V) appear on unused legs; ignore them. + if v < 50 then return 0 end + if a < 0 then a = 0 end + return v * a +end + +local function charge_power(vitals) + if split_phase then + local w = (as_number(vitals.grid_v) or 0) * (as_number(vitals.vehicle_current_a) or 0) + if w < 0 then w = 0 end + return w + end + local w = phase_power(vitals.voltageA_v, vitals.currentA_a) + + phase_power(vitals.voltageB_v, vitals.currentB_a) + + phase_power(vitals.voltageC_v, vitals.currentC_a) + if w <= 0 then + local fallback = (as_number(vitals.grid_v) or 0) * (as_number(vitals.vehicle_current_a) or 0) + if fallback > 0 then w = fallback end + end + return w +end + +local function active_phases(vitals) + local n = 0 + if phase_power(vitals.voltageA_v, 1) > 0 then n = n + 1 end + if phase_power(vitals.voltageB_v, 1) > 0 then n = n + 1 end + if phase_power(vitals.voltageC_v, 1) > 0 then n = n + 1 end + if n < 1 then n = 1 end + return n +end + +local function is_charging(state, vitals) + if state == EVSE_CHARGE_LOW or state == EVSE_CHARGE_FULL then return true end + return vitals.contactor_closed == true +end + +local function is_connected(state, vitals) + if vitals.vehicle_connected == true then return true end + return state == EVSE_CONNECTED or state == EVSE_READY + or state == EVSE_HANDSHAKE or state == EVSE_FINISHED + or state == EVSE_WAIT_CAR or is_charging(state, vitals) +end + +function driver_init(config) + host.set_make("Tesla") + host.set_model("Wall Connector Gen 3") + config = config or {} + BASE_URL = origin_from_config(config) + if not BASE_URL then + error("Tesla Wall Connector: host or base_url required") + end + if config.split_phase == true or config.split_phase == "true" then + split_phase = true + end + + local raw, err = safe_http_get(BASE_URL .. "/api/1/version") + if not err then + local ver = decode_json(raw) + if ver then + local sn = pick(ver, "serial_number", "serialNumber") + if sn and sn ~= "" then host.set_sn(tostring(sn)) end + local part = pick(ver, "part_number", "partNumber") + if part and part ~= "" then + host.set_model("Wall Connector " .. tostring(part)) + end + end + end + host.log("info", "Tesla Wall Connector: polling " .. BASE_URL) +end + +function driver_poll() + if not BASE_URL then return 10000 end + local raw, err = safe_http_get(BASE_URL .. "/api/1/vitals") + if err then + host.log("warn", "Tesla Wall Connector: vitals failed") + return 10000 + end + local vitals, derr = decode_json(raw) + if derr or type(vitals) ~= "table" then + host.log("warn", "Tesla Wall Connector: vitals decode failed") + return 10000 + end + + local state = as_number(vitals.evse_state) or EVSE_IDLE + local power_w = charge_power(vitals) + local session_wh = as_number(vitals.session_energy_wh) or 0 + local connected = is_connected(state, vitals) + local charging = is_charging(state, vitals) + local phases = active_phases(vitals) + + host.emit("ev", { + w = power_w, + connected = connected, + charging = charging, + session_wh = session_wh, + phases = phases, + evse_state = state, + }) + + local i1 = as_number(vitals.currentA_a) + if i1 then host.emit_metric("ev_l1_a", i1) end + local v1 = as_number(vitals.voltageA_v) + if v1 then host.emit_metric("ev_l1_v", v1) end + host.emit_metric("ev_power_w", power_w) + host.emit_metric("evse_state", state) + + return 5000 +end + +function driver_command(action, power_w, cmd) + if action == "init" or action == "deinit" then + return true + end + if action == "ev_set_current" or action == "ev_pause" + or action == "ev_resume" or action == "ev_start" then + if not warned_readonly then + host.log("warn", + "Tesla Wall Connector has no local current setpoint; " .. + "steer a Tesla through tesla_vehicle") + warned_readonly = true + end + return true + end + host.log("warn", "Tesla Wall Connector: unknown action " .. tostring(action)) + return false +end + +function driver_default_mode() + -- Observation-only: the box keeps charging at the car's last request. +end + +function driver_cleanup() + BASE_URL = nil +end diff --git a/drivers/lua/tibber.lua b/drivers/lua/tibber.lua index c8914ff..8247e09 100644 --- a/drivers/lua/tibber.lua +++ b/drivers/lua/tibber.lua @@ -34,9 +34,11 @@ DRIVER = { id = "tibber", name = "Tibber Pulse", manufacturer = "Tibber", - version = "1.0.0", + version = "1.1.2", protocols = { "websocket", "http" }, capabilities = { "meter" }, + read_only = true, + http_hosts = { "api.tibber.com" }, description = "Tibber Pulse grid meter via GraphQL-transport-ws liveMeasurement stream.", homepage = "https://tibber.com", authors = { "FTW contributors" }, diff --git a/drivers/lua/victron.lua b/drivers/lua/victron.lua index 847c5e2..893cc1d 100644 --- a/drivers/lua/victron.lua +++ b/drivers/lua/victron.lua @@ -8,9 +8,10 @@ DRIVER = { id = "victron", name = "Victron Energy GX", manufacturer = "Victron Energy", - version = "2.1.1", + version = "2.1.2", protocols = { "modbus" }, capabilities = { "meter", "pv", "battery" }, + read_only = true, description = "Victron Cerbo GX / Venus GX monitoring via Modbus TCP.", homepage = "https://www.victronenergy.com", authors = { "FTW contributors" }, diff --git a/drivers/lua/zaptec_cloud.lua b/drivers/lua/zaptec_cloud.lua new file mode 100644 index 0000000..12f1e27 --- /dev/null +++ b/drivers/lua/zaptec_cloud.lua @@ -0,0 +1,348 @@ +-- Zaptec Cloud EV charger driver (Go / Go 2 / Pro). +-- +-- Protocol: HTTPS against the Zaptec Cloud REST API (api.zaptec.com). +-- Canonical copy belongs in srcfl/device-drivers as zaptec_cloud.lua; +-- this testdata file is the in-tree source until that pin lands. Operators +-- can drop the same file in the user-drivers directory in the meantime. +-- +-- Auth: POST /oauth/token (OAuth2 password grant, form-encoded) +-- Telemetry: GET /api/chargers/{id}/state +-- +-- Sign convention: positive W is charging (power into the vehicle / site load). +-- +-- This driver is read-only for now. A control path (POST .../update for +-- current/phases, POST .../sendCommand/506|507 for pause/resume) was +-- reviewed before merge and held back per AGENTS.md ("New drivers start +-- read-only" / control needs HIL acceptance): no live charger has exercised +-- it end-to-end, and the reviewed version also computed the requested +-- current from the phase count *before* applying a same-call phase change, +-- so a 1<->3 phase switch would have offered roughly 3x/3x-under the +-- intended power for one command. Re-add control in a follow-up once that +-- ordering is fixed and a real Zaptec unit has verified pause/resume/current. +-- +-- Config: +-- email / username Zaptec account email +-- password Zaptec account password (secret) +-- serial optional charger Id (UUID) or SerialNo +-- base_url optional origin override (tests / staging) +-- +-- verification_status is experimental until a live charger has been +-- exercised end-to-end. + +DRIVER = { + host_api_min = 1, + host_api_max = 1, + id = "zaptec-cloud", + name = "Zaptec Cloud", + manufacturer = "Zaptec", + version = "0.1.0", + protocols = { "http" }, + capabilities = { "ev" }, + read_only = true, + description = "Zaptec Go / Go 2 / Pro via Zaptec Cloud. Email + password; optional charger serial.", + homepage = "https://zaptec.com", + http_hosts = { "api.zaptec.com" }, + authors = { "FTW contributors" }, + tested_models = { "Go", "Go 2", "Pro" }, + verification_status = "experimental", + config_secrets = { "password" }, +} + +PROTOCOL = "http" + +local BASE_URL = "https://api.zaptec.com" + +-- Observation IDs from the Zaptec Cloud charger state document. +local OBS_VOLTAGE_L1 = 501 +local OBS_CURRENT_L1 = 507 +local OBS_MAX_CURRENT = 510 +local OBS_ACTIVE_PHASES = 512 +local OBS_CHARGE_POWER = 513 +local OBS_SESSION_ENERGY = 553 +local OBS_OP_MODE = 710 + +-- Operating modes (observation 710). +local OP_DISCONNECTED = 1 +local OP_CONNECTED = 2 +local OP_CHARGING = 3 +local OP_FINISHED = 5 + +local email = nil +local password = nil +local serial_want = nil +local charger_id = nil +local phases = 3 +local access_token = nil +local refresh_token = nil +local token_expiry = 0 +local warned_readonly = false + +local function pick(t, ...) + if type(t) ~= "table" then return nil end + for i = 1, select("#", ...) do + local v = t[select(i, ...)] + if v ~= nil then return v end + end + return nil +end + +local function as_number(v) + if type(v) == "number" then return v end + if v == nil then return nil end + return tonumber(v) +end + +-- Keep HTTP error strings to a status prefix so a 4xx body that echoes +-- credentials or a bearer token never lands in the driver log. +local function redact_http_err(err) + if err == nil then return "request failed" end + local s = tostring(err) + return s:match("^(HTTP %d+)") or "request failed" +end + +local function url_encode(s) + s = tostring(s or "") + return (s:gsub("[^%w%-%.%_%~]", function(c) + return string.format("%%%02X", string.byte(c)) + end)) +end + +local function auth_headers() + return { Authorization = "Bearer " .. (access_token or "") } +end + +local function decode_json(raw) + if raw == nil or raw == "" then return nil, "empty body" end + local ok, data = pcall(host.json_decode, raw) + if not ok then return nil, tostring(data) end + if data == nil then return nil, "decode failed" end + return data, nil +end + +local function safe_http_get(url, headers) + local ok, resp, err = pcall(host.http_get, url, headers) + if not ok then return nil, tostring(resp) end + return resp, err +end + +local function login() + local body = "grant_type=password" + .. "&username=" .. url_encode(email) + .. "&password=" .. url_encode(password) + local resp, err = host.http_post( + BASE_URL .. "/oauth/token", + body, + { ["Content-Type"] = "application/x-www-form-urlencoded" }) + if err then + return false, err + end + local data, derr = decode_json(resp) + if derr or not data then + return false, derr or "invalid JSON" + end + local token = pick(data, "access_token", "accessToken") + if not token or token == "" then + return false, "no access_token" + end + access_token = token + refresh_token = pick(data, "refresh_token", "refreshToken") or refresh_token + local expires_in = as_number(pick(data, "expires_in", "expiresIn")) or 3600 + token_expiry = host.millis() + (expires_in * 1000) - 60000 + host.log("info", "Zaptec: logged in") + return true +end + +local function refresh() + if not refresh_token or refresh_token == "" then return false end + local body = "grant_type=refresh_token" + .. "&refresh_token=" .. url_encode(refresh_token) + local resp, err = host.http_post( + BASE_URL .. "/oauth/token", + body, + { ["Content-Type"] = "application/x-www-form-urlencoded" }) + if err then + host.log("warn", "Zaptec token refresh failed: " .. redact_http_err(err)) + return false + end + local data, derr = decode_json(resp) + if derr or not data then return false end + local token = pick(data, "access_token", "accessToken") + if not token or token == "" then return false end + access_token = token + local rotated = pick(data, "refresh_token", "refreshToken") + if rotated and rotated ~= "" then refresh_token = rotated end + local expires_in = as_number(pick(data, "expires_in", "expiresIn")) or 3600 + token_expiry = host.millis() + (expires_in * 1000) - 60000 + return true +end + +local function ensure_auth() + if access_token and host.millis() < token_expiry then return true end + if refresh() then return true end + local ok = login() + return ok +end + +local function charger_rows(data) + if type(data) ~= "table" then return nil end + local rows = pick(data, "Data", "data") + if type(rows) == "table" then return rows end + if data[1] ~= nil then return data end + return nil +end + +local function charger_matches(ch, want) + if not want or want == "" then return true end + local id = tostring(pick(ch, "Id", "id") or "") + local serial = tostring(pick(ch, "SerialNo", "serialNo", "Serial") or "") + local device = tostring(pick(ch, "DeviceId", "deviceId") or "") + return id == want or serial == want or device == want +end + +local function resolve_charger() + local resp, err = safe_http_get(BASE_URL .. "/api/chargers", auth_headers()) + if err then return nil, err end + local data, derr = decode_json(resp) + if derr then return nil, derr end + local rows = charger_rows(data) + if not rows or #rows == 0 then + return nil, "no chargers on account" + end + if serial_want and serial_want ~= "" then + for i = 1, #rows do + if charger_matches(rows[i], serial_want) then + return tostring(pick(rows[i], "Id", "id")), nil + end + end + return nil, "charger not found" + end + return tostring(pick(rows[1], "Id", "id")), nil +end + +local function observation_map(raw) + local data, err = decode_json(raw) + if err then return nil, err end + local rows = data + if type(data) == "table" and data[1] == nil then + rows = pick(data, "Data", "data") or data + end + local obs = {} + if type(rows) ~= "table" then return obs, nil end + for i = 1, #rows do + local item = rows[i] + local id = as_number(pick(item, "StateId", "stateId", "ObservationId", "observationId")) + if id then + obs[id] = pick(item, "ValueAsString", "valueAsString", "Value", "value") + end + end + return obs, nil +end + +function driver_init(config) + host.set_make("Zaptec") + config = config or {} + email = config.email or config.username + password = config.password + serial_want = config.serial + if serial_want == "" then serial_want = nil end + if email == "" then email = nil end + if password == "" then password = nil end + if config.base_url and tostring(config.base_url) ~= "" then + BASE_URL = tostring(config.base_url):gsub("/$", "") + end + local p = as_number(config.phases) + if p == 1 or p == 3 then phases = p end + + if not email or not password then + error("Zaptec: email/username and password required") + end + local ok, lerr = login() + if not ok then + error("Zaptec: initial login failed: " .. redact_http_err(lerr)) + end + local id, rerr = resolve_charger() + if not id then + error("Zaptec: could not list chargers: " .. redact_http_err(rerr)) + end + charger_id = id + host.set_sn(charger_id) + host.log("info", "Zaptec: driver initialized for " .. charger_id) +end + +function driver_poll() + if not charger_id or not email then + return 10000 + end + if not ensure_auth() then + host.log("warn", "Zaptec: auth failed, skipping poll") + return 10000 + end + local resp, err = safe_http_get( + BASE_URL .. "/api/chargers/" .. charger_id .. "/state", + auth_headers()) + if err then + host.log("warn", "Zaptec: state poll failed: " .. redact_http_err(err)) + return 10000 + end + local obs, oerr = observation_map(resp) + if oerr or not obs then + host.log("warn", "Zaptec: state decode failed") + return 10000 + end + + local op_mode = as_number(obs[OBS_OP_MODE]) or OP_DISCONNECTED + local power_w = as_number(obs[OBS_CHARGE_POWER]) or 0 + if power_w < 0 then power_w = 0 end + local session_kwh = as_number(obs[OBS_SESSION_ENERGY]) or 0 + local session_wh = session_kwh * 1000 + local connected = (op_mode == OP_CONNECTED or op_mode == OP_CHARGING or op_mode == OP_FINISHED) + local charging = (op_mode == OP_CHARGING) + local max_current = as_number(obs[OBS_MAX_CURRENT]) + local active_phases = as_number(obs[OBS_ACTIVE_PHASES]) or phases + + host.emit("ev", { + w = power_w, + connected = connected, + charging = charging, + session_wh = session_wh, + max_a = max_current, + phases = active_phases, + op_mode = op_mode, + }) + + local i1 = as_number(obs[OBS_CURRENT_L1]) + if i1 then host.emit_metric("ev_l1_a", i1) end + local v1 = as_number(obs[OBS_VOLTAGE_L1]) + if v1 then host.emit_metric("ev_l1_v", v1) end + host.emit_metric("ev_power_w", power_w) + + return 5000 +end + +function driver_command(action, power_w, cmd) + if action == "init" or action == "deinit" then + return true + end + if action == "ev_set_current" or action == "ev_pause" + or action == "ev_resume" or action == "ev_start" then + if not warned_readonly then + host.log("warn", + "Zaptec Cloud is read-only pending hardware verification of its " .. + "control path; command accepted but not applied") + warned_readonly = true + end + return true + end + + host.log("warn", "Zaptec: unknown action " .. tostring(action)) + return false +end + +function driver_default_mode() + -- Read-only: nothing was ever commanded, so there is nothing to release. +end + +function driver_cleanup() + access_token = nil + refresh_token = nil +end diff --git a/drivers/lua/zuidwijk_p1.lua b/drivers/lua/zuidwijk_p1.lua index 11f2ab0..c9252e0 100644 --- a/drivers/lua/zuidwijk_p1.lua +++ b/drivers/lua/zuidwijk_p1.lua @@ -14,9 +14,10 @@ DRIVER = { id = "zuidwijk-p1", name = "Zuidwijk P1 Reader Ethernet", manufacturer = "Zuidwijk", - version = "1.0.0", + version = "1.1.1", protocols = { "tcp" }, capabilities = { "meter" }, + read_only = true, description = "Dutch DSMR P1 smart-meter via Zuidwijk Serial-to-Ethernet bridge (raw TCP, port 23).", homepage = "https://www.zuidwijk.com/product/p1-reader-ethernet/", authors = { "FTW contributors" }, @@ -290,10 +291,12 @@ function driver_poll() host.log("warn", "zuidwijk-p1: CRC mismatch — dropping frame (total bad: " .. crc_errors .. ")") end end - host.emit_metric("p1_crc_errors", crc_errors) + -- Do not emit the CRC metric on silence: a metric alone can keep + -- driver health alive while no trusted meter reading exists. if not latest then return 1000 end + host.emit_metric("p1_crc_errors", crc_errors) local o = parse_obis(latest) @@ -317,9 +320,15 @@ function driver_poll() last_telegram_ms = host.millis() -- Active power in kW. DSMR sends import + export as separate positive - -- values; the difference gives site-convention W. - local p_imp_kw = obis_num(o, "1-0:1.7.0") or 0 - local p_exp_kw = obis_num(o, "1-0:2.7.0") or 0 + -- values; the difference gives site-convention W. Missing both OBIS + -- power readings is not "0 W" — skip the meter emit. + local p_imp_kw = obis_num(o, "1-0:1.7.0") + local p_exp_kw = obis_num(o, "1-0:2.7.0") + if p_imp_kw == nil and p_exp_kw == nil then + return 1000 + end + p_imp_kw = p_imp_kw or 0 + p_exp_kw = p_exp_kw or 0 local p_imp_l1 = obis_num(o, "1-0:21.7.0") or 0 local p_imp_l2 = obis_num(o, "1-0:41.7.0") or 0 local p_imp_l3 = obis_num(o, "1-0:61.7.0") or 0 diff --git a/drivers/tests/lua_harness/test_goodwe_ftw_contract.lua b/drivers/tests/lua_harness/test_goodwe_ftw_contract.lua index 8141d77..ef2ad60 100644 --- a/drivers/tests/lua_harness/test_goodwe_ftw_contract.lua +++ b/drivers/tests/lua_harness/test_goodwe_ftw_contract.lua @@ -21,7 +21,7 @@ end dofile("drivers/lua/goodwe.lua") -if type(DRIVER) ~= "table" or DRIVER.id ~= "goodwe" or DRIVER.version ~= "1.0.3" then +if type(DRIVER) ~= "table" or DRIVER.id ~= "goodwe" or type(DRIVER.version) ~= "string" then error("GoodWe FTW identity metadata is wrong") end if DRIVER.host_api_min ~= 1 or DRIVER.host_api_max ~= 1 or DRIVER.read_only ~= true then diff --git a/drivers/tests/lua_harness/test_mqtt_stale.lua b/drivers/tests/lua_harness/test_mqtt_stale.lua index a2307d7..d71cba7 100644 --- a/drivers/tests/lua_harness/test_mqtt_stale.lua +++ b/drivers/tests/lua_harness/test_mqtt_stale.lua @@ -42,6 +42,13 @@ if fresh_emits == 0 then error("fresh poll emitted no telemetry") end host._emitted = {} host._mqtt_buffer = {} +-- Ferroamp deliberately re-emits from a short age window so a WiFi blip +-- does not flap LastSuccess; past STALE_AFTER_MS (30 s) the cache drops +-- and idle polls must go silent. Advance the mock clock past that window +-- before asserting the no-emit rule for this driver. +if driver_name == "ferroamp" then + host._millis_counter = host._millis_counter + 31000 +end local stale_ok, stale_error = pcall(driver_poll) if not stale_ok then error("idle poll raised: " .. tostring(stale_error)) end diff --git a/drivers/tests/test_driver_contract.py b/drivers/tests/test_driver_contract.py index ee1132e..f58b65e 100644 --- a/drivers/tests/test_driver_contract.py +++ b/drivers/tests/test_driver_contract.py @@ -31,7 +31,10 @@ def test_protocol_value_valid(self, driver_name): code = read_driver(driver_name) match = re.search(r'^PROTOCOL\s*=\s*"([^"]*)"', code, re.MULTILINE) assert match, f"{driver_name}: cannot parse PROTOCOL value" - valid = {"modbus", "mqtt", "http", "serial", "p1", "standalone", ""} + # tcp / websocket are FTW host protocols used by promoted P1 and + # Tibber drivers; the catalog contract originally omitted them. + valid = {"modbus", "mqtt", "http", "serial", "p1", "standalone", + "tcp", "websocket", ""} assert match.group(1) in valid, \ f"{driver_name}: PROTOCOL={match.group(1)!r} not in {valid}" diff --git a/drivers/tests/test_emit_fields.py b/drivers/tests/test_emit_fields.py index 715ceb8..f9eb2a2 100644 --- a/drivers/tests/test_emit_fields.py +++ b/drivers/tests/test_emit_fields.py @@ -63,6 +63,8 @@ # the same kind of diagnostic the Pixii battery fields below carry. "mppt3_v", "mppt3_a", "mppt4_v", "mppt4_a", "dc_w", "status", "mode", + # Pixii MQTT PV meter: lifetime generation under the FTW field name. + "generation_wh", }, "battery": { "w", "v", "a", "soc", @@ -75,6 +77,9 @@ # list above was written from the catalog's narrower view. "charge_status", "control_mode", "battery_state", "battery_vendor_state", "battery_event1", + # sonnenBatterie FullChargeCapacity; Ferroamp multi-ESO capability. + "capacity_wh", + "discharge_capable", "charge_capable", }, "v2x_charger": { "w", "a", "v", "hz", @@ -169,8 +174,10 @@ def _extract_variable_emit_fields(code, der_type): all_fields.append(field) # Also check for inline table construction: local var = { field = ... } + # or a later reassignment `var = { field = ... }` (Ferroamp builds + # battery that way before host.emit("battery", battery)). table_pattern = re.compile( - rf'local\s+{re.escape(var_name)}\s*=\s*\{{([^}}]*)\}}', + rf'(?:local\s+)?{re.escape(var_name)}\s*=\s*\{{([^}}]*)\}}', re.DOTALL, ) for table_match in table_pattern.finditer(clean): diff --git a/drivers/tests/test_http_drivers.py b/drivers/tests/test_http_drivers.py index 3dfecad..b056eb7 100644 --- a/drivers/tests/test_http_drivers.py +++ b/drivers/tests/test_http_drivers.py @@ -35,14 +35,19 @@ def test_constructs_base_url(self, driver_name): code = read_driver(driver_name) clean = strip_lua_comments(code) - # Local HTTP builds the URL from config.host. The scheme may be - # http:// or https:// (a LAN device that only speaks TLS, with a pin). - has_host = "config.host" in clean or 'config["host"]' in clean + # Local HTTP builds the URL from config.host (or config.ip for some + # FTW-promoted drivers). The scheme may be http:// or https://. + has_host = ( + "config.host" in clean + or 'config["host"]' in clean + or "config.ip" in clean + or 'config["ip"]' in clean + ) has_scheme = '"http://"' in clean or '"https://"' in clean assert has_host and has_scheme, ( f"{driver_name}: HTTP driver should construct an http(s) URL " - f"from config.host" + f"from config.host or config.ip" ) def test_uses_json_decode(self, driver_name): @@ -109,15 +114,19 @@ def test_uses_config_port(self, driver_name): code = read_driver(driver_name) clean = strip_lua_comments(code) - # Most drivers use config.port or a default port + # Most drivers use config.port; some FTW-promoted ones hardcode a + # well-known proxy/device port or rely on the default HTTP port. has_port = ( 'config.port' in clean or 'config["port"]' in clean + or re.search(r'\bPROXY_PORT\b', clean) + or re.search(r':\s*\d{2,5}\b', clean) + or re.search(r'"http://"\s*\.\.', clean) ) assert has_port, ( - f"{driver_name}: HTTP driver should reference config.port " - f"for connection port" + f"{driver_name}: HTTP driver should reference a connection port " + f"(config.port, a named default, or scheme-only default 80)" ) def test_poll_returns_on_nil_data(self, driver_name): diff --git a/drivers/tests/test_manifest_consistency.py b/drivers/tests/test_manifest_consistency.py index 029952c..6a0acdc 100644 --- a/drivers/tests/test_manifest_consistency.py +++ b/drivers/tests/test_manifest_consistency.py @@ -250,9 +250,16 @@ def test_protocol_matches_driver(self, driver_name): # Handle empty/standalone mapping # Manifest may use "" or "standalone" for the same thing # Also handle the hello driver which has PROTOCOL="standalone" but manifest protocol="" + # + # FTW-promoted drivers may use a more specific Lua PROTOCOL than the + # catalog permission bucket: tibber speaks websocket (HTTP for auth) + # while the channel maps http permissions; zuidwijk_p1 speaks raw TCP + # while its catalog row still uses the modbus permission bucket. proto_aliases = { "": {"", "standalone"}, "standalone": {"", "standalone"}, + "http": {"http", "websocket"}, + "modbus": {"modbus", "tcp"}, } if manifest_protocol in proto_aliases: diff --git a/index.yaml b/index.yaml index ae1ca39..86c2415 100644 --- a/index.yaml +++ b/index.yaml @@ -193,15 +193,15 @@ drivers: size_bytes: 4054 sha256: "4a2cd1efb4a5583468ce897ebd88a000e348917295c40210e86ecf834c0ba543" - name: "easee_cloud" - version: "1.3.0" + version: "1.3.1" tier: core protocol: http connectivity: cloud setup: [vendor_portal] ders: [ev] control: true - size_bytes: 36344 - sha256: "f127426d39ccf831d132f401d0b7d676efa76568974eed3096b6c4569492ee23" + size_bytes: 36986 + sha256: "e28ae3a3805d8d13848dc6d9ce3f0f1c3ed51d9354bdd4d3329f323a02a36d27" - name: "esphome-dsmr" version: "1.0.3" tier: community @@ -232,14 +232,14 @@ drivers: size_bytes: 3545 sha256: "fdf3c3cae6ca45f9b546a97cf16a34b047172293711db2dfa759286f065d77c8" - name: "ferroamp" - version: "2.1.1" + version: "2.1.2" tier: core protocol: mqtt connectivity: local ders: [battery, meter, pv] control: true - size_bytes: 55058 - sha256: "c04d137d595ba50b8c6178c82d917b115dbe9a7cbd2cf671ef2660e871f96de3" + size_bytes: 55329 + sha256: "81de3c2f78618f4a30994b0397f4b6b4b56273a33946ae2d23f75a27698bc399" - name: "ferroamp_dc2_v2x" version: "2.1.0" tier: core @@ -268,14 +268,14 @@ drivers: size_bytes: 41341 sha256: "8d2775354a1716dee9b03aedef0b8415bafa94e60271e2eced24cecde03964aa" - name: "fronius" - version: "2.1.1" + version: "2.1.2" tier: core protocol: modbus connectivity: local ders: [battery, meter, pv] control: false - size_bytes: 12721 - sha256: "bc526decf391d60f20369d84a20ac564c03255bdaad061ba6d08426169efe3e7" + size_bytes: 12744 + sha256: "9ba747b7d54fb5e31fe9334c6a9e9408c9130370900f7ba30f019f232bc022ed" - name: "fronius_api" version: "1.0.2" tier: community @@ -286,14 +286,14 @@ drivers: size_bytes: 2080 sha256: "3094603577fc51aa45bb4c757a471d24e8cb77ee4f0c596bb2fd4112b22a6578" - name: "fronius_smart_meter" - version: "2.1.1" + version: "2.1.2" tier: core protocol: modbus connectivity: local ders: [meter] control: false - size_bytes: 7521 - sha256: "1ba6d717b2a04818da0d23cfedd5bddc0dd603da5d8ad838b68bf42c0595e0f2" + size_bytes: 8075 + sha256: "b5bfc8258d5f030f9121392397e3247d4131241c6cff03b957bf4fcc91383d13" - name: "goe" version: "1.0.3" tier: community @@ -313,24 +313,24 @@ drivers: size_bytes: 3340 sha256: "48b6d9ded6043e9b42a09bf03bd8edbab6363b18781e2279e957eee237556d25" - name: "goodwe" - version: "2.1.1" + version: "2.1.2" tier: community protocol: modbus connectivity: local setup: [bridge] ders: [pv, battery, meter] control: false - size_bytes: 9543 - sha256: "674845c2b9bb815cbabc3a7b698d6eee4248eeb7b919d20859c4785107f175e3" + size_bytes: 9566 + sha256: "148d36d25fa630dd11ab1f697c46dde60863dcd3a2a9944e78e322dba55bde49" - name: "growatt" - version: "2.1.1" + version: "2.1.2" tier: community protocol: modbus connectivity: local ders: [pv, battery, meter] control: false - size_bytes: 9353 - sha256: "3c4563fc848786caa8fb3a751b1a9c5fbf13db9e173b27a5fa2780c9cbadcf49" + size_bytes: 9376 + sha256: "1e9623cbf3453697f0fba429a5b8dab7d61cf9507587e616d0cd9de3cc3594c8" - name: "hardybarth" version: "1.0.1" tier: community @@ -396,14 +396,14 @@ drivers: size_bytes: 93318 sha256: "dcd2eccbccc9e97f24c63f6a65a3498b8441f249ae9a17b5268a97bd8b34bdb5" - name: "kostal" - version: "2.1.1" + version: "2.1.2" tier: community protocol: modbus connectivity: local ders: [pv, battery, meter] control: false - size_bytes: 11199 - sha256: "e0801761d200b1a948872b6bfca3c662483c7dfc00445e15f83eaf07f80a6bf5" + size_bytes: 11222 + sha256: "d85eabff2e1cb3076c27c0d980bd93c4c26df3d27a4801310baec30fda96c127" - name: "kstar" version: "1.1.1" tier: community @@ -508,23 +508,23 @@ drivers: size_bytes: 19344 sha256: "29f2bd2f73c0d39d809839ca60b40c3e4281f1dda267ba2b26e66bd9ab80c27b" - name: "pixii" - version: "2.1.3" + version: "2.1.4" tier: core protocol: modbus connectivity: local ders: [battery, meter] control: false - size_bytes: 27853 - sha256: "9f0c60a5e3414df07f2a8866a5275325b7539df92fb9d4d847028cfa5ed651fd" + size_bytes: 27846 + sha256: "2b9c691f7171794e39dd28e927a9d09ac706e507cd646b8f1ca2bf76c7d892ea" - name: "pixii_pv" - version: "0.3.0" + version: "0.3.1" tier: core protocol: mqtt connectivity: local ders: [pv, meter] control: false - size_bytes: 6692 - sha256: "d0901b6478e05b5cdf46fc2a9675700ba6008be2feb370be0ff7e87d2f1de883" + size_bytes: 6699 + sha256: "4f86b5c75ad1af8ed7bb48d7fa0f50db8ade8397b9a04c9ea1a8347f0bafff3d" - name: "saj" version: "1.2.0" tier: community @@ -590,14 +590,14 @@ drivers: size_bytes: 5837 sha256: "cf16fd07b4b55c04be72ddc46f4b75b8279512fa7871aea71e19762d73caa32f" - name: "sma" - version: "2.1.1" + version: "2.1.2" tier: core protocol: modbus connectivity: local ders: [battery, meter, pv] control: false - size_bytes: 13951 - sha256: "4af66894fef80d3955a94d4c5879d93d0110c8c91b32191bd8a597bee8dd17f3" + size_bytes: 13974 + sha256: "5faa2235261c22f6a59b17fc4f027f7d66ce4607dd6132d5a911e8079e793b7f" - name: "sma_pv" version: "1.1.1" tier: core @@ -618,14 +618,14 @@ drivers: size_bytes: 3818 sha256: "acceec60de05166d1760314850227d33c57b26559a1696fc347b035b5f21902b" - name: "sofar" - version: "2.1.1" + version: "2.1.2" tier: community protocol: modbus connectivity: local ders: [pv, battery, meter] control: false - size_bytes: 9083 - sha256: "26307ffaec7a11139038fc286a96d3d4ee7c6097bdba7c0eef22a2761597115c" + size_bytes: 9106 + sha256: "b818b0a0633d73d5588f5957e3b3935884b34dac2fea3e1ae30063be92cfe5d8" - name: "solaredge" version: "2.1.1" tier: core @@ -681,24 +681,24 @@ drivers: size_bytes: 17463 sha256: "ed089b7b41a5d15bf6144aea9f625039f3a14b82e44b41a8db40d2795dc69eb8" - name: "solis_string" - version: "1.1.1" + version: "1.1.2" tier: core protocol: modbus connectivity: local ders: [pv] control: false - size_bytes: 5784 - sha256: "3cfcf53653680dede7caf6173cee251efa3cf2c9ec46fdc16a89b9b526dceb7e" + size_bytes: 5807 + sha256: "be7c491e93dcbdf6271428c6c9268f99da6efba7903ae0356c585b0e3c5c9992" - name: "sonnen" - version: "2.0.2" + version: "2.0.3" tier: community protocol: http connectivity: local setup: [device_ui] ders: [battery, meter] control: false - size_bytes: 6158 - sha256: "92163e4356b895e16b0205e849aa42a2dcea0d799f8cc6bcbce60b5e2cd92285" + size_bytes: 6631 + sha256: "94ae5e0065f5793051acb89b7ef04a679b2898908dfac6f4ffd6e8deb77fa2c0" - name: "sungrow" version: "1.5.8" tier: core @@ -709,25 +709,34 @@ drivers: size_bytes: 48889 sha256: "a11ddc8de433c527a3d63cd014d243b09f3393ee3afa9b8e1594d4de8de37562" - name: "tesla_vehicle" - version: "0.2.1" + version: "0.2.2" tier: core protocol: http connectivity: local setup: [bridge] ders: [vehicle] control: true - size_bytes: 23154 - sha256: "a708681364facbd09fb94a813050f6889d952a0178b392e51a64eddcc43fbf0d" + size_bytes: 23888 + sha256: "b05cbd358a51409225b7043d22336b01cd85c2c005da6d64cc57368a34713aec" + - name: "tesla_wall_connector" + version: "0.1.0" + tier: community + protocol: http + connectivity: local + ders: [ev] + control: false + size_bytes: 8162 + sha256: "7715ce1ec3cf707fbcfc5ae58430430ffa79f7f82b38b242df9631ea1593af79" - name: "tibber" - version: "1.1.1" + version: "1.1.2" tier: core protocol: http connectivity: cloud setup: [vendor_portal] ders: [meter] control: false - size_bytes: 12410 - sha256: "845ca59a775a3a4a4c27962454f14ba654fba0d1b905f24647c80db5c26a62ae" + size_bytes: 12472 + sha256: "3cf10b93755fa8b840375f53343e91b5efa98ddffe119a5679b43ee4bfb61c88" - name: "varta" version: "1.1.1" tier: community @@ -738,14 +747,14 @@ drivers: size_bytes: 2794 sha256: "68eb714ff41551437e50ac0a1e9bca4e4d70eca8a43006c7481cfc8761fdf60d" - name: "victron" - version: "2.1.1" + version: "2.1.2" tier: community protocol: modbus connectivity: local ders: [pv, battery, meter] control: false - size_bytes: 7998 - sha256: "3c477072004592762c3422cc29a5570752e74bb7a1a59e51c279ab3d0fd654fd" + size_bytes: 8021 + sha256: "63c39e1d5a5ae7bf07ba35e9bb310c820e329e76663893bcd035e61db7c4116e" - name: "victron_mqtt" version: "1.0.3" tier: community @@ -782,13 +791,23 @@ drivers: control: false size_bytes: 2281 sha256: "78d66d1af8f8169a7fa3adaa914f27475907930dc733ea58757d7b394e5a9802" + - name: "zaptec_cloud" + version: "0.1.0" + tier: community + protocol: http + connectivity: cloud + setup: [vendor_portal] + ders: [ev] + control: false + size_bytes: 11275 + sha256: "b6fcdd70b15749caa13a77c4993568effcb199e05f913be0fd667ee9ad2ef134" - name: "zuidwijk_p1" - version: "1.1.0" + version: "1.1.1" tier: core protocol: modbus connectivity: local setup: [bridge] ders: [meter] - control: true - size_bytes: 16217 - sha256: "61f1d6522823b6054172d9f9fd17cc6f84088be515077043e1123b6dfb509f3a" + control: false + size_bytes: 16578 + sha256: "a08af3da055a64f082293389d6a1d8a13ca784b4a497bab3ca20c2f70361ec4e" diff --git a/manifests/easee_cloud.yaml b/manifests/easee_cloud.yaml index 1ab60f1..335569f 100644 --- a/manifests/easee_cloud.yaml +++ b/manifests/easee_cloud.yaml @@ -1,5 +1,5 @@ name: "easee_cloud" -version: "1.3.0" +version: "1.3.1" tier: core author: "Sourceful Labs AB" protocol: http @@ -16,9 +16,9 @@ tested_devices: notes: "Easee Home/Charge via Cloud REST API. No local protocol needed." min_driver_version: "1.0.1" min_host_version: "2.0.0" -size_bytes: 36344 +size_bytes: 36986 dkb_id: "easee_cloud" -sha256: "f127426d39ccf831d132f401d0b7d676efa76568974eed3096b6c4569492ee23" +sha256: "e28ae3a3805d8d13848dc6d9ce3f0f1c3ed51d9354bdd4d3329f323a02a36d27" signature: "" bytecode_sha256: "" diff --git a/manifests/ferroamp.yaml b/manifests/ferroamp.yaml index 280c2e0..84f3960 100644 --- a/manifests/ferroamp.yaml +++ b/manifests/ferroamp.yaml @@ -1,5 +1,5 @@ name: "ferroamp" -version: "2.1.1" +version: "2.1.2" tier: core author: "Sourceful Labs AB" protocol: mqtt @@ -18,9 +18,9 @@ connection_defaults: mqtt_username: "extapi" mqtt_password: "ferroampExtApi" min_host_version: "2.0.0" -size_bytes: 55058 +size_bytes: 55329 dkb_id: "ferroamp" -sha256: "c04d137d595ba50b8c6178c82d917b115dbe9a7cbd2cf671ef2660e871f96de3" +sha256: "81de3c2f78618f4a30994b0397f4b6b4b56273a33946ae2d23f75a27698bc399" signature: "" bytecode_sha256: "" diff --git a/manifests/fronius.yaml b/manifests/fronius.yaml index 2395f81..9968aa0 100644 --- a/manifests/fronius.yaml +++ b/manifests/fronius.yaml @@ -1,5 +1,5 @@ name: "fronius" -version: "2.1.1" +version: "2.1.2" tier: core author: "Sourceful Labs AB" protocol: modbus @@ -50,9 +50,9 @@ tested_devices: notes: "Classic single-phase string inverter" min_driver_version: "1.2.0" min_host_version: "2.0.0" -size_bytes: 12721 +size_bytes: 12744 dkb_id: "fronius_symo_gen24" -sha256: "bc526decf391d60f20369d84a20ac564c03255bdaad061ba6d08426169efe3e7" +sha256: "9ba747b7d54fb5e31fe9334c6a9e9408c9130370900f7ba30f019f232bc022ed" signature: "" bytecode_sha256: "" diff --git a/manifests/fronius_smart_meter.yaml b/manifests/fronius_smart_meter.yaml index 3d88d58..9e5dc76 100644 --- a/manifests/fronius_smart_meter.yaml +++ b/manifests/fronius_smart_meter.yaml @@ -1,5 +1,5 @@ name: "fronius_smart_meter" -version: "2.1.1" +version: "2.1.2" tier: core author: "Sourceful Labs AB" protocol: modbus @@ -15,9 +15,9 @@ tested_devices: notes: "" min_driver_version: "1.1.0" min_host_version: "2.0.0" -size_bytes: 7521 +size_bytes: 8075 dkb_id: "fronius_smart_meter" -sha256: "1ba6d717b2a04818da0d23cfedd5bddc0dd603da5d8ad838b68bf42c0595e0f2" +sha256: "b5bfc8258d5f030f9121392397e3247d4131241c6cff03b957bf4fcc91383d13" signature: "" bytecode_sha256: "" diff --git a/manifests/goodwe.yaml b/manifests/goodwe.yaml index 8eb6abd..9fe0764 100644 --- a/manifests/goodwe.yaml +++ b/manifests/goodwe.yaml @@ -1,5 +1,5 @@ name: "goodwe" -version: "2.1.1" +version: "2.1.2" tier: community author: "frekes81 and Sourceful contributors" protocol: modbus @@ -9,9 +9,9 @@ ders: [pv, battery, meter] control: false tested_devices: [] min_host_version: "1.10.0-beta.1" -size_bytes: 9543 +size_bytes: 9566 dkb_id: "" -sha256: "674845c2b9bb815cbabc3a7b698d6eee4248eeb7b919d20859c4785107f175e3" +sha256: "148d36d25fa630dd11ab1f697c46dde60863dcd3a2a9944e78e322dba55bde49" signature: "" bytecode_sha256: "" diff --git a/manifests/growatt.yaml b/manifests/growatt.yaml index 8375288..2ec89bd 100644 --- a/manifests/growatt.yaml +++ b/manifests/growatt.yaml @@ -1,5 +1,5 @@ name: "growatt" -version: "2.1.1" +version: "2.1.2" tier: community author: "Sourceful Labs AB" protocol: modbus @@ -50,9 +50,9 @@ tested_devices: notes: "Community driver, untested" min_driver_version: "1.0.0" min_host_version: "2.0.0" -size_bytes: 9353 +size_bytes: 9376 dkb_id: "" -sha256: "3c4563fc848786caa8fb3a751b1a9c5fbf13db9e173b27a5fa2780c9cbadcf49" +sha256: "1e9623cbf3453697f0fba429a5b8dab7d61cf9507587e616d0cd9de3cc3594c8" signature: "" bytecode_sha256: "" diff --git a/manifests/kostal.yaml b/manifests/kostal.yaml index 501807e..48629c1 100644 --- a/manifests/kostal.yaml +++ b/manifests/kostal.yaml @@ -1,5 +1,5 @@ name: "kostal" -version: "2.1.1" +version: "2.1.2" tier: community author: "Sourceful Labs AB" protocol: modbus @@ -36,9 +36,9 @@ tested_devices: notes: "SunSpec compliant, port 1502" min_driver_version: "1.0.0" min_host_version: "2.0.0" -size_bytes: 11199 +size_bytes: 11222 dkb_id: "" -sha256: "e0801761d200b1a948872b6bfca3c662483c7dfc00445e15f83eaf07f80a6bf5" +sha256: "d85eabff2e1cb3076c27c0d980bd93c4c26df3d27a4801310baec30fda96c127" signature: "" bytecode_sha256: "" diff --git a/manifests/pixii.yaml b/manifests/pixii.yaml index 429fc58..bcd82e5 100644 --- a/manifests/pixii.yaml +++ b/manifests/pixii.yaml @@ -1,5 +1,5 @@ name: "pixii" -version: "2.1.3" +version: "2.1.4" tier: core author: "Sourceful Labs AB and community contributors" protocol: modbus @@ -15,9 +15,9 @@ tested_devices: notes: "" min_driver_version: "1.1.0" min_host_version: "2.0.0" -size_bytes: 27853 +size_bytes: 27846 dkb_id: "pixii" -sha256: "9f0c60a5e3414df07f2a8866a5275325b7539df92fb9d4d847028cfa5ed651fd" +sha256: "2b9c691f7171794e39dd28e927a9d09ac706e507cd646b8f1ca2bf76c7d892ea" signature: "" bytecode_sha256: "" diff --git a/manifests/pixii_pv.yaml b/manifests/pixii_pv.yaml index 0e699f4..04b8232 100644 --- a/manifests/pixii_pv.yaml +++ b/manifests/pixii_pv.yaml @@ -1,5 +1,5 @@ name: "pixii_pv" -version: "0.3.0" +version: "0.3.1" tier: core author: "Sourceful Labs AB" protocol: mqtt @@ -15,9 +15,9 @@ tested_devices: notes: "Read-only Pixii telemetry: PV (external CTs) + grid meter (meter_w). Battery / SoC comes from the Modbus driver, not this one." min_driver_version: "0.2.0" min_host_version: "2.0.0" -size_bytes: 6692 +size_bytes: 6699 dkb_id: "pixii_pv" -sha256: "d0901b6478e05b5cdf46fc2a9675700ba6008be2feb370be0ff7e87d2f1de883" +sha256: "4f86b5c75ad1af8ed7bb48d7fa0f50db8ade8397b9a04c9ea1a8347f0bafff3d" signature: "" bytecode_sha256: "" diff --git a/manifests/sma.yaml b/manifests/sma.yaml index 7bd64fb..289982f 100644 --- a/manifests/sma.yaml +++ b/manifests/sma.yaml @@ -1,5 +1,5 @@ name: "sma" -version: "2.1.1" +version: "2.1.2" tier: core author: "Sourceful Labs AB" protocol: modbus @@ -57,9 +57,9 @@ tested_devices: notes: "Energy meter / system manager" min_driver_version: "1.2.0" min_host_version: "2.0.0" -size_bytes: 13951 +size_bytes: 13974 dkb_id: "sma" -sha256: "4af66894fef80d3955a94d4c5879d93d0110c8c91b32191bd8a597bee8dd17f3" +sha256: "5faa2235261c22f6a59b17fc4f027f7d66ce4607dd6132d5a911e8079e793b7f" signature: "" bytecode_sha256: "" diff --git a/manifests/sofar.yaml b/manifests/sofar.yaml index b14db25..1f067a9 100644 --- a/manifests/sofar.yaml +++ b/manifests/sofar.yaml @@ -1,5 +1,5 @@ name: "sofar" -version: "2.1.1" +version: "2.1.2" tier: community author: "Sourceful Labs AB" protocol: modbus @@ -29,9 +29,9 @@ tested_devices: notes: "Community driver, untested" min_driver_version: "1.0.0" min_host_version: "2.0.0" -size_bytes: 9083 +size_bytes: 9106 dkb_id: "" -sha256: "26307ffaec7a11139038fc286a96d3d4ee7c6097bdba7c0eef22a2761597115c" +sha256: "b818b0a0633d73d5588f5957e3b3935884b34dac2fea3e1ae30063be92cfe5d8" signature: "" bytecode_sha256: "" diff --git a/manifests/solis_string.yaml b/manifests/solis_string.yaml index 133da9a..b3be3f3 100644 --- a/manifests/solis_string.yaml +++ b/manifests/solis_string.yaml @@ -1,5 +1,5 @@ name: "solis_string" -version: "1.1.1" +version: "1.1.2" tier: core author: "Sourceful Labs AB" protocol: modbus @@ -15,9 +15,9 @@ tested_devices: notes: "Solis non-hybrid PV inverters via Modbus TCP." min_driver_version: "1.0.0" min_host_version: "2.0.0" -size_bytes: 5784 +size_bytes: 5807 dkb_id: "solis_string" -sha256: "3cfcf53653680dede7caf6173cee251efa3cf2c9ec46fdc16a89b9b526dceb7e" +sha256: "be7c491e93dcbdf6271428c6c9268f99da6efba7903ae0356c585b0e3c5c9992" signature: "" bytecode_sha256: "" diff --git a/manifests/sonnen.yaml b/manifests/sonnen.yaml index 68c6199..d4757fe 100644 --- a/manifests/sonnen.yaml +++ b/manifests/sonnen.yaml @@ -1,5 +1,5 @@ name: "sonnen" -version: "2.0.2" +version: "2.0.3" tier: community author: "Sourceful Labs AB" protocol: http @@ -16,9 +16,9 @@ tested_devices: notes: "Community driver, untested" min_driver_version: "1.0.0" min_host_version: "2.1.0" -size_bytes: 6158 +size_bytes: 6631 dkb_id: "" -sha256: "92163e4356b895e16b0205e849aa42a2dcea0d799f8cc6bcbce60b5e2cd92285" +sha256: "94ae5e0065f5793051acb89b7ef04a679b2898908dfac6f4ffd6e8deb77fa2c0" signature: "" bytecode_sha256: "" diff --git a/manifests/tesla_vehicle.yaml b/manifests/tesla_vehicle.yaml index c8aa3ca..3e9b5eb 100644 --- a/manifests/tesla_vehicle.yaml +++ b/manifests/tesla_vehicle.yaml @@ -1,5 +1,5 @@ name: "tesla_vehicle" -version: "0.2.1" +version: "0.2.2" tier: core author: "Sourceful Labs AB" protocol: http @@ -16,9 +16,9 @@ tested_devices: notes: "Read-only vehicle SoC + charge limit via Tesla API-compatible HTTP endpoint (e.g. TeslaBLEProxy)." min_driver_version: "0.1.0" min_host_version: "2.0.0" -size_bytes: 23154 +size_bytes: 23888 dkb_id: "tesla_vehicle" -sha256: "a708681364facbd09fb94a813050f6889d952a0178b392e51a64eddcc43fbf0d" +sha256: "b05cbd358a51409225b7043d22336b01cd85c2c005da6d64cc57368a34713aec" signature: "" bytecode_sha256: "" diff --git a/manifests/tesla_wall_connector.yaml b/manifests/tesla_wall_connector.yaml new file mode 100644 index 0000000..d46e222 --- /dev/null +++ b/manifests/tesla_wall_connector.yaml @@ -0,0 +1,23 @@ +name: "tesla_wall_connector" +version: "0.1.0" +tier: community +author: "Sourceful Labs AB" +protocol: http +connectivity: local +ders: [ev] +control: false +tested_devices: + - manufacturer: "Tesla" + model_family: "Tesla Wall Connector" + variants: [Wall Connector Gen 3] + regions: [] + firmware_versions: "" + notes: "Tesla Wall Connector Gen 3 on the LAN. Observation only; no local current setpoint." + min_driver_version: "0.1.0" +min_host_version: "2.0.0" +size_bytes: 8162 +dkb_id: "tesla_wall_connector" +sha256: "7715ce1ec3cf707fbcfc5ae58430430ffa79f7f82b38b242df9631ea1593af79" +signature: "" + +bytecode_sha256: "" diff --git a/manifests/tibber.yaml b/manifests/tibber.yaml index 6617f0b..8f447a5 100644 --- a/manifests/tibber.yaml +++ b/manifests/tibber.yaml @@ -1,5 +1,5 @@ name: "tibber" -version: "1.1.1" +version: "1.1.2" tier: core author: "Sourceful Labs AB" protocol: http @@ -16,9 +16,9 @@ tested_devices: notes: "Tibber Pulse grid meter via GraphQL-transport-ws liveMeasurement stream." min_driver_version: "1.0.0" min_host_version: "2.0.0" -size_bytes: 12410 +size_bytes: 12472 dkb_id: "tibber" -sha256: "845ca59a775a3a4a4c27962454f14ba654fba0d1b905f24647c80db5c26a62ae" +sha256: "3cf10b93755fa8b840375f53343e91b5efa98ddffe119a5679b43ee4bfb61c88" signature: "" bytecode_sha256: "" diff --git a/manifests/victron.yaml b/manifests/victron.yaml index e84e5b1..08c3fa5 100644 --- a/manifests/victron.yaml +++ b/manifests/victron.yaml @@ -1,5 +1,5 @@ name: "victron" -version: "2.1.1" +version: "2.1.2" tier: community author: "Sourceful Labs AB" protocol: modbus @@ -43,9 +43,9 @@ tested_devices: notes: "Requires Venus OS with Modbus TCP enabled on GX device" min_driver_version: "1.0.0" min_host_version: "2.0.0" -size_bytes: 7998 +size_bytes: 8021 dkb_id: "" -sha256: "3c477072004592762c3422cc29a5570752e74bb7a1a59e51c279ab3d0fd654fd" +sha256: "63c39e1d5a5ae7bf07ba35e9bb310c820e329e76663893bcd035e61db7c4116e" signature: "" bytecode_sha256: "" diff --git a/manifests/zaptec_cloud.yaml b/manifests/zaptec_cloud.yaml new file mode 100644 index 0000000..2149c31 --- /dev/null +++ b/manifests/zaptec_cloud.yaml @@ -0,0 +1,24 @@ +name: "zaptec_cloud" +version: "0.1.0" +tier: community +author: "Sourceful Labs AB" +protocol: http +connectivity: cloud +setup: [vendor_portal] +ders: [ev] +control: false +tested_devices: + - manufacturer: "Zaptec" + model_family: "Zaptec Cloud" + variants: [Go, Go 2, Pro] + regions: [] + firmware_versions: "" + notes: "Zaptec Go / Go 2 / Pro via Zaptec Cloud REST API. Email + password; optional charger serial." + min_driver_version: "0.1.0" +min_host_version: "2.0.0" +size_bytes: 11275 +dkb_id: "zaptec_cloud" +sha256: "b6fcdd70b15749caa13a77c4993568effcb199e05f913be0fd667ee9ad2ef134" +signature: "" + +bytecode_sha256: "" diff --git a/manifests/zuidwijk_p1.yaml b/manifests/zuidwijk_p1.yaml index e35158f..744aa59 100644 --- a/manifests/zuidwijk_p1.yaml +++ b/manifests/zuidwijk_p1.yaml @@ -1,12 +1,12 @@ name: "zuidwijk_p1" -version: "1.1.0" +version: "1.1.1" tier: core author: "Sourceful Labs AB" protocol: modbus connectivity: local setup: [bridge] ders: [meter] -control: true +control: false tested_devices: - manufacturer: "Zuidwijk" model_family: "Zuidwijk P1 Reader Ethernet" @@ -16,9 +16,9 @@ tested_devices: notes: "Dutch DSMR P1 smart-meter via Zuidwijk Serial-to-Ethernet bridge (raw TCP, port 23)." min_driver_version: "1.0.0" min_host_version: "2.0.0" -size_bytes: 16217 +size_bytes: 16578 dkb_id: "zuidwijk_p1" -sha256: "61f1d6522823b6054172d9f9fd17cc6f84088be515077043e1123b6dfb509f3a" +sha256: "a08af3da055a64f082293389d6a1d8a13ca784b4a497bab3ca20c2f70361ec4e" signature: "" bytecode_sha256: "" diff --git a/support-status.json b/support-status.json index c84ed05..276ba55 100644 --- a/support-status.json +++ b/support-status.json @@ -590,7 +590,7 @@ }, { "catalog_source": true, - "catalog_version": "1.3.0", + "catalog_version": "1.3.1", "driver_id": "easee_cloud", "package_id": null, "targets": { @@ -702,7 +702,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "ferroamp", "package_id": null, "targets": { @@ -814,7 +814,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "fronius", "package_id": null, "targets": { @@ -870,7 +870,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "fronius_smart_meter", "package_id": null, "targets": { @@ -954,7 +954,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "goodwe", "package_id": null, "targets": { @@ -982,7 +982,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "growatt", "package_id": null, "targets": { @@ -1206,7 +1206,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "kostal", "package_id": null, "targets": { @@ -1542,7 +1542,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.3", + "catalog_version": "2.1.4", "driver_id": "pixii", "package_id": "com.sourceful.driver.pixii", "targets": { @@ -1570,7 +1570,7 @@ }, { "catalog_source": true, - "catalog_version": "0.3.0", + "catalog_version": "0.3.1", "driver_id": "pixii_pv", "package_id": null, "targets": { @@ -1794,7 +1794,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "sma", "package_id": null, "targets": { @@ -1878,7 +1878,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "sofar", "package_id": null, "targets": { @@ -2074,7 +2074,7 @@ }, { "catalog_source": true, - "catalog_version": "1.1.1", + "catalog_version": "1.1.2", "driver_id": "solis_string", "package_id": null, "targets": { @@ -2102,7 +2102,7 @@ }, { "catalog_source": true, - "catalog_version": "2.0.2", + "catalog_version": "2.0.3", "driver_id": "sonnen", "package_id": null, "targets": { @@ -2158,7 +2158,7 @@ }, { "catalog_source": true, - "catalog_version": "0.2.1", + "catalog_version": "0.2.2", "driver_id": "tesla_vehicle", "package_id": null, "targets": { @@ -2186,7 +2186,35 @@ }, { "catalog_source": true, - "catalog_version": "1.1.1", + "catalog_version": "0.1.0", + "driver_id": "tesla_wall_connector", + "package_id": null, + "targets": { + "blixt-l1": { + "candidate_package_version": null, + "control_enabled": false, + "hil": "not_recorded", + "historical_signed_beta_version": null, + "legacy_parity": "not_assessed", + "note": "", + "stable_package_version": null, + "target_conformance": "not_assessed" + }, + "ftw-core": { + "candidate_package_version": null, + "control_enabled": false, + "hil": "not_recorded", + "historical_signed_beta_version": null, + "legacy_parity": "not_assessed", + "note": "", + "stable_package_version": null, + "target_conformance": "not_assessed" + } + } + }, + { + "catalog_source": true, + "catalog_version": "1.1.2", "driver_id": "tibber", "package_id": null, "targets": { @@ -2242,7 +2270,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.1", + "catalog_version": "2.1.2", "driver_id": "victron", "package_id": null, "targets": { @@ -2382,7 +2410,35 @@ }, { "catalog_source": true, - "catalog_version": "1.1.0", + "catalog_version": "0.1.0", + "driver_id": "zaptec_cloud", + "package_id": null, + "targets": { + "blixt-l1": { + "candidate_package_version": null, + "control_enabled": false, + "hil": "not_recorded", + "historical_signed_beta_version": null, + "legacy_parity": "not_assessed", + "note": "", + "stable_package_version": null, + "target_conformance": "not_assessed" + }, + "ftw-core": { + "candidate_package_version": null, + "control_enabled": false, + "hil": "not_recorded", + "historical_signed_beta_version": null, + "legacy_parity": "not_assessed", + "note": "", + "stable_package_version": null, + "target_conformance": "not_assessed" + } + } + }, + { + "catalog_source": true, + "catalog_version": "1.1.1", "driver_id": "zuidwijk_p1", "package_id": null, "targets": {