adminandClaude Opus 5 660d079bf3 Merge pull request 'TEL-10: the loop's integrator stops being invisible' (#92) from TEL-10 into release/1.0
Reviewed on the pull request and merged there. `checks / unit (pull_request)` run 798 is a real execution, not a skip: all five steps ran and "the add-on's own tests" occupied 08:04:51 to 08:12:38 UTC.

CONTROL BEHAVIOUR IS UNCHANGED, VERIFIED MECHANICALLY RATHER THAN READ. goodwe_controller/app/control.py is blob 180f58a on release/1.0 and on TEL-10 alike, same sha256 of contents, and release/1.0 is a direct ancestor of TEL-10 so there is no merge-base divergence for a change to hide behind. The only executable edit inside cycle() is prev_w=self.target becoming a named local, loop_prev, passed to the same call. Three write sites for self.i_w exist and only two have a telemetry counterpart; the third is the arbiter re-seed at main.py:3049, inside the same branch and immediately followed by compute(), so it cannot leave loop_i_w stale. app/mqtt.py appends to SENSORS and discovery_payloads() walks it in order, so every existing entity keeps its index.

SUITE RE-RUN BY THE REVIEWER, on the MERGE rather than the branch tip, in the CI image on a purged tree, :ro, one file at a time: release/1.0 29/29, this merge 30/30, and the stacked SAFETY-48 30/30 against both bases. All exit 0.

MUTATION EVIDENCE RE-DRIVEN RATHER THAN READ, because the previous round's counts were irreproducible. M1 is 5 reds on TEL-10's tree and 4 on SAFETY-48's, exactly as the header says and for the reason it gives; M2 is confirmed GREEN and is reported as a survivor; M7 is -96 B and 10 reds in test_mqtt.py plus 1 in test_telemetry.py; M9 is a zero-byte reordering and 7 reds. Every claimed count exact.

R6's correction is true and not merely made: part 1's headline now says EIGHT and the slice under it asserts eight entity configs, part 3's NINE headline is backed by a full missing/changed diff that does cover all nine, and the unasked-for fix two lines down reads eight against the eight-prefix slice.

Merging is not deploying. Nothing here reached the inverter and no deploy is authorised by this merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-19 10:54:02 +02:00

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

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:

  1. ESP32 watchdog — no fresh setpoint for ~30 s → command 0 W, and keep commanding it.
  2. Wind-down before firmware updates — 0 W written before the update starts.
  3. 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.
S
Description
No description provided
Readme
22 MiB
Languages
Python 98.1%
JavaScript 1.2%
CSS 0.3%
Shell 0.2%
HTML 0.2%