diff --git a/backend/app/services/station.py b/backend/app/services/station.py new file mode 100644 index 0000000..296d939 --- /dev/null +++ b/backend/app/services/station.py @@ -0,0 +1,121 @@ +"""Service de collecte des données de la station météo locale WeeWX.""" +import logging +import re +import xml.etree.ElementTree as ET +from datetime import datetime, timedelta, timezone + +import httpx + +from app.config import STATION_URL + +logger = logging.getLogger(__name__) + + +def _safe_float(text: str | None) -> float | None: + if text is None: + return None + try: + cleaned = text.strip().replace(",", ".") + # Retirer unités courantes + for unit in [" °C", " %", " hPa", " km/h", " W/m²", "°C", "%", "hPa"]: + cleaned = cleaned.replace(unit, "") + return float(cleaned.strip()) + except (ValueError, AttributeError): + return None + + +def _direction_to_abbr(deg: float | None) -> str | None: + if deg is None: + return None + dirs = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"] + return dirs[round(deg / 45) % 8] + + +def fetch_current(base_url: str = STATION_URL) -> dict | None: + """Scrape les données actuelles depuis le RSS de la station WeeWX. + + Retourne un dict avec les clés : temp_ext, humidite, pression, + pluie_mm, vent_kmh, vent_dir, uv, solaire — ou None si indisponible. + """ + try: + url = base_url.rstrip("/") + "/rss.xml" + r = httpx.get(url, timeout=10) + r.raise_for_status() + root = ET.fromstring(r.text) + + channel = root.find("channel") + if channel is None: + return None + + item = channel.find("item") + if item is None: + return None + + desc = item.findtext("description") or "" + + result: dict = {} + + patterns = { + "temp_ext": r"(?:Outside|Ext(?:erieur)?)\s*Temp(?:erature)?\s*[:\s]+(-?\d+(?:[.,]\d+)?)", + "temp_int": r"(?:Inside|Int(?:erieur)?)\s*Temp(?:erature)?\s*[:\s]+(-?\d+(?:[.,]\d+)?)", + "humidite": r"(?:Outside\s*)?Hum(?:idity)?\s*[:\s]+(\d+(?:[.,]\d+)?)", + "pression": r"(?:Bar(?:ometer)?|Pression)\s*[:\s]+(\d+(?:[.,]\d+)?)", + "pluie_mm": r"(?:Rain(?:fall)?|Pluie)\s*[:\s]+(\d+(?:[.,]\d+)?)", + "vent_kmh": r"(?:Wind\s*Speed|Vent)\s*[:\s]+(\d+(?:[.,]\d+)?)", + "uv": r"UV\s*[:\s]+(\d+(?:[.,]\d+)?)", + "solaire": r"(?:Solar\s*Radiation|Solaire)\s*[:\s]+(\d+(?:[.,]\d+)?)", + } + + for key, pattern in patterns.items(): + m = re.search(pattern, desc, re.IGNORECASE) + result[key] = _safe_float(m.group(1)) if m else None + + vent_dir_m = re.search( + r"(?:Wind\s*Dir(?:ection)?)\s*[:\s]+([NSEO]{1,2}|Nord|Sud|Est|Ouest|\d+)", + desc, re.IGNORECASE, + ) + if vent_dir_m: + val = vent_dir_m.group(1).strip() + if val.isdigit(): + result["vent_dir"] = _direction_to_abbr(float(val)) + else: + result["vent_dir"] = val[:2].upper() + else: + result["vent_dir"] = None + + return result if any(v is not None for v in result.values()) else None + + except Exception as e: + logger.warning(f"Station fetch_current error: {e}") + return None + + +def fetch_yesterday_summary(base_url: str = STATION_URL) -> dict | None: + """Récupère le résumé de la veille via le fichier NOAA mensuel de la station WeeWX. + + Retourne un dict avec : temp_ext (moy), t_min, t_max, pluie_mm — ou None. + """ + yesterday = (datetime.now() - timedelta(days=1)).date() + year = yesterday.strftime("%Y") + month = yesterday.strftime("%m") + day = yesterday.day + + try: + url = f"{base_url.rstrip('/')}/NOAA/NOAA-{year}-{month}.txt" + r = httpx.get(url, timeout=15) + r.raise_for_status() + + for line in r.text.splitlines(): + parts = line.split() + if len(parts) >= 7 and parts[0].isdigit() and int(parts[0]) == day: + # Format NOAA : jour tmax tmin tmoy precip ... + return { + "t_max": _safe_float(parts[1]), + "t_min": _safe_float(parts[2]), + "temp_ext": _safe_float(parts[3]), + "pluie_mm": _safe_float(parts[5]), + "vent_kmh": _safe_float(parts[6]) if len(parts) > 6 else None, + } + except Exception as e: + logger.warning(f"Station fetch_yesterday_summary error: {e}") + return None