S-1. The frozen branch admitted a correction only if it shrank |i_w|. That is unsatisfiable for BOTH signs of error whenever |correction| > 2*|i_w|, i.e. whenever the integrator is near zero, so the loop stopped moving and the freeze could never clear - it clears when the inverter tracks, and not tracking is what saturation means. Measured: 0 W held into a 2 kW import indefinitely, where release/1.0 recovers on the next cycle. Re-encoded as the same asymmetric rule the output freeze has always used: may not wind further in the direction it is already pushing, may fall, cross zero or reverse. Same interpretation, an encoding that cannot deadlock. S-2. integrator_max_w defaulted to 1.5x max_w, which ADDED windup: in release/1.0 the accumulator was the post-clamp command and could never pass the rail. Default is now "follow max_w" (config 0 = unset). Measured on the 4000 W load-drop sim, first cycle after the drop: 1000 W at the new default, 1800 W at 3000. DOCS row inverted - the useful direction is below max_w, and the 14 768 W anecdote is a vendor controller, not evidence about this code. S-3. The claim that i_w=None preserved release/1.0 exactly was false, because the S-1 gate ran regardless of seeding. It is true again, and now asserted rather than asserted-about: 3024-case exhaustive comparison against a transcription of the old law, over both freeze states, both signs and either side of the deadband. Added the carried-i_w convergence/overshoot sim that the shipped configuration was missing. S-4. Cycles are distinct meter values, not seconds: cycle() runs only when the meter reading changes, so the window has no wall-clock bound. Comment and DOCS corrected; the stall is detection latency, not a windup hazard, because the same condition stalls the whole loop. test_control.py: 33 -> 41 checks, all passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
113 lines
2.9 KiB
YAML
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: 0
|
|
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(0,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)
|