Automatisation IoT

Station météo DIY MQTT : vent, pluie et température avec un ESP32

Câblez un anémomètre, un pluviomètre et un BME280 à un ESP32, publiez vent, pluie, température, humidité et pression en JSON MQTT, puis enregistrez et tracez les données avec un court script Python.

Mât de jardin équipé d'un anémomètre, d'un pluviomètre et d'un abri de capteur reliés à un ESP32 dans un montage de station météo DIY MQTT
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Une station météo DIY MQTT vous fournit des mesures locales de vent, de pluie, de température, d'humidité et de pression auxquelles n'importe quel tableau de bord, système domotique de maison connectée ou contrôleur d'arrosage peut s'abonner. Dans ce tutoriel, vous allez câbler des capteurs météo courants à un ESP32, publier un message JSON par minute vers un broker et utiliser un abonné Python pour enregistrer les mesures et tracer un graphique. Tout est d'abord testé sur le serveur MQTT public gratuit, puis basculé vers des identifiants privés.

Composants et câblage du nœud de station météo DIY MQTT

La conception repose sur des composants faciles à trouver et bien documentés. Les capteurs de vent et de pluie sont ceux du kit « weather meter » classique : un anémomètre à coupelles et un pluviomètre à augets basculants, tous deux équipés d'interrupteurs reed qui se ferment brièvement à chaque rotation ou à chaque basculement.

Composant Rôle Connexion à l'ESP32
Carte ESP32 DevKit Wi-Fi et MQTT USB ou alimentation 5 V
Module BME280 (I2C) Température, humidité, pression SDA GPIO 21, SCL GPIO 22, 3,3 V, GND
Anémomètre à coupelles (interrupteur reed) Vitesse du vent Un fil sur GPIO 27, l'autre sur GND
Pluviomètre à augets basculants Précipitations Un fil sur GPIO 26, l'autre sur GND
Abri anti-rayonnement Protège le BME280 du soleil direct Mécanique
Boîtier étanche Protège l'ESP32 Mécanique

Les interrupteurs reed n'ont besoin que d'une résistance de pull-up ; le sketch active les pull-ups internes de l'ESP32, si bien qu'une fermeture tire la broche à l'état bas. Une girouette est souvent fournie dans ces kits ; elle utilise une échelle de résistances que l'on lit avec une broche ADC, et vous pourrez l'ajouter plus tard, une fois les bases fonctionnelles.

Deux constantes d'étalonnage proviennent de la fiche technique du capteur. Pour le kit courant utilisé ici, une fermeture de l'anémomètre par seconde correspond à 2,4 km/h, et un basculement d'auget équivaut à 0,2794 mm de pluie. Si votre matériel est différent, modifiez les deux constantes en haut du sketch.

Conception des topics et du payload

Utilisez un seul topic de télémétrie avec un payload JSON compact, plus un topic de statut retenu piloté par le Last Will MQTT :

connectmq/demo/<random-id>/weather/station1/telemetry   (JSON every 60 s)
connectmq/demo/<random-id>/weather/station1/status      (online/offline, retained)
{
  "temp_c": 14.8,
  "hum_pct": 71.2,
  "press_hpa": 1012.6,
  "wind_kmh": 9.6,
  "gust_kmh": 16.8,
  "rain_mm": 0.28,
  "rain_today_mm": 3.07,
  "rssi": -61,
  "uptime_s": 86412
}

Envoyer des moyennes par minute plutôt que chaque impulsion réduit le trafic à presque rien et reste largement dans les limites du serveur MQTT public gratuit : 10 messages par seconde et 64 Ko par message. Une valeur de rafale (la moyenne sur 3 secondes la plus élevée de la minute) conserve l'information que la moyenne masquerait autrement. Le guide consacré à ce serveur public énumère les autres limites, comme 8 niveaux de topic et 10 abonnements par client, que cette conception respecte sans difficulté.

Pourquoi ne pas retenir le message de télémétrie ? Vous le pouvez, et pour une station météo c'est souvent utile : un tableau de bord ouvert entre deux publications affiche immédiatement la dernière mesure au lieu d'un widget vide. La contrepartie est qu'une station tombée en panne depuis des heures semble toujours transmettre ses dernières valeurs, et c'est précisément pour cela que le topic status retenu séparé, avec Last Will, est important. Affichez les deux et grisez la mesure lorsque le statut vaut offline.

Le sketch ESP32

