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>
This commit is contained in:
parent
94cf618e0d
commit
d3305a243c
6 changed files with 280 additions and 196 deletions
|
|
@ -21,65 +21,72 @@ CACHE_KEY = "weather"
|
|||
|
||||
@router.get("/weather")
|
||||
async def get_weather() -> Dict[str, Any]:
|
||||
"""Return weather for both configured locations plus an hourly forecast.
|
||||
|
||||
The response shape is::
|
||||
|
||||
{
|
||||
"primary": { ... weather dict or error stub },
|
||||
"secondary": { ... weather dict or error stub },
|
||||
"hourly": [ ... forecast entries or empty list ],
|
||||
}
|
||||
"""
|
||||
"""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 ----------------------------
|
||||
primary_data: Dict[str, Any] = {}
|
||||
secondary_data: Dict[str, Any] = {}
|
||||
hourly_data: List[Dict[str, Any]] = []
|
||||
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(get_settings().weather_location),
|
||||
_safe_fetch_weather(get_settings().weather_location_secondary),
|
||||
_safe_fetch_hourly(get_settings().weather_location),
|
||||
return_exceptions=False, # we handle errors inside the helpers
|
||||
_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, get_settings().weather_cache_ttl)
|
||||
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:
|
||||
data = await fetch_weather(location)
|
||||
return data
|
||||
return await fetch_weather(location)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to fetch weather for %s", location)
|
||||
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:
|
||||
data = await fetch_hourly_forecast(location)
|
||||
return data
|
||||
return await fetch_hourly_forecast(location)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to fetch hourly forecast for %s", location)
|
||||
logger.exception("[WEATHER] Unhandled error fetching hourly for '%s'", location)
|
||||
return []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue