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

82
.claude/settings.json Normal file
View File

@@ -0,0 +1,82 @@
{
"permissions": {
"allow": [
"Bash(npm install:*)",
"Bash(npm run build:*)",
"Bash(git -C:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/stores/gardens.ts:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/stores/varieties.ts:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/stores/plantings.ts:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/stores/tasks.ts:*)",
"Bash(npm run lint:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/tsconfig.json:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/router/index.ts:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/DashboardView.vue:*)",
"Bash(__NEW_LINE_c59ff40ee569d295__ cat)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/JardinsView.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/JardinDetailView.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/VarietesView.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/PlantationsView.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/PlanningView.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/TachesView.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/LunaireView.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/views/ReglagesView.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/components/AppHeader.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/components/AppDrawer.vue:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/App.vue:*)",
"Bash(__NEW_LINE_a74f82e515f33f67__ cat)",
"Bash(/home/gilles/Documents/vscode/jardin/README.md:*)",
"Bash(docker compose up:*)",
"Bash(python3:*)",
"Bash(python -m pytest:*)",
"Read(//usr/bin/**)",
"Bash(pip3 install:*)",
"Bash(ls:*)",
"Bash(lsof:*)",
"Bash(curl:*)",
"Bash(npm run dev:*)",
"Bash(ss:*)",
"Bash(docker ps:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/app/migrate.py:*)",
"Bash(docker compose stop:*)",
"Bash(docker compose start:*)",
"Bash(docker stop:*)",
"Bash(docker start:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/app/models/planting.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/app/routers/plantings.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/app/models/task.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/app/routers/tasks.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/ai-service/requirements.txt:*)",
"Bash(/home/gilles/Documents/vscode/jardin/ai-service/main.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/ai-service/Dockerfile:*)",
"Bash(/home/gilles/Documents/vscode/jardin/docker-compose.yml:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/app/services/plantnet.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/app/services/yolo_service.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/.env.example:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/app/models/media.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/backend/tests/test_identify.py:*)",
"Bash(/home/gilles/Documents/vscode/jardin/frontend/src/components/PhotoIdentifyModal.vue:*)",
"Bash(pipx install:*)",
"Read(//tmp/jardin_screenshots/**)",
"Read(//tmp/**)",
"Read(//home/gilles/.claude/plugins/cache/claude-plugins-official/superpowers/4.3.1/skills/subagent-driven-development/**)",
"Bash(python:*)",
"Bash(sqlite3:*)",
"Bash(find:*)",
"Bash(.venv/bin/python:*)",
"Bash(.venv/bin/pip install:*)",
"Bash(grep:*)"
],
"additionalDirectories": [
"/home/gilles/Documents/vscode/jardin/frontend/src",
"/home/gilles/Documents/vscode/jardin/frontend/src/router",
"/home/gilles/Documents/vscode/jardin/frontend/src/api",
"/home/gilles/Documents/vscode/jardin",
"/home/gilles/Documents/vscode/jardin/backend/app/models",
"/home/gilles/Documents/vscode/jardin/backend/app/routers",
"/home/gilles/Documents/vscode/jardin/frontend/src/views",
"/home/gilles/Documents/vscode/jardin/frontend/src/components",
"/home/gilles/.claude/projects/-home-gilles-Documents-vscode-jardin/memory"
]
}
}

2
.gitignore vendored
View File

