- München als tertiärer Standort (iris-Akzent) hinzugefügt - Klick auf WeatherCard öffnet Detail-Modal mit: - 24h stündliche Prognose (horizontal scrollbar) - 7-Tage-Vorhersage mit Temperaturbalken - Wind, Feuchte, Sonnenauf/-untergang - Backend: 7-Tage statt 3-Tage Forecast, 24 Hourly-Slots pro Standort - Backend: forecast_3day → forecast Feldname-Konsistenz - Dashboard: 3-Spalten Wetter-Grid statt 4 (HourlyForecast → Modal) - Admin: Tertiärer Standort konfigurierbar - THERMAL Design: iris glow, modal animation, Portal-basiertes Modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
"""Weather data router -- primary, secondary & tertiary locations with hourly forecasts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Any, Dict, List
|
|
|
|
from fastapi import APIRouter
|
|
|
|
from server.cache import cache
|
|
from server.config import get_settings
|
|
from server.services.weather_service import fetch_hourly_forecast, fetch_weather
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api", tags=["weather"])
|
|
|
|
CACHE_KEY = "weather"
|
|
|
|
|
|
@router.get("/weather")
|
|
async def get_weather() -> Dict[str, Any]:
|
|
"""Return weather for both configured locations plus an hourly forecast."""
|
|
|
|
# --- cache hit? -----------------------------------------------------------
|
|
cached = await cache.get(CACHE_KEY)
|
|
if cached is not None:
|
|
logger.debug("[WEATHER] Cache hit (key=%s)", CACHE_KEY)
|
|
return cached
|
|
|
|
# --- cache miss -- fetch all three in parallel ----------------------------
|
|
cfg = get_settings()
|
|
logger.info("[WEATHER] Cache miss — fetching '%s' + '%s' + '%s'",
|
|
cfg.weather_location, cfg.weather_location_secondary,
|
|
cfg.weather_location_tertiary)
|
|
|
|
results = await asyncio.gather(
|
|
_safe_fetch_weather(cfg.weather_location),
|
|
_safe_fetch_weather(cfg.weather_location_secondary),
|
|
_safe_fetch_weather(cfg.weather_location_tertiary),
|
|
_safe_fetch_hourly(cfg.weather_location, max_slots=24),
|
|
_safe_fetch_hourly(cfg.weather_location_secondary, max_slots=24),
|
|
_safe_fetch_hourly(cfg.weather_location_tertiary, max_slots=24),
|
|
return_exceptions=False,
|
|
)
|
|
|
|
primary_data = results[0]
|
|
secondary_data = results[1]
|
|
tertiary_data = results[2]
|
|
hourly_data = results[3]
|
|
hourly_secondary = results[4]
|
|
hourly_tertiary = results[5]
|
|
|
|
# Log result summary
|
|
_log_weather_result("primary", cfg.weather_location, primary_data)
|
|
_log_weather_result("secondary", cfg.weather_location_secondary, secondary_data)
|
|
_log_weather_result("tertiary", cfg.weather_location_tertiary, tertiary_data)
|
|
logger.info("[WEATHER] Hourly: %d + %d + %d slots",
|
|
len(hourly_data), len(hourly_secondary), len(hourly_tertiary))
|
|
|
|
payload: Dict[str, Any] = {
|
|
"primary": primary_data,
|
|
"secondary": secondary_data,
|
|
"tertiary": tertiary_data,
|
|
"hourly": hourly_data,
|
|
"hourly_secondary": hourly_secondary,
|
|
"hourly_tertiary": hourly_tertiary,
|
|
}
|
|
|
|
await cache.set(CACHE_KEY, payload, cfg.weather_cache_ttl)
|
|
logger.debug("[WEATHER] Cached for %ds", cfg.weather_cache_ttl)
|
|
return payload
|
|
|
|
|
|
# -- internal helpers ---------------------------------------------------------
|
|
|
|
def _log_weather_result(label: str, location: str, data: Dict[str, Any]) -> None:
|
|
"""Log a concise summary of a weather result."""
|
|
if data.get("error"):
|
|
logger.error("[WEATHER] %s (%s): ERROR — %s", label, location,
|
|
data.get("message", data.get("error")))
|
|
else:
|
|
logger.info("[WEATHER] %s (%s): %d°C, %s",
|
|
label, data.get("location", location),
|
|
data.get("temp", 0), data.get("description", "?"))
|
|
|
|
|
|
async def _safe_fetch_weather(location: str) -> Dict[str, Any]:
|
|
"""Fetch weather for *location*, returning an error stub on failure."""
|
|
try:
|
|
return await fetch_weather(location)
|
|
except Exception as exc:
|
|
logger.exception("[WEATHER] Unhandled error fetching '%s'", location)
|
|
return {"error": True, "message": str(exc), "location": location}
|
|
|
|
|
|
async def _safe_fetch_hourly(location: str, max_slots: int = 8) -> List[Dict[str, Any]]:
|
|
"""Fetch hourly forecast for *location*, returning ``[]`` on failure."""
|
|
try:
|
|
return await fetch_hourly_forecast(location, max_slots=max_slots)
|
|
except Exception as exc:
|
|
logger.exception("[WEATHER] Unhandled error fetching hourly for '%s'", location)
|
|
return []
|