Automatisches Bewässerungssystem mit dem ESP32

Wenn man Zimmerpflanzen zuhause pflegt und mal einige Zeit unterwegs ist, so möchte man die Pflanzen natürlich besonders in den warmen Monaten nicht ohne Wasser auskommen lassen. Oft können Freunde, Nachbarn oder Familienmitglieder das Gießen in dieser Zeit übernehmen. Wenn aber diese Möglichkeit nicht in Frage kommt, dann bleibt noch eine weitere Option: ein automatisches Bewässerungssystem.

Im Folgenden beschreibe ich ein selbst entworfenes System (genannt: "Automatic Plant Watering System APWS") basierend auf dem ESP32, dass modular eine oder mehrere Pflanzen überwacht und gießt.

Planung

Im Gegensatz zu den Anleitungen zu ähnlichen Systemen, die man im WWW finden kann, so ist mein Ansatz ein modulares System zu bauen, welches genau eine Pflanze überwachen und gießen kann. Will man eine zweite oder noch mehr Pflanzen hinzufügen, so muss die Hardware für dieses Modul vervielfacht werden. Dies hat den Vorteil, dass pro Pflanze zwar mehr Hardware angeschafft werden muss. Der Vorteil des APWS ist jedoch, dass jedes Modul autark funktioniert und damit die Module auch eine größere räumliche Trennung haben können. Gemeinsam hat das APWS eine Server-Komponente, mit man als Mensch alle zugeschalteten Pflanzen überwachen und sogar manuell eingreifen kann...und dies über das WWW.

Aufbau und Funktionsweise des APWS

Die folgende Übersicht zeigt, wie ein APWS-Modul funktioniert und mit der zugehörigen Server-Komponente kommuniziert:

Übersicht der Funktionsweise des automatischen Bewässerungssystems APWS
Abb.: Übersicht der Funktionsweise des automatischen Bewässerungssystems APWS

Mit einem APWS-Modul soll eine Pflanze bzw. ein Pflanztopf mit Wasser versorgt werden. Natürlich kann es auch ein Blumenkasten oder sogar ein ganzes Beet sein, dafür müssen nur die Schläuche evtl. verzweigt werden. Außerdem sollte der Sensor dann an der richtigen Stelle platziert werden.

Verwendete Bauteile

Bauteile pro APWS-Modul

Weitere Bauteile

Schaltplan

Der folgende Schaltplan zeigt genau die Verdrahtung der einzelnen Komponenten für ein APWS-Modul (Bewässerung einer Pflanze bzw. eines Topfes). Wichtig: Die Ausgangsspannung des Abwärtswandlers muss vor dem Anschließen an den ESP32 auf die 5V eingestellt werden!

Schaltplan des automatischen Bewässerungssystems APWS
Abb.: Schaltplan des automatischen Bewässerungssystems APWS

Komponenten

Sensor

Es gibt etliche weit verbreitete Sensoren zur Messung der Bodenfeuchtigkeit oder man kann sich auch einen Selbstbau-Sensor verwenden. Doch diese Möglichkeiten haben meist das Problem, dass die blanken Metallsonden im Erdreich korrodieren und damit schnell unbrauchbar bzw. unzuverlässig werden. Daher habe ich mich für einen sog. kapazitativer Bodenfeuchtesensor entschieden, der keine offenen Metallteile im Erdreich benötigt und somit (hoffentlich) deutlich länger hält.

Kapazitativer Bodenfeuchtesensor
Abb.: Kapazitativer Bodenfeuchtesensor

Pumpe

Nach dem Lesen einiger DIY-Anleitungen in WWW war mein erster Gedanke eine kleine Gleichstrom-Pumpe für 12V zu verwenden. Solche Pumpen werden gerne als Tauchpumpen angeboten und sind recht günstig. Jedoch sind Tauchpumpen nicht unbedingt ideal, da sie nicht trockenlaufen sollten (z.B. wegen Überhitzung) und außerdem sind die meisten von ihnen nicht selbstentlüftend. Darüber hinaus produzieren diese Pumpen einen vergleichsweise hohen Wasserdruck, was zur Folge haben könnte, dass bei einem Bewässerungsvorgang Wasser über den Topf auf den Teppich oder das Mobiliar verspritzt werden könnte.

12V Peristaltik-Pumpe