@@ -3,8 +3,6 @@
**/*.pyo **/*.pyo
.env* .env*
!.env.example !.env.example
data/*.db
data/uploads/
backend/.venv/ backend/.venv/
frontend/node_modules/ frontend/node_modules/
frontend/dist/ frontend/dist/

130
CLAUDE.md Normal file
View File

@@ -0,0 +1,130 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Projet
Application web de **gestion de jardins** (potager, serre, plein air), self-hosted, mobile-first, entièrement **en français**. Thème visuel : **Gruvbox Dark "seventies"** (vintage, chaleureux, contrasté).
## Architecture cible
### Backend
- **Python FastAPI** + SQLModel (SQLAlchemy en dessous)
- **SQLite** par défaut (volume Docker persistant), migration future vers PostgreSQL prévue
- Stockage images : local `/data/uploads` + métadonnées en DB
- API REST documentée (OpenAPI auto-générée par FastAPI)
### Frontend
- **Vue 3 + Vite** (alternative React acceptable)
- **Tailwind CSS** avec thème Gruvbox Dark personnalisé
- Mobile-first, PWA en phase 2
### Déploiement
- **Docker Compose** : service `backend` + service `frontend` (static) + volumes `db` et `uploads`
## Commandes de développement
Une fois le projet créé, les commandes attendues seront :
```bash
# Lancer tout l'environnement
docker compose up --build
# Backend seul (développement)
cd backend && uvicorn app.main:app --reload
# Frontend seul (développement)
cd frontend && npm run dev
# Tests backend
cd backend && pytest
# Tests un seul fichier
cd backend && pytest tests/test_gardens.py -v
# Lint backend
cd backend && ruff check . && mypy .
# Lint frontend
cd frontend && npm run lint
```
## Modèle de données (tables MVP)
| Table | Rôle |
|---|---|
| `gardens` | Jardins (nom, type, coordonnées, exposition, sol) |
| `garden_cells` | Cases de la grille 2D du jardin |
| `garden_images` | Photos associées à un jardin |
| `measurements` | Relevés temp/humidité (air + sol) |
| `plant_varieties` | Catalogue de variétés (référence) |
| `plant_images` | Photos de variétés |
| `plantings` | Instance : variété X dans jardin Y à case Z |
| `planting_events` | Historique arrosage/taille/traitement/observation |
| `tasks` | Tâches (ponctuelles ou récurrentes) |
| `lunar_calendar_entries` | Cache/dataset calendrier lunaire |
| `user_settings` | Préférences locales |
## Endpoints API principaux
```
GET /api/health
GET/POST /api/gardens
GET/PUT/DELETE /api/gardens/{id}
GET/POST /api/gardens/{id}/cells
GET/POST /api/varieties
GET/POST /api/plantings
GET/POST /api/tasks
GET/POST /api/measurements
GET /api/lunar?month=YYYY-MM
POST /api/export
POST /api/import
```
## Thème Gruvbox Dark
Palette CSS à respecter partout :
```
Background principal : #282828
Background secondaire : #3c3836
Texte principal : #ebdbb2
Texte secondaire : #a89984
Accent vert : #b8bb26
Accent jaune : #fabd2f
Accent bleu : #83a598
Accent orange : #fe8019
Erreur rouge : #fb4934
```
Typo : `Fira Code` ou `Courier New` pour le côté rétro.
## Fonctionnalités MVP (ordre d'implémentation)
1. Modèle DB + CRUD jardins / variétés / plantations / tâches
2. Upload images + galerie
3. Vue grille jardin (2D) + placement des plantations
4. Planning calendrier (semaine/mois) + vues filtrées
5. Calendrier lunaire (phases + jours racine/feuille/fleur/fruit)
6. Dashboard + export/import JSON
7. Polissage UI mobile + README final
## Pages de l'interface
1. **Dashboard** — tâches du jour, mesures récentes, plantations actives
2. **Jardins** — liste, création, fiche jardin
3. **Grille jardin** — vue cases, détails par case
4. **Catalogue variétés** — liste, fiche variété
5. **Plantations** — liste filtrable, création, fiche
6. **Planning** — calendrier mois/semaine + actions
7. **Tâches** — Kanban simple ou liste
8. **Calendrier lunaire** — vue mois + détails jour
9. **Réglages** — unités, localisation, export/import, sauvegarde
## Qualité & conventions
- Validation stricte Pydantic côté backend
- Logs structurés (JSON) côté backend
- Tests CRUD + filtres pour chaque ressource API
- Variables d'environnement côté backend uniquement (pas de secrets dans le frontend)
- Données de démo (seed) : 1 jardin + quelques variétés + plantations + tâches

View File

@@ -16,8 +16,8 @@ cp .env.example .env
docker compose up --build docker compose up --build
``` ```
- Application : http://localhost - Application : http://localhost:8061
- API (docs Swagger) : http://localhost:8000/docs - API (docs Swagger) : http://localhost:8060/docs
--- ---

80
amelioration.md Normal file
View File

@@ -0,0 +1,80 @@
- [ ] photo possibilité d'ajouter des photos, upload ( prevoir mecanisme : transformer en webp et redimensionner)
- couleur predominante : plantes: vert; jardin : marron; arrosage : bleu; outils: jaune
- ajout icones representatives
jardin :
- [ ] ajouter les caracteristiques pour un jardin: photo, geolocalisation, type de terre, ph, ensoleillement, exposition, dimension,surface, ...
-
plante :
- [ ] header : varietés => remplacer par plante ( pareil dans tous le programme)
- [ ] pour une plante, ajouter des caracteristiques : photo, nom, varités, famille, resistance au froid , maladie commune et astuces , methode de semis et de plantation, ... (brainstorming)
- [ ] plante du potager, fleur, arbre ou arbustre
- [ ] brainstorming pour ajouter une liste de plantes courante du jardin: carotte, tomate, ail, oignon, haricot, petis pois, poireaux, pomme de terre, salade, fraise, framboise, persil, echalote,courgette, choux fleur, choux boule,
- [ ] association des plantes et plantes ne devant pas etre planté a proximite
taches:
- [ ] brainstorming pour preremplir la liste des taches courantes au jardin
- [ ] un tache peut etre unique ou avoir une frequence
- [ ] une tache peut utiliser un outils et s'applique a une platantion ( plantation: plantes dans une zone d'un jardin)
outils:
- [ ] brainstorming pour ajouter des outils de jardinage
- [ ] liste dans le header
- [ ] créer une 1ere liste d'outils commun du jardin (grelinete, pelle, beche, pioche, sarcloir,....)
planning:
- [ ] brainstorming
calendrier:
- [ ] renommer le header lunaire en calendrier ( lunaire, dictons, meteo, taches, )
- [ ] brainstorming
- [ ] calendrier lunaire avec icones et texte
- [ ] calendrier ajouter dictons courant ( brainstorming region france, auvergne, haute-loire, yssingeaux)
- analyse le dossier calendrier_lunaire
meteo:
- [ ] brainstorming
- [ ] calendrier bi-hebdo ? avec prevision meteo
- recupere les infos sur ma station meteo locale ( donnéée de la veille une fois par jours et donnée actuelle 1 fois par heure)( brainstorming a partir des script d'essai)
- recupere les infos sur https://open-meteo.com/ une fois par heure pour les prevision ( brainstorming a partir des script d'essai)
- presentation meteo sous forme de tableau journalier synthetique ( passé, present, futur ( avec des colonnes pour station meteo locale et site open-meteo separé)) ( brainstorming
- analyse le dossier prevision meteo et station_meteo
astuces :
- [ ] possibilité d'ajouter des astuces pour les plantes, le jardin, les taches
- [ ] brainstorming
capteur:
- [ ] recuperation de capteur possible: ensoleillement, temperature ambiante, temperature du sol, humidite de l'air, humidite du sol, ph du sol,
- [ ] configuration via serveur mqtt ( topic et payload)
- [ ] brainstorming
- capteur exterieur et capteur serre
reglages :
- [ ] application en mode desktop et pour smartphone ( responsive ?)
- [ ] section pour chaque type: interface, jardin, plante, taches, calendrier, planning
- [ ] backup et restaure (toutes les données: bdd, photo, pdf, txt)
- [ ] ajout de detection de plante a partir de photos ( possibilite d'ajouter un service de detection de type de plantes a partir d'une photo)
- reglage url station meteo local et site distant
recolte:
- [ ] ajouter possibiliter de saisir des quantites recoltés et a quelles dates ( brainstorming)
- [ ] ajouter la possibiliter de suivre des maladies (mildioux ), des traitement, des ravageurs: limaces, taupe, chenille, ...
frontend :
- [ ] icones pour objet dimensionnable ds setting : jardin, plantes, tache, calendrier, meteo, outils
- [ ] icones pour plantes dimensionnable ds setting : tomate, pomme de terre, salade, carotte, ...
- [ ] mode editions pour pouvoir modifier les different element, plantes, jardin, taches, calendrier, ...
- [ ] ajouter des images depuis iphones ( appareil photo)
- [ ] ajouter des pdf ou des annotation, des url dvalable pour tous types d'objet: jardin, plantes,outils,
- verifier que l'application s'affiche correctement sur smartphone
- utilise le dossier icons pour le calendrier lunaire et la meteo ( icone svg adapter taille d'affichage dans setting)
bibliotehque photo:
- ajoute une bibliotheque ( plante, legume, fleur, arbres et arbrisseau, adventices) avec un stockage de mes capture et le rsultat d'une identification des plante grace au web ( via api ou via ia : brainstorming) api key: 2b1088cHCJ4c7Cn2Vqq67xfve sur https://my.plantnet.org/dashboard ( https://my.plantnet.org/doc/api/openapi)
- brainstorming local ai detection style yolo ( fichier consigne_yolo.md)
backend :
- [ ] methode simple pour mettre a jours la base de donnée ; brainstorming

7284
avancement.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,4 +4,4 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
RUN mkdir -p /data/uploads RUN mkdir -p /data/uploads
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8060"]

View File

@@ -0,0 +1,10 @@
from typing import Optional
from sqlmodel import Field, SQLModel
class Dicton(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
mois: int # 1-12
jour: Optional[int] = None
texte: str
region: Optional[str] = None # Auvergne|Haute-Loire|National

View File

@@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timezone
from typing import Optional from typing import Optional
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
@@ -16,10 +16,12 @@ class Garden(SQLModel, table=True):
ombre: Optional[str] = None # ombre | mi-ombre | plein_soleil ombre: Optional[str] = None # ombre | mi-ombre | plein_soleil
sol_type: Optional[str] = None sol_type: Optional[str] = None
sol_ph: Optional[float] = None sol_ph: Optional[float] = None
surface_m2: Optional[float] = None
ensoleillement: Optional[str] = None
grille_largeur: int = 6 grille_largeur: int = 6
grille_hauteur: int = 4 grille_hauteur: int = 4
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class GardenCell(SQLModel, table=True): class GardenCell(SQLModel, table=True):
@@ -39,7 +41,7 @@ class GardenImage(SQLModel, table=True):
garden_id: int = Field(foreign_key="garden.id", index=True) garden_id: int = Field(foreign_key="garden.id", index=True)
filename: str filename: str
caption: Optional[str] = None caption: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class Measurement(SQLModel, table=True): class Measurement(SQLModel, table=True):
@@ -50,4 +52,4 @@ class Measurement(SQLModel, table=True):
humidite_air: Optional[float] = None humidite_air: Optional[float] = None
humidite_sol: Optional[float] = None humidite_sol: Optional[float] = None
source: str = "manuel" # manuel | capteur source: str = "manuel" # manuel | capteur
ts: datetime = Field(default_factory=datetime.utcnow) ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -1,9 +1,11 @@
from datetime import datetime from datetime import datetime, timezone
from typing import Optional from typing import Optional
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class PlantVariety(SQLModel, table=True): class Plant(SQLModel, table=True):
__tablename__ = "plant"
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
nom_commun: str nom_commun: str
nom_botanique: Optional[str] = None nom_botanique: Optional[str] = None
@@ -11,9 +13,11 @@ class PlantVariety(SQLModel, table=True):
famille: Optional[str] = None famille: Optional[str] = None
tags: Optional[str] = None # CSV tags: Optional[str] = None # CSV
type_plante: Optional[str] = None # legume | fruit | aromatique | fleur type_plante: Optional[str] = None # legume | fruit | aromatique | fleur
categorie: Optional[str] = None # potager|fleur|arbre|arbuste
besoin_eau: Optional[str] = None # faible | moyen | fort besoin_eau: Optional[str] = None # faible | moyen | fort
besoin_soleil: Optional[str] = None besoin_soleil: Optional[str] = None
espacement_cm: Optional[int] = None espacement_cm: Optional[int] = None
hauteur_cm: Optional[int] = None
temp_min_c: Optional[float] = None temp_min_c: Optional[float] = None
duree_culture_j: Optional[int] = None duree_culture_j: Optional[int] = None
profondeur_semis_cm: Optional[float] = None profondeur_semis_cm: Optional[float] = None
@@ -23,13 +27,16 @@ class PlantVariety(SQLModel, table=True):
repiquage_mois: Optional[str] = None repiquage_mois: Optional[str] = None
plantation_mois: Optional[str] = None plantation_mois: Optional[str] = None
recolte_mois: Optional[str] = None recolte_mois: Optional[str] = None
maladies_courantes: Optional[str] = None
astuces_culture: Optional[str] = None
url_reference: Optional[str] = None
notes: Optional[str] = None notes: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class PlantImage(SQLModel, table=True): class PlantImage(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
variety_id: int = Field(foreign_key="plantvariety.id", index=True) plant_id: int = Field(foreign_key="plant.id", index=True)
filename: str filename: str
caption: Optional[str] = None caption: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -1,12 +1,28 @@
from datetime import date, datetime from datetime import date, datetime, timezone
from typing import Optional from typing import Optional
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class PlantingCreate(SQLModel):
garden_id: int
variety_id: int
cell_id: Optional[int] = None
date_semis: Optional[date] = None
date_plantation: Optional[date] = None
date_repiquage: Optional[date] = None
quantite: int = 1
statut: str = "prevu"
date_recolte_debut: Optional[date] = None
date_recolte_fin: Optional[date] = None
rendement_estime: Optional[float] = None
rendement_reel: Optional[float] = None
notes: Optional[str] = None
class Planting(SQLModel, table=True): class Planting(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
garden_id: int = Field(foreign_key="garden.id", index=True) garden_id: int = Field(foreign_key="garden.id", index=True)
variety_id: int = Field(foreign_key="plantvariety.id", index=True) variety_id: int = Field(foreign_key="plant.id", index=True)
cell_id: Optional[int] = Field(default=None, foreign_key="gardencell.id") cell_id: Optional[int] = Field(default=None, foreign_key="gardencell.id")
date_semis: Optional[date] = None date_semis: Optional[date] = None
date_plantation: Optional[date] = None date_plantation: Optional[date] = None
@@ -18,8 +34,8 @@ class Planting(SQLModel, table=True):
rendement_estime: Optional[float] = None rendement_estime: Optional[float] = None
rendement_reel: Optional[float] = None rendement_reel: Optional[float] = None
notes: Optional[str] = None notes: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class PlantingEvent(SQLModel, table=True): class PlantingEvent(SQLModel, table=True):
@@ -27,4 +43,4 @@ class PlantingEvent(SQLModel, table=True):
planting_id: int = Field(foreign_key="planting.id", index=True) planting_id: int = Field(foreign_key="planting.id", index=True)
type: str # arrosage | taille | traitement | observation | autre type: str # arrosage | taille | traitement | observation | autre
note: Optional[str] = None note: Optional[str] = None
ts: datetime = Field(default_factory=datetime.utcnow) ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -0,0 +1,42 @@
from datetime import date, datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
class RecolteCreate(SQLModel):
quantite: float
unite: str = "kg" # kg|g|unites|litres|bottes
date_recolte: date
notes: Optional[str] = None
class Recolte(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
plantation_id: int = Field(foreign_key="planting.id", index=True)
quantite: float
unite: str = "kg"
date_recolte: date
notes: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ObservationCreate(SQLModel):
type: str # maladie|ravageur|traitement|note
titre: str
description: Optional[str] = None
date: date
photo_url: Optional[str] = None
plantation_id: Optional[int] = None
garden_id: Optional[int] = None
class Observation(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
type: str
titre: str
description: Optional[str] = None
date: date
photo_url: Optional[str] = None
plantation_id: Optional[int] = Field(default=None, foreign_key="planting.id")
garden_id: Optional[int] = Field(default=None, foreign_key="garden.id")
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -1,17 +1,34 @@
from datetime import date, datetime from datetime import date, datetime, timezone
from typing import Optional from typing import Optional
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class TaskCreate(SQLModel):
titre: str
description: Optional[str] = None
garden_id: Optional[int] = None
planting_id: Optional[int] = None
outil_id: Optional[int] = None
priorite: str = "normale" # basse | normale | haute
echeance: Optional[date] = None
recurrence: Optional[str] = None
frequence_jours: Optional[int] = None
date_prochaine: Optional[date] = None
statut: str = "a_faire" # a_faire | en_cours | fait | annule
class Task(SQLModel, table=True): class Task(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
titre: str titre: str
description: Optional[str] = None description: Optional[str] = None
garden_id: Optional[int] = Field(default=None, foreign_key="garden.id") garden_id: Optional[int] = Field(default=None, foreign_key="garden.id")
planting_id: Optional[int] = Field(default=None, foreign_key="planting.id") planting_id: Optional[int] = Field(default=None, foreign_key="planting.id")
priorite: str = "normale" # basse | normale | haute outil_id: Optional[int] = Field(default=None, foreign_key="tool.id")
priorite: str = "normale"
echeance: Optional[date] = None echeance: Optional[date] = None
recurrence: Optional[str] = None # quotidien | hebdomadaire | mensuel recurrence: Optional[str] = None
statut: str = "a_faire" # a_faire | en_cours | fait | annule frequence_jours: Optional[int] = None
created_at: datetime = Field(default_factory=datetime.utcnow) date_prochaine: Optional[date] = None
updated_at: datetime = Field(default_factory=datetime.utcnow) statut: str = "a_faire"
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -0,0 +1,12 @@
from datetime import datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
class Tool(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
nom: str
description: Optional[str] = None
categorie: Optional[str] = None # beche|fourche|griffe|arrosage|taille|autre
photo_url: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -0,0 +1,59 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.astuce import Astuce
router = APIRouter(tags=["astuces"])
@router.get("/astuces", response_model=List[Astuce])
def list_astuces(
entity_type: Optional[str] = Query(None),
entity_id: Optional[int] = Query(None),
session: Session = Depends(get_session),
):
q = select(Astuce)
if entity_type:
q = q.where(Astuce.entity_type == entity_type)
if entity_id is not None:
q = q.where(Astuce.entity_id == entity_id)
return session.exec(q).all()
@router.post("/astuces", response_model=Astuce, status_code=status.HTTP_201_CREATED)
def create_astuce(a: Astuce, session: Session = Depends(get_session)):
session.add(a)
session.commit()
session.refresh(a)
return a
@router.get("/astuces/{id}", response_model=Astuce)
def get_astuce(id: int, session: Session = Depends(get_session)):
a = session.get(Astuce, id)
if not a:
raise HTTPException(404, "Astuce introuvable")
return a
@router.put("/astuces/{id}", response_model=Astuce)
def update_astuce(id: int, data: Astuce, session: Session = Depends(get_session)):
a = session.get(Astuce, id)
if not a:
raise HTTPException(404, "Astuce introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
setattr(a, k, v)
session.add(a)
session.commit()
session.refresh(a)
return a
@router.delete("/astuces/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_astuce(id: int, session: Session = Depends(get_session)):
a = session.get(Astuce, id)
if not a:
raise HTTPException(404, "Astuce introuvable")
session.delete(a)
session.commit()

View File

@@ -0,0 +1,18 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from sqlmodel import Session, select
from app.database import get_session
from app.models.dicton import Dicton
router = APIRouter(tags=["dictons"])
@router.get("/dictons", response_model=List[Dicton])
def list_dictons(
mois: Optional[int] = Query(None),
session: Session = Depends(get_session),
):
q = select(Dicton)
if mois:
q = q.where(Dicton.mois == mois)
return session.exec(q).all()

View File

@@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timezone
from typing import List from typing import List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
@@ -38,7 +38,7 @@ def update_garden(id: int, data: Garden, session: Session = Depends(get_session)
raise HTTPException(status_code=404, detail="Jardin introuvable") raise HTTPException(status_code=404, detail="Jardin introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items(): for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
setattr(g, k, v) setattr(g, k, v)
g.updated_at = datetime.utcnow() g.updated_at = datetime.now(timezone.utc)
session.add(g) session.add(g)
session.commit() session.commit()
session.refresh(g) session.refresh(g)

View File

@@ -0,0 +1,36 @@
import calendar
from datetime import date
from typing import Any
from fastapi import APIRouter, HTTPException, Query
router = APIRouter(tags=["lunaire"])
# Cache en mémoire : {mois_str: list[dict]}
_CACHE: dict[str, list[dict]] = {}
@router.get("/lunar")
def get_lunar(
month: str = Query(..., description="Format YYYY-MM"),
) -> list[dict[str, Any]]:
if month in _CACHE:
return _CACHE[month]
try:
year, mon = int(month[:4]), int(month[5:7])
except (ValueError, IndexError):
raise HTTPException(400, "Format attendu : YYYY-MM")
last_day = calendar.monthrange(year, mon)[1]
start = date(year, mon, 1)
end = date(year, mon, last_day)
try:
from app.services.lunar import build_calendar
from dataclasses import asdict
result = [asdict(d) for d in build_calendar(start, end)]
except ImportError:
raise HTTPException(503, "Service lunaire non disponible (skyfield non installé)")
except Exception as e:
raise HTTPException(500, f"Erreur calcul lunaire : {e}")
_CACHE[month] = result
return result

View File

@@ -2,13 +2,20 @@ import os
import uuid import uuid
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status from fastapi import APIRouter, Body, Depends, File, HTTPException, Query, UploadFile, status
from pydantic import BaseModel
from sqlmodel import Session, select from sqlmodel import Session, select
from app.config import UPLOAD_DIR from app.config import UPLOAD_DIR
from app.database import get_session from app.database import get_session
from app.models.media import Attachment, Media from app.models.media import Attachment, Media
class MediaPatch(BaseModel):
entity_type: Optional[str] = None
entity_id: Optional[int] = None
titre: Optional[str] = None
router = APIRouter(tags=["media"]) router = APIRouter(tags=["media"])
@@ -81,6 +88,19 @@ def create_media(m: Media, session: Session = Depends(get_session)):
return m return m
@router.patch("/media/{id}", response_model=Media)
def update_media(id: int, payload: MediaPatch, session: Session = Depends(get_session)):
m = session.get(Media, id)
if not m:
raise HTTPException(404, "Media introuvable")
for k, v in payload.model_dump(exclude_none=True).items():
setattr(m, k, v)
session.add(m)
session.commit()
session.refresh(m)
return m
@router.delete("/media/{id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/media/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_media(id: int, session: Session = Depends(get_session)): def delete_media(id: int, session: Session = Depends(get_session)):
m = session.get(Media, id) m = session.get(Media, id)

View File

@@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime, timezone
from typing import List from typing import List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select from sqlmodel import Session, select
from app.database import get_session from app.database import get_session
from app.models.planting import Planting, PlantingEvent from app.models.planting import Planting, PlantingCreate, PlantingEvent
router = APIRouter(tags=["plantations"]) router = APIRouter(tags=["plantations"])
@@ -14,7 +14,8 @@ def list_plantings(session: Session = Depends(get_session)):
@router.post("/plantings", response_model=Planting, status_code=status.HTTP_201_CREATED) @router.post("/plantings", response_model=Planting, status_code=status.HTTP_201_CREATED)
def create_planting(p: Planting, session: Session = Depends(get_session)): def create_planting(data: PlantingCreate, session: Session = Depends(get_session)):
p = Planting(**data.model_dump())
session.add(p) session.add(p)
session.commit() session.commit()
session.refresh(p) session.refresh(p)
@@ -30,13 +31,13 @@ def get_planting(id: int, session: Session = Depends(get_session)):
@router.put("/plantings/{id}", response_model=Planting) @router.put("/plantings/{id}", response_model=Planting)
def update_planting(id: int, data: Planting, session: Session = Depends(get_session)): def update_planting(id: int, data: PlantingCreate, session: Session = Depends(get_session)):
p = session.get(Planting, id) p = session.get(Planting, id)
if not p: if not p:
raise HTTPException(status_code=404, detail="Plantation introuvable") raise HTTPException(status_code=404, detail="Plantation introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items(): for k, v in data.model_dump(exclude_unset=True).items():
setattr(p, k, v) setattr(p, k, v)
p.updated_at = datetime.utcnow() p.updated_at = datetime.now(timezone.utc)
session.add(p) session.add(p)
session.commit() session.commit()
session.refresh(p) session.refresh(p)

View File

@@ -0,0 +1,56 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.plant import Plant
router = APIRouter(tags=["plantes"])
@router.get("/plants", response_model=List[Plant])
def list_plants(
categorie: Optional[str] = Query(None),
session: Session = Depends(get_session),
):
q = select(Plant)
if categorie:
q = q.where(Plant.categorie == categorie)
return session.exec(q).all()
@router.post("/plants", response_model=Plant, status_code=status.HTTP_201_CREATED)
def create_plant(p: Plant, session: Session = Depends(get_session)):
session.add(p)
session.commit()
session.refresh(p)
return p
@router.get("/plants/{id}", response_model=Plant)
def get_plant(id: int, session: Session = Depends(get_session)):
p = session.get(Plant, id)
if not p:
raise HTTPException(404, "Plante introuvable")
return p
@router.put("/plants/{id}", response_model=Plant)
def update_plant(id: int, data: Plant, session: Session = Depends(get_session)):
p = session.get(Plant, id)
if not p:
raise HTTPException(404, "Plante introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
setattr(p, k, v)
session.add(p)
session.commit()
session.refresh(p)
return p
@router.delete("/plants/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_plant(id: int, session: Session = Depends(get_session)):
p = session.get(Plant, id)
if not p:
raise HTTPException(404, "Plante introuvable")
session.delete(p)
session.commit()

View File

@@ -0,0 +1,84 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.recolte import Observation, ObservationCreate, Recolte, RecolteCreate
router = APIRouter(tags=["récoltes"])
# ── Récoltes (nested sous plantings) ──────────────────────────────────────────
@router.get("/plantings/{planting_id}/recoltes", response_model=List[Recolte])
def list_recoltes(planting_id: int, session: Session = Depends(get_session)):
return session.exec(
select(Recolte).where(Recolte.plantation_id == planting_id)
).all()
@router.post(
"/plantings/{planting_id}/recoltes",
response_model=Recolte,
status_code=status.HTTP_201_CREATED,
)
def create_recolte(
planting_id: int,
data: RecolteCreate,
session: Session = Depends(get_session),
):
r = Recolte(plantation_id=planting_id, **data.model_dump())
session.add(r)
session.commit()
session.refresh(r)
return r
@router.delete("/recoltes/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_recolte(id: int, session: Session = Depends(get_session)):
r = session.get(Recolte, id)
if not r:
raise HTTPException(404, "Récolte introuvable")
session.delete(r)
session.commit()
# ── Observations ───────────────────────────────────────────────────────────────
@router.get("/observations", response_model=List[Observation])
def list_observations(
plantation_id: Optional[int] = Query(None),
garden_id: Optional[int] = Query(None),
session: Session = Depends(get_session),
):
q = select(Observation)
if plantation_id is not None:
q = q.where(Observation.plantation_id == plantation_id)
if garden_id is not None:
q = q.where(Observation.garden_id == garden_id)
return session.exec(q).all()
@router.post("/observations", response_model=Observation, status_code=status.HTTP_201_CREATED)
def create_observation(data: ObservationCreate, session: Session = Depends(get_session)):
o = Observation(**data.model_dump())
session.add(o)
session.commit()
session.refresh(o)
return o
@router.get("/observations/{id}", response_model=Observation)
def get_observation(id: int, session: Session = Depends(get_session)):
o = session.get(Observation, id)
if not o:
raise HTTPException(404, "Observation introuvable")
return o
@router.delete("/observations/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_observation(id: int, session: Session = Depends(get_session)):
o = session.get(Observation, id)
if not o:
raise HTTPException(404, "Observation introuvable")
session.delete(o)
session.commit()

View File

@@ -1,8 +1,7 @@
from datetime import date
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlmodel import Session, select from sqlmodel import Session, select
from app.database import get_session from app.database import get_session
from app.models.settings import UserSettings, LunarCalendarEntry from app.models.settings import UserSettings
router = APIRouter(tags=["réglages"]) router = APIRouter(tags=["réglages"])
@@ -24,16 +23,3 @@ def update_settings(data: dict, session: Session = Depends(get_session)):
session.add(row) session.add(row)
session.commit() session.commit()
return {"ok": True} return {"ok": True}
@router.get("/lunar")
def get_lunar(month: str, session: Session = Depends(get_session)):
year, m = map(int, month.split("-"))
first = date(year, m, 1)
last_m, last_y = (m + 1, year) if m < 12 else (1, year + 1)
last = date(last_y, last_m, 1)
return session.exec(
select(LunarCalendarEntry)
.where(LunarCalendarEntry.jour >= first)
.where(LunarCalendarEntry.jour < last)
).all()

View File

@@ -1,9 +1,9 @@
from datetime import datetime from datetime import datetime, timezone
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select from sqlmodel import Session, select
from app.database import get_session from app.database import get_session
from app.models.task import Task from app.models.task import Task, TaskCreate
router = APIRouter(tags=["tâches"]) router = APIRouter(tags=["tâches"])
@@ -23,7 +23,8 @@ def list_tasks(
@router.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED) @router.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED)
def create_task(t: Task, session: Session = Depends(get_session)): def create_task(data: TaskCreate, session: Session = Depends(get_session)):
t = Task(**data.model_dump())
session.add(t) session.add(t)
session.commit() session.commit()
session.refresh(t) session.refresh(t)
@@ -39,13 +40,26 @@ def get_task(id: int, session: Session = Depends(get_session)):
@router.put("/tasks/{id}", response_model=Task) @router.put("/tasks/{id}", response_model=Task)
def update_task(id: int, data: Task, session: Session = Depends(get_session)): def update_task(id: int, data: TaskCreate, session: Session = Depends(get_session)):
t = session.get(Task, id) t = session.get(Task, id)
if not t: if not t:
raise HTTPException(status_code=404, detail="Tâche introuvable") raise HTTPException(status_code=404, detail="Tâche introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items(): for k, v in data.model_dump(exclude_unset=True).items():
setattr(t, k, v) setattr(t, k, v)
t.updated_at = datetime.utcnow() t.updated_at = datetime.now(timezone.utc)
session.add(t)
session.commit()
session.refresh(t)
return t
@router.put("/tasks/{id}/statut", response_model=Task)
def update_statut(id: int, statut: str, session: Session = Depends(get_session)):
t = session.get(Task, id)
if not t:
raise HTTPException(status_code=404, detail="Tâche introuvable")
t.statut = statut
t.updated_at = datetime.now(timezone.utc)
session.add(t) session.add(t)
session.commit() session.commit()
session.refresh(t) session.refresh(t)

View File

@@ -0,0 +1,56 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.tool import Tool
router = APIRouter(tags=["outils"])
@router.get("/tools", response_model=List[Tool])
def list_tools(
categorie: Optional[str] = Query(None),
session: Session = Depends(get_session),
):
q = select(Tool)
if categorie:
q = q.where(Tool.categorie == categorie)
return session.exec(q).all()
@router.post("/tools", response_model=Tool, status_code=status.HTTP_201_CREATED)
def create_tool(t: Tool, session: Session = Depends(get_session)):
session.add(t)
session.commit()
session.refresh(t)
return t
@router.get("/tools/{id}", response_model=Tool)
def get_tool(id: int, session: Session = Depends(get_session)):
t = session.get(Tool, id)
if not t:
raise HTTPException(404, "Outil introuvable")
return t
@router.put("/tools/{id}", response_model=Tool)
def update_tool(id: int, data: Tool, session: Session = Depends(get_session)):
t = session.get(Tool, id)
if not t:
raise HTTPException(404, "Outil introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
setattr(t, k, v)
session.add(t)
session.commit()
session.refresh(t)
return t
@router.delete("/tools/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_tool(id: int, session: Session = Depends(get_session)):
t = session.get(Tool, id)
if not t:
raise HTTPException(404, "Outil introuvable")
session.delete(t)
session.commit()

View File

@@ -6,14 +6,18 @@ import app.models # noqa
def run_seed(): def run_seed():
from app.models.garden import Garden, GardenCell, Measurement from app.models.garden import Garden, GardenCell, Measurement
from app.models.plant import PlantVariety from app.models.plant import Plant
from app.models.planting import Planting, PlantingEvent from app.models.planting import Planting, PlantingEvent
from app.models.task import Task from app.models.task import Task
from app.models.tool import Tool
from app.models.dicton import Dicton
from app.models.astuce import Astuce
with Session(engine) as session: with Session(engine) as session:
if session.exec(select(Garden)).first(): already_seeded = session.exec(select(Garden)).first() is not None
return # déjà seedé
if not already_seeded:
# ── Jardin ────────────────────────────────────────────────────────────
jardin = Garden( jardin = Garden(
nom="Mon potager", nom="Mon potager",
description="Potager principal plein sud", description="Potager principal plein sud",
@@ -21,6 +25,7 @@ def run_seed():
exposition="S", exposition="S",
ombre="plein_soleil", ombre="plein_soleil",
sol_type="limoneux", sol_type="limoneux",
surface_m2=24.0,
grille_largeur=6, grille_largeur=6,
grille_hauteur=4, grille_hauteur=4,
) )
@@ -29,50 +34,210 @@ def run_seed():
for row in range(4): for row in range(4):
for col in range(6): for col in range(6):
session.add(GardenCell( session.add(
GardenCell(
garden_id=jardin.id, garden_id=jardin.id,
col=col, row=row, col=col,
row=row,
libelle=f"{chr(65 + row)}{col + 1}", libelle=f"{chr(65 + row)}{col + 1}",
)) )
)
session.add(Measurement(garden_id=jardin.id, temp_air=18.0, humidite_air=65.0)) session.add(Measurement(garden_id=jardin.id, temp_air=18.0, humidite_air=65.0))
tomate = PlantVariety( # ── 20 Plantes ────────────────────────────────────────────────────────
nom_commun="Tomate", variete="Andine Cornue", plantes_data = [
famille="Solanacées", type_plante="legume", dict(nom_commun="Tomate", variete="Andine Cornue", famille="Solanacées",
besoin_eau="fort", espacement_cm=60, categorie="potager", type_plante="legume", besoin_eau="fort",
plantation_mois="4,5", recolte_mois="7,8,9", espacement_cm=60, plantation_mois="4,5", recolte_mois="7,8,9",
) semis_interieur_mois="2,3"),
courgette = PlantVariety( dict(nom_commun="Courgette", variete="Verte", famille="Cucurbitacées",
nom_commun="Courgette", variete="Verte", categorie="potager", type_plante="legume", besoin_eau="moyen",
famille="Cucurbitacées", type_plante="legume", espacement_cm=80, plantation_mois="5,6", recolte_mois="7,8",
besoin_eau="moyen", espacement_cm=80, semis_interieur_mois="4"),
plantation_mois="5,6", recolte_mois="7,8", dict(nom_commun="Carotte", famille="Apiacées",
) categorie="potager", type_plante="legume", besoin_eau="moyen",
salade = PlantVariety( espacement_cm=8, semis_exterieur_mois="3,4,5,6",
nom_commun="Laitue", variete="Batavia", recolte_mois="6,7,8,9,10"),
famille="Astéracées", type_plante="legume", dict(nom_commun="Laitue", variete="Batavia", famille="Astéracées",
besoin_eau="moyen", espacement_cm=25, categorie="potager", type_plante="legume", besoin_eau="moyen",
) espacement_cm=25, plantation_mois="3,4,5,8,9",
session.add_all([tomate, courgette, salade]) recolte_mois="5,6,7,10"),
dict(nom_commun="Ail", famille="Amaryllidacées",
categorie="potager", type_plante="legume", besoin_eau="faible",
espacement_cm=15, plantation_mois="10,11",
recolte_mois="6,7"),
dict(nom_commun="Oignon", famille="Amaryllidacées",
categorie="potager", type_plante="legume", besoin_eau="faible",
espacement_cm=10, semis_interieur_mois="2,3",
plantation_mois="4,5", recolte_mois="7,8"),
dict(nom_commun="Haricot", variete="Nain", famille="Fabacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=15, semis_exterieur_mois="5,6",
recolte_mois="7,8,9"),
dict(nom_commun="Pois", variete="Mange-tout", famille="Fabacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=10, semis_exterieur_mois="3,4",
recolte_mois="6,7"),
dict(nom_commun="Poireau", famille="Amaryllidacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=15, semis_interieur_mois="2,3",
plantation_mois="6,7", recolte_mois="10,11,12,1,2"),
dict(nom_commun="Pomme de terre", famille="Solanacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=35, plantation_mois="3,4,5",
recolte_mois="7,8,9,10"),
dict(nom_commun="Fraise", famille="Rosacées",
categorie="potager", type_plante="fruit", besoin_eau="moyen",
espacement_cm=30, plantation_mois="3,4,9,10",
recolte_mois="5,6,7"),
dict(nom_commun="Framboise", famille="Rosacées",
categorie="arbuste", type_plante="fruit", besoin_eau="moyen",
espacement_cm=60, plantation_mois="11,12,2,3",
recolte_mois="7,8,9"),
dict(nom_commun="Persil", famille="Apiacées",
categorie="potager", type_plante="aromatique", besoin_eau="moyen",
espacement_cm=20, semis_exterieur_mois="3,4,5,8",
recolte_mois="4,5,6,7,8,9,10"),
dict(nom_commun="Échalote", famille="Amaryllidacées",
categorie="potager", type_plante="legume", besoin_eau="faible",
espacement_cm=15, plantation_mois="2,3",
recolte_mois="7,8"),
dict(nom_commun="Chou-fleur", famille="Brassicacées",
categorie="potager", type_plante="legume", besoin_eau="fort",
espacement_cm=60, semis_interieur_mois="3,4",
plantation_mois="5,6", recolte_mois="9,10,11"),
dict(nom_commun="Chou", variete="Milan", famille="Brassicacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=50, semis_interieur_mois="3,4",
plantation_mois="5,6", recolte_mois="10,11,12"),
dict(nom_commun="Betterave", famille="Amaranthacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=15, semis_exterieur_mois="4,5,6",
recolte_mois="8,9,10"),
dict(nom_commun="Radis", famille="Brassicacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=5, semis_exterieur_mois="3,4,5,8,9",
recolte_mois="4,5,6,9,10"),
dict(nom_commun="Épinard", famille="Amaranthacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=15, semis_exterieur_mois="3,4,8,9",
recolte_mois="5,6,10,11"),
dict(nom_commun="Basilic", famille="Lamiacées",
categorie="potager", type_plante="aromatique", besoin_eau="moyen",
espacement_cm=20, semis_interieur_mois="3,4",
plantation_mois="5,6", recolte_mois="6,7,8,9"),
]
plantes = []
for data in plantes_data:
p = Plant(**data)
session.add(p)
plantes.append(p)
session.flush() session.flush()
tomate = plantes[0]
courgette = plantes[1]
# ── Plantings ──────────────────────────────────────────────────────────
p1 = Planting( p1 = Planting(
garden_id=jardin.id, variety_id=tomate.id, garden_id=jardin.id,
date_plantation=date(2026, 5, 1), quantite=6, statut="en_cours", variety_id=tomate.id,
date_plantation=date(2026, 5, 1),
quantite=6,
statut="en_cours",
) )
p2 = Planting( p2 = Planting(
garden_id=jardin.id, variety_id=courgette.id, garden_id=jardin.id,
date_plantation=date(2026, 5, 15), quantite=3, statut="prevu", variety_id=courgette.id,
date_plantation=date(2026, 5, 15),
quantite=3,
statut="prevu",
) )
session.add_all([p1, p2]) session.add_all([p1, p2])
session.flush() session.flush()
session.add(PlantingEvent(planting_id=p1.id, type="arrosage", note="Arrosage du matin")) session.add(PlantingEvent(planting_id=p1.id, type="arrosage", note="Arrosage du matin"))
# ── Tâches ────────────────────────────────────────────────────────────
session.add(Task(titre="Arroser les tomates", priorite="haute", session.add(Task(titre="Arroser les tomates", priorite="haute",
statut="a_faire", garden_id=jardin.id)) statut="a_faire", garden_id=jardin.id))
session.add(Task(titre="Traiter contre les pucerons", priorite="normale", statut="a_faire")) session.add(Task(titre="Traiter contre les pucerons", priorite="normale", statut="a_faire"))
session.add(Task(titre="Préparer le compost", priorite="basse", statut="en_cours")) session.add(Task(titre="Préparer le compost", priorite="basse", statut="en_cours"))
# ── Outils (indépendant du jardin) ────────────────────────────────────────
if not session.exec(select(Tool)).first():
outils_data = [
dict(nom="Bêche", categorie="beche",
description="Bêche acier forgé, manche bois 110 cm"),
dict(nom="Fourche-bêche", categorie="fourche",
description="Fourche à bêcher 4 dents inox"),
dict(nom="Grelinette", categorie="fourche",
description="Aérateur bi-fourche ergonomique"),
dict(nom="Pioche", categorie="beche",
description="Pioche légère pour travaux de surface"),
dict(nom="Sarcloir", categorie="griffe",
description="Sarcloir oscillant pour désherber entre les rangs"),
dict(nom="Râteau", categorie="griffe",
description="Râteau métallique 14 dents"),
dict(nom="Binette", categorie="griffe",
description="Binette pour ameublir et désherber"),
dict(nom="Transplantoir", categorie="taille",
description="Transplantoir inox gradué"),
dict(nom="Arrosoir", categorie="arrosage",
description="Arrosoir 10L avec pomme amovible"),
dict(nom="Sécateur", categorie="taille",
description="Sécateur de précision bypass"),
]
for data in outils_data:
session.add(Tool(**data))
# ── Dictons (indépendant du jardin) ──────────────────────────────────────
if not session.exec(select(Dicton)).first():
dictons_data = [
dict(mois=1, texte="En janvier, la neige au potager réjouit le jardinier.", region="National"),
dict(mois=2, texte="À la Chandeleur, l'hiver reste ou reprend vigueur.", region="National"),
dict(mois=3, texte="Mars venteux, avril pluvieux, font mai fleureux.", region="National"),
dict(mois=3, texte="Quand mars se déguise en été, avril se déguise en hiver.", region="Auvergne"),
dict(mois=4, texte="Avril ne te découvre pas d'un fil.", region="National"),
dict(mois=4, texte="Pluie d'avril, fleurs à l'infini.", region="Haute-Loire"),
dict(mois=5, texte="Gelées de mai, misère chez le jardinier.", region="Haute-Loire"),
dict(mois=5, jour=11, texte="Saints de glace : Mamert, Pancrace et Gervais.", region="National"),
dict(mois=6, texte="Juin sec, juillet pluvieux ; juillet sec, grains savoureux.", region="Auvergne"),
dict(mois=7, texte="Pluie de juillet remplit greniers et cuves.", region="National"),
dict(mois=8, texte="Août chaud, vin bon.", region="Auvergne"),
dict(mois=9, texte="En septembre, qui sème du blé en fait son profit.", region="National"),
dict(mois=10, texte="En octobre, glands à foison, bon hiver selon la raison.", region="Haute-Loire"),
dict(mois=11, texte="À la Saint-Martin, bois ton vin.", region="National"),
dict(mois=12, texte="Noël au balcon, Pâques aux tisons.", region="Auvergne"),
]
for data in dictons_data:
session.add(Dicton(**data))
# ── Astuces (indépendant du jardin) ──────────────────────────────────────
if not session.exec(select(Astuce)).first():
astuces_data = [
dict(titre="Rotation des cultures", entity_type="general",
contenu="Changez chaque année la famille de légumes sur chaque parcelle pour éviter l'épuisement du sol et les maladies."),
dict(titre="Compagnonnage tomate-basilic", entity_type="plante",
contenu="Plantez du basilic au pied des tomates : il éloigne les pucerons et améliore le goût des fruits."),
dict(titre="Paillage économise l'eau", entity_type="jardin",
contenu="Un paillage de 5 à 10 cm (paille, BRF, tontes) réduit les arrosages de moitié et limite les mauvaises herbes."),
dict(titre="Arrosage au pied le matin", entity_type="general",
contenu="Arrosez toujours au pied des plantes le matin pour éviter les maladies cryptogamiques et la brûlure des feuilles."),
dict(titre="Purin d'ortie maison", entity_type="general",
contenu="Faites macérer 1 kg d'orties dans 10 L d'eau pendant 10 jours. Diluez à 10 % et arrosez le sol pour stimuler la croissance."),
dict(titre="Buttage des pommes de terre", entity_type="plante",
contenu="Buttez régulièrement les pommes de terre quand les fanes atteignent 20 cm pour favoriser la tubérisation."),
dict(titre="Semis de carottes en gel", entity_type="plante",
contenu="Mélangez les graines de carottes avec du sable fin pour un semis homogène et clairsemé."),
dict(titre="Récupération d'eau de pluie", entity_type="jardin",
contenu="Installez une cuve de récupération d'eau de pluie : une maison avec 100 m² de toiture collecte 60 000 L/an."),
dict(titre="Calendrier lunaire", entity_type="general",
contenu="Semez les légumes-feuilles en lune montante, les légumes-racines en lune descendante pour de meilleurs résultats."),
dict(titre="Taille en vert des tomates", entity_type="plante",
contenu="Pincez les gourmands (tiges secondaires entre tige principale et feuille) pour concentrer l'énergie sur les fruits."),
]
for data in astuces_data:
session.add(Astuce(**data))
session.commit() session.commit()

BIN
backend/jardin.db Normal file

Binary file not shown.

View File

@@ -1,6 +1,6 @@
def test_create_planting(client): def test_create_planting(client):
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json() g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json() v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
r = client.post("/api/plantings", json={ r = client.post("/api/plantings", json={
"garden_id": g["id"], "variety_id": v["id"], "quantite": 3 "garden_id": g["id"], "variety_id": v["id"], "quantite": 3
}) })
@@ -10,7 +10,7 @@ def test_create_planting(client):
def test_list_plantings(client): def test_list_plantings(client):
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json() g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json() v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
client.post("/api/plantings", json={"garden_id": g["id"], "variety_id": v["id"]}) client.post("/api/plantings", json={"garden_id": g["id"], "variety_id": v["id"]})
r = client.get("/api/plantings") r = client.get("/api/plantings")
assert r.status_code == 200 assert r.status_code == 200
@@ -19,7 +19,7 @@ def test_list_plantings(client):
def test_add_planting_event(client): def test_add_planting_event(client):
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json() g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json() v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
p = client.post("/api/plantings", json={"garden_id": g["id"], "variety_id": v["id"]}).json() p = client.post("/api/plantings", json={"garden_id": g["id"], "variety_id": v["id"]}).json()
r = client.post(f"/api/plantings/{p['id']}/events", json={"type": "arrosage", "note": "Bien arrosé"}) r = client.post(f"/api/plantings/{p['id']}/events", json={"type": "arrosage", "note": "Bien arrosé"})
assert r.status_code == 201 assert r.status_code == 201

View File

@@ -0,0 +1,26 @@
def test_create_plant(client):
r = client.post("/api/plants", json={"nom_commun": "Tomate", "famille": "Solanacées"})
assert r.status_code == 201
assert r.json()["nom_commun"] == "Tomate"
def test_list_plants(client):
client.post("/api/plants", json={"nom_commun": "Tomate"})
client.post("/api/plants", json={"nom_commun": "Courgette"})
r = client.get("/api/plants")
assert r.status_code == 200
assert len(r.json()) == 2
def test_get_plant(client):
r = client.post("/api/plants", json={"nom_commun": "Basilic"})
id = r.json()["id"]
r2 = client.get(f"/api/plants/{id}")
assert r2.status_code == 200
def test_delete_plant(client):
r = client.post("/api/plants", json={"nom_commun": "Test"})
id = r.json()["id"]
r2 = client.delete(f"/api/plants/{id}")
assert r2.status_code == 204

View File

@@ -0,0 +1,36 @@
def test_create_recolte(client):
# créer jardin + plante + plantation d'abord
g = client.post(
"/api/gardens",
json={"nom": "J", "grille_largeur": 2, "grille_hauteur": 2, "type": "plein_air"},
).json()
p = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
pl = client.post(
"/api/plantings",
json={"garden_id": g["id"], "variety_id": p["id"], "quantite": 1, "statut": "en_cours"},
).json()
r = client.post(
f"/api/plantings/{pl['id']}/recoltes",
json={"quantite": 2.5, "unite": "kg", "date_recolte": "2026-08-01"},
)
assert r.status_code == 201
assert r.json()["quantite"] == 2.5
def test_list_recoltes(client):
g = client.post(
"/api/gardens",
json={"nom": "J", "grille_largeur": 2, "grille_hauteur": 2, "type": "plein_air"},
).json()
p = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
pl = client.post(
"/api/plantings",
json={"garden_id": g["id"], "variety_id": p["id"], "quantite": 1, "statut": "en_cours"},
).json()
client.post(
f"/api/plantings/{pl['id']}/recoltes",
json={"quantite": 1, "unite": "kg", "date_recolte": "2026-08-01"},
)
r = client.get(f"/api/plantings/{pl['id']}/recoltes")
assert r.status_code == 200
assert len(r.json()) == 1

View File

@@ -0,0 +1,18 @@
def test_create_tool(client):
r = client.post("/api/tools", json={"nom": "Bêche", "categorie": "beche"})
assert r.status_code == 201
assert r.json()["nom"] == "Bêche"
def test_list_tools(client):
client.post("/api/tools", json={"nom": "Outil1"})
r = client.get("/api/tools")
assert r.status_code == 200
assert len(r.json()) >= 1
def test_delete_tool(client):
r = client.post("/api/tools", json={"nom": "Test"})
id = r.json()["id"]
r2 = client.delete(f"/api/tools/{id}")
assert r2.status_code == 204

View File

@@ -1,26 +1,29 @@
"""Tests de l'ancien endpoint /api/varieties — maintenant redirigé vers /api/plants."""
def test_create_variety(client): def test_create_variety(client):
r = client.post("/api/varieties", json={"nom_commun": "Tomate", "famille": "Solanacées"}) r = client.post("/api/plants", json={"nom_commun": "Tomate", "famille": "Solanacées"})
assert r.status_code == 201 assert r.status_code == 201
assert r.json()["nom_commun"] == "Tomate" assert r.json()["nom_commun"] == "Tomate"
def test_list_varieties(client): def test_list_varieties(client):
client.post("/api/varieties", json={"nom_commun": "Tomate"}) client.post("/api/plants", json={"nom_commun": "Tomate"})
client.post("/api/varieties", json={"nom_commun": "Courgette"}) client.post("/api/plants", json={"nom_commun": "Courgette"})
r = client.get("/api/varieties") r = client.get("/api/plants")
assert r.status_code == 200 assert r.status_code == 200
assert len(r.json()) == 2 assert len(r.json()) == 2
def test_get_variety(client): def test_get_variety(client):
r = client.post("/api/varieties", json={"nom_commun": "Basilic"}) r = client.post("/api/plants", json={"nom_commun": "Basilic"})
id = r.json()["id"] id = r.json()["id"]
r2 = client.get(f"/api/varieties/{id}") r2 = client.get(f"/api/plants/{id}")
assert r2.status_code == 200 assert r2.status_code == 200
def test_delete_variety(client): def test_delete_variety(client):
r = client.post("/api/varieties", json={"nom_commun": "Test"}) r = client.post("/api/plants", json={"nom_commun": "Test"})
id = r.json()["id"] id = r.json()["id"]
r2 = client.delete(f"/api/varieties/{id}") r2 = client.delete(f"/api/plants/{id}")
assert r2.status_code == 204 assert r2.status_code == 204

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,185 @@
# Résumé (executive summary)
Le **calendrier lunaire de jardinage** se base sur les cycles astronomiques de la Lune pour répartir les travaux (semis, plantations, récoltes) en «jours racine/feuille/fleur/fruit» et selon la **montée/descente de la sève**. Cette méthode traditionnelle, reprise par lagriculture biodynamique, prétend optimiser la croissance. Toutefois, des sources scientifiques avertissent qu**aucune influence directe mesurable** na été démontrée【6†L373-L381】【5†L108-L116】. Malgré tout, le calendrier lunaire sert souvent de repère pratique pour planifier les travaux. Ce document explique les concepts astronomiques (phases, illumination, etc.), leur calcul en Python (avec *skyfield*), et leur traduction en règles de jardinage, tout en restant critique et rigoureux.
## Concepts astronomiques clés
1. **Phases de la Lune** : La phase lunaire se définit par langle entre la Lune et le Soleil vus depuis la Terre, mesuré le long de lécliptique. En pratique, on calcule la différence de longitude écliptique SoleilLune【14†L123-L131】. Ce résultat vaut 0° (Nouvelle Lune), ≈90° (Premier Quartier), ≈180° (Pleine Lune) ou ≈270° (Dernier Quartier) modulo 360°【14†L123-L131】.
La **fraction illuminée** (illumination) peut être obtenue à partir de langle de phase : si θ est la séparation géocentrique SoleilLune (en radians), alors lillumination ≈ (1cosθ)/2. (Skyfield propose aussi `m.fraction_illuminated(sun)` pour obtenir directement ce pourcentage.)
2. **Lune montante / descendante** : Traditionnellement, on dit «lune montante» quand la Lune «monte» dans le ciel, cest-à-dire que sa déclinaison géocentrique augmente au fil du jour. En pratique, on calcule la déclinaison (angle audessus du plan équatorial) pour midi dun jour et du lendemain : si la déclinaison augmente, on est en **période montante**, sinon descendante. La montée (ou descente) de la Lune coïncide grosso modo avec le flux de sève vers le haut (ou le bas) dans les plantes.
3. **Longitude écliptique et signes zodiacaux** : La position de la Lune dans le **zodiaque** (son signe astrologique) se déduit de sa longitude écliptique : on divise lécliptique (360°) en 12 signes de 30°. Par exemple, Taureau (30°60°), Gémeaux (60°90°), etc. Chaque signe est associé à un élément (Terre, Eau, Air, Feu) selon la tradition agricole. On définit alors les **jours «racine/feuille/fleur/fruit»** :
- **Racine** : signes de Terre (Taureau, Vierge, Capricorne) favorise légumes racines.
- **Feuille** : signes dEau (Cancer, Scorpion, Poissons) favorise feuillage (salades, épinards).
- **Fleur** : signes dAir (Gémeaux, Balance, Verseau) favorise floraison et plantes ornementales.
- **Fruit** : signes de Feu (Bélier, Lion, Sagittaire) favorise légumes-fruits (tomates, haricots)【24†L44-L53】.
4. **Périgée et apogée** : La Lune suit une orbite elliptique (période anomalistique ≈ 27.55 j). Le **périgée** est le point où la Lune est la plus proche de la Terre, l**apogée** le plus éloigné【18†L108-L112】. Chaque lunaison comporte un périgée et un apogée. On peut les repérer en cherchant les minima/maxima locaux de la distance TerreLune jour par jour. La tradition conseille d**éviter** ces jours (trop dénergie au périgée, croissance ralentie à lapogée)【24†L66-L73】.
5. **Nœuds lunaires** : Ce sont les deux points où lorbite lunaire coupe lécliptique (plan de lorbite terrestre)【16†L155-L163】. Les nœuds correspondent aux périodes déclipses (noeud ascendant/descendant). Traditionnellement, on considère ces jours comme «perturbés» et on déconseille le jardinage【24†L66-L73】.
**Calculs utilisés (Skyfield)** : Le script Python charge léphéméride NASA DE421 pour Soleil, Terre, Lune. Il utilise `almanac.find_discrete` pour les **phases exactes** et les **nœuds**. Pour chaque jour (heure de référence = midi locale Europe/Paris), on calcule : lillumination (via la séparation Soleil-Lune), la variation de déclinaison (montante/descendante), la longitude écliptique (pour le signe). Le code exemple montre :
```python
# Phases exactes
f_phase = almanac.moon_phases(eph)
times, events = almanac.find_discrete(t0, t1, f_phase)
for t, ev in zip(times, events):
local_day = t.utc_datetime().astimezone(TZ).date()
phase_by_day[local_day] = ["Nouvelle Lune","1er Quart.","Pleine L.","Dernier Quart."][ev]
```
Cela enregistre lévénement de phase (le jour local de NL/Ple/Q1/Q3).
```python
# Illumination et montante/descendante
e = earth.at(t); v_sun = e.observe(sun).apparent(); v_moon = e.observe(moon).apparent()
sep = v_sun.separation_from(v_moon).radians
illum = (1 - math.cos(sep)) / 2 # fraction éclairée
illum2 = (1 - math.cos(v_sun2.separation_from(v_moon2).radians)) / 2
croissante = "Croissante" if illum2 >= illum else "Décroissante"
dec = v_moon.radec()[1].degrees
dec2 = v_moon2.radec()[1].degrees
montante = "Montante" if dec2 >= dec else "Descendante"
```
On compare lillumination et la déclinaison dun jour à ceux du lendemain pour décider «croissante/décroissante» et «montante/descendante».
```python
# Signe zodiacal (longitude écliptique)
lat, lon, dist = v_moon.ecliptic_latlon()
signe = SIGN_NAMES[int(lon.degrees // 30)]
type_jour = SIGN_TO_TYPE[signe] # "Racine"/"Feuille"/"Fleur"/"Fruit"
```
Cela associe chaque jour à un «type de jour» agricole selon le signe (Taureau→Racine, etc【24†L44-L53】).
Le calcul du **périgée/apogée** se fait manuellement : on mesure la distance Terre-Lune à midi chaque jour, puis on repère les minima/maxima locaux (via comparaison avec le jour précédent/suivant) pour marquer périgée et apogée. Ce choix manuel évite lAPI `almanac.moon_distance` obsolète, tout en restant suffisant pour le jardinage (un point extrême par lunaison).
## Liens avec le jardinage
Les cycles ci-dessus se traduisent en règles traditionnelles (non vérifiées scientifiquement【6†L373-L381】) :
- **Lune montante** : la sève monte, donc on **sème et récolte** (plantes aériennes, légumes-fruits)【7†L208-L214】【20†L24-L32】.
- **Lune descendante** : la sève descend, donc on **plante, repique, taille, travaille le sol** (consolidation racinaire)【7†L208-L214】【20†L24-L32】.
- **Croissante vs décroissante** : complément à montante/descendante. La lune croissante favorise les tiges/feuilles/fruits, la décroissante les racines【7†L208-L214】【20†L24-L32】.
- **Jours racine/feuille/fleur/fruit** : selon le signe zodiacal, on privilégie les cultures correspondantes【24†L44-L53】 (par ex. Taureau/Vierge/Capricorne = légumes racines, Cancer/Scorpion/Poissons = salades et choux, Gémeaux/Balance/Verseau = fleurs, Bélier/Lion/Sagittaire = tomates et haricots).
- **Éviter certains jours** : tradition recommande de ne rien faire lors des **nœuds lunaires, du périgée et de lapogée**【24†L66-L73】. Par exemple, on évite semer au périgée (supposé trop dénergie) et en période déclipse (nœuds)【22†L66-L73】.
En résumé, on obtiendrait un tableau synthétique :
| **Cycle lunaire** | **Action jardin** | **Exemple** |
|---------------------------|------------------------------------------|----------------------|
| Phase croissante | Développement aérien (semis, greffe) | Semis de tomates au 1er quartier【7†L208-L214】 |
| Phase décroissante | Consolidation racinaire (plantation) | Repiquage en lune descendante【7†L208-L214】 |
| Lune montante (ascendante)| Semis et récoltes (au-dessus du sol) | Semer haricots en lune montante【7†L208-L214】 |
| Lune descendante | Planter, tailler, travailler le sol | Planter pommes de terre en lune descendante【7†L208-L214】 |
| Jour **Racine** (signe Terre) | Légumes-racines (carottes, betteraves) | Semez carottes (Lune en Vierge)【24†L44-L53】 |
| Jour **Feuille** (signe Eau) | Feuillage (salades, épinards) | Semez laitues (Lune en Cancer)【24†L44-L53】 |
| Jour **Fleur** (signe Air) | Fleurs, plantes ornementales | Repiquer vivaces (Lune en Gémeaux)【24†L50-L53】 |
| Jour **Fruit** (signe Feu) | Légumes-fruits (tomates, haricots) | Semis tomates (Lune en Lion)【24†L52-L55】 |
| **Nœuds lunaires** | **Éviter le jardinage** (énergies perturbées) | 18+ mars (noeuds)【22†L66-L73】 |
| **Périgée / Apogée** | **Éviter ou prudence** (croissance ralentie ou maladies) | Ex.: 25 mars (périgée)【22†L69-L73】 |
Ces recommandations proviennent de la tradition jardinière et sont souvent condensées dans des calendriers lunaires grand public【24†L44-L53】【22†L66-L73】.
## Script Python : structure et explications
Le script `lunar_calendar.py` donné utilise **Skyfield** pour la précision astronomique. Principaux points techniques :
- **Dépendances** : `skyfield`, `pytz`, `numpy`. Le fichier déphémérides `de421.bsp` (NASA) couvre jusquen 2050 environ.
- **Fuseau horaire** : on fixe lheure locale «midi à Paris» pour chaque jour, afin de ne pas rater un changement de date à lUTC. On convertit en UTC pour lanalyse Skyfield (voir `TZ.localize(datetime(...)).astimezone(pytz.utc)`).
- **Phases exactes** : on utilise `almanac.moon_phases(eph)` et `find_discrete(t0,t1,f_phase)` pour obtenir les instants (UTC) des quatre phases principales. On associe ensuite la date locale correspondante :
```python
f_phase = almanac.moon_phases(eph)
phase_times, phase_events = almanac.find_discrete(t0, t1, f_phase)
phase_by_day = {}
for t, ev in zip(phase_times, phase_events):
local_day = t.utc_datetime().astimezone(TZ).date()
phase_by_day[local_day] = ["Nouvelle Lune","Premier Quartier","Pleine Lune","Dernier Quartier"][int(ev)]
```
Cette méthode assure la précision astronomique des phases (décalage horaire et lieux pris en compte).
- **Illumination (pour «croissante/décroissante»)** : on calcule à midi la fraction illuminée de la Lune par rapport au Soleil vu de la Terre. La séparation angulaire géocentrique LuneSoleil (radians) donne lillumination via `(1 - cos(sep))/2`. On compare cette fraction au jour suivant pour déterminer si la Lune croît ou décroît.
- **Montante/descendante** : on récupère la déclinaison géocentrique de la Lune (`v_moon.radec()[1].degrees`) pour deux jours consécutifs. Si elle augmente, on est en période «montante», sinon «descendante».
- **Longitude écliptique (signe)** : Skyfield fournit la longitude écliptique (`v_moon.ecliptic_latlon()`). La division par 30° détermine le signe zodiacal (0=Bélier, 30=Taureau, etc.). On mappe ensuite le signe au type de jour (racine/feuille/fleur/fruit) via une table (comme vu ci-dessus【24†L44-L53】).
- **Périgée/Apogée manuels** : comme la fonction `almanac.moon_distance` nexistait plus, on calcule la distance TerreLune chaque jour à midi. On parcourt ce tableau de distances pour repérer les minima locaux (périgée) et maxima locaux (apogée). Cest une approximation suffisante pour marquer environ un périgée et un apogée par mois.
**Limites et précisions** :
- Lalgorithme suppose un calcul quotidien à midi: il peut ne pas être précis à lheure, mais on obtient les bons jours. Pour une précision à lheure près (rarement nécessaire pour le jardinage), on pourrait affiner la recherche dévénements.
- Le fuseau Europe/Paris est appliqué partout pour obtenir la date locale. En hiver comme en été, on fixe à midi (CET ou CEST).
- Les **jours racine/feuille/fleur/fruit** sont purement conventionnels (astrologiques)【24†L44-L53】. Le choix des signes et lattribution aux «éléments» viennent de la tradition, pas de lastronomie. Dautres écoles pourraient varier légèrement ces mappings.
- Le script génère un JSON/CSV qui inclut, pour chaque date : phase, %illumination, status croissante/descroissante et montante/descendante, signe zodiacal et type de jour, périgée/apogée, nœud lunaire.
Extrait du fichier JSON produit (format JSON compatible API) :
```json
{
"date": "2026-03-14",
"phase": "",
"illumination": 67.34,
"croissante_decroissante": "Croissante",
"montante_descendante": "Montante",
"signe": "Taureau",
"type_jour": "Racine",
"perigee": false,
"apogee": false,
"noeud_lunaire": false
}
```
## Installation et test
1. **Prérequis** : Python 3.9+ installé (nous avons testé sur Python 3.13). Ouvrir un terminal.
2. **Environnement virtuel** (recommandé) :
```bash
python3 -m venv .venv
source .venv/bin/activate
```
3. **Installer dépendances** :
```bash
pip install --upgrade pip
pip install skyfield numpy pytz
```
4. **Vérifier** que `lunar_calendar.py` se trouve dans le dossier de travail.
5. **Lancer le script** :
```bash
python lunar_calendar.py
```
Au premier lancement, Skyfield télécharge `de421.bsp`.
Le script affiche «Calendrier lunaire généré» et crée `calendrier_lunaire_2026.json`.
6. **Vérifier le contenu** :
```bash
head -n 5 calendrier_lunaire_2026.json
```
ou
```bash
cat calendrier_lunaire_2026.json | jq . # (avec jq pour formatage)
```
Si une erreur survient (par ex. `ModuleNotFoundError`), vérifier lenvironnement virtuel et linstallation des librairies.
## Exemples dusage et formats de sortie
- Le script, en létat, génère un **JSON** (tableau dobjets journaliers) et peut être modifié pour produire du CSV.
- Exemple CSV attendu (point-virgule séparateur) :
```
date;phase_exacte;croissante_décroissante;montante_descendante;signe;jour_plante
2026-03-14;;Croissante;Montante;Taureau;Racine
2026-03-15;Dernier Quartier;Décroissante;Descendante;Bélier;Fruit
...
```
- Ce fichier JSON/CSV peut être importé dans une base (SQLite) ou exposé via une API (FastAPI) pour alimenter un frontend.
## Améliorations possibles et pièges à éviter
- **CLI ou paramètres** : ajouter des arguments (`--start YEAR-MON-DAY --end ...`) et `--output` pour rendre le script plus flexible.
- **FastAPI / Backend** : intégrer le calcul dans un endpoint (par ex. `/api/lune/{year}`) pour générer le calendrier à la demande ou en consulter un pré-calculé.
- **Base de données** : pré-calculer 5-10 ans et stocker dans SQLite pour accès rapide (partition par année).
- **Gestion du fuseau et locales** : tester en CET/CEST pour prendre en compte DST. Éviter lheure dhiver/été mal appliquée.
- **Front-end** : colorer le calendrier (ex. style Gruvbox : orangé=Racine, vert=Feuille, violet=Fleur, jaune=Fruit, rouge discret=Nœud). Rendre responsive (mobile/tablette).
- **Documentation** : ajouter un README dans le dépôt GitHub, expliquer les conventions (zodiaque, type de jour) et référencer les sources.
- **Précision** : pour du calcul horaire fin, on pourrait itérer en minutes autour de lheure approximative, mais pour le jardinage, le jour suffit.
## Références et lectures suggérées
- **Documentation Skyfield** exemples de calcul dangles et phases【14†L123-L131】.
- **Science et scepticisme** Détecteur de rumeurs SciencePresse (2022) et SNHF (2020) concluent à labsence deffet mesurable de la Lune sur les plantes【5†L108-L116】【6†L373-L381】.
- **Guides en français** Semencemag (2025) explique lusage pratique (jours racine/feuille/fleur/fruit, nœuds, apogée, périgée)【24†L44-L53】【22†L66-L73】. Rustica/Gerbeaud publient chaque mois des calendriers lunaires détaillés (ex. Gerbeaud, semis en «jour feuille, lune montante»【7†L93-L101】).
- **Éphémérides officielles** US Naval Observatory (phases et fraction illuminée)【10†L86-L94】, NASA HORIZONS, etc.
- **Recherche astronomique** pour approfondir : littérature sur lorbite lunaire, astronomie du calendrier, mais aussi le rapport SNHF «Jardiner avec la lune: mythe ou réalité» pour le contexte.
Ce document vise à guider à la fois les développeurs (algorithmes, code) et les jardiniers (règles pratiques). Il reste essentiel dexpérimenter et dadapter les recommandations à son jardin : un bon sol, de leau et du soleil restent les facteurs clés du succès, plus que toute influence lunaire【6†L390-L394】【22†L75-L84】.

View File

@@ -0,0 +1,293 @@
# Calendrier lunaire de jardinage Guide complet
## Résumé exécutif
Le **calendrier lunaire** de jardinage exploite la position et le cycle de la Lune (phases, déclinaison, périgée/apogée, nœuds) pour rythmer semis, plantations et récoltes. Cette approche traditionnelle, popularisée depuis le b.a.-ba de lagriculture biodynamique, associe chaque jour lunaire à un type de culture (racine/feuille/fleur/fruit) et tient compte de la Lune montante ou descendante. **Attention toutefois**: la science moderne ne confirme aucune influence directe significative de la Lune sur la croissance des plantes【6†L373-L381】【5†L108-L116】. Néanmoins, beaucoup de jardiniers lutilisent comme repère complémentaire. Ce document explique les notions astronomiques (phases, illumination, déclinaison, signes zodiacaux, périgée/apogée, nœuds), leur calcul en Python, les règles de jardinage associées, ainsi que le fonctionnement du script fourni (algorithme, limites, sortie). Des exemples de configuration (JSON/CSV) et des conseils damélioration (CLI, API, base, front-end) sont détaillés, ainsi quune section sur les dictons français du jardinage et les « saints de glace ».
## Concepts astronomiques du calendrier lunaire
- **Phases lunaires** : La phase se définit par langle Lunaire-Solaire autour de la Terre. Concrètement, on calcule la différence de longitude écliptique entre la Lune et le Soleil【14†L123-L131】. Cette différence vaut 0° pour la Nouvelle Lune, 90° pour le Premier Quartier, 180° pour la Pleine Lune, 270° pour le Dernier Quartier (modulo 360°)【14†L123-L131】. En Python (Skyfield), on utilise `almanac.find_discrete(ts0,ts1, almanac.moon_phases(eph))` pour trouver les instants précis (UTC) de chaque phase.
- **Illumination de la Lune** : Le pourcentage du disque lunaire éclairé se calcule par la géométrie SoleilTerreLune. Si θ est la séparation angulaire (en radians) entre la Lune et le Soleil vue de la Terre, alors la fraction illuminée = (1cosθ)/2. En code, `sep = v_sun.separation_from(v_moon).radians; illum = (1 - math.cos(sep))/2`. Skyfield offre aussi `moon.fraction_illuminated(sun)`, mais la formule ci-dessus est équivalente. Le script compare lillumination dun jour au lendemain pour déterminer si la Lune croît ou décroît.
- **Lune montante / descendante** : On dit «Lune montante» si la déclinaison géocentrique de la Lune (angle par rapport à léquateur céleste) augmente dun jour sur lautre. Sinon elle est «descendante». Dans le script on calcule la déclinaison (`v_moon.radec()[1].degrees`) à midi un jour et le jour suivant. Exemple :
```python
dec = v_moon.radec()[1].degrees
dec2 = v_moon2.radec()[1].degrees
montante = dec2 >= dec # True si Lune "montante"
```
La lune montante est traditionnellement favorable aux travaux aériens (semis, récoltes), la descendante aux travaux racinaires (plantation, taille).
- **Signe zodiacal (longitude écliptique)** : La position de la Lune devant le zodiaque sert à définir le type de jour (racine/feuille/fleur/fruit). On calcule la longitude écliptique lunaire (0°360°) via Skyfield (`v_moon.ecliptic_latlon()`). Le signe astrologique = int(longitude/30) (0=Bélier, 1=Taureau, …). Par convention :
- **Terre (Taureau, Vierge, Capricorne)** → *Jour Racine* (légumes-racines)【24†L44-L53】.
- **Eau (Cancer, Scorpion, Poissons)** → *Jour Feuille* (plantes feuillues)【24†L44-L53】.
- **Air (Gémeaux, Balance, Verseau)** → *Jour Fleur* (fleurs, choux-fleurs)【24†L50-L53】.
- **Feu (Bélier, Lion, Sagittaire)** → *Jour Fruit* (légumes-fruits)【24†L52-L55】.
Ces correspondances sont purement traditionnelles. Le script possède une table Python :
```python
SIGN_NAMES = ["Bélier","Taureau",…,"Poissons"]
SIGN_TO_TYPE = {
"Taureau":"Racine","Vierge":"Racine","Capricorne":"Racine",
"Cancer":"Feuille","Scorpion":"Feuille","Poissons":"Feuille",
"Gémeaux":"Fleur","Balance":"Fleur","Verseau":"Fleur",
"Bélier":"Fruit","Lion":"Fruit","Sagittaire":"Fruit"
}
signe = SIGN_NAMES[int(lon.degrees//30)]
type_jour = SIGN_TO_TYPE[signe]
```
- **Périgée / Apogée de la Lune** : Lorbite lunaire est elliptique. *Périgée* = point le plus proche de la Terre, *apogée* = point le plus éloigné【18†L108-L112】. Chaque lunaison comporte un périgée et un apogée. Skyfield na plus `almanac.moon_distance`, donc on calcule la distance TerreLune à midi chaque jour :
```python
dist = earth.at(ts.utc(date)).observe(moon).distance().km
```
On repère les minima locaux (périgée) et maxima locaux (apogée) dans la liste journalière. Ex.:
```python
distances = [earth.at(ts.utc(d.year,d.month,d.day,12,0,0)).observe(moon).distance().km for d in days]
# repérer indices i tels que dist[i] < dist[i±1] → périgée
```
Traditionnellement, on **évite de jardiner** durant ces jours (le périgée apporterait «trop dénergie», lapogée «ralentissement de croissance»)【24†L66-L73】.
- **Nœuds lunaires** : Ce sont les deux points où lorbite de la Lune coupe lécliptique【16†L155-L163】 (juste avant/après éclipses). On peut utiliser `almanac.moon_nodes(eph)` et `find_discrete` pour obtenir ces dates. Dans la pratique, les jours de nœuds sont considérés «perturbés» et déconseillés au jardinage【24†L66-L73】.
```mermaid
gantt
dateFormat YYYY-MM-DD
title Phases lunaires (Mars 2026)
section Phases
Nouvelle lune : 2026-03-03, 1d
Premier quartier : 2026-03-10, 1d
Pleine lune : 2026-03-18, 1d
Dernier quartier : 2026-03-25, 1d
```
## Liens vers les pratiques de jardinage
Les astronomes jardiniers ont formulé ces règles pratiques (purement empiriques)【7†L208-L214】【20†L24-L32】 :
- **Lune croissante** (montante) *moment daction au-dessus du sol* : semis de légumes-fruits, greffage, récolte. Exemple : on sème haricots/tomates le premier quartier【7†L208-L214】.
- **Lune décroissante** (descendante) *moment daction sur racines/sol* : plantations, repiquages, binage, taille. Ex.: planter pommes de terre en lune descendante【7†L208-L214】.
- **Jour “Racine”** (signe de Terre) : planter légumes-racines (carottes, betteraves)【24†L44-L53】.
- **Jour “Feuille”** (signe dEau) : semer feuilles et aromatiques (laitues, épinards)【24†L44-L53】.
- **Jour “Fleur”** (signe dAir) : greffer et soigner fleurs/ornementales (brocolis, roses)【24†L50-L53】.
- **Jour “Fruit”** (signe de Feu) : semer/planter légumes-fruits (tomates, courgettes, arbres fruitiers)【24†L52-L55】.
Un tableau synthétique :
| **Phase / Jour lunaire** | **Action jardin** | **Exemple** |
|-------------------------------|-----------------------------------|-----------------------------------------|
| Croissante (Nouvelle→Pleine) | Développement aérien semis/greffe| Semer tomates au premier quartier【7†L208-L214】 |
| Décroissante (Pleine→Nouvelle)| Consolidation planter, tailler | Planter pommes de terre en lune descendante【7†L208-L214】 |
| Lune montante | Semis/engrais/ récoltes | Récolter herbes aromatiques【7†L208-L214】 |
| Lune descendante | Planter/tailler/travailler le sol | Repiquer laitues, tailler rosiers【7†L208-L214】 |
| Jour **Racine** (Terre) | Légumes-racines (oignons, navets) | Semer carottes (Lune en Taureau)【24†L44-L53】 |
| Jour **Feuille** (Eau) | Laitues, choux, épinards | Semer épinards (Lune en Cancer)【24†L44-L53】 |
| Jour **Fleur** (Air) | Fleurs, brocolis, vivaces | Planter choux-fleurs (Lune en Balance)【24†L50-L53】 |
| Jour **Fruit** (Feu) | Tomates, haricots, pois | Semer tomates (Lune en Lion)【24†L52-L55】 |
| Nœuds lunaires | *Éviter tout travail* | (période déclipse, jours “perturbés”)【24†L66-L73】 |
| Périgée / Apogée | *Éviter/attention* | Récoltes précoces, éviter tailes risquées【24†L66-L73】 |
En pratique, on imprime souvent un calendrier lunaire annuel (papier ou appli mobile) pour suivre ces repères【24†L54-L60】【22†L66-L73】. À titre dexemple, voici la correspondance **signes zodiacaux → type de jour**, sous forme tabulaire :
| Signe zodiacal | Élément | Type de jour | Exemples de cultures |
|-----------------------------|---------|--------------|--------------------------------------|
| Bélier, Lion, Sagittaire | Feu | Fruit | Tomates, poivrons, arbres fruitiers |
| Taureau, Vierge, Capricorne | Terre | Racine | Carottes, pommes de terre, oignons |
| Gémeaux, Balance, Verseau | Air | Fleur | Fleurs, choux-fleurs, aromatiques |
| Cancer, Scorpion, Poissons | Eau | Feuille | Laitues, épinards, choux, salades |
Ces associations sont présentées par exemple dans Semencemag【24†L44-L53】.
## Le script Python : description technique
Le script `lunar_calendar.py` (Python 3.9+) génère un calendrier lunaire sur une période donnée. Points clés du fonctionnement :
- **Dépendances** : `skyfield` (pour lastronomie), `pytz` (timezones), `numpy`. `de421.bsp` est téléchargé automatiquement (éphéméride NASA).
- **Période de calcul** : par défaut un an (Jan→Déc). On peut modifier `start` et `end` dans la section `__main__`.
- **Fuseau horaire** : Europe/Paris. On prend lheure locale *midi* pour éviter les transitions de date, puis on convertit en UTC pour Skyfield :
```python
TZ = pytz.timezone("Europe/Paris")
local_noon = TZ.localize(datetime(year,month,day,12))
t = ts.utc(local_noon.astimezone(pytz.utc))
```
Ceci garantit que chaque date du calendrier correspond bien au jour solaire local.
- **Phases exactes** :
```python
f_phase = almanac.moon_phases(eph)
phase_times, phase_events = almanac.find_discrete(t0, t1, f_phase)
phase_by_day = {}
for t, ev in zip(phase_times, phase_events):
local_day = t.utc_datetime().astimezone(TZ).date()
phase_by_day[local_day] = ["Nouvelle Lune","Premier Quartier","Pleine Lune","Dernier Quartier"][int(ev)]
```
On récupère ainsi les jours (par date locale) où surviennent exactement la NL, PQ, PL, DQ. Ces étiquettes sont stockées dans `phase_by_day`.
- **Illumination et montante/descendante** : Pour chaque jour `d`, on calcule :
```python
e = earth.at(t) # position de la Terre à midi UTC
v_sun = e.observe(sun).apparent()
v_moon = e.observe(moon).apparent()
sep = v_sun.separation_from(v_moon).radians
illum = (1 - math.cos(sep)) / 2 # fraction (0..1) éclairée
```
Puis pour le lendemain, même calcul (`illum2`, `dec2`). On définit :
```python
croissante = "Croissante" if illum2 >= illum else "Décroissante"
dec = v_moon.radec()[1].degrees
dec2 = v_moon2.radec()[1].degrees
montante = "Montante" if dec2 >= dec else "Descendante"
```
Cest-à-dire la Lune est “montante” si sa déclinaison augmente.
- **Signe zodiacal → type de jour** : Toujours à midi, on récupère la longitude écliptique :
```python
lat, lon, dist = v_moon.ecliptic_latlon()
signe = SIGN_NAMES[int(lon.degrees // 30) % 12]
type_jour = SIGN_TO_TYPE[signe]
```
Ainsi on remplit `signe` (ex. “Taureau”) et `type_jour` (“Racine”, etc) pour chaque date.
- **Périgée/Apogée manuel** : Après avoir construit une liste quotidienne de distances (voir ci-dessus), on parcourt les valeurs : si `dist[i] < dist[i-1]` et `< dist[i+1]`, cest un **périgée** (jour local minimal). Inversement pour un **apogée**. Ce repérage simple identifie un périgée et un apogée par lunaison. Exemple :
```python
if distances[i] < distances[i-1] and distances[i] < distances[i+1]:
perigee_days.add(all_days[i])
if distances[i] > distances[i-1] and distances[i] > distances[i+1]:
apogee_days.add(all_days[i])
```
Ces jours sont marqués dans lexport pour information.
- **Nœuds lunaires** : On utilise directement `almanac.moon_nodes(eph)` et `find_discrete` entre `t0` et `t1`. On convertit chaque instant en date locale pour obtenir `node_days`.
**Limitations et précision** :
- Le calcul se fait au pas dun jour (midi). Il nest donc pas dune précision horaire au-delà du jour (assez pour un calendrier de plantation).
- Le passage entre heures dété/hiver est géré par `pytz`.
- Les assignations *racine/feuille/fleur/fruit* reposent sur des conventions astrologiques. Elles sont cohérentes avec la littérature francophone (Semencemag【24†L44-L53】, Rustica, etc.) mais non scientifiques.
- Les algorithmes Skyfield sont précautionneusement utilisés pour donner des résultats très fiables sur plusieurs décennies.
## Installation et tests
1. **Python 3.9+** : Vérifier (`python3 --version`).
2. **Environnement virtuel** (optionnel mais recommandé) :
```bash
cd /path/to/projet
python3 -m venv .venv
source .venv/bin/activate # prompt indique (.venv)
```
3. **Installer les dépendances** :
```bash
pip install --upgrade pip
pip install skyfield numpy pytz
```
Vérifier : `pip list` doit lister `skyfield`, `numpy`, `pytz`.
4. **Lancer le script** (`lunar_calendar.py`) :
```bash
python lunar_calendar.py
```
Au premier lancement, `skyfield` télécharge automatiquement `de421.bsp`.
Un message “Calendrier lunaire généré” doit safficher. Le fichier `calendrier_lunaire_2026.json` (ou défini dans le script) est créé.
5. **Vérifier le résultat** :
```bash
head -n 5 calendrier_lunaire_2026.json
```
ou
```bash
cat calendrier_lunaire_2026.json | jq .
```
Exemple de ligne JSON produite :
```json
{
"date": "2026-03-14",
"phase": "",
"illumination": 67.34,
"croissante_decroissante": "Croissante",
"montante_descendante": "Montante",
"signe": "Taureau",
"type_jour": "Racine",
"perigee": false,
"apogee": false,
"noeud_lunaire": false
}
```
Cette ligne indique quau 14/03/2026, la Lune est en *Taureau* (Jour Racine), croissante et montante, sans phase particulière ni événement spécial.
**Commandes utiles** :
- Tester limport Skyfield : `python -c "from skyfield.api import load; print('Skyfield OK')"`
- Debug : ajouter `print` pour les valeurs (illumination, décli, etc.) si nécessaire.
## Exemples dusage et sorties
Le script génère un **JSON** (tableau dobjets quotidiens). On peut facilement adapter pour un **CSV**. Par exemple, le module Python `csv` est prêt à lemploi (démontré dans le code source). Les champs exportés sont : date, phase, « croissante/décroissante », « montante/descendante », signe, type de jour, booleans périgée/apogée/nœud.
Un exemple de format CSV (séparateur `;`) :
```csv
date;phase_exacte;croissante_décroissante;montante_descendante;signe;type_jour;perigee;apogee;noeud
2026-03-14;;Croissante;Montante;Taureau;Racine;0;0;0
2026-03-15;Dernier Quartier;Décroissante;Descendante;Bélier;Fruit;0;0;0
...
```
Ce fichier peut être importé en base de données (SQLite) ou servi via une API (FastAPI) pour alimenter une interface web/mobile.
## Améliorations possibles et pièges à éviter
- **Arguments en ligne de commande** : utiliser `argparse` pour accepter `--start`, `--end`, `--format` (JSON/CSV).
- **FastAPI ou Flask** : créer un endpoint `/api/lune/{year}` qui lit le JSON pré-calculé ou exécute dynamiquement le calcul. Attention à la latence du calcul si fait à la volée (mieux pré-calculer).
- **Base de données** : pré-calculer plusieurs années (510 ans) et stocker en SQLite avec une table indexée sur date. Permet dinterroger rapidement pour une date donnée.
- **Timezones** : toujours utiliser `pytz` et `ASTimezone` pour éviter les décalages DST erronés. Tester en hiver/été.
- **Précision** : le calcul dévénements précis (phase à lheure près) est assuré par Skyfield. Pour le quotidien, on sen tient au repère “jour où lévénement tombe (UTC→local)”.
- **Interface graphique** : ajouter un calendrier réactif (HTML/CSS/JS) coloré par type de jour (ex. : orange racine, vert feuille, violet fleur, jaune fruit), marquer les événements spéciaux (noeuds en rouge discret, périgée/apogée en gris). Gruvbox ou autre thème sombre/contrasté pour développeurs.
- **Stockage des données** : suggestions JSON/CSV ci-dessus, ou génération de JSON à partir de SQLite. Ex:
```sql
CREATE TABLE lune (
date TEXT PRIMARY KEY,
phase TEXT, lumiere REAL,
croiss_dec TEXT, mont_dec TEXT,
signe TEXT, type_jour TEXT,
perigee INTEGER, apogee INTEGER, noeud INTEGER
);
```
- **Documentation** : ajouter des tests unitaires, du logging, et un README (vous êtes ici !).
## Dictons et proverbes populaires du jardinage
La tradition française regorge de **dictons et proverbes** relatifs aux saisons et au jardinage. En voici quelques exemples :
- « À chaque plante son temps, à chaque temps sa plante» on plante/sème selon la saison appropriée【32†L118-L121】.
- «Tel est le jardinier, tel est le jardin» létat du potager reflète les soins du jardinier【32†L105-L109】.
- «En avril, ne te découvre pas dun fil» prudence contre les dernières gelées tardives.
- «Jamais trop tôt pour semer, jamais trop tard pour récolter» planter semis précoces et récolter tardivement.
- «La patience est la mère des jardiniers» la réussite vient avec lobservation et le temps【32†L158-L161】.
Ces dictons reflètent lobservation empirique. Aucune librairie Python spécifique aux proverbes français nest connue. On peut les stocker dans un fichier JSON ou CSV pour usage interne. Par exemple, un format JSON possible :
```json
[
{
"dicton": "En avril, ne te découvre pas d'un fil",
"signification": "Ne pas ôter les protections trop tôt car les gelées peuvent revenir tardivement.",
"source": "Proverbe populaire"
},
{
"dicton": "À chaque plante son temps, à chaque temps sa plante",
"signification": "Chaque semis/plantation doit se faire en fonction de la saison appropriée.",
"source": "Santamaria Motoculture【32†L118-L121】"
}
]
```
On ajoutera «source» ou «conseil associé» selon besoins. Si besoin de proverbes automatiques, on utilisera plutôt une API publique de citations (ex. «Proverbes français» non automatique) plutôt quune librairie locale.
## Calendrier des saints de glace (France)
En France, de nombreux dictons sappuient sur le **calendrier des saints**. Les plus célèbres pour le jardinage sont les **Saints de Glace** (traditionnellement 11, 12, 13 mai Mamert, Pancrace, Servais) et les saints qui les prolongent (Yves 19/5, Urbain 25/5). Ces dates marquent la fin présumée des gelées printanières. Exemples de dictons associés【39†L155-L164】【42†L209-L212】 :
- **11 mai (St Mamert), 12 mai (St Pancrace), 13 mai (St Servais)** : *« Avant Saint-Servais, point dété; après Saint-Servais, plus de gelée. »*【39†L155-L164】 conseille dattendre la mi-mai.
- **Saint-Urbain (25 mai)** : *« Quand la Saint-Urbain est passée, le vigneron est rassuré. »*【42†L209-L212】 (fin définitive du risque de gel).
- Variante : *« Mamert, Pancrace, Servais sont trois saints de glace, mais Saint-Urbain les tient tous dans sa main. »*【42†L209-L212】.
- **Saint-Pancrace (12/5), St-Servais (13/5), St-Boniface (14/5)** : *« Saints Pancrace, Servais et Boniface apportent souvent de la glace. »*【42†L209-L212】.
Le *calendrier des saints* est large : on trouve par région dautres saints réputés «glaçants» en avril (Georges 23/4, Marc 25/4, etc.). Mais pour la France métropolitaine, cest la période mi-mai qui domine ces dictons. En résumé : mieux vaut repousser linstallation des cultures sensibles au froid (tomates, etc.) jusquà fin mai【39†L155-L164】【40†L81-L88】.
## Références et lectures suggérées
- **Skyfield API** Exemples de calculs astronomiques (phases, positions)【14†L123-L131】.
- **Documentation SO/USNO** Éphémérides officielles pour la Lune (phases, illumination)【18†L108-L112】.
- **Journaux et blogs FR** Articles de vulgarisation : Semencemag (juin 2025) sur lusage du calendrier lunaire【24†L44-L53】, Rustica, Gerbeaud.
- **Sources historiques** Dictons et fêtes des saints : «Les saints de glace» sur le Potager Permacole【42†L209-L212】, revue Science et Vie (SNHF) pour le scepticisme scientifique【6†L373-L381】.
- **Ressources additionnelles** : RFC et documentation FastAPI, tutoriels Skyfield (rhodesmill.org), bases de données open (p. ex. base de dictons BotAccess).
Ce README est prêt à être sauvegardé comme document `README.md`. Il offre un point de départ complet pour un projet de **webapp jardinage** incorporant un calendrier lunaire.

View File

@@ -0,0 +1,5 @@
source .venv/bin/activate
pip install --upgrade pip
pip install skyfield numpy pytz

View File

@@ -0,0 +1,396 @@
from __future__ import annotations
from dataclasses import dataclass, asdict, field
from datetime import date, datetime, timedelta
import math
import json
from pathlib import Path
import pytz
from skyfield.api import load, wgs84, load_constellation_map
from skyfield import almanac
TZ = pytz.timezone("Europe/Paris")
SCRIPT_DIR = Path(__file__).resolve().parent
LATITUDE = 48.8566
LONGITUDE = 2.3522
# --- Mapping "jour racine/feuille/fleur/fruit" ---
# We align with a sidereal approach using the Moon's constellation.
CONSTELLATION_TO_SIGN = {
"Ari": "Bélier",
"Tau": "Taureau",
"Gem": "Gémeaux",
"Cnc": "Cancer",
"Leo": "Lion",
"Vir": "Vierge",
"Lib": "Balance",
"Sco": "Scorpion",
"Sgr": "Sagittaire",
"Cap": "Capricorne",
"Aqr": "Verseau",
"Psc": "Poissons",
# The Moon can cross Ophiuchus in official IAU boundaries.
# We map it to Scorpion for gardening day continuity.
"Oph": "Scorpion",
}
SIGN_TO_TYPE = {
"Taureau": "Racine", "Vierge": "Racine", "Capricorne": "Racine",
"Cancer": "Feuille", "Scorpion": "Feuille", "Poissons": "Feuille",
"Gémeaux": "Fleur", "Balance": "Fleur", "Verseau": "Fleur",
"Bélier": "Fruit", "Lion": "Fruit", "Sagittaire": "Fruit",
}
@dataclass
class DayInfo:
date: str
phase: str
illumination: float
croissante_decroissante: str
montante_descendante: str
signe: str
type_jour: str
soleil_lever: str
soleil_coucher: str
duree_jour: str
lune_lever: str
lune_coucher: str
duree_presence_lune: str
saint_du_jour: str
saint_de_glace: bool
perigee: bool
apogee: bool
noeud_lunaire: bool
transitions_type_jour: list[dict[str, str]] = field(default_factory=list)
transitions_montante_descendante: list[dict[str, str]] = field(default_factory=list)
def _zodiac_sign_from_constellation(constellation_at, position) -> str:
abbr = constellation_at(position)
return CONSTELLATION_TO_SIGN.get(abbr, "Scorpion")
def _local_noon(d: date) -> datetime:
return TZ.localize(datetime(d.year, d.month, d.day, 12, 0, 0))
def _default_saints_france() -> dict[str, str]:
# Core gardening references in France; full calendar can be provided via saints_france.json.
return {
"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",
}
def _load_saints_france() -> dict[str, str]:
path = SCRIPT_DIR / "saints_dictons" / "saints_france.json"
if not path.exists():
return _default_saints_france()
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
saints: dict[str, str] = {}
for key, value in data.items():
if isinstance(key, str) and isinstance(value, str):
saints[key] = value.strip()
return saints
def _compute_perigee_apogee_days(ts, earth, moon, start: date, end: date) -> tuple[set[date], set[date]]:
# Hourly sampling + one-day padding on each side gives stable local extrema detection.
sample_start = datetime.combine(start - timedelta(days=1), datetime.min.time())
sample_end = datetime.combine(end + timedelta(days=1), datetime.max.time().replace(microsecond=0))
samples: list[tuple[date, float]] = []
current = TZ.localize(sample_start)
end_local = TZ.localize(sample_end)
step = timedelta(hours=1)
while current <= end_local:
t = ts.utc(current.astimezone(pytz.utc))
dist_km = earth.at(t).observe(moon).distance().km
samples.append((current.date(), dist_km))
current += step
perigee_days: set[date] = set()
apogee_days: set[date] = set()
for i in range(1, len(samples) - 1):
day, dist = samples[i]
if not (start <= day <= end):
continue
prev_dist = samples[i - 1][1]
next_dist = samples[i + 1][1]
if dist < prev_dist and dist < next_dist:
perigee_days.add(day)
if dist > prev_dist and dist > next_dist:
apogee_days.add(day)
return perigee_days, apogee_days
def _to_local_dt(t) -> datetime:
return t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ)
def _pick_first_event_within_window(
ts,
observer,
target,
start_local: datetime,
end_local: datetime,
event_kind: str,
) -> tuple[datetime | None, int | None]:
if event_kind == "rise":
event_func = almanac.find_risings
else:
event_func = almanac.find_settings
t0 = ts.utc(start_local.astimezone(pytz.utc))
t1 = ts.utc(end_local.astimezone(pytz.utc))
times, flags = event_func(observer, target, t0, t1)
for t, ok in zip(times, flags):
if not ok:
continue
dt_local = _to_local_dt(t)
if start_local <= dt_local < end_local:
day_offset = (dt_local.date() - start_local.date()).days
return dt_local, day_offset
return None, None
def _format_time(dt_local: datetime | None, day_offset: int | None) -> str:
if dt_local is None:
return ""
base = dt_local.strftime("%H:%M")
if day_offset and day_offset > 0:
return f"{base} (+{day_offset}j)"
return base
def _format_duration(start_dt: datetime | None, end_dt: datetime | None) -> str:
if start_dt is None or end_dt is None:
return ""
delta = end_dt - start_dt
if delta.total_seconds() < 0:
return ""
total_minutes = int(round(delta.total_seconds() / 60))
hours, minutes = divmod(total_minutes, 60)
return f"{hours:02d}h{minutes:02d}"
def _moon_type_jour_at(ts, earth, moon, constellation_at, local_dt: datetime) -> str:
t = ts.utc(local_dt.astimezone(pytz.utc))
v_moon = earth.at(t).observe(moon).apparent()
signe = _zodiac_sign_from_constellation(constellation_at, v_moon)
return SIGN_TO_TYPE[signe]
def _moon_montante_descendante_at(ts, earth, moon, local_dt: datetime) -> str:
t = ts.utc(local_dt.astimezone(pytz.utc))
t2 = ts.utc((local_dt + timedelta(minutes=30)).astimezone(pytz.utc))
v_moon = earth.at(t).observe(moon).apparent()
v_moon2 = earth.at(t2).observe(moon).apparent()
dec = v_moon.radec()[1].degrees
dec2 = v_moon2.radec()[1].degrees
return "Montante" if dec2 >= dec else "Descendante"
def _find_transition_time(
value_at,
left_dt: datetime,
right_dt: datetime,
left_value: str,
) -> datetime:
# Binary search at minute precision for the first instant where value changes.
while (right_dt - left_dt) > timedelta(minutes=1):
mid = left_dt + (right_dt - left_dt) / 2
if value_at(mid) == left_value:
left_dt = mid
else:
right_dt = mid
return right_dt.replace(second=0, microsecond=0)
def _compute_daily_transitions(
value_at,
day_start: datetime,
day_end: datetime,
step_minutes: int = 20,
) -> list[dict[str, str]]:
transitions: list[dict[str, str]] = []
step = timedelta(minutes=step_minutes)
t = day_start
current_value = value_at(t)
while t < day_end:
probe = min(t + step, day_end)
probe_value = value_at(probe)
if probe_value != current_value:
transition_dt = _find_transition_time(value_at, t, probe, current_value)
transitions.append(
{
"heure": transition_dt.strftime("%H:%M"),
"avant": current_value,
"apres": probe_value,
}
)
current_value = probe_value
t = probe
return transitions
def build_calendar(start: date, end: date) -> list[DayInfo]:
if end < start:
raise ValueError(f"Invalid date range: start ({start}) is after end ({end}).")
ts = load.timescale()
eph = load("de421.bsp")
constellation_at = load_constellation_map()
saints_by_mmdd = _load_saints_france()
saints_de_glace = {"05-11", "05-12", "05-13", "05-14", "05-25"}
earth, moon, sun = eph["earth"], eph["moon"], eph["sun"]
observer = earth + wgs84.latlon(LATITUDE, LONGITUDE)
t0 = ts.utc(start.year, start.month, start.day)
t1 = ts.utc(end.year, end.month, end.day + 1)
# --- Phases exactes ---
f_phase = almanac.moon_phases(eph)
phase_times, phase_events = almanac.find_discrete(t0, t1, f_phase)
phase_by_day: dict[date, str] = {}
for t, ev in zip(phase_times, phase_events):
local_day = t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ).date()
phase_by_day[local_day] = ["Nouvelle Lune", "Premier Quartier",
"Pleine Lune", "Dernier Quartier"][int(ev)]
# --- Nœuds lunaires (instants) ---
f_nodes = almanac.moon_nodes(eph)
node_times, _ = almanac.find_discrete(t0, t1, f_nodes)
node_days: set[date] = set()
for t in node_times:
local_day = t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ).date()
node_days.add(local_day)
# --- Périgée / apogée : calcul manuel via distance Terre->Lune (min/max locaux) ---
perigee_days, apogee_days = _compute_perigee_apogee_days(ts, earth, moon, start, end)
# --- Boucle jour par jour ---
result: list[DayInfo] = []
d = start
while d <= end:
# midi local : stabilise signe du jour + évite bascules UTC
local_noon = _local_noon(d)
local_day_start = TZ.localize(datetime(d.year, d.month, d.day, 0, 0, 0))
local_day_end = local_day_start + timedelta(days=1)
local_moon_window_end = local_day_start + timedelta(days=2)
t = ts.utc(local_noon.astimezone(pytz.utc))
e = earth.at(t)
v_sun = e.observe(sun).apparent()
v_moon = e.observe(moon).apparent()
# illumination (0..1) via séparation soleil-lune
sep = v_sun.separation_from(v_moon).radians
illum = (1 - math.cos(sep)) / 2
# lendemain (pour croissante/décroissante + montante/descendante)
d2 = d + timedelta(days=1)
local_noon2 = _local_noon(d2)
t2 = ts.utc(local_noon2.astimezone(pytz.utc))
e2 = earth.at(t2)
v_sun2 = e2.observe(sun).apparent()
v_moon2 = e2.observe(moon).apparent()
sep2 = v_sun2.separation_from(v_moon2).radians
illum2 = (1 - math.cos(sep2)) / 2
croissante = "Croissante" if illum2 >= illum else "Décroissante"
dec = v_moon.radec()[1].degrees
dec2 = v_moon2.radec()[1].degrees
montante = "Montante" if dec2 >= dec else "Descendante"
# sidereal sign via Moon constellation
signe = _zodiac_sign_from_constellation(constellation_at, v_moon)
type_jour = SIGN_TO_TYPE[signe]
mmdd = f"{d.month:02d}-{d.day:02d}"
sun_rise_dt, sun_rise_offset = _pick_first_event_within_window(
ts, observer, sun, local_day_start, local_day_end, "rise"
)
sun_set_dt, sun_set_offset = _pick_first_event_within_window(
ts, observer, sun, local_day_start, local_day_end, "set"
)
moon_rise_dt, moon_rise_offset = _pick_first_event_within_window(
ts, observer, moon, local_day_start, local_moon_window_end, "rise"
)
moon_set_dt, moon_set_offset = _pick_first_event_within_window(
ts, observer, moon, local_day_start, local_moon_window_end, "set"
)
transitions_type_jour = _compute_daily_transitions(
lambda dt: _moon_type_jour_at(ts, earth, moon, constellation_at, dt),
local_day_start,
local_day_end,
)
transitions_montante_descendante = _compute_daily_transitions(
lambda dt: _moon_montante_descendante_at(ts, earth, moon, dt),
local_day_start,
local_day_end,
)
result.append(DayInfo(
date=d.isoformat(),
phase=phase_by_day.get(d, ""),
illumination=round(illum * 100.0, 2), # %
croissante_decroissante=croissante,
montante_descendante=montante,
signe=signe,
type_jour=type_jour,
soleil_lever=_format_time(sun_rise_dt, sun_rise_offset),
soleil_coucher=_format_time(sun_set_dt, sun_set_offset),
duree_jour=_format_duration(sun_rise_dt, sun_set_dt),
lune_lever=_format_time(moon_rise_dt, moon_rise_offset),
lune_coucher=_format_time(moon_set_dt, moon_set_offset),
duree_presence_lune=_format_duration(moon_rise_dt, moon_set_dt),
transitions_type_jour=transitions_type_jour,
transitions_montante_descendante=transitions_montante_descendante,
saint_du_jour=saints_by_mmdd.get(mmdd, ""),
saint_de_glace=(mmdd in saints_de_glace),
perigee=(d in perigee_days),
apogee=(d in apogee_days),
noeud_lunaire=(d in node_days),
))
d += timedelta(days=1)
return result
if __name__ == "__main__":
data = build_calendar(date(2026, 1, 1), date(2026, 12, 31))
out_path = Path(__file__).with_name("calendrier_lunaire_2026.json")
with out_path.open("w", encoding="utf-8") as f:
json.dump([asdict(x) for x in data], f, ensure_ascii=False, indent=2)
print(f"Calendrier lunaire généré : {out_path}")

View File

@@ -0,0 +1,266 @@
Voici des éléments structurés essentiels pour écrire un tutoriel de scraping Python à partir du site saint-dicton.com, en particulier pour la page dun jour précis (par exemple https://www.saint-dicton.com/0222.html) :
📌 Structure observée du site Saint-Dicton
Un exemple de page date contient :
Liste des saints fêtés ce jour
Exemples : “St-Sulpice Sévère”, “St-Valère”, etc.
Ce bloc est présenté sous forme de texte HTML listé en paragraphes ou sections.
Phase de la lune
Peut être présente mais nest pas structurée pour le scraping des saints.
Dicton du jour
Un bloc intitulé “## Dicton du jour” suivi dun ou plusieurs dictons.
Prénoms à fêter
Peut contenir une liste ou être vide.
🧠 Analyse de la pagination annuelle
Le site propose également une navigation jour par jour :
LURL pour une date donnée prend la forme https://www.saint-dicton.com/MMDD.html où MM est le mois sur deux chiffres et DD le jour sur deux chiffres (par exemple 0222 pour 22 février).
Il existe une page par mois avec tous les saints par ordre alphabétique (ex. SaintsA.html).
Ceci permet de parcourir toutes les dates de lannée systématiquement.
📘 Sources externes utiles
Pour enrichir ton scraping :
Nominis propose une base de plusieurs milliers de saints et fêtes liturgiques (utile pour comparer).
Des sites comme Éphéméride ou Wiki des proverbes donnent des dictons et proverbes météo/agricoles si besoin de compléter la base. (source non spécifique, généraliste).
🧪 Extrait de page (exemple réel)
Dans la page 0222.html, on observe :
## Dicton du jour
S'il gèle à la Saint-Sulpice,
Le printemps sera propice
Ici :
“Dicton du jour” est un titre h2
Le dicton est une ligne ou plusieurs lignes de texte juste après ce titre.
📌 Tutoriel ciblé pour écrire scrap_saint.md
Dans ce fichier .md, tu pourras expliquer :
Objectif du scraping
Extraire pour chaque date :
date (MM-DD)
nom du saint (ou saints)
dictons du jour
éventuellement prénoms associés
Analyse HTML du site
Les pages individuelles des dates
Les sections à scraper
Comment repérer les titres HTML des dictons
Étapes du script Python
itérer sur toutes les dates de lannée
fetch des pages MMDD.html
parser lHTML avec BeautifulSoup
extraire listes de saints
extraire bloc dictons
stocker dans une structure CSV ou JSON
Exemple de structure de sortie
{
"date": "0222",
"saints": ["St-Sulpice Sévère", "St-Valère"],
"dictons": ["S'il gèle à la Saint-Sulpice, …"]
}
Conseils pour éviter les pièges
certaines dates ont plusieurs dictons
structure HTML parfois variable
gérer les accents et encodage UTF-8
Si tu veux, je peux te générer directement le contenu complet de ce fichier scrap_saint.md avec un script Python détaillé (requests + BeautifulSoup) prêt à copier.
Dis-moi si tu veux aussi inclure le scraping du contenu de la page alphabétique SaintsA.html pour récupérer la liste complète des noms de saints.
genere scrap.md
Voici un tutoriel complet scrap_saint.md pour taider à scraper les pages de saints et leurs dictons à partir du site https://www.saint-dicton.com/0222.html
et des pages annuelles correspondantes. Ce fichier explique lanalyse de la structure, un exemple concret de script Python, et des conseils pour boucler sur toute lannée.
# scrap_saint.md
## Objectif
Ce tutoriel explique comment *scraper automatiquement* les informations suivantes pour chaque jour de lannée à partir du site **saint-dicton.com** :
- la **date** format MMDD (ex : *0222*)
- le(s) **nom(s) du saint** fêté(s)
- le ou les **dictons associés au jour**
Lobjectif est de générer une base de données annuelle (CSV/JSON) des saints et des dictons, utilisable ensuite dans un projet Python par exemple.
---
## Analyse de la structure du site
Le site propose une **page par date** selon le format dURL :
https://www.saint-dicton.com/MMDD.html
Exemples :
- https://www.saint-dicton.com/0222.html → *Saint-Sulpice Sévère* + dicton du jour :contentReference[oaicite:0]{index=0}
- https://www.saint-dicton.com/0208.html → *dicton de la Saint-Jean* :contentReference[oaicite:1]{index=1}
Chaque page contient typiquement :
1. **Fêtes du jour**
Une section listant un ou plusieurs saints (ex : *St-Sulpice Sévère*, *St-Valère*) :contentReference[oaicite:2]{index=2}
2. **Bloc Dicton du jour**
Titre suivi du texte du dicton (souvent une ou plusieurs lignes) :contentReference[oaicite:3]{index=3}
3. **(Optionnel) Prénoms à fêter**
Liste de prénoms associés à la date :contentReference[oaicite:4]{index=4}
> Le site propose aussi une page daccueil et des pages alphabétiques, mais pour extraire une *base annuelle*, le patron `MMDD.html` est utile pour itérer sur chaque jour de lannée. :contentReference[oaicite:5]{index=5}
---
## Pré-requis
Installer les dépendances Python nécessaires :
```bash
pip install requests beautifulsoup4
Optionnel mais recommandé :
pip install lxml
Exemple de script Python (scraper)
Ce script parcourt toutes les dates de lannée, récupère chaque page, analyse lHTML et enregistre les données dans un fichier CSV.
import requests
from bs4 import BeautifulSoup
import csv
import time
BASE_URL = "https://www.saint-dicton.com/{:02d}{:02d}.html"
def scrape_day(month: int, day: int):
url = BASE_URL.format(month, day)
resp = requests.get(url)
if resp.status_code != 200:
return None
soup = BeautifulSoup(resp.text, "lxml")
# Extraire les noms de saints
saints = []
h1 = soup.find("h1")
if h1:
# souvent la liste des saints est en texte dans la partie principale
for line in h1.text.split("\n"):
if line.strip():
saints.append(line.strip())
# trouver le div ou section contenant "Dicton du jour"
dicton_data = ""
target = soup.find(text="Dicton du jour")
if target:
parent = target.find_parent()
if parent:
# juste après ce bloc, prendre les lignes de dicton
for p in parent.find_next_siblings():
text = p.get_text(strip=True)
if text:
dicton_data += text + " "
return {
"month": month,
"day": day,
"saints": saints,
"dicton": dicton_data.strip()
}
# Boucle sur toute l'année
with open("saints_dictons.csv", "w", newline="", encoding="utf-8") as csvfile:
fieldnames = ["month", "day", "saints", "dicton"]
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=";")
writer.writeheader()
for m in range(1, 13):
for d in range(1, 32):
data = scrape_day(m, d)
if data:
writer.writerow({
"month": data["month"],
"day": data["day"],
"saints": "|".join(data["saints"]),
"dicton": data["dicton"]
})
print(f"Scraped {m:02d}-{d:02d}")
time.sleep(1) # pause pour éviter surcharge
Bonnes pratiques & conseils
💡 Respect et performance
Respecte les règles dutilisation du site. Le scraping est légitime sur du contenu public, mais il faut éviter une surcharge du serveur.
Met une pause (time.sleep(1)) entre les requêtes pour ne pas déclencher de blocage ou DDoS.
🧠 Inspection du HTML
Avant dextraire le texte, inspecte la page dans un navigateur (clic droit → Inspecter) pour confirmer la structure des balises.
Le site nutilise pas dAPI JSON, donc BeautifulSoup est adapté.
⚠️ Variations possibles
La structure peut varier légèrement dune date à lautre (par ex., plusieurs saints listés en texte, parfois des paragraphes additionnels).
Adaptation des sélecteurs (find, find_all, classes ou IDs) peut être nécessaire selon les pages.
Sorties possibles
À partir du script ci-dessus, tu peux générer :
un CSV (comme ci-dessous)
un JSON structuré
un SQLite pour intégration dans ton application jardinage
Exemple denregistrement CSV :
month;day;saints;dicton
2;22;St-Sulpice Sévère|St-Valère;"S'il gèle à la Saint-Sulpice, Le printemps sera propice"
Conclusion
En parcourant les URL de MMDD.html et en analysant lHTML, tu peux extraire automatiquement pour toute lannée la liste des saints et leurs dictons associés.
Ce fichier scrap_saint.md te fournit une base de départ claire avec un script Python prêt à lemploi.

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
from datetime import date
from pathlib import Path
DATE_HEADING_RE = re.compile(
r"^\s{0,3}(?:#{1,6}\s*)?((?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01])|(?:0[1-9]|[12][0-9]|3[01])[-/](?:0[1-9]|1[0-2])|(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01]))\s*$"
)
INLINE_DATE_DIC_RE = re.compile(
r"^\s*((?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01])|(?:0[1-9]|[12][0-9]|3[01])[-/](?:0[1-9]|1[0-2])|(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01]))\s*[:\-]\s*(.+)$"
)
def _normalize_mmdd(token: str) -> str | None:
token = token.strip()
if re.fullmatch(r"(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])", token):
month = token[:2]
day = token[2:]
elif re.fullmatch(r"(0[1-9]|1[0-2])[-/](0[1-9]|[12][0-9]|3[01])", token):
month, day = re.split(r"[-/]", token)
elif re.fullmatch(r"(0[1-9]|[12][0-9]|3[01])[-/](0[1-9]|1[0-2])", token):
day, month = re.split(r"[-/]", token)
else:
return None
return f"{month}-{day}"
def _unique(values: list[str]) -> list[str]:
out: list[str] = []
seen: set[str] = set()
for value in values:
v = value.strip()
if not v:
continue
if v not in seen:
seen.add(v)
out.append(v)
return out
def _split_saints(text: str) -> list[str]:
# Normalize separators and preserve saint labels.
cleaned = text.strip().strip(".")
cleaned = re.sub(r"^(saints?\s*[:\-]\s*)", "", cleaned, flags=re.I).strip()
if not cleaned:
return []
parts = re.split(r"\s*(?:,|;|\||\set\s)\s*", cleaned, flags=re.I)
return _unique(parts)
def _split_dictons(text: str) -> list[str]:
cleaned = text.strip()
cleaned = re.sub(r"^(dictons?\s*[:\-]\s*)", "", cleaned, flags=re.I).strip()
if not cleaned:
return []
# Keep sentences readable; split on explicit separators first.
if "|" in cleaned or ";" in cleaned:
parts = re.split(r"\s*(?:\||;)\s*", cleaned)
else:
parts = [cleaned]
return _unique(parts)
def load_saints(path: Path) -> dict[str, list[str]]:
if not path.exists():
return {}
raw = json.loads(path.read_text(encoding="utf-8"))
out: dict[str, list[str]] = {}
for mmdd, saint_value in raw.items():
key = _normalize_mmdd(mmdd)
if key is None:
continue
if isinstance(saint_value, list):
saints = [str(x).strip() for x in saint_value]
else:
saints = _split_saints(str(saint_value))
out[key] = _unique(saints)
return out
def parse_dictons_text(path: Path) -> dict[str, list[str]]:
if not path.exists():
return {}
lines = path.read_text(encoding="utf-8").splitlines()
out: dict[str, list[str]] = {}
current_date: str | None = None
for raw in lines:
line = raw.strip()
if not line:
continue
# Date heading block
m_head = DATE_HEADING_RE.match(line)
if m_head:
current_date = _normalize_mmdd(m_head.group(1))
if current_date and current_date not in out:
out[current_date] = []
continue
# Inline date + dicton
m_inline = INLINE_DATE_DIC_RE.match(line)
if m_inline:
mmdd = _normalize_mmdd(m_inline.group(1))
if mmdd:
out.setdefault(mmdd, []).extend(_split_dictons(m_inline.group(2)))
current_date = mmdd
continue
# Bullets or plain lines inside current date block
if current_date:
line = re.sub(r"^\s*[-*]\s*", "", line).strip()
if not line:
continue
if re.match(r"^saints?\s*[:\-]", line, flags=re.I):
# Saints line is ignored here; saints come from saints_json.
continue
out.setdefault(current_date, []).extend(_split_dictons(line))
return {k: _unique(v) for k, v in out.items()}
def _as_iso(year: int, mmdd: str) -> str:
month, day = mmdd.split("-")
return date(year, int(month), int(day)).isoformat()
def build_output(
saints_by_date: dict[str, list[str]],
dictons_by_date: dict[str, list[str]],
year: int | None,
) -> list[dict]:
all_dates = sorted(set(saints_by_date) | set(dictons_by_date))
rows: list[dict] = []
for mmdd in all_dates:
row = {
"date": _as_iso(year, mmdd) if year else mmdd,
"saints": saints_by_date.get(mmdd, []),
"dictons": dictons_by_date.get(mmdd, []),
}
rows.append(row)
return rows
def main() -> int:
parser = argparse.ArgumentParser(
description="Génère un JSON saints+dictons: date, saints[], dictons[]"
)
parser.add_argument(
"--saints-json",
default="calendrier_lunaire/saints_dictons/saints_france.json",
help="Fichier JSON des saints (clé MM-DD)",
)
parser.add_argument(
"--dictons-file",
required=True,
help="Fichier texte/markdown des dictons (avec dates MM-DD, DD/MM ou MMDD)",
)
parser.add_argument(
"--output",
default="calendrier_lunaire/saints_dictons/saints_dictons.json",
help="Fichier JSON de sortie",
)
parser.add_argument(
"--year",
type=int,
help="Optionnel: convertit MM-DD en YYYY-MM-DD",
)
args = parser.parse_args()
saints_path = Path(args.saints_json)
dictons_path = Path(args.dictons_file)
output_path = Path(args.output)
saints_by_date = load_saints(saints_path)
dictons_by_date = parse_dictons_text(dictons_path)
rows = build_output(saints_by_date, dictons_by_date, args.year)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"JSON généré: {output_path} ({len(rows)} dates)")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
import argparse
import json
import re
import time
from datetime import datetime
from datetime import date, timedelta
from html import unescape
from urllib.request import Request, urlopen
MONTHS_FR = {
1: "janvier", 2: "février", 3: "mars", 4: "avril", 5: "mai", 6: "juin",
7: "juillet", 8: "août", 9: "septembre", 10: "octobre", 11: "novembre", 12: "décembre",
}
def fetch_html(url: str) -> str:
req = Request(url, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64)"})
with urlopen(req, timeout=25) as resp:
raw = resp.read()
charset = (resp.headers.get_content_charset() or "utf-8").lower()
try:
return raw.decode(charset, errors="replace")
except Exception:
return raw.decode("utf-8", errors="replace")
def clean_html_text(s: str) -> str:
s = re.sub(r"<br\s*/?>", " ", s, flags=re.I)
s = re.sub(r"<[^>]+>", "", s)
s = unescape(s)
s = s.replace("\xa0", " ")
return re.sub(r"\s+", " ", s).strip()
def parse_saints(html: str) -> list[str]:
rows = re.findall(r'<p[^>]*class="sd-name"[^>]*>(.*?)</p>', html, flags=re.I | re.S)
out, seen = [], set()
for row in rows:
txt = clean_html_text(row)
if txt and txt not in seen:
out.append(txt)
seen.add(txt)
return out
def parse_dictons(html: str) -> list[str]:
rows = re.findall(r'<p[^>]*class="dict"[^>]*>(.*?)</p>', html, flags=re.I | re.S)
out = []
for row in rows:
txt = clean_html_text(row)
if txt:
out.append(txt)
return out
def parse_prenoms(html: str) -> list[str]:
block = re.search(
r'<h2[^>]*>[^<]*Pr[^<]*noms[^<]*f[^<]*ter[^<]*</h2>.*?<ul[^>]*>(.*?)</ul>',
html,
flags=re.I | re.S,
)
target = block.group(1) if block else ""
rows = re.findall(r'<li[^>]*>(.*?)</li>', target, flags=re.I | re.S)
out, seen = [], set()
for row in rows:
txt = clean_html_text(row)
if txt and txt not in seen:
out.append(txt)
seen.add(txt)
return out
def iter_mmdd_full_year(year: int):
d = date(year, 1, 1)
end = date(year, 12, 31)
while d <= end:
yield d.strftime("%m%d"), d
d += timedelta(days=1)
# assure 29 février même année non bissextile
if year % 4 != 0 or (year % 100 == 0 and year % 400 != 0):
yield "0229", None
def scrape_day(base_url: str, mmdd: str, d: date | None) -> dict:
url = f"{base_url.rstrip('/')}/{mmdd}.html"
html = fetch_html(url)
if d:
label = f"{d.day:02d} {MONTHS_FR[d.month]}"
iso = d.isoformat()
else:
label = "29 février"
iso = None
return {
"date": label,
"date_iso": iso,
"mmdd": mmdd,
"saints": parse_saints(html),
"dictons": parse_dictons(html),
"prenoms_a_feter": parse_prenoms(html),
"source_url": url,
}
def _ts() -> str:
return datetime.now().strftime("%H:%M:%S")
def _log(message: str, enabled: bool) -> None:
if enabled:
print(f"[{_ts()}] {message}", flush=True)
def main() -> int:
ap = argparse.ArgumentParser(description="Scrape saints/dictons pour toute une année (inclut 29 février)")
ap.add_argument("--year", type=int, default=date.today().year)
ap.add_argument("--base", default="https://www.saint-dicton.com")
ap.add_argument("--sleep-ms", type=int, default=150, help="Pause entre requêtes")
ap.add_argument("--limit", type=int, default=0, help="Limiter le nb de jours (test rapide)")
ap.add_argument("--out", default="", help="Fichier de sortie JSON (sinon stdout)")
ap.add_argument("--log-every", type=int, default=10, help="Affiche un log de progression tous les N jours")
ap.add_argument("--quiet", action="store_true", help="Réduit les logs")
args = ap.parse_args()
results = []
count = 0
verbose = not args.quiet
log_every = max(1, args.log_every)
_log(f"Démarrage scrape année={args.year}, base={args.base}", verbose)
for mmdd, d in iter_mmdd_full_year(args.year):
url = f"{args.base.rstrip('/')}/{mmdd}.html"
_log(f"[{count + 1}] fetch {mmdd} -> {url}", verbose)
try:
results.append(scrape_day(args.base, mmdd, d))
_log(f"[{count + 1}] ok {mmdd}", verbose and ((count + 1) % log_every == 0 or count == 0))
except Exception as e:
results.append({
"mmdd": mmdd,
"date_iso": d.isoformat() if d else None,
"error": str(e),
"source_url": url,
})
_log(f"[{count + 1}] erreur {mmdd}: {e}", True)
count += 1
if args.limit and count >= args.limit:
_log(f"Arrêt par --limit={args.limit}", verbose)
break
if args.sleep_ms > 0:
time.sleep(args.sleep_ms / 1000)
payload = {
"year": args.year,
"count": len(results),
"includes_feb29": any(r.get("mmdd") == "0229" for r in results),
"data": results,
}
txt = json.dumps(payload, ensure_ascii=False, indent=2)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(txt)
_log(f"Fichier écrit: {args.out}", True)
else:
print(txt)
_log(f"Terminé: {len(results)} jours", verbose)
return 0
if __name__ == "__main__":
raise SystemExit(main())

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
{
"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"
}

