Chapitre 10 — Collecte et Visualisation des Données

Mesurer c’est bien, enregistrer et visualiser c’est mieux. Ce chapitre couvre la chaîne complète : acquisition des données par l’ESP32, transmission (WiFi, MQTT), stockage (fichier local, base de données, cloud), et visualisation (tableaux de bord, graphiques). L’objectif : transformer des flux de nombres bruts en informations exploitables pour piloter son jardin.

10.1 Architecture de la chaîne de données

[Capteurs] → [ESP32] → [WiFi] → [Broker MQTT / API HTTP]
                                        │
                                        ▼
                              [Base de données]
                              (SQLite / InfluxDB)
                                        │
                                        ▼
                              [Visualisation]
                         (Grafana / Matplotlib / Web)

Deux approches coexistent :

Approche Avantages Inconvénients
Locale (fichier CSV sur carte SD) Autonome, pas de réseau Pas de consultation à distance
Connectée (WiFi + serveur) Temps réel, alertes, historique Dépendance réseau et serveur

Un bon système combine les deux : stockage local en tampon + envoi réseau quand disponible.

10.2 Stockage local sur l’ESP32

Système de fichiers LittleFS

L’ESP32 dispose de 4 Mo de flash, dont une partie est formatée en LittleFS (anciennement SPIFFS). On peut y écrire des fichiers CSV :

# Écriture d'une mesure sur le système de fichiers
import time, os

def enregistrer_mesure(fichier, zone, humidite,
                       temperature, action):
    t = time.localtime()
    ligne = "{:04d}-{:02d}-{:02d},{:02d}:{:02d}:{:02d}," \
            "{},{:.1f},{:.1f},{}\n".format(
        *t[:6], zone, humidite, temperature, action)
    with open(fichier, "a") as f:
        f.write(ligne)

Capacité : un fichier CSV avec 6 champs × 50 caractères/ligne ≈ 50 octets/ligne. Avec 2 Mo disponibles, on peut stocker environ 40 000 lignes, soit ~400 jours à 100 mesures/jour.

Rotation des fichiers

Pour éviter de saturer la mémoire :

def rotation_log(fichier, taille_max=500_000):
    """Supprime le fichier si > 500 Ko"""
    try:
        stat = os.stat(fichier)
        if stat[6] > taille_max:
            os.rename(fichier, fichier + ".old")
    except OSError:
        pass  # fichier n'existe pas encore

Carte SD (SPI)

Pour un stockage plus important, une carte micro-SD connectée en SPI offre des Go de capacité :

from machine import SPI, Pin
import sdcard, os

spi = SPI(1, baudrate=1_000_000,
          sck=Pin(18), mosi=Pin(23), miso=Pin(19))
cs = Pin(5, Pin.OUT)
sd = sdcard.SDCard(spi, cs)
os.mount(sd, "/sd")
# Écriture identique : open("/sd/data.csv", "a")

10.3 Transmission WiFi

Connexion au réseau

import network, time

def connecter_wifi(ssid, password, timeout=15):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(ssid, password)
    debut = time.time()
    while not wlan.isconnected():
        if time.time() - debut > timeout:
            raise RuntimeError("WiFi timeout")
        time.sleep(0.5)
    print("IP:", wlan.ifconfig()[0])
    return wlan

Point pratique : la connexion WiFi consomme ~240 mA. Pour maximiser l’autonomie, connectez-vous uniquement pour envoyer les données, puis repassez en deep sleep.

HTTP POST vers un serveur

L’approche la plus simple est un POST HTTP vers un serveur Flask ou FastAPI :

import urequests, json

def envoyer_donnees(url, donnees):
    """Envoie un dict de mesures en JSON"""
    headers = {"Content-Type": "application/json"}
    r = urequests.post(url, data=json.dumps(donnees),
                       headers=headers)
    ok = r.status_code == 200
    r.close()
    return ok

10.4 Protocole MQTT

MQTT (Message Queuing Telemetry Transport) est le protocole standard pour l’IoT. Il est léger, fiable, et parfaitement adapté aux microcontrôleurs.

Concepts clés

Concept Description
Broker Serveur central qui relaie les messages (ex : Mosquitto)
Topic Canal de communication hiérarchique (ex : jardin/potager/humidite)
Publish Envoyer un message sur un topic
Subscribe Écouter un topic pour recevoir les messages
QoS Qualité de service : 0 (au plus une fois), 1 (au moins une fois), 2 (exactement une fois)
Retain Le broker conserve le dernier message pour les nouveaux abonnés

Hiérarchie de topics pour le jardin