Daher entschied ich mit für eine sog. Schlauchpumpe bzw. Peristaltik-Pumpe mit 12V. Diese hat zwar einen recht geringen Durchsatz an Wassermenge (hier max. 150ml/min.), jedoch ist diese zur Bewässerung von Zimmerpflanzen ausreichend und kann sehr gut dosiert werden. Außerdem hat der hier verwendete Motor eine Stromaufnahme von nur 250-300 mA.
Wichtig ist, den passenden Schlauch für die Pumpe zu kaufen (hier: Innendurchmesser 3mm; Außendurchmesser: 5mm). Als Material würde ich immer Silikon empfehlen, da dies Kälte, Hitze, Trockenheit, Feuchtigkeit und Korrosion nicht anhaben kann und im Gegensatz zu PVC-Schläuchen nicht verhärtet und weiterhin flexibel bleibt. Allerdings ist der Preis ein wenig höher als bei PVC.

Wassertank

Eine leere Kunststoff-Flasche wird als Wasservorrat für den ersten Prototypen verwendet. Zur späteren Bewässerung einer echten Zimmerpflanze wird vermutlich ein 5-Liter-Kanister zum Einsatz kommen, damit auch genug Wasser vorrätig ist. Um den Schlauch gut zu fixieren, habe ich in den Deckel ein Loch von 5mm Durchmesser gebohrt, welches dann den Schlauch aufnimmt. Ein zweites Loch soll den Luftausgleich gewährleisten.

Leere Kunststoff-Flasche als Wasservorrat
Abb.: Leere Kunststoff-Flasche als Wasservorrat

Schlauch-Halterungen

Damit die Silikonschläuche gut am bzw. im Pflanztopf halten und kein Wasser auf den Teppichboden vergossen wird, habe ich zwei verschiedene Sorten von Halterungen in OpenSCAD entworfen, die man mit einem 3D-Drucker selbst fertigen kann.

Steck-Halterung für Silikonschlauch

Steck-Halterung für Silikonschlauch

Diese Halterung führt den Silikonschlauch auf der Oberseite und kann dann direkt in die Erde des Pflanztopfes gesteckt werden.

tube_mount1.scad

Clip-Halterung für Silikonschlauch

Clip-Halterung für Silikonschlauch

Die zweite Variante ist eine Clip-Halterung, die man am Übertopf festklemmen kann. Für diese Halterung wird zusätzlich noch ein 2D-Pfad im SVG-Format benötigt, damit man die Halterung selbst erstellen kann.

clip_half.svg tube_mount2.scad
Abb.: Mit dem Ender-3 Pro gefertigte Halterungen mit PLA-Filament im Marmor-Look.
Abb.: Silikonschlauch wird durch eine Halterung geführt und passt gut, so dass der Schlauch nicht herausrutschen kann.
Abb.: Beide Halterungen am Pflanztopf bzw. in der Erde, die den Silikonschlauch so führen, dass kein Wasser verschüttet werden kann.

Abwärtswandler

Hier ist es nur wichtig, dass die Eingangsspannung 12V betragen darf (um sicher zu gehen besser 16V). Die Ausgangsspannung muss entweder fest oder regelbar auf 5V eingerichtet sein, damit der ESP32 funktioniert.

Stromversorgung

Natürlich könnte das ganze Projekt auch über Batterie (Powerbank oder LiPo) betrieben werden, dann müssen allerdings die Pumpe und der Spannungswandler anders dimensioniert werden. Außerdem muss man vorher gut testen, wie lange solche eine Batterie hält, denn die Pumpe kann u.U. einiges an Energie verbrauchen. Da ich aus der gesamten Rechnung die Zeitspanne der automatischen Bewässerung nicht berücksichtigen wollte, habe ich mich für eine Lösung mit einem 12V-Netz-Adapter entschieden. Damit kann der Motor direkt und der ESP32 über einen einzigen Spannungswandler betrieben werden.

Sketch(es)

Test: Sensor

Zunächst habe ich den Sensor getestet bzw. die Daten analysiert, die dieser liefert. Dazu habe ich einen recht einfachen Sketch erstellt, der einmal pro Sekunde den ermittelten Wert des Sensors auf der seriellen Konsole ausgibt.

#define PIN_SENSOR 15

void setup()
{
    Serial.begin(9600);
}

void loop()
{
    Serial.println(analogRead(PIN_SENSOR));
    delay(1000);
}

Resultate des Sensors bei verschiedenen Messungen waren:

Hinweis: Eine Messung direkt nach dem Reset des ESP32 (oder auch Arduino) liefert immer den Wert 4095. Anscheinend benötigt der Sensor eine gewisse Zeit der Kalibrierung und daher sollten die ersten Messungen 30-60 Sekunden nach Einschalten des Mikrocontrollers ausgewertet werden.

