# GoodWe RS485 Controller Drives a GoodWe ES/BP battery inverter over its RS485 meter bus: holds net grid exchange at zero, and runs a monthly battery maintenance cycle so the BMS can balance cells and recalibrate its coulomb counter. Installers: read `FIELD-GUIDE.md` in the repository. It is not optional reading — it contains the commissioning gates and the failure modes. ## Before you start You need: - A GoodWe **ES / BP family** inverter (AA55 / RS485 meter-bus generation) - The vendor's meter-emulating controller **disconnected** from the bus - A T-CAN485 (ESP32) flashed with `firmware/goodwe-master.yaml` - A grid-power sensor already working in Home Assistant, updating every ~5–10 s ## The safety model, in short The inverter **holds its last command forever** — it has no meter-timeout. So: - The ESP32 commands 0 W if this add-on stops refreshing for ~30 s, and keeps commanding it. - This add-on commands 0 W when its inputs go missing, when you stop control, and when it shuts down. - The **optional RS485 e-stop** is the only thing that covers this machine dying. Without it, a failed host leaves the battery latched at its last command until someone intervenes. **If anything looks wrong: stop the add-on.** That commands 0 W and the hardware holds it there. ## Configuration ### Sources | option | required | meaning | |---|---|---| | `meter_entity` | yes | Net grid power. **Positive must mean importing** | | `meter_invert` | | Flip the sign if the meter reports the other way | | `soc_entity` | yes | Battery state of charge — use the **ESP32's own read** | | `batt_entity` | yes | Battery power — again the ESP32's read, `+` = discharging | | `batt_invert` | | Flip if needed | | `setpoint_entity` | yes | The ESPHome `number.*_goodwe_setpoint_w` | Use the ESP32's readings rather than the inverter's cloud or dongle sensors: those serve cached values, and a stale reading here ends the maintenance charge phase having charged nothing. ### Control | option | default | meaning | |---|---|---| | `max_w` | 2000 | Hard limit on what may be commanded. Start low, raise after commissioning | | `gain` | 0.6 | Correction per cycle. **At the limit — do not raise** | | `slew_w` | 1000 | Maximum change per cycle | | `deadband_w` | 15 | Ignore errors smaller than this | | `target_grid_w` | -10 | What the meter should rest at. Negative = a slight export | | `step_w` | 10 | Quantisation | | `saturation_w` | 500 | Divergence that counts as "the inverter is at a limit" | | `saturation_cycles` | 3 | How many consecutive cycles before freezing. **Do not set to 1** | | `integrator_max_w` | 3000 | Bound on the loop's accumulator, separate from `max_w`. Caps how much stale error can be waiting to unwind when the sign flips. **Keep it above `max_w`, and do not set it equal to `max_w`** | | `heartbeat_s` | 10 | Refresh interval; must stay well under the firmware watchdog | | `stale_input_s` | 15 | How long inputs may be missing before commanding 0 W | | `auto_start` | false | Start controlling on boot (only after commissioning) | #### Why `target_grid_w` is not zero The deadband is a one-way ratchet: any resting point inside it holds until something disturbs it. Import and export are **separate registers on the meter**, so a rest point of +14 W is billed for every second it holds and no amount of export cancels it - 14 W all day is 0.34 kWh. Biasing the target below zero moves that residue into the export register, which is not billed. The resting band becomes `target ± deadband`, so: | `target_grid_w` | resting band | worst billed leak | export given away | |---|---|---|---| | 0 | -15 … +15 W | ~15 W (0.35 kWh/day) | none | | **-10** | -25 … +5 W | ~5 W (0.12 kWh/day) | ~10 W | | -15 | -30 … 0 W | none | ~15 W (0.36 kWh/day) | Set it to `-deadband_w` if injection is worth nothing to you and you would rather give the energy away than buy it back. Set it to `0` if you are paid properly for export, or if you are debugging and want the loop centred. ⚠️ This is a **billing** knob, not a speed knob. If import is arriving in bursts rather than as a trickle, the cause is tracking lag, and this will not help - see "Why the tuning is what it is". ### Maintenance | option | default | meaning | |---|---|---| | `maintenance_enabled` | false | Enable the monthly cycle | | `maintenance_interval_days` | 28 | Minimum gap between cycles | | `maintenance_start_hour` | 10 | Hour of day a due cycle begins | | `maintenance_discharge_w` | 2500 | Drain rate (exports the surplus) | | `maintenance_charge_w` | 2500 | Charge ceiling, capped again by peak headroom | | `maintenance_soc_floor` | 11 | Drain target — stay just above the inverter's own floor | | `maintenance_soc_target` | 99 | Charge target | | `maintenance_hold_min` | 120 | Hold at full so the BMS can balance | ### Tariff (all optional) | option | meaning | |---|---| | `peak_forecast_entity` | Quarter-hour demand forecast, for capacity-tariff markets. Empty = no cap | | `peak_cap_w` | The site's capacity-tariff target | | `price_now_entity`, `price_avg_entity` | Dynamic tariff. Empty = never force a paid grid top-up | On a capacity-tariff site the maintenance charge is capped by the headroom left under `peak_cap_w`, and if the forecast goes over the cap the charge-only clamp is dropped so the battery can shave the peak instead. Money outranks the maintenance schedule. ### Site | option | meaning | |---|---| | `estop_fitted` | Whether the RS485 e-stop is installed. Drives the warning banner | | `log_level` | `trace`/`debug`/`info`/`warning`/`error` | ## The Web UI The ingress panel shows live values, why the controller is commanding what it is, and a **Commissioning** checklist that names any problem in words. It also carries the three buttons: start/stop control, force a maintenance cycle, and abort one. ## Status entities If an MQTT broker is available the add-on publishes setpoint, grid power, battery power, state of charge, maintenance phase and controller status by MQTT discovery. This is observability only — the controller works fine without a broker, and MQTT problems can never affect control.