mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-08 23:46:38 +00:00
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>
129 lines
4.5 KiB
Python
129 lines
4.5 KiB
Python
# ruff: noqa: S101
|
|
|
|
import numpy as np
|
|
|
|
from akkudoktoreos.devices.devicesabc import BatteryOperationMode
|
|
from akkudoktoreos.optimization.genetic.genetic import GeneticOptimization
|
|
from akkudoktoreos.optimization.genetic.geneticsolution import GeneticSolution
|
|
|
|
|
|
def test_battery_discharge_allowed_remains_local_load_mode(config_eos):
|
|
config_eos.merge_settings_from_dict(
|
|
{"feedintariff": {"direct_marketing_enabled": True}}
|
|
)
|
|
solution = GeneticSolution.model_construct()
|
|
|
|
operation_mode, operation_mode_factor = solution._battery_operation_from_solution(
|
|
ac_charge=0.0,
|
|
dc_charge=0.0,
|
|
discharge_allowed=True,
|
|
)
|
|
|
|
assert operation_mode == BatteryOperationMode.PEAK_SHAVING
|
|
assert operation_mode_factor == 1.0
|
|
|
|
|
|
def test_battery_grid_export_signal_maps_to_grid_support_export(config_eos):
|
|
config_eos.merge_settings_from_dict(
|
|
{"feedintariff": {"direct_marketing_enabled": True}}
|
|
)
|
|
solution = GeneticSolution.model_construct()
|
|
|
|
operation_mode, operation_mode_factor = solution._battery_operation_from_solution(
|
|
ac_charge=0.0,
|
|
dc_charge=0.0,
|
|
discharge_allowed=False,
|
|
battery_grid_export_allowed=True,
|
|
)
|
|
|
|
assert operation_mode == BatteryOperationMode.GRID_SUPPORT_EXPORT
|
|
assert operation_mode_factor == 1.0
|
|
|
|
|
|
def test_decode_charge_discharge_has_separate_battery_grid_export_state():
|
|
optimization = GeneticOptimization()
|
|
optimization.bat_possible_charge_values = [1.0]
|
|
optimization.optimize_dc_charge = True
|
|
optimization.optimize_battery_grid_export = True
|
|
|
|
ac_charge, dc_charge, discharge, battery_grid_export = (
|
|
optimization.decode_charge_discharge(np.array([5]))
|
|
)
|
|
|
|
assert ac_charge.tolist() == [0.0]
|
|
assert dc_charge.tolist() == [0]
|
|
assert discharge.tolist() == [0]
|
|
assert battery_grid_export.tolist() == [1]
|
|
|
|
|
|
def test_decode_charge_discharge_has_self_consumption_state_after_legacy_export():
|
|
optimization = GeneticOptimization()
|
|
optimization.bat_possible_charge_values = [1.0]
|
|
optimization.optimize_dc_charge = True
|
|
optimization.optimize_battery_grid_export = True
|
|
|
|
layout = optimization._battery_state_layout()
|
|
ac_charge, dc_charge, discharge, battery_grid_export = (
|
|
optimization.decode_charge_discharge(np.array([6]))
|
|
)
|
|
|
|
assert layout.total_states == 7
|
|
assert layout.grid_export_state == 5
|
|
assert layout.self_consumption_state == 6
|
|
assert ac_charge.tolist() == [0.0]
|
|
assert dc_charge.tolist() == [1]
|
|
assert discharge.tolist() == [1]
|
|
assert battery_grid_export.tolist() == [0]
|
|
|
|
|
|
def test_graded_grid_export_states_decode_to_rates():
|
|
"""Each configured export rate gets its own state; state 5 stays full power."""
|
|
optimization = GeneticOptimization()
|
|
optimization.bat_possible_charge_values = [1.0]
|
|
optimization.bat_possible_grid_export_values = [1.0, 0.5, 0.25]
|
|
optimization.optimize_dc_charge = True
|
|
optimization.optimize_battery_grid_export = True
|
|
|
|
layout = optimization._battery_state_layout()
|
|
|
|
assert layout.grid_export_states == (5, 6, 7)
|
|
# The full-power state keeps its index, so existing seeds stay valid.
|
|
assert layout.grid_export_state == 5
|
|
assert layout.self_consumption_state == 8
|
|
assert layout.total_states == 9
|
|
|
|
_, _, _, battery_grid_export = optimization.decode_charge_discharge(
|
|
np.array([0, 5, 6, 7])
|
|
)
|
|
assert battery_grid_export.tolist() == [0.0, 1.0, 0.5, 0.25]
|
|
|
|
|
|
def test_single_export_rate_keeps_all_or_nothing_layout():
|
|
"""Without configured rates the state space is the one from before grading."""
|
|
optimization = GeneticOptimization()
|
|
optimization.bat_possible_charge_values = [1.0]
|
|
optimization.optimize_dc_charge = True
|
|
optimization.optimize_battery_grid_export = True
|
|
|
|
layout = optimization._battery_state_layout()
|
|
|
|
assert layout.grid_export_states == (5,)
|
|
assert layout.total_states == 7
|
|
|
|
|
|
def test_battery_grid_export_factor_becomes_operation_factor(config_eos):
|
|
"""A partial export level is reported as the GRID_SUPPORT_EXPORT factor."""
|
|
config_eos.merge_settings_from_dict({"feedintariff": {"direct_marketing_enabled": True}})
|
|
solution = GeneticSolution.model_construct()
|
|
|
|
operation_mode, operation_mode_factor = solution._battery_operation_from_solution(
|
|
ac_charge=0.0,
|
|
dc_charge=0.0,
|
|
discharge_allowed=False,
|
|
battery_grid_export_allowed=True,
|
|
battery_grid_export_factor=0.25,
|
|
)
|
|
|
|
assert operation_mode == BatteryOperationMode.GRID_SUPPORT_EXPORT
|
|
assert operation_mode_factor == 0.25
|