avant 50
This commit is contained in:
@@ -90,6 +90,70 @@ def _store_open_meteo() -> None:
|
||||
logger.info(f"Open-Meteo stocké : {len(rows)} jours")
|
||||
|
||||
|
||||
def backfill_station_missing_dates(max_days_back: int = 365) -> None:
|
||||
"""Remplit les dates manquantes de la station météo au démarrage.
|
||||
|
||||
Cherche toutes les dates sans entrée « veille » dans meteostation
|
||||
depuis max_days_back jours en arrière jusqu'à hier (excl. aujourd'hui),
|
||||
puis télécharge les fichiers NOAA mois par mois pour remplir les trous.
|
||||
Un seul appel HTTP par mois manquant.
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
from itertools import groupby
|
||||
from app.services.station import fetch_month_summaries
|
||||
from app.models.meteo import MeteoStation
|
||||
from app.database import engine
|
||||
from sqlmodel import Session, select
|
||||
|
||||
today = date.today()
|
||||
start_date = today - timedelta(days=max_days_back)
|
||||
|
||||
# 1. Dates « veille » déjà présentes en BDD
|
||||
with Session(engine) as session:
|
||||
rows = session.exec(
|
||||
select(MeteoStation.date_heure).where(MeteoStation.type == "veille")
|
||||
).all()
|
||||
existing_dates: set[str] = {dh[:10] for dh in rows}
|
||||
|
||||
# 2. Dates manquantes entre start_date et hier (aujourd'hui exclu)
|
||||
missing: list[date] = []
|
||||
cursor = start_date
|
||||
while cursor < today:
|
||||
if cursor.isoformat() not in existing_dates:
|
||||
missing.append(cursor)
|
||||
cursor += timedelta(days=1)
|
||||
|
||||
if not missing:
|
||||
logger.info("Backfill station : aucune date manquante")
|
||||
return
|
||||
|
||||
logger.info(f"Backfill station : {len(missing)} date(s) manquante(s) à récupérer")
|
||||
|
||||
# 3. Grouper par (année, mois) → 1 requête HTTP par mois
|
||||
def month_key(d: date) -> tuple[int, int]:
|
||||
return (d.year, d.month)
|
||||
|
||||
filled = 0
|
||||
for (year, month), group_iter in groupby(sorted(missing), key=month_key):
|
||||
month_data = fetch_month_summaries(year, month)
|
||||
if not month_data:
|
||||
logger.debug(f"Backfill station : pas de données NOAA pour {year}-{month:02d}")
|
||||
continue
|
||||
|
||||
with Session(engine) as session:
|
||||
for d in group_iter:
|
||||
data = month_data.get(d.day)
|
||||
if not data:
|
||||
continue
|
||||
date_heure = f"{d.isoformat()}T00:00"
|
||||
if not session.get(MeteoStation, date_heure):
|
||||
session.add(MeteoStation(date_heure=date_heure, type="veille", **data))
|
||||
filled += 1
|
||||
session.commit()
|
||||
|
||||
logger.info(f"Backfill station terminé : {filled} date(s) insérée(s)")
|
||||
|
||||
|
||||
def setup_scheduler() -> None:
|
||||
"""Configure et démarre le scheduler."""
|
||||
scheduler.add_job(
|
||||
|
||||
@@ -130,45 +130,63 @@ def fetch_current(base_url: str = STATION_URL) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_noaa_day_line(parts: list[str]) -> dict | None:
|
||||
"""Parse une ligne de données journalières du fichier NOAA WeeWX.
|
||||
|
||||
Format standard : day mean max hh:mm min hh:mm HDD CDD rain wind_avg wind_max hh:mm dir
|
||||
"""
|
||||
if not parts or not parts[0].isdigit():
|
||||
return None
|
||||
# Format complet avec timestamps hh:mm en positions 3 et 5
|
||||
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 sans hh:mm)
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
def fetch_month_summaries(year: int, month: int, base_url: str = STATION_URL) -> dict[int, dict]:
|
||||
"""Récupère tous les résumés journaliers d'un mois depuis le fichier NOAA WeeWX.
|
||||
|
||||
Retourne un dict {numéro_jour: data_dict} pour chaque jour disponible du mois.
|
||||
Un seul appel HTTP par mois — utilisé pour le backfill groupé.
|
||||
"""
|
||||
try:
|
||||
url = f"{base_url.rstrip('/')}/NOAA/NOAA-{year:04d}-{month:02d}.txt"
|
||||
r = httpx.get(url, timeout=15)
|
||||
r.raise_for_status()
|
||||
|
||||
result: dict[int, dict] = {}
|
||||
for line in r.text.splitlines():
|
||||
parts = line.split()
|
||||
if not parts or not parts[0].isdigit():
|
||||
continue
|
||||
data = _parse_noaa_day_line(parts)
|
||||
if data:
|
||||
result[int(parts[0])] = data
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Station fetch_month_summaries({year}-{month:02d}) error: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
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
|
||||
month_data = fetch_month_summaries(yesterday.year, yesterday.month, base_url)
|
||||
return month_data.get(yesterday.day)
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
import os
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
AI_SERVICE_URL = os.environ.get("AI_SERVICE_URL", "http://localhost:8070")
|
||||
AI_SERVICE_URL = os.environ.get("AI_SERVICE_URL", "http://ai-service:8070")
|
||||
|
||||
# Mapping class_name YOLO → nom commun français (partiel)
|
||||
_NOMS_FR = {
|
||||
"Tomato___healthy": "Tomate (saine)",
|
||||
"Tomato___Early_blight": "Tomate (mildiou précoce)",
|
||||
"Tomato___Late_blight": "Tomate (mildiou tardif)",
|
||||
"Pepper__bell___healthy": "Poivron (sain)",
|
||||
"Apple___healthy": "Pommier (sain)",
|
||||
"Potato___healthy": "Pomme de terre (saine)",
|
||||
"Grape___healthy": "Vigne (saine)",
|
||||
"Corn_(maize)___healthy": "Maïs (sain)",
|
||||
"Strawberry___healthy": "Fraisier (sain)",
|
||||
"Peach___healthy": "Pêcher (sain)",
|
||||
# Mapping complet class_name YOLO → Infos détaillées
|
||||
_DIAGNOSTICS = {
|
||||
"Tomato___healthy": {
|
||||
"label": "Tomate (saine)",
|
||||
"conseil": "Votre plant est en pleine forme. Pensez au paillage pour garder l'humidité.",
|
||||
"actions": ["Pailler le pied", "Vérifier les gourmands"]
|
||||
},
|
||||
"Tomato___Early_blight": {
|
||||
"label": "Tomate (Alternariose)",
|
||||
"conseil": "Champignon fréquent. Retirez les feuilles basses touchées et évitez de mouiller le feuillage.",
|
||||
"actions": ["Retirer feuilles infectées", "Traitement bouillie bordelaise"]
|
||||
},
|
||||
"Tomato___Late_blight": {
|
||||
"label": "Tomate (Mildiou)",
|
||||
"conseil": "Urgent : Le mildiou se propage vite avec l'humidité. Coupez les parties atteintes immédiatement.",
|
||||
"actions": ["Couper parties infectées", "Traitement purin de prêle", "Abriter de la pluie"]
|
||||
},
|
||||
"Pepper__bell___healthy": {
|
||||
"label": "Poivron (sain)",
|
||||
"conseil": "Le poivron aime la chaleur et un sol riche.",
|
||||
"actions": ["Apport de compost", "Arrosage régulier"]
|
||||
},
|
||||
"Potato___healthy": {
|
||||
"label": "Pomme de terre (saine)",
|
||||
"conseil": "Pensez à butter les pieds pour favoriser la production de tubercules.",
|
||||
"actions": ["Butter les pieds"]
|
||||
},
|
||||
"Grape___healthy": {
|
||||
"label": "Vigne (saine)",
|
||||
"conseil": "Surveillez l'apparition d'oïdium si le temps est chaud et humide.",
|
||||
"actions": ["Taille en vert", "Vérifier sous les feuilles"]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def identify(image_bytes: bytes) -> List[dict]:
|
||||
"""Appelle l'ai-service interne et retourne les détections YOLO."""
|
||||
"""Appelle l'ai-service interne et retourne les détections YOLO avec diagnostics."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
resp = await client.post(
|
||||
@@ -36,10 +56,18 @@ async def identify(image_bytes: bytes) -> List[dict]:
|
||||
results = []
|
||||
for det in data[:3]:
|
||||
cls = det.get("class_name", "")
|
||||
diag = _DIAGNOSTICS.get(cls, {
|
||||
"label": cls.replace("___", " — ").replace("_", " "),
|
||||
"conseil": "Pas de diagnostic spécifique disponible pour cette espèce.",
|
||||
"actions": []
|
||||
})
|
||||
|
||||
results.append({
|
||||
"species": cls.replace("___", " — ").replace("_", " "),
|
||||
"common_name": _NOMS_FR.get(cls, cls.split("___")[0].replace("_", " ")),
|
||||
"species": cls,
|
||||
"common_name": diag["label"],
|
||||
"confidence": det.get("confidence", 0.0),
|
||||
"conseil": diag["conseil"],
|
||||
"actions": diag["actions"],
|
||||
"image_url": "",
|
||||
})
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user