Im späteren Sketch wird auch die WLAN-Verbindung des ESP32 genutzt. Ist diese Verbindung aktiv, sind nur die analogen Eingänge des ADC1 lesbar, d.h. die Pins 32, 33, 34, 35, 36 und 39!

Test: Relais/Pumpe

Um zu testen, wie das Relais mit der Pumpe funktioniert, kann man den folgenden, einfachen Sketch verwenden, der in regelmäßigen Abständen das Relais an- bzw. abstellt.
Sollte das Relais NICHT auf diesen Sketch reagieren, so ist die Spannung von 3,3V -die am digitalen Ausgang des ESP32 anliegt- nicht genug, um das Relais zu steuern. Daher sollte man entweder ein auf anderes Relais-Modul oder sogar einen MOSFET zurückgreifen (siehe unten).

#define PIN_RELAY 5

void setup()
{
    pinMode(PIN_RELAY, OUTPUT);
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
    digitalWrite(LED_BUILTIN, LOW);
    digitalWrite(PIN_RELAY, LOW);
    delay(5000);
    digitalWrite(LED_BUILTIN, HIGH);
    digitalWrite(PIN_RELAY, HIGH);
    delay(5000);
}

Bewässerungs-Sketch

Der folgende Sketch ist das gesamte Programm, welches zur Analyse und automatischen Bewässerung des APWS notwendig ist. Vor der Benutzung müssen noch einige Werte auf die eigenen Bedürfnisse eingestellt werden:

Natürlich kann der Sketch auch angepasst werden, wenn die Server-Komponente gar nicht genutzt werden soll.

/**
   Automatic Plant Watering System (APWS)
   (c) 2021 Christian Grieger
*/

/**
 * This is the unique IoT deviceId (1-255) which identifies the
 * current plant on the server component
 */
#define DEVICE_ID 1

/**
  Pins for the capacitity soil moisture sensor and the
  relay for switching the water pump
  Important: You can only use ADC1 pins (32, 33, 34, 35, 36, 39)
  to use with analogRead() when WiFi is enabled!
*/
#define PIN_SENSOR 32
#define PIN_RELAY  5
#define PIN_LED_INDICATOR LED_BUILTIN

/**
   Watering time (in seconds): Sets how long the pump
   will spread water in one action.
   (default: 10)
*/
#define WATERING_TIME 10

/**
   Time interval (in seconds) how often new commands
   are fetched from the server queue.
   (default: 60)
*/
#define COMMAND_FETCH_INTERVAL 60

/**
   Time interval (in seconds) how often the soil moisture
   is been measured and possible automatic actions are taken.
   (default: 1800; = 60 min.)
*/
#define MEASURING_INTERVAL 1800

/**
  Threshold of the measured soil moisture when
  the system begins with automatic watering.
*/
#define MOISTURE_THRESHOLD 2200
#define MOISTURE_THRESHOLD_STEP 100

/**
   WiFi settings
*/
const char* WIFI_SSID     = "mySSID";
const char* WIFI_PASSWORD = "myPassword";

/**
   Server component settings
*/
const char*    SERVER_HOST = "http://apws.example.com";
const char*    SERVER_URI  = "/";
const uint16_t SERVER_PORT = 80;
const char*    SERVER_PSK  = "mySecretPsk";

#include <WiFi.h>
#include <HTTPClient.h>

#define lmillis() ((long)millis())

#define _DEBUG_MODE
#ifdef _DEBUG_MODE
#define DEBUG_INIT(baudSpeed) Serial.begin(baudSpeed)
#define DEBUG_PRINT(value) Serial.print(value)
#define DEBUG_PRINTLN(value) Serial.println(value)
#else
// Empty macros bodies if debugging is not needed
#define DEBUG_INIT(baudSpeed)
#define DEBUG_PRINT(value)
#define DEBUG_PRINTLN(value)
#endif

#define PAUSE_WATERING     1
#define RESUME_WATERING    2
#define MANUAL_WATERING    3
#define MANUAL_MEASUREMENT 4
#define THRESHOLD_INCREASE 5
#define THRESHOLD_DECREASE 6

#define LOG_TYPE_MEASUREMENT 1
#define LOG_TYPE_ACTION      2
#define LOG_TYPE_COMMAND     3
#define LOG_TYPE_MESSAGE     4