jardin/
├── potager/
│   ├── humidite        → 45.2
│   ├── temperature     → 22.1
│   ├── arrosage/status → ON
│   └── arrosage/volume → 12.5
├── pelouse/
│   ├── humidite        → 38.7
│   └── temperature     → 21.8
├── meteo/
│   ├── temperature     → 24.3
│   ├── humidite_air    → 62
│   └── pluie           → false
└── systeme/
    ├── batterie        → 85
    ├── wifi_rssi       → -67
    └── status          → OK

Client MQTT sur ESP32

from umqtt.simple import MQTTClient
import json

def publier_mqtt(broker, client_id, topic, donnees):
    client = MQTTClient(client_id, broker)
    client.connect()
    payload = json.dumps(donnees)
    client.publish(topic, payload, retain=True)
    client.disconnect()

Équation clé — Bande passante MQTT :

Débit = (taille_header + taille_payload) × fréquence_publication × nombre_capteurs

Header MQTT ≈ 5 octets. Payload JSON typique ≈ 50 octets. À 1 msg/min pour 5 capteurs : 55 × 5 / 60 ≈ 4,6 octets/s — négligeable.

10.5 Serveur de réception (Python)

Serveur Flask minimal

Côté serveur (Raspberry Pi, PC, VPS), un script Python reçoit et stocke les données :

# code/serveur_donnees.py (extrait)
from flask import Flask, request, jsonify
import sqlite3, datetime

app = Flask(__name__)

@app.route("/api/mesure", methods=["POST"])
def recevoir_mesure():
    data = request.json
    conn = sqlite3.connect("jardin.db")
    conn.execute(
        "INSERT INTO mesures VALUES (?,?,?,?,?)",
        (datetime.datetime.now().isoformat(),
         data["zone"], data["humidite"],
         data["temperature"], data.get("action", ""))
    )
    conn.commit()
    conn.close()
    return jsonify({"status": "ok"})

Récepteur MQTT avec Paho

# code/mqtt_receiver.py (extrait)
import paho.mqtt.client as mqtt
import sqlite3, json, datetime

def on_message(client, userdata, msg):
    data = json.loads(msg.payload)
    topic_parts = msg.topic.split("/")
    zone = topic_parts[1] if len(topic_parts) > 1 else "inconnu"
    conn = sqlite3.connect("jardin.db")
    conn.execute(
        "INSERT INTO mesures VALUES (?,?,?,?,?)",
        (datetime.datetime.now().isoformat(),
         zone, data.get("humidite"),
         data.get("temperature"), "")
    )
    conn.commit()
    conn.close()

10.6 Base de données

SQLite (simple, local)

Parfait pour un projet personnel. Schéma de la table principale :

CREATE TABLE mesures (
    timestamp TEXT NOT NULL,
    zone TEXT NOT NULL,
    humidite REAL,
    temperature REAL,
    action TEXT,
    volume_eau REAL DEFAULT 0
);

CREATE INDEX idx_mesures_time ON mesures(timestamp);
CREATE INDEX idx_mesures_zone ON mesures(zone);

InfluxDB (séries temporelles)

Pour des volumes plus importants ou une rétention configurée, InfluxDB est une base de données optimisée pour les séries temporelles :

from influxdb_client import InfluxDBClient, Point

client = InfluxDBClient(
    url="http://localhost:8086",
    token="mon_token", org="jardin")
write_api = client.write_api()

point = (Point("sol")
    .tag("zone", "potager")
    .field("humidite", 45.2)
    .field("temperature", 22.1))
write_api.write(bucket="jardin", record=point)

Avantages d’InfluxDB :

10.7 Visualisation avec Python (Matplotlib)

Graphique d’humidité sur 7 jours

# code/visualisation_humidite.py (extrait)
import sqlite3
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime

conn = sqlite3.connect("jardin.db")
rows = conn.execute(
    "SELECT timestamp, humidite FROM mesures "
    "WHERE zone='potager' "
    "ORDER BY timestamp DESC LIMIT 1008"
).fetchall()
conn.close()

temps = [datetime.fromisoformat(r[0]) for r in rows]
hum = [r[1] for r in rows]

Le fichier complet code/visualisation_humidite.py génère un graphique avec zones de confort, seuils d’arrosage et annotations des événements d’irrigation.

Graphique multi-capteurs

fig, axes = plt.subplots(3, 1, figsize=(12, 8),
                          sharex=True)
axes[0].plot(temps, humidite, "b-", label="Humidité (%)")
axes[0].axhspan(40, 70, alpha=0.1, color="green",
                label="Zone optimale")
axes[1].plot(temps, temperature, "r-",
             label="Température (°C)")
axes[2].bar(temps_arrosage, volumes, width=0.02,
            color="cyan", label="Arrosage (L)")

Statistiques descriptives

import numpy as np

