glenn schrooyenandClaude Opus 5 98109a9b91 TEL-05: read the meter, not Home Assistant's opinion of the meter
A fourth meter_source, `homewizard_local`, polling a HomeWizard P1's own
local API (GET /api/v1/data) instead of watching an HA entity.

The point is the age sensor. sensor.p1_sample_age_s is FW-01's watchdog
input, and on every transport we had it measured "time since the value
CHANGED", not "time since the meter REPORTED". Home Assistant offers
nothing better: a repeated reading emits no state_changed, advances
last_reported on neither serialiser, and state_reported cannot be
subscribed to at all. Measured twice - 70 s of a frozen meter on the
ENV-01 rig, and ten repeated readings against the live house. Our own
capture of this house's meter goes 42.2 s and 97.0 s between changes,
both past the default meter_max_age_s of 30, so the age sensor would
have commanded 0 W on a perfectly healthy meter.

Here every HTTP response is an arrival. The meter answered, now, with
its current reading; whether the number moved is not consulted. Five
identical readings are five arrivals.

Reuses TEL-01's pipeline rather than restructuring it: same split_signed
sign convention as ha_signed, same make_sample, same ingest stamping,
meter_max_age_s, clock-recomputed age, plausibility bounds and the §20
unsigned-decode rejection. A failed or timed-out poll submits nothing,
so it is a missing reading - never 0 W - and does not reset the age.
meter_poll_s (default 5 s, the meter's own rate) is checked against
meter_max_age_s once at startup, like the ha_signed entity ids.

⚠️ An arrival stamp cannot see a FROZEN meter, and no arrival detector
can - one answering 200 OK with a stale number is arriving. The local
API does expose what HA never had (the total_power_*_kwh registers stop
advancing) and the transport tracks it as `unchanged_s`, but it is
deliberately not folded into the age and not thresholded: this
controller regulates grid toward ~0 W, and at a converged -10 W the
export register needs six minutes to move by its 1 Wh resolution while
the power figure legitimately repeats. Thresholding that would rebuild
the false-trip limit cycle at the exact operating point we aim for.

test_p1.py 179 -> 236 checks. Still defaults to off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
2026-08-25 20:51:23 +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
    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
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. 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
1.6 MiB
Languages
Python 98.4%
Shell 0.9%
Dockerfile 0.7%