feat(service): scraper station WeeWX (RSS current + NOAA yesterday)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
121
backend/app/services/station.py
Normal file
121
backend/app/services/station.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user