Build an automatic plant watering system with Blynk

The ESP32 waters your plant when the soil gets dry, checks that the water tank isn't empty and reports everything to the Blynk app, where you can also water by hand.
  • Level:Advanced
  • Duration: 1 h 50 min
  • Ages 13+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build an automatic plant watering system with Blynk – EEMENTOR STEM™ lesson

In the Blynk plant lesson, your phone told you when the plant was thirsty. This capstone goes one step further: the system waters the plant by itself. A soil sensor measures the moisture, a water level sensor checks that there's water in the tank, and a relay switches the pump. The Blynk app shows both readings, lets you switch automatic mode on and off and has a button to water by hand from anywhere. If the tank runs empty, the system refuses to pump and sends you a notification.

What you'll learn

  • How to combine automatic control with manual control from an app
  • How to add a safety check that blocks an action
  • How to report what a device did and when
  • How to keep to five datastreams by choosing what really matters

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • Soil moisture module with probe (45-in-1 Sensor Kit)
  • Water level sensor (45-in-1 Sensor Kit)
  • Relay module (45-in-1 Sensor Kit)
  • A free Blynk account and the Blynk IoT app on a phone
  • A plant in a pot, and a glass or jar of water as the "tank"
  • Optional: a small 3–6 V battery or USB water pump and tube (not included)
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)
  • A 2.4 GHz Wi-Fi network

Safety first

Keep the ESP32, the breadboard and the relay away from the water. Only the soil probe and the lower part of the water level sensor may get wet. If you add a pump, use a small 3–6 V pump with its own battery pack or USB supply, and let the relay switch one of its two wires. Never connect mains electricity to the relay. Without a pump the project works the same way: you hear the relay click and see its LED.

Wiring

Breadboard wiring diagram: Build an automatic plant watering system with Blynk
Breadboard view. Check each connection against the wiring table below.
From To
Soil module VCC ESP32 GPIO 32 (powers the sensor only while measuring)
Soil module GND GND
Soil module AO ESP32 GPIO 35
Soil module DO Not used
Water level sensor S ESP32 GPIO 34
Water level sensor + ESP32 GPIO 33 (powers the sensor only while measuring)
Water level sensor − GND
Relay module S ESP32 GPIO 26
Relay module + ESP32 5V
Relay module − GND

Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.

Set up Blynk

In the Blynk console, create a new template called Plant watering (hardware ESP32, connection type Wi-Fi) and add these datastreams. Connect the ESP32 to Blynk shows each step in detail.

Virtual pin Name Data type Range Phone widget
V0 Moisture Integer 0 – 100, units % Gauge
V1 Tank Integer 0 – 100, units % Gauge
V2 Auto mode Integer 0 – 1 Button (switch mode)
V3 Water now Integer 0 – 1 Button (push mode)
V4 Status String n/a Value Display

Then open the template's Events & Notifications tab, add an event with the code tank_empty (name: Water tank is empty) and turn on push notifications for it. The event code must match the sketch exactly.

Save the template, create a new device from it and copy its three BLYNK_… lines. In the phone app, open the device, tap the wrench icon and add two Gauges (V0 and V1), a Button in switch mode (V2), a Button in push mode (V3) and a Value Display (V4). Link each widget to its datastream.

Example screen for: Build an automatic plant watering system with Blynk
Example screen with sample values. Yours may look different.

The sketch

Replace the three BLYNK_… lines at the top with the ones from your device's Device info page, and add your Wi-Fi name and password (2.4 GHz only). The BLYNK_… lines must stay above #include <BlynkSimpleEsp32.h>.

#define BLYNK_TEMPLATE_ID "YOUR_TEMPLATE_ID"
#define BLYNK_TEMPLATE_NAME "Plant watering"
#define BLYNK_AUTH_TOKEN "YOUR_AUTH_TOKEN"
#define BLYNK_PRINT Serial          // show connection messages in the Serial Monitor

#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>
#include <time.h>

char ssid[] = "YOUR_WIFI_NAME";
char pass[] = "YOUR_WIFI_PASSWORD";
const char* TIME_ZONE = "EET-2EEST,M3.5.0/3,M10.5.0/4";  // Estonia; change for your country

const int SOIL_POWER = 32;
const int SOIL_PIN = 35;
const int TANK_POWER = 33;
const int TANK_PIN = 34;
const int RELAY_PIN = 26;

// Calibrate these for your own sensors (see "Calibrate")
int DRY_READING = 3500;       // soil probe in dry air
int WET_READING = 1400;       // soil probe in a glass of water
int TANK_FULL_READING = 1800; // water level sensor fully in the water

const int WATER_BELOW = 35;                   // water when the soil is drier than this (%)
const int TANK_MINIMUM = 15;                  // don't pump below this tank level (%)
const unsigned long WATER_TIME = 4000;        // the pump runs for 4 seconds
const unsigned long SOAK_TIME = 120000;       // then wait 2 minutes before watering again

BlynkTimer timer;
bool autoMode = true;
bool watering = false;
bool tankAlertSent = false;
int moisture = 0;
int tank = 0;
unsigned long lastWater = 0;

String timeNow() {
  struct tm t;
  if (!getLocalTime(&t, 10)) return "time not set yet";
  char text[20];
  strftime(text, sizeof(text), "%d.%m %H:%M", &t);
  return String(text);
}

