Field guide as a 10-page A4 PDF, one page per step
Rebuilt the guide as something usable on site rather than a document to read at a desk: strict order of operations, DO / VERIFY / IF NOT blocks per step, pass-fail gates in a table, a printable sign-off sheet, and troubleshooting keyed by symptom rather than by subsystem. Includes real screenshots of the add-on's own Web UI - captured by running it against stub data - showing the healthy state and the misconfigured one side by side, because telling those apart at a glance is the single most useful skill on site. Plus the ESPHome step from a live builder. The text lives only in docs/build_guide.py, which renders the HTML and drives headless Chrome to produce the PDF. One source: a Markdown copy would inevitably drift from the PDF someone is holding in a cellar. Pagination is enforced, not hoped for. Each section must render under the A4 printable height (1039 px at 96 dpi) or Chrome silently spills it onto a second page and the page numbers stop matching the step numbers. Measured every section rather than eyeballing it: the Web UI reference page was 1170 px and is now 1017 px, and the PDF comes out at exactly one page per section. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016NckgXecasQb2eSsPYNSW6
This commit is contained in:
+27
-368
@@ -1,382 +1,41 @@
|
||||
# GoodWe RS485 Controller — Field Guide
|
||||
# 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.
|
||||
The guide technicians carry is the PDF:
|
||||
|
||||
---
|
||||
**`docs/GoodWe-RS485-Field-Guide.pdf`** — 10 pages, A4, print it double-sided.
|
||||
|
||||
## 1. The one fact this whole product is built around
|
||||
It covers, in the order the work is done: pre-visit checks, hardware, firmware,
|
||||
add-on install and configuration, how to read the Web UI, the six commissioning
|
||||
gates, the e-stop (and the statement a site without one must sign), proving a
|
||||
maintenance cycle, handover with a sign-off sheet, and troubleshooting.
|
||||
|
||||
**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.
|
||||
## Editing it
|
||||
|
||||
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:
|
||||
The text lives in `docs/build_guide.py` — deliberately one source, so a Markdown
|
||||
copy cannot drift from the PDF someone is holding in a cellar.
|
||||
|
||||
```bash
|
||||
python3 rs485_log.py --out /home/pi/bus.log --panic
|
||||
python3 docs/build_guide.py # regenerates field-guide.html and the PDF
|
||||
```
|
||||
|
||||
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.
|
||||
Requires Chrome or Chromium (used headless for print-to-PDF). Screenshots live
|
||||
in `docs/img/` and are base64-embedded, so `field-guide.html` is standalone.
|
||||
|
||||
⚠️ Arm the panic write **only** while our controller owns the bus. Pointed at a
|
||||
bus somebody else is driving, it is unrequested interference.
|
||||
⚠️ **Every section must stay under the A4 printable height** (1039 px at 96 dpi
|
||||
with the configured margins). Chrome silently spills an over-long section onto a
|
||||
second page, and the pagination stops matching the step numbers. After editing,
|
||||
open `field-guide.html` and check:
|
||||
|
||||
⚠️ 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.
|
||||
```js
|
||||
document.querySelectorAll('.page').forEach(el =>
|
||||
console.log(el.getBoundingClientRect().height)) // each must be < 1039
|
||||
```
|
||||
|
||||
**Not fitted — put this in front of the client, in writing:**
|
||||
The PDF should always come out at exactly **one page per section**.
|
||||
|
||||
> 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.
|
||||
## Regenerating the screenshots
|
||||
|
||||
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.
|
||||
`docs/img/ui-running.png` and `ui-notready.png` are real captures of the add-on's
|
||||
own Web UI, taken by running it against stub data. If the UI changes, retake them
|
||||
— a field guide showing a screen that no longer exists is worse than one with no
|
||||
screenshots at all.
|
||||
|
||||
Reference in New Issue
Block a user