avant codex

This commit is contained in:
2026-02-22 15:05:40 +01:00
parent fed449c784
commit 20af00d653
291 changed files with 51868 additions and 424 deletions
Binary file not shown.
+272
View File
@@ -0,0 +1,272 @@
# consigne.md — Scraping / Collecte des prévisions météo (Open-Meteo)
## Objectif
Mettre en place une collecte **automatique** des données météo de **prévision** (et optionnellement dhistorique) depuis **Open-Meteo** pour alimenter un **calendrier météo** dans lapplication jardin.
Le système doit :
- récupérer les **prévisions** (horizon configurable, ex. 7 à 16 jours)
- stocker les données dans un format exploitable (JSON + SQLite conseillé)
- pouvoir recalculer/mettre à jour chaque jour sans dupliquer inutilement
- gérer correctement le **fuseau Europe/Paris**
- fournir une structure de données stable pour lUI (calendrier jour + détail horaire)
---
## Source de données
### API Open-Meteo (sans clé)
- Endpoint prévisions : `https://api.open-meteo.com/v1/forecast`
- Endpoint historique (optionnel) : `https://api.open-meteo.com/v1/archive`
- Format : JSON
Variables recommandées (prévisions) :
- **hourly** :
- `temperature_2m`
- `precipitation_probability`
- `precipitation`
- `relativehumidity_2m`
- `windspeed_10m`
- `winddirection_10m`
- `cloudcover`
- `weathercode`
- **daily** :
- `temperature_2m_max`
- `temperature_2m_min`
- `precipitation_sum`
- `precipitation_probability_max`
- `windspeed_10m_max`
- `sunrise`
- `sunset`
- `weathercode`
Remarques :
- Toujours inclure `timezone=Europe/Paris`
- Toujours inclure `timeformat=iso8601` (par défaut)
- Conserver l’élévation renvoyée (utile pour contexte jardin)
Documentation :
- https://open-meteo.com/en/docs
---
## Paramètres de localisation
Localisation cible : **Messinhac Bessamorel** (ou proche).
Le système doit permettre :
- configuration par **latitude/longitude**
- stockage de la localisation dans un fichier `config.yml` (ou `.env`)
Exemple :
- `latitude: 45.05`
- `longitude: 3.48`
---
## Requêtes à implémenter
### 1) Prévisions quotidiennes + horaires (une seule requête)
Exemple `curl` (référence) :
```bash
curl "https://api.open-meteo.com/v1/forecast?latitude=45.05&longitude=3.48&hourly=temperature_2m,precipitation_probability,precipitation,relativehumidity_2m,windspeed_10m,winddirection_10m,cloudcover,weathercode&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,windspeed_10m_max,sunrise,sunset,weathercode&timezone=Europe/Paris&forecast_days=16"
Règles :
forecast_days configurable (7/10/16)
si forecast_days absent, Open-Meteo peut renvoyer une valeur par défaut (éviter : toujours préciser)
2) Historique (optionnel) pour “ce qui sest réellement passé”
Exemple curl :
curl "https://api.open-meteo.com/v1/archive?latitude=45.05&longitude=3.48&start_date=2026-01-01&end_date=2026-02-21&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=Europe/Paris"
Règles :
requête par tranche (ex. mensuelle) pour éviter des payloads trop gros
historiser “source=archive” vs “source=forecast”
Exigences de stockage (recommandé)
1) JSON brut (cache)
Conserver la réponse JSON brute pour audit/debug :
data/cache/openmeteo_forecast_<YYYY-MM-DD>.json
Conserver un historique minimal des runs (ex. 30 derniers) :
rotation ou purge automatique
2) SQLite (données normalisées)
Créer 2 tables :
Table meteo_daily
date (YYYY-MM-DD) PRIMARY KEY
tmin_c, tmax_c
precip_mm (cumul)
precip_prob_max_pct
wind_max_kmh
sunrise_local, sunset_local
weathercode
source (forecast|archive)
fetched_at (timestamp)
lat, lon, elevation
Table meteo_hourly
datetime_local (YYYY-MM-DDTHH:MM) PRIMARY KEY
temp_c
precip_mm
precip_prob_pct
humidity_pct
wind_kmh
wind_dir_deg
cloud_pct
weathercode
source (forecast|archive)
fetched_at
lat, lon, elevation
Règles dupsert :
si source=forecast : écraser les mêmes timestamps lors dun nouveau run (les prévisions changent)
si source=archive : ne pas écraser si déjà présent (ou écraser seulement si “qualité” supérieure)
Normalisation / conversions
Open-Meteo renvoie souvent les vitesses en km/h selon endpoint/paramètre, vérifier les unités :
lire hourly_units et daily_units dans la réponse
stocker les unités (au moins en logging)
conserver les dates en timezone Europe/Paris pour affichage calendrier
Plan de collecte (cron)
Prévisions :
fréquence : 1 fois par jour (ex. 06:10 Europe/Paris)
stocker fetched_at pour savoir quand la prévision a été téléchargée
Historique (optionnel) :
1 fois par jour pour la veille (ex. récupérer “hier” en archive) afin de figer le “réel”
ou batch mensuel si besoin de rattrapage
Contrôles qualité (obligatoires)
Après chaque run :
vérifier que daily.time contient au moins 7 jours
vérifier cohérence : tmin <= tmax
vérifier que les tableaux time et temperature_2m ont la même longueur
si champs manquants : log derreur + sauvegarde JSON brute + arrêt gracieux
Sorties attendues pour lUI
Vue calendrier (jour)
Pour chaque jour :
date
Tmin/Tmax
pluie cumulée
probabilité max pluie
icône/code météo (weathercode)
lever/coucher du soleil
badges : gel (tmin <= 0), pluie (precip_mm > 0), vent fort (wind_max_kmh > seuil)
Vue détail (horaire)
Pour une date sélectionnée :
liste des heures (locales)
température + probabilité pluie + pluie mm + vent + humidité + nuages
Notes sur weathercode
Open-Meteo utilise un weathercode (WMO).
Le projet doit :
stocker le code brut
fournir une table de mapping côté UI (code -> libellé FR + icône)
Livrables
scripts/fetch_openmeteo_forecast.py
récupère prévisions + écrit cache JSON + upsert SQLite
scripts/fetch_openmeteo_archive.py (optionnel)
récupère archive sur période + écrit cache JSON + insert SQLite
db/schema.sql
schéma SQLite (tables + index)
config.yml.example
latitude/longitude, timezone, forecast_days, chemins
Commandes de test
Test rapide (curl)
curl -s "https://api.open-meteo.com/v1/forecast?latitude=45.05&longitude=3.48&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=Europe/Paris&forecast_days=7" | head
Test avec jq (si installé)
curl -s "https://api.open-meteo.com/v1/forecast?latitude=45.05&longitude=3.48&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=Europe/Paris&forecast_days=7" | jq '.daily'
Critères dacceptation
Une exécution quotidienne met à jour les prévisions (forecast) sans dupliquer en base.
Le calendrier UI peut afficher au minimum : Tmin/Tmax + pluie + probabilité pluie + code météo.
Le détail horaire dune journée est affichable (température + probabilité pluie).
Les données sont en Europe/Paris (pas de décalage dun jour).
Les fichiers JSON de cache existent pour debug.
https://api.open-meteo.com/v1/forecast?latitude=45.1412&longitude=4.0736&hourly=temperature_2m,weather_code,cloud_cover,evapotranspiration,precipitation,precipitation_probability&forecast_days=14
https://api.open-meteo.com/v1/forecast?latitude=45.1412&longitude=4.0736&hourly=temperature_2m,weather_code,cloud_cover,evapotranspiration,precipitation,precipitation_probability&forecast_days=14
+217
View File
@@ -0,0 +1,217 @@
# Consigne — Collecte météo OpenMeteo (Prévisions + Historique)
## 1. Objectif
Mettre en place une collecte automatique des données OpenMeteo pour alimenter :
- une vue calendrier météo (agrégat journalier),
- une vue détail horaire.
Le système doit :
- récupérer les prévisions sur horizon configurable (`7`, `10`, `16` jours),
- stocker un cache JSON brut (debug/audit),
- upserter en SQLite sans duplication,
- gérer `Europe/Paris` sans décalage de date,
- permettre un mode historique (`archive`) optionnel.
---
## 2. Source de données
- API prévisions : `https://api.open-meteo.com/v1/forecast`
- API historique : `https://api.open-meteo.com/v1/archive`
- Doc : `https://open-meteo.com/en/docs`
Toujours envoyer :
- `timezone=Europe/Paris`
- `timeformat=iso8601`
---
## 3. Configuration
Créer `config.yml` (et `config.yml.example`) :
```yaml
open_meteo:
latitude: 45.05
longitude: 3.48
timezone: Europe/Paris
forecast_days: 14
cache_dir: data/cache
db_path: data/jardin.db
wind_strong_kmh: 40
```
---
## 4. Requête prévisions (obligatoire)
Variables demandées :
- `hourly`: `temperature_2m,precipitation_probability,precipitation,relative_humidity_2m,wind_speed_10m,wind_direction_10m,cloud_cover,weather_code`
- `daily`: `temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,wind_speed_10m_max,sunrise,sunset,weather_code`
Exemple :
```bash
curl -s "https://api.open-meteo.com/v1/forecast?latitude=45.05&longitude=3.48&hourly=temperature_2m,precipitation_probability,precipitation,relative_humidity_2m,wind_speed_10m,wind_direction_10m,cloud_cover,weather_code&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,wind_speed_10m_max,sunrise,sunset,weather_code&timezone=Europe/Paris&timeformat=iso8601&forecast_days=14"
```
Règles :
- `forecast_days` doit toujours être explicite.
- Lire et logger `hourly_units` et `daily_units`.
---
## 5. Requête archive (optionnelle)
Exemple :
```bash
curl -s "https://api.open-meteo.com/v1/archive?latitude=45.05&longitude=3.48&start_date=2026-01-01&end_date=2026-01-31&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&hourly=temperature_2m,precipitation,weather_code&timezone=Europe/Paris&timeformat=iso8601"
```
Règles :
- exécuter par tranche (mensuelle recommandée),
- source marquée `archive`.
---
## 6. Stockage
### 6.1 Cache JSON brut
Fichiers à écrire à chaque run :
- `data/cache/openmeteo_forecast_<YYYY-MM-DDTHHMMSS>.json`
- `data/cache/openmeteo_archive_<YYYY-MM-DDTHHMMSS>.json` (si archive)
Purge :
- conserver les `30` derniers fichiers par type.
### 6.2 SQLite normalisée
Table `meteo_daily` :
- `date TEXT NOT NULL`
- `source TEXT NOT NULL` (`forecast` | `archive`)
- `tmin_c REAL`
- `tmax_c REAL`
- `precip_mm REAL`
- `precip_prob_max_pct REAL`
- `wind_max_kmh REAL`
- `sunrise_local TEXT`
- `sunset_local TEXT`
- `weather_code INTEGER`
- `fetched_at TEXT NOT NULL`
- `lat REAL NOT NULL`
- `lon REAL NOT NULL`
- `elevation REAL`
- `PRIMARY KEY (date, source)`
Table `meteo_hourly` :
- `datetime_local TEXT NOT NULL` (`YYYY-MM-DDTHH:MM`)
- `source TEXT NOT NULL` (`forecast` | `archive`)
- `temp_c REAL`
- `precip_mm REAL`
- `precip_prob_pct REAL`
- `humidity_pct REAL`
- `wind_kmh REAL`
- `wind_dir_deg REAL`
- `cloud_pct REAL`
- `weather_code INTEGER`
- `fetched_at TEXT NOT NULL`
- `lat REAL NOT NULL`
- `lon REAL NOT NULL`
- `elevation REAL`
- `PRIMARY KEY (datetime_local, source)`
Index recommandés :
- `idx_meteo_daily_date ON meteo_daily(date)`
- `idx_meteo_hourly_dt ON meteo_hourly(datetime_local)`
---
## 7. Règles dupsert
- `forecast` : `INSERT ... ON CONFLICT (...) DO UPDATE` (les prévisions évoluent).
- `archive` : `INSERT ... ON CONFLICT (...) DO NOTHING` (on fige lobservé).
Conflits :
- `meteo_daily` : `(date, source)`
- `meteo_hourly` : `(datetime_local, source)`
---
## 8. Contrôles qualité obligatoires
Après récupération JSON :
- `daily.time` contient au moins `7` jours,
- longueurs cohérentes des tableaux `hourly`,
- pour chaque jour : `tmin_c <= tmax_c`,
- présence de `timezone == Europe/Paris`.
En cas d’échec :
- logger erreur explicite,
- conserver le JSON brut,
- sortie non fatale avec code derreur contrôlé.
---
## 9. Champs attendus pour lUI
### Vue calendrier (jour)
- `date`
- `tmin_c`, `tmax_c`
- `precip_mm`
- `precip_prob_max_pct`
- `weather_code`
- `sunrise_local`, `sunset_local`
- badges calculés :
- `gel` si `tmin_c <= 0`
- `pluie` si `precip_mm > 0`
- `vent_fort` si `wind_max_kmh >= wind_strong_kmh`
### Vue détail (horaire)
- `datetime_local`
- `temp_c`
- `precip_prob_pct`
- `precip_mm`
- `wind_kmh`, `wind_dir_deg`
- `humidity_pct`
- `cloud_pct`
- `weather_code`
---
## 10. Livrables
- `prevision meteo/scripts/fetch_openmeteo_forecast.py`
- `prevision meteo/scripts/fetch_openmeteo_archive.py` (optionnel)
- `prevision meteo/db/schema.sql`
- `prevision meteo/config.yml.example`
---
## 11. Exécution planifiée
- Prévisions : tous les jours à `06:10` Europe/Paris.
- Archive (optionnel) : tous les jours à `06:20` pour `J-1`.
Exemple cron :
```cron
10 6 * * * /usr/bin/python3 /path/prevision\ meteo/scripts/fetch_openmeteo_forecast.py
20 6 * * * /usr/bin/python3 /path/prevision\ meteo/scripts/fetch_openmeteo_archive.py --yesterday
```
---
## 12. Critères dacceptation
- Une exécution quotidienne met à jour `forecast` sans duplication.
- Le calendrier UI affiche au minimum : Tmin/Tmax, pluie, probabilité pluie, weather code.
- Le détail horaire est consultable pour un jour donné.
- Les dates/heures sont correctes en `Europe/Paris`.
- Les caches JSON sont présents et exploitables pour debug.
https://api.open-meteo.com/v1/forecast?latitude=45.1412&longitude=4.0736&hourly=temperature_2m,weather_code,cloud_cover,evapotranspiration,precipitation,precipitation_probability,rain,snowfall,wind_speed_10m,relative_humidity_2m,wind_direction_10m,soil_temperature_0cm,soil_temperature_6cm,soil_moisture_1_to_3cm,soil_moisture_3_to_9cm,sunshine_duration&forecast_days=14
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
import argparse
import json
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlencode
import openmeteo_requests
import requests_cache
from retry_requests import retry
WMO_LABELS = {
0: "Clair", 1: "Plutôt clair", 2: "Partiellement nuageux", 3: "Couvert",
45: "Brouillard", 48: "Brouillard givrant", 51: "Bruine légère", 53: "Bruine modérée",
55: "Bruine dense", 61: "Pluie légère", 63: "Pluie modérée", 65: "Pluie forte",
71: "Neige légère", 73: "Neige modérée", 75: "Neige forte", 80: "Averses légères",
81: "Averses modérées", 82: "Averses fortes", 95: "Orage",
}
HOURLY_FIELDS = [
"temperature_2m", "weather_code", "cloud_cover", "evapotranspiration", "precipitation",
"precipitation_probability", "rain", "snowfall", "wind_speed_10m", "relative_humidity_2m",
"wind_direction_10m", "soil_temperature_0cm", "soil_temperature_6cm",
"soil_moisture_1_to_3cm", "soil_moisture_3_to_9cm", "sunshine_duration", "freezing_level_height",
]
CURRENT_FIELDS = [
"temperature_2m", "is_day", "snowfall", "showers", "rain", "precipitation",
"weather_code", "cloud_cover", "relative_humidity_2m", "apparent_temperature",
]
def build_url(lat: float, lon: float, days: int, past_days: int, tz: str) -> str:
params = {
"latitude": lat,
"longitude": lon,
"hourly": ",".join(HOURLY_FIELDS),
"current": ",".join(CURRENT_FIELDS),
"past_days": past_days,
"forecast_days": days,
"timezone": tz,
}
return f"https://api.open-meteo.com/v1/forecast?{urlencode(params)}"
def _unit_of(variable) -> str:
unit_fn = getattr(variable, "Unit", None)
if callable(unit_fn):
return unit_fn() or ""
return ""
def _to_json_safe(value):
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
return value
def _iso_times(start_s: int, end_s: int, interval_s: int, utc_offset_s: int) -> list[str]:
out: list[str] = []
current = start_s
while current < end_s:
local_epoch = current + utc_offset_s
dt = datetime.fromtimestamp(local_epoch, tz=timezone.utc)
out.append(dt.strftime("%Y-%m-%dT%H:%M"))
current += interval_s
return out
def fetch_data(lat: float, lon: float, days: int, past_days: int, tz: str) -> dict:
cache_session = requests_cache.CachedSession(".cache", expire_after=3600)
retry_session = retry(cache_session, retries=5, backoff_factor=0.2)
client = openmeteo_requests.Client(session=retry_session)
params = {
"latitude": lat,
"longitude": lon,
"hourly": HOURLY_FIELDS,
"current": CURRENT_FIELDS,
"past_days": past_days,
"timezone": tz,
"forecast_days": days,
}
responses = client.weather_api("https://api.open-meteo.com/v1/forecast", params=params)
response = responses[0]
hourly = response.Hourly()
hourly_time = _iso_times(
hourly.Time(),
hourly.TimeEnd(),
hourly.Interval(),
response.UtcOffsetSeconds(),
)
hourly_payload: dict[str, list] = {"time": hourly_time}
hourly_units: dict[str, str] = {}
for idx, field in enumerate(HOURLY_FIELDS):
variable = hourly.Variables(idx)
hourly_payload[field] = variable.ValuesAsNumpy().tolist()
hourly_units[field] = _unit_of(variable)
current = response.Current()
current_payload: dict[str, float | int | None] = {}
current_units: dict[str, str] = {}
for idx, field in enumerate(CURRENT_FIELDS):
variable = current.Variables(idx)
current_payload[field] = variable.Value()
current_units[field] = _unit_of(variable)
current_payload["time"] = datetime.fromtimestamp(
current.Time() + response.UtcOffsetSeconds(),
tz=timezone.utc,
).strftime("%Y-%m-%dT%H:%M")
metadata = {
"latitude": response.Latitude(),
"longitude": response.Longitude(),
"elevation": response.Elevation(),
"timezone": _to_json_safe(response.Timezone()),
"timezone_abbr": _to_json_safe(response.TimezoneAbbreviation()),
"utc_offset_seconds": response.UtcOffsetSeconds(),
}
return {
"hourly": hourly_payload,
"hourly_units": hourly_units,
"current": current_payload,
"current_units": current_units,
"metadata": metadata,
}
def _clean(vals):
return [v for v in vals if v is not None]
def _avg(vals, ndigits=1):
c = _clean(vals)
return round(sum(c) / len(c), ndigits) if c else None
def _sum(vals, ndigits=1):
c = _clean(vals)
return round(sum(c), ndigits) if c else 0.0
def summarize(data: dict) -> list[dict]:
h = data["hourly"]
days = defaultdict(lambda: defaultdict(list))
for i, iso in enumerate(h["time"]):
day = iso.split("T", 1)[0]
for k in HOURLY_FIELDS:
days[day][k].append(h[k][i])
out = []
for day in sorted(days.keys()):
d = days[day]
codes = d["weather_code"]
dom_code = max(set(codes), key=codes.count)
tvals = _clean(d["temperature_2m"])
out.append({
"date": day,
"t_min": min(tvals) if tvals else None,
"t_max": max(tvals) if tvals else None,
"pluie_total_mm": _sum(d["precipitation"], 1),
"pluie_mm": _sum(d["rain"], 1),
"neige_cm": _sum(d["snowfall"], 1),
"proba_pluie_max": max(_clean(d["precipitation_probability"])) if _clean(d["precipitation_probability"]) else None,
"humidite_moy": _avg(d["relative_humidity_2m"], 0),
"vent_moy_kmh": _avg(d["wind_speed_10m"], 1),
"vent_dir_moy_deg": _avg(d["wind_direction_10m"], 0),
"nuages_moy": _avg(d["cloud_cover"], 0),
"sol_0cm_moy": _avg(d["soil_temperature_0cm"], 1),
"sol_6cm_moy": _avg(d["soil_temperature_6cm"], 1),
"hum_sol_1_3_moy": _avg(d["soil_moisture_1_to_3cm"], 3),
"hum_sol_3_9_moy": _avg(d["soil_moisture_3_to_9cm"], 3),
"ensoleillement_h": round((_sum(d["sunshine_duration"], 0)) / 3600, 1),
"isotherme_0m_moy": _avg(d["freezing_level_height"], 0),
"etp_mm": _sum(d["evapotranspiration"], 2),
"wmo": dom_code,
"meteo": WMO_LABELS.get(dom_code, f"Code {dom_code}"),
})
return out
def _fmt(v, spec):
if v is None:
return " n/a"
return format(v, spec)
def print_table(rows: list[dict], units: dict[str, str]) -> None:
t_unit = units.get("temperature_2m", "°C")
precip_unit = units.get("precipitation", "mm")
rain_unit = units.get("rain", "mm")
snow_unit = units.get("snowfall", "cm")
pprob_unit = units.get("precipitation_probability", "%")
wind_unit = units.get("wind_speed_10m", "km/h")
wdir_unit = units.get("wind_direction_10m", "°")
hum_unit = units.get("relative_humidity_2m", "%")
cloud_unit = units.get("cloud_cover", "%")
soil0_unit = units.get("soil_temperature_0cm", "°C")
soil6_unit = units.get("soil_temperature_6cm", "°C")
soilm13_unit = units.get("soil_moisture_1_to_3cm", "m³/m³")
soilm39_unit = units.get("soil_moisture_3_to_9cm", "m³/m³")
etp_unit = units.get("evapotranspiration", "mm")
freeze_unit = units.get("freezing_level_height", "m")
print(
f"{'Date':<10} | {'Tmin':>6} | {'Tmax':>6} | {'Precip':>7} | {'Rain':>6} | {'Snow':>6} | "
f"{'Pmax':>5} | {'Hum':>5} | {'Wind':>6} | {'Dir':>4} | {'Cloud':>5} | "
f"{'Soil0':>6} | {'Soil6':>6} | {'SM1-3':>7} | {'SM3-9':>7} | {'Sun(h)':>6} | "
f"{'ETP':>6} | {'Iso0':>6} | Meteo"
)
print(
f"{'':<10} | {t_unit:>6} | {t_unit:>6} | {precip_unit:>7} | {rain_unit:>6} | {snow_unit:>6} | "
f"{pprob_unit:>5} | {hum_unit:>5} | {wind_unit:>6} | {wdir_unit:>4} | {cloud_unit:>5} | "
f"{soil0_unit:>6} | {soil6_unit:>6} | {soilm13_unit:>7} | {soilm39_unit:>7} | {'h':>6} | "
f"{etp_unit:>6} | {freeze_unit:>6} |"
)
print("-" * 245)
for r in rows:
print(
f"{r['date']:<10} | {_fmt(r['t_min'], '>6.1f')} | {_fmt(r['t_max'], '>6.1f')} | {_fmt(r['pluie_total_mm'], '>7.1f')} | "
f"{_fmt(r['pluie_mm'], '>6.1f')} | {_fmt(r['neige_cm'], '>6.1f')} | {_fmt(r['proba_pluie_max'], '>5.0f')} | "
f"{_fmt(r['humidite_moy'], '>5.0f')} | {_fmt(r['vent_moy_kmh'], '>6.1f')} | {_fmt(r['vent_dir_moy_deg'], '>4.0f')} | "
f"{_fmt(r['nuages_moy'], '>5.0f')} | {_fmt(r['sol_0cm_moy'], '>6.1f')} | {_fmt(r['sol_6cm_moy'], '>6.1f')} | "
f"{_fmt(r['hum_sol_1_3_moy'], '>7.3f')} | {_fmt(r['hum_sol_3_9_moy'], '>7.3f')} | {_fmt(r['ensoleillement_h'], '>6.1f')} | "
f"{_fmt(r['etp_mm'], '>6.2f')} | {_fmt(r['isotherme_0m_moy'], '>6.0f')} | {r['meteo']}"
)
def print_current(current: dict, current_units: dict) -> None:
print("\nCurrent")
print("-------")
order = [
"time",
"temperature_2m",
"apparent_temperature",
"is_day",
"weather_code",
"cloud_cover",
"relative_humidity_2m",
"precipitation",
"rain",
"showers",
"snowfall",
]
for key in order:
if key not in current:
continue
unit = current_units.get(key, "")
suffix = f" {unit}" if unit else ""
print(f"{key}: {current[key]}{suffix}")
def main() -> int:
ap = argparse.ArgumentParser(description="Résumé jardinier Open-Meteo (journalier)")
ap.add_argument("--lat", type=float, default=45.1412)
ap.add_argument("--lon", type=float, default=4.0736)
ap.add_argument("--days", type=int, default=14)
ap.add_argument("--past-days", type=int, default=7)
ap.add_argument("--timezone", default="auto")
ap.add_argument("--json", action="store_true", help="Sortie JSON")
ap.add_argument("--json-out", help="Chemin de fichier JSON de sortie")
args = ap.parse_args()
url = build_url(args.lat, args.lon, args.days, args.past_days, args.timezone)
data = fetch_data(args.lat, args.lon, args.days, args.past_days, args.timezone)
rows = summarize(data)
units = data.get("hourly_units", {})
payload = {
"url": url,
"metadata": data.get("metadata", {}),
"units": units,
"current_units": data.get("current_units", {}),
"current": data.get("current", {}),
"days": rows,
}
if args.json_out:
out_path = Path(args.json_out)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"JSON écrit: {out_path}")
if args.json:
print(json.dumps(payload, ensure_ascii=False, indent=2))
else:
print(f"Source: {url}")
print(f"Location: {data.get('metadata', {}).get('latitude')}N {data.get('metadata', {}).get('longitude')}E | Elevation: {data.get('metadata', {}).get('elevation')} m")
print(f"Timezone: {data.get('metadata', {}).get('timezone')} ({data.get('metadata', {}).get('timezone_abbr')})")
print_current(data.get("current", {}), data.get("current_units", {}))
print_table(rows, units)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,519 @@
{
"url": "https://api.open-meteo.com/v1/forecast?latitude=45.1412&longitude=4.0736&hourly=temperature_2m%2Cweather_code%2Ccloud_cover%2Cevapotranspiration%2Cprecipitation%2Cprecipitation_probability%2Crain%2Csnowfall%2Cwind_speed_10m%2Crelative_humidity_2m%2Cwind_direction_10m%2Csoil_temperature_0cm%2Csoil_temperature_6cm%2Csoil_moisture_1_to_3cm%2Csoil_moisture_3_to_9cm%2Csunshine_duration%2Cfreezing_level_height&current=temperature_2m%2Cis_day%2Csnowfall%2Cshowers%2Crain%2Cprecipitation%2Cweather_code%2Ccloud_cover%2Crelative_humidity_2m%2Capparent_temperature&past_days=7&forecast_days=14&timezone=auto",
"metadata": {
"latitude": 45.13999938964844,
"longitude": 4.0799994468688965,
"elevation": 891.0,
"timezone": "Europe/Paris",
"timezone_abbr": "GMT+1",
"utc_offset_seconds": 3600
},
"units": {
"temperature_2m": 1,
"weather_code": 40,
"cloud_cover": 35,
"evapotranspiration": 32,
"precipitation": 32,
"precipitation_probability": 35,
"rain": 32,
"snowfall": 2,
"wind_speed_10m": 24,
"relative_humidity_2m": 35,
"wind_direction_10m": 5,
"soil_temperature_0cm": 1,
"soil_temperature_6cm": 1,
"soil_moisture_1_to_3cm": 3,
"soil_moisture_3_to_9cm": 3,
"sunshine_duration": 36,
"freezing_level_height": 29
},
"current_units": {
"temperature_2m": 1,
"is_day": 6,
"snowfall": 2,
"showers": 32,
"rain": 32,
"precipitation": 32,
"weather_code": 40,
"cloud_cover": 35,
"relative_humidity_2m": 35,
"apparent_temperature": 1
},
"current": {
"temperature_2m": 0.762499988079071,
"is_day": 1.0,
"snowfall": 0.0,
"showers": 0.0,
"rain": 0.0,
"precipitation": 0.0,
"weather_code": 2.0,
"cloud_cover": 52.0,
"relative_humidity_2m": 95.0,
"apparent_temperature": -1.9485669136047363,
"time": "2026-02-22T08:00"
},
"days": [
{
"date": "2026-02-15",
"t_min": -5.1875,
"t_max": 6.162499904632568,
"pluie_total_mm": 1.6,
"pluie_mm": 1.6,
"neige_cm": 0.0,
"proba_pluie_max": 95.0,
"humidite_moy": 82.0,
"vent_moy_kmh": 17.4,
"vent_dir_moy_deg": 272.0,
"nuages_moy": 91.0,
"sol_0cm_moy": 0.5,
"sol_6cm_moy": 0.9,
"hum_sol_1_3_moy": 0.293,
"hum_sol_3_9_moy": 0.291,
"ensoleillement_h": 6.7,
"isotherme_0m_moy": 1375.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-02-16",
"t_min": 2.362499952316284,
"t_max": 6.262499809265137,
"pluie_total_mm": 16.5,
"pluie_mm": 16.3,
"neige_cm": 0.0,
"proba_pluie_max": 100.0,
"humidite_moy": 83.0,
"vent_moy_kmh": 17.9,
"vent_dir_moy_deg": 262.0,
"nuages_moy": 98.0,
"sol_0cm_moy": 4.0,
"sol_6cm_moy": 3.9,
"hum_sol_1_3_moy": 0.32,
"hum_sol_3_9_moy": 0.319,
"ensoleillement_h": 1.5,
"isotherme_0m_moy": 1631.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-02-17",
"t_min": 0.612500011920929,
"t_max": 6.5625,
"pluie_total_mm": 0.3,
"pluie_mm": 0.3,
"neige_cm": 0.0,
"proba_pluie_max": 55.0,
"humidite_moy": 74.0,
"vent_moy_kmh": 11.3,
"vent_dir_moy_deg": 264.0,
"nuages_moy": 83.0,
"sol_0cm_moy": 2.8,
"sol_6cm_moy": 3.0,
"hum_sol_1_3_moy": 0.291,
"hum_sol_3_9_moy": 0.298,
"ensoleillement_h": 7.1,
"isotherme_0m_moy": 1234.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-02-18",
"t_min": 1.2625000476837158,
"t_max": 10.762499809265137,
"pluie_total_mm": 2.8,
"pluie_mm": 2.8,
"neige_cm": 0.0,
"proba_pluie_max": 100.0,
"humidite_moy": 73.0,
"vent_moy_kmh": 18.0,
"vent_dir_moy_deg": 210.0,
"nuages_moy": 96.0,
"sol_0cm_moy": 5.4,
"sol_6cm_moy": 4.8,
"hum_sol_1_3_moy": 0.276,
"hum_sol_3_9_moy": 0.281,
"ensoleillement_h": 8.1,
"isotherme_0m_moy": 2064.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-02-19",
"t_min": 2.2125000953674316,
"t_max": 6.0625,
"pluie_total_mm": 8.3,
"pluie_mm": 7.8,
"neige_cm": 0.0,
"proba_pluie_max": 95.0,
"humidite_moy": 78.0,
"vent_moy_kmh": 25.8,
"vent_dir_moy_deg": 248.0,
"nuages_moy": 95.0,
"sol_0cm_moy": 4.0,
"sol_6cm_moy": 4.3,
"hum_sol_1_3_moy": 0.295,
"hum_sol_3_9_moy": 0.291,
"ensoleillement_h": 6.5,
"isotherme_0m_moy": 1392.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-02-20",
"t_min": 1.8624999523162842,
"t_max": 5.962499618530273,
"pluie_total_mm": 0.2,
"pluie_mm": 0.2,
"neige_cm": 0.0,
"proba_pluie_max": 73.0,
"humidite_moy": 80.0,
"vent_moy_kmh": 14.4,
"vent_dir_moy_deg": 318.0,
"nuages_moy": 90.0,
"sol_0cm_moy": 3.7,
"sol_6cm_moy": 4.0,
"hum_sol_1_3_moy": 0.292,
"hum_sol_3_9_moy": 0.295,
"ensoleillement_h": 5.4,
"isotherme_0m_moy": 1236.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-02-21",
"t_min": 2.1625001430511475,
"t_max": 10.16249942779541,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 0.0,
"humidite_moy": 82.0,
"vent_moy_kmh": 5.8,
"vent_dir_moy_deg": 258.0,
"nuages_moy": 58.0,
"sol_0cm_moy": 4.9,
"sol_6cm_moy": 4.6,
"hum_sol_1_3_moy": 0.271,
"hum_sol_3_9_moy": 0.276,
"ensoleillement_h": 9.2,
"isotherme_0m_moy": 2190.0,
"etp_mm": 0.0,
"wmo": 2.0,
"meteo": "Partiellement nuageux"
},
{
"date": "2026-02-22",
"t_min": 0.762499988079071,
"t_max": 13.012499809265137,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 0.0,
"humidite_moy": 80.0,
"vent_moy_kmh": 4.3,
"vent_dir_moy_deg": 235.0,
"nuages_moy": 79.0,
"sol_0cm_moy": 5.4,
"sol_6cm_moy": 5.1,
"hum_sol_1_3_moy": 0.261,
"hum_sol_3_9_moy": 0.267,
"ensoleillement_h": 9.2,
"isotherme_0m_moy": 3118.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-02-23",
"t_min": 4.362499713897705,
"t_max": 11.762499809265137,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 3.0,
"humidite_moy": 75.0,
"vent_moy_kmh": 6.4,
"vent_dir_moy_deg": 214.0,
"nuages_moy": 80.0,
"sol_0cm_moy": 6.7,
"sol_6cm_moy": 6.2,
"hum_sol_1_3_moy": 0.254,
"hum_sol_3_9_moy": 0.261,
"ensoleillement_h": 7.5,
"isotherme_0m_moy": 2597.0,
"etp_mm": 0.0,
"wmo": 2.0,
"meteo": "Partiellement nuageux"
},
{
"date": "2026-02-24",
"t_min": 4.001999855041504,
"t_max": 14.402000427246094,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 0.0,
"humidite_moy": 86.0,
"vent_moy_kmh": 3.1,
"vent_dir_moy_deg": 164.0,
"nuages_moy": 57.0,
"sol_0cm_moy": 7.1,
"sol_6cm_moy": 6.6,
"hum_sol_1_3_moy": 0.304,
"hum_sol_3_9_moy": 0.306,
"ensoleillement_h": 9.7,
"isotherme_0m_moy": 3117.0,
"etp_mm": 0.0,
"wmo": 2.0,
"meteo": "Partiellement nuageux"
},
{
"date": "2026-02-25",
"t_min": 5.202000141143799,
"t_max": 14.85200023651123,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 0.0,
"humidite_moy": 67.0,
"vent_moy_kmh": 7.6,
"vent_dir_moy_deg": 194.0,
"nuages_moy": 69.0,
"sol_0cm_moy": 7.6,
"sol_6cm_moy": 7.2,
"hum_sol_1_3_moy": 0.312,
"hum_sol_3_9_moy": 0.314,
"ensoleillement_h": 10.0,
"isotherme_0m_moy": 3335.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-02-26",
"t_min": 5.052000045776367,
"t_max": 17.00200080871582,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 2.0,
"humidite_moy": 56.0,
"vent_moy_kmh": 5.4,
"vent_dir_moy_deg": 207.0,
"nuages_moy": 15.0,
"sol_0cm_moy": 8.3,
"sol_6cm_moy": 7.7,
"hum_sol_1_3_moy": 0.31,
"hum_sol_3_9_moy": 0.312,
"ensoleillement_h": 10.1,
"isotherme_0m_moy": 2972.0,
"etp_mm": 0.0,
"wmo": 0.0,
"meteo": "Clair"
},
{
"date": "2026-02-27",
"t_min": 5.2860002517700195,
"t_max": 17.38599967956543,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 20.0,
"humidite_moy": 66.0,
"vent_moy_kmh": 7.4,
"vent_dir_moy_deg": 171.0,
"nuages_moy": 16.0,
"sol_0cm_moy": 8.8,
"sol_6cm_moy": 8.3,
"hum_sol_1_3_moy": 0.294,
"hum_sol_3_9_moy": 0.296,
"ensoleillement_h": 10.1,
"isotherme_0m_moy": 2904.0,
"etp_mm": 0.0,
"wmo": 0.0,
"meteo": "Clair"
},
{
"date": "2026-02-28",
"t_min": 4.836000442504883,
"t_max": 12.886000633239746,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 31.0,
"humidite_moy": 82.0,
"vent_moy_kmh": 5.6,
"vent_dir_moy_deg": 207.0,
"nuages_moy": 73.0,
"sol_0cm_moy": 7.7,
"sol_6cm_moy": 7.6,
"hum_sol_1_3_moy": 0.293,
"hum_sol_3_9_moy": 0.295,
"ensoleillement_h": 6.8,
"isotherme_0m_moy": 2218.0,
"etp_mm": 0.0,
"wmo": 2.0,
"meteo": "Partiellement nuageux"
},
{
"date": "2026-03-01",
"t_min": 6.5360002517700195,
"t_max": 11.947500228881836,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 32.0,
"humidite_moy": 72.0,
"vent_moy_kmh": 11.7,
"vent_dir_moy_deg": 186.0,
"nuages_moy": 73.0,
"sol_0cm_moy": NaN,
"sol_6cm_moy": NaN,
"hum_sol_1_3_moy": NaN,
"hum_sol_3_9_moy": NaN,
"ensoleillement_h": 6.6,
"isotherme_0m_moy": 2229.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-03-02",
"t_min": 2.8975000381469727,
"t_max": 12.697500228881836,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 19.0,
"humidite_moy": 64.0,
"vent_moy_kmh": 11.6,
"vent_dir_moy_deg": 161.0,
"nuages_moy": 80.0,
"sol_0cm_moy": NaN,
"sol_6cm_moy": NaN,
"hum_sol_1_3_moy": NaN,
"hum_sol_3_9_moy": NaN,
"ensoleillement_h": 10.2,
"isotherme_0m_moy": 2206.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-03-03",
"t_min": 6.047499656677246,
"t_max": 11.697500228881836,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 29.0,
"humidite_moy": 69.0,
"vent_moy_kmh": 14.1,
"vent_dir_moy_deg": 152.0,
"nuages_moy": 99.0,
"sol_0cm_moy": NaN,
"sol_6cm_moy": NaN,
"hum_sol_1_3_moy": NaN,
"hum_sol_3_9_moy": NaN,
"ensoleillement_h": 9.3,
"isotherme_0m_moy": 2141.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-03-04",
"t_min": 4.147500038146973,
"t_max": 13.297500610351562,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 29.0,
"humidite_moy": 80.0,
"vent_moy_kmh": 4.4,
"vent_dir_moy_deg": 108.0,
"nuages_moy": 74.0,
"sol_0cm_moy": NaN,
"sol_6cm_moy": NaN,
"hum_sol_1_3_moy": NaN,
"hum_sol_3_9_moy": NaN,
"ensoleillement_h": 9.1,
"isotherme_0m_moy": 2200.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-03-05",
"t_min": 3.3975000381469727,
"t_max": 12.197500228881836,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 29.0,
"humidite_moy": 88.0,
"vent_moy_kmh": 4.2,
"vent_dir_moy_deg": 138.0,
"nuages_moy": 79.0,
"sol_0cm_moy": NaN,
"sol_6cm_moy": NaN,
"hum_sol_1_3_moy": NaN,
"hum_sol_3_9_moy": NaN,
"ensoleillement_h": 9.6,
"isotherme_0m_moy": 2821.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-03-06",
"t_min": 4.547499656677246,
"t_max": 11.697500228881836,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 42.0,
"humidite_moy": 85.0,
"vent_moy_kmh": 3.9,
"vent_dir_moy_deg": 196.0,
"nuages_moy": 98.0,
"sol_0cm_moy": NaN,
"sol_6cm_moy": NaN,
"hum_sol_1_3_moy": NaN,
"hum_sol_3_9_moy": NaN,
"ensoleillement_h": 9.4,
"isotherme_0m_moy": 2344.0,
"etp_mm": 0.0,
"wmo": 3.0,
"meteo": "Couvert"
},
{
"date": "2026-03-07",
"t_min": 0.5475000143051147,
"t_max": 11.09749984741211,
"pluie_total_mm": 0.0,
"pluie_mm": 0.0,
"neige_cm": 0.0,
"proba_pluie_max": 29.0,
"humidite_moy": 83.0,
"vent_moy_kmh": 5.2,
"vent_dir_moy_deg": 177.0,
"nuages_moy": 58.0,
"sol_0cm_moy": NaN,
"sol_6cm_moy": NaN,
"hum_sol_1_3_moy": NaN,
"hum_sol_3_9_moy": NaN,
"ensoleillement_h": 10.3,
"isotherme_0m_moy": 2071.0,
"etp_mm": 0.0,
"wmo": 1.0,
"meteo": "Plutôt clair"
}
]
}
+56
View File
@@ -0,0 +1,56 @@
{
"WMO_Weather_Interpretation_Codes": [
{
"codes": [0],
"description_fr": "Ciel clair"
},
{
"codes": [1, 2, 3],
"description_fr": "Principalement clair, partiellement nuageux ou couvert"
},
{
"codes": [45, 48],
"description_fr": "Brouillard et brouillard givrant"
},
{
"codes": [51, 53, 55],
"description_fr": "Bruine : légère, modérée ou dense"
},
{
"codes": [56, 57],
"description_fr": "Bruine verglaçante : légère ou dense"
},
{
"codes": [61, 63, 65],
"description_fr": "Pluie : légère, modérée ou forte"
},
{
"codes": [66, 67],
"description_fr": "Pluie verglaçante : légère ou forte"
},
{
"codes": [71, 73, 75],
"description_fr": "Chute de neige : faible, modérée ou forte"
},
{
"codes": [77],
"description_fr": "Neige en grains"
},
{
"codes": [80, 81, 82],
"description_fr": "Averses de pluie : légère, modérée ou violente"
},
{
"codes": [85, 86],
"description_fr": "Averses de neige : légère ou forte"
},
{
"codes": [95],
"description_fr": "Orage : léger ou modéré"
},
{
"codes": [96, 99],
"description_fr": "Orage avec grêle : légère ou forte"
}
]
}