Skip to content

Feature/tibber/quarter hourly prices#2801

Merged
LKuemmel merged 19 commits intoopenWB:masterfrom
tpd-opitz:feature/tibber/quarter-hourly-prices
Nov 4, 2025
Merged

Feature/tibber/quarter hourly prices#2801
LKuemmel merged 19 commits intoopenWB:masterfrom
tpd-opitz:feature/tibber/quarter-hourly-prices

Conversation

@tpd-opitz
Copy link
Contributor

@tpd-opitz tpd-opitz commented Sep 30, 2025

  • Tibber

    • Afrage für Tibber auf 15-Mituten Auflösung umgestellt
  • alle ET Provider

    • Abfragehäufigkeit für alle Provider auf ein mal am Tag begrenzt
    • zufälliger Abfragezeitpunkt zwischen 13:30 und 14:00 zur Entlastung der ET-Provider-Resourcen
    • Wiederholung der Abfrage wenn Daten noch nicht verfügbar
    • Wiederholung der Abfrage bei Abfragefehler
    • Reduzierung der Preisliste so dass der erste Eintrag die aktuelle Zeit beinhaltet
    • sofortiger Abruf bei Anbieterwechsel
  • Backend

    • minimal 5Min Auflösung (Intervall für Reduzierung der Preisliste)
    • Ladezeitenberechnung für Eco-Modus
      • unterstützt beliebige Auflösungen bei den Preisdaten, solange alle Einträge in der Liste die selbe Gültigkeitsdauer haben.
      • Ladeziel wird spätest möglich erreicht
      • Laden beginnt ggf. im aktuellen Zeitfenster
  • Frontend

    • Zeitpunkt der nächsten Abfrage wird im Status angezeigt
    • Meldung über zu wenige Einträge berücksichtigt (beliebige) Auflösung der Preisdaten

@benderl
Copy link
Contributor

benderl commented Sep 30, 2025

Den Diagrammen ist es ziemlich egal, in welchem Abstand die Werte sind. Es wird eine Zeitachse verwendet und darauf die Timestamps abgebildet.

Eine Anpassung im Status oder den offiziellen Themes ist daher zu 99% nicht erforderlich.

@tpd-opitz
Copy link
Contributor Author

tpd-opitz commented Sep 30, 2025

Den Diagrammen ist es ziemlich egal, in welchem Abstand die Werte sind. Es wird eine Zeitachse verwendet und darauf die Timestamps abgebildet.

Eine Anpassung im Status oder den offiziellen Themes ist daher zu 99% nicht erforderlich.

stimmt...
image

aber dafür habe ich das hier auskommentiert:
./web/et_tibber/tibbergetprices.py"

210                 pricelist.remove(price)$
211 #        # jetzt prüfen auf Start mit aktueller Stunde und Stundenabstände$
212 #        if len(pricelist) > 0:$
213 #            timestamp_prev = float(pricelist[0][0])  # erster Listeneintrag$
214 #            starttime_utc = _get_utcfromtimestamp(float(pricelist[0][0]))$
215 #            if _get_utcfromtimestamp(timestamp_prev) == now_full_hour:  # erster Preis ist der von aktueller Stunde    $
216 #                for index, price in enumerate(pricelist[:]):  # über Kopie der Liste iterieren, um das Original zu     manipulieren$
217 #                    if index > 0:$
218 #                        timestamp = float(price[0])$
219 #                        secondsdiff = timestamp - timestamp_prev$
220 #                        timestamp_prev = float(price[0])$
221 #                        if secondsdiff != 3600.0:  # ist Abstand <> 1h dann ab hier Liste löschen$
222 #                            del pricelist[index:]$
223 #                            break$
224 #            else:$
225 #                return []$
226         return pricelist$

@tpd-opitz
Copy link
Contributor Author

@benderl : muss ich mich wirklich um den timeout-Fehler kümmern?

@benderl
Copy link
Contributor

benderl commented Sep 30, 2025

Welchen Timeout Fehler? Ich sehe aktuell nur einen fehlgeschlagenen Test im Tibber Modul. Da passt das erwartete Ergebnis nicht.

@tpd-opitz tpd-opitz marked this pull request as ready for review September 30, 2025 18:21
@tpd-opitz

This comment was marked as resolved.

@benderl
Copy link
Contributor

benderl commented Oct 1, 2025

mit primitiven Datentypen rechnen ist keine so tolle Idee, wenn's um Geld geht...

Hast Du einen besseren Vorschlag?

@benderl
Copy link
Contributor

benderl commented Oct 1, 2025

aber dafür habe ich das hier auskommentiert: ./web/et_tibber/tibbergetprices.py"

210                 pricelist.remove(price)$
211 #        # jetzt prüfen auf Start mit aktueller Stunde und Stundenabstände$
212 #        if len(pricelist) > 0:$
213 #            timestamp_prev = float(pricelist[0][0])  # erster Listeneintrag$
214 #            starttime_utc = _get_utcfromtimestamp(float(pricelist[0][0]))$
215 #            if _get_utcfromtimestamp(timestamp_prev) == now_full_hour:  # erster Preis ist der von aktueller Stunde    $
216 #                for index, price in enumerate(pricelist[:]):  # über Kopie der Liste iterieren, um das Original zu     manipulieren$
217 #                    if index > 0:$
218 #                        timestamp = float(price[0])$
219 #                        secondsdiff = timestamp - timestamp_prev$
220 #                        timestamp_prev = float(price[0])$
221 #                        if secondsdiff != 3600.0:  # ist Abstand <> 1h dann ab hier Liste löschen$
222 #                            del pricelist[index:]$
223 #                            break$
224 #            else:$
225 #                return []$
226         return pricelist$

Wo soll die Datei liegen? Ich finde die aktuell nicht.

@tpd-opitz

This comment was marked as outdated.

@tpd-opitz
Copy link
Contributor Author

mit primitiven Datentypen rechnen ist keine so tolle Idee, wenn's um Geld geht...

Hast Du einen besseren Vorschlag?

Decimal wäre das Mittel der Wahl, aber das nachträglich einzuziehen dürfte aufwendig sein...

@tpd-opitz
Copy link
Contributor Author

Puhhh.. Geschafft...
Zielladen funktioniert mit viertelstündlichen Preisen von Tibber.

TODO: viertelstündliches Update des MQTT, damit der aktuelle Preis ins Frontend kommt...

@LKuemmel
Copy link
Contributor

LKuemmel commented Oct 8, 2025

Warte bitte erstmal mit weiteren Implmentierungen an deiner Lösung. Mit dieser legst Du dich auf 60 und 15 Min fest. Die Info, ob die Preise alle 15 oder 60 Min übermittelt werden, sollte nur im Stromtarif-Modul relevant sein und keine Abhängigkeit von den Stromtarifmodulen ins Ziel- und Ecoladen ziehen. Die Preise werden mit Timestamps übermittelt und sind bis zum folgenden Timestamp gültig, das sollte als Info ausreichen.

@tpd-opitz
Copy link
Contributor Author

tpd-opitz commented Oct 8, 2025

Die Info, ob die Preise alle 15 oder 60 Min übermittelt werden, sollte nur im Stromtarif-Modul relevant sein und keine Abhängigkeit von den Stromtarifmodulen ins Ziel- und Ecoladen ziehen.

Im Zielladen (Option.get_loading_hours()) wird die Länge der übermittelten Zeit-Intervalle berechnet und nicht übergeben. da wurde keine neue Abhängigkeit eingefügt.
Beim Eco-Laden habe ich nichts geändert aber es funktioniert out of the box...

Das neue Property in TariffState ist in erster Linie für die Info-Meldung im Status um dort die Anzahl der Einträge für einen Tag zu berechnen. Das könnte man auch so wie beim Zielladen machen, aber die Abhängigkeit zu TariffState existiert hier sowieso schon...

Die einzige Stelle im Backed, die tatsächlich auf 60/14 Minuten festgelegt ist ist timecheck mit den create_unix_timestamp_current_*_hour() Funktionen. Die müssten dahin gehend verallgemeinert werden, dass es nur eine Funktion gibt, die die Länge eines Preis-Zeit-Slots übergeben bekommt...
(+ Aufruf in Optional anpassen...)

Comment on lines +234 to +245
return current_time - (current_time % quarter_hour)
return int(round_to_quarter_hour(datetime.datetime.today().timestamp()))


Copy link
Contributor Author

@tpd-opitz tpd-opitz Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LKuemmel was hältst Du davon?

Suggested change
def __calculate_price_timeslot_length(self, prices: dict[str, int]) -> int:
first_timestamps = sorted(list(prices.keys()))[:2]
return int(first_timestamps[1]) - int(first_timestamps[0])
def select_unix_timestamp_current_price_slot(prices: dict[str, float]) -> int:
now = int(create_timestamp())
price_timeslot_seconds = self.__calculate_price_timeslot_length(prices)
return next(iter( [int(timestamp) for timestamp in list(prices.keys()) if (
int(timestamp) <= now and int(timestamp) >= now - price_timeslot_seconds
)]))

@tpd-opitz tpd-opitz force-pushed the feature/tibber/quarter-hourly-prices branch from 8ebfdfa to 939a371 Compare October 8, 2025 19:25
@tpd-opitz
Copy link
Contributor Author

Erledigt ist:

  • Abfrage gegen Tibber angepasst
  • Abfrage der API (aller ET-Provider) nur ein mal am Tag 14:00 +- 7Minuten
  • Aktualisierung der Strompreise im Backend alle 5 Minuten -> Frontendaktualisierung implizit auch
  • Zielladen errechnet benötigte Anzahl der Zeitslots aus den Zeitstempeln der Strompreisdaten
    • know issue: Strompreislisten mit variablen Gültigkeitsdauern (octopus intelligent go?)
  • fix Zielladen
    • wenn der erste Zeitslot ausgewählt, aber schon weit abgelaufen muss ggf. ein weiterer Zeitslot ausgewählt werden, dammit genügend lange geladen wird

@tpd-opitz tpd-opitz force-pushed the feature/tibber/quarter-hourly-prices branch 13 times, most recently from a60fcde to 3ef43ec Compare October 12, 2025 18:45
@gvzdus
Copy link
Contributor

gvzdus commented Oct 31, 2025

Jetzt "lebt" das System wieder: 19:15 Uhr brachte die fällige Rotation der Preise.

@tpd-opitz
Copy link
Contributor Author

Jetzt "lebt" das System wieder: 19:15 Uhr brachte die fällige Rotation der Preise.

Schauen wir mal, ob denn morgen Mittag die neuen Preise sauber eingepflegt werden und dein "leere Preisliste" Problem noch mal auf tritt...

@gvzdus
Copy link
Contributor

gvzdus commented Oct 31, 2025

Ich weiß, dass ich Dich im Zweifelsfalls zum "Python war ein Irrtum" treibe, aber:
Testcase:
Wechsele auf EnergyCharts. Hänge Dich auf das 'openWB/optional/et/get/prices'-Topic. Die Preise werden neu gesendet, aber es sind die Tibber-Preise.
Ebenso werden mir aktuell - obwohl "auf Tibber" - die EnergyCharts-Preise weiterhin angezeigt.

Hast Du einen Tipp, wie ich die den Restart überlebende Persistenz der ET-Objekte wegbekomme?

@gvzdus
Copy link
Contributor

gvzdus commented Oct 31, 2025

Okay, ich hatte
find . -type d -name "__pycache__" -exec rm -rf {} +
vergessen. So kann ich wenigstens per Restart umstellen.

@tpd-opitz
Copy link
Contributor Author

tpd-opitz commented Oct 31, 2025

Ich weiß, dass ich Dich im Zweifelsfalls zum "Python war ein Irrtum" treibe,

Die Sprache hat halt so ihrer Herausforderungen... ;o)

aber: Testcase: Wechsele auf EnergyCharts. Hänge Dich auf das 'openWB/optional/et/get/prices'-Topic. Die Preise werden neu gesendet, aber es sind die Tibber-Preise. Ebenso werden mir aktuell - obwohl "auf Tibber" - die EnergyCharts-Preise weiterhin angezeigt.

Hast Du einen Tipp, wie ich die den Restart überlebende Persistenz der ET-Objekte wegbekomme?

Ich hab noch mal nachgebessert, bei Tarifwechsel wird jetzt sofort neu beim Anbieter abgerufen.

@tpd-opitz
Copy link
Contributor Author

bei Tarifwechsel wird jetzt sofort neu beim Anbieter abgerufen.

Wenn ich es mir genau überlege findet der Anbieter-Wechsel abrechnungstechnisch ja immer Mitternacht statt. Daher wäre in diesem Spezialfall der Abruf Mitternacht ggf. sinnvoller. Womöglich wäre dass aber auch eine Aufgabe für den Konfigurationscode, dass man da angeben kann, ab wann die Änderung gültig sein soll...
Auf jeden Fall würde ich das für einen weiteren PR aufheben.

@gvzdus
Copy link
Contributor

gvzdus commented Nov 1, 2025

Die 877dddf von gestern lief jetzt rundum sauber durch.
Keine Nuller, kein Dauerpollen von Tibber, sauberes Update.
Ich aktualisiere jetzt nochmal - aber es sieht für mich gut aus.
Den "Wechsel" zur Uhrzeit 0:00 finde ich sehr überflüssig, m.E. erwarten fast alle Nutzer, dass man etwas einstellt und es dann - ohne Reboot - sofort live ist. Die Leute würden doch auch "verrückt", käme jemand auf die Idee, eine Änderung des Lademodus auf einen Zeitpunkt X zu legen.

@tpd-opitz
Copy link
Contributor Author

Den "Wechsel" zur Uhrzeit 0:00 finde ich sehr überflüssig, m.E. erwarten fast alle Nutzer, dass man etwas einstellt und es dann - ohne Reboot - sofort live ist. Die Leute würden doch auch "verrückt", käme jemand auf die Idee, eine Änderung des Lademodus auf einen Zeitpunkt X zu legen.

Dabei ging es mir nicht um den Lademodus, sondern wann der neue Traif, also die Preisliste des neuen Tarifs aktiv wird.
Wenn jemand zum Beispiel vom Tibber zu octopus wechselt bekommt er ja ganz andere Preis vorhersagen.

Copy link
Contributor

@LKuemmel LKuemmel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ein paar kleiner Anmerkungen und eine Lösung für die globalen Variablen, die in einem der letzten Commits reingekommen sind.

Comment on lines +17 to +22
'''
next_query_time and internal_tariff_state are defined outside of class ConfigurableElectricityTariff because
for an unknown reason defining them as a class variable does not keep their values.
'''
next_query_time: datetime = datetime.fromtimestamp(1)
internal_tariff_state: TariffState = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Die globalen Variablen sind ein architektonischer Fallstrick und können da nicht bleiben.
Vorschlag: Die Verantwortlichkeit wird aufgeteilt: ConfigurableElectricityTariff ist nur für das Abfragen und Publishen der Preisliste zuständig, et_get_prices prüft, ob optional eine neue Preisliste braucht und ruft dann die update-Methode auf. et_get_prices hat ohnehin die Information, bis wann die letzte Preisliste geht.
Wenn im Fall einer Warnung die Preisliste nicht aktualisiert werden soll, wird die self.store.update() einfach nicht aufgerufen und die letzte, abgerufene Preisliste bleibt im Broker stehen.
Dann sind die beiden globalen Variablen obsolet und die Entscheidung, wird dort getroffen, wo die Preisliste vorhanden ist und die Daten nicht über den Code verstreut.

Copy link
Contributor Author

@tpd-opitz tpd-opitz Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dann sind die beiden globalen Variablen obsolet und die Entscheidung, wird dort getroffen, wo die Preisliste vorhanden ist und die Daten nicht über den Code verstreut.

Das sehe ich genau anders herum, denn "wo die Preisliste vorhanden ist" ist ja in den einzelnen ET-Provider-Modulen also muss da jedes ET-Provider Modul selbst das Caching implementieren, und dass bedeutet für mich "über den Code verstreut". Ja, ich bin auch kein Freund von globalen Variablen, aber weil der in anderen Programmiersprachen übliche Weg über Objektvariablen in Python offenbar nicht funktioniert ist das für mich das kleiner Übel.

Wenn das OWB Team aber darauf besteht werde ich das noch ändern, damit dieser PR aber bald möglichst life gehen kann würde ich das in einem neuen PR machen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reden wir von der gleichen Stelle? Ich würde hier prüfen, ob die Preisliste aktualisiert werden muss: https://github.com/tpd-opitz/openwb-core/blob/d9e3006a947d2cf9a64255b5738bb3b5054ba7bb/packages/control/optional.py#L204
Das ist unabhängig vom Provider-Modul und in self.data.et.get.prices ist die letzte Preisliste vorhanden.

Copy link
Contributor Author

@tpd-opitz tpd-opitz Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reden wir von der gleichen Stelle? Ich würde hier prüfen, ob die Preisliste aktualisiert werden muss: https://github.com/tpd-opitz/openwb-core/blob/d9e3006a947d2cf9a64255b5738bb3b5054ba7bb/packages/control/optional.py#L204 Das ist unabhängig vom Provider-Modul und in self.data.et.get.prices ist die letzte Preisliste vorhanden.

Diese Stelle wird "nur" der Thread zum Update gestartet, wenn der schon existiert passiert nichts. Zudem hätte ich hier das selbe Problem mit den nicht persistenten Objektvariablen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich habe hier mal einen Entwurf gemacht: #2897

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants