# -*- coding: utf-8 -*- """ Bright Sky (DWD) Wetter-Provider. Nutzt offizielle Daten des Deutschen Wetterdienstes. Kein API-Key erforderlich, nur fuer Standorte in Deutschland. https://brightsky.dev/ """ from datetime import date, datetime, timedelta import httpx from plugins.weather.models import ForecastDay, WeatherData, WeatherResult from plugins.weather.providers.base import WeatherProvider from plugins.weather.utils import get_brightsky_description from trixy_core.utils.debug import pdebug, perror class BrightSkyProvider(WeatherProvider): """Wetter-Provider ueber die Bright Sky API (DWD-Daten).""" BASE_URL = "https://api.brightsky.dev" def __init__(self, config: dict, http=None) -> None: super().__init__(name="brightsky", config=config, http=http) async def get_weather( self, lat: float, lon: float, location_name: str = "" ) -> WeatherResult | None: """Ruft Wetterdaten von Bright Sky ab.""" today = date.today() end_date = today + timedelta(days=7) try: async with httpx.AsyncClient(timeout=10.0) as client: # Aktuelles Wetter current_resp = await client.get( f"{self.BASE_URL}/current_weather", params={"lat": lat, "lon": lon}, ) current_resp.raise_for_status() current_data = current_resp.json() # Vorhersage forecast_resp = await client.get( f"{self.BASE_URL}/weather", params={ "lat": lat, "lon": lon, "date": today.isoformat(), "last_date": end_date.isoformat(), }, ) forecast_resp.raise_for_status() forecast_data = forecast_resp.json() return self._parse_response( current_data, forecast_data, location_name ) except Exception as e: perror(f"BrightSky: Fehler beim Abruf: {e}") return None async def health_check(self) -> bool: """Prueft ob die Bright Sky API erreichbar ist.""" try: async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get( f"{self.BASE_URL}/current_weather", params={"lat": 49.25, "lon": 8.88}, ) return response.status_code == 200 except Exception: return False def _parse_response( self, current_data: dict, forecast_data: dict, location_name: str, ) -> WeatherResult | None: """Parst die Bright Sky API-Antworten.""" try: weather = current_data.get("weather", {}) condition = weather.get("condition", "dry") current_weather = WeatherData( location=location_name, temperature=weather.get("temperature", 0.0), feels_like=None, humidity=int(weather.get("relative_humidity", 0)), description=get_brightsky_description(condition), wind_speed=weather.get("wind_speed", 0.0), wind_direction=int(weather.get("wind_direction", 0)), precipitation=weather.get("precipitation_10", 0.0), cloud_cover=_safe_int(weather.get("cloud_cover")), pressure=weather.get("pressure_msl"), provider_name=self.name, timestamp=datetime.now(), raw_data=weather, ) # Vorhersage parsen forecast = self._parse_forecast(forecast_data) return WeatherResult( current=current_weather, forecast=forecast, fetched_at=datetime.now(), ) except Exception as e: perror(f"BrightSky: Fehler beim Parsen: {e}") return None def _parse_forecast(self, forecast_data: dict) -> list[ForecastDay]: """ Parst die stuendliche Vorhersage zu Tageswerten. Aggregiert stuendliche Werte zu Min/Max pro Tag. """ daily: dict[date, dict] = {} for entry in forecast_data.get("weather", []): timestamp = entry.get("timestamp", "") if not timestamp: continue try: dt = datetime.fromisoformat(timestamp) except ValueError: continue day = dt.date() temp = entry.get("temperature") if temp is None: continue wind_speed = entry.get("wind_speed", 0.0) precip = entry.get("precipitation", 0.0) condition = entry.get("condition", "dry") if day not in daily: daily[day] = { "temp_min": temp, "temp_max": temp, "precipitation": 0.0, "wind_speed_max": wind_speed, "condition": condition, } else: d = daily[day] d["temp_min"] = min(d["temp_min"], temp) d["temp_max"] = max(d["temp_max"], temp) d["precipitation"] += precip d["wind_speed_max"] = max(d["wind_speed_max"], wind_speed) forecast: list[ForecastDay] = [] for day_date in sorted(daily.keys()): d = daily[day_date] forecast.append(ForecastDay( date=day_date, temp_min=d["temp_min"], temp_max=d["temp_max"], description=get_brightsky_description(d["condition"]), precipitation=round(d["precipitation"], 1), wind_speed_max=round(d["wind_speed_max"], 1), )) 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