| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- # -*- 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
|