Files
EOS/tests/test_timewindow_boundaries.py
T

155 lines
5.7 KiB
Python
Raw Normal View History

"""Regression tests for explicit window ends and cycle scheduling."""
import json
import numpy as np
import pendulum
import pytest
from akkudoktoreos.config.configabc import (
CycleTimeWindowSequence,
TimeWindow,
ValueTimeWindow,
)
from akkudoktoreos.devices.genetic.homeappliance import (
HomeAppliance,
HomeApplianceParameters,
)
@pytest.mark.parametrize("model", [TimeWindow, ValueTimeWindow])
def test_end_only_window_json_round_trip(model):
window = model.model_validate({"start_time": "08:00", "end_time": "12:00"})
serialized = window.model_dump_json()
assert json.loads(serialized)["duration"] == "PT0S"
restored = model.model_validate_json(serialized)
assert restored.model_dump() == window.model_dump()
assert restored.contains(pendulum.datetime(2026, 10, 26, 9))
@pytest.mark.parametrize(
("start_date", "start_time", "end_time", "expected_end"),
[
# The missing 02:30 on the opening date must not shift Monday's end.
("2026-03-29", "22:00", "02:30", "2026-03-30T02:30:00+02:00"),
# The explicit end lies after the spring transition.
("2026-03-28", "22:00", "06:00", "2026-03-29T06:00:00+02:00"),
# The explicit end lies after the autumn transition.
("2026-10-24", "22:00", "06:00", "2026-10-25T06:00:00+01:00"),
],
)
def test_overnight_end_uses_clock_time_on_end_date(start_date, start_time, end_time, expected_end):
window = TimeWindow.model_validate({"start_time": start_time, "end_time": end_time})
reference = pendulum.from_format(start_date, "YYYY-MM-DD", tz="Europe/Berlin")
_, end = window._window_start_end(reference)
assert end.isoformat() == expected_end
assert not window.contains(end)
assert window.contains(end.subtract(minutes=1))
@pytest.mark.parametrize(
("config", "start", "hours", "active_steps"),
[
({"start_time": "08:00", "end_time": "12:00"}, "2026-10-26", 24, [8, 9, 10, 11]),
# Duration may extend the explicit end.
(
{"start_time": "08:00", "end_time": "10:00", "duration": "4 hours"},
"2026-10-26",
24,
[8, 9, 10, 11],
),
({"start_time": "08:00", "end_time": "12:00", "month": 11}, "2026-10-26", 24, []),
({"start_time": "08:00", "end_time": "12:00", "day_of_week": 1}, "2026-10-26", 24, []),
({"start_time": "08:00", "end_time": "12:00", "date": "2026-10-27"}, "2026-10-26", 24, []),
# Include the tail of Friday's window on Saturday.
(
{"start_time": "22:00", "end_time": "06:00", "day_of_week": 4},
"2026-10-24",
24,
list(range(6)),
),
# Month restrictions belong to the opening date.
(
{"start_time": "22:00", "end_time": "06:00", "month": 10},
"2026-11-01",
24,
list(range(6)),
),
# Long durations can begin more than one calendar day earlier.
(
{"start_time": "08:00", "duration": "3 days", "date": "2026-10-24"},
"2026-10-26",
24,
list(range(24)),
),
# Recurring windows must also appear on later horizon days.
({"start_time": "08:00", "end_time": "10:00"}, "2026-10-26", 48, [8, 9, 32, 33]),
],
)
def test_cycle_arrays_use_effective_windows(config, start, hours, active_steps):
window = ValueTimeWindow.model_validate({**config, "value": 0})
sequence = CycleTimeWindowSequence(windows=[window])
reference = pendulum.from_format(start, "YYYY-MM-DD", tz="UTC")
end = reference.add(hours=hours)
interval = pendulum.duration(hours=1)
expected = np.zeros(hours)
expected[active_steps] = 1
actual = sequence.cycle_to_array(0, reference, end, interval)
indices, matrix = sequence.cycles_to_matrix(reference, end, interval)
assert indices == [0]
np.testing.assert_array_equal(actual, expected)
np.testing.assert_array_equal(matrix[0], expected)
@pytest.mark.parametrize(("date", "hours"), [("2026-03-29", 23), ("2026-10-25", 25)])
def test_cycle_arrays_across_dst(date, hours):
window = ValueTimeWindow.model_validate(
{"start_time": "00:00", "end_time": "06:00", "value": 0}
)
sequence = CycleTimeWindowSequence(windows=[window])
start = pendulum.from_format(date, "YYYY-MM-DD", tz="Europe/Berlin")
end = start.add(days=1)
expected = np.zeros(hours)
expected[: hours - 18] = 1 # Five elapsed hours in spring; seven in autumn.
actual = sequence.cycle_to_array(0, start, end, pendulum.duration(hours=1))
_, matrix = sequence.cycles_to_matrix(start, end, pendulum.duration(hours=1))
np.testing.assert_array_equal(actual, expected)
np.testing.assert_array_equal(matrix[0], expected)
def test_end_only_cycle_window_allows_appliance_starts():
sequence = CycleTimeWindowSequence.model_validate(
{"windows": [{"start_time": "08:00", "end_time": "12:00", "value": 0}]}
)
# Exercise the serialized settings used by the dashboard/API as well.
sequence = CycleTimeWindowSequence.model_validate_json(sequence.model_dump_json())
appliance = HomeAppliance(
parameters=HomeApplianceParameters(
device_id="dishwasher",
duration_h=2,
consumption_wh=2000,
num_cycles=1,
time_windows=sequence,
),
prediction_hours=24,
optimization_hours=24,
)
start = pendulum.datetime(2026, 10, 26)
appliance.start_allowed = []
appliance.start_earliest = []
appliance.start_latest = []
appliance._build_cycle_start_allowed(
sequence, start, start.add(days=1), pendulum.duration(hours=1)
)
np.testing.assert_array_equal(np.flatnonzero(appliance.start_allowed[0]), [8, 9, 10])