"""Service de collecte des données de la station météo locale WeeWX.""" import html import logging import re import unicodedata 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", " mbar", " km/h", " m/s", " mm/h", " mm", " W/m²", " W/m2", "°C", "%", "hPa", "mbar", ]: cleaned = cleaned.replace(unit, "") return float(cleaned.strip()) except (ValueError, AttributeError): return None def _normalize(text: str) -> str: text = unicodedata.normalize("NFKD", text) text = "".join(ch for ch in text if not unicodedata.combining(ch)) text = text.lower() return re.sub(r"\s+", " ", text).strip() def _to_kmh(value: float | None, unit: str | None) -> float | None: if value is None: return None u = (unit or "").strip().lower() if u == "m/s": return round(value * 3.6, 1) return round(value, 1) 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 = html.unescape(item.findtext("description") or "") result: dict = {} segments = [seg.strip() for seg in desc.split(";") if seg.strip()] for seg in segments: if ":" not in seg: continue raw_key, raw_value = seg.split(":", 1) key = _normalize(raw_key) value = raw_value.strip() if "temperature exterieure" in key or "outside temperature" in key: result["temp_ext"] = _safe_float(value) continue if "temperature interieure" in key or "inside temperature" in key: result["temp_int"] = _safe_float(value) continue if "hygrometrie exterieure" in key or "outside humidity" in key: result["humidite"] = _safe_float(value) continue if "pression atmospherique" in key or "barometer" in key: result["pression"] = _safe_float(value) continue if "precipitations" in key and "taux" not in key and "rate" not in key: result["pluie_mm"] = _safe_float(value) continue if key in {"uv", "ultra-violet"} or "ultra violet" in key: result["uv"] = _safe_float(value) continue if "rayonnement solaire" in key or "solar radiation" in key: result["solaire"] = _safe_float(value) continue if key == "vent" or "wind" in key: speed_match = re.search(r"(-?\d+(?:[.,]\d+)?)\s*(m/s|km/h)?", value, re.IGNORECASE) speed_val = _safe_float(speed_match.group(1)) if speed_match else None speed_unit = speed_match.group(2) if speed_match else None result["vent_kmh"] = _to_kmh(speed_val, speed_unit) deg_match = re.search(r"(\d{1,3}(?:[.,]\d+)?)\s*°", value) if deg_match: result["vent_dir"] = _direction_to_abbr(_safe_float(deg_match.group(1))) continue card_match = re.search(r"\b(N|NE|E|SE|S|SO|O|NO|NNE|ENE|ESE|SSE|SSO|OSO|ONO|NNO)\b", value, re.IGNORECASE) result["vent_dir"] = card_match.group(1).upper() if card_match else 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 not parts or not parts[0].isdigit() or int(parts[0]) != day: continue # Format WeeWX NOAA (fréquent) : # day mean max hh:mm min hh:mm HDD CDD rain wind_avg wind_max hh:mm dir if len(parts) >= 11 and ":" in parts[3] and ":" in parts[5]: return { "temp_ext": _safe_float(parts[1]), "t_max": _safe_float(parts[2]), "t_min": _safe_float(parts[4]), "pluie_mm": _safe_float(parts[8]), "vent_kmh": _to_kmh(_safe_float(parts[10]), "m/s"), } # Fallback générique (anciens formats) return { "t_max": _safe_float(parts[1]) if len(parts) > 1 else None, "t_min": _safe_float(parts[2]) if len(parts) > 2 else None, "temp_ext": _safe_float(parts[3]) if len(parts) > 3 else None, "pluie_mm": _safe_float(parts[5]) if len(parts) > 5 else None, "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