def stats_zone(conn, zone, jours=7):
    rows = conn.execute(
        "SELECT humidite, temperature FROM mesures "
        "WHERE zone=? AND timestamp > datetime("
        "'now', ?)", (zone, f"-{jours} days")
    ).fetchall()
    h = np.array([r[0] for r in rows if r[0]])
    t = np.array([r[1] for r in rows if r[1]])
    return {
        "humidite_moy": np.mean(h),
        "humidite_std": np.std(h),
        "temp_moy": np.mean(t),
        "temp_min": np.min(t),
        "temp_max": np.max(t),
    }

10.8 Tableaux de bord avec Grafana

Grafana est un outil open-source de visualisation qui se connecte directement à InfluxDB, SQLite (via plugin), ou d’autres sources.

Installation

# Sur Raspberry Pi ou serveur Linux
sudo apt install -y grafana
sudo systemctl enable grafana-server
sudo systemctl start grafana-server
# Interface web : http://localhost:3000

Tableau de bord type pour le jardin

Un dashboard Grafana pour le jardin pourrait contenir :

Panel Type Données
Humidité temps réel Gauge Dernière valeur par zone
Historique humidité Time series 7 derniers jours
Température sol/air Time series Superposition sol + air
Volume d’eau quotidien Bar chart Somme par jour
Batterie ESP32 Gauge Niveau en %
Alertes Table Événements anormaux
Statistiques Stat Min/Max/Moy de la semaine

Alertes Grafana

Grafana peut envoyer des alertes par email, Telegram ou webhook :

10.9 Analyse des données historiques

Tendances saisonnières

Avec un historique de plusieurs mois, on peut analyser les cycles :

# code/analyse_saisonniere.py (extrait)
import pandas as pd

df = pd.read_sql(
    "SELECT * FROM mesures", conn,
    parse_dates=["timestamp"])
df.set_index("timestamp", inplace=True)

# Moyenne journalière
quotidien = df.groupby("zone").resample("D").mean()

# Moyenne mobile sur 7 jours
quotidien["hum_lissee"] = (
    quotidien["humidite"].rolling(7).mean())

Corrélations

L’analyse des corrélations entre variables révèle les dynamiques du jardin :

r(humidité, température) : corrélation négative attendue (plus chaud → sol plus sec)

r(humidité, arrosage) : corrélation positive avec un décalage temporel (lag)

# Matrice de corrélation
corr = df[["humidite", "temperature",
           "volume_eau"]].corr()
print(corr)

Détection d’anomalies

Une mesure est anomale si elle s’écarte de plus de 3 écarts-types de la moyenne mobile :

Anomalie si : x - μ_mobile > 3σ_mobile
def detecter_anomalies(serie, fenetre=48):
    """Détection par z-score sur moyenne mobile"""
    mu = serie.rolling(fenetre).mean()
    sigma = serie.rolling(fenetre).std()
    z = (serie - mu) / sigma
    return z.abs() > 3

10.10 Export et partage des données

Export CSV

df.to_csv("export_jardin_2026.csv", index=True)

Rapport automatique (email)

# code/rapport_hebdo.py (extrait)
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage

def envoyer_rapport(destinataire, graphique_path):
    msg = MIMEMultipart()
    msg["Subject"] = "Rapport Jardin - Semaine"
    msg["To"] = destinataire
    with open(graphique_path, "rb") as f:
        img = MIMEImage(f.read())
    msg.attach(img)
    # ... envoi via SMTP

API REST pour applications tierces

Le serveur Flask peut aussi exposer les données pour une application mobile ou un site web :

@app.route("/api/derniere/<zone>")
def derniere_mesure(zone):
    conn = sqlite3.connect("jardin.db")
    row = conn.execute(
        "SELECT * FROM mesures "
        "WHERE zone=? ORDER BY timestamp DESC "
        "LIMIT 1", (zone,)
    ).fetchone()
    conn.close()
    return jsonify({
        "timestamp": row[0], "zone": row[1],
        "humidite": row[2], "temperature": row[3]
    })

10.11 Récapitulatif

Couche Technologie Complexité
Stockage local Fichier CSV sur flash/SD ★☆☆
Transmission HTTP POST ou MQTT ★★☆
Base de données SQLite (simple) ou InfluxDB (avancé) ★★☆ / ★★★
Visualisation Matplotlib (scripts) ou Grafana (dashboard) ★★☆ / ★★★
Analyse Pandas + NumPy ★★☆
Alertes Grafana ou script Python ★★☆
API Flask / FastAPI ★★☆

💡 Point pratique : Pour démarrer, un simple fichier CSV sur la flash de l’ESP32 + un script Matplotlib suffit. Ajoutez MQTT + InfluxDB + Grafana quand vous avez besoin de surveillance temps réel ou d’historique long terme.


← Précédent : Irrigation Automatique Suivant : Projet Intégré — Le Jardin Connecté →

← Retour à la Table des Matières