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:
@@ -10,18 +10,43 @@ import (
|
||||
|
||||
// Config is the top-level EMS configuration.
|
||||
type Config struct {
|
||||
Prometheus PrometheusConfig `yaml:"prometheus"`
|
||||
Shelly ShellyConfig `yaml:"shelly"`
|
||||
Viessmann ViessmannConfig `yaml:"viessmann"`
|
||||
SOC SOCThresholds `yaml:"soc_thresholds"`
|
||||
Hysteresis HysteresisConfig `yaml:"hysteresis"`
|
||||
Thresholds PowerThresholds `yaml:"thresholds"`
|
||||
Consumers ConsumersConfig `yaml:"consumers"`
|
||||
Strategic StrategicConfig `yaml:"strategic"`
|
||||
Season SeasonConfig `yaml:"season"`
|
||||
Forecast ForecastConfig `yaml:"forecast"`
|
||||
Cars map[string]CarProfile `yaml:"cars"`
|
||||
EMS EMSConfig `yaml:"ems"`
|
||||
Prometheus PrometheusConfig `yaml:"prometheus"`
|
||||
Shelly ShellyConfig `yaml:"shelly"`
|
||||
Viessmann ViessmannConfig `yaml:"viessmann"`
|
||||
SOC SOCThresholds `yaml:"soc_thresholds"`
|
||||
Hysteresis HysteresisConfig `yaml:"hysteresis"`
|
||||
Thresholds PowerThresholds `yaml:"thresholds"`
|
||||
Consumers ConsumersConfig `yaml:"consumers"`
|
||||
Strategic StrategicConfig `yaml:"strategic"`
|
||||
Season SeasonConfig `yaml:"season"`
|
||||
Forecast ForecastConfig `yaml:"forecast"`
|
||||
Battery BatteryConfig `yaml:"battery"`
|
||||
CarCharging CarChargingConfig `yaml:"car_charging"`
|
||||
Cars map[string]CarProfile `yaml:"cars"`
|
||||
EMS EMSConfig `yaml:"ems"`
|
||||
}
|
||||
|
||||
// BatteryConfig holds physical battery properties.
|
||||
type BatteryConfig struct {
|
||||
CapacityKWh float64 `yaml:"capacity_kwh"` // usable battery capacity in kWh
|
||||
}
|
||||
|
||||
// CarChargingConfig holds parameters for the proactive car charging strategy.
|
||||
type CarChargingConfig struct {
|
||||
MinSOC int `yaml:"min_soc"` // minimum SOC% to start car charging (e.g. 35)
|
||||
SOCFloor int `yaml:"soc_floor"` // never drain battery below this % (e.g. 5)
|
||||
EODSOCTarget int `yaml:"eod_soc_target"` // target SOC% to reach by sunset (e.g. 90)
|
||||
EODTime string `yaml:"eod_time"` // soft-stop check starts at this time (e.g. "16:00")
|
||||
NoCarRetryMin int `yaml:"no_car_retry_min"` // minutes before retrying after no-car detection
|
||||
PVThresholdAW float64 `yaml:"pv_threshold_a_w"` // min PV production to start WallboxA (W)
|
||||
PVThresholdBW float64 `yaml:"pv_threshold_b_w"` // min PV production to start WallboxB (W)
|
||||
GridDeltaThreshW float64 `yaml:"grid_delta_thresh_w"` // min grid power shift after WallboxB activation = car detected (W)
|
||||
}
|
||||
|
||||
func (c *CarChargingConfig) EODTimeParsed(ref time.Time) time.Time {
|
||||
var h, m int
|
||||
fmt.Sscanf(c.EODTime, "%d:%d", &h, &m)
|
||||
return time.Date(ref.Year(), ref.Month(), ref.Day(), h, m, 0, 0, ref.Location())
|
||||
}
|
||||
|
||||
// CarProfile holds the display name and battery capacity of a known vehicle.
|
||||
@@ -122,9 +147,11 @@ type StrategicConfig struct {
|
||||
ForecastMidKWh float64 `yaml:"forecast_mid_kwh"`
|
||||
WWBoostHighC float64 `yaml:"ww_boost_high_c"`
|
||||
WWBoostMidC float64 `yaml:"ww_boost_mid_c"`
|
||||
WWBaseC float64 `yaml:"ww_base_c"` // normal WW setpoint (°C)
|
||||
WWWindowStart string `yaml:"ww_window_start"` // e.g. "12:30"
|
||||
WWWindowEnd string `yaml:"ww_window_end"` // e.g. "18:00"
|
||||
WWBaseC float64 `yaml:"ww_base_c"` // normal WW setpoint (°C)
|
||||
WWMaxSetpointC float64 `yaml:"ww_max_setpoint_c"` // absolute maximum WW setpoint (°C), e.g. 60
|
||||
WWHysteresisC float64 `yaml:"ww_hysteresis_c"` // Viessmann switchOn = setpoint - hysteresis (°C), e.g. 5
|
||||
WWWindowStart string `yaml:"ww_window_start"` // e.g. "12:30"
|
||||
WWWindowEnd string `yaml:"ww_window_end"` // e.g. "18:00"
|
||||
ScheduleOn string `yaml:"schedule_on"`
|
||||
ScheduleOff string `yaml:"schedule_off"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user