Community project

Automated Plant Watering

ESP32
Photo of Automated Plant Watering
Generated with AI

mc4444

Published November 2, 2026

This project builds an automated watering system that monitors soil moisture and water availability, then activates a pump to keep plants hydrated. The ESP32 reads a capacitive soil sensor to detect when watering is needed and uses an ultrasonic distance sensor to ensure the water bucket hasn't run dry before dispensing.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions with emphasis on electrical safety around water. Firmware with WiFi connectivity and safety lockouts is included, along with calibration procedures and testing protocols to verify the system operates reliably before leaving it unattended.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

Gravity: IP65 Capacitive Soil Moisture Sensor

Capacitive soil moisture sensor with IP65 waterproof and corrosion-resistant construction. Compatible with Arduino, ESP32, and Raspberry Pi.

1

HC-SR04

HC-SR04 ultrasonic distance sensor module with separate 40 kHz transmitter and receiver transducers, measuring 2 to 400 cm. A 10 us pulse on TRIG starts a measurement and ECHO stays high for the echo's round-trip time. This HC-SR04+ board runs from 3.3 V or 5 V.

1

12V DC Submersible Mini Water Pump

Two-wire brushed DC submersible water pump for small irrigation or circulation projects. It is an external load and must be driven through a relay, MOSFET, or motor driver; do not connect directly to an MCU GPIO.

1

1N4001 Flyback Diode

General-purpose silicon rectifier diode used as a flyback / freewheeling diode to suppress back-EMF voltage spikes from inductive loads (DC motors, relays). Rated 50 V reverse voltage and 1 A forward current. Connected in parallel across the motor terminals with cathode (striped band) to the motor positive (+) / 5 V supply side and anode to the motor negative (−) / transistor collector side. Requires no firmware or library — purely passive protection component. Higher-voltage 1N400x rectifier variants are compatible substitutes when the current and package fit the design.

1

5 V active-low single-channel relay module

Cytron-style 5 V active-low module with onboard coil driver and diode. IN low closes COM to NO; use only low-voltage DC contacts.

1

onsemi 2N3904 NPN transistor

Open-collector level interface: emitter pin 1, base pin 2, collector pin 3. Keeps 5 V away from ESP32.

1

2.2 kohm resistor

2.2 kohm

Quarter-watt resistor.

1

100 kohm resistor

100 kohm

Quarter-watt resistor.

1

10 kohm resistor

10 kohm

Quarter-watt resistor.

1

10 kohm resistor

10 kohm

Quarter-watt resistor.

1

15 kohm resistor

15 kohm

Quarter-watt resistor.

1

Regulated isolated 12 V DC 2 A adapter

Certified enclosed wall adapter, 12 V output only; no exposed mains connections. Pump chosen for <=500 mA running current and <=1 A startup.

1

1 A inline fuse and holder

Protects pump wiring; select pump with startup below 1 A.

Assemble it in 6 steps

1. Keep electricity away from water

Unplug all supplies. Use only the specified low-voltage DC pump, an isolated enclosed 12 V adapter, and an active-low 5 V relay module with onboard coil protection. Keep the ESP32, relay, connections and adapter dry and above the bucket.

  • Never wire mains electricity to this project. Do not power the pump from ESP32 or USB.

2. Connect the soil probe and distance sensor

Soil VCC to 3V3, GND to GND, AOUT to GPIO0 (soil reading). Sonar VCC to 5V, GND to GND, TRIG to GPIO6 (measurement start). ECHO goes through the 10 kohm top resistor to GPIO7; the 15 kohm bottom resistor joins GPIO7 to GND. Mount sonar facing straight down at the water, above splash level, with at least 2 cm clearance at full water.

  • Never connect a 5 V ECHO directly to the ESP32. The HC-SR04 is not waterproof; condensation and angled surfaces can cause faults.

3. Connect the protected relay input

Relay VCC to USB 5V and GND to ESP32 GND. GPIO10 connects through the 2.2 kohm resistor to transistor base B. Connect 100 kohm from B to GND, emitter E to GND, collector C to relay IN, and 10 kohm from IN to 5V. For the specified onsemi 2N3904, pins are 1 emitter, 2 base, 3 collector; check its package drawing before insertion. This makes GPIO10 HIGH turn the pump on while keeping 5 V away from the ESP32.

4. Connect pump power

Adapter positive to inline 1 A fuse, fuse output to relay COM, relay NO to pump positive. Pump negative to adapter negative. Leave NC unused. Fit diode across pump terminals, striped end to positive and unstriped end to negative. Keep this 12 V circuit separate from ESP32 rails. Use a pump rated no more than 500 mA running and below 1 A at startup. Use secure screw connections for motor current.

  • A reversed diode can short the supply. Keep pump submerged as its maker requires. Keep the hose outlet above the bucket's maximum water line to avoid continuous siphoning.

