"""Build the field guide PDF: python3 docs/build_guide.py HTML -> headless Chrome -> A4 PDF. The guide's text lives in this file, so there is exactly one copy of it and no chance of a Markdown version drifting from the PDF a technician actually carries. Screenshots in docs/img/ are base64-embedded, so field-guide.html is standalone. ⚠️ Keep every section under the A4 printable height (1039 px at 96 dpi with the 11 mm margins set below), or Chrome silently spills it onto a second page and the pagination stops matching the steps. To check after editing: document.querySelectorAll(".page").forEach(el => console.log(el.getBoundingClientRect().height)) // must each be < 1039 """ import base64 import os import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) SCRATCH = os.path.join(HERE, "img") OUT_DIR = HERE CHROME = r"C:/Program Files/Google/Chrome/Application/chrome.exe" def img(name): with open(os.path.join(SCRATCH, name), "rb") as fh: return "data:image/png;base64," + base64.b64encode(fh.read()).decode() UI_OK = img("ui-running.png") UI_BAD = img("ui-notready.png") ESPHOME = img("esphome.png") HTML = """
The inverter holds its last command forever. It has no meter-timeout. If the controller stops talking, the battery keeps charging or discharging at whatever it was last told — indefinitely.
Measured: a controller went silent mid-command and the inverter held 5 kW of discharge for 113 seconds until a person intervened.
Do not reorder. Each step assumes the previous one passed.
| Step | Action | Gate to pass |
|---|---|---|
| 0 | Pre-visit checks (client, warranty, HA type) | All boxes ticked |
| 1 | Survey & photograph existing wiring | Photos taken |
| 2 | Bring system to 0 W, disconnect vendor controller | Battery idle |
| 3 | Wire and power the T-CAN485 | Board online |
| 4 | Flash the ESP32 firmware | Entities appear in HA |
| 5 | Install & configure the add-on | All checks green |
| 6 | Commissioning gates 1–6 | Every gate passes |
| 7 | E-stop: fit & test, or get signature | Tested or signed |
| 8 | Prove a maintenance cycle | Phases observed |
| 9 | Handover & sign-off | Sheet complete |
| Layer | Covers | Where it lives |
|---|---|---|
| 1 Watchdog | Controller alive but silent → 0 W, and keeps writing it | ESP32 firmware |
| 2 Wind-down | Planned firmware update → 0 W before it starts | ESP32 firmware |
| 3 E-stop | The HA machine itself dying → 0 W after 30 s of bus silence | Optional Pi + RS485 |
Read the inverter's serial number (label, app, or portal) and take characters 6–8. That three-letter tag is the platform.
| Tag | Platform | Verdict |
|---|---|---|
ESU EMU ESA BPS |
Platform 105 — ES / EM / BP, AA55-era meter bus | SUPPORTED |
SPB SPN | SBP G2 (GW…-SBP-20) — platform 745 | NO |
ESN ESC | ES G2 (GW…-ES-20) — platform 745 | NO |
ETU EHU BTU … | ET / EH / BT hybrids — platform 205/745/753 | NO |
SDT DST MSU NSU … | PV-only inverters — no battery to control | NO |
Reference unit: 95000BPS225W0290 → BPS → supported.
A “-20” or “G2” suffix in the model name means a different platform with a different meter
protocol. If the tag is not on the supported list, stop — do not quote the job.
If HA is Container or Core, add-ons cannot be installed and this
product cannot go in. If the grid sensor updates slower than ~10 s, the default tuning is wrong
for the site — do not proceed without adjusting gain.
| Never | Because |
|---|---|
| Flash third-party firmware on the inverter's WiFi dongle | Common image targets different hardware — bricks it |
| Long-press (3–5 s) the inverter's WiFi Reset button | Factory-resets the dongle, loses network access. Short press (~1 s) is safe |
| Connect our controller while the vendor box is still attached | Two masters on one bus — contradictory commands |
| "Compensate" for an inverted meter sign in the tuning | Fix meter_invert or the wiring. A wrong sign drives the grid away from zero at full gain |
| Raise the e-stop's 30 s threshold to stop it firing during updates | Firing during an update is correct behaviour |
firmware/goodwe-master.yaml.secrets.yaml.number.<node>_goodwe_setpoint_w exists in HA.| Substitution | Set to | Why it matters |
|---|---|---|
name | Node name, e.g. goodwe-master |
Write it on the sheet. Changing it later renames every entity in HA and silently breaks the add-on |
max_w | Inverter continuous rating, e.g. 5000 |
Hard firmware limit, independent of HA. Last line of defence against a controller bug |
wd_ms | 30000 |
Watchdog timeout. Must stay well above the add-on heartbeat (10 s) |
| Option | Set to |
|---|---|
meter_entity | Client's grid power sensor. Positive must mean importing — else set meter_invert |
soc_entity | sensor.<node>_goodwe_battery_soc — the ESP32's own read, never the cloud/dongle sensor |
batt_entity | sensor.<node>_goodwe_inverter_ac_power |
setpoint_entity | number.<node>_goodwe_setpoint_w |
max_w | 1000 for commissioning. Raise at the end of Step 6 |
estop_fitted | true only if you actually fitted one |
peak_forecast_entity | Capacity-tariff sites only, else leave empty |
price_now_entity / price_avg_entity | Dynamic-tariff sites only, else leave empty |
HA prefixes entity ids with the device's area at creation time, so the
same firmware yields 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 system two safety alarms
pointed at non-existent entities and were dead for a day while reading “on”.
Two states you must tell apart at a glance. The banner is the summary; the Commissioning list is the detail.
tracking.
Grid near zero and the commanded figure resting between updates — a command that
moves every single cycle means the deadband is too small for this meter.inputs-missing and Commanded has gone to
0 W: with a source missing, the controller zeroes the inverter rather than guessing.Judge every gate on the wire or the meter — never on how Home Assistant looks.
| # | Do | Pass | If it fails |
|---|---|---|---|
| 1 | Add-on running, control stopped | Steady 0 W written, inverter idle, no errors | Check wiring and Step 5 |
| 2 | By hand (Developer Tools → number.set_value):
+300 W, then −300 W |
Battery discharges ~300 W, then charges ~300 W, within ~10 s | Sign inverted → fix batt_invert/wiring, not the tuning |
| 3 | Compare ESP32 readings to inverter display | SoC identical; power within ~5 % | Wrong entity, or dongle sensor used by mistake |
| 4 | Set +300 W, then STOP the add-on | Inverter reaches 0 W within ~30 s and stays | STOP THE INSTALL. Nothing here is safe without this |
| 5 | max_w=1000, start control |
Grid settles within tens of watts of zero and rests | Hunting → raise deadband_w; slow meter → lower gain |
| 6 | Switch on a kettle/oven (~2 kW) | Grid back near zero within ~20 s; command stops rising once the battery catches up | Command keeps climbing = runaway. Stop the add-on immediately |
max_w to the value the site is sold with and
repeat gate 6 once.python3 rs485_log.py --out /home/pi/bus.log --panicPANIC ARMED.TX line and the
inverter goes to 0 W.--panic.Read this to the client and have them sign. Note it on the sign-off sheet.
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.
drain → charge → hold → idle.maintenance_enabled: true.maintenance_soc_floor just below current SoC, maintenance_soc_target
just below that, maintenance_hold_min: 5. Restore real values afterwards and
record it as a partial test.Why it matters: the full charge is when the BMS balances cells; the deep discharge recalibrates the 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.
| Site / client | |
| Inverter model / serial | |
| ESPHome node name | |
Add-on version / max_w | |
| Meter entity + update rate |
| Symptom | Likely cause | Do this |
|---|---|---|
| Web UI says NOT READY | Wrong entity id, or sensor unavailable | Banner names it. Copy the id from Developer Tools → States |
| Configured, but nothing moves | Control is stopped | Press start; banner turns green |
| Inverter does nothing, no errors | A/B swapped, or vendor box still connected | Swapped RS485 gives a silent bus, not an error |
| HA shows one setpoint, inverter does another | Firmware max_w below add-on max_w | Firmware wins by design. Align them |
| Log shows writes rejected (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 defeated | Stop the add-on now. Verify meter_invert with a known load |
| Grid hunts, never rests | Deadband too small, or meter too slow | Raise deadband_w; if meter > 10 s, lower gain |
| Battery flat overnight and stays flat | Maintenance drain never exited | Check the SoC entity is the ESP32's, not a cached cloud value |
| Maintenance never runs | Disabled, or not yet due | Check maintenance_enabled and the “Maintenance due” row |
| E-stop fires on every update | Expected — upload silence > 30 s | Nothing to fix. Do not raise the threshold |
| Add-on will not install | HA is Container/Core | Not supportable — see Step 0 |
Every value came from hardware. Do not change them without measuring.
| Setting | Value | Reason |
|---|---|---|
gain | 0.6 | Inverter needs 3–6 s to settle (1.4 s dead time, 94 % by 3.3 s) against a ~5 s cycle. At the limit — do not raise |
slew_w | 1000 | Most of a correction in the first cycle without running far ahead of the hardware |
deadband_w | 15 | Measured residual: mean 15.4 W, max 27 W. At 10 W the command never rests |
saturation_w / cycles | 500 / 3 | Divergence = inverter at a limit → magnitude may fall, never rise. The 3-cycle term is essential: tested instantly it fires on every large correction |
step_w | 10 | Register is 1 W but response lands on a ~17.6 W ladder. Finer is meaningless |
heartbeat_s | 10 | Three chances to be heard before the 30 s watchdog acts |