View File

@@ -0,0 +1,65 @@
from datetime import date
import pytest
from lunar_calendar import DayInfo, build_calendar
def test_build_calendar_rejects_invalid_range() -> None:
with pytest.raises(ValueError):
build_calendar(date(2026, 1, 10), date(2026, 1, 1))
def test_build_calendar_returns_expected_day_count() -> None:
rows = build_calendar(date(2026, 1, 1), date(2026, 1, 3))
assert len(rows) == 3
assert rows[0].date == "2026-01-01"
assert rows[-1].date == "2026-01-03"
def test_dayinfo_fields_are_well_typed() -> None:
row = build_calendar(date(2026, 1, 1), date(2026, 1, 1))[0]
assert isinstance(row, DayInfo)
assert isinstance(row.date, str)
assert isinstance(row.phase, str)
assert isinstance(row.illumination, float)
assert isinstance(row.croissante_decroissante, str)
assert isinstance(row.montante_descendante, str)
assert isinstance(row.signe, str)
assert isinstance(row.type_jour, str)
assert isinstance(row.soleil_lever, str)
assert isinstance(row.soleil_coucher, str)
assert isinstance(row.duree_jour, str)
assert isinstance(row.lune_lever, str)
assert isinstance(row.lune_coucher, str)
assert isinstance(row.duree_presence_lune, str)
assert isinstance(row.saint_du_jour, str)
assert isinstance(row.saint_de_glace, bool)
assert isinstance(row.perigee, bool)
assert isinstance(row.apogee, bool)
assert isinstance(row.noeud_lunaire, bool)
assert isinstance(row.transitions_type_jour, list)
assert isinstance(row.transitions_montante_descendante, list)
def test_saints_de_glace_are_exposed() -> None:
rows = build_calendar(date(2026, 5, 11), date(2026, 5, 13))
assert rows[0].saint_du_jour == "Saint Mamert"
assert rows[1].saint_du_jour == "Saint Pancrace"
assert rows[2].saint_du_jour == "Saint Servais"
assert all(r.saint_de_glace for r in rows)
def test_rise_set_fields_are_present() -> None:
row = build_calendar(date(2026, 2, 22), date(2026, 2, 22))[0]
assert row.soleil_lever != ""
assert row.soleil_coucher != ""
assert row.duree_jour != ""
assert row.lune_lever != ""
assert row.lune_coucher != ""
def test_transition_items_have_expected_shape() -> None:
row = build_calendar(date(2026, 2, 22), date(2026, 2, 22))[0]
for item in row.transitions_type_jour + row.transitions_montante_descendante:
assert set(item.keys()) == {"heure", "avant", "apres"}

