Packaged as a Home Assistant add-on, with a field guide

Turns the reference RS485 controller into something a technician can install
at a client site: a typed config form instead of YAML, an ingress UI that
names misconfiguration in words, and persistent state that cannot be broken by
a timezone.

Why an add-on rather than YAML packages or blueprints:

- Blueprints cannot create helpers, and the maintenance cycle is a state
  machine whose phase and completion date must survive restarts.
- YAML packages need filesystem access, a configuration.yaml edit and a
  restart - none of which belong in a client install.
- Add-ons authenticate with SUPERVISOR_TOKEN, so there is no long-lived token
  to generate, store or leak on someone else's machine.
- Requires HA OS/Supervised. Container and Core installs cannot run add-ons at
  all, which is a market decision, not an oversight.

The control law and the maintenance machine are pure functions with no Home
Assistant imports, and both ship with runnable checks (22 and 22 assertions).
Every assertion corresponds to a rule whose absence caused an observed failure
on hardware - the saturation duration term, the clamp-before-slew ordering, the
deadband, the sign convention.

One behaviour deliberately differs from the implementation it replaces: when
its inputs go missing this commands 0 W rather than replaying the last
setpoint. The reference version kept replaying, which the hardware watchdog
cannot catch - from the ESP32's side, Home Assistant is still talking to it.