5. Calibrate before connecting pump power

Leave 12 V disconnected. Observe soil millivolts with the probe in dry soil and watered, drained soil. Record these as SOIL_DRY_MV and SOIL_WET_MV in config.h. Measure distance from sonar face to water at the lowest safe level while the pump inlet remains submerged; set LOW_WATER_CM and MAX_BUCKET_CM for your bucket. Default values are examples, not calibrated measurements. Set CALIBRATED true only after checking them.

6. Test the cutoff before unattended use

With pump supply disconnected, check relay stays off at startup and clicks for five seconds only with dry soil and enough water. Test low water and blocked/unplugged sonar: both must latch watering off. After refilling or repair, send capital R over the serial monitor to clear the lock; a power cycle does not clear it. Confirm an ntfy alert arrives before relying on notifications. Connect pump power only after these checks and supervise initial watering.

  • Set Wi-Fi, HTTPS ntfy topic URL, access token and the server's trusted root CA certificate in src/config.h; subscribe to the same topic in the ntfy app. Token alone does not identify a topic. Alerts retry after network failures, but cannot arrive without working Wi-Fi and ntfy.
  • A steady distance can be normal in a large bucket. This build uses a measured low-water limit, not lack of movement. It cannot detect every stuck-but-plausible sensor reading or a broken hose.

Review all connections

1. Connections between "soil" and "ESP32"

FunctionsoilESP32
powerVCC3V3
groundGNDGND
analogAOUTGPIO 0

2. Connections between "sonar" and "ESP32"

FunctionsonarESP32
powerVCC5V
groundGNDGND
digitalTRIGGPIO 6
digitalECHO → 10 kohm resistor 1EXT

3. Connections between "echo_top" and "ESP32"

Functionecho_topESP32
data2GPIO 7
data2 → 15 kohm resistor 1EXT

4. Connections between "echo_bottom" and "ESP32"

Functionecho_bottomESP32
ground2GND

5. Connections between "relay" and "ESP32"

FunctionrelayESP32
powerVCC5V
groundGNDGND
digitalIN → onsemi 2N3904 NPN transistor CEXT
digitalIN → 10 kohm resistor 1EXT
powerNO → 12V DC Submersible Mini Water Pump PUMP+EXT

6. Connections between "pullup" and "ESP32"

FunctionpullupESP32
power25V

7. Connections between "q1" and "ESP32"

Functionq1ESP32
groundEGND
digitalB → 2.2 kohm resistor 2EXT
digitalB → 100 kohm resistor 1EXT

8. Connections between "base" and "ESP32"

FunctionbaseESP32
data1GPIO 10

9. Connections between "pulldown" and "ESP32"

FunctionpulldownESP32
ground2GND

10. Connections between "supply" and "ESP32"

FunctionsupplyESP32
powerPLUS → 1 A inline fuse and holder INEXT

11. Connections between "fuse" and "ESP32"

FunctionfuseESP32
powerOUT → 5 V active-low single-channel relay module COMEXT

12. Connections between "pump" and "ESP32"

FunctionpumpESP32
dataPUMP- → Regulated isolated 12 V DC 2 A adapter MINUSEXT

13. Connections between "diode" and "ESP32"

