banyaro/backend/weather.py
rene 0461f936ce Wetter-Chip auf Karte + Bugfix private Routen zählen für km-Stats
- GET /api/weather?lat=&lon= (Open-Meteo, 30-min TTL-Cache)
- Zecken-Warnung regelbasiert: März–Okt + Temp > 7°C
- Karte: Wetterchip oben rechts nach GPS-Fix
- stats.py + achievements.py: is_public-Filter entfernt —
  private Routen zählen jetzt für eigene km/Achievements
- SW by-v320, APP_VER 308
2026-04-24 07:59:15 +02:00

163 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
BAN YARO — Wetter via Open-Meteo
- get_weather_summary(): Push-Job, prüft 5 deutsche Städte
- get_weather_for_location(): API-Endpoint, beliebiger Standort mit TTL-Cache
"""
import time
import logging
import httpx
from datetime import datetime
logger = logging.getLogger(__name__)
# WMO-Wettercodes → (Beschreibung, Phosphor-Icon-Name)
_WMO = {
0: ('Klar', 'sun'),
1: ('Überwiegend klar', 'cloud-sun'),
2: ('Teilweise bewölkt', 'cloud-sun'),
3: ('Bedeckt', 'cloud'),
45: ('Nebel', 'cloud-fog'),
48: ('Gefrierender Nebel', 'cloud-fog'),
51: ('Leichter Nieselregen', 'cloud-rain'),
53: ('Nieselregen', 'cloud-rain'),
55: ('Starker Nieselregen', 'cloud-rain'),
61: ('Leichter Regen', 'cloud-rain'),
63: ('Regen', 'cloud-rain'),
65: ('Starker Regen', 'cloud-rain'),
71: ('Leichter Schnee', 'snowflake'),
73: ('Schnee', 'snowflake'),
75: ('Starker Schnee', 'snowflake'),
77: ('Schneekörner', 'snowflake'),
80: ('Leichte Schauer', 'cloud-rain'),
81: ('Schauer', 'cloud-rain'),
82: ('Starke Schauer', 'cloud-rain'),
85: ('Schneeschauer', 'snowflake'),
86: ('Starke Schneeschauer', 'snowflake'),
95: ('Gewitter', 'cloud-lightning'),
96: ('Gewitter mit Hagel', 'cloud-lightning'),
99: ('Schweres Gewitter', 'cloud-lightning'),
}
# TTL-Cache: (round(lat,1), round(lon,1)) → (timestamp, data)
_location_cache: dict = {}
_CACHE_TTL = 1800 # 30 Minuten
async def get_weather_for_location(lat: float, lon: float) -> dict:
"""Holt aktuelles Wetter für einen Standort. 30-min TTL-Cache."""
key = (round(lat, 1), round(lon, 1))
now = time.time()
if key in _location_cache:
ts, cached = _location_cache[key]
if now - ts < _CACHE_TTL:
return cached
url = (
"https://api.open-meteo.com/v1/forecast"
f"?latitude={lat}&longitude={lon}"
"&current=temperature_2m,apparent_temperature,weathercode,windspeed_10m,is_day"
"&daily=precipitation_probability_max,uv_index_max"
"&timezone=Europe%2FBerlin&forecast_days=1"
)
async with httpx.AsyncClient(timeout=8.0) as client:
resp = await client.get(url)
resp.raise_for_status()
raw = resp.json()
cur = raw.get('current', {})
daily = raw.get('daily', {})
temp = cur.get('temperature_2m')
feels_like = cur.get('apparent_temperature')
wcode = cur.get('weathercode', 0)
wind = cur.get('windspeed_10m')
is_day = cur.get('is_day', 1)
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'
month = datetime.now().month
zecken = None
if temp is not None and temp > 7.0 and 3 <= month <= 10:
zecken = 'hoch' if temp > 20 else ('mittel' if temp > 12 else 'niedrig')
data = {
'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,
}
_location_cache[key] = (now, data)
return data
# Wichtige deutsche Städte als Stichprobe
CITIES = [
{"name": "Berlin", "lat": 52.52, "lon": 13.41},
{"name": "München", "lat": 48.14, "lon": 11.58},
{"name": "Hamburg", "lat": 53.55, "lon": 10.00},
{"name": "Frankfurt", "lat": 50.11, "lon": 8.68},
{"name": "Köln", "lat": 50.94, "lon": 6.96},
]
# WMO-Wettercodes 9599 = Gewitter
THUNDERSTORM_CODES = {95, 96, 99}
async def get_weather_summary() -> dict:
"""
Holt Tagesprognose für mehrere deutsche Städte von Open-Meteo.
Gibt zurück: {"max_temp_c": float, "thunderstorm": bool}
"""
max_temp = -999.0
thunderstorm = False
async with httpx.AsyncClient(timeout=10.0) as client:
for city in CITIES:
url = (
"https://api.open-meteo.com/v1/forecast"
f"?latitude={city['lat']}&longitude={city['lon']}"
"&daily=temperature_2m_max,precipitation_probability_max,weathercode"
"&timezone=Europe%2FBerlin&forecast_days=1"
)
try:
resp = await client.get(url)
resp.raise_for_status()
data = resp.json()
daily = data.get("daily", {})
temps = daily.get("temperature_2m_max", [None])
precip_probs = daily.get("precipitation_probability_max", [None])
weathercodes = daily.get("weathercode", [None])
temp = temps[0] if temps else None
precip_prob = precip_probs[0] if precip_probs else None
weathercode = weathercodes[0] if weathercodes else None
if temp is not None and temp > max_temp:
max_temp = temp
if (
precip_prob is not None and precip_prob > 50
and weathercode is not None and int(weathercode) in THUNDERSTORM_CODES
):
thunderstorm = True
logger.info(f"Gewitter erkannt: {city['name']} (Code {weathercode}, {precip_prob}% Niederschlag)")
except Exception as e:
logger.warning(f"Wetter-Abruf für {city['name']} fehlgeschlagen: {e}")
if max_temp == -999.0:
max_temp = 0.0
logger.info(f"Wetter-Zusammenfassung: max_temp={max_temp}°C, thunderstorm={thunderstorm}")
return {"max_temp_c": max_temp, "thunderstorm": thunderstorm}