← Projects
electronics intermediate Jul 15, 2026 ◯ 5 min read

Build an ESP32 Auto-Watering System with Soil Moisture Sensing and WiFi Reporting

Build time: 1 day
Tools needed: Soldering iron
Parts List
esp32soil moistureauto wateringgardenrelaypumpwifiarduinodiy automation

An ESP32-based auto-watering system solves a genuinely common problem: plants that need consistent moisture but don't get it because watering by memory is unreliable. This build reads soil moisture, waters automatically when it drops below a threshold, and reports status over WiFi so you can check on it remotely instead of just hoping it's working.

System Overview

The core loop: read a capacitive soil moisture sensor, compare against a threshold, and if the soil is dry, trigger a relay-controlled water pump for a set duration. Add WiFi reporting on top and you get remote visibility without needing to physically check the plant.

Parts List

ComponentNotes ESP32 dev boardAny standard ESP32 dev board works — this project doesn't need anything specialized Capacitive soil moisture sensorCapacitive, not resistive — resistive sensors corrode from constant electrolysis within weeks of continuous soil contact; capacitive sensors don't have this problem and are worth the small extra cost Relay module (or MOSFET for low-voltage pumps)Sized to switch your pump's actual voltage/current — a relay module is the simpler, more universal choice Small submersible water pump or peristaltic pumpSubmersible DC pumps are cheap and simple for a reservoir setup; peristaltic pumps give more precise, lower-flow dosing if you need gentler watering Water reservoirSized to your watering frequency and away-time needs — bigger buys you more days before refilling TubingFood-safe silicone tubing sized to your pump's fitting Power supplySeparate supply for the pump if it draws more current than the ESP32's regulator can safely provide — don't try to run a pump off the ESP32's 3.3V/5V pins directly unless you've confirmed the current draw is within safe limits

Wiring

Calibrating the Sensor

Capacitive soil sensors output an analog value that needs calibration to your specific sensor and soil — there's no universal "dry" and "wet" number across all sensors.

  1. Read the sensor completely dry (in air) — note this raw ADC value as your "dry" baseline
  2. Insert the sensor in water (not soil) — note this value as your "wet" baseline
  3. Your actual soil readings will fall somewhere between these two extremes; map that range to a 0–100% moisture scale for a more intuitive threshold than raw ADC numbers

Core Firmware Logic

#include <WiFi.h> const int SOIL_PIN = 34; const int RELAY_PIN = 26; const int DRY_VALUE = 3000; // your calibrated dry reading const int WET_VALUE = 1200; // your calibrated wet reading const int MOISTURE_THRESHOLD = 30; // water when below 30% const int WATER_DURATION_MS = 5000; // pump run time per watering event void setup() { pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW); // ensure pump starts off Serial.begin(115200); } int readMoisturePercent() { int raw = analogRead(SOIL_PIN); int percent = map(raw, DRY_VALUE, WET_VALUE, 0, 100); return constrain(percent, 0, 100); } void loop() { int moisture = readMoisturePercent(); Serial.println("Moisture: " + String(moisture) + "%"); if (moisture < MOISTURE_THRESHOLD) { digitalWrite(RELAY_PIN, HIGH); delay(WATER_DURATION_MS); digitalWrite(RELAY_PIN, LOW); delay(600000); // wait 10 min after watering before re-checking, let water absorb } delay(1800000); // check every 30 minutes }

The delay after watering before re-checking matters — soil moisture readings don't reflect a watering event instantly, and checking too soon risks over-watering by triggering the pump again before the water has actually absorbed and the sensor catches up.

Adding WiFi Reporting

Extend the base logic to report status to a simple endpoint (a Home Assistant sensor via its REST API, or your own small logging script) so you can check moisture history and confirm the system is actually running, not just trust it silently:

#include <HTTPClient.h> void reportStatus(int moisture, bool watered) { HTTPClient http; http.begin("http://your-home-server/api/garden-status"); http.addHeader("Content-Type", "application/json"); String payload = "{\"moisture\":" + String(moisture) + ",\"watered\":" + String(watered ? "true" : "false") + "}"; http.POST(payload); http.end(); }

Call this after each moisture check in the main loop, alongside the WiFi connection setup from the CYD dashboard project's pattern if you want a visual history over time.

Safety and Reliability Notes

Extending the Project

AdditionValue Multiple zones (several plants, independent sensors/pumps)Scale to a whole garden bed rather than one plant, each with its own threshold Light sensorCorrelate watering behavior with light exposure, useful for understanding plant health beyond just moisture Scheduled watering windowsCombine threshold-based watering with time-of-day restrictions — e.g., never water at night, to reduce fungal/rot risk Home Assistant integrationFull dashboard, historical graphs, and manual override controls instead of a bare API endpoint

This is a genuinely practical project — unlike a lot of maker builds that are cool but not something you'll use daily, an auto-watering system that actually keeps a plant alive while you're traveling earns its keep immediately.