daily-briefing/server/routers/weather.py
Sam d3305a243c Weather: Replace wttr.in with Open-Meteo + structured logging
- Replace wttr.in (unreachable from Docker) with Open-Meteo API
  (free, no API key, reliable) with geocoding cache
- WMO weather codes mapped to German descriptions + emoji icons
- Add [WEATHER], [NEWS], [UNRAID], [DASHBOARD] log prefixes
- Structured integration status table on startup
- Suppress noisy httpx INFO logs (services log their own summaries)
- Add logging to unraid_service (was completely silent on errors)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:45:23 +01:00

92 lines
3.2 KiB
Python

"""Weather data router -- primary + secondary locations and hourly forecast."""
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'",
cfg.weather_location, cfg.weather_location_secondary)
results = await asyncio.gather(
_safe_fetch_weather(cfg.weather_location),
_safe_fetch_weather(cfg.weather_location_secondary),
_safe_fetch_hourly(cfg.weather_location),
return_exceptions=False,
)
primary_data = results[0]
secondary_data = results[1]
hourly_data = results[2]
# Log result summary
_log_weather_result("primary", cfg.weather_location, primary_data)
_log_weather_result("secondary", cfg.weather_location_secondary, secondary_data)
logger.info("[WEATHER] Hourly: %d slots", len(hourly_data))
payload: Dict[str, Any] = {
"primary": primary_data,
"secondary": secondary_data,
"hourly": hourly_data,
}
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) -> List[Dict[str, Any]]:
"""Fetch hourly forecast for *location*, returning ``[]`` on failure."""
try:
return await fetch_hourly_forecast(location)
except Exception as exc:
logger.exception("[WEATHER] Unhandled error fetching hourly for '%s'", location)
return []