brightsky.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. # -*- coding: utf-8 -*-
  2. """
  3. Bright Sky (DWD) Wetter-Provider.
  4. Nutzt offizielle Daten des Deutschen Wetterdienstes.
  5. Kein API-Key erforderlich, nur fuer Standorte in Deutschland.
  6. https://brightsky.dev/
  7. """
  8. from datetime import date, datetime, timedelta
  9. import httpx
  10. from plugins.weather.models import ForecastDay, WeatherData, WeatherResult
  11. from plugins.weather.providers.base import WeatherProvider
  12. from plugins.weather.utils import get_brightsky_description
  13. from trixy_core.utils.debug import pdebug, perror
  14. class BrightSkyProvider(WeatherProvider):
  15. """Wetter-Provider ueber die Bright Sky API (DWD-Daten)."""
  16. BASE_URL = "https://api.brightsky.dev"
  17. def __init__(self, config: dict, http=None) -> None:
  18. super().__init__(name="brightsky", config=config, http=http)
  19. async def get_weather(
  20. self, lat: float, lon: float, location_name: str = ""
  21. ) -> WeatherResult | None:
  22. """Ruft Wetterdaten von Bright Sky ab."""
  23. today = date.today()
  24. end_date = today + timedelta(days=7)
  25. try:
  26. async with httpx.AsyncClient(timeout=10.0) as client:
  27. # Aktuelles Wetter
  28. current_resp = await client.get(
  29. f"{self.BASE_URL}/current_weather",
  30. params={"lat": lat, "lon": lon},
  31. )
  32. current_resp.raise_for_status()
  33. current_data = current_resp.json()
  34. # Vorhersage
  35. forecast_resp = await client.get(
  36. f"{self.BASE_URL}/weather",
  37. params={
  38. "lat": lat,
  39. "lon": lon,
  40. "date": today.isoformat(),
  41. "last_date": end_date.isoformat(),
  42. },
  43. )
  44. forecast_resp.raise_for_status()
  45. forecast_data = forecast_resp.json()
  46. return self._parse_response(
  47. current_data, forecast_data, location_name
  48. )
  49. except Exception as e:
  50. perror(f"BrightSky: Fehler beim Abruf: {e}")
  51. return None
  52. async def health_check(self) -> bool:
  53. """Prueft ob die Bright Sky API erreichbar ist."""
  54. try:
  55. async with httpx.AsyncClient(timeout=5.0) as client:
  56. response = await client.get(
  57. f"{self.BASE_URL}/current_weather",
  58. params={"lat": 49.25, "lon": 8.88},
  59. )
  60. return response.status_code == 200
  61. except Exception:
  62. return False
  63. def _parse_response(
  64. self,
  65. current_data: dict,
  66. forecast_data: dict,
  67. location_name: str,
  68. ) -> WeatherResult | None:
  69. """Parst die Bright Sky API-Antworten."""
  70. try:
  71. weather = current_data.get("weather", {})
  72. condition = weather.get("condition", "dry")
  73. current_weather = WeatherData(
  74. location=location_name,
  75. temperature=weather.get("temperature", 0.0),
  76. feels_like=None,
  77. humidity=int(weather.get("relative_humidity", 0)),
  78. description=get_brightsky_description(condition),
  79. wind_speed=weather.get("wind_speed", 0.0),
  80. wind_direction=int(weather.get("wind_direction", 0)),
  81. precipitation=weather.get("precipitation_10", 0.0),
  82. cloud_cover=_safe_int(weather.get("cloud_cover")),
  83. pressure=weather.get("pressure_msl"),
  84. provider_name=self.name,
  85. timestamp=datetime.now(),
  86. raw_data=weather,
  87. )
  88. # Vorhersage parsen
  89. forecast = self._parse_forecast(forecast_data)
  90. return WeatherResult(
  91. current=current_weather,
  92. forecast=forecast,
  93. fetched_at=datetime.now(),
  94. )
  95. except Exception as e:
  96. perror(f"BrightSky: Fehler beim Parsen: {e}")
  97. return None
  98. def _parse_forecast(self, forecast_data: dict) -> list[ForecastDay]:
  99. """
  100. Parst die stuendliche Vorhersage zu Tageswerten.
  101. Aggregiert stuendliche Werte zu Min/Max pro Tag.
  102. """
  103. daily: dict[date, dict] = {}
  104. for entry in forecast_data.get("weather", []):
  105. timestamp = entry.get("timestamp", "")
  106. if not timestamp:
  107. continue
  108. try:
  109. dt = datetime.fromisoformat(timestamp)
  110. except ValueError:
  111. continue
  112. day = dt.date()
  113. temp = entry.get("temperature")
  114. if temp is None:
  115. continue
  116. wind_speed = entry.get("wind_speed", 0.0)
  117. precip = entry.get("precipitation", 0.0)
  118. condition = entry.get("condition", "dry")
  119. if day not in daily:
  120. daily[day] = {
  121. "temp_min": temp,
  122. "temp_max": temp,
  123. "precipitation": 0.0,
  124. "wind_speed_max": wind_speed,
  125. "condition": condition,
  126. }
  127. else:
  128. d = daily[day]
  129. d["temp_min"] = min(d["temp_min"], temp)
  130. d["temp_max"] = max(d["temp_max"], temp)
  131. d["precipitation"] += precip
  132. d["wind_speed_max"] = max(d["wind_speed_max"], wind_speed)
  133. forecast: list[ForecastDay] = []
  134. for day_date in sorted(daily.keys()):
  135. d = daily[day_date]
  136. forecast.append(ForecastDay(
  137. date=day_date,
  138. temp_min=d["temp_min"],
  139. temp_max=d["temp_max"],
  140. description=get_brightsky_description(d["condition"]),
  141. precipitation=round(d["precipitation"], 1),
  142. wind_speed_max=round(d["wind_speed_max"], 1),
  143. ))
  144. return forecast
  145. def _safe_int(value: object) -> int | None:
  146. """Konvertiert einen Wert sicher zu int oder None."""
  147. if value is None:
  148. return None
  149. try:
  150. return int(value)
  151. except (TypeError, ValueError):
  152. return None