Table of Contents

Smart Plant Watering with ESP32 and MQTT

Applied Measurement and Control 2025

Paul-Christian Thoma(32436)-Elham Mohammadi(32475)-Deniz-Zeynep Adem(33784)

1. Introduction

By Elham Mohammadi

Maintaining optimal soil moisture is essential for plant health, especially in indoor or unattended environments. Manual watering is often inconsistent, which can lead to overwatering, underwatering, or resource waste. This project addresses these challenges by developing a semi-autonomous, Wi-Fi-enabled plant watering system that monitors soil moisture in real time and triggers irrigation when needed. This is especially helpful when leaving over a longer period of time but still wanting to make sure that the plants covered by the system are regularly watered. added by PT

The system consists of two ESP32-S3 microcontrollers communicating via Wi-Fi using the MQTT protocol. One ESP32 functions as a sensor node, measuring soil moisture and publishing readings to an MQTT broker. The second ESP32 acts as an actuator node (pump nodes), subscribing to control messages and operating a peristaltic pump through servo control.

A key feature of the system is that the decision to activate watering is not made directly on the sensor node. Instead, the control logic is handled externally using Node-RED, which processes incoming MQTT data and publishes appropriate commands. This approach allows the control logic to be easily modified without reprogramming the ESP32 devices.

Sensor data is also logged to Google Sheets via Google Apps Script, and email notifications are triggered when dry soil conditions are detected. The system integrates with Node-RED for real-time visualization and monitoring.

By combining local sensing, cloud-based logic, and remote monitoring tools, this system provides a scalable, modular approach to smart irrigation in home or laboratory environments.

{{ amc:ss2025:group-f:prototype2.jpeg |}} Figure 1. Automated Plant Watering System Prototype

2. Materials and Methods

2.1 Materials

By Elham Mohammadi

2.2 Software

By Elham Mohammadi and Deniz Zeynep Adem

Arduino IDE:

Node-RED:

Google Sheets:

Google Apps Script:

MQTT Broker (HiveMQ Public Broker):

Tinkercad / Fritzing:

AI Tools:

2.3 Setup Overview

by Elham Mohammadi and Deniz Zeynep Adem

The Sensor Node (ESP32-S3) and Pump Node (ESP32-S3) both connect to the local Wi-Fi network. The MQTT connection is established with the HiveMQ public broker.

Soil Moisture Reading:

Node-RED Evaluation and Decision:

Pump Activation:

Logging and Alerts:

If:

To bring the designed system to life, the next step involves assembling the hardware components and ensuring proper electrical connections. The wiring setup includes the soil moisture sensor connected to the Sensor Node and a peristaltic pump controlled by the Pump Node via a latching relay. Accurate and safe wiring is essential to ensure reliable communication, sensor readings, and pump activation.

The following diagrams, created using Tinkercad, illustrate the complete hardware connections for both the Sensor and Pump nodes in the system.

Figure 2. Schematic Diagram of Sensor Setup (designed by Deniz-Zeynep Adem)

Figure 3. Schematic Diagram of Pump Setup (designed by Deniz-Zeynep Adem)

3. Results

3.1 Code Explanation

By Paul-Christian Thoma & Elham Mohammadi & Deniz-Zeynep Adem

Sensor Node Code:

#include <WiFi.h>              // Library for WiFi connection
#include <HTTPClient.h>        // Library for HTTP requests
#include <MQTTClient.h>        // Lightweight MQTT client
 
// Wi-Fi and MQTT credentials
const char WIFI_SSID[] = "iotlab";
const char WIFI_PASSWORD[] = "iotlab18";
const char MQTT_BROKER[] = "broker.hivemq.com";  // Public MQTT broker
const int MQTT_PORT = 1883;
const char MQTT_CLIENT_ID[] = "ESP32_Sensor_001"; // Unique client ID
 
// Google Apps Script webhook URL for sending data to Google Sheets
const char GOOGLE_SHEET_URL[] = "https://script.google.com/macros/s/AKfycbwWx8W6ov2rufz6ODUtUngPOhJMdYazwkHozmzk8-3n6gyL-qNCGgX9YxKtBTL7Qm5Vfg/exec";
 
