Implement proactive forecast-driven car charging strategy

- Replace reactive export-threshold wallbox activation with proactive
  logic: WallboxA/B activate when forecast ≥ mid AND PV ≥ threshold
  AND SOC ≥ 35%, without requiring grid export surplus
- Add WallboxB no-car detection via grid-delta probe (no PM available):
  after probe window, if grid shift < GridDeltaThreshW → no car, retry
  after configured timeout
- Add EOD soft stop: after 16:00, stop proactive car charging if
  remaining PV estimate can't cover battery deficit to 90% by sunset
- WW boost no longer requires export threshold; dynamic setpoint uses
  tank top temp + hysteresis + boost delta, capped at 60°C
- Proactive wallboxes bypass import-hysteresis shutdown; SOC emergency
  brake uses SOCFloor (5%) instead of standard AllConsumers gate
- Add WWTopTempC to SystemState (ww_top_temp metric from DHW cylinder)
- Add BatteryConfig (capacity_kwh), CarChargingConfig to config
- Add WWMaxSetpointC, WWHysteresisC to StrategicConfig
- Update Decide() signature: forecastKWh + sunsetTime parameters
- Update all tests; add proactive charging test cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 21:23:04 +02:00
parent fc535d395c
commit 971e22c4be
6 changed files with 447 additions and 114 deletions

View File

@@ -26,6 +26,7 @@ type SystemState struct {
PhaseL1PowerW float64 // per-phase grid power L1 (positive=import, negative=export)
PhaseL2PowerW float64 // per-phase grid power L2
PhaseL3PowerW float64 // per-phase grid power L3
WWTopTempC float64 // DHW cylinder top temperature (°C)
}
// IsExporting returns true if the system is exporting to grid.
@@ -91,6 +92,7 @@ func (c *Collector) Collect(ctx context.Context) (SystemState, error) {
{"phase_l1_power", &state.PhaseL1PowerW, 1},
{"phase_l2_power", &state.PhaseL2PowerW, 1},
{"phase_l3_power", &state.PhaseL3PowerW, 1},
{"ww_top_temp", &state.WWTopTempC, 1},
}
for _, t := range targets {
@@ -120,6 +122,7 @@ func (c *Collector) Collect(ctx context.Context) (SystemState, error) {
"l1_w", state.PhaseL1PowerW,
"l2_w", state.PhaseL2PowerW,
"l3_w", state.PhaseL3PowerW,
"ww_top_c", state.WWTopTempC,
)
return state, nil