Files
goodwe-addon/goodwe_controller/config.yaml
T
glenn schrooyenandClaude Opus 5 37bac79ad8 SAFETY-04: clamp the integrator, not only the output
The loop was in velocity form: the accumulator WAS the commanded power, so
"clamp the integrator" and "clamp the output" were the same line of code and
could not be set apart. This makes the accumulator an explicit carried value
(`i_w`), bounds it with its own `integrator_max_w`, and keeps the output clamp
where it was. Killing either mechanism now still leaves the other holding -
which is the point of the ticket, and what the new regression test asserts.

Freeze semantics: while saturated the integrator may unwind but not wind
further. A strict freeze would strand the command at whatever it reached,
because the condition that releases it is the inverter tracking again, and not
tracking is exactly what saturation means.

The integrator is re-seeded from the arbiter's actual output whenever the loop
did not get what it asked for, so entering any failsafe (all of which resolve
to 0 W) zeroes it, and the first cycle after release does not dump the stale
period as power.

test_control.py: 24 -> 33 checks, all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
2026-08-24 21:44:05 +02:00

113 lines
2.9 KiB
YAML

name: GoodWe RS485 Controller
version: "0.2.1"
slug: goodwe_controller
description: >-
Drives a GoodWe ES/BP battery inverter over RS485 by emulating its smart
meter, holding net grid exchange at zero and running a monthly battery
maintenance cycle.
url: https://gittea.kammenstraatha.duckdns.org/admin/goodwe-addon
arch:
- aarch64
- amd64
- armv7
init: false
startup: application
boot: auto
# Needed to read the meter and to write the ESPHome setpoint number.
homeassistant_api: true
hassio_api: true
# Own diagnostics/operations UI inside HA. This is what a field tech and a
# remote supporter both look at, so it is not optional.
ingress: true
ingress_port: 8099
panel_icon: mdi:battery-sync
panel_title: GoodWe
# Optional: publish status entities by MQTT discovery. `want` rather than
# `need` - the add-on runs fine with no broker, it just publishes nothing.
services:
- mqtt:want
options:
# --- sources (required) ---------------------------------------------------
meter_entity: sensor.p1_meter_active_power
meter_invert: false
soc_entity: ""
batt_entity: ""
batt_invert: false
setpoint_entity: ""
# --- control ---------------------------------------------------------------
max_w: 2000
gain: 0.6
slew_w: 1000
deadband_w: 15
target_grid_w: -10
step_w: 10
saturation_w: 500
saturation_cycles: 3
integrator_max_w: 3000
heartbeat_s: 10
stale_input_s: 15
auto_start: false
# --- maintenance -----------------------------------------------------------
maintenance_enabled: false
maintenance_interval_days: 28
maintenance_start_hour: 10
maintenance_discharge_w: 2500
maintenance_charge_w: 2500
maintenance_soc_floor: 11
maintenance_soc_target: 99
maintenance_hold_min: 120
# --- tariff / capacity tariff (all optional) ------------------------------
peak_forecast_entity: ""
peak_cap_w: 3500
price_now_entity: ""
price_avg_entity: ""
# --- site ------------------------------------------------------------------
estop_fitted: false
log_level: info
schema:
meter_entity: str
meter_invert: bool
soc_entity: str
batt_entity: str
batt_invert: bool
setpoint_entity: str
max_w: int(100,5000)
gain: float(0.05,1.0)
slew_w: int(50,5000)
deadband_w: int(0,500)
target_grid_w: float(-200,200)
step_w: int(1,100)
saturation_w: int(100,2000)
saturation_cycles: int(1,10)
integrator_max_w: int(100,15000)
heartbeat_s: int(2,25)
stale_input_s: int(5,120)
auto_start: bool
maintenance_enabled: bool
maintenance_interval_days: int(1,90)
maintenance_start_hour: int(0,23)
maintenance_discharge_w: int(500,5000)
maintenance_charge_w: int(500,5000)
maintenance_soc_floor: int(5,30)
maintenance_soc_target: int(50,100)
maintenance_hold_min: int(5,480)
peak_forecast_entity: str?
peak_cap_w: int(500,15000)
price_now_entity: str?
price_avg_entity: str?
estop_fitted: bool
log_level: list(trace|debug|info|warning|error)