// MQTT Topics
const char PUBLISH_TOPIC_VALUE[] = "plant/moisture/value";   // Publishes raw moisture reading
const char PUBLISH_TOPIC_STATUS[] = "plant/moisture/status"; // Publishes human-readable status
const char COMMAND_TOPIC[] = "plant/pump/control";           // Used to send watering command
 
// Sensor pin and threshold
const int sensorPin = 4;         // GPIO pin connected to moisture sensor
const int threshold = 2500;      // Dry threshold – adjust based on calibration
 
WiFiClient net;                  // WiFi client object
MQTTClient mqtt(256);            // MQTT client with 256-byte buffer
 
unsigned long lastRead = 0;
const int readInterval = 5000;   // Time between measurements (5 seconds)
 
// Function to connect to MQTT broker
void connectToMQTT() {
  mqtt.begin(MQTT_BROKER, MQTT_PORT, net);     // Start MQTT with broker info
  while (!mqtt.connect(MQTT_CLIENT_ID)) {      // Keep trying until connected
    Serial.print(".");
    delay(500);
  }
  Serial.println("\nMQTT connected.");
}
 
// Function to send data to Google Sheets using HTTP POST
void postToGoogleSheet(int moisture, const String& status) {
  HTTPClient http;                             // Create HTTP client
  http.begin(GOOGLE_SHEET_URL);                // Set URL for POST
  http.addHeader("Content-Type", "application/json"); // Send as JSON
 
  // Format data as JSON payload
  String action;
  if (status == "Dry") {
    action = "Pump is ON 💧";
  } else if (status == "Moist") {
    action = "OK 👍";
  } else {
    action = "Alert ⚠️ Maintenance Needed";
  }
 
  String jsonPayload = "{\"moisture\":\"" + String(moisture) +
                       "\",\"status\":\"" + status +
                       "\",\"action\":\"" + action + "\"}";
 
  Serial.println("POSTing: " + jsonPayload);
 
  int responseCode = http.POST(jsonPayload);   // Send POST request
  Serial.println("HTTP Response: " + String(responseCode)); // Print result
  http.end();                                  // Close connection
}
 
void setup() {
  Serial.begin(115200);                  // Initialize serial monitor
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);  // Connect to WiFi
 
  // Wait until WiFi is connected
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println("\nWiFi connected.");
 
  connectToMQTT();                       // Connect to MQTT broker
}
 
void loop() {
  mqtt.loop();                           // Handle background MQTT tasks
 
  // Only read sensor and publish every 5 seconds
  if (millis() - lastRead > readInterval) {
    int value = analogRead(sensorPin);               // Read analog moisture value
    String status = value > threshold ? "Dry ☹️" : "Moist 😊"; // Determine status
 
    // Publish raw value and status to MQTT topics
    mqtt.publish(PUBLISH_TOPIC_VALUE, String(value));
    mqtt.publish(PUBLISH_TOPIC_STATUS, status);
 
    // Print to Serial for debugging
    Serial.print("Soil Moisture: ");
    Serial.print(value);
    Serial.print(" → Status: ");
    Serial.println(status);
 
    // Send the data to Google Sheets
    postToGoogleSheet(value, status);
 
    // If soil is dry, send water command
    if (status.startsWith("Dry")) {
      mqtt.publish(COMMAND_TOPIC, "WATER"); // Triggers pump
    }
 
    lastRead = millis(); // Update time for next read
  }
}

Pump Node Code:

By Paul-Christian Thoma & Elham Mohammadi & Deniz-Zeynep Adem

#include <WiFi.h>              // WiFi library for ESP32
#include <PubSubClient.h>      // MQTT library
#include <Servo.h>             // For controlling the servo pump
#include <HTTPClient.h>        // For HTTP POST to Google Sheets
 
// Wi-Fi credentials
const char* ssid = "iotlab";
const char* password = "iotlab18";
 
// MQTT broker
const char* mqtt_server = "broker.hivemq.com";
const char* control_topic = "plant/pump/control";
 