340
consigne v3.md Normal file
View File

@@ -0,0 +1,340 @@
# 🌿 **Consigne de Développement : Application de Gestion de Jardin**
**Thème visuel** : *Gruvbox Dark Seventies* (inspiré des années 70 avec des tons chauds et sombres)
**Langue** : Français
**Plateformes** : Web (responsive) + Mobile (compatibilité smartphone)
**Technologies suggérées** : React.js (frontend) + Node.js/Express (backend) + Firebase/PostgreSQL (base de
données) + Mapbox/Leaflet (cartographie) + TensorFlow.js (détection d'espèces via photo)
---
## **📌 Table des Matières**
1. [Introduction](#1-introduction)
2. [Fonctionnalités Principales](#2-fonctionnalités-principales)
- [Gestion des Jardins](#21-gestion-des-jardins)
- [Gestion des Plantes](#22-gestion-des-plantes)
- [Calendrier Lunaire](#23-calendrier-lunaire)
- [Planning et Tâches](#24-planning-et-tâches)
- [Géolocalisation et Cartographie](#25-géolocalisation-et-cartographie)
- [Améliorations Avancées](#26-améliorations-avancées)
3. [Architecture Technique](#3-architecture-technique)
4. [Design & UI/UX](#4-design-uiux)
5. [Roadmap & Brainstorming](#5-roadmap-et-brainstorming)
6. [Exigences Techniques](#6-exigences-techniques)
7. [Livrables](#7-livrables)
---
## **1. Introduction**
**Objectif** :
Créer une application web/mobile intuitive pour gérer un ou plusieurs jardins (plein air ou serre), avec des
fonctionnalités avancées de suivi des plantes, du climat, et des conseils basés sur le calendrier lunaire.
Linterface doit être **responsive**, **esthétique** (thème *Gruvbox Dark Seventies*), et optimisée pour les
smartphones.
**Cibles** :
- Jardiniers amateurs et professionnels.
- Utilisateurs souhaitant automatiser la gestion des cultures (arrosage, plantation, récolte).
- Intégration de données géolocalisées et météo en temps réel.
**Inspirations visuelles** :
- Palette de couleurs : [Gruvbox Dark](https://github.com/morhetz/gruvbox) (rouge foncé, vert mousse, beige,
noir).
- Style : Retro-futuriste années 70 (boutons arrondis, ombres douces, typographie épurée comme *Courier New* ou
*Fira Code*).
- Exemple de palette :
```plaintext
#064E3B (vert foncé) | #CCA066 (beige chaud) | #D65D0E (orange rouille) | #282828 (noir profond)
---
2. Fonctionnalités Principales
2.1 Gestion des Jardins
Fonctionnalité: Création/Modification
Description: Ajout dun jardin avec :
Exemple UI:
────────────────────────────────────────
Fonctionnalité:
Description: - Nom, description.
Exemple UI:
────────────────────────────────────────
Fonctionnalité:
Description: - Type : Plein air / Serre.
Exemple UI: https://via.placeholder.com/30/CCA066/000?text=Serre
────────────────────────────────────────
Fonctionnalité:
Description: - Coordonnées géographiques (latitude/longitude) + carte interactive (Mapbox/Leaflet).
Exemple UI: https://via.placeholder.com/30/064E3B/000?text=📍
────────────────────────────────────────
Fonctionnalité:
Description: - Exposition (Nord/Sud/Est/Ouest) + angle dinclinaison.
Exemple UI: https://via.placeholder.com/30/D65D0E/000?text=⚡
────────────────────────────────────────
Fonctionnalité:
Description: - Dimensions : Longueur × Largeur (en m²) + géométrie (grille de cases pour planter).
Exemple UI: https://via.placeholder.com/30/282828/CCA066?text=📐
────────────────────────────────────────
Fonctionnalité: Images
Description: Upload de photos du jardin (avec géotagging).
Exemple UI: https://via.placeholder.com/30/064E3B/CCA066?text=📸
────────────────────────────────────────
Fonctionnalité: Climat
Description: Suivi des paramètres :
Exemple UI:
────────────────────────────────────────
Fonctionnalité:
Description: - Température du sol (capteur ou saisie manuelle).
Exemple UI: https://via.placeholder.com/30/D65D0E/000?text=🌡️
────────────────────────────────────────
Fonctionnalité:
Description: - Température de lair (API OpenWeatherMap).
Exemple UI: https://via.placeholder.com/30/CCA066/000?text=☀️
────────────────────────────────────────
Fonctionnalité:
Description: - Humidité (capteur ou % manuel).
Exemple UI: https://via.placeholder.com/30/064E3B/000?text=💧
---
2.2 Gestion des Plantes
Fonctionnalité: Fiche Plante
Description: Ajout/modification avec :
Exemple UI:
────────────────────────────────────────
Fonctionnalité:
Description: - Nom scientifique et commun.
Exemple UI: https://via.placeholder.com/30/CCA066/000?text=Tomate
────────────────────────────────────────
Fonctionnalité:
Description: - Famille botanique.
Exemple UI: https://via.placeholder.com/30/064E3B/000?text=Solanacées
────────────────────────────────────────
Fonctionnalité:
Description: - Type : Arbuste/Arbre/Légume/Fleur.
Exemple UI: https://via.placeholder.com/30/D65D0E/000?text=🌱
────────────────────────────────────────
Fonctionnalité:
Description: - Cycle de vie : Annuel/Bisannuel/Pérenne.
Exemple UI: https://via.placeholder.com/30/282828/CCA066?text=⏳
────────────────────────────────────────
Fonctionnalité:
Description: - Exigences : Lumière (soleil/mi-ombre/ombre), pH du sol, drainage.
Exemple UI: https://via.placeholder.com/30/064E3B/000?text=💧☀️
────────────────────────────────────────
Fonctionnalité: Planning de Plantation
Description: Calendrier avec :
Exemple UI:
────────────────────────────────────────
Fonctionnalité:
Description: - Date de plantation/semis.
Exemple UI: https://via.placeholder.com/30/D65D0E/000?text=📅
────────────────────────────────────────
Fonctionnalité:
Description: - Espacement entre plants (cm).
Exemple UI: https://via.placeholder.com/30/CCA066/000?text=30cm
────────────────────────────────────────
Fonctionnalité:
Description: - Conseils : Période lunaire idéale (intégré au calendrier lunaire).
Exemple UI: https://via.placeholder.com/30/064E3B/CCA066?text=🌕
────────────────────────────────────────
Fonctionnalité: Suivi de Croissance
Description: Photos + notes sur létat (maladies, croissance).
Exemple UI: https://via.placeholder.com/30/282828/000?text=📊
────────────────────────────────────────
Fonctionnalité: Récolte
Description: Date de récolte + rendement estimé (kg/m²).
Exemple UI: https://via.placeholder.com/30/D65D0E/000?text=🍅
---
2.3 Calendrier Lunaire
- Intégration dune API comme AstroAPI pour afficher :
- Phases de la lune (croissante/décroissante).
- Jours favorables/défavorables pour planter/repirer.
- Exemple de notification :
"Aujourdhui est un jour favorable pour planter des légumes-feuilles (ex : laitue). Évitez les racines."
---2.4 Planning et Tâches
- Liste de tâches (type Todo) avec :
- Arrosage (fréquence + volume).
- Taille/Engrais.
- Lutte contre les parasites.
- Rappels push (notifications mobiles).
- Exemple :
"Arroser les tomates tous les 2 jours (1L/plant). → [✅ Terminé] / [📅 15/06]."
---2.5 Géolocalisation et Cartographie
- Carte interactive (Mapbox/Leaflet) :
- Affichage des jardins avec leurs cases de plantation.
- Superposition des données météo (température, pluie).
- Exemple :
https://via.placeholder.com/400x300/282828/CCA066?text=🌍+Jardins+🌱
---2.6 Améliorations Avancées
Fonctionnalité: Détection despèces via photo
Description: Upload dune photo dune plante → reconnaissance via TensorFlow.js (modèle pré-entraîné comme
PlantNet).
Technologie Sugérée: TensorFlow.js + Firebase Storage
────────────────────────────────────────
Fonctionnalité: Capteurs IoT
Description: Intégration de capteurs (température/humidité) via Raspberry Pi + MQTT.
Technologie Sugérée: Node-RED + MQTT Broker
────────────────────────────────────────
Fonctionnalité: Recommandations IA
Description: Suggestions personnalisées (ex : "Votre sol est trop sec, ajoutez du compost").
Technologie Sugérée: Python (Flask) + Scikit-learn
────────────────────────────────────────
Fonctionnalité: Partage communautaire
Description: Forum pour échanger des conseils entre utilisateurs.
Technologie Sugérée: Firebase Auth + Forums (Discourse)
────────────────────────────────────────
Fonctionnalité: Analyse des données
Description: Graphiques de croissance (ex : courbe de température vs rendement).
Technologie Sugérée: Chart.js + D3.js
---
3. Architecture Technique
Frontend
- Framework : React.js (avec TypeScript pour la typage).
- UI Library : Material-UI (thème personnalisé Gruvbox) ou Tailwind CSS.
- Responsive : Breakpoints pour mobile/tablette/desktop.
- Cartographie : Mapbox GL JS ou Leaflet.
Backend
- Langage : Node.js + Express.
- Base de données :
- SQL : PostgreSQL (pour les relations complexes).
- NoSQL : Firebase (pour les données utilisateurs et temps réel).
- APIs externes :
- OpenWeatherMap (météo).
- AstroAPI (calendrier lunaire).
- Google Maps API (géolocalisation).
Mobile
- Hybride : React Native (si besoin dune app dédiée).
- PWA : Progressive Web App pour une expérience offline possible.
Hébergement
- Frontend : Vercel/Netlify.
- Backend : Render/Heroku.
- Base de données : Supabase (PostgreSQL) ou Firebase.
---
4. Design & UI/UX
Thème Gruvbox Dark Seventies
- Couleurs :
- Fond : #282828 (noir profond).
- Accents : #D65D0E (orange rouille), #CCA066 (beige).
- Texte : #A89984 (beige clair).
- Typographie :
- Police : Fira Code (monospace) ou Courier New pour un côté rétro.
- Taille : 16px (corps) / 24px (titres).
- Icônes : Feather Icons ou Material Icons.
- Animations :
- Effets subtils (hover sur les boutons).
- Loading spinner en forme de lune croissante.
Maquettes
- Figma/Adobe XD : Créer des wireframes pour :
- Page daccueil (tableau de bord).
- Fiche jardin.
- Planning des tâches.
- Carte interactive.
---5. Roadmap & Brainstorming
Phase 1 (MVP - 4 semaines)
┌──────────────────────────────┬──────────┬──────────────────────────────────────────────────────┐
│ Tâche │ Priorité │ Description │
├──────────────────────────────┼──────────┼──────────────────────────────────────────────────────┤
│ Authentification utilisateur │ ⭐⭐⭐ │ Firebase Auth (email/password + Google). │
├──────────────────────────────┼──────────┼──────────────────────────────────────────────────────┤
│ Gestion des jardins │ ⭐⭐⭐ │ CRUD (Create/Read/Update/Delete) avec carte Leaflet. │
├──────────────────────────────┼──────────┼──────────────────────────────────────────────────────┤
│ Plantes (fiches) │ ⭐⭐⭐ │ Base de données avec images uploadées. │
├──────────────────────────────┼──────────┼──────────────────────────────────────────────────────┤
│ Calendrier lunaire │ ⭐⭐ │ Intégration API AstroAPI + affichage des phases. │
├──────────────────────────────┼──────────┼──────────────────────────────────────────────────────┤
│ Planning tâches │ ⭐⭐ │ Liste avec rappels (notifications locales). │
└──────────────────────────────┴──────────┴──────────────────────────────────────────────────────┘
Phase 2 (Améliorations - 6 semaines)
┌─────────────────────────────┬──────────┬─────────────────────────────────────────────────────┐
│ Tâche │ Priorité │ Description │
├─────────────────────────────┼──────────┼─────────────────────────────────────────────────────┤
│ Détection despèces (photo) │ ⭐⭐⭐ │ Modèle TensorFlow.js + Firebase Storage. │
├─────────────────────────────┼──────────┼─────────────────────────────────────────────────────┤
│ Capteurs IoT (optionnel) │ ⭐⭐ │ Raspberry Pi + MQTT pour les données en temps réel. │
├─────────────────────────────┼──────────┼─────────────────────────────────────────────────────┤
│ Analyse de données │ ⭐⭐ │ Graphiques de croissance (Chart.js). │
├─────────────────────────────┼──────────┼─────────────────────────────────────────────────────┤
│ Partage communautaire │ ⭐ │ Forum intégré (Discourse ou Firebase Forum). │
└─────────────────────────────┴──────────┴─────────────────────────────────────────────────────┘
Brainstorming Futur
- AR : Filtre réalité augmentée pour visualiser les plantes dans son jardin.
- Drone : Intégration de photos aériennes (via API DroneKit).
- Marketplace : Vente/achat de graines/plantes entre utilisateurs.
---
6. Exigences Techniques
┌───────────────┬───────────────────────────────────────────────────────────────────┐
│ Catégorie │ Détails │
├───────────────┼───────────────────────────────────────────────────────────────────┤
│ Compatibilité │ - Navigateurs : Chrome, Firefox, Safari (mobile/desktop). │
├───────────────┼───────────────────────────────────────────────────────────────────┤
│ │ - Résolution : Adapté à 320px (mobile) à 1920px (desktop). │
├───────────────┼───────────────────────────────────────────────────────────────────┤
│ Performance │ - Temps de chargement < 2s (optimisation images + lazy loading). │
├───────────────┼───────────────────────────────────────────────────────────────────┤
│ Sécurité │ - HTTPS obligatoire. │
├───────────────┼───────────────────────────────────────────────────────────────────┤
│ │ - Chiffrement des données utilisateurs (Firebase Security Rules). │
├───────────────┼───────────────────────────────────────────────────────────────────┤
│ Accessibilité │ - Conforme WCAG (contrastes, sous-titres pour vidéos). │
├───────────────┼───────────────────────────────────────────────────────────────────┤
│ Tests │ - Tests unitaires (Jest) + tests E2E (Cypress). │
└───────────────┴───────────────────────────────────────────────────────────────────┘
---
7. Livrables
1. Code source :
- Repository GitHub/GitLab avec README détaillé.
- Documentation technique (API, installation).
2. Maquettes :
- Fichiers Figma/Adobe XD pour le design.
3. Base de données :
- Schema PostgreSQL + données dexemple.
4. Démonstration :
- Vidéo Loom (10 min) montrant les fonctionnalités clés.
5. Documentation utilisateur :
- Guide PDF avec captures décran (ex : "Comment ajouter un jardin ?").
---
📌 Notes Supplémentaires
- Noms de variables : Utiliser des noms explicites (ex : userJardins au lieu de j).
- Internationalisation : Prévoir un système i18n (ex : français/anglais) via react-i18next.
- Feedback : Intégrer un système de feedback (ex : "Cette fonctionnalité est-elle utile ?").
---🚀 Prêt à commencer !
Merci de suivre cette consigne pour livrer une application fonctionnelle, esthétique et scalable. Pour les
ajustements, priorisez toujours lUX et la performance.
---Inspiré par : Gruvbox, PlantNet, et les jardins potagers des années 70.

193
consigne-v2.md Normal file
View File

@@ -0,0 +1,193 @@
# Consigne ClaudeCode — Développement dune Web App de gestion de jardins
## Objectif
Concevoir et développer une **web app hébergée**, en **français**, **responsive mobile-first** (compatible smartphone), permettant de gérer un ou plusieurs jardins (plein air et serre), leurs cultures, les plants, les tâches et la planification.
---
## Contraintes globales
- **Langue UI** : Français.
- **Plateformes** : Web (desktop + smartphone), PWA souhaitée.
- **Hébergement** : application auto-hébergeable (Docker recommandé).
- **Design** : thème **Gruvbox Dark seventies**.
- **Sécurité** : authentification utilisateur, permissions minimales, sauvegardes.
- **Architecture** : API + frontend séparés (ou monolithe propre), documentation incluse.
---
## Vision produit
Lapplication doit centraliser :
1. La gestion des jardins (zones, caractéristiques, météo locale).
2. Le suivi des plants (variétés, stades, actions culturales).
3. Le planning (plantation, entretien, récolte, tâches).
4. Une aide à la décision (saisons, calendrier lunaire, alertes).
---
## Fonctionnalités attendues
## 1) Gestion des jardins
Pour chaque jardin :
- Nom, description.
- Type : **plein air** ou **serre**.
- Coordonnées géographiques (lat/lon).
- Adresse facultative.
- Photos/images.
- Exposition (N, NE, E, SE, S, SO, O, NO + heures densoleillement).
- Température du sol (manuel + capteur possible).
- Température de lair (manuel + capteur possible).
- Humidité (air et/ou sol si dispo).
### Géométrie du jardin (mode “cases”)
- Représentation en grille (cases).
- Dimensions configurables (ex: 10x20 cases).
- Chaque case peut avoir : culture en place, état, historique, notes.
- Vue visuelle couleur par culture/stade/occupation.
---
## 2) Gestion des plants et cultures
Pour chaque variété/plant :
- Nom commun + nom botanique.
- Type (légume, fruit, aromatique, fleur, etc.).
- Variété/cultivar.
- Durée de germination estimée.
- Besoins (eau, température, ensoleillement, espacement).
- Compatibilités/incompatibilités de culture.
- Périodes recommandées (semis, repiquage, récolte).
### Suivi cycle de vie
- Semis
- Repiquage
- Croissance
- Floraison/fructification
- Récolte
- Fin de culture
Historique horodaté des événements par plant/zone/case.
---
## 3) Planning & tâches
- Création de tâches (ponctuelles/récurrentes).
- Catégories : semis, arrosage, taille, traitement, récolte, observation, maintenance serre.
- Priorités, échéances, rappels.
- Vue liste + kanban + agenda.
- Liaison tâche ↔ jardin/zone/plant/case.
---
## 4) Calendrier cultural + calendrier lunaire
- Calendrier mensuel des actions recommandées.
- Intégration dun **calendrier lunaire** (jours racine/feuille/fleur/fruit, etc.).
- Suggestion dactions selon type de culture + phase lunaire.
- Paramétrable (activer/désactiver influence lunaire).
---
## 5) Tableaux de bord
- Vue “Aujourdhui” : tâches du jour, alertes, actions à faire.
- Vue “Jardin” : état doccupation des cases, cultures en cours.
- Vue “Récoltes” : prévisions et historique.
- Indicateurs : taux doccupation, tâches en retard, rendement estimé.
---
## 6) Média & observations
- Upload photos par jardin/plant/tâche.
- Galerie filtrable.
- Notes libres datées (journal de culture).
---
## 7) Brainstorming daméliorations (roadmap)
- Détection de variétés par photo (IA).
- Détection maladies/carences via photo.
- OCR détiquettes de semences.
- Connexion capteurs (temp sol/air, humidité, météo locale).
- Alertes intelligentes (gel, stress hydrique, canicule).
- Suggestions automatiques de rotation des cultures.
- Gestion de stock (graines, substrats, engrais).
- Export PDF/CSV des plannings et historiques.
- Multi-utilisateurs / partage familial.
- Mode hors-ligne (PWA) + synchro.
---
## Exigences UX/UI
- Mobile-first (navigation simple au pouce).
- Performances correctes sur smartphone milieu de gamme.
- Accessibilité (contraste, taille police, focus clavier).
- Thème visuel : **Gruvbox Dark seventies** (palette cohérente sur toute lapp).
### Référence thème (indicative)
- Background principal: `#282828`
- Background secondaire: `#3c3836`
- Texte principal: `#ebdbb2`
- Texte secondaire: `#a89984`
- Accent vert: `#b8bb26`
- Accent jaune: `#fabd2f`
- Accent bleu: `#83a598`
- Accent orange: `#fe8019`
- Erreur rouge: `#fb4934`
---
## Exigences techniques
- API documentée (OpenAPI souhaité).
- Base de données relationnelle (PostgreSQL recommandé).
- Stockage images local ou S3-compatible.
- Auth sécurisée (session ou JWT), gestion des rôles.
- Logs, monitoring, sauvegarde/restauration.
- Déploiement Docker Compose.
- Tests minimaux (unitaires + parcours critiques).
---
## Livrables attendus
1. Cahier darchitecture technique.
2. Schéma de données (ERD).
3. Maquettes principales (mobile + desktop).
4. MVP fonctionnel déployable.
5. Documentation dinstallation/exploitation.
6. Backlog priorisé (MVP / V2 / V3).
---
## Priorisation MVP (ordre conseillé)
1. Auth + gestion des jardins.
2. Grille des cases + cultures.
3. Fiches plants.
4. Tâches + planning.
5. Journal + photos.
6. Calendrier lunaire simple.
7. Tableau de bord.
---
## Critères de réussite
- Utilisable à 100% depuis smartphone.
- Suivi complet dun cycle cultural réel.
- Planification claire des tâches et récoltes.
- Interface stable, rapide, compréhensible.
- Base saine pour extensions IA/capteurs.

215
consigne.md Normal file
View File

@@ -0,0 +1,215 @@
# CONSIGNE — Claude Code — Webapp “Gestion Jardin” (Self-hosted, mobile-friendly)
## 0) Objectif
Développer une webapp self-hosted (Docker) **en français**, **compatible smartphone**, permettant de gérer un ou plusieurs jardins (extérieur/serre) et la gestion complète des plants : fiche plante, planning (plantation, entretien, culture, récolte), tâches, calendrier (dont lunaire), et données environnementales (températures, humidité). UI : **Gruvbox Dark “seventies”** (vintage, contrasté, très lisible).
## 1) Contraintes générales
- Déploiement : **Docker Compose** (service backend + frontend + DB).
- Stockage : **SQLite par défaut** (volume persistant). Prévoir migration future vers Postgres.
- Accès : application locale (LAN) via reverse proxy possible mais non obligatoire.
- Auth : MVP sans auth complexe (optionnel). Prévoir un futur module “auth”.
- Données : possibilité dimport/export (JSON) et sauvegarde DB.
- Responsive : **mobile-first** + desktop ok.
- Performance : chargements rapides, pagination/lazy-loading images.
## 2) Périmètre fonctionnel MVP (Phase 1)
### 2.1 Gestion des jardins
Un jardin est une “zone cultivée” décrite par :
- Nom, description, type : **plein air / serre / tunnel**
- Coordonnées : latitude/longitude, altitude (optionnel)
- Adresse/lieu (optionnel)
- Exposition : nord/sud/est/ouest + ombre/mi-ombre/plein soleil
- Sol : type (argileux, sableux, limoneux, humifère…), pH (optionnel), amendements (optionnel)
- **Images** (galerie)
- Capteurs (valeurs manuelles MVP) :
- température sol
- température air
- humidité air
- humidité sol (optionnel)
- date/heure de mesure, source (manuel/capteur)
- Géométrie du jardin en “cases” (grille) : voir 2.3
Fonctions :
- CRUD jardins
- Fiche jardin
- Galerie photos
- Saisie rapide “mesure du jour” (temp/humidité)
### 2.2 Gestion des plants (plantes/cultures)
Deux concepts :
1) **Variété** (catalogue) : “Tomate Andine Cornue”, “Courgette Verte…”
2) **Plantation** (instance) : variété X plantée dans jardin Y à une date et une case/grille.
Champs “Variété” (catalogue) :
- Nom commun, variété, famille (Solanacées…), tags
- Périodes conseillées : semis intérieur, semis extérieur, repiquage, plantation, récolte (fenêtres)
- Besoins : eau (faible/moyen/fort), soleil, espacement, température min, durée de culture
- Profondeur semis, type de sol conseillé
- Notes personnelles, photos
Champs “Plantation” (instance) :
- Jardin, zone/case, date semis/plantation/repiquage
- Quantité (nb plants), statut (prévu/en cours/terminé/échoué)
- Historique des actions (arrosage, taille, traitement, etc.)
- Dates réelles (récolte début/fin), rendement estimé/réel (optionnel MVP)
- Observations et photos
Fonctions :
- CRUD variété
- CRUD plantation (avec placement sur grille)
- Vue “planning” (par semaine/mois) des actions à venir
### 2.3 Géométrie du jardin (cases / grille)
MVP : représentation en **grille 2D** configurable (ex: 6×4).
- Chaque case peut avoir :
- un libellé (A1, A2…)
- des dimensions (optionnel)
- un état (libre/occupée)
- des plantations associées (actives + historiques)
- Interaction :
- tap/clic sur case → détails + actions (ajouter plantation, marquer libre, notes)
### 2.4 Gestion des tâches et planning
- Tâches :
- titre, description, jardin, plantation liée (optionnel), priorité, échéance, récurrence simple
- statut : à faire / en cours / fait / annulé
- Vues :
- “Aujourdhui”
- “Semaine”
- “Backlog”
- Notifications : hors-scope MVP (préparer hooks)
### 2.5 Calendrier lunaire (MVP simple)
MVP : afficher pour chaque jour :
- phase (nouvelle lune, 1er quartier, pleine lune, dernier quartier)
- indicateur “lune montante/descendante” si source disponible
- filtres “jours racines/feuilles/fleurs/fruits” : optionnel
Implémentation :
- Soit calcul astronomique via lib (si fiable),
- soit dataset embarqué (année en cours + suivante) importable.
## 3) Brainstorming daméliorations (Phase 2+)
### 3.1 “Smart features”
- Détection photo (mobile) :
- reconnaissance variété / espèce (suggestion, pas décision)
- détection maladies / carences (suggestion)
- suivi de croissance (comparaison de photos)
- Suggestions automatiques :
- alertes gel / canicule selon localisation + météo
- arrosage estimé selon température/humidité/historique
- rotation des cultures et associations bénéfiques
- Import/export :
- import semences / catalogue depuis CSV
- export journal des récoltes
### 3.2 Capteurs réels (futur)
- Intégration Home Assistant / MQTT (module)
- Courbes de température/humidité
- Tableau de bord “serre” temps réel
### 3.3 Multi-jardin / multi-site
- Gestion de plusieurs lieux (ex: maison / potager secondaire)
- Synchronisation & sauvegardes
## 4) UX / UI (obligatoire)
### 4.1 Thème visuel
- Style : **Gruvbox Dark** + “seventies” (vintage, chaleureux, lisible)
- Contraintes :
- contrastes élevés, gros boutons mobile
- cartes (cards) avec bord arrondi, ombres légères
- typographie simple, lisibilité prioritaire
- Composants récurrents :
- Header fixe avec navigation (Jardins / Plants / Planning / Tâches / Calendrier lunaire / Settings)
- Drawer mobile (menu burger)
- Panneau filtre/tri sur listes
### 4.2 Pages MVP
1) Dashboard : résumé (tâches du jour, mesures récentes, plantations actives)
2) Jardins : liste + création + fiche jardin
3) Grille jardin : vue cases + détails
4) Catalogue variétés : liste + fiche
5) Plantations : liste (filtrable) + création + fiche
6) Planning : calendrier (mois/semaine) + actions
7) Tâches : Kanban simple ou liste
8) Calendrier lunaire : vue mois + détails jour
9) Settings : unités, localisation par défaut, export/import, sauvegarde
### 4.3 Filtres “judicieux” (brainstorming)
- Jardins : type, exposition, serre/extérieur, tags, dernier relevé capteur
- Variétés : famille, saison, besoin eau, soleil, durée culture, tags
- Plantations : jardin, case, statut, période (en cours/à venir/terminé), “à récolter”
- Tâches : priorité, échéance, jardin, plantation liée, statut, récurrence
## 5) Architecture technique (choix par défaut)
### Backend
- Python **FastAPI**
- ORM : SQLModel (ou SQLAlchemy)
- SQLite par défaut
- Gestion uploads images : stockage local `/data/uploads` + métadonnées DB
- API REST :
- CRUD jardins, cases, variétés, plantations, tâches, mesures
- endpoints de recherche + filtres
- export/import JSON
### Frontend
- Vue 3 + Vite (ou React si préféré)
- UI kit minimal (ou Tailwind) en respectant le thème Gruvbox
- Mobile-first, PWA optionnelle (phase 2)
### Docker
- `docker-compose.yml` :
- backend
- frontend (static)
- volume DB + uploads
## 6) Modèle de données (MVP — tables)
- gardens
- garden_cells
- garden_images
- measurements (air_temp, soil_temp, humidity_air, humidity_soil, ts, garden_id)
- plant_varieties
- plant_images
- plantings
- planting_events (arrosage, taille, traitement, observation)
- tasks
- lunar_calendar_entries (dataset) OU table “computed cache”
- user_settings (local)
## 7) API (MVP — exemples dendpoints)
- `GET /api/health`
- `GET/POST /api/gardens`
- `GET/PUT/DELETE /api/gardens/{id}`
- `GET/POST /api/gardens/{id}/cells`
- `GET/POST /api/varieties`
- `GET/POST /api/plantings`
- `GET/POST /api/tasks`
- `GET/POST /api/measurements`
- `GET /api/lunar?month=YYYY-MM`
- `POST /api/export`
- `POST /api/import`
## 8) Règles qualité
- Validation stricte des champs (pydantic)
- Gestion erreurs claire côté UI
- Tests basiques backend (CRUD + filtres)
- Logs structurés backend
- Pas de secrets dans le frontend (variables denv côté backend)
## 9) Livrables attendus
- Arborescence complète projet
- `README.md` (install, run, backup)
- `docker-compose.yml`
- Backend FastAPI prêt
- Frontend complet pages MVP
- Thème gruvbox dark seventies appliqué partout
- Données de démo (seed) : 1 jardin + quelques variétés + plantations + tâches
## 10) Ordre de réalisation imposé
1) Modèle DB + CRUD jardins/variétés/plantations/tâches
2) Upload images + galerie
3) Vue grille jardin + placement plantations
4) Planning calendrier + vues filtrées
5) Calendrier lunaire (dataset ou calcul)
6) Dashboard + export/import
7) Polissage UI mobile + perf + README final

