Build an ESP32 Auto-Watering System with Soil Moisture Sensing and WiFi Reporting
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 limitsWiring
- Soil moisture sensor: analog output to an ESP32 ADC-capable GPIO pin, power from 3.3V, ground to ESP32 ground
- Relay module: control pin to a GPIO pin, relay's switched side wired in series with the pump and its own power supply — the ESP32 only switches the relay, it doesn't carry pump current directly
- Keep the pump's power supply and wiring physically separate from the ESP32 and sensor wiring where practical — motor switching can introduce electrical noise that affects sensitive analog sensor readings if everything shares ground poorly
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.
- Read the sensor completely dry (in air) — note this raw ADC value as your "dry" baseline
- Insert the sensor in water (not soil) — note this value as your "wet" baseline
- 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
- Always add a maximum watering duration cap in software, even beyond the per-event duration above — if a sensor fails and reads permanently "dry," you don't want the pump running indefinitely and flooding everything. Consider a hard daily watering-event limit as a backup safeguard.
- Check the reservoir level separately if possible — a simple float switch or a second capacitive sensor in the reservoir prevents the pump from running dry, which shortens pump lifespan significantly
- Test with water you don't mind spilling before trusting this near anything valuable — verify tubing connections are secure and the pump actually delivers water where you expect before walking away from an unattended system
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 endpointThis 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.
Related Guides
- Arduino vs ESP32: Which Should You Use? A Practical Comparison
- Getting Started with ESP32: GPIO, WiFi, and Your First Project
- ESP32-WROOM-32
- ESP32-C6
- How to Program Addressable LED Strips: WS2812B Patterns, Effects, and Power Design
- I2C vs SPI vs UART: How to Choose and Use Serial Communication Protocols
- How to Use Sensors with Arduino and ESP32: Temperature, Distance, Load, Current, and Hall Effect
- How to Control Motors with Arduino and ESP32: Stepper, DC, and Servo Drivers