maj via codex
This commit is contained in:
@@ -19,6 +19,17 @@ SIGN_TO_TYPE = {
|
||||
"Bélier": "Fruit", "Lion": "Fruit", "Sagittaire": "Fruit",
|
||||
}
|
||||
|
||||
SAINTS_BY_MMDD = {
|
||||
"04-23": "Saint Georges",
|
||||
"04-25": "Saint Marc",
|
||||
"05-11": "Saint Mamert",
|
||||
"05-12": "Saint Pancrace",
|
||||
"05-13": "Saint Servais",
|
||||
"05-14": "Saint Boniface",
|
||||
"05-19": "Saint Yves",
|
||||
"05-25": "Saint Urbain",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DayInfo:
|
||||
@@ -29,6 +40,7 @@ class DayInfo:
|
||||
montante_descendante: str
|
||||
signe: str
|
||||
type_jour: str
|
||||
saint_du_jour: str
|
||||
perigee: bool
|
||||
apogee: bool
|
||||
noeud_lunaire: bool
|
||||
@@ -126,6 +138,7 @@ def build_calendar(start: date, end: date) -> list[DayInfo]:
|
||||
lat, lon, dist = v_moon.ecliptic_latlon()
|
||||
signe = zodiac_sign_from_lon(lon.degrees % 360.0)
|
||||
type_jour = SIGN_TO_TYPE[signe]
|
||||
saint_du_jour = SAINTS_BY_MMDD.get(d.strftime("%m-%d"), "")
|
||||
result.append(
|
||||
DayInfo(
|
||||
date=d.isoformat(),
|
||||
@@ -135,6 +148,7 @@ def build_calendar(start: date, end: date) -> list[DayInfo]:
|
||||
montante_descendante=montante,
|
||||
signe=signe,
|
||||
type_jour=type_jour,
|
||||
saint_du_jour=saint_du_jour,
|
||||
perigee=(d in perigee_days),
|
||||
apogee=(d in apogee_days),
|
||||
noeud_lunaire=(d in node_days),
|
||||
|
||||
@@ -32,6 +32,46 @@ _DAILY_FIELDS = [
|
||||
"et0_fao_evapotranspiration",
|
||||
]
|
||||
|
||||
_HOURLY_FIELDS = [
|
||||
"soil_temperature_0cm",
|
||||
]
|
||||
|
||||
|
||||
def _to_float(value: Any) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _value_at(values: list[Any], index: int, default: Any = None) -> Any:
|
||||
if index < 0 or index >= len(values):
|
||||
return default
|
||||
return values[index]
|
||||
|
||||
|
||||
def _daily_soil_average(raw: dict[str, Any]) -> dict[str, float]:
|
||||
"""Construit un mapping ISO-date -> moyenne de soil_temperature_0cm."""
|
||||
hourly = raw.get("hourly", {})
|
||||
times = hourly.get("time", []) or []
|
||||
soils = hourly.get("soil_temperature_0cm", []) or []
|
||||
by_day: dict[str, list[float]] = {}
|
||||
|
||||
for idx, ts in enumerate(times):
|
||||
soil = _to_float(_value_at(soils, idx))
|
||||
if soil is None or not isinstance(ts, str) or len(ts) < 10:
|
||||
continue
|
||||
day = ts[:10]
|
||||
by_day.setdefault(day, []).append(soil)
|
||||
|
||||
return {
|
||||
day: round(sum(vals) / len(vals), 2)
|
||||
for day, vals in by_day.items()
|
||||
if vals
|
||||
}
|
||||
|
||||
|
||||
def fetch_and_store_forecast(lat: float = METEO_LAT, lon: float = METEO_LON) -> list[dict]:
|
||||
"""Appelle Open-Meteo et retourne la liste des jours (past_days=7 + forecast=8).
|
||||
@@ -50,6 +90,8 @@ def fetch_and_store_forecast(lat: float = METEO_LAT, lon: float = METEO_LON) ->
|
||||
]
|
||||
for field in _DAILY_FIELDS:
|
||||
params.append(("daily", field))
|
||||
for field in _HOURLY_FIELDS:
|
||||
params.append(("hourly", field))
|
||||
|
||||
try:
|
||||
r = httpx.get(url, params=params, timeout=15)
|
||||
@@ -61,22 +103,23 @@ def fetch_and_store_forecast(lat: float = METEO_LAT, lon: float = METEO_LON) ->
|
||||
|
||||
daily = raw.get("daily", {})
|
||||
dates = daily.get("time", [])
|
||||
soil_by_day = _daily_soil_average(raw)
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
rows = []
|
||||
|
||||
for i, d in enumerate(dates):
|
||||
code = int(daily.get("weather_code", [0] * len(dates))[i] or 0)
|
||||
code = int(_value_at(daily.get("weather_code", []), i, 0) or 0)
|
||||
row = {
|
||||
"date": d,
|
||||
"t_min": daily.get("temperature_2m_min", [None] * len(dates))[i],
|
||||
"t_max": daily.get("temperature_2m_max", [None] * len(dates))[i],
|
||||
"pluie_mm": daily.get("precipitation_sum", [0] * len(dates))[i] or 0.0,
|
||||
"vent_kmh": daily.get("wind_speed_10m_max", [0] * len(dates))[i] or 0.0,
|
||||
"t_min": _to_float(_value_at(daily.get("temperature_2m_min", []), i)),
|
||||
"t_max": _to_float(_value_at(daily.get("temperature_2m_max", []), i)),
|
||||
"pluie_mm": _to_float(_value_at(daily.get("precipitation_sum", []), i, 0.0)) or 0.0,
|
||||
"vent_kmh": _to_float(_value_at(daily.get("wind_speed_10m_max", []), i, 0.0)) or 0.0,
|
||||
"wmo": code,
|
||||
"label": WMO_LABELS.get(code, f"Code {code}"),
|
||||
"humidite_moy": daily.get("relative_humidity_2m_max", [None] * len(dates))[i],
|
||||
"sol_0cm": None, # soil_temperature_0cm est hourly uniquement
|
||||
"etp_mm": daily.get("et0_fao_evapotranspiration", [None] * len(dates))[i],
|
||||
"humidite_moy": _to_float(_value_at(daily.get("relative_humidity_2m_max", []), i)),
|
||||
"sol_0cm": soil_by_day.get(d),
|
||||
"etp_mm": _to_float(_value_at(daily.get("et0_fao_evapotranspiration", []), i)),
|
||||
"fetched_at": now_iso,
|
||||
}
|
||||
rows.append(row)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""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
|
||||
|
||||
@@ -17,13 +19,37 @@ def _safe_float(text: str | None) -> float | None:
|
||||
try:
|
||||
cleaned = text.strip().replace(",", ".")
|
||||
# Retirer unités courantes
|
||||
for unit in [" °C", " %", " hPa", " km/h", " W/m²", "°C", "%", "hPa"]:
|
||||
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
|
||||
@@ -51,37 +77,51 @@ def fetch_current(base_url: str = STATION_URL) -> dict | None:
|
||||
if item is None:
|
||||
return None
|
||||
|
||||
desc = item.findtext("description") or ""
|
||||
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()
|
||||
|
||||
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+)?)",
|
||||
}
|
||||
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)
|
||||
|
||||
for key, pattern in patterns.items():
|
||||
m = re.search(pattern, desc, re.IGNORECASE)
|
||||
result[key] = _safe_float(m.group(1)) if m else None
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -107,15 +147,28 @@ def fetch_yesterday_summary(base_url: str = STATION_URL) -> dict | None:
|
||||
|
||||
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 ...
|
||||
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 {
|
||||
"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,
|
||||
"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
|
||||
|
||||
Reference in New Issue
Block a user