Installez ces bibliothèques depuis le gestionnaire de bibliothèques Arduino : PubSubClient de Nick O'Leary, Adafruit BME280 Library et Adafruit Unified Sensor. Modifiez les identifiants Wi-Fi et l'identifiant aléatoire dans le préfixe de topic.

#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/k3x9q2/weather/station1";

const int PIN_WIND = 27;
const int PIN_RAIN = 26;
const float KMH_PER_HZ = 2.4;     // anémomètre : 1 fermeture/s = 2,4 km/h
const float MM_PER_TIP = 0.2794;  // pluviomètre : 1 basculement = 0,2794 mm

WiFiClient net;
PubSubClient mqtt(net);
Adafruit_BME280 bme;

volatile uint32_t windCount = 0, rainCount = 0;
volatile uint32_t lastWindUs = 0, lastRainMs = 0;

void IRAM_ATTR onWind() {
  uint32_t now = micros();
  if (now - lastWindUs > 5000) { windCount++; lastWindUs = now; }  // anti-rebond 5 ms
}
void IRAM_ATTR onRain() {
  uint32_t now = millis();
  if (now - lastRainMs > 200) { rainCount++; lastRainMs = now; }   // anti-rebond 200 ms
}

char clientId[32];
char statusTopic[96];
float rainTodayMm = 0;
float maxGust = 0;
uint32_t lastGustCount = 0, lastGustMs = 0, lastPublishMs = 0, windAtPublish = 0;

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(clientId, nullptr, nullptr, statusTopic, 1, true, "offline")) {
      mqtt.publish(statusTopic, "online", true);
    } else {
      Serial.printf("MQTT failed, state=%d\n", mqtt.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_WIND, INPUT_PULLUP);
  pinMode(PIN_RAIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_WIND), onWind, FALLING);
  attachInterrupt(digitalPinToInterrupt(PIN_RAIN), onRain, FALLING);

  if (!bme.begin(0x76) && !bme.begin(0x77)) {
    Serial.println("BME280 not found, check wiring");
  }

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }

  snprintf(clientId, sizeof(clientId), "wx-%llx", ESP.getEfuseMac());
  snprintf(statusTopic, sizeof(statusTopic), "%s/status", PREFIX);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  connectMqtt();
  lastPublishMs = lastGustMs = millis();
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  uint32_t now = millis();

  // Rafale : moyenne sur 3 secondes la plus élevée de la fenêtre de publication
  if (now - lastGustMs >= 3000) {
    noInterrupts(); uint32_t c = windCount; interrupts();
    float gust = (c - lastGustCount) / ((now - lastGustMs) / 1000.0) * KMH_PER_HZ;
    if (gust > maxGust) maxGust = gust;
    lastGustCount = c; lastGustMs = now;
  }

  if (now - lastPublishMs >= 60000) {
    noInterrupts();
    uint32_t w = windCount; uint32_t r = rainCount; rainCount = 0;
    interrupts();

    float seconds = (now - lastPublishMs) / 1000.0;
    float windKmh = (w - windAtPublish) / seconds * KMH_PER_HZ;
    float rainMm = r * MM_PER_TIP;
    rainTodayMm += rainMm;   // remise à zéro à minuit par le logger ou via NTP

    char payload[256];
    snprintf(payload, sizeof(payload),
      "{\"temp_c\":%.1f,\"hum_pct\":%.1f,\"press_hpa\":%.1f,"
      "\"wind_kmh\":%.1f,\"gust_kmh\":%.1f,\"rain_mm\":%.2f,"
      "\"rain_today_mm\":%.2f,\"rssi\":%d,\"uptime_s\":%lu}",
      bme.readTemperature(), bme.readHumidity(), bme.readPressure() / 100.0,
      windKmh, maxGust, rainMm, rainTodayMm, WiFi.RSSI(), now / 1000);

    char topic[96];
    snprintf(topic, sizeof(topic), "%s/telemetry", PREFIX);
    mqtt.publish(topic, payload);
    Serial.println(payload);

    windAtPublish = w;
    maxGust = 0;
    lastPublishMs = now;
  }
}

Ouvrez le moniteur série à 115200 bauds. Faites tourner l'anémomètre à la main et basculez l'auget du pluviomètre plusieurs fois ; le message de la minute suivante doit afficher des valeurs non nulles. Dans un autre terminal, vérifiez que le broker reçoit bien les données :

mosquitto_sub -h broker.connectmq.com -p 1883 -i wx-viewer-5521 -t "connectmq/demo/k3x9q2/weather/#" -v

Si la carte ne se connecte jamais, le tutoriel ESP32 MQTT détaille pas à pas le dépannage du Wi-Fi et du broker.

Enregistrer et tracer les mesures

Le broker distribue les messages en direct ; il ne conserve pas l'historique pour vous. Un court abonné Python peut ajouter chaque mesure à un fichier CSV et générer un graphique. Installez les dépendances :

pip install "paho-mqtt>=2.0" matplotlib
import csv
import json
import os
import sys
from datetime import datetime, timezone
import paho.mqtt.client as mqtt

TOPIC = "connectmq/demo/k3x9q2/weather/station1/telemetry"
CSV_FILE = "weather.csv"
FIELDS = ["time", "temp_c", "hum_pct", "press_hpa", "wind_kmh", "gust_kmh", "rain_mm"]


def on_connect(client, userdata, flags, reason_code, properties):
    print("Connected:", reason_code)
    client.subscribe(TOPIC, qos=1)


def on_message(client, userdata, msg):
    data = json.loads(msg.payload)
    row = {"time": datetime.now(timezone.utc).isoformat(timespec="seconds")}
    row.update({k: data.get(k) for k in FIELDS[1:]})
    new_file = not os.path.exists(CSV_FILE)
    with open(CSV_FILE, "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        if new_file:
            writer.writeheader()
        writer.writerow(row)
    print(row)


def plot():
    import matplotlib.pyplot as plt
    times, temp, wind, gust, rain = [], [], [], [], []
    with open(CSV_FILE) as f:
        for r in csv.DictReader(f):
            times.append(datetime.fromisoformat(r["time"]))
            temp.append(float(r["temp_c"]))
            wind.append(float(r["wind_kmh"]))
            gust.append(float(r["gust_kmh"]))
            rain.append(float(r["rain_mm"]))
    fig, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex=True, figsize=(10, 8))
    ax1.plot(times, temp); ax1.set_ylabel("Temp (C)")
    ax2.plot(times, wind, label="wind"); ax2.plot(times, gust, label="gust")
    ax2.set_ylabel("km/h"); ax2.legend()
    ax3.bar(times, rain, width=0.0007); ax3.set_ylabel("Rain (mm/min)")
    fig.tight_layout()
    fig.savefig("weather.png", dpi=120)
    print("Saved weather.png")


if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "plot":
        plot()
    else:
        client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="wx-logger-8812")
        client.on_connect = on_connect
        client.on_message = on_message
        client.connect("broker.connectmq.com", 1883, keepalive=60)
        client.loop_forever()

Lancez python weather_logger.py pour collecter les données et python weather_logger.py plot pour produire weather.png. Pour une installation permanente, beaucoup remplacent le CSV par InfluxDB alimenté par Telegraf, ou par un tableau de bord Node-RED ; les deux se connectent au broker comme de simples clients MQTT.

Liste de contrôle de la précision

Une station météo ne vaut que par son emplacement. Passez cette liste en revue avant de vous fier aux chiffres :

  1. Abritez le capteur de température. Un BME280 en plein soleil ou enfermé dans un boîtier hermétique affiche plusieurs degrés de trop. Utilisez un abri anti-rayonnement ventilé.
  2. Montez l'anémomètre en hauteur et à découvert. Les toits et les clôtures créent des turbulences ; un mât dépassant les obstacles voisins donne un vent bien plus représentatif.
  3. Mettez le pluviomètre de niveau et éloignez-le des murs et des arbres qui bloquent la pluie ou font goutter.
  4. Éloignez l'ESP32 du BME280. La chaleur propre de la carte fausse les mesures s'ils partagent le même boîtier.
  5. Appliquez un anti-rebond logiciel et vérifiez le câblage. Les longs câbles non blindés captent du bruit qui ressemble à des basculements de pluie fantômes.
  6. Remettez à zéro le cumul de pluie journalier de façon cohérente, soit à minuit heure locale dans le logger, soit sur l'ESP32 grâce à l'heure NTP.

Faire évoluer votre station météo DIY MQTT

Une fois la station de base stable, quelques ajouts la rendent nettement plus utile.

Direction du vent. La girouette de la plupart des kits commute entre des résistances internes en tournant. Câblez-la en pont diviseur de tension avec une résistance fixe (la fiche technique du kit suggère une valeur) vers une broche ADC1 comme GPIO 34, lisez-la avec analogRead() et associez chaque plage de tension à l'un des 16 points cardinaux. Utilisez uniquement les broches ADC1, car l'ADC2 est inutilisable lorsque le Wi-Fi est actif sur l'ESP32. Publiez la direction en degrés, plus pratique pour les graphiques que « NNE ».