bool automaticWateringEnabled = true;
long lastMeasurement, lastCommandFetched;
unsigned int moistureThreshold;
HTTPClient httpClient;

void setup()
{
    DEBUG_INIT(9600);
    pinMode(PIN_SENSOR, INPUT);

    pinMode(PIN_RELAY, OUTPUT);
    digitalWrite(PIN_RELAY, LOW);

    pinMode(PIN_LED_INDICATOR, OUTPUT);
    digitalWrite(PIN_LED_INDICATOR, LOW);

    lastMeasurement = lmillis() + 60;
    lastCommandFetched = lmillis() + 5;

    moistureThreshold = MOISTURE_THRESHOLD;
}

void loop()
{
    if (lmillis() - lastCommandFetched >= 0) {
        lastCommandFetched = lmillis() + COMMAND_FETCH_INTERVAL * 1000;
        String serverResult = requestServer("request", String(DEVICE_ID));
        executeCommand(serverResult.toInt());
    }

    if (lmillis() - lastMeasurement >= 0) {
        lastMeasurement = lmillis() + MEASURING_INTERVAL * 1000;
        if (isSoilDry()) {
            doWater();
        }
    }
}

bool isSoilDry()
{
    return getSoilMoisture() > moistureThreshold;
}

int getSoilMoisture()
{
    int moisture = analogRead(PIN_SENSOR);
    String mStr = String(moisture);
    DEBUG_PRINTLN("Measured moisture = " + mStr);

    logToServer(LOG_TYPE_MEASUREMENT, mStr);
    return moisture;
}

void doWater()
{
    if (!automaticWateringEnabled) {
        DEBUG_PRINTLN("Soil too dry, but automatic watering is paused.");
        logToServer(LOG_TYPE_MESSAGE, "Soil too dry but automatic watering is paused.");
        return;
    }

    logToServer(LOG_TYPE_COMMAND, "Watering " + String(WATERING_TIME) + "s (automatic)");
    DEBUG_PRINTLN("Begin automatic watering.");

    int timeWatering = WATERING_TIME * 1000;
    digitalWrite(PIN_RELAY, HIGH);

    // safetly check after every 5 sec. if moisture is above threshold, stop watering!
    do {
        if (!isSoilDry()) {
            digitalWrite(PIN_RELAY, LOW);
            DEBUG_PRINTLN("Safety stop watering!");
            return;
        }
        delay(timeWatering);
        timeWatering -= 5000;
    } while (timeWatering > 0);

    digitalWrite(PIN_RELAY, LOW);
    DEBUG_PRINTLN("Ended automatic watering.");
}

void executeCommand(byte command)
{
    DEBUG_PRINT("Executing command ");
    switch (command) {
        case PAUSE_WATERING:
            DEBUG_PRINTLN("PAUSE_WATERING");
            automaticWateringEnabled = false;
            logToServer(LOG_TYPE_COMMAND, "Automatic watering paused");
            break;
        case RESUME_WATERING:
            DEBUG_PRINTLN("RESUME_WATERING");
            automaticWateringEnabled = true;
            logToServer(LOG_TYPE_COMMAND, "Automatic watering resumed");
            break;
        case MANUAL_WATERING:
            DEBUG_PRINTLN("MANUAL_WATERING");
            digitalWrite(PIN_RELAY, HIGH);
            delay(WATERING_TIME * 1000);
            digitalWrite(PIN_RELAY, LOW);
            logToServer(LOG_TYPE_COMMAND, "Watering " + String(WATERING_TIME) + "s (manual)");
            break;
        case MANUAL_MEASUREMENT:
            DEBUG_PRINTLN("MANUAL_MEASUREMENT");
            getSoilMoisture();
            break;
        case THRESHOLD_INCREASE:
            DEBUG_PRINTLN("THRESHOLD_INCREASE");
            if (moistureThreshold < (1023 - MOISTURE_THRESHOLD_STEP)) {
                moistureThreshold += MOISTURE_THRESHOLD_STEP;
            }
            logToServer(LOG_TYPE_COMMAND, "Increased threshold by +" + String(MOISTURE_THRESHOLD_STEP));
            break;
        case THRESHOLD_DECREASE:
            DEBUG_PRINTLN("THRESHOLD_DECREASE");
            if (moistureThreshold > MOISTURE_THRESHOLD_STEP) {
                moistureThreshold -= MOISTURE_THRESHOLD_STEP;
            }
            logToServer(LOG_TYPE_COMMAND, "Decreased threshold by -" + String(MOISTURE_THRESHOLD_STEP));
            break;
        default:
            DEBUG_PRINTLN("NONE/UNKNOWN");
            break;
    }
}

