From 37c5248fa366fc518c099f3155d5397d4a31c390 Mon Sep 17 00:00:00 2001 From: seaspotter Date: Thu, 15 Oct 2026 14:50:52 +0200 Subject: [PATCH] tariff: kein Sprung auf Tagesende, solange Folgetagspreise noch fehlen und Warnschwelle nicht erreicht --- .../modules/common/configurable_tariff.py | 11 ++++ .../common/configurable_tariff_test.py | 50 +++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/packages/modules/common/configurable_tariff.py b/packages/modules/common/configurable_tariff.py index bf36c57cca..b7bd6b91f9 100644 --- a/packages/modules/common/configurable_tariff.py +++ b/packages/modules/common/configurable_tariff.py @@ -93,6 +93,17 @@ def _calculate_next_query_time(self, latest_price_timestamp: datetime) -> None: next_query_time = (now.replace(hour=next_hour, minute=0, second=0, microsecond=0) + timedelta(days=day_offset, minutes=random.randint(1, 7) * -5)) if latest_price_timestamp < next_query_time: + tomorrow_start = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + warning_hour = min(self.tariff_update_hours) + LATE_WARNING_OFFSET_HOURS + if latest_price_timestamp < tomorrow_start and now.hour < warning_hour: + # Noch keine Preise für den Folgetag und die Warnschwelle ist noch nicht erreicht: + # kurzer Backoff statt auf das Ende der heute schon bekannten Preise zu springen. + # Sonst würde bei Anbietern mit rollierender Tagesantwort (Preise reichen jeden + # Abruf etwas weiter in den noch laufenden Tag, ohne je den Folgetag zu erreichen) + # der Abruf für Stunden pausiert, noch bevor überhaupt die Warnschwelle erreicht ist + # (siehe #3703). + self._delay_next_query() + return # Falls die Preise nicht bis zum nächsten geplanten Abruf gültig sind, # den Abruf auf die Gültigkeitsdauer der Preise setzen log.debug(f"Die Preise sind nur bis {latest_price_timestamp} gültig, " diff --git a/packages/modules/common/configurable_tariff_test.py b/packages/modules/common/configurable_tariff_test.py index ec1f621942..7cef4b84b2 100644 --- a/packages/modules/common/configurable_tariff_test.py +++ b/packages/modules/common/configurable_tariff_test.py @@ -160,8 +160,9 @@ def now(cls, tz: Optional[tzinfo] = None): def test_calculate_next_query_time_uses_latest_price_timestamp(monkeypatch: pytest.MonkeyPatch): - # setup - fixed_now = datetime(2022, 5, 16, 10, 10, 0) + # Preise reichen bereits in den Folgetag, enden dort aber vor dem naechsten geplanten Abruf - + # der Abruf darf auf das Ende der bekannten (Folgetags-)Preise vorgezogen werden. + fixed_now = datetime(2022, 5, 16, 15, 10, 0) class FixedDateTime(datetime): @classmethod @@ -174,7 +175,50 @@ def now(cls, tz: Optional[tzinfo] = None): tariff = ConfigurableFlexibleTariff(AwattarTariff(), Mock(return_value=Mock())) tariff.tariff_update_hours = [14] - latest_price_timestamp = datetime(2022, 5, 16, 12, 30, 0) + latest_price_timestamp = datetime(2022, 5, 17, 2, 30, 0) + + # execution + tariff._calculate_next_query_time(latest_price_timestamp) # pyright: ignore[reportPrivateUsage] + + # evaluation + expected_timestamp = int(latest_price_timestamp.timestamp()) + assert tariff.get.next_query_time == expected_timestamp + + +def test_calculate_next_query_time_same_day_rollover_before_late_hour_schedules_short_backoff( + monkeypatch: pytest.MonkeyPatch): + # Regression-Test für #3703: Preise reichen nur bis spaeter am selben Tag (noch kein Folgetag), + # Warnschwelle noch nicht erreicht - kurzer Backoff statt Sprung auf das Tagesende, sonst pausiert + # der Abruf bei Anbietern mit rollierender Tagesantwort stundenlang, bevor ueberhaupt gewarnt wurde. + fixed_now = datetime(2022, 5, 16, 15, 10, 0) + _patch_now(monkeypatch, fixed_now) + monkeypatch.setattr(configurable_tariff.random, "randint", Mock(return_value=1)) + + tariff = ConfigurableFlexibleTariff(AwattarTariff(), Mock(return_value=Mock())) + tariff.tariff_update_hours = [14] + + latest_price_timestamp = datetime(2022, 5, 16, 23, 45, 0) + + # execution + tariff._calculate_next_query_time(latest_price_timestamp) # pyright: ignore[reportPrivateUsage] + + # evaluation + expected_timestamp = int(fixed_now.timestamp()) + RETRY_BACKOFF_SECONDS + assert tariff.get.next_query_time == expected_timestamp + + +def test_calculate_next_query_time_same_day_rollover_after_late_hour_still_uses_clamp( + monkeypatch: pytest.MonkeyPatch): + # Nach der Warnschwelle ist das Tagesende als naechster Abruf wieder in Ordnung - es wurde ja + # bereits gewarnt, staendiges Nachfragen bringt nichts mehr. + fixed_now = datetime(2022, 5, 16, 16, 10, 0) + _patch_now(monkeypatch, fixed_now) + monkeypatch.setattr(configurable_tariff.random, "randint", Mock(return_value=1)) + + tariff = ConfigurableFlexibleTariff(AwattarTariff(), Mock(return_value=Mock())) + tariff.tariff_update_hours = [14] + + latest_price_timestamp = datetime(2022, 5, 16, 23, 45, 0) # execution tariff._calculate_next_query_time(latest_price_timestamp) # pyright: ignore[reportPrivateUsage]