Un système d'arrosage automatique n'arrose que lorsque le sol est réellement sec et qu'aucune pluie n'est attendue, au lieu de fonctionner sur minuterie à chaque averse. Dans ce tutoriel, vous allez relier des mesures d'humidité du sol, des prévisions météo gratuites et des électrovannes pilotées par MQTT : un contrôleur Python prend les décisions, et des nœuds de vannes ESP32 les exécutent avec des limites de sécurité strictes, de sorte qu'un message perdu ne puisse jamais laisser l'eau couler.
Comment s'articule le système
Soil sensors ---- telemetry ---->+
|
Weather API --> Python controller (decides) <---- MQTT broker ----> Dashboard
| ^
zone/<n>/set ---+ |
v |
ESP32 valve node --- zone/<n>/state
(relay -> 24 V AC or 12 V DC solenoid)
Chaque partie a une seule tâche. Les capteurs signalent l'humidité. Le contrôleur combine l'humidité, les prévisions et une plage horaire. Les nœuds de vannes s'ouvrent pour la durée demandée et se referment d'eux-mêmes. Comme tout passe par le broker, vous pouvez ajouter des zones, remplacer le contrôleur par Node-RED ou par la box domotique de votre maison connectée, ou tout surveiller depuis un téléphone sans modifier les autres parties. Le guide agriculture intelligente IoT couvre la conception d'ensemble, et les mesures d'humidité de cet article proviennent du nœud construit dans le tutoriel capteur d'humidité du sol MQTT.
Topics et payloads
Les exemples utilisent le serveur MQTT public gratuit, réservé aux tests. Remplacez i8r3c6 par votre propre chaîne aléatoire ; l'espace public est partagé et lisible par tous.
| Topic | Payload | Retained |
|---|---|---|
connectmq/demo/i8r3c6/irrigation/zone1/moisture |
{"moisture_pct":22.5} |
oui |
connectmq/demo/i8r3c6/irrigation/zone1/set |
{"state":"open","duration_s":600} ou {"state":"close"} |
non |
connectmq/demo/i8r3c6/irrigation/zone1/state |
{"state":"open","remaining_s":540} |
oui |
connectmq/demo/i8r3c6/irrigation/valves/status |
online / offline (last will) |
oui |
connectmq/demo/i8r3c6/irrigation/log |
{"zone":1,"action":"skip","reason":"rain_forecast"} |
non |
La commande porte toujours une durée. Un simple « open » suivi plus tard d'un « close » séparé est fragile : si le message de fermeture se perd, la zone est inondée. Avec une durée, le nœud de vanne se referme de lui-même.
Règles de décision d'un système d'arrosage automatique
Écrivez les règles avant d'écrire le code. Un premier ensemble pratique :
- N'arroser que dans les plages autorisées (le petit matin est l'usage, car l'évaporation et le vent y sont au plus bas).
- N'arroser une zone que si son humidité est inférieure au seuil de la zone.
- Ne pas arroser si les prévisions pour aujourd'hui ou demain dépassent une limite de pluie.
- Ne jamais arroser deux fois la même zone en deçà d'un intervalle minimal.
- Plafonner chaque cycle à une durée maximale, appliquée à la fois par le contrôleur et par le nœud de vanne.
- Si les données d'humidité datent de plus de quelques heures, ne pas arroser automatiquement ; déclencher plutôt une alerte.
Les seuils dépendent du sol et de la culture. Commencez avec un seuil prudent, observez la vitesse à laquelle l'humidité remonte après un cycle, et ajustez la durée plutôt que la fréquence.
Le contrôleur Python
Le contrôleur utilise paho-mqtt 2.x et l'API de prévisions gratuite Open-Meteo, qui ne nécessite pas de clé. Installez avec pip install paho-mqtt.
import json
import time
import urllib.request
from datetime import datetime
import paho.mqtt.client as mqtt
BROKER = "broker.connectmq.com"
BASE = "connectmq/demo/i8r3c6/irrigation"
LAT, LON = 41.01, 28.97 # votre position
ZONES = {
1: {"threshold": 28.0, "duration_s": 600},
2: {"threshold": 30.0, "duration_s": 420},
}
WINDOWS = [(5, 8), (19, 21)] # heures locales, début inclus, fin exclue
RAIN_LIMIT_MM = 3.0
MIN_INTERVAL_S = 12 * 3600
STALE_S = 3 * 3600
moisture = {} # zone -> (valeur, horodatage)
last_run = {} # zone -> horodatage
forecast_cache = {"ts": 0, "rain": None}
def get_rain_forecast_mm():
if time.time() - forecast_cache["ts"] < 3600:
return forecast_cache["rain"]
url = (f"https://api.open-meteo.com/v1/forecast?latitude={LAT}&longitude={LON}"
"&daily=precipitation_sum&forecast_days=2&timezone=auto")
try:
with urllib.request.urlopen(url, timeout=10) as r:
daily = json.load(r)["daily"]["precipitation_sum"]
rain = max(v or 0.0 for v in daily)
except Exception as exc:
print("forecast unavailable:", exc)
rain = None
forecast_cache.update(ts=time.time(), rain=rain)
return rain
def log(client, **fields):
client.publish(f"{BASE}/log", json.dumps(fields), qos=1)
print(fields)
def on_connect(client, userdata, flags, reason_code, properties):
print("connected:", reason_code)
client.subscribe(f"{BASE}/+/moisture", qos=1)
client.subscribe(f"{BASE}/+/state", qos=1)
def on_message(client, userdata, msg):
parts = msg.topic.split("/")
zone_name, kind = parts[-2], parts[-1]
if not zone_name.startswith("zone"):
return
zone = int(zone_name[4:])
data = json.loads(msg.payload)
if kind == "moisture":
moisture[zone] = (float(data["moisture_pct"]), time.time())
elif kind == "state":
print(f"zone {zone} state:", data)
def in_window(now):
return any(start <= now.hour < end for start, end in WINDOWS)
def evaluate(client):
now = datetime.now()
if not in_window(now):
return
rain = get_rain_forecast_mm()
for zone, cfg in ZONES.items():
if time.time() - last_run.get(zone, 0) < MIN_INTERVAL_S:
continue
value, ts = moisture.get(zone, (None, 0))
if value is None or time.time() - ts > STALE_S:
log(client, zone=zone, action="alert", reason="stale_moisture")
last_run[zone] = time.time() # ne pas répéter l'alerte à chaque cycle
continue
if value >= cfg["threshold"]:
continue
if rain is not None and rain >= RAIN_LIMIT_MM:
log(client, zone=zone, action="skip", reason="rain_forecast", rain_mm=rain)
last_run[zone] = time.time()
continue
cmd = {"state": "open", "duration_s": cfg["duration_s"]}
client.publish(f"{BASE}/zone{zone}/set", json.dumps(cmd), qos=1)
last_run[zone] = time.time()
log(client, zone=zone, action="water", moisture=value, duration_s=cfg["duration_s"])
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="irrigation-ctl-i8r3c6")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, 1883, keepalive=60)
client.loop_start()
while True:
evaluate(client)
time.sleep(300)
Dans cette version simple, les zones ne sont arrosées l'une après l'autre que parce qu'elles sont évaluées en séquence ; si votre alimentation en eau ne peut pas desservir deux zones à la fois, ajoutez une vérification qu'aucune autre zone n'est ouverte avant d'envoyer la commande suivante, à l'aide des messages state retained.
Le nœud de vannes ESP32
Le nœud de vannes s'abonne à ses zones, impose une durée maximale stricte, ferme tout si la connexion au broker est perdue plus d'une minute, et rend compte de son état. Il utilise PubSubClient et ArduinoJson 7.
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const char* CLIENT_ID = "valves-i8r3c6";
const char* BASE = "connectmq/demo/i8r3c6/irrigation";
const int NUM_ZONES = 2;
const int VALVE_PIN[NUM_ZONES] = {26, 27};
const uint32_t MAX_RUN_S = 1800; // plafond absolu
const uint32_t OFFLINE_CLOSE_MS = 60000;
uint32_t closeAt[NUM_ZONES] = {0, 0}; // échéance millis(), 0 = fermée
uint32_t lastConnected = 0;
uint32_t lastReport = 0;
WiFiClient net;
PubSubClient mqtt(net);
void publishState(int z) {
char topic[96], buf[64];
snprintf(topic, sizeof(topic), "%s/zone%d/state", BASE, z + 1);
uint32_t rem = closeAt[z] ? (closeAt[z] - millis()) / 1000 : 0;
snprintf(buf, sizeof(buf), "{\"state\":\"%s\",\"remaining_s\":%lu}",
closeAt[z] ? "open" : "closed", (unsigned long)rem);
mqtt.publish(topic, buf, true);
}
void setValve(int z, bool open, uint32_t seconds) {
if (open) {
seconds = min(seconds, MAX_RUN_S);
closeAt[z] = millis() + seconds * 1000UL;
if (closeAt[z] == 0) closeAt[z] = 1;
} else {
closeAt[z] = 0;
}
digitalWrite(VALVE_PIN[z], open ? HIGH : LOW);
if (mqtt.connected()) publishState(z);
}
void onMessage(char* topic, byte* payload, unsigned int len) {
int z = -1;
for (int i = 0; i < NUM_ZONES; i++) {
char t[96];
snprintf(t, sizeof(t), "%s/zone%d/set", BASE, i + 1);
if (strcmp(topic, t) == 0) z = i;
}
if (z < 0) return;
JsonDocument doc;
if (deserializeJson(doc, payload, len)) return;
const char* state = doc["state"] | "close";
uint32_t dur = doc["duration_s"] | 0;
setValve(z, strcmp(state, "open") == 0 && dur > 0, dur);
}
void connectMqtt() {
char willTopic[96];
snprintf(willTopic, sizeof(willTopic), "%s/valves/status", BASE);
if (mqtt.connect(CLIENT_ID, willTopic, 1, true, "offline")) {
mqtt.publish(willTopic, "online", true);
for (int i = 0; i < NUM_ZONES; i++) {
char t[96];
snprintf(t, sizeof(t), "%s/zone%d/set", BASE, i + 1);
mqtt.subscribe(t, 1);
publishState(i);
}
}
}
void setup() {
for (int i = 0; i < NUM_ZONES; i++) {
pinMode(VALVE_PIN[i], OUTPUT);
digitalWrite(VALVE_PIN[i], LOW); // fermée au démarrage
}
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
mqtt.setServer(MQTT_HOST, 1883);
mqtt.setCallback(onMessage);
}
void loop() {
if (WiFi.status() == WL_CONNECTED && !mqtt.connected()) {
static uint32_t lastTry = 0;
if (millis() - lastTry > 5000) { lastTry = millis(); connectMqtt(); }
}
if (mqtt.connected()) lastConnected = millis();
mqtt.loop();
for (int i = 0; i < NUM_ZONES; i++) {
if (closeAt[i] && (int32_t)(millis() - closeAt[i]) >= 0) setValve(i, false, 0);
if (closeAt[i] && millis() - lastConnected > OFFLINE_CLOSE_MS) setValve(i, false, 0);
}
if (mqtt.connected() && millis() - lastReport > 30000) {
lastReport = millis();
for (int i = 0; i < NUM_ZONES; i++) if (closeAt[i]) publishState(i);
}
}
Commandez les électrovannes via un module relais ou une carte MOSFET dimensionnés pour la tension et le courant de la vanne, avec une diode de roue libre sur les électrovannes en courant continu. La plupart des vannes de jardin fonctionnent en 24 V AC ; les vannes à impulsion (bistables) utilisées sur les systèmes à batterie nécessitent un driver à impulsion plutôt qu'un relais maintenu. L'article relais ESP32 MQTT couvre le câblage des relais.
Alimenter le contrôleur en données d'humidité
Le contrôleur attend un message retained par zone sur zone<n>/moisture. Le moyen le plus simple d'y parvenir est de régler directement le topic de télémétrie du nœud de sol sur ce chemin. Si vos capteurs publient déjà ailleurs, par exemple des sondes LoRaWAN arrivant via un pont vers le serveur de réseau, quelques lignes dans le pont suffisent à republier le champ d'humidité sur le topic de la zone. Conservez la correspondance capteur-zone à un seul endroit, idéalement dans la configuration du contrôleur, afin que déplacer une sonde n'exige aucun reflashage.
Lorsqu'une zone a deux sondes, faites la moyenne ou arrosez selon la plus sèche, mais enregistrez les deux. Une seule sonde défectueuse affichant zéro ne doit pas pouvoir déclencher l'arrosage à elle seule ; la règle des données périmées couvre les sondes muettes, et une plage de cohérence (ignorer les valeurs inférieures à 1 % ou supérieures à 99 %) couvre les sondes cassées.
Choisir les seuils et les durées d'arrosage
Le seuil indique quand arroser ; la durée indique combien. Réglez-les dans cet ordre. Observez la courbe d'humidité pendant une semaine sans arrosage automatique, en notant la valeur à laquelle les plantes commencent à montrer des signes de stress, et fixez le seuil quelques points au-dessus. Ensuite, faites fonctionner une zone manuellement pendant une durée fixe et regardez jusqu'où et à quelle vitesse l'humidité monte. Si la mesure bondit puis retombe en quelques heures, l'eau s'écoule au-delà des racines ; des cycles plus courts et plus fréquents sont préférables. Si elle bouge à peine, le cycle est trop court ou la sonde se trouve hors de la zone humectée par le goutte-à-goutte.
Consignez chaque modification dans le topic de journal. Au fil d'une saison, le journal publié sur le broker devient la preuve des réglages qui fonctionnent, et une vue en direct comme l'explorateur de topics de ConnectMQ permet de suivre facilement le raisonnement du contrôleur au moment où il se produit.
Tester sans eau
Avant de brancher un tuyau, testez avec des LED sur les broches des vannes :
# simuler une mesure sèche pour la zone 1
mosquitto_pub -h broker.connectmq.com -p 1883 -i t-i8r3c6 -r \
-t 'connectmq/demo/i8r3c6/irrigation/zone1/moisture' -m '{"moisture_pct":19}'
# ouvrir manuellement la zone 1 pendant 20 secondes
mosquitto_pub -h broker.connectmq.com -p 1883 -i t-i8r3c6 \
-t 'connectmq/demo/i8r3c6/irrigation/zone1/set' -m '{"state":"open","duration_s":20}'
# tout observer
mosquitto_sub -h broker.connectmq.com -p 1883 -i w-i8r3c6 \
-t 'connectmq/demo/i8r3c6/irrigation/#' -v
Ensuite, retirez l'antenne Wi-Fi de l'ESP32 ou éteignez le routeur pendant un cycle et vérifiez que la vanne se ferme en moins d'une minute. Ce seul test vaut plus que n'importe quelle relecture de code.
Liste de contrôle de durcissement
- La vanne se ferme au démarrage, à l'expiration de la durée, à la perte du broker et à toute commande mal formée.
- Un débitmètre sur la conduite principale confirme que l'eau a réellement circulé ; publiez les litres par cycle et déclenchez une alerte en cas de débit alors que toutes les vannes sont fermées (rupture de canalisation).
- Les commandes ne sont jamais retained, les états le sont toujours.
- Chaque contrôleur et chaque nœud de vannes dispose de ses propres identifiants sur un broker privé avec TLS.
- Une commande manuelle de secours existe : un interrupteur physique ou un bouton de tableau de bord qui publie
{"state":"close"}.
Configuration de production
Remplacez le serveur MQTT public gratuit par un broker privé avant de raccorder de vraies vannes, car n'importe qui pourrait publier sur un topic set public. Sur l'ESP32, utilisez WiFiClientSecure avec la CA ISRG Root X1 sur le port 8883 et mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...) ; en Python, appelez client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD") et client.tls_set(), puis connectez-vous au port 8883. Un broker MQTT managé garde le broker disponible même quand le PC de l'exploitation redémarre, et un broker MQTT cloud permet aux nœuds de terrain sur liaison cellulaire de le joindre sans VPN. Pour la régulation climatique d'une serre en complément de l'arrosage, consultez automatisation de serre, et pour d'autres modèles, visitez le guide de l'automatisation IoT.
Arrosez plus intelligemment avec ConnectMQ
ConnectMQ offre à votre système d'irrigation un broker MQTT privé avec TLS, un explorateur de topics en direct pour suivre chaque commande et chaque confirmation, et des règles d'alerte à seuil pour les zones sèches ou les débits inattendus. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, et migrez-y votre contrôleur et vos vannes.
Questions fréquentes
Que se passe-t-il si la connexion MQTT est coupée pendant qu'une vanne est ouverte ?
Le nœud de vannes ferme toutes les vannes ouvertes après 60 secondes sans connexion au broker, et chaque cycle a une durée plafonnée : l'eau ne peut donc pas couler indéfiniment.
Puis-je utiliser Home Assistant ou Node-RED à la place du contrôleur Python ?
Oui. Tout ce qui s'abonne aux topics d'humidité et publie la même commande JSON fonctionne ; le nœud de vannes ne se soucie pas de l'expéditeur. Jeedom et Domoticz savent aussi communiquer en MQTT.
Est-ce aux prévisions ou au capteur de sol de décider ?
Utilisez les deux. Le capteur de sol dit si l'eau est nécessaire maintenant, les prévisions disent si la nature va bientôt la fournir. N'annulez l'arrosage que lorsque les deux concordent.
Combien de zones un ESP32 peut-il piloter ?
Autant que vous avez de broches de sortie libres et de canaux de relais, généralement de quatre à huit. Faites fonctionner les zones une par une si votre pression d'eau ne permet pas d'en alimenter plusieurs à la fois.



