mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-03-12 09:36:17 +00:00
feat: add fixed electricity prediction with time window support (#930)
Some checks are pending
Bump Version / Bump Version Workflow (push) Waiting to run
docker-build / platform-excludes (push) Waiting to run
docker-build / build (push) Blocked by required conditions
docker-build / merge (push) Blocked by required conditions
pre-commit / pre-commit (push) Waiting to run
Run Pytest on Pull Request / test (push) Waiting to run
Some checks are pending
Bump Version / Bump Version Workflow (push) Waiting to run
docker-build / platform-excludes (push) Waiting to run
docker-build / build (push) Blocked by required conditions
docker-build / merge (push) Blocked by required conditions
pre-commit / pre-commit (push) Waiting to run
Run Pytest on Pull Request / test (push) Waiting to run
Add a fixed electricity prediction that supports prices per time window.
The time windows may flexible be defined by day or date.
The prediction documentation is updated to also cover the ElecPriceFixed
provider.
The feature includes several changes that are not directly related to the
electricity price prediction implementation but are necessary to keep
EOS running properly and to test and document the changes.
* feat: add value time windows
Add time windows with an associated float value.
* feat: harden eos measurements endpoints error detection and reporting
Cover more errors that may be raised during endpoint access. Report the
errors including trace information to ease debugging.
* feat: extend server configuration to cover all arguments
Make the argument controlled options also available in server configuration.
* fix: eos config configuration by cli arguments
Move the command line argument handling to config eos so that it is
excuted whenever eos config is rebuild or reset.
* chore: extend measurement endpoint system test
* chore: refactor time windows
Move time windows to configabc as they are only used in configurations.
Also move all tests to test_configabc.
* chore: provide config update errors in eosdash with summarized error text
If there is an update error provide the error text as a summary. On click
provide the full error text.
* chore: force eosdash ip address and port in makefile dev run
Ensure eosdash ip address and port are correctly set for development runs.
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
@@ -23,8 +23,6 @@ from akkudoktoreos.utils.datetimeutil import (
|
||||
DateTime,
|
||||
Duration,
|
||||
Time,
|
||||
TimeWindow,
|
||||
TimeWindowSequence,
|
||||
_parse_time_string,
|
||||
compare_datetimes,
|
||||
hours_in_day,
|
||||
@@ -839,459 +837,6 @@ class TestPendulumTypes:
|
||||
assert model.run_duration.total_minutes() == 180
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# TimeWindow
|
||||
# -----------------------------
|
||||
|
||||
|
||||
class TestTimeWindow:
|
||||
"""Tests for the TimeWindow model."""
|
||||
|
||||
def test_datetime_within_and_outside_window(self):
|
||||
"""Test datetime containment logic inside and outside the time window."""
|
||||
window = TimeWindow(start_time=Time(6, 0), duration=Duration(hours=3))
|
||||
assert window.contains(DateTime(2025, 7, 12, 7, 30)) is True # Inside
|
||||
assert window.contains(DateTime(2025, 7, 12, 9, 30)) is False # Outside
|
||||
|
||||
def test_contains_with_duration(self):
|
||||
"""Test datetime with duration that does and doesn't fit in the window."""
|
||||
window = TimeWindow(start_time=Time(6, 0), duration=Duration(hours=3))
|
||||
assert window.contains(DateTime(2025, 7, 12, 6, 30), duration=Duration(minutes=60)) is True
|
||||
assert window.contains(DateTime(2025, 7, 12, 6, 30), duration=Duration(hours=3)) is False
|
||||
|
||||
def test_day_of_week_filter(self):
|
||||
"""Test time window restricted by day of week."""
|
||||
window = TimeWindow(start_time=Time(6, 0), duration=Duration(hours=2), day_of_week=5) # Saturday
|
||||
assert window.contains(DateTime(2025, 7, 12, 6, 30)) is True # Saturday
|
||||
assert window.contains(DateTime(2025, 7, 11, 6, 30)) is False # Friday
|
||||
|
||||
def test_day_of_week_as_english_name(self):
|
||||
"""Test time window with English weekday name."""
|
||||
window = TimeWindow(start_time=Time(6, 0), duration=Duration(hours=2), day_of_week="monday")
|
||||
assert window.contains(DateTime(2025, 7, 7, 6, 30)) is True # Monday
|
||||
assert window.contains(DateTime(2025, 7, 5, 6, 30)) is False # Saturday
|
||||
|
||||
def test_specific_date_filter(self):
|
||||
"""Test time window restricted by exact date."""
|
||||
window = TimeWindow(start_time=Time(6, 0), duration=Duration(hours=2), date=Date(2025, 7, 12))
|
||||
assert window.contains(DateTime(2025, 7, 12, 6, 30)) is True
|
||||
assert window.contains(DateTime(2025, 7, 13, 6, 30)) is False
|
||||
|
||||
def test_invalid_field_types_raise_validation(self):
|
||||
"""Test invalid types raise a Pydantic validation error."""
|
||||
with pytest.raises(ValidationError):
|
||||
TimeWindow(start_time="not_a_time", duration="3h")
|
||||
|
||||
@pytest.mark.parametrize("locale, weekday_name, expected_dow", [
|
||||
("de", "Montag", 0),
|
||||
("de", "Samstag", 5),
|
||||
("es", "lunes", 0),
|
||||
("es", "sábado", 5),
|
||||
("fr", "lundi", 0),
|
||||
("fr", "samedi", 5),
|
||||
])
|
||||
def test_localized_day_names(self, locale, weekday_name, expected_dow):
|
||||
"""Test that localized weekday names are resolved to correct weekday index."""
|
||||
window = TimeWindow(start_time=Time(6, 0), duration=Duration(hours=2), day_of_week=weekday_name, locale=locale)
|
||||
assert window.day_of_week == expected_dow
|
||||
|
||||
|
||||
# ------------------
|
||||
# TimeWindowSequence
|
||||
# ------------------
|
||||
|
||||
|
||||
class TestTimeWindowSequence:
|
||||
"""Test suite for TimeWindowSequence model."""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_time_window_1(self):
|
||||
"""Morning window: 9:00 AM - 12:00 PM."""
|
||||
return TimeWindow(
|
||||
start_time=Time(9, 0, 0),
|
||||
duration=Duration(hours=3)
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_time_window_2(self):
|
||||
"""Afternoon window: 2:00 PM - 5:00 PM."""
|
||||
return TimeWindow(
|
||||
start_time=Time(14, 0, 0),
|
||||
duration=Duration(hours=3)
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def monday_window(self):
|
||||
"""Monday only window: 10:00 AM - 11:00 AM."""
|
||||
return TimeWindow(
|
||||
start_time=Time(10, 0, 0),
|
||||
duration=Duration(hours=1),
|
||||
day_of_week=0 # Monday
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def specific_date_window(self):
|
||||
"""Specific date window: 1:00 PM - 3:00 PM on 2025-01-15."""
|
||||
return TimeWindow(
|
||||
start_time=Time(13, 0, 0),
|
||||
duration=Duration(hours=2),
|
||||
date=Date(2025, 1, 15)
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_sequence(self, sample_time_window_1, sample_time_window_2):
|
||||
"""Sequence with morning and afternoon windows."""
|
||||
return TimeWindowSequence(windows=[sample_time_window_1, sample_time_window_2])
|
||||
|
||||
@pytest.fixture
|
||||
def sample_sequence_json(self, sample_time_window_1, sample_time_window_2):
|
||||
"""Sequence with morning and afternoon windows."""
|
||||
seq_json = TimeWindowSequence(windows=[sample_time_window_1, sample_time_window_2]).model_dump()
|
||||
return seq_json
|
||||
|
||||
@pytest.fixture
|
||||
def sample_sequence_json_str(self, sample_time_window_1, sample_time_window_2):
|
||||
"""Sequence with morning and afternoon windows."""
|
||||
seq_json_str = TimeWindowSequence(windows=[sample_time_window_1, sample_time_window_2]).model_dumps(indent=2)
|
||||
return seq_json_str
|
||||
|
||||
@pytest.fixture
|
||||
def reference_date(self):
|
||||
"""Reference date for testing: 2025-01-15 (Wednesday)."""
|
||||
return pendulum.parse("2025-01-15T08:00:00")
|
||||
|
||||
def test_init_with_none_windows(self):
|
||||
"""Test initialization with None windows creates empty list."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert sequence.windows == []
|
||||
assert len(sequence) == 0
|
||||
|
||||
def test_init_with_explicit_none(self):
|
||||
"""Test initialization with explicit None windows."""
|
||||
sequence = TimeWindowSequence(windows=None)
|
||||
assert sequence.windows == []
|
||||
assert len(sequence) == 0
|
||||
|
||||
def test_init_with_empty_list(self):
|
||||
"""Test initialization with empty list."""
|
||||
sequence = TimeWindowSequence(windows=[])
|
||||
assert sequence.windows == []
|
||||
assert len(sequence) == 0
|
||||
|
||||
def test_init_with_windows(self, sample_time_window_1, sample_time_window_2):
|
||||
"""Test initialization with windows."""
|
||||
sequence = TimeWindowSequence(windows=[sample_time_window_1, sample_time_window_2])
|
||||
assert len(sequence) == 2
|
||||
assert sequence.windows is not None # make mypy happy
|
||||
assert sequence.windows[0] == sample_time_window_1
|
||||
assert sequence.windows[1] == sample_time_window_2
|
||||
|
||||
def test_iterator_protocol(self, sample_sequence):
|
||||
"""Test that sequence supports iteration."""
|
||||
windows = list(sample_sequence)
|
||||
assert len(windows) == 2
|
||||
assert all(isinstance(window, TimeWindow) for window in windows)
|
||||
|
||||
def test_indexing(self, sample_sequence, sample_time_window_1):
|
||||
"""Test indexing into sequence."""
|
||||
assert sample_sequence[0] == sample_time_window_1
|
||||
|
||||
def test_length(self, sample_sequence):
|
||||
"""Test len() support."""
|
||||
assert len(sample_sequence) == 2
|
||||
|
||||
def test_contains_empty_sequence(self, reference_date):
|
||||
"""Test contains() with empty sequence returns False."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert not sequence.contains(reference_date)
|
||||
assert not sequence.contains(reference_date, Duration(hours=1))
|
||||
|
||||
def test_contains_datetime_in_window(self, sample_sequence, reference_date):
|
||||
"""Test contains() finds datetime in one of the windows."""
|
||||
# 10:00 AM should be in the morning window (9:00 AM - 12:00 PM)
|
||||
test_time = reference_date.replace(hour=10, minute=0)
|
||||
assert sample_sequence.contains(test_time)
|
||||
|
||||
def test_contains_datetime_not_in_any_window(self, sample_sequence, reference_date):
|
||||
"""Test contains() returns False when datetime is not in any window."""
|
||||
# 1:00 PM should not be in any window (gap between morning and afternoon)
|
||||
test_time = reference_date.replace(hour=13, minute=0)
|
||||
assert not sample_sequence.contains(test_time)
|
||||
|
||||
def test_contains_with_duration_fits(self, sample_sequence, reference_date):
|
||||
"""Test contains() with duration that fits in a window."""
|
||||
# 10:00 AM with 1 hour duration should fit in morning window
|
||||
test_time = reference_date.replace(hour=10, minute=0)
|
||||
assert sample_sequence.contains(test_time, Duration(hours=1))
|
||||
|
||||
def test_contains_with_duration_too_long(self, sample_sequence, reference_date):
|
||||
"""Test contains() with duration that doesn't fit in any window."""
|
||||
# 11:00 AM with 2 hours duration won't fit in remaining morning window time
|
||||
test_time = reference_date.replace(hour=11, minute=0)
|
||||
assert not sample_sequence.contains(test_time, Duration(hours=2))
|
||||
|
||||
def test_earliest_start_time_empty_sequence(self, reference_date):
|
||||
"""Test earliest_start_time() with empty sequence returns None."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert sequence.earliest_start_time(Duration(hours=1), reference_date) is None
|
||||
|
||||
def test_earliest_start_time_finds_earliest(self, sample_sequence, reference_date):
|
||||
"""Test earliest_start_time() finds the earliest time across all windows."""
|
||||
# Should return 9:00 AM (start of morning window)
|
||||
earliest = sample_sequence.earliest_start_time(Duration(hours=1), reference_date)
|
||||
expected = reference_date.replace(hour=9, minute=0, second=0, microsecond=0)
|
||||
assert earliest == expected
|
||||
|
||||
def test_earliest_start_time_duration_too_long(self, sample_sequence, reference_date):
|
||||
"""Test earliest_start_time() with duration longer than any window."""
|
||||
# 4 hours won't fit in any 3-hour window
|
||||
assert sample_sequence.earliest_start_time(Duration(hours=4), reference_date) is None
|
||||
|
||||
def test_latest_start_time_empty_sequence(self, reference_date):
|
||||
"""Test latest_start_time() with empty sequence returns None."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert sequence.latest_start_time(Duration(hours=1), reference_date) is None
|
||||
|
||||
def test_latest_start_time_finds_latest(self, sample_sequence, reference_date):
|
||||
"""Test latest_start_time() finds the latest time across all windows."""
|
||||
# Should return 4:00 PM (latest start for 1 hour in afternoon window)
|
||||
latest = sample_sequence.latest_start_time(Duration(hours=1), reference_date)
|
||||
expected = reference_date.replace(hour=16, minute=0, second=0, microsecond=0)
|
||||
assert latest == expected
|
||||
|
||||
def test_can_fit_duration_empty_sequence(self, reference_date):
|
||||
"""Test can_fit_duration() with empty sequence returns False."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert not sequence.can_fit_duration(Duration(hours=1), reference_date)
|
||||
|
||||
def test_can_fit_duration_fits_in_one_window(self, sample_sequence, reference_date):
|
||||
"""Test can_fit_duration() returns True when duration fits in one window."""
|
||||
assert sample_sequence.can_fit_duration(Duration(hours=2), reference_date)
|
||||
|
||||
def test_can_fit_duration_too_long(self, sample_sequence, reference_date):
|
||||
"""Test can_fit_duration() returns False when duration is too long."""
|
||||
assert not sample_sequence.can_fit_duration(Duration(hours=4), reference_date)
|
||||
|
||||
def test_available_duration_empty_sequence(self, reference_date):
|
||||
"""Test available_duration() with empty sequence returns None."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert sequence.available_duration(reference_date) is None
|
||||
|
||||
def test_available_duration_sums_all_windows(self, sample_sequence, reference_date):
|
||||
"""Test available_duration() sums durations from all applicable windows."""
|
||||
# 3 hours + 3 hours = 6 hours total
|
||||
total = sample_sequence.available_duration(reference_date)
|
||||
assert total == Duration(hours=6)
|
||||
|
||||
def test_available_duration_with_day_restriction(self, monday_window, reference_date):
|
||||
"""Test available_duration() respects day restrictions."""
|
||||
sequence = TimeWindowSequence(windows=[monday_window])
|
||||
|
||||
# Reference date is Wednesday, so Monday window shouldn't apply
|
||||
assert sequence.available_duration(reference_date) is None
|
||||
|
||||
# Monday date should apply
|
||||
monday_date = pendulum.parse("2025-01-13T08:00:00") # Monday
|
||||
assert sequence.available_duration(monday_date) == Duration(hours=1)
|
||||
|
||||
def test_get_applicable_windows_empty_sequence(self, reference_date):
|
||||
"""Test get_applicable_windows() with empty sequence."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert sequence.get_applicable_windows(reference_date) == []
|
||||
|
||||
def test_get_applicable_windows_all_apply(self, sample_sequence, reference_date):
|
||||
"""Test get_applicable_windows() returns all windows when they all apply."""
|
||||
applicable = sample_sequence.get_applicable_windows(reference_date)
|
||||
assert len(applicable) == 2
|
||||
|
||||
def test_get_applicable_windows_with_restrictions(self, monday_window, reference_date):
|
||||
"""Test get_applicable_windows() respects day restrictions."""
|
||||
sequence = TimeWindowSequence(windows=[monday_window])
|
||||
|
||||
# Wednesday - no applicable windows
|
||||
assert sequence.get_applicable_windows(reference_date) == []
|
||||
|
||||
# Monday - one applicable window
|
||||
monday_date = pendulum.parse("2025-01-13T08:00:00")
|
||||
applicable = sequence.get_applicable_windows(monday_date)
|
||||
assert len(applicable) == 1
|
||||
assert applicable[0] == monday_window
|
||||
|
||||
def test_find_windows_for_duration_empty_sequence(self, reference_date):
|
||||
"""Test find_windows_for_duration() with empty sequence."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert sequence.find_windows_for_duration(Duration(hours=1), reference_date) == []
|
||||
|
||||
def test_find_windows_for_duration_all_fit(self, sample_sequence, reference_date):
|
||||
"""Test find_windows_for_duration() when duration fits in all windows."""
|
||||
fitting = sample_sequence.find_windows_for_duration(Duration(hours=2), reference_date)
|
||||
assert len(fitting) == 2
|
||||
|
||||
def test_find_windows_for_duration_some_fit(self, sample_sequence, reference_date):
|
||||
"""Test find_windows_for_duration() when duration fits in some windows."""
|
||||
# Add a short window that can't fit 2.5 hours
|
||||
short_window = TimeWindow(start_time=Time(18, 0, 0), duration=Duration(hours=1))
|
||||
sequence = TimeWindowSequence(windows=sample_sequence.windows + [short_window])
|
||||
|
||||
fitting = sequence.find_windows_for_duration(Duration(hours=2, minutes=30), reference_date)
|
||||
assert len(fitting) == 2 # Only the first two windows can fit 2.5 hours
|
||||
|
||||
def test_get_all_possible_start_times_empty_sequence(self, reference_date):
|
||||
"""Test get_all_possible_start_times() with empty sequence."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert sequence.get_all_possible_start_times(Duration(hours=1), reference_date) == []
|
||||
|
||||
def test_get_all_possible_start_times_multiple_windows(self, sample_sequence, reference_date):
|
||||
"""Test get_all_possible_start_times() returns ranges for all fitting windows."""
|
||||
ranges = sample_sequence.get_all_possible_start_times(Duration(hours=1), reference_date)
|
||||
assert len(ranges) == 2
|
||||
|
||||
# Check morning window range
|
||||
earliest_morning, latest_morning, morning_window = ranges[0]
|
||||
assert earliest_morning == reference_date.replace(hour=9, minute=0, second=0, microsecond=0)
|
||||
assert latest_morning == reference_date.replace(hour=11, minute=0, second=0, microsecond=0)
|
||||
|
||||
# Check afternoon window range
|
||||
earliest_afternoon, latest_afternoon, afternoon_window = ranges[1]
|
||||
assert earliest_afternoon == reference_date.replace(hour=14, minute=0, second=0, microsecond=0)
|
||||
assert latest_afternoon == reference_date.replace(hour=16, minute=0, second=0, microsecond=0)
|
||||
|
||||
def test_add_window(self, sample_time_window_1):
|
||||
"""Test adding a window to the sequence."""
|
||||
sequence = TimeWindowSequence()
|
||||
assert len(sequence) == 0
|
||||
|
||||
sequence.add_window(sample_time_window_1)
|
||||
assert len(sequence) == 1
|
||||
assert sequence[0] == sample_time_window_1
|
||||
|
||||
def test_remove_window(self, sample_sequence, sample_time_window_1):
|
||||
"""Test removing a window from the sequence."""
|
||||
assert len(sample_sequence) == 2
|
||||
|
||||
removed = sample_sequence.remove_window(0)
|
||||
assert removed == sample_time_window_1
|
||||
assert len(sample_sequence) == 1
|
||||
|
||||
def test_remove_window_invalid_index(self, sample_sequence):
|
||||
"""Test removing a window with invalid index raises IndexError."""
|
||||
with pytest.raises(IndexError):
|
||||
sample_sequence.remove_window(10)
|
||||
|
||||
def test_remove_window_from_empty_sequence(self):
|
||||
"""Test removing a window from empty sequence raises IndexError."""
|
||||
sequence = TimeWindowSequence()
|
||||
with pytest.raises(IndexError):
|
||||
sequence.remove_window(0)
|
||||
|
||||
def test_clear_windows(self, sample_sequence):
|
||||
"""Test clearing all windows from the sequence."""
|
||||
assert len(sample_sequence) == 2
|
||||
|
||||
sample_sequence.clear_windows()
|
||||
assert len(sample_sequence) == 0
|
||||
assert sample_sequence.windows == []
|
||||
|
||||
def test_sort_windows_by_start_time(self, reference_date):
|
||||
"""Test sorting windows by start time."""
|
||||
# Create windows in reverse chronological order
|
||||
afternoon_window = TimeWindow(start_time=Time(14, 0, 0), duration=Duration(hours=2))
|
||||
morning_window = TimeWindow(start_time=Time(9, 0, 0), duration=Duration(hours=2))
|
||||
evening_window = TimeWindow(start_time=Time(18, 0, 0), duration=Duration(hours=2))
|
||||
|
||||
sequence = TimeWindowSequence(windows=[afternoon_window, morning_window, evening_window])
|
||||
sequence.sort_windows_by_start_time(reference_date)
|
||||
|
||||
# Should now be sorted: morning, afternoon, evening
|
||||
assert sequence[0] == morning_window
|
||||
assert sequence[1] == afternoon_window
|
||||
assert sequence[2] == evening_window
|
||||
|
||||
def test_sort_windows_with_non_applicable_windows(self, monday_window, reference_date):
|
||||
"""Test sorting windows with some non-applicable windows."""
|
||||
daily_window = TimeWindow(start_time=Time(10, 0, 0), duration=Duration(hours=1))
|
||||
|
||||
sequence = TimeWindowSequence(windows=[monday_window, daily_window])
|
||||
sequence.sort_windows_by_start_time(reference_date) # Wednesday
|
||||
|
||||
# Daily window should come first (applicable), Monday window last (not applicable)
|
||||
assert sequence[0] == daily_window
|
||||
assert sequence[1] == monday_window
|
||||
|
||||
def test_sort_windows_empty_sequence(self, reference_date):
|
||||
"""Test sorting an empty sequence doesn't raise errors."""
|
||||
sequence = TimeWindowSequence()
|
||||
sequence.sort_windows_by_start_time(reference_date)
|
||||
assert len(sequence) == 0
|
||||
|
||||
def test_default_reference_date_handling(self, sample_sequence):
|
||||
"""Test that methods handle default reference date (today) correctly."""
|
||||
# These should not raise errors and should return reasonable values
|
||||
assert isinstance(sample_sequence.can_fit_duration(Duration(hours=1)), bool)
|
||||
assert sample_sequence.available_duration() is not None
|
||||
assert isinstance(sample_sequence.get_applicable_windows(), list)
|
||||
|
||||
def test_specific_date_window_functionality(self, specific_date_window):
|
||||
"""Test functionality with specific date restrictions."""
|
||||
sequence = TimeWindowSequence(windows=[specific_date_window])
|
||||
|
||||
# Should work on the specific date
|
||||
specific_date = pendulum.parse("2025-01-15T12:00:00")
|
||||
assert sequence.can_fit_duration(Duration(hours=1), specific_date)
|
||||
|
||||
# Should not work on other dates
|
||||
other_date = pendulum.parse("2025-01-16T12:00:00")
|
||||
assert not sequence.can_fit_duration(Duration(hours=1), other_date)
|
||||
|
||||
def test_edge_cases_with_zero_duration(self, sample_sequence, reference_date):
|
||||
"""Test edge cases with zero duration."""
|
||||
zero_duration = Duration()
|
||||
|
||||
# Should be able to fit zero duration
|
||||
assert sample_sequence.can_fit_duration(zero_duration, reference_date)
|
||||
|
||||
# Should find start times for zero duration
|
||||
earliest = sample_sequence.earliest_start_time(zero_duration, reference_date)
|
||||
assert earliest is not None
|
||||
|
||||
def test_overlapping_windows(self, reference_date):
|
||||
"""Test behavior with overlapping windows."""
|
||||
window1 = TimeWindow(start_time=Time(10, 0, 0), duration=Duration(hours=3))
|
||||
window2 = TimeWindow(start_time=Time(11, 0, 0), duration=Duration(hours=3))
|
||||
|
||||
sequence = TimeWindowSequence(windows=[window1, window2])
|
||||
|
||||
# Should handle overlapping windows correctly
|
||||
test_time = reference_date.replace(hour=11, minute=30)
|
||||
assert sequence.contains(test_time)
|
||||
|
||||
# Total duration should be sum of both windows (even though they overlap)
|
||||
total = sequence.available_duration(reference_date)
|
||||
assert total == Duration(hours=6)
|
||||
|
||||
def test_sequence_model_dump(self, sample_sequence_json):
|
||||
"""Test that model dump creates the correct json."""
|
||||
assert sample_sequence_json == json.loads("""
|
||||
{
|
||||
"windows": [
|
||||
{
|
||||
"start_time": "09:00:00.000000",
|
||||
"duration": "3 hours",
|
||||
"day_of_week": null,
|
||||
"date": null,
|
||||
"locale": null
|
||||
},
|
||||
{
|
||||
"start_time": "14:00:00.000000",
|
||||
"duration": "3 hours",
|
||||
"day_of_week": null,
|
||||
"date": null,
|
||||
"locale": null
|
||||
}
|
||||
]
|
||||
}""")
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# to_datetime
|
||||
# -----------------------------
|
||||
|
||||
Reference in New Issue
Block a user