FunctiondiodeESP32
passiveCathode → 12V DC Submersible Mini Water Pump PUMP+EXT
passiveAnode → 12V DC Submersible Mini Water Pump PUMP-EXT

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Preferences.h>
#include <time.h>
#include "config.h"
constexpr int SOIL_PIN=0, TRIG_PIN=6, ECHO_PIN=7, RELAY_PIN=10;
constexpr uint32_t WAIT_MS=180000, RUN_MS=5000;
Preferences prefs;
bool locked=false, pending=false;
String alert;
uint32_t lastCheck=0, lastNotify=0;
bool first=true;
void lockWater(const char *why) {
  digitalWrite(RELAY_PIN, LOW);
  if (!locked) {
    locked=true; prefs.putBool("locked",true);
    alert=why; prefs.putString("reason",alert); pending=true; prefs.putBool("pending",true);
    Serial.println(alert);
  }
}
float distanceCm() {
  float a[3];
  for(int i=0;i<3;i++) {
    digitalWrite(TRIG_PIN,LOW); delayMicroseconds(3);
    digitalWrite(TRIG_PIN,HIGH); delayMicroseconds(10); digitalWrite(TRIG_PIN,LOW);
    unsigned long us=pulseIn(ECHO_PIN,HIGH,30000);
    a[i]=us/58.0f;
    if(!us || a[i]<2 || a[i]>MAX_BUCKET_CM) return -1;
    delay(65);
  }
  if(a[0]>a[1]) {float t=a[0];a[0]=a[1];a[1]=t;}
  if(a[1]>a[2]) {float t=a[1];a[1]=a[2];a[2]=t;}
  if(a[0]>a[1]) {float t=a[0];a[0]=a[1];a[1]=t;}
  return a[1];
}
bool safeWater() {
  float cm=distanceCm(); Serial.printf("Water distance %.1f cm\n",cm);
  if(cm<0) {lockWater("Watering stopped: ultrasonic sensor reading invalid. Check sensor and bucket, then reset lock.");return false;}
  if(cm>=LOW_WATER_CM) {lockWater("Watering stopped: bucket water is low. Refill bucket, then reset lock.");return false;}
  return true;
}
void notify() {
  if(!pending || WiFi.status()!=WL_CONNECTED || !strlen(NTFY_TOKEN) || !strlen(NTFY_CA_PEM)) return;
  if(time(nullptr)<1700000000) return;
  if(lastNotify && millis()-lastNotify<60000) return;
  lastNotify=millis();
  WiFiClientSecure tls; tls.setCACert(NTFY_CA_PEM); tls.setHandshakeTimeout(5);
  HTTPClient http; http.setConnectTimeout(5000); http.setTimeout(5000);
  if(!String(NTFY_URL).startsWith("https://") || !http.begin(tls,NTFY_URL)) return;
  http.addHeader("Authorization",String("Bearer ")+NTFY_TOKEN);
  http.addHeader("Title","Plant watering stopped");
  http.addHeader("Priority","high");
  int code=http.POST(alert); http.end();
  Serial.printf("ntfy HTTP status: %d\n",code);
  if(code>=200 && code<300) {pending=false; prefs.putBool("pending",false);}
}
void setup() {
  digitalWrite(RELAY_PIN,LOW); pinMode(RELAY_PIN,OUTPUT);
  pinMode(TRIG_PIN,OUTPUT); digitalWrite(TRIG_PIN,LOW); pinMode(ECHO_PIN,INPUT);
  Serial.begin(115200); analogReadResolution(12); analogSetPinAttenuation(SOIL_PIN,ADC_11db);
  prefs.begin("watering",false); locked=prefs.getBool("locked",false);
  pending=prefs.getBool("pending",false); alert=prefs.getString("reason","Watering locked. Inspect bucket and sensor.");
  WiFi.setAutoReconnect(true);
  if(strlen(WIFI_SSID)) {WiFi.begin(WIFI_SSID,WIFI_PASSWORD);configTime(0,0,"pool.ntp.org","time.nist.gov");}
  Serial.println("Pump defaults OFF. Send R over serial after refill/repair to clear safety lock. Calibration must be enabled in config.h.");
  // Full soak delay on every boot prevents rapid watering after power interruptions.
  lastCheck=millis();
}
void loop() {
  if(Serial.available() && Serial.read()=='R') {
    if(CALIBRATED && safeWater()) {locked=false;pending=false;prefs.putBool("locked",false);prefs.putBool("pending",false);lastCheck=millis();Serial.println("Lock cleared; waiting 180 seconds.");}
  }
  // All network operations happen with pump OFF.
  notify();
  if(millis()-lastCheck<WAIT_MS) {delay(10);return;}
  lastCheck=millis();
  long mv=0;for(int i=0;i<16;i++){mv+=analogReadMilliVolts(SOIL_PIN);delay(3);}mv/=16;
  Serial.printf("Soil: %ld mV\n",mv);
  if(!CALIBRATED) {Serial.println("Calibration required; pump disabled.");return;}
  if(locked) return;
  if(SOIL_DRY_MV<=SOIL_WET_MV || mv<100 || mv>3200) {lockWater("Watering stopped: check soil probe connection or calibration.");return;}
  if(!safeWater()) return;
  int moisture=constrain((SOIL_DRY_MV-mv)*100/(SOIL_DRY_MV-SOIL_WET_MV),0L,100L);
  Serial.printf("Moisture: %d percent\n",moisture);
  if(moisture<DRY_BELOW_PERCENT) {
    digitalWrite(RELAY_PIN,HIGH); // NPN pulls active-low relay input down.
    delay(RUN_MS); // No networking or measurements can extend this run.
    digitalWrite(RELAY_PIN,LOW);
    lastCheck=millis(); // At least 180 seconds from pump OFF before soil recheck.
  }
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik