Wettervorhersage sammeln und archivieren (Open-Meteo)
gatherForecastData.py holt alle zwanzig Minuten Stunden- und Tageswerte und
schreibt sie nach solarLog (weatherHours, weatherDays, weatherForecastLog,
weatherTilted). Grundlage des neuen Meteogramms im Web-Repository.
Kein Anbau an gatherRainData.py, obwohl es dieselbe Schnittstelle benutzt:
dessen Fehlerregel ist sicherheitsrelevant - es leert bei einer Stoerung
bewusst seine Werte, damit der Runner nicht auf eine alte "0,0 mm" hin
giesst. Hier gilt das Gegenteil, eine Vorhersage von vor zwei Stunden ist
brauchbar und wird nur als alt beschriftet. Zwei gegenlaeufige Fehlerregeln
in einer Schleife waeren eine Falle.
Es wird nichts geloescht. Die Tabellen sind zugleich ein Archiv: aus der
Einstrahlung und EnergyFlow_hourly.pv_kwh soll spaeter eine eigene
Ertragsprognose gerechnet werden. 8.760 Zeilen im Jahr kosten 1,5 MB.
wetterarchiv_nachtragen.py fuellt das Archiv rueckwirkend aus der
ERA5-Schnittstelle von Open-Meteo - 39.264 Stunden ab dem 02.04.2022, dem
Beginn von EnergyFlow_hourly. Damit ist die Korrelation sofort rechenbar
statt erst in einem Jahr.
Sonnenzeiten schreibt es dabei ausdruecklich nicht: die Archiv-Schnittstelle
rechnet alle Zeiten mit dem heute gueltigen Zeitzonenversatz um und meldet
das auch selbst ("utc_offset_seconds: 7200" fuer einen Dezembertag).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Das Wetterarchiv rueckwirkend fuellen - einmalig, kein Dauerlaeufer.
|
||||
|
||||
python3 wetterarchiv_nachtragen.py [--von 2022-04-02] [--bis 2026-09-20]
|
||||
|
||||
gatherForecastData.py sammelt ab heute. Warten muss man deswegen nicht:
|
||||
Open-Meteo hat neben der Vorhersage eine Archiv-Schnittstelle
|
||||
(archive-api.open-meteo.com, aus den ERA5-Reanalysen), die dieselben
|
||||
Stundenwerte rueckwirkend liefert - Temperatur, Wind, Niederschlag,
|
||||
Bewoelkung und vor allem die Einstrahlung.
|
||||
|
||||
Wozu: EnergyFlow_hourly.pv_kwh haelt den gemessenen Ertrag je Stunde seit dem
|
||||
02.04.2022. Das ist die eine Haelfte eines Datenpaares; die andere ist die
|
||||
Einstrahlung derselben Stunde. Ist sie nachgetragen, laesst sich die
|
||||
Korrelation sofort rechnen statt erst in einem Jahr - ueber alle
|
||||
Jahreszeiten, mit Schnee, Dunst und Hochsommer.
|
||||
|
||||
Geschrieben wird in dieselbe Tabelle wie im laufenden Betrieb, mit
|
||||
ist_vorhersage = 0: eine Analyse ist keine Vorhersage. Bereits vorhandene
|
||||
Stunden werden ueberschrieben - die Analyse ist genauer als die Vorhersage,
|
||||
die dort vielleicht noch steht. weatherForecastLog bleibt unberuehrt: dort
|
||||
gehoert hinein, was *vorher* gesagt wurde, und das laesst sich nicht
|
||||
nachtraegen.
|
||||
|
||||
Die Archiv-Schnittstelle hinkt dem heutigen Tag rund fuenf Tage hinterher.
|
||||
Ohne --bis endet der Lauf deshalb eine Woche vor heute; was danach kommt,
|
||||
hat der Sammler ohnehin schon.
|
||||
|
||||
Abgerufen wird jahresweise. Ein einzelner Aufruf ueber vier Jahre waere
|
||||
mehrere Megabyte in einer Antwort, und faellt er in der Mitte aus, faengt
|
||||
alles von vorn an.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import time
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
import mysql.connector as mc
|
||||
import requests
|
||||
|
||||
import konfig
|
||||
import gatherForecastData as vorhersage
|
||||
|
||||
ARCHIV_URL = "https://archive-api.open-meteo.com/v1/archive"
|
||||
|
||||
# Dieselben Felder wie im laufenden Betrieb, abzueglich derer, die das Archiv
|
||||
# nicht kennt: precipitation_probability ist eine Eigenschaft der Vorhersage -
|
||||
# im Nachhinein hat es geregnet oder nicht.
|
||||
ARCHIVFELDER = [f for f in vorhersage.STUNDENFELDER
|
||||
if f != "precipitation_probability"]
|
||||
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s")
|
||||
logging.getLogger("mysql.connector").setLevel(logging.WARNING)
|
||||
logger = logging.getLogger("wetterarchiv")
|
||||
|
||||
|
||||
def archiv_holen(von, bis, felder=None, zusatz=None):
|
||||
p = {
|
||||
"latitude": konfig.wert("vorhersage", "lat"),
|
||||
"longitude": konfig.wert("vorhersage", "lon"),
|
||||
"timezone": konfig.wert("vorhersage", "timezone", "Europe/Berlin"),
|
||||
"start_date": von.isoformat(),
|
||||
"end_date": bis.isoformat(),
|
||||
"hourly": ",".join(felder if felder else ARCHIVFELDER),
|
||||
}
|
||||
if felder is None:
|
||||
p["daily"] = ",".join(vorhersage.TAGESFELDER)
|
||||
if zusatz:
|
||||
p.update(zusatz)
|
||||
antwort = requests.get(ARCHIV_URL, params=p, timeout=120)
|
||||
antwort.raise_for_status()
|
||||
return antwort.json()
|
||||
|
||||
|
||||
def zeilen(daten, gelaufen):
|
||||
"""
|
||||
Wie vorhersage.stundenzeilen(), nur ohne Regenwahrscheinlichkeit und
|
||||
immer als Analyse.
|
||||
|
||||
Absichtlich eine eigene Funktion und kein Schalter in der anderen: der
|
||||
Sammler soll nicht bei jedem Takt eine Bedingung auswerten, die nur
|
||||
dieses Skript braucht.
|
||||
"""
|
||||
h = daten["hourly"]
|
||||
aus = []
|
||||
for i, t in enumerate(h["time"]):
|
||||
w = lambda name: h[name][i] if name in h else None
|
||||
druck = w("pressure_msl")
|
||||
aus.append((
|
||||
vorhersage._zeit(t),
|
||||
w("temperature_2m"), w("apparent_temperature"),
|
||||
w("relativehumidity_2m"), w("dewpoint_2m"),
|
||||
None if druck is None else round(druck),
|
||||
w("precipitation"), None, w("snowfall"),
|
||||
w("weathercode"),
|
||||
w("cloudcover_low"), w("cloudcover_mid"), w("cloudcover_high"),
|
||||
w("windspeed_10m"), w("windgusts_10m"), w("winddirection_10m"),
|
||||
w("shortwave_radiation"), w("direct_radiation"), w("diffuse_radiation"),
|
||||
1 if w("is_day") else 0,
|
||||
0, # ist_vorhersage: eine Analyse ist keine
|
||||
gelaufen,
|
||||
))
|
||||
return aus
|
||||
|
||||
|
||||
def schreiben(zug, stunden, tage, geneigt):
|
||||
"""Wie im Sammler, aber ohne weatherForecastLog."""
|
||||
with mc.connect(**zug) as verbindung:
|
||||
with verbindung.cursor() as cursor:
|
||||
cursor.executemany(
|
||||
"REPLACE INTO weatherHours (datetime, temp, gefuehlt, feuchte, "
|
||||
"taupunkt, druck, regen, regen_wkt, schnee, wettercode, "
|
||||
"wolken_tief, wolken_mittel, wolken_hoch, wind, boe, richtung, "
|
||||
"strahlung, direkt, diffus, tag, ist_vorhersage, abgerufen) "
|
||||
"VALUES (" + ",".join(["%s"] * 22) + ");", stunden)
|
||||
if tage:
|
||||
cursor.executemany(
|
||||
"REPLACE INTO weatherDays (date, wettercode, temp_min, "
|
||||
"temp_max, sonnenauf, sonnenunter, regen_summe, regen_wkt, "
|
||||
"wind_max, boe_max, strahlung_summe, ist_vorhersage, abgerufen) "
|
||||
"VALUES (" + ",".join(["%s"] * 13) + ");", tage)
|
||||
if geneigt:
|
||||
cursor.executemany(
|
||||
"REPLACE INTO weatherTilted (datetime, flaeche, strahlung) "
|
||||
"VALUES (%s,%s,%s);", geneigt)
|
||||
verbindung.commit()
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--von", default="2022-04-02",
|
||||
help="erster Tag (Vorgabe: Beginn von EnergyFlow_hourly)")
|
||||
p.add_argument("--bis", default=None,
|
||||
help="letzter Tag (Vorgabe: heute minus sieben Tage)")
|
||||
a = p.parse_args()
|
||||
|
||||
von = date.fromisoformat(a.von)
|
||||
bis = date.fromisoformat(a.bis) if a.bis else date.today() - timedelta(days=7)
|
||||
if von > bis:
|
||||
raise SystemExit("--von liegt hinter --bis")
|
||||
|
||||
flaechen = vorhersage.flaechen_lesen()
|
||||
gelaufen = datetime.now().replace(microsecond=0)
|
||||
zug = konfig.datenbank()
|
||||
gesamt = 0
|
||||
|
||||
jahr_von = von
|
||||
while jahr_von <= bis:
|
||||
jahr_bis = min(date(jahr_von.year, 12, 31), bis)
|
||||
logger.info("hole %s bis %s", jahr_von, jahr_bis)
|
||||
daten = archiv_holen(jahr_von, jahr_bis)
|
||||
stunden = zeilen(daten, gelaufen)
|
||||
|
||||
tage = vorhersage.tageszeilen(daten, gelaufen) if "daily" in daten else []
|
||||
# Zwei Korrekturen gegenueber dem laufenden Betrieb:
|
||||
#
|
||||
# ist_vorhersage ist hier immer 0 - tageszeilen() entscheidet das
|
||||
# anhand von "heute", und im Archiv ist jeder Tag Vergangenheit.
|
||||
#
|
||||
# Sonnenauf- und -untergang werden verworfen. Die Archiv-Schnittstelle
|
||||
# rechnet alle Zeiten mit dem *heute* gueltigen Zeitzonenversatz um -
|
||||
# die Antwort sagt selbst "utc_offset_seconds: 7200", auch fuer einen
|
||||
# Dezembertag. Sonnenaufgang am 21.12.2025 kaeme so als 09:04 statt
|
||||
# 08:04. Bei den Stundenwerten faellt das nicht ins Gewicht (sie sind
|
||||
# ohnehin an ihrem Zeitstempel aufgehaengt und werden nur als Reihe
|
||||
# gelesen), bei einer Uhrzeit schon. Gebraucht werden die Sonnenzeiten
|
||||
# nur fuer die sichtbaren Tage, und die schreibt der Sammler richtig.
|
||||
tage = [t[:4] + (None, None) + t[6:-2] + (0, gelaufen) for t in tage]
|
||||
|
||||
geneigt = []
|
||||
for name, neigung, azimut in flaechen:
|
||||
g = archiv_holen(jahr_von, jahr_bis,
|
||||
felder=["global_tilted_irradiance"],
|
||||
zusatz={"tilt": neigung, "azimuth": azimut})
|
||||
h = g["hourly"]
|
||||
geneigt += [(vorhersage._zeit(t), name,
|
||||
h["global_tilted_irradiance"][i])
|
||||
for i, t in enumerate(h["time"])]
|
||||
|
||||
schreiben(zug, stunden, tage, geneigt)
|
||||
gesamt += len(stunden)
|
||||
logger.info(" %d Stunden, %d Tage, %d Flaechenwerte geschrieben",
|
||||
len(stunden), len(tage), len(geneigt))
|
||||
jahr_von = date(jahr_von.year + 1, 1, 1)
|
||||
time.sleep(2) # nicht in einem Zug durch die fremde Schnittstelle
|
||||
|
||||
logger.info("fertig: %d Stunden nachgetragen", gesamt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user