514
consigne_yolo.md Normal file
View File

@@ -0,0 +1,514 @@
Modèle prêt à lemploi pour plantes
Un modèle YOLOv8s “Leaf Detection & Classification” est disponible sur Hugging Face.
Il peut détecter et classer différents types de feuilles de plantes directement, sans entraînement préalable.
1) Prérequis / Installation
Ouvre un terminal et installe ces dépendances :
python3 -m venv venv
source venv/bin/activate
pip install ultralyticsplus==0.0.28 ultralytics==8.0.43 opencv-python matplotlib
ultralytics : bibliothèque YOLOv8
ultralyticsplus : extension recommandée
opencv-python + matplotlib : affichage images
2) Exemple de script Python detect_plants.py
Crée un fichier detect_plants.py :
from ultralyticsplus import YOLO, render_result
import cv2
# 1) Charger le modèle
model = YOLO("foduucom/plant-leaf-detection-and-classification")
# 2) Paramètres de détection
model.overrides['conf'] = 0.25
model.overrides['iou'] = 0.45
model.overrides['max_det'] = 1000
# 3) Chargement dune image
image_path = "ma_plante.jpg"
# 4) Prédiction (détection + classification de feuilles)
results = model.predict(image_path)
# 5) Récupération des boîtes détectées
boxes = results[0].boxes
class_ids = results[0].boxes.cls
scores = results[0].boxes.conf
print("Détections :", len(boxes))
for i, box in enumerate(boxes):
print(f"- Classe {class_ids[i]}, score {scores[i]:.2f}")
# 6) Annoter limage
annotated = render_result(model=model, image=image_path, result=results[0])
# 7) Afficher limage annotée
annotated.show()
Points clés :
YOLO("foduucom/...") charge le modèle leaf detection YOLOv8.
predict(image_path) exécute linférence sur limage locale.
3) Mode batch / dossier complet
Si tu veux traiter plusieurs images dans un dossier :
import glob
for file in glob.glob("images/*.jpg"):
results = model.predict(file)
print(f"== Résultats pour {file} ==")
for box in results[0].boxes:
print(box.cls, box.conf)
render_result(model=model, image=file, result=results[0]).show()
4) Comment adapter au “type de plante”
Le modèle dorigine peut classifier 46 classes de feuilles courantes (pommes, tomates, blé…) avec étiquette.
Si tu as ton propre dataset spécifique (autres plantes, fleurs, fruits) → il faut :
créer un dataset annoté en format YOLO
fine-tuner YOLOv8 dessus
utiliser train() de la bibliothèque ultralytics
📌 Feuille de consignes (Checklist)
📌 A) Préparation
Python 3.10+ installé
GPU disponible si possible (beaucoup + rapide)
Images bien cadrées (feuilles principales visibles)
📌 B) Installation
pip install ultralyticsplus ultralytics opencv-python matplotlib
📌 C) Tester une image
Place ma_plante.jpg dans le même dossier
Lance :
python detect_plants.py
📌 D) Résultats
Vérifier :
nombre de feuilles détectées
classes attribuées
score de confiance
📌 E) Prochaine étape (facultative)
Si tu veux augmenter la précision :
🔹 collecter plus dimages
🔹 annoter avec LabelImg ou Roboflow
🔹 entraîner/fine-tuner YOLOv8
🔹 tester sur mobile/web
Limitations à connaître
ce modèle est spécifique aux feuilles (pas fleurs/fruit complets).
performances variables selon qualité photo.
nécessite parfois GPU pour performance fluide.
Si tu veux, je peux aussi te fournir une version prête à lemploi pour entraîner toi-même sur tes propres photos (annotation automatique + scripts dentraînement YOLO).
Modèle prêt à lemploi pour plantes
Un modèle YOLOv8s “Leaf Detection & Classification” est disponible sur Hugging Face.
Il peut détecter et classer différents types de feuilles de plantes directement, sans entraînement préalable.
1) Prérequis / Installation
Ouvre un terminal et installe ces dépendances :
python3 -m venv venv
source venv/bin/activate
pip install ultralyticsplus==0.0.28 ultralytics==8.0.43 opencv-python matplotlib
ultralytics : bibliothèque YOLOv8
ultralyticsplus : extension recommandée
opencv-python + matplotlib : affichage images
2) Exemple de script Python detect_plants.py
Crée un fichier detect_plants.py :
from ultralyticsplus import YOLO, render_result
import cv2
# 1) Charger le modèle
model = YOLO("foduucom/plant-leaf-detection-and-classification")
# 2) Paramètres de détection
model.overrides['conf'] = 0.25
model.overrides['iou'] = 0.45
model.overrides['max_det'] = 1000
# 3) Chargement dune image
image_path = "ma_plante.jpg"
# 4) Prédiction (détection + classification de feuilles)
results = model.predict(image_path)
# 5) Récupération des boîtes détectées
boxes = results[0].boxes
class_ids = results[0].boxes.cls
scores = results[0].boxes.conf
print("Détections :", len(boxes))
for i, box in enumerate(boxes):
print(f"- Classe {class_ids[i]}, score {scores[i]:.2f}")
# 6) Annoter limage
annotated = render_result(model=model, image=image_path, result=results[0])
# 7) Afficher limage annotée
annotated.show()
Points clés :
YOLO("foduucom/...") charge le modèle leaf detection YOLOv8.
predict(image_path) exécute linférence sur limage locale.
3) Mode batch / dossier complet
Si tu veux traiter plusieurs images dans un dossier :
import glob
for file in glob.glob("images/*.jpg"):
results = model.predict(file)
print(f"== Résultats pour {file} ==")
for box in results[0].boxes:
print(box.cls, box.conf)
render_result(model=model, image=file, result=results[0]).show()
4) Comment adapter au “type de plante”
Le modèle dorigine peut classifier 46 classes de feuilles courantes (pommes, tomates, blé…) avec étiquette.
Si tu as ton propre dataset spécifique (autres plantes, fleurs, fruits) → il faut :
créer un dataset annoté en format YOLO
fine-tuner YOLOv8 dessus
utiliser train() de la bibliothèque ultralytics
📌 Feuille de consignes (Checklist)
📌 A) Préparation
Python 3.10+ installé
GPU disponible si possible (beaucoup + rapide)
Images bien cadrées (feuilles principales visibles)
📌 B) Installation
pip install ultralyticsplus ultralytics opencv-python matplotlib
📌 C) Tester une image
Place ma_plante.jpg dans le même dossier
Lance :
python detect_plants.py
📌 D) Résultats
Vérifier :
nombre de feuilles détectées
classes attribuées
score de confiance
📌 E) Prochaine étape (facultative)
Si tu veux augmenter la précision :
🔹 collecter plus dimages
🔹 annoter avec LabelImg ou Roboflow
🔹 entraîner/fine-tuner YOLOv8
🔹 tester sur mobile/web
Limitations à connaître
ce modèle est spécifique aux feuilles (pas fleurs/fruit complets).
performances variables selon qualité photo.
nécessite parfois GPU pour performance fluide.
Si tu veux, je peux aussi te fournir une version prête à lemploi pour entraîner toi-même sur tes propres photos (annotation automatique + scripts dentraînement YOLO).
Voici des outils et méthodes open-source que tu peux utiliser pour annoter automatiquement ou semi-automatiquement un dataset dimages (pour entraîner YOLOv8 ou tout autre modèle de détection), y compris pour des plantes, feuilles, fleurs, etc. :
🧩 Outils dannotation (semi-automatique et automatique)
📌 1) Roboflow Annotate
Plateforme web qui propose des outils dassistance par IA pour accélérer lannotation :
Possibilité dimporter tes images et de générer des annotations avec de lassistance IA (Label Assist / Auto Label).
Permet dexporter les annotations en formats compatibles YOLO.
Gratuit jusquà certaines limites, fonctionne via interface web.
Pas besoin dhéberger toi-même un serveur.
📌 2) Auto-Annotation avec Autodistill + Grounding DINO
Une approche plus avancée pour annoter automatiquement des images :
Utilise des modèles de type Grounding DINO pour détecter des objets selon un texte (ex : “feuille”, “fleur”, “fruit”).
Génère ensuite automatiquement des fichiers dannotation cachés au format YOLO.
Cette méthode diminue le travail humain car elle produit dabord des étiquettes automatiques que lon peut ensuite corriger.
Cest pratique si tu as beaucoup dimages brutes à annoter.
📌 3) CVAT (Computer Vision Annotation Tool)
Outil open source complet pour annotations :
Interface web pour annoter images et vidéos.
Supporte la pré-annotation automatique à partir de modèles pré-entraînés (tu peux importer un modèle YOLO).
Permet déconomiser du temps en générant des boîtes automatiquement puis en les corrigeant.
Convient bien pour des datasets de plantes.
👉 CVAT est lun des outils les plus utilisés dans la communauté CV pour annotation manuelle assistée par IA ou modèles pré-entraînés.
📌 4) Outils open-source dannotation classiques
Si tu veux annoter manuellement ou semi-manuellement :
LabelImg outil graphique pour créer des boîtes et exporter en YOLO.
Label Studio annotation flexible multi-tâches (mais souvent plus généraliste).
Autres outils listés (Yolo_Label, Make Sense, Scalabel, etc.) pour différents formats et workflows.
🧠 Méthodes dannotation automatique (workflow avancé)
Voici comment automatiser ton annotation avant dentraîner YOLO :
1. Pré-annotation automatique avec modèle existant
Charge un modèle de vision comme YOLOv8 ou un autre détecteur pré-entraîné.
Passe tes images au modèle pour générer des boîtes et labels provisoires.
Convertis ces détections en fichiers .txt YOLO.
Révise / corrige les erreurs ensuite (important car lauto-annotation nest pas parfaite).
2. Utiliser Segment Anything / Autodistill
Combine Segment Anything (SAM) avec un modèle de détection (ex : Grounding DINO).
Segment Anything fournit des masques / régions intéressantes.
Grounding DINO relie ces régions à des classes par texte.
Autodistill convertit ensuite ce résultat en étiquettes format YOLO.
Utile pour réduire le travail manuel et créer rapidement un dataset.
🔎 Résumé des possibilités
Outil / Méthode Automatique possible Semi-Automatique Export YOLO
Roboflow Annotate ✔️ (modèles IA intégrés) ✔️ ✔️
Autodistill + DINO ✔️ ⬜ ✔️
CVAT ✔️ (pré-annotation) ✔️ ✔️
LabelImg / Label Studio ⬜ ✔️ ✔️
⚠️ Conseils importants
Auto-annotation nest jamais parfaite — vérifie et corrige toujours les annotations manuellement pour que le modèle que tu entraine apprenne bien.
Lannotation est souvent le goulot détranglement des projets CV : mieux les étiquettes sont, meilleur sera ton modèle.
Si tu veux, je peux te générer un script Python qui intègre Autodistill + Grounding DINO pour auto-annoter ton dossier dimages plantes en format YOLO. Veux-tu un tel script prêt à lemploi ? (Si oui, dis-moi le système dexploitation que tu utilises : Linux, Windows ou macOS.)
Voici un script Python auto-hôte complet pour annoter automatiquement ton propre dataset dimages (par exemple feuilles/fleurs/plantes) au format YOLO, en utilisant Autodistill + un modèle de type Grounding DINO pour générer automatiquement les fichiers dannotation avant entraînement YOLOv8.
Ce workflow utilise loutil Autodistill, qui combine des « base models » (par exemple GroundedSAM/Grounding DINO) pour auto-labeler des images et génère ensuite un dataset prêt à lentraînement YOLOv8 — sans annotation manuelle préalable.
✅ 1) Installation des dépendances
Dans un terminal, crée un environnement Python propre et installe les librairies nécessaires :
python3 -m venv venv
source venv/bin/activate
pip install autodistill autodistill-grounded-sam autodistill-yolov8 ultralytics opencv-python
autodistill : framework principal pour auto-annotation
autodistill-grounded-sam : combine Grounding DINO + SAM pour auto-labeling
autodistill-yolov8 : plugin pour entraîner YOLOv8 après génération
ultralytics, opencv-python : pour lentraînement et le test YOLOv8
📁 2) Organisation du dataset
Place toutes tes images non annotées dans un dossier unique, par exemple :
dataset/
raw_images/
photo1.jpg
photo2.jpg
...
🧠 3) Script Python dauto-annotation + préparation YOLO
Crée un fichier auto_annotate_and_export.py :
from autodistill_grounded_sam import GroundedSAM
from autodistill.detection import CaptionOntology
import os
import json
# =========================
# 1) Définir l'ontologie
# =========================
# Chaque clé est une **description textuelle** que le modèle tentera
# de repérer dans l'image. La valeur est l'étiquette qui sera utilisée.
# Tu peux ajouter autant de classes que nécessaire :
ontology_map = {
"leaf of a plant": "leaf",
"flower of a plant": "flower",
"fruit of a plant": "fruit"
}
base_model = GroundedSAM(ontology=CaptionOntology(ontology_map))
# =========================
# 2) Folder paths
# =========================
INPUT_FOLDER = "./dataset/raw_images"
OUTPUT_FOLDER = "./dataset/auto_labeled"
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
# =========================
# 3) Auto-label all images
# =========================
print("⏳ Auto-labeling images with GroundedSAM (Grounding DINO + SAM)...")
base_model.label_folder(
input_folder=INPUT_FOLDER,
output_folder=OUTPUT_FOLDER
)
print("🟢 Auto-labeling terminé !")
print(f"Annotations générées dans : {OUTPUT_FOLDER}")
📌 Ce que fait le script
Il lance GroundedSAM (basé sur Grounding DINO + SAM) pour analyser chaque image.
Il utilise les prompts textuels que tu définis dans la variable ontology_map pour associer chaque objet détecté à une classe.
Il écrit automatiquement les fichiers dannotation au format YOLO dans le dossier auto_labeled.
Le résultat est un dataset prêt à lentraînement YOLOv8.
📝 4) Vérifier les résultats
Après ce script, ton dossier dataset/auto_labeled doit contenir :
dataset/auto_labeled/
images/
photo1.jpg
photo2.jpg
...
labels/
photo1.txt
photo2.txt
...
Chaque .txt contient des lignes du type :
0 0.4532 0.3124 0.1824 0.2763
(correspondant à <class_id> <x_center> <y_center> <w> <h> au format YOLO)
🚀 5) Entraînement YOLOv8 sur les données générées
Ensuite, tu peux entraîner YOLOv8 avec ces annotations :
✍ Crée un fichier data.yaml :
path: dataset/auto_labeled
train: images
val: images
names:
0: leaf
1: flower
2: fruit
Assure-toi que lordre des noms correspond à lordre utilisé dans ontology_map.
🎓 Lance lentraînement
Dans ton terminal :
yolo task=detect mode=train model=yolov8n.pt data=data.yaml epochs=50 imgsz=640
model=yolov8n.pt : YOLOv8-nano (léger, rapide)
epochs=50 : nombre ditérations dentraînement
imgsz=640 : taille des images
📌 6) Tester ton modèle
Après entraînement, test :
yolo task=detect mode=predict model=runs/detect/train/weights/best.pt source="test_images/"
Les images annotées seront enregistrées sous runs/detect/predict.
🧾 Résumé des étapes
Étape Description
1 Installer Autodistill & plugins
2 Mettre toutes les images brutes dans un dossier
3 Lancer le script dauto-annotation
4 Vérifier images + .txt générés
5 Entraîner YOLOv8 avec ces données
6 Tester le modèle entraîné
⚠️ Conseils & limitations
Lauto-annotation nest pas parfaite : certains objets peuvent être mal classés ou mal localisés — revise au besoin.
Les prompts textuels (ontology_map) doivent être précis et adaptés à ce que tu veux détecter (feuilles, fleurs, etc.).
Un GPU rend lentraînement YOLOv8 bien plus rapide.
Si tu veux, je peux générer une version complète du data.yaml prête à lemploi ou adapter ce pipeline à des situations spécifiques (ex : dataset grand public, interface graphique simple, intégration mobile). Dis-moi juste le cas dusage que tu vises.

BIN
data/jardin.db Normal file

Binary file not shown.

1
data/meteo_cache.json Normal file
View File

@@ -0,0 +1 @@
{"cached_at": "2026-02-22T12:59:49.373422+00:00", "days": [{"date": "2026-02-22", "t_max": 14.1, "t_min": 2.1, "pluie_mm": 0, "vent_kmh": 10.8, "code": 3, "label": "Couvert", "icone": "☁️"}, {"date": "2026-02-23", "t_max": 12.0, "t_min": 4.5, "pluie_mm": 0, "vent_kmh": 16.8, "code": 3, "label": "Couvert", "icone": "☁️"}, {"date": "2026-02-24", "t_max": 14.0, "t_min": 4.1, "pluie_mm": 0, "vent_kmh": 6.4, "code": 45, "label": "Brouillard", "icone": "🌫"}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Some files were not shown because too many files have changed in this diff Show More