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

11
main.go
View File

@@ -263,8 +263,17 @@ func runCycle(
if !wwBoostDisabled.IsActive() {
wwBoostC = computeWWBoost(fcResult, cfg)
}
forecastKWh := 0.0
if fcResult != nil {
forecastKWh = fcResult.TotalKWh
}
// Estimate sunset as SurplusWindowEnd + 1h; fall back to zero (disables EOD check)
var sunsetTime time.Time
if fcResult != nil && !fcResult.SurplusWindowEnd.IsZero() {
sunsetTime = fcResult.SurplusWindowEnd.Add(time.Hour)
}
decisionStart := time.Now()
actions := eng.Decide(state, now, wwBoostC)
actions := eng.Decide(state, now, wwBoostC, forecastKWh, sunsetTime)
m.DecisionDuration.Observe(time.Since(decisionStart).Seconds())
// Step 3b: Trip mode — session energy tracking + wallbox activation