diff --git a/CHANGELOG.md b/CHANGELOG.md index 6189c34..fef5fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html ### Removed - **`foxess` (H1/H3 11000-range map) — removed untested.** Every tested_devices entry said "Community driver, untested", and no hardware has ever validated the map. This is not a clean supersession: `foxess_h3_smart` covers the 1K5-HI and H3-Smart families, not the H1 / H3 / H3-PRO / AIO-H3 families the old driver claimed — those lose their only, unvalidated, listing. The removal is still right on this vendor's hardware behaviour: a Fox ESS inverter answers unknown registers with silence, so a wrong-map driver produces no error, only timeouts — a real 1K5 answered none of the 11000-range map, and the catalog offering it anyway cost that operator a full "device not supported" detour. A catalog that reports what is known must not list a map nobody has seen answer. H1-range coverage can return the way H3-Smart did: a driver written against hardware someone actually has. +### Changed +- **`foxess_h3_smart` 0.9.1** — `verification_status` moves `experimental` → `production` ("verified on hardware" in the FTW UI), with `verification_notes` recording exactly what was verified and on what: telemetry, battery dispatch in all three regimes, and PV curtailment on a 1K5-HI-10-V1 over multi-day operation — and that the H3-Smart family shares the map but remains untested on H3-Smart hardware. Header and description drop the "local control build" framing that stopped being true when the channel shipped it + ### Added - **`foxess_h3_smart` 0.9.0** — battery control through the vendor remote-control block, hardware-validated in charge, discharge and hold on a 1K5-HI-10-V1. The setpoint at 46003/46004 is the inverter's AC active power, export-positive — not battery power and not a grid target — so the driver translates `vendor = pv × PV_AC_EFF − battery_target` and guards the charge path: BMS ceiling (46018/46019) minus a 200 W margin, a daylight split on PV string voltage, a one-cycle 0 W pause on import/export sign crossings, and charge refusal at SoC ≥ 99% (the inverter ignores its own Max SoC under remote control). Zero is a held setpoint, not a release — releasing let native self-use surge charging back against the host's ceiling in a ~90 s limit cycle. Two dead-man's switches: the vendor timeout at 46002 (≥ 60 s — the master samples that block slowly and a shorter session expires unseen) and a 60 s driver-side command lease; `driver_default_mode` releases remote control explicitly and SELF_USE is the only fallback work mode. `PV_AC_EFF = 0.977` is calibrated from two held-zero hardware points (−70 W residual at 2455 W PV, +71 W at 4025 W) — the efficiency curve rises with load, so one constant lands within ±25 W across the daytime range, erring toward a few watts of charge rather than a steady drain. 0.8.0 adds `pv-curtail` (`curtail` / `curtail_disable`), gated on the operator's `supports_pv_curtail` opt-in: the cap is a ceiling on the same AC setpoint — on a hybrid, PV beyond the cap charges the battery first (up to the live BMS limit) and genuine curtailment begins past that, which is the ordering the negative-export guard wants; a curtail-only session holds battery-at-zero under the ceiling in daylight and stands down at night so it cannot block self-use discharge. 0.9.0 implements the **control v2 contract** (srcfl/ftw#738/#741) — `driver_command_v2` / `driver_default_mode_v2` return structured results whose "applied"/"defaulted" claims the host verifies against its own write-scope evidence (write ack + setpoint readback), and default mode always writes the release and reads it back; the v1 entrypoints remain for local operator builds, whose default mode keeps the skip-if-not-ours courtesy toward FoxESS-app schedule periods. The migration also surfaced that the host's write bindings return error strings rather than raising, so a bare `pcall` around a write reports failure as success — every write now checks both layers. `drivers/tests/test_foxess_h3_smart_control.py` holds the v2 results to the contract: statuses, codes, device_state, evidence lists, and the registers actually written. Also since 0.1.0: per-phase grid CT (voltage, power, amps per phase), lifetime energy counters, inverter temperature and state metrics, and a fault-code latch on 39067..39069 - **A driver-authoring rule for the hybrid inverter that has no battery** — *A hybrid inverter may have no battery* in `docs/WRITING-A-DRIVER.md`, and rule 8 in `drivers/lua/GUIDELINES.md`. The existing rules cover a read that **failed**; this is the case where nothing failed — the device is healthy, every register answered, and the battery still is not there. Nearly every hybrid inverter is sold both with storage and without it under one model number and one register map, so a PV-only site is not an edge case, it is half the product line. The SG12RT already cited at the top of that document is this same fact arriving as an outage rather than as a wrong number. The rule: fill each battery field only from a register that answered, emit the DER only when at least one did, and **detect** it rather than reading it off the model number or asking the operator to declare it — the site nobody told the driver about is exactly the one that reports wrong. Records how the absence actually arrives, which is vendor-specific and cannot be assumed from one example: registers that go silent (Sigenergy), that answer a plain zero, that answer `0xFFFF`/`0x7FFFFFFF`/NaN (`sma` and `solis` already carry sentinel helpers), or that fault diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index ff1ffc8..c12e7f3 100644 --- a/SUPPORT_STATUS.md +++ b/SUPPORT_STATUS.md @@ -50,8 +50,8 @@ Catalog source is not proof that a target can install or run a driver. | ferroamp_dc2_v2x | 2.1.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | ferroamp_modbus | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | ferroamp_modbus | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| foxess_h3_smart | 0.9.0 | ftw-core | not_assessed | 0.9.0 | — | not_recorded | — | not_assessed | yes | -| foxess_h3_smart | 0.9.0 | blixt-l1 | not_assessed | 0.9.0 | — | not_recorded | — | not_assessed | yes | +| foxess_h3_smart | 0.9.1 | ftw-core | not_assessed | 0.9.1 | — | not_recorded | — | not_assessed | yes | +| foxess_h3_smart | 0.9.1 | blixt-l1 | not_assessed | 0.9.1 | — | 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_api | 1.0.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | diff --git a/devices.yaml b/devices.yaml index 5608a15..0bc2645 100644 --- a/devices.yaml +++ b/devices.yaml @@ -393,7 +393,7 @@ manufacturers: protocols: - protocol: modbus driver: "foxess_h3_smart" - version: "0.9.0" + version: "0.9.1" ders: [pv, battery, meter] control: true firmware_versions: "" @@ -404,7 +404,7 @@ manufacturers: protocols: - protocol: modbus driver: "foxess_h3_smart" - version: "0.9.0" + version: "0.9.1" ders: [pv, battery, meter] control: true firmware_versions: "" diff --git a/drivers/lua/foxess_h3_smart.lua b/drivers/lua/foxess_h3_smart.lua index 5a314bc..4a58a83 100644 --- a/drivers/lua/foxess_h3_smart.lua +++ b/drivers/lua/foxess_h3_smart.lua @@ -20,8 +20,11 @@ -- The distinct H1/H3 (11000-range) register map lives in the separate -- `foxess` driver. -- --- LOCAL CONTROL BUILD (operator's own risk, not the signed channel): --- battery dispatch through the vendor remote-control block. +-- CONTROL DRIVER: battery dispatch and PV curtailment through the +-- vendor remote-control block. Ships through the signed channel with +-- control intact (the catalog marks it control: true); it began as a +-- local operator override and was hardware-validated in that role +-- before publishing. -- -- ============================ SEMANTICS ============================ -- The setpoint at 46003/46004 is the INVERTER'S AC ACTIVE POWER, @@ -128,15 +131,16 @@ DRIVER = { id = "foxess_h3_smart", name = "FoxESS H3-Smart / 1K5", manufacturer = "Fox ESS", - version = "0.9.0", + version = "0.9.1", host_api_min = 1, host_api_max = 2, protocols = { "modbus" }, capabilities = { "pv", "battery", "meter", "pv-curtail" }, - description = "Fox ESS H3-Smart register map: 1K5-HI series and H3-Smart three-phase hybrids. Modbus-TCP port 502, unit 247. Local control build: battery dispatch via the remote-control block.", + description = "Fox ESS H3-Smart register map: 1K5-HI series and H3-Smart three-phase hybrids. Modbus-TCP port 502, unit 247. Battery dispatch and PV curtailment via the vendor remote-control block.", authors = { "Sourceful Labs AB" }, tested_models = { "1K5-HI-10-V1" }, - verification_status = "experimental", + verification_status = "production", + verification_notes = "Verified on 1K5-HI-10-V1 hardware over multi-day operation: telemetry (PV per-string, battery, per-phase grid CT, energy counters), battery dispatch in charge, discharge and hold, and PV curtailment. H3-Smart family shares the register map but has not been tested on H3-Smart hardware.", read_only = false, } @@ -150,7 +154,7 @@ PROTOCOL = "modbus" -- other field here. DRIVER_MANIFEST = { name = "foxess_h3_smart", - version = "0.9.0", + version = "0.9.1", role = "inverter", requires = {}, options = {}, diff --git a/index.yaml b/index.yaml index 45e0e52..f1783c5 100644 --- a/index.yaml +++ b/index.yaml @@ -214,14 +214,14 @@ drivers: size_bytes: 14450 sha256: "8be39a1d41b18b1e12fb9b669e6fe11c93775abfdb4f41338f9dc79fe66fd2d9" - name: "foxess_h3_smart" - version: "0.9.0" + version: "0.9.1" tier: community protocol: modbus connectivity: local ders: [pv, battery, meter] control: true - size_bytes: 33379 - sha256: "e4d50a700df28bcb43d4251996b294274cb50d04e6e47d3047470200a90c1e74" + size_bytes: 33859 + sha256: "1b6769f4e2583866603ba2fa71f6d3eaa972cc14f5556f99fe9b04c9a194a0b5" - name: "fronius" version: "2.1.1" tier: core diff --git a/manifests/foxess_h3_smart.yaml b/manifests/foxess_h3_smart.yaml index 5d193aa..69d6746 100644 --- a/manifests/foxess_h3_smart.yaml +++ b/manifests/foxess_h3_smart.yaml @@ -1,5 +1,5 @@ name: "foxess_h3_smart" -version: "0.9.0" +version: "0.9.1" tier: community author: "Sourceful Labs AB" protocol: modbus @@ -27,9 +27,9 @@ upstream_docs: kind: register_map url_stability: stable min_host_version: "1.5.0" -size_bytes: 33379 +size_bytes: 33859 dkb_id: "" -sha256: "e4d50a700df28bcb43d4251996b294274cb50d04e6e47d3047470200a90c1e74" +sha256: "1b6769f4e2583866603ba2fa71f6d3eaa972cc14f5556f99fe9b04c9a194a0b5" signature: "" bytecode_sha256: "" bytecode_signature: "" diff --git a/packages/v1/foxess_h3_smart/package-source.json b/packages/v1/foxess_h3_smart/package-source.json index 0a43d3b..588856c 100644 --- a/packages/v1/foxess_h3_smart/package-source.json +++ b/packages/v1/foxess_h3_smart/package-source.json @@ -1,7 +1,7 @@ { "schema_version": "sourceful.driver-package-source/v1", "package_id": "com.sourceful.driver.foxess-h3-smart", - "version": "0.9.0", + "version": "0.9.1", "channel": "beta", "display_name": "FoxESS H3-Smart / 1K5", "identity": { diff --git a/support-status.json b/support-status.json index 39f986d..ccc8f9e 100644 --- a/support-status.json +++ b/support-status.json @@ -646,12 +646,12 @@ }, { "catalog_source": true, - "catalog_version": "0.9.0", + "catalog_version": "0.9.1", "driver_id": "foxess_h3_smart", "package_id": "com.sourceful.driver.foxess-h3-smart", "targets": { "blixt-l1": { - "candidate_package_version": "0.9.0", + "candidate_package_version": "0.9.1", "control_enabled": true, "hil": "not_recorded", "historical_signed_beta_version": null, @@ -661,7 +661,7 @@ "target_conformance": "not_assessed" }, "ftw-core": { - "candidate_package_version": "0.9.0", + "candidate_package_version": "0.9.1", "control_enabled": true, "hil": "not_recorded", "historical_signed_beta_version": null,