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
383 lines
18 KiB
Markdown
383 lines
18 KiB
Markdown
# GoodWe RS485 Controller — Field Guide
|
||
|
||
For installing technicians. Read section 1 before the first site visit; work
|
||
through sections 5–11 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 (3–5 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 3–6 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.
|