// Google Apps Script Web App URL
const char* google_script_url = "https://script.google.com/macros/s/AKfycbwWx8W6ov2rufz6ODUtUngPOhJMdYazwkHozmzk8-3n6gyL-qNCGgX9YxKtBTL7Qm5Vfg/exec";
 
WiFiClient espClient;
PubSubClient client(espClient);
 
Servo pumpServo;
const int servoPin = 5;
 
void setup_wifi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
}
 
void postToGoogleSheet(const String& status, const String& action) {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    http.begin(google_script_url);
    http.addHeader("Content-Type", "application/json");
 
    // Build JSON payload
    String payload = "{\"status\":\"" + status + "\",\"moisture\":\"-\",\"action\":\"" + action + "\"}";
 
    int httpResponseCode = http.POST(payload);
    Serial.print("Google Sheet POST response: ");
    Serial.println(httpResponseCode);
    http.end();
  } else {
    Serial.println("WiFi not connected. Could not send to Google Sheets.");
  }
}
 
void callback(char* topic, byte* payload, unsigned int length) {
  String msg = "";
  for (unsigned int i = 0; i < length; i++) {
    msg += (char)payload[i];
  }
  msg.trim();
 
  Serial.print("Message received [");
  Serial.print(topic);
  Serial.print("]: ");
  Serial.println(msg);
 
  if (String(topic) == control_topic) {
    if (msg == "WATER" || msg == "ON") {
      pumpServo.write(180);
      Serial.println("Pump is ON 💧");
      postToGoogleSheet("Dry ☹️", "Pump is ON");
    } else if (msg == "OFF") {
      pumpServo.write(90);
      Serial.println("Pump is OFF ⛔");
      postToGoogleSheet("Moist 🙂", "Pump is OFF");
    }
  }
}
 
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (client.connect("ESP32_Pump_Node")) {
      Serial.println("connected");
      client.subscribe(control_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      delay(2000);
    }
  }
}
 
void setup() {
  Serial.begin(115200);
  setup_wifi();
  pumpServo.attach(servoPin);
  pumpServo.write(90); // Default OFF position
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}
 
void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

3.3 Code Logic/Overview:

by Paul Thoma

This project employs two ESP32 microcontroller systems to create a smart irrigation system capable of:

For that it is divided into two key parts: 1. Soil Moisture Publishing node 2. Pump Control Subscriber node

How the code is achieving that and the logic behind that will be explained in the following text.

3.4 Soil Moisture Publishing Node

Measures soil and controls watering logic via MQTT.

This ESP32 is responsible for:

Setup:

Key Libraries used:

Loop Behavior:

3.5 Pump Control Subscriber Node

Controls pump based on MQTT messages.

This ESP32 handles the actuation based on MQTT control messages and is responsible for:

Key Libraries used:

Setup:

Callback Function:

Loop:

3.6 Google Apps Script and Google sheets

by Deniz Zeynep Adem

Google Sheets and Google Apps Script play a key role in the data logging and monitoring functionality of the project. Google Sheets acts as a live, cloud-based dashboard where all sensor readings, status updates, and system actions are recorded in real time. To enable this, a custom Google Apps Script was deployed as a web app, which allows external devices—like the ESP32 microcontroller—to send HTTP POST requests directly to the sheet.

The script receives and analyzes incoming JSON data containing the timestamp, moisture level, system status (such as “Moist 😊” or “Dry ☹️”), and any actions taken (e.g., “Pump ON” or “Alert/Maintenance Needed”). This integration not only removes the need for manual logging but also allows for enhanced automation features, such as triggering email alerts when the moisture drops below a threshold or when abnormal behavior is detected.

Overall, the use of Google Sheets and Apps Script provides a simple yet powerful solution for real-time monitoring, data storage, and system feedback without the need for additional servers or complex backend infrastructure.

The saved data can be accessed through this link:

https://docs.google.com/spreadsheets/d/1N0_HnzShdQWwzD-0LYbjcm9xt-VcR1N9Uz19G0Y0euU/edit?usp=sharing

Step-by-Step Guide to Connect ESP32 to Google Sheets

1. Create a Google Sheet

2. Open Google Apps Script

3. Deploy as a Web App

In the Apps Script editor:

