mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-09 07:56:40 +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:
@@ -1,6 +1,8 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from akkudoktoreos.devices.devices import BatteriesCommonSettings
|
||||
from akkudoktoreos.devices.genetic.battery import Battery, SolarPanelBatteryParameters
|
||||
|
||||
|
||||
@@ -343,3 +345,41 @@ def test_quarter_hour_discharge_calls_share_one_power_budget():
|
||||
battery.reset()
|
||||
|
||||
assert battery.discharged_energy_wh(0) == 0.0
|
||||
|
||||
|
||||
def test_grid_export_rates_are_sorted_and_deduplicated():
|
||||
"""Export rates are normalized like the charge rates."""
|
||||
settings = BatteriesCommonSettings(
|
||||
device_id="battery1", grid_export_rates=[1.0, 0.5, 0.5, 0.25]
|
||||
)
|
||||
assert list(settings.grid_export_rates) == [0.25, 0.5, 1.0]
|
||||
|
||||
|
||||
def test_grid_export_rates_default_and_override():
|
||||
"""None falls back to the defaults; [1.0] restores all-or-nothing export."""
|
||||
assert list(BatteriesCommonSettings(device_id="battery1").grid_export_rates) == [
|
||||
0.25,
|
||||
0.5,
|
||||
0.75,
|
||||
1.0,
|
||||
]
|
||||
assert list(
|
||||
BatteriesCommonSettings(device_id="battery1", grid_export_rates=None).grid_export_rates
|
||||
) == [0.25, 0.5, 0.75, 1.0]
|
||||
assert list(
|
||||
BatteriesCommonSettings(device_id="battery1", grid_export_rates=[1.0]).grid_export_rates
|
||||
) == [1.0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rates", [[0.0, 0.5], [1.5], [-0.25], []])
|
||||
def test_grid_export_rates_reject_invalid_values(rates):
|
||||
"""0.0 is not an export level, and rates above the rated power are rejected."""
|
||||
with pytest.raises(ValidationError):
|
||||
BatteriesCommonSettings(device_id="battery1", grid_export_rates=rates)
|
||||
|
||||
|
||||
def test_rated_discharge_energy_scales_with_slot_duration(setup_pv_battery):
|
||||
"""The rate reference is the rated discharge energy of one slot."""
|
||||
battery = setup_pv_battery
|
||||
expected = battery.max_charge_power_w * battery.slot_duration_h * battery.discharging_efficiency
|
||||
assert battery.rated_discharge_energy_wh() == pytest.approx(expected)
|
||||
|
||||
Reference in New Issue
Block a user