Tagebuch: manuelle Positionierung reichert POIs + Wetter an (v1305)

Bisher holte nur der Create-/Foto-EXIF-Pfad Wetter+POIs. Wer einem Eintrag
nachträglich (Edit) einen Standort gab, bekam nichts. Jetzt: GPS neu/geändert
im Update-Handler -> POIs immer + Wetter fürs Eintragsdatum.

- weather.get_weather_for_date(): heute -> aktuelles Wetter; Vergangenheit ->
  stündliche Historie (Open-Meteo Forecast <=90 Tage, sonst Archive-API),
  Stunde aus created_at. Gleiche Dict-Struktur wie get_weather_for_location.
- diary.update_diary(): erfasst alten GPS-Stand, reichert nach DB-Commit an
  (async), schreibt nur erfolgreich geholte Felder (kein Datenverlust bei
  API-Fehler), identische Koordinaten -> kein erneuter Abruf.
- Tests: tests/test_diary_location_enrich.py (Anreicherung, kein GPS=kein
  Abruf, Resave ohne Re-Fetch).
This commit is contained in:
rene 2026-06-18 21:13:47 +02:00
parent e2219fb8ba
commit 140140f690
8 changed files with 244 additions and 17 deletions

View file

@ -164,6 +164,88 @@ async def get_weather_for_location(lat: float, lon: float) -> dict:
_location_cache[key] = (now, data)
return data
async def get_weather_for_date(lat: float, lon: float, datum: str,
hour: int | None = None) -> dict:
"""Wetter für ein bestimmtes Datum (YYYY-MM-DD) an einem Standort.
Für nachträgliches Positionieren alter Tagebucheinträge: heutiges Datum
aktuelles Wetter; vergangene Tage stündliche Historie. Open-Meteo
Forecast-API deckt die jüngsten ~90 Tage Vergangenheit ab, ältere Tage
holt die Archive-API. `hour` (023, Default 12) wählt die Tagesstunde.
Rückgabe hat dieselbe Struktur wie get_weather_for_location (die nach
vorn gerichteten Felder next_rain_time/rain_warning_time sind None)."""
from datetime import date as _date
try:
target = _date.fromisoformat(datum[:10])
except (ValueError, TypeError):
return await get_weather_for_location(lat, lon)
today = datetime.now().date()
if target >= today:
# Heute/Zukunft → aktuelles Wetter (inkl. Vorhersage-Felder + Ortsname).
return await get_weather_for_location(lat, lon)
hour = 12 if hour is None else max(0, min(23, hour))
age_days = (today - target).days
base = ("https://archive-api.open-meteo.com/v1/archive"
if age_days > 90 else
"https://api.open-meteo.com/v1/forecast")
url = (
f"{base}?latitude={lat}&longitude={lon}"
f"&start_date={target.isoformat()}&end_date={target.isoformat()}"
"&hourly=temperature_2m,apparent_temperature,weathercode,windspeed_10m,is_day"
"&daily=precipitation_probability_max,uv_index_max"
"&timezone=Europe%2FBerlin"
)
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(url)
resp.raise_for_status()
raw = resp.json()
hourly = raw.get('hourly', {})
daily = raw.get('daily', {})
def _at(arr):
try:
return arr[hour]
except (IndexError, TypeError, KeyError):
return None
temp = _at(hourly.get('temperature_2m', []))
feels_like = _at(hourly.get('apparent_temperature', []))
wcode = _at(hourly.get('weathercode', [])) or 0
wind = _at(hourly.get('windspeed_10m', []))
is_day_v = _at(hourly.get('is_day', []))
is_day = 1 if is_day_v is None else is_day_v
precip = (daily.get('precipitation_probability_max') or [None])[0]
uv = (daily.get('uv_index_max') or [None])[0]
desc, icon = _WMO.get(wcode, ('Unbekannt', 'cloud'))
if wcode == 0 and not is_day:
icon = 'moon'
zecken = None
if temp is not None and temp > 7.0 and 3 <= target.month <= 10:
zecken = 'hoch' if temp > 20 else ('mittel' if temp > 12 else 'niedrig')
return {
'temp_c': temp,
'feels_like_c': feels_like,
'weathercode': wcode,
'desc': desc,
'icon': icon,
'wind_kmh': wind,
'precip_prob': precip,
'uv_index': uv,
'is_day': bool(is_day),
'zecken_warnung': zecken,
'next_rain_time': None,
'rain_warning_time': None,
'location_name': None,
'historical': True,
}
# WMO-Wettercodes 9599 = Gewitter
THUNDERSTORM_CODES = {95, 96, 99}