Files
jardin/backend/app/services/station.py
2026-02-22 18:34:50 +01:00

175 lines
6.3 KiB
Python

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