openweathermap.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. # -*- coding: utf-8 -*-
  2. """
  3. OpenWeatherMap Wetter-Provider.
  4. Erfordert einen kostenlosen API-Key.
  5. https://openweathermap.org/api
  6. """
  7. from datetime import date, datetime
  8. import httpx
  9. from plugins.weather.models import ForecastDay, WeatherData, WeatherResult
  10. from plugins.weather.providers.base import WeatherProvider
  11. from trixy_core.utils.debug import pdebug, perror
  12. class OpenWeatherMapProvider(WeatherProvider):
  13. """Wetter-Provider ueber die OpenWeatherMap API."""
  14. BASE_URL = "https://api.openweathermap.org/data/2.5"
  15. def __init__(self, config: dict, http=None) -> None:
  16. super().__init__(name="openweathermap", config=config, http=http)
  17. self._api_key = config.get("api_key", "")
  18. async def get_weather(
  19. self, lat: float, lon: float, location_name: str = ""
  20. ) -> WeatherResult | None:
  21. """Ruft Wetterdaten von OpenWeatherMap ab."""
  22. if not self._api_key:
  23. perror("OpenWeatherMap: Kein API-Key konfiguriert")
  24. return None
  25. base_params = {
  26. "lat": lat,
  27. "lon": lon,
  28. "appid": self._api_key,
  29. "lang": "de",
  30. "units": "metric",
  31. }
  32. try:
  33. async with httpx.AsyncClient(timeout=10.0) as client:
  34. # Aktuelles Wetter
  35. current_resp = await client.get(
  36. f"{self.BASE_URL}/weather", params=base_params
  37. )
  38. current_resp.raise_for_status()
  39. current_data = current_resp.json()
  40. # Vorhersage (5 Tage / 3 Stunden)
  41. forecast_resp = await client.get(
  42. f"{self.BASE_URL}/forecast", params=base_params
  43. )
  44. forecast_resp.raise_for_status()
  45. forecast_data = forecast_resp.json()
  46. return self._parse_response(current_data, forecast_data, location_name)
  47. except Exception as e:
  48. perror(f"OpenWeatherMap: Fehler beim Abruf: {e}")
  49. return None
  50. async def health_check(self) -> bool:
  51. """Prueft ob die OpenWeatherMap API erreichbar ist."""
  52. if not self._api_key:
  53. return False
  54. try:
  55. async with httpx.AsyncClient(timeout=5.0) as client:
  56. response = await client.get(
  57. f"{self.BASE_URL}/weather",
  58. params={
  59. "lat": 49.25,
  60. "lon": 8.88,
  61. "appid": self._api_key,
  62. "units": "metric",
  63. },
  64. )
  65. return response.status_code == 200
  66. except Exception:
  67. return False
  68. def _parse_response(
  69. self,
  70. current_data: dict,
  71. forecast_data: dict,
  72. location_name: str,
  73. ) -> WeatherResult | None:
  74. """Parst die OpenWeatherMap API-Antworten."""
  75. try:
  76. main = current_data.get("main", {})
  77. wind = current_data.get("wind", {})
  78. weather = current_data.get("weather", [{}])[0]
  79. clouds = current_data.get("clouds", {})
  80. rain = current_data.get("rain", {})
  81. # Windgeschwindigkeit: m/s -> km/h
  82. wind_speed_ms = wind.get("speed", 0.0)
  83. wind_speed_kmh = wind_speed_ms * 3.6
  84. name = location_name or current_data.get("name", "Unbekannt")
  85. current_weather = WeatherData(
  86. location=name,
  87. temperature=main.get("temp", 0.0),
  88. feels_like=main.get("feels_like"),
  89. humidity=int(main.get("humidity", 0)),
  90. description=weather.get("description", "").capitalize(),
  91. wind_speed=round(wind_speed_kmh, 1),
  92. wind_direction=int(wind.get("deg", 0)),
  93. precipitation=rain.get("1h", 0.0),
  94. cloud_cover=clouds.get("all"),
  95. pressure=main.get("pressure"),
  96. provider_name=self.name,
  97. timestamp=datetime.now(),
  98. raw_data=current_data,
  99. )
  100. # Vorhersage parsen
  101. forecast = self._parse_forecast(forecast_data)
  102. return WeatherResult(
  103. current=current_weather,
  104. forecast=forecast,
  105. fetched_at=datetime.now(),
  106. )
  107. except Exception as e:
  108. perror(f"OpenWeatherMap: Fehler beim Parsen: {e}")
  109. return None
  110. def _parse_forecast(self, forecast_data: dict) -> list[ForecastDay]:
  111. """
  112. Parst die 5-Tage/3-Stunden-Vorhersage zu Tageswerten.
  113. Aggregiert die 3-Stunden-Intervalle zu Min/Max pro Tag.
  114. """
  115. daily: dict[date, dict] = {}
  116. for entry in forecast_data.get("list", []):
  117. dt = datetime.fromtimestamp(entry.get("dt", 0))
  118. day = dt.date()
  119. main = entry.get("main", {})
  120. wind = entry.get("wind", {})
  121. weather = entry.get("weather", [{}])[0]
  122. rain = entry.get("rain", {})
  123. temp = main.get("temp", 0.0)
  124. wind_speed = wind.get("speed", 0.0) * 3.6
  125. precip = rain.get("3h", 0.0)
  126. if day not in daily:
  127. daily[day] = {
  128. "temp_min": temp,
  129. "temp_max": temp,
  130. "precipitation": 0.0,
  131. "wind_speed_max": wind_speed,
  132. "description": weather.get("description", "").capitalize(),
  133. }
  134. else:
  135. d = daily[day]
  136. d["temp_min"] = min(d["temp_min"], temp)
  137. d["temp_max"] = max(d["temp_max"], temp)
  138. d["precipitation"] += precip
  139. d["wind_speed_max"] = max(d["wind_speed_max"], wind_speed)
  140. forecast: list[ForecastDay] = []
  141. for day_date in sorted(daily.keys()):
  142. d = daily[day_date]
  143. forecast.append(ForecastDay(
  144. date=day_date,
  145. temp_min=d["temp_min"],
  146. temp_max=d["temp_max"],
  147. description=d["description"],
  148. precipitation=round(d["precipitation"], 1),
  149. wind_speed_max=round(d["wind_speed_max"], 1),
  150. ))
  151. return forecast