Two files, and no third. config.yaml 0.10.1 -> 0.11.0; CHANGELOG.md gains `## 0.11.0` and three corrections to sections below it. A MINOR BUMP, NOT A PATCH. TEL-11 adds an option - `pv_entity`, 75 keys -> 76 in `options:` and `schema:` together - which is the same test 0.8.0, 0.9.0 and 0.10.0 were cut on. Nothing here can refuse a stored blob: the key is `str?` with an `options:` default of "", validate.py does not mention it, no default moved and no enum widened, so a blob that starts the add-on on 0.10.1 starts it on 0.11.0. THE TABLE HAS TWO ROWS AND THE WINDOW HAD FOUR MERGES. CI-9's follow-up (9f6b9f1, `.gitea/workflows/checks.yml` only) and SEC-2 (239e120, `docs/RELEASE.md` + README.md) are excluded under PKG-15's rule - AUDIENCE, NOT IMPORTANCE. Neither file is under `goodwe_controller/`, neither is in the built image, neither is in predeploy_check.py's RUNTIME tuple, and there is no version of this add-on on which an operator can observe either. SEC-2 is the closer call and the section argues it in full rather than asserting it: it is the more consequential ticket of the two - it is why this release is a pull request rather than a push to master - and it is still a procedure for whoever cuts a release, not something the person deciding whether to press Update can act on. Both are named in the section with pointers to PR #102 / checks.yml and PR #108 / docs/RELEASE.md. WHAT THE ENTRIES LET AN OPERATOR DECIDE. SAFETY-56: behaviour byte-identical (6030175ed2ab..., reproduced in the CI image over 2390 capture reads, a 66-cell grid stepped 120 times and six driven worlds), three surfaces named with the old and new wording side by side - the STALE_READING line quoted onto the action log and status page, the SAFETY-37 ERROR log line, and the readiness row that asserted a false fact about a named entity. TEL-11: unset is byte-identical in BOTH the control and telemetry documents, set is byte-identical in the control document only; the three TEL-11 rows of that table were re-measured in the CI image on this tree (control 846a10a5... twice, telemetry a443a6ce... / 4e6d1cb2...). It is stated plainly that pv_entity is not a control input and does not change what the loop does, and that absence publishes `unknown` rather than 0. THREE CORRECTIONS BELOW THE NEW SECTION, WITHOUT REWRITING HISTORY. The 0.10.1 preamble said SAFETY-56 was open; the SAFETY-57 entry said its accusatory diagnosis half was "deliberately untouched"; the 0.10.0 preamble left SAFETY-56 with no pointer while SAFETY-57 had one. All three quotations are what the 0.10.1 image actually prints and are left standing - a reader on 0.10.1 has to recognise them - each now scoped to 0.10.1 and pointed at `## 0.11.0`. VERIFIED. test_version_gate.py part 11 all four: 11a one contents table and the slicer closed (302 lines under '0.11.0'), 11b 0.11.0 == config.yaml, 11c all three RETIRED_PROVIDERS ids named - scoped to a pre-0.10.0 crossing and pointing at `## 0.10.0`, which is where the evidence is - 11d still exactly one `## Unreleased` below, PKG-08's, untouched. 31/31 in the CI image, checks.yml's exact command, purged tree, `:ro`. Not deployed and not installed. Shipping is a separate pull request from release/1.0 into master, per docs/RELEASE.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
GoodWe RS485 Controller — Home Assistant add-on repository
Replaces a GoodWe battery inverter's vendor controller with a local one: holds net grid exchange at zero by emulating the inverter's smart meter over RS485, and runs the monthly battery maintenance cycle the vendor box was doing.
repository.yaml add-on repository metadata
goodwe_controller/ the add-on
config.yaml manifest + options schema (the per-site config form)
app/ the controller
control.py the control law — pure functions, no I/O
maintenance.py the monthly cycle state machine
hass.py Supervisor/Core API access
store.py persistent state in /data
mqtt.py optional status entities
web.py ingress UI — routes only, status and configuration
config_page.py the configuration page's markup, style and script
configui.py its field model, built from the Supervisor's own
schema + translations — owns no key, label or rule
selfconfig.py reading and writing this add-on's own options,
with the validate-first / compare-and-swap gate
main.py orchestration, heartbeat, failsafe behaviour
test_control.py runnable checks — no framework needed
test_maintenance.py runnable checks — walks a full cycle in fake time
DOCS.md the add-on's documentation tab
MUTATION.md how to run a mutation so the result means something
tools/mutation_run.sh the mutation harness - purges bytecode, then grades
tools/supervisor_selfapi_probe.py what the Supervisor really permits and
returns, run against its own release image
firmware/goodwe-master.yaml ESPHome config for the T-CAN485
estop/rs485_log.py optional RS485 e-stop / bus witness
docs/GoodWe-RS485-Field-Guide.pdf the guide techs carry (10pp, A4)
docs/build_guide.py its single source - rebuilds the HTML and PDF
FIELD-GUIDE.md how to edit and rebuild the guide
docs/RELEASE.md how a change reaches the battery - `master` is
protected, releases go via PR from `release/1.0`
Picking this up after a break? Read HANDOVER.md first — current state,
what is known broken, and what to do next.
Install
Settings → Add-ons → Add-on store → ⋮ → Repositories → add
https://gittea.kammenstraatha.duckdns.org/admin/goodwe-addon
then install GoodWe RS485 Controller.
⚠️ The repository must be PUBLIC for this to work. Home Assistant clones it
anonymously; against a private repo the store shows nothing and gives no useful
error. If it must stay private, install locally instead: copy
goodwe_controller/ into the machine's /addons/ folder, then
ha store reload — it appears as a Local add-on.
⚠️ Bump version: in goodwe_controller/config.yaml for every change.
Supervisor keys the built image by that version, so without a bump it silently
reuses the old image and your fix appears not to work — on your bench and on
every client's machine.
Requires Home Assistant OS or Supervised. Add-ons cannot be installed on HA Container or Core.
Read this first
The inverter holds its last command forever. It has no meter-timeout of its own — a controller that dies mid-command leaves the battery running until a human intervenes. Measured on real hardware: 5 kW of discharge held for 113 seconds after a controller went silent.
Three layers exist because of that, and only the third covers the Home Assistant machine itself dying:
- ESP32 watchdog — no fresh setpoint for ~30 s → command 0 W, and keep commanding it.
- Wind-down before firmware updates — 0 W written before the update starts.
- RS485 e-stop (optional) — writes 0 W after 30 s of total bus silence.
Sites sold without the e-stop must have the acknowledgement in FIELD-GUIDE.md
§10 signed.
Development
The control law and the maintenance machine are pure Python with no Home Assistant imports, so they run anywhere:
cd goodwe_controller
python3 test_control.py
python3 test_maintenance.py
Both must pass before shipping any change. To prove a check bites - the way
almost every claim in this repository was established - mutate the module and
run tools/mutation_run.sh, which clears __pycache__ first. It is not
optional: a same-size edit reverted inside one second leaves CPython reusing the
old bytecode, and the run grades code that is not there. goodwe_controller/MUTATION.md
has the mechanism, the measurement, and what the flags do not do. They are not unit-test theatre —
each assertion corresponds to a rule whose absence produced an observed failure
on real hardware, and the comments in control.py say which.
Compatibility
Check the serial number, not the model name. Characters 6-8 of the serial are the platform tag:
| tag | platform | verdict |
|---|---|---|
ESU EMU ESA BPS BPU EMJ IJL |
105 — ES/EM/BP, AA55-era meter bus | supported |
SPB SPN |
745 — SBP G2 (GW…-SBP-20) |
no |
ESN ESC |
745 — ES G2 (GW…-ES-20) |
no |
ETU EHU BTU … |
205/745/753 — ET/EH/BT hybrids | no |
SDT DST MSU NSU … |
PV-only inverters — no battery to control | no |
A "-20" or "G2" suffix means a different platform with a different meter
protocol. The tags come from the goodwe library's PLATFORM_105_MODELS, which
is also what Home Assistant's own integration uses to pick a protocol.
- GoodWe ES / BP family inverters (AA55 / RS485 meter-bus generation)
- The protocol is reverse-engineered. There is no vendor contract, and a firmware change on GoodWe's side could break every installation at once. Say so when you sell it.