int readSensor(int powerPin, int analogPin) {
  digitalWrite(powerPin, HIGH);               // power on only while measuring
  delay(50);
  int reading = analogRead(analogPin);
  digitalWrite(powerPin, LOW);
  return reading;
}

void stopWatering() {
  digitalWrite(RELAY_PIN, LOW);
  watering = false;
  lastWater = millis();
}

void startWatering(String reason) {
  if (watering) return;
  if (tank < TANK_MINIMUM) {                  // safety check: never run the pump dry
    Blynk.virtualWrite(V4, "Tank empty, not watering");
    return;
  }
  watering = true;
  digitalWrite(RELAY_PIN, HIGH);
  timer.setTimeout(WATER_TIME, stopWatering); // switch off again after WATER_TIME
  Blynk.virtualWrite(V4, "Watered " + timeNow() + " (" + reason + ")");
}

BLYNK_WRITE(V2) { autoMode = param.asInt(); }

BLYNK_WRITE(V3) {
  if (param.asInt() == 1) startWatering("by hand");
}

BLYNK_CONNECTED() { Blynk.syncVirtual(V2); }

void checkPlant() {
  if (watering) return;                       // don't measure while the pump runs

  int soilReading = readSensor(SOIL_POWER, SOIL_PIN);
  int tankReading = readSensor(TANK_POWER, TANK_PIN);
  moisture = constrain(map(soilReading, DRY_READING, WET_READING, 0, 100), 0, 100);
  tank = constrain(map(tankReading, 0, TANK_FULL_READING, 0, 100), 0, 100);
  Serial.printf("Soil reading %d = %d %%, tank reading %d = %d %%\n", soilReading, moisture, tankReading, tank);

  Blynk.virtualWrite(V0, moisture);
  Blynk.virtualWrite(V1, tank);

  if (tank < TANK_MINIMUM && !tankAlertSent) {
    Blynk.logEvent("tank_empty", "Please refill the water tank");
    tankAlertSent = true;
  }
  if (tank > TANK_MINIMUM + 15) tankAlertSent = false;    // refilled: ready for the next alert

  bool soaked = millis() - lastWater >= SOAK_TIME;
  if (autoMode && moisture < WATER_BELOW && soaked) startWatering("auto");
}

void setup() {
  Serial.begin(115200);
  pinMode(SOIL_POWER, OUTPUT);
  pinMode(TANK_POWER, OUTPUT);
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW);
  Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
  configTzTime(TIME_ZONE, "pool.ntp.org");    // real time from the internet
  timer.setInterval(10000L, checkPlant);      // every 10 seconds
  checkPlant();                               // first reading straight away
}

void loop() {
  Blynk.run();
  timer.run();
}

Calibrate

  1. Upload the sketch and open the Serial Monitor at 115200 baud. Every ten seconds it prints both raw readings.
  2. Hold the soil probe in the air and put its reading in DRY_READING. Dip it in water up to the line and put that reading in WET_READING.
  3. Lower the water level sensor into the tank until the water reaches the top of its metal lines, and put that reading in TANK_FULL_READING.
  4. Upload again. Push the soil probe into the pot and fix the water level sensor inside the tank, with its connector above the water.

Test it

  1. Tap Water now in the app. The relay clicks on for four seconds and the status shows the time and "(by hand)".
  2. Lift the soil probe out of the pot. After the next reading, the relay switches on by itself and the status shows "(auto)". It won't water again for two minutes.
  3. Lift the water level sensor out of the tank. You get a "Water tank is empty" notification, and both the button and automatic mode now refuse to pump.
  4. Switch Auto mode off. The system keeps measuring but waters only when you press the button.

How it works

All watering goes through one function, startWatering(), whether it was asked for by the app or by the automatic rule. That function contains the safety check, so there's no way around it: with too little water in the tank, nothing can switch the pump on. Putting a safety rule in the one place that everything must pass through is called an interlock, and you find it in every well-designed machine.

timer.setTimeout(WATER_TIME, stopWatering) asks Blynk's timer to call stopWatering() once, four seconds later. The sketch doesn't wait during that time, so the connection to Blynk stays alive. After watering, the system waits two minutes before automatic mode may water again, because the water needs time to reach the probe.

The free Blynk plan gives you a limited number of datastreams per template, so this project uses five and makes each one count. The limits for moisture and tank level are constants in the sketch instead of sliders, and one text line reports what happened and when.

Try this next

  • Count the waterings per day and send the number in the status text.
  • Send a second event when the system has watered five times in one hour: the tube may have slipped out of the pot.
  • Water only in the morning and evening by checking the hour with getLocalTime().
  • Add a Chart widget for moisture and watch the saw-tooth pattern of drying and watering.

Troubleshooting

  • The device shows "Offline" in the app: check the Serial Monitor at 115200 baud. "Invalid auth token" means the BLYNK_AUTH_TOKEN line is wrong; if it never connects, check the Wi-Fi name and password, and that your network is 2.4 GHz.
  • The moisture or tank value is always 0 % or 100 %: calibrate again; your readings may be outside the default numbers.
  • The status says "Tank empty, not watering" with water in the tank: TANK_FULL_READING is too high. Calibrate the water level sensor again.
  • The relay is on when it should be off: some relay modules switch on with LOW. Swap HIGH and LOW in the three relay lines.
  • The relay doesn't click: its + pin must be on 5V, not 3V3.
  • The status shows "time not set yet": the ESP32 hasn't received the time from the internet yet. It appears after a few seconds online.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit