Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html

## [Unreleased]

### Fixed
- **sungrow** 1.5.6 — **the zero-power refusal is restored. 1.5.4 was wrong, on a premise that did not survive reading the driver.** The case for waving zero through was that it hands the device back to itself, so refusing it could strand an inverter in a forced state. Neither half holds. `set_battery_idle` writes EMS mode **2** — forced — with command `0xCC` and setpoint 0: it pins the battery at zero under the host's control rather than releasing it. The release is `driver_default_mode`, which FTW reaches through `SendDefault` on shutdown, lease expiry and the watchdog, never through `driver_command`, and which no guard touches. So refusing withholds nothing that is not reachable by a route that cannot refuse — while accepting put mode 2 and a setpoint back on an SG inverter that implements neither, and reported success, because the read-back that would catch it fails on that device too. That is the SG12RT bug through the one action nobody guarded
- The two sungrow copies disagree about what zero writes — the catalog driver forces idle at mode 2, the FTW v2 target hands the inverter back at mode 0 and clears the lease — and they use different register maps throughout. That belongs to the register-map review `packages/v1/sungrow/PILOT.md` already requires before the v2 target ships; its `verification_status` is `experimental` and the signed package builds from `targets/ftw-observe.lua`, so nothing there reaches hardware today. Recorded because untested drift between these two files is how Pixii's 40288 came back
- **FTW v2 target** 1.3.4 → 1.3.5 — `driver_command_v2`'s matching zero exemption is removed in the same commit. Raised so a package build cannot claim bytes under a version number that was never assigned to them

### Fixed
- **sungrow** 1.5.5 — **Startup asked a family with no battery about its battery limits.** `configure_power_limits` reads 33046, 33047 and 13057 — charge power, discharge power, and the SoC ceiling and floor — and raises the first two when the unit shipped capped. A model that named itself a string inverter answers none of them, so an SG12RT paid three failed reads on every restart to learn nothing. It now skips them. These run once rather than on every poll, so this is not the SG12RT outage returning; the reason to gate them is that they are free to gate. Each write is already conditional on its read having answered, and none of them clears a forced state
- **The wider version of this change was wrong, and the test that says so ships with it.** The first draft gated `set_self_consumption` on the same family test, covering `driver_init`'s startup reset, the watchdog, `driver_cleanup` and `deinit` — three writes at startup and three on every watchdog tick, aimed at a block the family has no use for. But since 1.5.4 a zero-watt command is accepted whatever the family, and it writes EMS mode 2. So "the device named itself a string inverter" is not proof there is nothing to release: Sungrow shipping a hybrid under a device-type code `classify_device_type` has not been taught lands in exactly that branch, takes the forced write, and is then left with no path that writes mode 0 back. Measured: mode 2 survived the watchdog and cleanup both. `test_the_release_is_still_reachable_on_a_string_inverter` fails if the gate is put back
Expand Down
4 changes: 2 additions & 2 deletions SUPPORT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ Catalog source is not proof that a target can install or run a driver.
| solis_string | 1.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| sonnen | 2.0.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
| sonnen | 2.0.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| sungrow | 1.5.5 | ftw-core | observe_only_profile_blocked | 1.3.3 | 1.3.0 | exact_model_firmware_required | — | not_assessed | no |
| sungrow | 1.5.5 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| sungrow | 1.5.6 | ftw-core | observe_only_profile_blocked | 1.3.3 | 1.3.0 | exact_model_firmware_required | — | not_assessed | no |
| sungrow | 1.5.6 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| tesla_vehicle | 0.2.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
| tesla_vehicle | 0.2.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no |
| tibber | 1.1.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
Expand Down
10 changes: 5 additions & 5 deletions devices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1722,7 +1722,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.5"
version: "1.5.6"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1733,7 +1733,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.5"
version: "1.5.6"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1744,7 +1744,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.5"
version: "1.5.6"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1755,7 +1755,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.5"
version: "1.5.6"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1766,7 +1766,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.5"
version: "1.5.6"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand Down
31 changes: 9 additions & 22 deletions drivers/lua/sungrow.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DRIVER = {
id = "sungrow-shx",
name = "Sungrow SH Hybrid Inverter",
manufacturer = "Sungrow",
version = "1.5.5",
version = "1.5.6",
protocols = { "modbus" },
capabilities = { "meter", "pv", "battery", "pv-curtail" },
description = "Sungrow SH-series hybrid inverters with LFP battery, via Modbus TCP.",
Expand Down Expand Up @@ -139,13 +139,13 @@ end
-- Whether the device has named itself a model with no battery registers.
--
-- Read this narrowly. It gates startup *reads* of the battery limit block and
-- nothing else. It must never gate a write that clears a forced state: since
-- 1.5.4 a zero-watt command is accepted on any family, so a device this
-- function calls "string" can still be holding EMS mode 2, and
-- set_self_consumption is the only thing that writes mode 0 back. A device
-- classified here is not proof there is nothing to release -- Sungrow shipping
-- a hybrid under a device-type family classify_device_type has not been taught
-- lands in exactly this branch.
-- nothing else. It must never gate a write that clears a forced state, because
-- a device this function calls "string" can still be holding EMS mode 2 --
-- left there by an earlier run, or by a firmware that answers the type
-- register differently after an update -- and set_self_consumption is the only
-- thing that writes mode 0 back. Being classified here is not proof there is
-- nothing to release: a hybrid shipped under a device-type family
-- classify_device_type has not been taught lands in exactly this branch.
local function known_to_have_no_battery()
return model_family == "string"
end
Expand Down Expand Up @@ -747,20 +747,7 @@ function driver_command(action, power_w, cmd)
if action == "init" then
return true
elseif action == "battery" then
-- Zero is not a dispatch. It is the host handing the device back to
-- itself: forced mode off, setpoint nought. Refusing to write "stop"
-- is a different risk from refusing to write "charge" -- a device
-- left in a forced state stays there, and the safe default is the
-- one path that must never be gated on how much we know about the
-- hardware. It also arrives before the first poll, from the
-- lifecycle rather than from the planner, which is exactly when
-- nothing has been confirmed yet.
--
-- On a genuine string inverter this writes to a register block the
-- model does not implement. That write fails at the Modbus layer and
-- costs nothing; the outage it protects against is a battery left
-- charging with no way to say stop.
if power_w ~= 0 and model_family ~= "hybrid" and not battery_confirmed then
if model_family ~= "hybrid" and not battery_confirmed then
local why = model_family == "string"
and "this model has no battery registers"
or "no battery register has answered on this device"
Expand Down
Loading