Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
- **`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
- **nibe_local** 1.2.0 — the driver's first write path, and deliberately its only one: the pump's native **Solar PV surplus feed** ([srcfl/ftw#537](https://github.com/srcfl/ftw/issues/537)). The S-series was built to take a live "available solar power" number from NIBE's Modbus accessory (registers 2107/2109) and soak the surplus into heating and hot water using owner-tuned offsets; FTW now acts as that accessory. Control-by-hint: the pump's firmware decides what to do with the number, so a wrong value degrades to wasted comfort, never to unsafe operation. Off by default and triple-gated — host `capabilities.http.allow_write`, driver `write.solar_pv: true` with a mandatory `write.max_w` clamp ceiling, and the owner-side enable on the pump itself. The pump's timeout for a silently stopped feed is undocumented, so the driver does not lean on it: a dead-man's switch clears the feed when commands stop, `driver_default_mode` clears it on watchdog/stale-meter/stop, and a startup sweep clears a feed a crashed run left behind — and the sweep stays armed even when a config mistake (missing `max_w`) refuses new writes. All of that runs only while FTW runs, so the driver header documents the decommission step (turn 2107 off, or set the API read-only in menu 7.5.15) for the day it does not. The write API's most dangerous habit is covered too: the Local REST API rejects writes *inside an HTTP 200* ("error: read only value"), which the driver surfaces as an actionable error naming the installer menu (7.5.15) instead of reporting success. Requires `host.http_patch` from the FTW core; on older cores the driver states so and stays read-only. The `DRIVER` block declares the path as `write_capabilities = { "solar_pv" }`, which is what lets a host offer a switch for it: FTW's Settings screen renders the feed's controls only for a driver that states it has a write path, so the alternative was an owner hand-editing two keys in `config.yaml` to use the feature at all
- **`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
- The rule is stated with the catalog measured rather than asserted: **24 drivers emit both `pv` and `battery`, and 20 of them emit the battery DER with no guard on whether any battery register answered.** Two of the 20 (`ferroamp`, `zap`) gate it on configuration or on API discovery instead — better than nothing, and still not detection, which is why the rule names the difference. `sigenergy` 1.1.3 below is the worked example of the fix. The other 19 are not touched here: each is its own driver, its own register map and its own version, and a sweep that changes nineteen drivers at once is not reviewable
Expand Down
4 changes: 2 additions & 2 deletions SUPPORT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ Catalog source is not proof that a target can install or run a driver.
| mennekes | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| myuplink | 1.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
| myuplink | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| nibe_local | 1.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
| nibe_local | 1.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| nibe_local | 1.2.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
| nibe_local | 1.2.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| opendtu | 1.0.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
| opendtu | 1.0.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| opendtu_mqtt | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
Expand Down
6 changes: 3 additions & 3 deletions devices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -956,11 +956,11 @@ manufacturers:
protocols:
- protocol: http
driver: "nibe_local"
version: "1.1.2"
version: "1.2.0"
ders: [heatpump]
control: false
control: true
firmware_versions: ""
notes: "Read-only NIBE S-series heat-pump telemetry over the on-prem Local REST API (HTTPS + Basic auth, self-signed cert pinned via tls_pin_sha256). Emits compressor/used power, lifetime energy meters, and the full ~980-point register map. Observe-only — no control."
notes: "NIBE S-series heat-pump telemetry over the on-prem Local REST API (HTTPS + Basic auth, self-signed cert pinned via tls_pin_sha256). Emits compressor/used power, lifetime energy meters, and the full ~980-point register map. Read-only by default; opt-in write path feeds the pump's native Solar PV surplus input (2107/2109)."
- name: "OpenEVSE"
model_families:
- name: "OpenEVSE"
Expand Down
Loading