"""Router météo — station WeeWX + Open-Meteo + tableau synthétique.""" from datetime import date, timedelta from typing import Any, Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import text from sqlalchemy.exc import OperationalError from sqlmodel import Session from app.database import get_session router = APIRouter(tags=["météo"]) def _station_daily_summary(session: Session, iso_date: str) -> Optional[dict]: """Agrège les mesures horaires d'une journée en résumé.""" try: rows = session.exec( text( "SELECT temp_ext, t_min, t_max, pluie_mm, vent_kmh, humidite " "FROM meteostation WHERE substr(date_heure, 1, 10) = :d" ), params={"d": iso_date}, ).fetchall() except OperationalError: return None if not rows: return None temps = [r[0] for r in rows if r[0] is not None] t_mins = [r[1] for r in rows if r[1] is not None] t_maxs = [r[2] for r in rows if r[2] is not None] pluies = [r[3] for r in rows if r[3] is not None] vents = [r[4] for r in rows if r[4] is not None] hums = [r[5] for r in rows if r[5] is not None] min_candidates = temps + t_mins max_candidates = temps + t_maxs return { "t_min": round(min(min_candidates), 1) if min_candidates else None, "t_max": round(max(max_candidates), 1) if max_candidates else None, # WeeWX RSS expose souvent une pluie cumulée journalière. "pluie_mm": round(max(pluies), 1) if pluies else 0.0, "vent_kmh": round(max(vents), 1) if vents else None, "humidite": round(sum(hums) / len(hums), 0) if hums else None, } def _station_current_row(session: Session) -> Optional[dict]: """Dernière mesure station (max 2h d'ancienneté).""" try: row = session.exec( text( "SELECT temp_ext, humidite, pression, pluie_mm, vent_kmh, vent_dir, uv, solaire, date_heure " "FROM meteostation WHERE type='current' ORDER BY date_heure DESC LIMIT 1" ) ).fetchone() except OperationalError: return None if not row: return None return { "temp_ext": row[0], "humidite": row[1], "pression": row[2], "pluie_mm": row[3], "vent_kmh": row[4], "vent_dir": row[5], "uv": row[6], "solaire": row[7], "date_heure": row[8], } def _open_meteo_day(session: Session, iso_date: str) -> Optional[dict]: try: row = session.exec( text( "SELECT t_min, t_max, pluie_mm, vent_kmh, wmo, label, humidite_moy, sol_0cm, etp_mm " "FROM meteoopenmeteo WHERE date = :d" ), params={"d": iso_date}, ).fetchone() except OperationalError: return None if not row: return None return { "t_min": row[0], "t_max": row[1], "pluie_mm": row[2], "vent_kmh": row[3], "wmo": row[4], "label": row[5], "humidite_moy": row[6], "sol_0cm": row[7], "etp_mm": row[8], } @router.get("/meteo/tableau") def get_tableau( center_date: Optional[str] = Query(None, description="Date centrale YYYY-MM-DD"), span: int = Query(7, ge=1, le=31, description="Nombre de jours avant/après la date centrale"), session: Session = Depends(get_session), ) -> dict[str, Any]: """Tableau synthétique centré sur une date, avec historique + prévision.""" today = date.today() center = today if center_date: try: center = date.fromisoformat(center_date) except ValueError as exc: raise HTTPException(status_code=400, detail="center_date invalide (format YYYY-MM-DD)") from exc rows = [] for delta in range(-span, span + 1): d = center + timedelta(days=delta) iso = d.isoformat() delta_today = (d - today).days if delta_today < 0: row_type = "passe" station = _station_daily_summary(session, iso) om = _open_meteo_day(session, iso) elif delta_today == 0: row_type = "aujourd_hui" station_current = _station_current_row(session) or {} station_daily = _station_daily_summary(session, iso) or {} station = {**station_daily, **station_current} or None om = _open_meteo_day(session, iso) else: row_type = "futur" station = None om = _open_meteo_day(session, iso) rows.append({"date": iso, "type": row_type, "station": station, "open_meteo": om}) return {"rows": rows} @router.get("/meteo/station/current") def get_station_current(session: Session = Depends(get_session)) -> Optional[dict]: return _station_current_row(session) @router.get("/meteo/station/history") def get_station_history( days: int = Query(7, ge=1, le=30), session: Session = Depends(get_session), ) -> dict[str, Any]: today = date.today() result = [] for delta in range(-days, 0): d = today + timedelta(days=delta) iso = d.isoformat() summary = _station_daily_summary(session, iso) result.append({"date": iso, "station": summary}) return {"days": result} @router.get("/meteo/previsions") def get_previsions( days: int = Query(7, ge=1, le=14), session: Session = Depends(get_session), ) -> dict[str, Any]: today = date.today() result = [] for delta in range(0, days + 1): d = today + timedelta(days=delta) iso = d.isoformat() om = _open_meteo_day(session, iso) if om: result.append({"date": iso, **om}) return {"days": result} @router.get("/meteo") def get_meteo_legacy( days: int = Query(14, ge=1, le=16), lat: float = Query(45.14), lon: float = Query(4.12), ): """Compatibilité ascendante avec l'ancien endpoint.""" from app.services.meteo import fetch_forecast return fetch_forecast(lat=lat, lon=lon, days=days) @router.post("/meteo/refresh") def refresh_meteo() -> dict[str, str]: """Force le rafraîchissement immédiat des 3 jobs.""" from app.services.scheduler import scheduler import datetime as dt for job_id in ["station_current", "station_veille", "open_meteo"]: job = scheduler.get_job(job_id) if job: job.modify(next_run_time=dt.datetime.now()) return {"status": "refresh planifié"}