Eine automatische Bewässerung gießt nur dann, wenn der Boden tatsächlich trocken ist und kein Regen in Sicht ist, statt per Zeitschaltuhr bei jedem Schauer weiterzulaufen. In diesem Tutorial verbinden Sie Bodenfeuchtewerte, eine kostenlose Wetterprognose und per MQTT gesteuerte Magnetventile: Ein Python-Controller trifft die Entscheidungen, und ESP32-Ventilknoten führen sie mit harten Sicherheitsgrenzen aus, sodass eine verlorene Nachricht das Wasser niemals laufen lassen kann.
Wie das System zusammenspielt
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)
Jeder Teil hat genau eine Aufgabe. Sensoren melden die Feuchte. Der Controller kombiniert Feuchte, Prognose und ein Zeitfenster. Ventilknoten öffnen für eine angeforderte Dauer und schließen sich selbst. Da alles über den Broker läuft, können Sie Zonen hinzufügen, den Controller gegen Node-RED tauschen oder alles vom Smartphone aus beobachten, ohne die anderen Teile zu ändern. Der Leitfaden zu Smart Farming IoT behandelt das übergreifende Design, und die Feuchtewerte in diesem Artikel stammen von dem Knoten aus dem Tutorial Bodenfeuchtesensor mit MQTT.
Topics und Payloads
Die Beispiele verwenden den öffentlichen MQTT-Broker. Ersetzen Sie i8r3c6 durch Ihre eigene Zufallszeichenfolge; der öffentliche MQTT-Broker wird geteilt, ist für jeden lesbar und nur für Tests und Prototypen gedacht.
| Topic | Payload | Retained |
|---|---|---|
connectmq/demo/i8r3c6/irrigation/zone1/moisture |
{"moisture_pct":22.5} |
ja |
connectmq/demo/i8r3c6/irrigation/zone1/set |
{"state":"open","duration_s":600} oder {"state":"close"} |
nein |
connectmq/demo/i8r3c6/irrigation/zone1/state |
{"state":"open","remaining_s":540} |
ja |
connectmq/demo/i8r3c6/irrigation/valves/status |
online / offline (Last Will) |
ja |
connectmq/demo/i8r3c6/irrigation/log |
{"zone":1,"action":"skip","reason":"rain_forecast"} |
nein |
Der Befehl enthält immer eine Dauer. Ein einfaches "open" mit einem späteren separaten "close" ist fragil: Geht die Schließnachricht verloren, wird die Zone überflutet. Mit einer Dauer schließt der Ventilknoten von selbst. Dasselbe Muster, ein Befehl mit Laufzeit und eine Selbstabschaltung am Aktor, bewährt sich auch bei Ventilen und Pumpen in Industrieanlagen, etwa in Kühlwasserkreisläufen.
Entscheidungsregeln für eine automatische Bewässerung
Schreiben Sie die Regeln auf, bevor Sie Code schreiben. Ein praxistauglicher Ausgangssatz:
- Nur innerhalb erlaubter Zeitfenster gießen (üblich ist der frühe Morgen, weil Verdunstung und Wind dann am geringsten sind).
- Eine Zone nur gießen, wenn ihre Feuchte unter dem Schwellwert der Zone liegt.
- Aussetzen, wenn die Prognose für heute oder morgen einen Regengrenzwert überschreitet.
- Dieselbe Zone nie zweimal innerhalb eines Mindestabstands gießen.
- Jeden Lauf auf eine Maximaldauer begrenzen, durchgesetzt sowohl vom Controller als auch vom Ventilknoten.
- Sind die Feuchtedaten älter als einige Stunden, nicht automatisch gießen, sondern stattdessen einen Alarm auslösen.
Schwellwerte hängen von Boden und Kultur ab. Beginnen Sie mit einem vorsichtigen Schwellwert, beobachten Sie, wie schnell sich die Feuchte nach einem Lauf erholt, und passen Sie eher die Dauer als die Häufigkeit an.
Der Python-Controller
Der Controller nutzt paho-mqtt 2.x und die kostenlose Prognose-API von Open-Meteo, die keinen Schlüssel benötigt. Installation mit 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 = 48.78, 9.18 # Ihr Standort (hier Stuttgart)
ZONES = {
1: {"threshold": 28.0, "duration_s": 600},
2: {"threshold": 30.0, "duration_s": 420},
}
WINDOWS = [(5, 8), (19, 21)] # lokale Stunden, Start inklusive, Ende exklusive
RAIN_LIMIT_MM = 3.0
MIN_INTERVAL_S = 12 * 3600
STALE_S = 3 * 3600
moisture = {} # Zone -> (Wert, Zeitstempel)
last_run = {} # Zone -> Zeitstempel
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() # den Alarm nicht in jedem Zyklus wiederholen
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)
In dieser einfachen Version werden die Zonen nur deshalb nacheinander gegossen, weil sie der Reihe nach ausgewertet werden. Kann Ihre Wasserversorgung nicht zwei Zonen gleichzeitig speisen, ergänzen Sie anhand der retained state-Nachrichten eine Prüfung, dass keine andere Zone gerade offen ist, bevor Sie den nächsten Befehl senden.
Der ESP32-Ventilknoten
Der Ventilknoten abonniert seine Zonen, erzwingt eine harte Maximaldauer, schließt alles, wenn die Broker-Verbindung länger als eine Minute verloren ist, und meldet seinen Zustand. Er nutzt PubSubClient und 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; // absolute Obergrenze
const uint32_t OFFLINE_CLOSE_MS = 60000;
uint32_t closeAt[NUM_ZONES] = {0, 0}; // millis()-Frist, 0 = geschlossen
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); // beim Booten geschlossen
}
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);
}
}
Steuern Sie die Magnetventile über ein Relaismodul oder eine MOSFET-Platine an, die für Spannung und Strom des Ventils ausgelegt ist, mit einer Freilaufdiode bei Gleichstromventilen. Die meisten Gartenventile arbeiten mit 24 V AC; bistabile Ventile (Latching), wie sie in Batteriesystemen verwendet werden, brauchen einen Impulstreiber statt eines dauerhaft angezogenen Relais. Die Relaisverdrahtung behandelt der Artikel ESP32-Relais mit MQTT.
Feuchtedaten an den Controller liefern
Der Controller erwartet pro Zone eine retained Nachricht auf zone<n>/moisture. Am einfachsten setzen Sie das Telemetrie-Topic des Bodenknotens direkt auf diesen Pfad. Veröffentlichen Ihre Sensoren bereits woanders, zum Beispiel LoRaWAN-Sonden, die über eine Bridge vom Netzwerkserver eintreffen, kann die Bridge mit wenigen Zeilen das Feuchtefeld erneut auf das Zonen-Topic veröffentlichen. Halten Sie die Zuordnung von Sensor zu Zone an einer Stelle, idealerweise in der Konfiguration des Controllers, damit das Versetzen einer Sonde kein Neuflashen erfordert.
Hat eine Zone zwei Sonden, mitteln Sie sie oder gießen Sie nach der trockeneren, protokollieren Sie aber beide. Eine einzelne defekte Sonde, die null meldet, darf nicht allein eine Bewässerung auslösen können; die Regel für veraltete Daten deckt verstummte Sonden ab, und ein Plausibilitätsbereich (Werte unter 1 oder über 99 Prozent ignorieren) deckt defekte ab.
Schwellwerte und Laufzeiten wählen
Der Schwellwert bestimmt, wann gegossen wird, die Dauer, wie viel. Stimmen Sie sie in dieser Reihenfolge ab. Beobachten Sie eine Woche lang die Feuchtekurve ohne automatische Bewässerung, notieren Sie den Wert, bei dem die Pflanzen gestresst wirken, und setzen Sie den Schwellwert ein paar Punkte darüber. Lassen Sie dann eine Zone manuell für eine feste Zeit laufen und beobachten Sie, wie weit und wie schnell die Feuchte steigt. Springt der Wert hoch und fällt innerhalb von Stunden wieder ab, versickert das Wasser an den Wurzeln vorbei; kürzere, häufigere Läufe sind dann besser. Bewegt er sich kaum, ist der Lauf zu kurz oder die Sonde sitzt außerhalb des Tropfbereichs.
Protokollieren Sie jede Änderung im Log-Topic. Über eine Saison wird das an den Broker veröffentlichte Protokoll zum Beleg dafür, welche Einstellungen funktionieren, und eine Live-Ansicht wie der ConnectMQ-Topic-Explorer macht die Überlegungen des Controllers in Echtzeit sichtbar.
Testen ohne Wasser
Bevor Sie einen Schlauch anschließen, testen Sie mit LEDs an den Ventilpins:
# einen trockenen Messwert für Zone 1 vortäuschen
mosquitto_pub -h broker.connectmq.com -p 1883 -i t-i8r3c6 -r \
-t 'connectmq/demo/i8r3c6/irrigation/zone1/moisture' -m '{"moisture_pct":19}'
# Zone 1 manuell für 20 Sekunden öffnen
mosquitto_pub -h broker.connectmq.com -p 1883 -i t-i8r3c6 \
-t 'connectmq/demo/i8r3c6/irrigation/zone1/set' -m '{"state":"open","duration_s":20}'
# alles beobachten
mosquitto_sub -h broker.connectmq.com -p 1883 -i w-i8r3c6 \
-t 'connectmq/demo/i8r3c6/irrigation/#' -v
Ziehen Sie dann während eines Laufs die WLAN-Antenne des ESP32 ab oder schalten Sie den Router aus und prüfen Sie, ob das Ventil innerhalb einer Minute schließt. Dieser eine Test ist mehr wert als jede noch so gründliche Code-Review.
Checkliste zur Absicherung
- Das Ventil schließt beim Booten, nach Ablauf der Dauer, bei Verlust des Brokers und bei jedem fehlerhaften Befehl.
- Ein Durchflussmesser an der Hauptleitung bestätigt, dass tatsächlich Wasser geflossen ist; veröffentlichen Sie die Liter pro Lauf und alarmieren Sie bei Durchfluss, während alle Ventile geschlossen sind (Rohrbruch).
- Befehle sind nie retained, Zustände immer.
- Jeder Controller und jeder Ventilknoten hat eigene Zugangsdaten auf einem privaten Broker mit TLS.
- Es gibt eine manuelle Übersteuerung: einen physischen Schalter oder eine Dashboard-Schaltfläche, die
{"state":"close"}veröffentlicht.
Einrichtung für den Produktivbetrieb
Ersetzen Sie den öffentlichen MQTT-Broker durch einen privaten Broker, bevor Sie echte Ventile anschließen, denn jeder könnte auf ein öffentliches set-Topic veröffentlichen. Auf dem ESP32 verwenden Sie WiFiClientSecure mit der CA ISRG Root X1 auf Port 8883 und mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...); in Python rufen Sie client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD") und client.tls_set() auf und verbinden sich mit Port 8883. Ein verwalteter MQTT-Broker bleibt verfügbar, auch wenn der Hof-PC neu startet, und ein Cloud-MQTT-Broker ist für Feldknoten mit Mobilfunkanbindung ohne VPN erreichbar. Für die Klimaregelung im Gewächshaus neben der Bewässerung siehe Gewächshaus-Automatisierung, und weitere Muster finden Sie im Leitfaden zur IoT-Automatisierung.
Smarter bewässern mit ConnectMQ
ConnectMQ gibt Ihrer Bewässerung einen privaten MQTT-Broker mit TLS, einen Live-Topic-Explorer zum Beobachten jedes Befehls und jeder Bestätigung sowie Schwellwert-Alarmregeln für trockene Zonen oder unerwarteten Durchfluss. Erstellen Sie unter app.connectmq.com einen kostenlosen Serverless-Broker, ohne Kreditkarte, und ziehen Sie Controller und Ventile darauf um.
Häufig gestellte Fragen
Was passiert, wenn die MQTT-Verbindung abbricht, während ein Ventil offen ist?
Der Ventilknoten schließt nach 60 Sekunden ohne Broker-Verbindung jedes offene Ventil, und jeder Lauf hat eine Obergrenze für die Dauer, sodass Wasser nicht unbegrenzt laufen kann.
Kann ich statt des Python-Controllers Home Assistant oder Node-RED verwenden?
Ja. Alles, was die Feuchte-Topics abonniert und denselben JSON-Befehl veröffentlicht, funktioniert; dem Ventilknoten ist egal, wer ihn gesendet hat.
Soll die Prognose oder der Bodensensor entscheiden?
Nutzen Sie beides. Der Bodensensor sagt, ob jetzt Wasser gebraucht wird, die Prognose, ob die Natur es bald liefert. Setzen Sie die Bewässerung nur aus, wenn beide übereinstimmen.
Wie viele Zonen kann ein ESP32 steuern?
So viele, wie Sie freie Ausgangspins und Relaiskanäle haben, typischerweise vier bis acht. Lassen Sie die Zonen nacheinander laufen, wenn Ihr Wasserdruck nicht mehrere gleichzeitig versorgen kann.