Includes the ESPHome firmware (now parameterised: node name, inverter rating,
watchdog timeout) and the optional RS485 e-stop. FIELD-GUIDE.md carries the
commissioning gates, all judged on the wire rather than on how Home Assistant
looks, plus the written statement a site without an e-stop needs signed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NckgXecasQb2eSsPYNSW6
This commit is contained in:
2026-08-23 01:15:25 +02:00
co-authored by Claude Opus 5
commit 0a1e61dbc9
24 changed files with 2737 additions and 0 deletions
+382
View File
@@ -0,0 +1,382 @@
# GoodWe RS485 Controller — Field Guide
For installing technicians. Read section 1 before the first site visit; work
through sections 511 in order at the site; do not sign off until section 12 is
complete.
---
## 1. The one fact this whole product is built around
**The inverter holds the last command it understood, forever.** It has no
meter-timeout of its own. If whatever is driving it stops talking, it does not
fall back to idle, to a safe value, or to anything else — it keeps doing exactly
what it was last told, indefinitely.
This is not a theory. On the reference system a controller went silent
mid-command and the inverter held **5 kW of discharge for 113 seconds**, until a
person noticed and intervened.
Everything else in this guide follows from that:
| layer | covers | where it lives |
|---|---|---|
| 1 — watchdog | controller alive but silent | ESP32 firmware: no fresh setpoint for 30 s → command 0 W **and keep commanding it** |
| 2 — wind-down | planned firmware update | ESP32 firmware: 0 W written *before* the update starts |
| 3 — e-stop | **the controller or its host is dead** | optional RS485 e-stop: writes 0 W after 30 s of total bus silence |
**Layer 3 is the only thing that covers the Home Assistant machine dying.**
Nothing running on that machine can cover its own death. On a site without the
e-stop, a failed Pi or a pulled plug leaves the battery running at whatever it
was last commanded until a human intervenes. Section 10 tells you what to say
about that, in writing.
> Stopping is the failure mode, not the fix. Anything in this system that finds
> itself in doubt must command **0 W** — never hold the last value "to be safe".
---
## 2. Before you go: what to confirm with the client
Do not treat these as formalities. Each has ended an installation.
- [ ] **Inverter is a GoodWe ES / BP family unit** (the AA55 / RS485 meter-bus
generation). Other GoodWe families use a different protocol and are not
supported.
- [ ] **There is an existing controller on the meter bus** (vendor box emulating
a smart meter). Confirm what it is, and that the client accepts it being
**disconnected**.
- [ ] **Warranty and installer agreement.** Replacing the vendor controller may
affect both. Get the client's written acknowledgement. This is a
commercial question, and it is cheaper to ask than to discover.
- [ ] **Grid connection rules.** In Belgium the installation is governed
(Synergrid C10/11). Modifying how the inverter is driven can touch the DSO
agreement. Confirm the client's position before quoting.
- [ ] **Any active vendor subscription** — cancel *after* the replacement is
proven, not before.
- [ ] **Home Assistant is HA OS or Supervised.** Add-ons cannot be installed on
HA Container or Core. Check Settings → System → Repairs → System
information. **If it says Container, this product cannot be installed.**
- [ ] **A working grid-power sensor exists in HA** with a fast update
(≤ ~10 s). HomeWizard P1, DSMR, Shelly EM are all fine. Note its update
rate — see §9.
- [ ] **E-stop fitted or not** — and if not, that the client has signed the
statement in §10.
---
## 3. What is in the box
**Base SKU**
- T-CAN485 (ESP32 + RS485 transceiver), pre-flashed or flashed on site
- Wiring loom to the inverter's meter port
- The add-on (installed from your repository URL)
**E-stop upgrade**
- Raspberry Pi (any model with USB) + USB-RS485 adapter
- Pre-loaded `rs485_log.py`
- Tap wiring to the same bus
---
## 4. How the pieces fit
```
grid meter ──► Home Assistant ──► GoodWe RS485 Controller add-on
(client's) (client's) │
│ number.<node>_goodwe_setpoint_w
T-CAN485 (ESP32)
│ RS485, 9600 8N1, AA55/Modbus
GoodWe inverter meter port
│ passive tap + emergency write
Pi e-stop (optional)
```
The add-on never talks to the inverter directly. It writes one number; the ESP32
turns that into meter frames at a steady cadence and owns the safety timing.
---
## 5. Site survey and safety
⚠️ **Qualified persons only.** The inverter carries mains AC and battery DC.
The meter bus itself is low-voltage, but you are working inside an energised
installation.
1. Photograph the existing wiring at the inverter's meter port **before**
touching anything.
2. Identify the RS485 pair (A/B) going to the vendor controller.
3. Note the inverter model and serial from its label.
4. Record the battery: capacity (Ah), nominal voltage, and the inverter's
depth-of-discharge setting if visible.
**Never do these:**
- ❌ Do not flash third-party firmware onto the inverter's WiFi dongle. The
widely circulated image targets different hardware and will brick it.
- ❌ Do not long-press (35 s) the inverter's WiFi Reset/Reload button. It
factory-resets the dongle and you lose network access to it. A short press
(~1 s) is safe.
- ❌ Do not connect our controller while the vendor controller is still
attached. **Two masters on one bus is the one configuration that can produce
contradictory commands.** Disconnect the vendor box first, at 0 W.
---
## 6. Hardware installation
1. **Bring the system to 0 W.** Set the vendor controller to idle if it allows
it, or simply confirm on the inverter display that the battery is neither
charging nor discharging.
2. **Disconnect the vendor controller** from the meter port. Leave it physically
installed but disconnected if the client wants a reversible install — that
also gives you a rollback story if you ever need one.
3. **Wire the T-CAN485** to the meter port: A→A, B→B, plus its own power supply.
Observe polarity; swapped A/B produces a completely silent bus, not an error.
4. If fitting the **e-stop**, connect the USB-RS485 adapter to the *same* pair,
in parallel. It is passive until it decides to act.
5. Power up the T-CAN485 **last**.
⚠️ It begins transmitting within seconds of boot. Never power it up while the
vendor controller is still connected.
---
## 7. Flash the ESP32
Use the ESPHome Device Builder add-on on the client's HA, or your own laptop.
1. Copy `firmware/goodwe-master.yaml` into ESPHome.
2. Set the three substitutions at the top — **and only those**:
- `name` — the node name. Write it on the commissioning sheet. **Changing it
later renames every entity in HA and silently breaks the add-on's config.**
- `max_w` — the inverter's continuous rating (e.g. `5000`). This is a hard
firmware limit, independent of anything HA asks for, and it is the last
line of defence against a controller bug.
- `wd_ms` — watchdog timeout, default `30000`. Must stay comfortably above
the add-on's heartbeat.
3. Add the client's WiFi credentials to ESPHome's `secrets.yaml`.
4. Install. First flash is by USB; everything after that is over the air.
⚠️ **A firmware update that lands new shutdown-path code still runs the OLD code
on the way out.** If you change the wind-down or watchdog behaviour, upload
twice before believing a test result.
---
## 8. Install and configure the add-on
1. Settings → Add-ons → Add-on store → ⋮ → **Repositories** → add your repo URL.
2. Install **GoodWe RS485 Controller**. Do not start it yet.
3. Open **Configuration** and fill in:
| option | what to put | notes |
|---|---|---|
| `meter_entity` | the client's grid power sensor | **+ must mean importing.** If theirs is the other way round, set `meter_invert` |
| `soc_entity` | `sensor.<node>_goodwe_battery_soc` | **the ESP32's own read**, not the inverter's cloud/dongle sensor |
| `batt_entity` | `sensor.<node>_goodwe_inverter_ac_power` | same — the ESP32's read |
| `setpoint_entity` | `number.<node>_goodwe_setpoint_w` | what the add-on writes |
| `max_w` | start at **1000** for commissioning | raise after §9 passes |
| `estop_fitted` | true only if you actually fitted one | drives the warning banner |
| `peak_forecast_entity` | capacity-tariff sites only | leave empty elsewhere |
| `price_now_entity` / `price_avg_entity` | dynamic-tariff sites only | leave empty on fixed tariffs |
⚠️ **Get the entity ids exactly right.** Home Assistant prefixes entity ids with
the device's *area* at creation time, so the same firmware produces
`sensor.goodwe_master_...` on one site and `sensor.cellar_goodwe_master_...` on
another. **A wrong entity id is not an error anywhere in HA** — it simply never
produces a value. On the reference install two safety alarms pointed at
non-existent entities and were dead for a day while their state read "on".
Copy ids from Developer Tools → States. Do not type them from memory.
4. Start the add-on and open its **Web UI** (ingress panel).
5. Work down the **Commissioning** list until every line is green. It tells you
what is wrong in words. Do not proceed while anything is red.
---
## 9. Commissioning — the acceptance gates
**Every gate is judged on the wire or the meter, never on "it looks right in
Home Assistant".** If you fitted the e-stop, its log is your witness; if not,
use the inverter's own display and the client's meter.
### Gate 1 — the inverter answers us
Add-on running, control **stopped**. Expect a steady 0 W command being written,
the inverter idle, no errors in the add-on log.
If the e-stop is fitted, `bus.log` shows our write frames being ACKed:
```
F7 10 05 6E 00 02 04 00 03 00 00 A6 D0 our write, 0 W
F7 10 05 6E 00 02 34 4F inverter ACK
```
✅ Pass: frames ACKed, no CRC errors, inverter idle.
### Gate 2 — we can move power, both directions
With control still stopped, set the setpoint by hand from Developer Tools
(`number.set_value` on the setpoint entity):
- **+300 W** → battery discharges ~300 W within ~10 s
- **300 W** → battery charges ~300 W
✅ Pass: both signs work and the inverter's reported power follows within ~10 s.
❌ If the sign is inverted, fix `batt_invert` / your wiring — **do not**
"compensate" in the tuning.
### Gate 3 — telemetry agrees
Compare the ESP32's readings against the inverter display or the vendor app:
state of charge should match exactly; power within a few percent (conversion
loss). ✅ Pass: SoC identical, power within ~5 %.
### Gate 4 — the watchdog (**the important one**)
Set +300 W by hand, confirm it is running, then **stop the add-on**.
✅ Pass: the inverter reaches **0 W within ~30 s** and stays there.
❌ Fail: anything still moving after a minute. Stop the installation and
investigate — without this, nothing else in this product is safe.
Restart the add-on afterwards.
### Gate 5 — the closed loop
Set `max_w` to 1000, start control from the Web UI, and watch the client's grid
power.
✅ Pass: grid settles to within a few tens of watts of zero and *stays* there,
with the command resting rather than hunting continuously.
### Gate 6 — a real load step
Switch on a kettle or oven (~2 kW).
✅ Pass: grid returns to near zero within ~20 s, and the command does **not**
keep climbing after the battery has caught up. A command that keeps rising while
the battery is pinned is runaway — stop immediately and see §13.
Then raise `max_w` to the value the site is sold with (typically the inverter
rating) and repeat Gate 6 once.
### Gate 7 — the maintenance cycle
See §11. **Do not sign off without it.**
---
## 10. The e-stop, and what it means when it is absent
**Fitted:** connect the Pi to the same RS485 pair, power it, and run:
```bash
python3 rs485_log.py --out /home/pi/bus.log --panic
```
Confirm it prints `PANIC ARMED`. Test it: with the battery at +300 W, cut power
to the T-CAN485. Within ~35 s the log must show a ` TX ` line and the inverter
must go to 0 W.
⚠️ Arm the panic write **only** while our controller owns the bus. Pointed at a
bus somebody else is driving, it is unrequested interference.
⚠️ Expect it to fire during every firmware update — the upload silence exceeds
30 s. That is correct behaviour, not a fault. Do not raise the threshold to
silence it.
**Not fitted — put this in front of the client, in writing:**
> Without the RS485 e-stop, if the Home Assistant machine fails, loses power, or
> its storage fails, the battery inverter will continue charging or discharging
> at whatever level it was last commanded, indefinitely, until someone
> intervenes manually. The inverter has no automatic fallback of its own. The
> e-stop is the only component that prevents this.
Have them acknowledge it. Note it on the commissioning sheet.
---
## 11. Prove the maintenance cycle
The monthly cycle takes the battery low, then charges it fully and holds it
there. It exists so the BMS can **balance cells** and **recalibrate its coulomb
counter**. Skipping it breaks nothing visibly — it degrades the pack over months,
and the first symptom is a state-of-charge reading nobody can trust.
**Do not wait a month to discover the schedule does not fire.** Force one:
1. Web UI → **Force maintenance cycle**.
2. Watch the phase go `drain → charge → hold → idle`.
3. Confirm the inverter actually exports during `drain`, actually charges during
`charge`, and sits at 0 W during `hold`.
A full cycle takes hours. If the client cannot spare the time on the day,
temporarily set `maintenance_soc_floor` just below current SoC and
`maintenance_soc_target` just below it again, plus `maintenance_hold_min: 5`
that walks the whole state machine in about ten minutes. **Put the real values
back afterwards and note it on the sheet as a partial test.**
Finally set `maintenance_enabled: true`.
---
## 12. Handover and sign-off
- [ ] All gates in §9 passed, on the wire or the meter
- [ ] Maintenance cycle proven (full or partial — state which)
- [ ] E-stop fitted and tested, **or** client acknowledgement signed (§10)
- [ ] `max_w` set to the agreed value
- [ ] Node name, entity ids and add-on version written on the sheet
- [ ] Client shown: the Web UI, the start/stop button, and what "stopped" means
- [ ] Client told: **if anything looks wrong, stop the add-on** — that commands
0 W and the battery idles safely
- [ ] Vendor subscription cancelled only *after* the above
---
## 13. Troubleshooting
| symptom | likely cause | what to do |
|---|---|---|
| Web UI says **NOT READY** | an entity id is wrong or the sensor is unavailable | The banner names the failing item. Copy the id from Developer Tools → States |
| Everything looks configured but nothing moves | control is stopped | Press start in the Web UI; the banner should turn green |
| Inverter does nothing, no errors anywhere | A/B swapped, or the vendor controller is still connected | Swapped RS485 gives a *silent* bus, not an error |
| HA shows one setpoint, the inverter does another | the firmware `max_w` is lower than the add-on's `max_w` | The firmware wins by design. Raise it there, or lower it in the add-on |
| Writes rejected in the log (HTTP 400) | value outside the number entity's range | Lower `max_w`; check the firmware substitution |
| Command keeps climbing while the battery is pinned | **runaway** — sign inverted, or saturation detection defeated | **Stop the add-on immediately.** Verify `meter_invert` with a known load |
| Grid hunts continuously, never rests | deadband too small for that meter, or the meter is slow | Raise `deadband_w`; if the meter updates slower than ~10 s, lower `gain` |
| Battery goes flat overnight and stays flat | maintenance `drain` phase never exited | Check the SoC entity is the ESP32's, not a cached cloud value |
| Maintenance never runs | schedule disabled, or never became due | Check `maintenance_enabled`, and the "Maintenance due" row in the Web UI |
| E-stop fires during every update | expected — update silence exceeds 30 s | Nothing to fix. Do not raise the threshold |
| Add-on will not install | HA is Container/Core, not OS/Supervised | Not supportable. See §2 |
**When in doubt: stop the add-on.** That commands 0 W, the watchdog holds it
there, and nothing is at risk while you think.
---
## 14. Why the tuning is what it is
Do not change these without measuring. Every value came from hardware.
- **`gain` 0.6 per cycle** — a cycle is one meter update (~5 s). The inverter
needs 36 s to settle: ~1.4 s dead time, 94 % of a step by 3.3 s. So the loop's
next correction lands just as the plant arrives. **0.6 is at the limit — do
not raise it**, and do not shorten the cycle below the meter's update rate.
- **`slew_w` 1000 W per cycle** — most of a correction in the first cycle
without letting the command run far ahead of the hardware.
- **`deadband_w` 15 W** — measured residual while regulating: mean 15.4 W, max
27 W. At 10 W, ~69 % of cycles act and the command never rests. A resting
command is a diagnostic asset: "flat for 70 s" is how you recognise a healthy
loop at a glance.
- **`saturation_w` 500 W over `saturation_cycles` 3** — command and reading
diverging means the inverter is at a limit; then the magnitude may fall but
never rise. **The 3-cycle duration term is essential**: tested instantaneously
it fires on every large correction, because the plant itself lags.
- **`step_w` 10 W** — the register is 1 W, but the inverter's response lands on
a coarser ladder (~17.6 W measured at ~900 W). 10 W just avoids a visible
staircase; finer is meaningless.
- **`heartbeat_s` 10 s against a 30 s watchdog** — three chances to be heard
before the hardware takes over.
The failure this tuning is designed against is real: the vendor controller
commanded **14 547 W** against an inverter reporting 5250 W, and kept climbing
for six minutes, because its integrator never stopped.