🌱 GrowManager : L'appli de gestion de culture (mais pas que)


Messages recommandés

Bonjour a tous

 

Allez reprenons la suite de la présentation avec la page suivante : 
 

6 - Les Extractions - Rosin & Hash

 

Ce que c'est
GrowManager intègre deux pages dédiées aux extractions : une pour la Rosin et une pour le Hash. L'idée est la même dans les deux cas - enregistrer chaque session, suivre vos rendements, et accumuler des stats pour progresser d'une session à l'autre.

 

Extractions Rosin
La liste des sessions
Toutes vos sessions de presse sont listées dans un tableau triable : variété, maillage, nombre de passes, poids d'entrée, poids de sortie, rendement en %, date. Triable sur toutes ces colonnes, avec tri par défaut sur la date (les plus récentes en premier).
Une barre de recherche vous permet de filtrer par variété - et quand vous filtrez, les statistiques en haut de page se recalculent automatiquement pour ne refléter que les sessions de la variété sélectionnée. Pratique pour comparer les rendements d'un même strain sur plusieurs sessions.

1774584802_06-ExtractionsRosin.thumb.png.8de640631f93e997b3b55484c9ce7ea4.png

 

Les stats
Quatre cartes en haut de page :

  • Rendement moyen
  • Total pressĂ© (poids d'entrĂ©e cumulĂ©)
  • Rosin extrait (poids de sortie cumulĂ©)
  • Nombre de sessions

Ces chiffres s'adaptent au filtre variété si vous en avez un d'actif.


1449475704_06-ExtractionRosin-Statsparvarieteetparanne.thumb.png.342765b583b28ff490564393ab4ee01a.png

 

Enregistrer une session
Quand vous lancez une nouvelle session, vous renseignez :

  • La variĂ©tĂ© pressĂ©e
  • Le maillage du rosin bag
  • Le nombre de passes
  • Le poids de chaque sac individuellement (en grammes avec deux dĂ©cimales) - le total d'entrĂ©e se calcule automatiquement
  • Le poids de sortie (rosin rĂ©cupĂ©rĂ©)
  • La tempĂ©rature de presse
  • Des notes libres

Le rendement est calculé automatiquement.

1664060987_06-ExtractionRosin-ModalAjoutExtraction1.thumb.png.28a687ef9de2ab7090a69201881ceeaf.png1081693828_06-ExtractionRosin-ModalAjoutExtraction2.thumb.png.2fbcce483d15604b5533ce3f746f66d7.png

 

Le détail d'une session
Chaque session est cliquable pour accéder à sa fiche complète - tous les paramètres enregistrés, les poids par sac, le rendement calculé, les notes.
913676626_06-ExtractionRosin-ModalDetailExtraction.thumb.png.81fe51b173d1ad833476b7ce585bbe20.png

 

Extractions Hash
La page Hash fonctionne sur le même principe mais adaptée aux techniques de tamisage à sec (Polinator) et à l'eau (Ice-o-lator).
350743632_06-ExtractionHash.thumb.png.4e790f40eb111f00d40048d5d2826e3f.png

 

Vous enregistrez vos sessions avec :

la variété, la technique utilisée, le nombre de passages, les sacs et leurs rendements, les notes.

Même système de filtre par variété, même logique de stats cumulées.


Ice-O-Lator :

1232043922_06-ExtractionHash-IceOLator2.thumb.png.5f7283672d4ae1b69f522fdd3ae964f9.png892523844_06-ExtractionHash-IceOLator3.thumb.png.5d4790b3f1947f4d4e45cce91a9657c0.png

 

Polinator (mais marche aussi pour les tamis)

1781731505_06-ExtractionHash-Pollinator.png.69b35edf29d168d53de5e84371b2acba.png

 

N'ayant pas eu l'occasion de faire du hash dernièrement je n'ai pas eu l'occasion de tester les fonctionnalités de cette page en conditions réelles donc il manque surement plein de choses

 

Les deux pages extractions sont aussi alimentées par l'onglet "Pour extraction" de la page Stock - vous savez toujours exactement ce que vous avez en attente de traitement et depuis combien de temps.

 

D'autres options d'extractions seront ajoutées dans le futur (BHO, etc...)

 

A+

Hey bonjour a tous,

 

@Lamic-Tal : Désolé j'avais zappé de te répondre. L'intégration des outils bluetooth est pas prete d'arrivée. Je sais meme pas si c'est possible. Je vais ajouter des options pour d'autres outils mais uniquement via wifi pour le moment.

@Maledikchaouch encore merci pour tous tes retours.

- Pour le Dashboard c'est pas normal il doit s'afficher meme si tu n'a rien. Il s'affiche juste avec des valeurs a 0 partout mais il doit t'afficher les modules.
- Tres bonne idée pour les clones je vais bosser dessus et je te tiens au jus

- Tres bonne idée aussi pour les croisements je vais bosser dessus aussi

 

A+

Modifié par mdf73
  • Like 1
  • Thanks 6
Lien Ă  poster
Partager sur d’autres sites

Hello dominical !

 

Il y a 21 heures, mdf73 a dit:

- Pour le Dashboard c'est pas normal il doit s'afficher meme si tu n'a rien. Il s'affiche juste avec des valeurs a 0 partout mais il doit t'afficher les modules.

 

nodash.thumb.PNG.923689c807bbee8f8ab35b6abed82b5e.PNG

 

J'ai suivi tes instructions (pas le choix vu mon expertise dans le domaine), c'est donc ta faute 🤣. Plus sérieusement, c'est comme ça depuis la première installation et ça m'énerve d'être trop une buse pour essayer de comprendre pourquoi (plus que le non-affichage).

 

Tentative  de suivre le guide que tu m'as envoyé pour Github dans la matinée, je viendrai éditer le post après avoir tenté une réinstallation via la plateforme.

 

 

++ 

  • Like 1
  • Haha 1
Lien Ă  poster
Partager sur d’autres sites

Bravo, super application. C'est la plus complète et relativement simple à metttre en place. 

Je l'ai fait tourner via portainer sur mon serveur. J'ai mouliné un peu avec l'AI pour ajouter les capteurs que j'ai déjà. 

Les capteurs tournent sur esphome, une solution portée par la communauté Home Assistant. Pour se faire, il suffit : 


Créer backend/app/schemas/esphome.py

 

"""
Schémas Pydantic pour l'intégration ESPHome.
NOUVEAU FICHIER — backend/app/schemas/esphome.py
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel


class ESPHomePushPayload(BaseModel):
    """Corps JSON envoyé par le capteur ESPHome via http_request."""
    device_id:   str
    temperature: Optional[float] = None
    humidite:    Optional[float] = None
    co2:         Optional[float] = None
    timestamp:   Optional[int]  = None   # Unix timestamp UTC (optionnel)


class ESPHomePushResult(BaseModel):
    status:  str
    id_log:  Optional[int]  = None
    vpd:     Optional[float] = None
    message: Optional[str]  = None


class ESPHomeDeviceCreate(BaseModel):
    """Enregistrement d'un nouveau capteur ESPHome."""
    nom:       str
    device_id: str
    ip_lan:    Optional[str] = None
    id_espace: Optional[int] = None
    notes:     Optional[str] = None



Étape 2 — Créer backend/app/routers/esphome.py


 

"""
Router ESPHome — intégration capteurs ESPHome dans GrowManager.
NOUVEAU FICHIER — backend/app/routers/esphome.py

Architecture :
  - ESPHome pousse ses données via POST /api/capteurs/esphome/push
  - Les capteurs ESPHome sont stockés dans GoveeDevice avec modele="esphome"
  - Les relevés sont dans TemperatureLog avec source="esphome"
    => graphiques et filtres existants déjà compatibles sans toucher au frontend
"""
from datetime import datetime, timezone
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from app.database import get_db
from app.models.all_models import GoveeDevice, TemperatureLog
from app.schemas.esphome import ESPHomePushPayload, ESPHomePushResult, ESPHomeDeviceCreate
from app.services.govee_poller import compute_vpd, _get_active_culture_id

router = APIRouter(tags=["esphome"])

ESPHOME_MODELE = "esphome"


# ─────────────────────────────────────────────────────────────────────────────
# Helper interne
# ─────────────────────────────────────────────────────────────────────────────

def _get_device_by_esphome_id(device_id: str, db: Session) -> Optional[GoveeDevice]:
    return (
        db.query(GoveeDevice)
        .filter(
            GoveeDevice.device_id == device_id,
            GoveeDevice.modele    == ESPHOME_MODELE,
        )
        .first()
    )


# ─────────────────────────────────────────────────────────────────────────────
# Endpoint principal : réception des données ESPHome
# ─────────────────────────────────────────────────────────────────────────────

@router.post("/api/capteurs/esphome/push", response_model=ESPHomePushResult)
def esphome_push(payload: ESPHomePushPayload, db: Session = Depends(get_db)):
    """
    Reçoit un relevé depuis un capteur ESPHome via HTTP POST.
    Le capteur doit être enregistré au préalable via POST /api/capteurs/esphome/devices.
    """
    device = _get_device_by_esphome_id(payload.device_id, db)
    if not device:
        raise HTTPException(
            status_code=404,
            detail=f"Capteur ESPHome '{payload.device_id}' non enregistré. "
                   f"Créez-le d'abord via POST /api/capteurs/esphome/devices"
        )

    if not device.actif:
        return ESPHomePushResult(status="ignored", message="Capteur inactif — donnée ignorée")

    # Calcul VPD si temp + humidité disponibles
    vpd = None
    if payload.temperature is not None and payload.humidite is not None:
        vpd = compute_vpd(payload.temperature, payload.humidite)

    # Culture active liée à l'espace du capteur
    id_culture = None
    if device.id_espace:
        id_culture = _get_active_culture_id(db, device.id_espace)

    # Horodatage : timestamp ESPHome si fourni, sinon maintenant
    if payload.timestamp:
        date_heure = datetime.fromtimestamp(payload.timestamp, tz=timezone.utc)
    else:
        date_heure = datetime.now(timezone.utc)

    # Insertion dans TemperatureLog avec source="esphome"
    log = TemperatureLog(
        id_device=   device.id_device,
        id_culture=  id_culture,
        id_espace=   device.id_espace,
        date_heure=  date_heure,
        temperature= payload.temperature,
        humidite=    payload.humidite,
        vpd=         vpd,
        source=      "esphome",
    )
    db.add(log)
    db.commit()
    db.refresh(log)

    return ESPHomePushResult(status="ok", id_log=log.id_log, vpd=vpd)


# ─────────────────────────────────────────────────────────────────────────────
# CRUD capteurs ESPHome
# ─────────────────────────────────────────────────────────────────────────────

@router.get("/api/capteurs/esphome/devices")
def list_esphome_devices(db: Session = Depends(get_db)):
    """Liste tous les capteurs ESPHome enregistrés."""
    return (
        db.query(GoveeDevice)
        .filter(GoveeDevice.modele == ESPHOME_MODELE)
        .order_by(GoveeDevice.nom)
        .all()
    )


@router.post("/api/capteurs/esphome/devices", status_code=201)
def create_esphome_device(payload: ESPHomeDeviceCreate, db: Session = Depends(get_db)):
    """
    Enregistre un nouveau capteur ESPHome.
    Le device_id doit correspondre exactement à celui configuré dans le YAML ESPHome.
    """
    existing = _get_device_by_esphome_id(payload.device_id, db)
    if existing:
        raise HTTPException(
            status_code=409,
            detail=f"Un capteur ESPHome avec device_id='{payload.device_id}' existe déjà"
        )

    device = GoveeDevice(
        nom=       payload.nom,
        device_id= payload.device_id,
        modele=    ESPHOME_MODELE,
        ip_lan=    payload.ip_lan,
        id_espace= payload.id_espace,
        actif=     True,
        notes=     payload.notes,
    )
    db.add(device)
    db.commit()
    db.refresh(device)
    return device


@router.delete("/api/capteurs/esphome/devices/{device_id}", status_code=204)
def delete_esphome_device(device_id: int, db: Session = Depends(get_db)):
    """Supprime un capteur ESPHome (les logs associés sont conservés)."""
    device = db.query(GoveeDevice).filter(GoveeDevice.id_device == device_id).first()
    if not device or device.modele != ESPHOME_MODELE:
        raise HTTPException(status_code=404, detail="Capteur ESPHome introuvable")
    db.delete(device)
    db.commit()


Étape 3 — Modifier backend/app/main.py


Ajouter : 

from app.routers import esphome

Et

app.include_router(esphome.router)


A la suite des autres from.... et app.inclu.... 

Ouvrir http://votregrowmanager:port/docs
Aller à POST /api/capteurs/esphome/devices


Ajouter : 

 

{
  "nom": "LeNom",
  "device_id": "LeNom",
  "id_espace": 1,
  "notes": "Capteur température/humidité ESPHome"
}

 

 

 

Ensuite dans esphome, ajouter Ă  votre yaml :

 

interval:
  - interval: 60s
    then:
      - if:
          condition:
            lambda: |-
              return !isnan(id(LeNom_temperature).state) &&
                     !isnan(id(LeNom_humidite).state);
          then:
            - http_request.post:
                url: "http://localhost:port/api/capteurs/esphome/push"
                request_headers:
                  Content-Type: application/json
                json: |-
                  root["device_id"] = "LeNom";
                  root["temperature"] = id(LeNom_temperature).state;
                  root["humidite"] = id(LeNom_humidite).state;
                  root["co2"] = 0;


Petite modification de niche, mais ça pourra peut être guider ceux qui veulent modifier un peu... 

J'ai aussi fait une petite modification perso du docker-compose.yaml

 

version: "3.8"

services:
  db:
    image: mysql:8.2
    container_name: growmanager-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-growroot}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-growmanager}
      MYSQL_USER: ${MYSQL_USER:-grow}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD:-grow2024}
      TZ: ${TZ:-Europe/Paris}
    volumes:
      - ${GM_DATA_ROOT}/mysql:/var/lib/mysql
      - ./backend/init.sql:/docker-entrypoint-initdb.d/01_init.sql:ro
    ports:
      - "${MYSQL_PORT:-3306}:3306"
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h localhost -u${MYSQL_USER:-grow} -p${MYSQL_PASSWORD:-grow2024} --silent"]
      interval: 10s
      timeout: 5s
      retries: 10

  backend:
    build:
      context: ./backend
    container_name: growmanager-backend
    restart: unless-stopped
    environment:
      DATABASE_URL: mysql+pymysql://${MYSQL_USER:-grow}:${MYSQL_PASSWORD:-grow2024}@db:3306/${MYSQL_DATABASE:-growmanager}
      SECRET_KEY: ${SECRET_KEY:-changeme-in-production}
      GROWMANAGER_URL: ${GROWMANAGER_URL:-http://growmanager}
      TZ: ${TZ:-Europe/Paris}
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "${BACKEND_PORT:-8000}:8000"
    volumes:
      - ${GM_DATA_ROOT}/uploads:/app/uploads

  frontend:
    build:
      context: ./frontend
    container_name: growmanager-frontend
    restart: unless-stopped
    depends_on:
      - backend
    ports:
      - "${FRONTEND_PORT:-5173}:5173"

  nginx:
    image: nginx:alpine
    container_name: growmanager-nginx
    restart: unless-stopped
    ports:
      - "${APP_PORT:-80}:80"
    volumes:
      - ${GM_DATA_ROOT}/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - frontend
      - backend



Avec en environnement : 

 

GM_DATA_ROOT=/mnt/user/appdata/growmanager
MYSQL_ROOT_PASSWORD=ChangeMoiRoot
MYSQL_DATABASE=growmanager
MYSQL_USER=grow
MYSQL_PASSWORD=ChangeMoiApp
SECRET_KEY=UneCleLongueAleatoire
GROWMANAGER_URL=http://xxx.xxx.xxx.:420
TZ=Europe/Paris
MYSQL_PORT=3306
BACKEND_PORT=8000
FRONTEND_PORT=5173
APP_PORT=420


En changeant les ports dans l'environnement, ça ignore les ports du compose, ça m'arrange. Plus simple de les gérer ici directement. 




Petite question au passage, comment modifier les offsets pour la température des feuilles dans le calcul du vpd ? Serait il plus simple de tourner directement avec une image ? Comme ça on a un versioning plus logique avec des tags etc... Encore une fois, je ne suis qu'un amateur, je dis peut être des conneries... 


A bientĂ´t !


Edit : Je dois être nul mais si l'on crée une culture (alors déjà en cours) comment éditer les différentes dates germination, passage 12/12 etc ? 
Edit2 : On ne peut pas cliquer sur les cases du calendrier passées, mais on peut cliquer sur une à venir et dans le menu déroulant choisir une date passée. Je pense que c'est un bon moyen d'assurer une erreur éventuelle de sélection de date, pas la peine de modifier pour pouvoir cliquer sur les cases passées bien que ça puisse être rébarbatif

Modifié par Dominiquesuppo
Ajoute question culture.
  • Like 1
  • Thanks 2
Lien Ă  poster
Partager sur d’autres sites

Bonsoir les jardigeek

Deja un grand merci Ă  tous pour vos retours c'est top !!

@Maledikchaouch : La partie Croisements a été revue en profondeur. On a ajouté un troisième onglet dédié "Open Field" qui gère exactement ce que tu décrivais : tu peux créer un projet avec N plantes mères, ajouter autant de pères que tu veux (depuis le stock de pollen ou en phénotype libre), et cocher les pères probables par mère. Pour chaque mère tu indiques si la pollinisation est simple (1 mâle identifié) ou ouverte (plusieurs pères, voire inconnus). À la récolte, ça crée automatiquement une nouvelle variété (♀ × ♂) et un pack de graines dans le catalogue. Et oui, tu peux lier tout ça à une culture active existante pour choisir les parents directement depuis tes plantes en cours.

@Dominiquesuppo : On a traité tous tes points :

  • ESPHome est maintenant intĂ©grĂ© nativement. Les capteurs ESPHome s'ajoutent dans ParamĂ©trage → onglet Capteurs, cĂ´te Ă  cĂ´te avec les Govee. Les logs rentrent dans la mĂŞme table que les autres capteurs, donc les graphiques et le VPD fonctionnent sans rien changer.
  • Offset tempĂ©rature foliaire (VPD) : c'est maintenant un paramètre configurable dans ParamĂ©trage → onglet Capteurs. La valeur par dĂ©faut est 2°C (diffĂ©rentiel feuille/air classique) mais tu peux l'ajuster.
  • Édition des dates d'une culture dĂ©jĂ  en cours : un bouton "Dates" a Ă©tĂ© ajoutĂ© dans le header du dĂ©tail de culture. Tu peux modifier germination, passage 12/12, dĂ©but floraison, rĂ©colte estimĂ©e, date de fin — sans contrainte sur les dates passĂ©es.
  • Images Docker avec versioning : c'est fait. Les images sont buildĂ©es et publiĂ©es automatiquement sur ghcr.io/mdf73/ Ă  chaque tag vX.Y.Z. Pour Portainer, tu utilises docker-compose.prod.yml et un simple ./update.sh v1.2.0 pour passer Ă  une nouvelle version. Plus besoin de builder en local.

Merci encore pour les retours constructifs, c'est exactement ce qui fait avancer le projet ! 🙏

  • Like 3
Lien Ă  poster
Partager sur d’autres sites