mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-12 01:01:57 +00:00
feat(optimization): deadlines for consumers and EV, graded grid export
Three related scheduling improvements, all opt-in and behaviour-preserving
when the new fields are not set.
Flexible consumers get absolute time bounds next to the recurring
time_windows: earliest_start_datetime and deadline_datetime, where the
deadline requires the complete run to have *finished* before that moment
("clean dishes by 03:00 tonight"). When no start can meet it,
deadline_policy decides between BEST_EFFORT (run as early as possible, so
the delay rather than the cost is minimized) and STRICT (keep the
deadline; a ONCE consumer then fails the optimization). The solution
reports appliance_deadline_missed per device.
The EV charging target can be given the same kind of deadline, as an
absolute min_soc_deadline_datetime and/or a relative min_soc_max_duration_h
("full in 6 hours"), the earlier of the two winning. The ev_soc_miss
penalty is then evaluated at that slot instead of at the end of the
horizon, and the seeding heuristic only proposes charge slots before it.
Battery-to-grid export under direct marketing is no longer all-or-nothing:
grid_export_rates configures the selectable export levels as a factor of
the rated discharge power (default [0.25, 0.5, 0.75, 1.0]). Each rate is
its own optimizer state, with the full-power state keeping its previous
index so existing seeds and heuristics are unaffected. The chosen level
per slot is reported in battery_grid_export_factor and as the
GRID_SUPPORT_EXPORT operation factor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ def mock_battery() -> Mock:
|
||||
mock_battery = Mock()
|
||||
mock_battery.charge_energy = Mock(return_value=(0.0, 0.0))
|
||||
mock_battery.discharge_energy = Mock(return_value=(0.0, 0.0))
|
||||
# Rated discharge energy of one slot - the reference a grid-export rate is
|
||||
# applied to. Large enough to never bind at the default factor of 1.0.
|
||||
mock_battery.rated_discharge_energy_wh = Mock(return_value=1e9)
|
||||
mock_battery.parameters.device_id = "battery1"
|
||||
return mock_battery
|
||||
|
||||
@@ -243,6 +246,26 @@ def test_process_energy_allows_battery_grid_export(inverter, mock_battery):
|
||||
inverter.self_consumption_predictor.calculate_expected_direct_consumption.assert_not_called()
|
||||
|
||||
|
||||
def test_process_energy_grid_export_rate_limits_export(inverter, mock_battery):
|
||||
"""An export rate caps the export at that share of the rated discharge power."""
|
||||
mock_battery.max_charge_power_w = 300.0
|
||||
mock_battery.remaining_discharge_energy_wh.return_value = 200.0
|
||||
mock_battery.rated_discharge_energy_wh.return_value = 300.0
|
||||
mock_battery.discharge_energy.side_effect = [(100.0, 0.0), (150.0, 0.0)]
|
||||
|
||||
grid_export, grid_import, losses, self_consumption = inverter.process_energy(
|
||||
generation=0.0,
|
||||
consumption=100.0,
|
||||
hour=12,
|
||||
allow_battery_grid_export=True,
|
||||
battery_grid_export_factor=0.5,
|
||||
)
|
||||
|
||||
# 0.5 * 300 Wh rated = 150 Wh, below the 200 Wh the battery could still give.
|
||||
assert grid_export == pytest.approx(150.0)
|
||||
mock_battery.discharge_energy.assert_has_calls([call(100.0, 12), call(150.0, 12)])
|
||||
|
||||
|
||||
def test_process_energy_battery_empty(inverter, mock_battery):
|
||||
# Battery is empty, so no energy can be discharged
|
||||
mock_battery.discharge_energy.return_value = (0.0, 0.0)
|
||||
|
||||
Reference in New Issue
Block a user