void logToServer(byte logType, String message)
{
    String logTypeStr;
    switch (logType) {
        case LOG_TYPE_MEASUREMENT:
            logTypeStr = "MEASUREMENT";
            break;
        case LOG_TYPE_ACTION:
            logTypeStr = "ACTION";
            break;
        case LOG_TYPE_COMMAND:
            logTypeStr = "COMMAND";
            break;
        case LOG_TYPE_MESSAGE:
        default:
            logTypeStr = "MESSAGE";
            break;
    }

    String logMessage = logTypeStr + "," + String(DEVICE_ID) + "," + message;
    requestServer("log", logMessage);
}

String requestServer(String serverParam, String payload)
{
    String auth = "?psk=" + String(SERVER_PSK);
    String param = "&" + serverParam + "=" + payload;
    String url = String(SERVER_HOST) + ":" + String(SERVER_PORT) + SERVER_URI + auth + param;

    digitalWrite(PIN_LED_INDICATOR, HIGH);

    connectWiFi();
    httpClient.begin(url);

    String httpBody = "";
    int httpCode = httpClient.GET();
    if (httpCode > 0) {
        httpBody = httpClient.getString();
        DEBUG_PRINT("httpCode=");
        DEBUG_PRINT(httpCode);
        DEBUG_PRINT(" - httpContent=");
        DEBUG_PRINTLN(httpBody);
    } else {
        DEBUG_PRINT("ERROR requesting ");
        DEBUG_PRINT(url);
        DEBUG_PRINTLN(" - " + httpClient.errorToString(httpCode));
    }
    httpClient.end();

    digitalWrite(PIN_LED_INDICATOR, LOW);

    return httpBody;
}

void connectWiFi()
{
    if (WiFi.status() == WL_CONNECTED) {
        return;
    }

    DEBUG_PRINT("Connecting to WiFi ");
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        DEBUG_PRINT(".");
    }
    DEBUG_PRINTLN("[ok]");
}

Server-Komponente

Die Server-Komponente besteht primär aus einem PHP-Script, welches sowohl die Messdaten empfängt und in einer Datenbank (MySQL) speichert, als auch Befehle an die angeschlossenen APWS-Module weitergibt. Generell dient die Server-Komponente als Polling-Dienst, welches das APWS-Modul anfragt. Der Server selbst kann keinen Kontakt zu den APWS-Modulen herstellen.

Voraussetzungen

Um die Server-Komponente betreiben zu können sind folgende Voraussetzungen notwendig:

Server-Komponente laden [6,3 kB]

Setup

Nach dem Einrichten sollte nun unter der URL, die man im Webserver konfiguriert hat und mit Angabe des gültigen psk (z.B. "http://apws.example.com/?psk=xxxxx") die Oberfläche der APWS Server-Komponente erscheinen:

APWS Server-Komponente
Abb.: Screenshot der APWS Server-Komponente

Steuerbefehle

Standardmäßig sind die folgenden Steuerbefehle implementiert und können sofort genutzt werden:

Prototyp

Nachdem der Sketch und das PHP-Script fertig programmiert und der Server eingerichtet war, konnte ein erster Prototyp gebaut werden, mit dem man nun alle eingebauten Funktionen testen kann.

Gesamtaufbau des Prototyps des APWS
Abb.: Gesamtaufbau des Prototyps des APWS. Ein Topf trockener Erde wurde als Test-Objekt verwendet. Die Verkabelung ist auch nur provisorisch gesteckt.
Detailansicht des Pflanztopfes mit Sensor und Schlauchzuführung
Abb.: Detailansicht des Pflanztopfes mit Sensor und Schlauchzuführung
Detailansicht der elektronischen Komponenten des Prototyps
Abb.: Detailansicht der elektronischen Komponenten des Prototyps

MOSFET statt Relais

Da viele der handelsüblichen Relais-Module 5V benötigen, um durchzuschalten, sind sie bei den max. 3,3V, die ein ESP32 liefern kann, nutzlos. Eine Alternative ist ein MOSFET, der als Schalter betrieben ebenfalls verwendet werden könnte. Allerdings muss bei der Auswahl des richtigen Transistors auf drei Werte geachtet werden:

Der IRF 1404 oder 1N60 könnten diese Anforderungen erfüllen. (siehe: Informationen wichtiger MOSFETs)

Gehäuse

- noch in Arbeit -

zurück