diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 7e108667ba..f85dd8c8f0 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -58,7 +58,7 @@ class UpdateConfig: - DATASTORE_VERSION = 132 + DATASTORE_VERSION = 133 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -3406,3 +3406,18 @@ def upgrade(topic: str, payload) -> None: return {topic: payload} self._loop_all_received_topics(upgrade) self._append_datastore_version(132) + + def upgrade_datastore_133(self) -> None: + def upgrade(topic: str, payload) -> None: + if ("openWB/optional/ep/flexible_tariff/provider" == topic or + "openWB/optional/ep/grid_fee/provider" == topic): + provider = decode_payload(payload) + if provider["type"] == "fixed_hours": + if provider["configuration"]: + if provider["configuration"]["tariffs"]: + for tariff in provider["configuration"]["tariffs"] or []: + if tariff.get("weekdays") is None: + tariff["weekdays"] = list(range(7)) + return {topic: provider} + self._loop_all_received_topics(upgrade) + self._append_datastore_version(133) diff --git a/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/config.py b/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/config.py index 4e39618144..dbf78dcd0d 100644 --- a/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/config.py +++ b/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/config.py @@ -12,6 +12,7 @@ class FixedHoursTariffConfiguration: "price": 0.20, "active_times": { "dates": [("01-01", "31-03"), ("01-07", "30-09")], # applicable date ranges (day-month) + "weekdays": [0, 1, 2, 3, 4], # active on weekdays (0=Monday, ..., 6=Sunday) "times": [("08:00", "12:00"), ("18:00", "22:00")] # active times during the day } }, @@ -20,6 +21,7 @@ class FixedHoursTariffConfiguration: "price": 0.05, "active_times": { "dates": [("01-04", "30-06"), ("01-10", "31-12")], # applicable date ranges (day-month) + "weekdays": [5, 6], # active on weekends (0=Monday, ..., 6=Sunday) "times": [("00:00", "06:00"), ("22:00", "23:59")] # active times during the day } } diff --git a/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/tariff.py b/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/tariff.py index 9754a7281a..23649602e5 100644 --- a/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/tariff.py +++ b/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/tariff.py @@ -3,6 +3,7 @@ import datetime import time from typing import List, Tuple, Dict +from dataclasses import dataclass from modules.electricity_pricing.flexible_tariffs.fixed_hours.config import (FixedHoursTariff, FixedHoursTariffConfiguration) @@ -12,6 +13,14 @@ log = logging.getLogger(__name__) +@dataclass +class TimeSlot: + start_time: datetime.time + end_time: datetime.time + weekdays: List[int] + dates: List[Tuple[str, str]] + + def _to_time(time_str: str) -> datetime.time: if time_str == "24:00": return datetime.time(23, 59, 59) @@ -25,17 +34,31 @@ def _to_date(date_str: str, time_slot: datetime.datetime) -> datetime.date: return date +def _date_ranges_overlap(slot1: TimeSlot, slot2: TimeSlot) -> bool: + def _single_range_overlap(range1: Tuple[str, str], range2: Tuple[str, str]) -> bool: + start1, end1 = _to_date(range1[0], datetime.datetime.now()), _to_date(range1[1], datetime.datetime.now()) + start2, end2 = _to_date(range2[0], datetime.datetime.now()), _to_date(range2[1], datetime.datetime.now()) + return start1 <= end2 and end1 >= start2 + return any(_single_range_overlap(r1, r2) for r1 in slot1.dates for r2 in slot2.dates) + + def _validate_tariff_times(config: FixedHoursTariffConfiguration) -> None: - time_slots: List[Tuple[datetime.time, datetime.time, List[Tuple[str, str]]]] = [] + time_slots: List[TimeSlot] = [] for tariff in config.tariffs: for start, end in tariff["active_times"]["times"]: start_time = _to_time(start) end_time = _to_time(end) - for existing_start, existing_end, existing_dates in time_slots: - if (start_time < existing_end and end_time > existing_start and - any(start <= existing_end and end >= existing_start for start, end in existing_dates)): - raise ValueError(f"Overlapping time window detected: {start} - {end} in tariff '{tariff['name']}'") - time_slots.append((start_time, end_time, tariff["active_times"]["dates"])) + weekdays = tariff["active_times"]["weekdays"] + dates = tariff["active_times"]["dates"] + current_slot = TimeSlot(start_time, end_time, weekdays, dates) + for existing in time_slots: + if (start_time < existing.end_time and end_time > existing.start_time and + not set(weekdays).isdisjoint(existing.weekdays) and + _date_ranges_overlap(current_slot, existing)): + overlapping_weekdays = set(weekdays) & set(existing.weekdays) + raise ValueError(f"Overlapping time window detected: {start} - {end}" + f" on weekdays {sorted(overlapping_weekdays)} in tariff '{tariff['name']}'") + time_slots.append(current_slot) def _fetch(config: FixedHoursTariffConfiguration) -> TariffState: @@ -52,8 +75,11 @@ def _fetch_price_for_time_slot(config: FixedHoursTariffConfiguration, (_to_date(start, time_slot), _to_date(end, time_slot)) for start, end in tariff["active_times"]["dates"] ] + # If no weekdays specified, assume all days are active + weekdays = tariff["active_times"]["weekdays"] or range(7) if (any(start <= time_slot.time() < end for start, end in active_times) and - any(start <= time_slot.date() <= end for start, end in active_dates)): + any(start <= time_slot.date() <= end for start, end in active_dates) and + time_slot.weekday() in weekdays): return tariff["price"] return config.default_price diff --git a/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/tariff_test.py b/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/tariff_test.py index ae4c01d6a1..2febc8a30e 100644 --- a/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/tariff_test.py +++ b/packages/modules/electricity_pricing/flexible_tariffs/fixed_hours/tariff_test.py @@ -96,6 +96,7 @@ class TestValidateTariffTimes: "active_times": { "dates": [("01-01", "31-12")], "times": [("08:00", "12:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, }, { @@ -104,6 +105,7 @@ class TestValidateTariffTimes: "active_times": { "dates": [("01-01", "31-12")], "times": [("14:00", "18:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, }, ], @@ -119,6 +121,7 @@ class TestValidateTariffTimes: "active_times": { "dates": [("01-01", "31-12")], "times": [("08:00", "12:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, }, { @@ -127,12 +130,63 @@ class TestValidateTariffTimes: "active_times": { "dates": [("01-01", "31-12")], "times": [("12:00", "14:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, }, ], True, id="adjacent_times", ), + # Overlapping times but different weekdays + pytest.param( + [ + { + "name": "tariff1", + "price": 100, + "active_times": { + "dates": [("01-01", "31-12")], + "times": [("08:00", "12:00")], + "weekdays": [0, 1, 2, 3, 4], # active on weekdays + }, + }, + { + "name": "tariff2", + "price": 200, + "active_times": { + "dates": [("01-01", "31-12")], + "times": [("08:00", "12:00")], + "weekdays": [5, 6], # active on weekends + }, + }, + ], + True, + id="differing weekdays allows overlap", + ), + # Overlapping times one day overlapping + pytest.param( + [ + { + "name": "tariff1", + "price": 100, + "active_times": { + "dates": [("01-01", "31-12")], + "times": [("08:00", "12:00")], + "weekdays": [0, 1, 2, 3, 4], # active on weekdays + }, + }, + { + "name": "tariff2", + "price": 200, + "active_times": { + "dates": [("01-01", "31-12")], + "times": [("08:00", "12:00")], + "weekdays": [4, 5, 6], # active on weekends + }, + }, + ], + False, + id="differing weekdays one day overlaps", + ), # Empty tariffs pytest.param([], True, id="empty_tariffs"), ], @@ -166,6 +220,7 @@ class TestFetch: "active_times": { "dates": [("01-01", "31-12")], "times": [("00:00", "24:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, } ], @@ -182,6 +237,7 @@ class TestFetch: "active_times": { "dates": [("01-01", "31-12")], "times": [("00:00", "24:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, } ], @@ -223,6 +279,7 @@ def test_fetch_price_configurations(self, default_price, tariffs, expected_price "active_times": { "dates": [("01-01", "31-12")], "times": [("00:00", "24:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, } ], @@ -273,6 +330,7 @@ class TestCreateElectricityTariff: "active_times": { "dates": [("01-01", "31-12")], "times": [("00:00", "24:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, } ], @@ -289,6 +347,7 @@ class TestCreateElectricityTariff: "active_times": { "dates": [("01-01", "31-12")], "times": [("00:00", "24:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, } ], @@ -304,6 +363,7 @@ class TestCreateElectricityTariff: "active_times": { "dates": [("01-01", "01-01")], "times": [("00:00", "00:00")], + "weekdays": [0, 1, 2, 3, 4, 5, 6], }, } ],