4. Integrate the Web App URL into Your ESP32 Code

In your ESP32 Arduino code, update the `GOOGLE_SHEET_URL` with the copied URL from your own project:

const char GOOGLE_SHEET_URL[] = “https://script.google.com/macros/s/your_script_id/exec”;

5. Upload and Run the ESP32 Code

Upload your finalized ESP32 code via Arduino IDE.

6. (Optional) Enable Email Alerts

With this setup:

Google Apps Script Codes:

function doPost(e) {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
    const data = JSON.parse(e.postData.contents);
 
    const status = data.status || "";
    const moisture = data.moisture || "";
    const action = data.action || "";
    const timestamp = new Date();
 
    // Add new data to top (2nd row)
    sheet.insertRows(2, 1);
    sheet.getRange(2, 1).setValue(timestamp);
    sheet.getRange(2, 2).setValue(action);
    sheet.getRange(2, 3).setValue(moisture);
    sheet.getRange(2, 4).setValue(status);
 
    // Send emails if there is an alert
    if (action.toLowerCase().includes("alert")) {
      MailApp.sendEmail("youremail@example.com", "⚠️ Plant Alert", 
        `Soil Alert!\nStatus: ${status}\nMoisture: ${moisture}\nAction: ${action}`);
    }
 
    return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);
  } catch (err) {
    return ContentService.createTextOutput("Error: " + err)
                         .setMimeType(ContentService.MimeType.TEXT);
  }
}

4. Node-RED Overview

Created by Paul-Christian Thoma & Documented by Elham Mohammadi

Node-RED is used as an external control system. It subscribes to MQTT topics published by the sensor node, applies threshold rules to determine soil dryness, and publishes control commands to the pump node. This separation allows the logic to be updated or expanded easily without changing ESP32 firmware.

Node-RED also supports visualization of sensor data and logging, which facilitates remote monitoring. Email notifications are triggered when dry conditions are detected but not improving within a set period of time, alerting users to take action or verify the system.

Below is a video showcasing the node-Red flow and showing the code and possible errors that came up and how they were fixed. Starting with the discussion part of this documentation.

Figure 4. Documentation of the data in note-red (designed by Paul-Christian Thoma)

5. Discussion and Conclusion

By Elham Mohammadi & Paul-Christian Thoma & Deniz-Zeynep Adem

The project successfully demonstrates an automated plant watering system based on real-time soil moisture monitoring and MQTT communication. Separating control logic from the sensor node to Node-RED allows flexible and remote decision-making. Integration with Google Sheets and email notifications adds monitoring and alert capabilities.

The setup of the hardware is quick and easy especially since the water pump had a build in relay. As long as the right amount of power in this case 9V can be supplied to the pump. Most of the effort was to program the software and setup the connection to a cloud based spreadsheet.

When setting up the email-Alert extra steps where needed to allow node-red to send an email. For example creating an app password for the gmail email and enabling 2FA authentication. If feasible it might be an option to rely on sms messaging or a telegram bot for communicating the alert.

A possible advantage would be real time communication with no delay. Additionally it was necessary to include a command to prevent the alert from being triggered multiple times during the same event. Otherwise if the pump would malfunction once the alert would continously be send every 10 seconds until it was fixed which would lead to the inbox being filled with e-mails.

As of right now the system is only suitable for indoor application of plants inside a living room. Since the current setup is not weatherproof/waterproof.

To make it self sustainable it could be considered to use a PV-module for supplying power and charging the battery which is powering the pump. It should be connected to a charging circuit to prevent overcharging of the battery. To safe energy a deep sleep function could be integrated so the sensor is not constantly reading values but rather every couple hours since the moisture will most likely not rapidly change. When setting it up outside a stable connection to a local WiFi network needs to be possible.

5.1 Limitations & Improvements

By Elham Mohammadi

While the system successfully demonstrates automated irrigation using MQTT and ESP32 microcontrollers, there are several limitations that affect its performance and scalability:

6. Media and Demonstration

By Elham Mohammadi & Paul-Christian Thoma & Deniz-Zeynep Adem

7. References

By Elham Mohammadi & Paul-Christian Thoma & Deniz-Zeynep Adem