base.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. # -*- coding: utf-8 -*-
  2. """
  3. Abstrakte Basisklasse fuer Wetter-Provider.
  4. Stellt die gemeinsame Geocoding-Funktionalitaet bereit
  5. (Open-Meteo Geocoding API, kostenlos, kein API-Key noetig).
  6. Nutzt den OnlineService fuer HTTP-Requests mit Offline-Cache.
  7. """
  8. from __future__ import annotations
  9. from abc import ABC, abstractmethod
  10. from typing import TYPE_CHECKING
  11. from plugins.weather.models import WeatherResult
  12. from trixy_core.utils.debug import pdebug, perror
  13. if TYPE_CHECKING:
  14. from trixy_core.online.client import HttpClient
  15. class WeatherProvider(ABC):
  16. """
  17. Abstrakte Basisklasse fuer alle Wetter-Provider.
  18. Jeder Provider muss get_weather() und health_check() implementieren.
  19. Geocoding ueber die Open-Meteo-API steht allen Providern zur Verfuegung.
  20. """
  21. def __init__(self, name: str, config: dict, http: "HttpClient | None" = None) -> None:
  22. self.name = name
  23. self.config = config
  24. self._http = http
  25. @abstractmethod
  26. async def get_weather(
  27. self, lat: float, lon: float, location_name: str = ""
  28. ) -> WeatherResult | None:
  29. """
  30. Ruft aktuelle Wetterdaten und Vorhersage ab.
  31. Args:
  32. lat: Breitengrad
  33. lon: Laengengrad
  34. location_name: Optionaler Stadtname fuer die Anzeige
  35. Returns:
  36. WeatherResult oder None bei Fehler
  37. """
  38. ...
  39. @abstractmethod
  40. async def health_check(self) -> bool:
  41. """
  42. Prueft ob der Provider erreichbar ist.
  43. Returns:
  44. True wenn der Provider verfuegbar ist
  45. """
  46. ...
  47. async def geocode(self, city: str) -> tuple[float, float, str] | None:
  48. """
  49. Loest einen Stadtnamen in Koordinaten auf.
  50. Nutzt die kostenlose Open-Meteo Geocoding API.
  51. Args:
  52. city: Stadtname (z.B. "Berlin", "Muenchen")
  53. Returns:
  54. (lat, lon, aufgeloester_name) oder None bei Fehler
  55. """
  56. # Gaengige Umlaut-Umschreibungen zurueckwandeln
  57. # (STT gibt oft "Muenchen" statt "München")
  58. _umlaut_map = {
  59. "ue": "ü", "Ue": "Ü",
  60. "oe": "ö", "Oe": "Ö",
  61. "ae": "ä", "Ae": "Ä",
  62. }
  63. resolved_city = city
  64. for ascii_form, umlaut in _umlaut_map.items():
  65. resolved_city = resolved_city.replace(ascii_form, umlaut)
  66. url = "https://geocoding-api.open-meteo.com/v1/search"
  67. params = {
  68. "name": resolved_city,
  69. "count": "5",
  70. "language": "de",
  71. "format": "json",
  72. }
  73. try:
  74. # OnlineService HTTP-Client nutzen (mit Cache)
  75. if self._http:
  76. resp = await self._http.get(url, params=params, cache_ttl=86400)
  77. if not resp.ok:
  78. pdebug(f"WeatherProvider: Geocoding HTTP-Fehler: {resp.error or resp.status_code}")
  79. return None
  80. data = resp.json()
  81. else:
  82. # Fallback: httpx direkt
  83. import httpx
  84. async with httpx.AsyncClient(timeout=10.0) as client:
  85. response = await client.get(url, params=params)
  86. response.raise_for_status()
  87. data = response.json()
  88. results = data.get("results")
  89. if not results:
  90. pdebug(f"WeatherProvider: Keine Ergebnisse fuer '{city}'")
  91. return None
  92. # Bestes Ergebnis waehlen: exakter Name-Match bevorzugen
  93. result = results[0]
  94. for r in results:
  95. r_name = r.get("name", "").lower()
  96. if r_name == resolved_city.lower() or r_name == city.lower():
  97. result = r
  98. break
  99. lat = result["latitude"]
  100. lon = result["longitude"]
  101. name = result.get("name", city)
  102. pdebug(f"WeatherProvider: Geocoding '{city}' -> {name} ({lat}, {lon})")
  103. return (lat, lon, name)
  104. except Exception as e:
  105. perror(f"WeatherProvider: Geocoding-Fehler fuer '{city}': {e}")
  106. return None