# -*- coding: utf-8 -*- """ Abstrakte Basisklasse fuer Wetter-Provider. Stellt die gemeinsame Geocoding-Funktionalitaet bereit (Open-Meteo Geocoding API, kostenlos, kein API-Key noetig). Nutzt den OnlineService fuer HTTP-Requests mit Offline-Cache. """ from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING from plugins.weather.models import WeatherResult from trixy_core.utils.debug import pdebug, perror if TYPE_CHECKING: from trixy_core.online.client import HttpClient class WeatherProvider(ABC): """ Abstrakte Basisklasse fuer alle Wetter-Provider. Jeder Provider muss get_weather() und health_check() implementieren. Geocoding ueber die Open-Meteo-API steht allen Providern zur Verfuegung. """ def __init__(self, name: str, config: dict, http: "HttpClient | None" = None) -> None: self.name = name self.config = config self._http = http @abstractmethod async def get_weather( self, lat: float, lon: float, location_name: str = "" ) -> WeatherResult | None: """ Ruft aktuelle Wetterdaten und Vorhersage ab. Args: lat: Breitengrad lon: Laengengrad location_name: Optionaler Stadtname fuer die Anzeige Returns: WeatherResult oder None bei Fehler """ ... @abstractmethod async def health_check(self) -> bool: """ Prueft ob der Provider erreichbar ist. Returns: True wenn der Provider verfuegbar ist """ ... async def geocode(self, city: str) -> tuple[float, float, str] | None: """ Loest einen Stadtnamen in Koordinaten auf. Nutzt die kostenlose Open-Meteo Geocoding API. Args: city: Stadtname (z.B. "Berlin", "Muenchen") Returns: (lat, lon, aufgeloester_name) oder None bei Fehler """ # Gaengige Umlaut-Umschreibungen zurueckwandeln # (STT gibt oft "Muenchen" statt "München") _umlaut_map = { "ue": "ü", "Ue": "Ü", "oe": "ö", "Oe": "Ö", "ae": "ä", "Ae": "Ä", } resolved_city = city for ascii_form, umlaut in _umlaut_map.items(): resolved_city = resolved_city.replace(ascii_form, umlaut) url = "https://geocoding-api.open-meteo.com/v1/search" params = { "name": resolved_city, "count": "5", "language": "de", "format": "json", } try: # OnlineService HTTP-Client nutzen (mit Cache) if self._http: resp = await self._http.get(url, params=params, cache_ttl=86400) if not resp.ok: pdebug(f"WeatherProvider: Geocoding HTTP-Fehler: {resp.error or resp.status_code}") return None data = resp.json() else: # Fallback: httpx direkt import httpx async with httpx.AsyncClient(timeout=10.0) as client: response = await client.get(url, params=params) response.raise_for_status() data = response.json() results = data.get("results") if not results: pdebug(f"WeatherProvider: Keine Ergebnisse fuer '{city}'") return None # Bestes Ergebnis waehlen: exakter Name-Match bevorzugen result = results[0] for r in results: r_name = r.get("name", "").lower() if r_name == resolved_city.lower() or r_name == city.lower(): result = r break lat = result["latitude"] lon = result["longitude"] name = result.get("name", city) pdebug(f"WeatherProvider: Geocoding '{city}' -> {name} ({lat}, {lon})") return (lat, lon, name) except Exception as e: perror(f"WeatherProvider: Geocoding-Fehler fuer '{city}': {e}") return None