# -*- coding: utf-8 -*- """ Open-Meteo Wetter-Provider. Nutzt das DWD-ICON-Modell. Kein API-Key erforderlich. https://open-meteo.com/ Nutzt den OnlineService HTTP-Client mit 48h Offline-Cache. """ from __future__ import annotations from datetime import date, datetime from typing import TYPE_CHECKING from plugins.weather.models import ForecastDay, WeatherData, WeatherResult from plugins.weather.providers.base import WeatherProvider from plugins.weather.utils import get_wmo_description from trixy_core.utils.debug import pdebug, perror if TYPE_CHECKING: from trixy_core.online.client import HttpClient # 48 Stunden Cache fuer Offline-Betrieb _CACHE_TTL = 48 * 3600 class OpenMeteoProvider(WeatherProvider): """Wetter-Provider ueber die Open-Meteo API (DWD-ICON Modell).""" BASE_URL = "https://api.open-meteo.com/v1/dwd-icon" def __init__(self, config: dict, http: "HttpClient | None" = None) -> None: super().__init__(name="open_meteo", config=config, http=http) async def get_weather( self, lat: float, lon: float, location_name: str = "" ) -> WeatherResult | None: """Ruft Wetterdaten von Open-Meteo ab (mit 48h Offline-Cache).""" params = { "latitude": str(lat), "longitude": str(lon), "current": ",".join([ "temperature_2m", "relative_humidity_2m", "weather_code", "wind_speed_10m", "wind_direction_10m", "precipitation", "apparent_temperature", "surface_pressure", "cloud_cover", ]), "daily": ",".join([ "temperature_2m_max", "temperature_2m_min", "precipitation_sum", "weather_code", "wind_speed_10m_max", ]), "timezone": "Europe/Berlin", "forecast_days": "7", } try: # OnlineService HTTP-Client nutzen (mit 48h Cache) if self._http: resp = await self._http.get( self.BASE_URL, params=params, cache_ttl=_CACHE_TTL, ) if not resp.ok: if resp.from_cache: pdebug(f"OpenMeteo: Verwende Offline-Cache") else: perror(f"OpenMeteo: Fehler: {resp.error or resp.status_code}") return None data = resp.json() if resp.from_cache: pdebug(f"OpenMeteo: Daten aus Offline-Cache") else: # Fallback: httpx direkt (ohne Cache) import httpx async with httpx.AsyncClient(timeout=10.0) as client: response = await client.get(self.BASE_URL, params=params) response.raise_for_status() data = response.json() return self._parse_response(data, location_name) except Exception as e: perror(f"OpenMeteo: Fehler beim Abruf: {e}") return None async def health_check(self) -> bool: """Prueft ob die Open-Meteo API erreichbar ist.""" params = { "latitude": "49.25", "longitude": "8.88", "current": "temperature_2m", } try: if self._http: resp = await self._http.get(self.BASE_URL, params=params, timeout=5.0) return resp.ok else: import httpx async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get(self.BASE_URL, params=params) return response.status_code == 200 except Exception: return False def _parse_response( self, data: dict, location_name: str ) -> WeatherResult | None: """Parst die Open-Meteo API-Antwort.""" try: current = data.get("current", {}) daily = data.get("daily", {}) weather_code = current.get("weather_code", 0) current_weather = WeatherData( location=location_name, temperature=current.get("temperature_2m", 0.0), feels_like=current.get("apparent_temperature"), humidity=int(current.get("relative_humidity_2m", 0)), description=get_wmo_description(weather_code), wind_speed=current.get("wind_speed_10m", 0.0), wind_direction=int(current.get("wind_direction_10m", 0)), precipitation=current.get("precipitation", 0.0), cloud_cover=_safe_int(current.get("cloud_cover")), pressure=current.get("surface_pressure"), provider_name=self.name, timestamp=datetime.now(), raw_data=current, ) # Vorhersage parsen forecast = self._parse_forecast(daily) return WeatherResult( current=current_weather, forecast=forecast, fetched_at=datetime.now(), ) except Exception as e: perror(f"OpenMeteo: Fehler beim Parsen: {e}") return None def _parse_forecast(self, daily: dict) -> list[ForecastDay]: """Parst die taegliche Vorhersage.""" forecast: list[ForecastDay] = [] dates = daily.get("time", []) temp_max = daily.get("temperature_2m_max", []) temp_min = daily.get("temperature_2m_min", []) precip = daily.get("precipitation_sum", []) codes = daily.get("weather_code", []) wind_max = daily.get("wind_speed_10m_max", []) for i in range(len(dates)): try: forecast.append(ForecastDay( date=date.fromisoformat(dates[i]), temp_min=temp_min[i] if i < len(temp_min) else 0.0, temp_max=temp_max[i] if i < len(temp_max) else 0.0, description=get_wmo_description( codes[i] if i < len(codes) else 0 ), precipitation=precip[i] if i < len(precip) else 0.0, wind_speed_max=wind_max[i] if i < len(wind_max) else 0.0, )) except (ValueError, IndexError) as e: pdebug(f"OpenMeteo: Forecast-Tag {i} uebersprungen: {e}") return forecast def _safe_int(value: object) -> int | None: """Konvertiert einen Wert sicher zu int oder None.""" if value is None: return None try: return int(value) except (TypeError, ValueError): return None