mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-16 11:01:56 +00:00
fix(planning): accept full SoC targets and clarify unlimited EV SoC age (#1407)
* fix(devices): allow charging targets equal to the maximum SoC * test(planning): protect EV SoC without age limits
This commit is contained in:
@@ -398,3 +398,33 @@ def test_pv_converter_preserves_id_lcos_charge_and_export_rates():
|
||||
assert parameters.levelized_cost_of_storage_kwh == pytest.approx(0.123)
|
||||
battery = Battery(parameters, prediction_hours=4, slot_duration_h=0.25)
|
||||
assert battery.levelized_cost_of_storage_kwh == pytest.approx(0.123)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", [0, 50, 100])
|
||||
def test_equal_soc_limits_are_valid(target):
|
||||
settings = BatteriesCommonSettings(device_id="ev", min_soc_percentage=target,
|
||||
max_soc_percentage=target)
|
||||
assert settings.to_genetic_ev_bat_param().min_soc_percentage == target
|
||||
assert settings.to_genetic0_ev_bat_param().min_soc_percentage == target
|
||||
|
||||
|
||||
@pytest.mark.parametrize("minimum,maximum", [(100, 99), (81, 80), (101, 100), (-1, 100)])
|
||||
def test_inverted_or_out_of_range_limits_still_fail(minimum, maximum):
|
||||
with pytest.raises(ValidationError):
|
||||
BatteriesCommonSettings(device_id="ev", min_soc_percentage=minimum,
|
||||
max_soc_percentage=maximum)
|
||||
|
||||
|
||||
def test_full_target_charges_to_capacity_without_overcharge():
|
||||
settings = BatteriesCommonSettings(device_id="ev", capacity_wh=1000,
|
||||
min_soc_percentage=100, max_soc_percentage=100, max_charge_power_w=1000,
|
||||
charging_efficiency=1)
|
||||
params = settings.to_genetic_ev_bat_param()
|
||||
params.initial_soc_percentage = 70
|
||||
battery = Battery(params, prediction_hours=4, slot_duration_h=0.25)
|
||||
battery.charge_array = np.ones(4)
|
||||
for slot in range(4):
|
||||
battery.charge_energy(wh=1000, hour=slot)
|
||||
assert battery.current_soc_percentage() <= 100
|
||||
assert battery.current_soc_percentage() == pytest.approx(100)
|
||||
assert battery.charge_energy(wh=1000, hour=3) == (0, 0)
|
||||
|
||||
@@ -451,3 +451,66 @@ async def test_missing_ev_soc_does_not_replace_explicit_battery_soc(configured_r
|
||||
assert parameters.ev is not None
|
||||
assert parameters.pv_battery.initial_soc_percentage == 42
|
||||
assert parameters.ev.initial_soc_percentage == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("soc", [70, 100])
|
||||
async def test_http_config_and_real_optimizer_allow_full_ev_target(configured_request,
|
||||
monkeypatch, soc):
|
||||
from akkudoktoreos.server import eos
|
||||
|
||||
config, fake, _, data = configured_request
|
||||
config.merge_settings_from_dict({"devices": {"max_electric_vehicles": 1,
|
||||
"electric_vehicles": {"ev": {"capacity_wh": 1000, "max_charge_power_w": 1000,
|
||||
"charging_efficiency": 1, "min_soc_percentage": 0,
|
||||
"max_soc_percentage": 100}}}})
|
||||
data["soc"]["ev"] = soc
|
||||
ems = EnergyManagement()
|
||||
monkeypatch.setattr(configrequest, "get_ems", lambda: ems)
|
||||
monkeypatch.setattr(EnergyManagement, "_genetic_solution", None)
|
||||
monkeypatch.setattr(EnergyManagement, "prediction", Mock(update_data=AsyncMock()))
|
||||
monkeypatch.setattr(EnergyManagement, "adapter", Mock(update_data=AsyncMock()))
|
||||
|
||||
async def run(**kwargs):
|
||||
return await ems.run(start_datetime=fake.start_datetime, **kwargs)
|
||||
|
||||
from types import SimpleNamespace
|
||||
monkeypatch.setattr(eos, "get_ems", lambda: SimpleNamespace(run=run))
|
||||
monkeypatch.setattr(eos, "get_config", lambda: config)
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=eos.app),
|
||||
base_url="http://test") as client:
|
||||
updated = await client.put("/v1/config/devices/electric_vehicles/ev/min_soc_percentage",
|
||||
json=100)
|
||||
assert updated.status_code == 200, updated.text
|
||||
assert config.devices.electric_vehicles["ev"].min_soc_percentage == 100
|
||||
result = await client.post("/v1/optimize", json=data)
|
||||
assert result.status_code == 200, result.text
|
||||
plan = result.json()
|
||||
assert plan["parameters"]["ev"]["min_soc_percentage"] == 100
|
||||
assert plan["parameters"]["ev"]["initial_soc_percentage"] == soc
|
||||
assert len(plan["result"]["ev_soc_per_hour"]) == 4
|
||||
assert all(0 <= value <= 100 for value in plan["result"]["ev_soc_per_hour"])
|
||||
assert plan["result"]["ev_soc_per_hour"][-1] == pytest.approx(100)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("age_seconds", [901, 86400, 365 * 86400])
|
||||
async def test_explicit_ev_soc_has_no_measurement_age_limit(configured_request, age_seconds):
|
||||
config, ems, measurement, data = configured_request
|
||||
config.merge_settings_from_dict(
|
||||
{"devices": {"max_electric_vehicles": 1,
|
||||
"electric_vehicles": {"car": {"capacity_wh": 60000,
|
||||
"min_soc_percentage": 100,
|
||||
"max_soc_percentage": 100}}}}
|
||||
)
|
||||
old_sample = ems.observation_datetime.subtract(seconds=age_seconds)
|
||||
measurement.key_to_lists.return_value = ([old_sample], [0.35])
|
||||
data["soc"]["car"] = 70
|
||||
config_before = config.model_dump_json()
|
||||
parameters = await ConfigOptimizationRequest.model_validate(data).resolve()
|
||||
assert parameters.ev is not None
|
||||
assert parameters.ev.initial_soc_percentage == 70
|
||||
assert parameters.ev.min_soc_percentage == 100
|
||||
measurement.key_to_lists.assert_not_awaited()
|
||||
measurement.update_value.assert_not_called()
|
||||
assert config.model_dump_json() == config_before
|
||||
|
||||
Reference in New Issue
Block a user