"""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