Files
glenn schrooyenandClaude Opus 5 5184a2cfc2 Rest the meter just below zero, not at zero
The deadband is a one-way ratchet: any resting point inside it holds
indefinitely. Import and export are separate registers on the meter, so a
loop resting at +14 W bills 0.34 kWh/day while behaving perfectly.

target_grid_w (default -10 W) moves that residue onto the export register.
Worst billed rest point drops from 15 W to under 5 W. Behaviour is unchanged
at target_grid_w: 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 04:20:07 +02:00

5.8 KiB
Raw Permalink Blame History

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 ~510 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
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.