| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- # -*- 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
|