Alimentation solaire et batterie. Un petit panneau, une cellule lithium et un régulateur de charge peuvent faire fonctionner la station en autonomie, mais un ESP32 toujours allumé avec le Wi-Fi consomme trop pour un petit panneau en hiver. Passez en deep sleep entre deux publications et comptez les impulsions de l'anémomètre avec le coprocesseur ULP ou une puce de comptage dédiée, ou acceptez un intervalle de publication plus long. Ajoutez la tension de la batterie au JSON pour voir venir les problèmes.

Synthèses quotidiennes. Faites publier par le logger une synthèse quotidienne retenue (températures minimale et maximale, cumul de pluie, rafale maximale) sur un topic séparé juste après minuit. Les outils domotiques (Home Assistant, mais aussi Jeedom et Domoticz, qui savent communiquer en MQTT) peuvent alors utiliser la pluie de la veille pour sauter un cycle d'arrosage sans lire tout l'historique.

Valeurs dérivées. Le point de rosée et la température ressentie se calculent facilement à partir de la température et de l'humidité, dans le logger ou dans Node-RED. Calculez-les en aval plutôt que sur l'ESP32 afin de pouvoir modifier la formule sans reflasher.

Quitter le broker de test

Le serveur MQTT public gratuit est idéal pour valider le montage et prototyper, mais tout ce qui s'y trouve est lisible par n'importe qui et le préfixe de topic est votre seule séparation. Pour une station permanente, en particulier si elle alimente un système d'arrosage automatique, utilisez un broker privé avec TLS et identifiants. Un broker MQTT managé comme ConnectMQ vous l'apporte sans faire tourner de serveur chez vous. Passez le client à WiFiClientSecure, au port 8883, et utilisez le nom d'utilisateur, le mot de passe et le client ID fournis par le portail :

#include <WiFiClientSecure.h>
WiFiClientSecure net;          // remplace WiFiClient
// dans setup() : charger le certificat ISRG Root X1 pour Let's Encrypt
// net.setCACert(ISRG_ROOT_X1_PEM);
mqtt.setServer("broker.connectmq.com", 8883);
// se connecter avec : mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", statusTopic, 1, true, "offline");

L'explorateur de topics en direct du portail ConnectMQ affiche alors chaque message à son arrivée, et des règles d'alerte à seuil peuvent vous prévenir en cas de gel ou de vent fort sans code supplémentaire. D'autres idées orientées exploitation agricole se trouvent dans l'article sur l'IoT pour l'agriculture connectée et dans le guide de l'automatisation IoT, plus général.

Publiez vos données météo sur ConnectMQ

ConnectMQ est un broker MQTT managé avec TLS, un accès WebSocket pour les tableaux de bord dans le navigateur et un client intégré au navigateur pour des vérifications rapides. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, et faites passer votre station de la zone de test publique à des identifiants privés en quelques minutes.

Questions fréquentes

À quelle fréquence une station météo DIY doit-elle publier en MQTT ?

Une fois par minute est un bon choix par défaut pour des valeurs moyennées. Le trafic reste minime tout en capturant les rafales, à condition d'envoyer la vitesse de vent maximale sur 3 secondes à côté de la moyenne.

Pourquoi mes relevés de température sont-ils trop élevés ?

Presque toujours à cause du rayonnement solaire ou de la chaleur dégagée par l'ESP32 lui-même. Placez le BME280 dans un abri anti-rayonnement ventilé, loin du microcontrôleur et du régulateur de tension.

Puis-je utiliser un BME680 ou un SHT31 à la place du BME280 ?

Oui. Changez la bibliothèque et les fonctions de lecture ; la partie MQTT reste identique tant que vous conservez les noms des champs JSON.

Le broker conserve-t-il l'historique de mes données météo ?

Non. Les brokers MQTT distribuent les messages en direct et la dernière valeur retenue. Conservez l'historique avec un abonné comme le logger Python ci-dessus, Telegraf avec une base de données de séries temporelles, ou Node-RED.

Articles liés

Guide d'installation de Zigbee2MQTT : du coordinateur au tableau de bord

Installez Zigbee2MQTT avec Docker, configurez le coordinateur et la connexion MQTT, appairez et renommez les appareils, pilotez-les via MQTT et affichez leurs états sur un tableau de bord, en testant d'abord sur le serveur MQTT public gratuit.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.