Smart Garden Irrigation System - Complete DIY Guide

Automatic irrigation saves water, time and ensures healthy plants even during vacation. In this detailed guide, I'll show you how to build a smart irrigation system that responds to weather, soil moisture and can be controlled from anywhere.
Why Smart Irrigation?
Traditional irrigation systems run on timers regardless of actual need. A smart system:
- Measures soil moisture - waters only when needed
- Monitors weather - skips irrigation before rain
- Different zones - each garden area according to need
- Remote control - monitoring from anywhere
- Statistics - track water consumption
System Components
Basic Hardware
1. Control Unit
- ESP32/ESP8266 ($8-16) - WiFi microcontroller
- Raspberry Pi ($60-100) - more advanced option
- Arduino + WiFi shield ($32-48) - classic choice
2. Valves and Piping
Solenoid valves:
- 24V AC valves ($32-60/piece)
- 12V DC valves ($24-40/piece)
- Diameter by flow rate (3/4" or 1")
Piping:
- PE hose 16-25mm
- Connectors, elbows, T-joints
- Drippers or sprinklers
3. Power Supply
- 24V AC transformer ($20-32)
- Or 12V DC supply + relay board
4. Sensors
- Soil moisture ($6-12/piece)
- Rain sensor ($8-16)
- Water flow ($20-40)
- Temperature/humidity ($8-16)
Example Budget
Small garden (4 zones):
ESP32: $12
4x 12V valve: $96
8-channel relay module: $8
4x moisture sensor: $32
Power supply: $16
Box, cables: $20
Piping and fittings: $60
Total: ~$244
Medium garden (8 zones):
Raspberry Pi: $80
8x 24V valve: $320
Expander + relays: $32
8x sensors: $64
24V transformer: $32
Flow meter: $32
Installation materials: $80
Total: ~$640
System Planning
1. Zone Division
By plant type:
- Zone 1: Lawn (daily short)
- Zone 2: Flower beds (2-3x weekly longer)
- Zone 3: Shrubs (1x weekly heavy)
- Zone 4: Greenhouse (frequent light)
By sun exposure:
- South side - more frequent watering
- North/shade - less frequent
2. Flow Calculation
Example calculation:
- Water pressure: 3 bars
- Faucet flow: 15 l/min
- Sprinkler: 2 l/min
- Max sprinklers/zone: 7
Drippers:
- Dripper: 2-4 l/hour
- Per zone: up to 50 drippers
3. Water Distribution
Main supply piping:
- PE 25-32mm or 1"
- Bury 30-40cm (frost protection)
- Slope for drainage
Distribution to plants:
- PE 16-20mm
- Or micro tubing 4-6mm
- Stake anchoring
Electronics Assembly
ESP32 Solution (Recommended)
Required Components:
- ESP32 DevKit
- 8-channel 5V relay
- 5V/3A power supply
- Terminal blocks
- IP65 enclosure
- Connection cables
Wiring:
ESP32 -> Relay module:
GPIO23 -> IN1 (Zone 1)
GPIO22 -> IN2 (Zone 2)
GPIO21 -> IN3 (Zone 3)
GPIO19 -> IN4 (Zone 4)
VIN -> VCC
GND -> GND
Moisture sensors:
GPIO34 -> Sensor 1 (analog)
GPIO35 -> Sensor 2 (analog)
GPIO32 -> Sensor 3 (analog)
GPIO33 -> Sensor 4 (analog)
Valves:
Relay NO -> Valve (+)
12V supply -> Valve (-)
ESP32 Code
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
// WiFi credentials
const char* ssid = "YourWiFi";
const char* password = "YourPassword";
// Pins
const int zone_pins[] = {23, 22, 21, 19};
const int moisture_pins[] = {34, 35, 32, 33};
const int num_zones = 4;
// Settings
int moisture_threshold[] = {30, 40, 35, 45}; // %
int watering_duration[] = {300, 600, 900, 300}; // seconds
WebServer server(80);
void setup() {
Serial.begin(115200);
// Setup pins
for (int i = 0; i < num_zones; i++) {
pinMode(zone_pins[i], OUTPUT);
digitalWrite(zone_pins[i], LOW);
}
// WiFi connection
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected! IP: " + WiFi.localIP().toString());
// Web server endpoints
server.on("/", handleRoot);
server.on("/status", handleStatus);
server.on("/water", handleWater);
server.on("/settings", handleSettings);
server.begin();
}
void loop() {
server.handleClient();
// Check moisture every 30 minutes
static unsigned long lastCheck = 0;
if (millis() - lastCheck > 1800000) { // 30 min
checkMoistureAndWater();
lastCheck = millis();
}
}
void checkMoistureAndWater() {
for (int i = 0; i < num_zones; i++) {
int moisture = readMoisture(i);
if (moisture < moisture_threshold[i]) {
Serial.println("Zone " + String(i+1) + " dry (" +
String(moisture) + "%) - watering");
waterZone(i, watering_duration[i]);
}
}
}
int readMoisture(int zone) {
int raw = analogRead(moisture_pins[zone]);
// Convert to percentage (calibrate per sensor)
// Dry = 4095, Wet = 1000
int percent = map(raw, 4095, 1000, 0, 100);
return constrain(percent, 0, 100);
}
void waterZone(int zone, int duration) {
digitalWrite(zone_pins[zone], HIGH);
delay(duration * 1000);
digitalWrite(zone_pins[zone], LOW);
}
void handleRoot() {
String html = R"(
<!DOCTYPE html>
<html>
<head>
<title>Smart Irrigation</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<style>
body { font-family: Arial; margin: 20px; }
.zone { border: 1px solid #ddd; padding: 15px; margin: 10px 0; }
button { padding: 10px 20px; margin: 5px; }
.moisture { font-weight: bold; }
</style>
</head>
<body>
<h1>Smart Irrigation</h1>
<div id='zones'></div>
<script>
function updateStatus() {
fetch('/status')
.then(r => r.json())
.then(data => {
let html = '';
data.zones.forEach((zone, i) => {
html += `
<div class='zone'>
<h3>Zone ${i+1}</h3>
<p>Moisture: <span class='moisture'>${zone.moisture}%</span></p>
<p>Threshold: ${zone.threshold}%</p>
<p>Status: ${zone.active ? 'Watering' : 'Off'}</p>
<button onclick='waterZone(${i})'>Water (${zone.duration}s)</button>
</div>
`;
});
document.getElementById('zones').innerHTML = html;
});
}
function waterZone(zone) {
fetch(`/water?zone=${zone}`)
.then(() => updateStatus());
}
setInterval(updateStatus, 5000);
updateStatus();
</script>
</body>
</html>
)";
server.send(200, "text/html", html);
}
void handleStatus() {
StaticJsonDocument<512> doc;
JsonArray zones = doc.createNestedArray("zones");
for (int i = 0; i < num_zones; i++) {
JsonObject zone = zones.createNestedObject();
zone["moisture"] = readMoisture(i);
zone["threshold"] = moisture_threshold[i];
zone["duration"] = watering_duration[i];
zone["active"] = digitalRead(zone_pins[i]);
}
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
void handleWater() {
if (server.hasArg("zone")) {
int zone = server.arg("zone").toInt();
if (zone >= 0 && zone < num_zones) {
waterZone(zone, watering_duration[zone]);
server.send(200, "text/plain", "OK");
return;
}
}
server.send(400, "text/plain", "Bad Request");
}
Weather Integration
OpenWeatherMap API
#include <HTTPClient.h>
const char* api_key = "YOUR_API_KEY";
const char* city = "Prague,CZ";
bool willRainSoon() {
HTTPClient http;
String url = "http://api.openweathermap.org/data/2.5/forecast?q=" +
String(city) + "&appid=" + String(api_key);
http.begin(url);
int httpCode = http.GET();
if (httpCode == 200) {
String payload = http.getString();
StaticJsonDocument<2048> doc;
deserializeJson(doc, payload);
// Check forecast for 6 hours
for (int i = 0; i < 2; i++) {
String weather = doc["list"][i]["weather"][0]["main"];
if (weather == "Rain") {
return true;
}
}
}
http.end();
return false;
}
// Add to checkMoistureAndWater:
if (willRainSoon()) {
Serial.println("Rain expected, skipping irrigation");
return;
}
Home Assistant Integration
ESPHome Configuration
esphome:
name: smart-irrigation
platform: ESP32
board: esp32dev
wifi:
ssid: "YourWiFi"
password: "YourPassword"
api:
password: "api-password"
ota:
password: "ota-password"
# Moisture sensors
sensor:
- platform: adc
pin: GPIO34
name: "Zone 1 Moisture"
unit_of_measurement: "%"
filters:
- calibrate_linear:
- 2.8 -> 0.0
- 1.1 -> 100.0
update_interval: 30min
- platform: adc
pin: GPIO35
name: "Zone 2 Moisture"
# ... similar for other zones
# Valve controls
switch:
- platform: gpio
pin: GPIO23
name: "Irrigation Zone 1"
id: zone1
icon: "mdi:water"
- platform: gpio
pin: GPIO22
name: "Irrigation Zone 2"
id: zone2
icon: "mdi:water"
# ... additional zones
# Automation
interval:
- interval: 30min
then:
- if:
condition:
sensor.in_range:
id: moisture_zone1
below: 30.0
then:
- switch.turn_on: zone1
- delay: 5min
- switch.turn_off: zone1
Home Assistant Automation
automation:
- alias: "Morning watering"
trigger:
- platform: time
at: "06:00:00"
condition:
- condition: numeric_state
entity_id: sensor.moisture_zone1
below: 40
- condition: numeric_state
entity_id: weather.home
attribute: precipitation_probability
below: 30
action:
- service: switch.turn_on
entity_id: switch.irrigation_zone_1
- delay: "00:05:00"
- service: switch.turn_off
entity_id: switch.irrigation_zone_1
- alias: "Low moisture alert"
trigger:
- platform: numeric_state
entity_id: sensor.moisture_zone2
below: 25
for: "01:00:00"
action:
- service: notify.mobile_app_phone
data:
title: "Garden is dry!"
message: "Zone 2 moisture is only {{ states('sensor.moisture_zone2') }}%"
Mechanical Installation
Valve Installation
Valve box placement
- Plastic box 30x40cm
- Drainage at bottom (gravel)
- Access for maintenance
Valve connections
Main supply → Main shutoff → Filter → → Manifold → Valve 1 → Zone 1 → Valve 2 → Zone 2 → etc.
Electrical connections
- CYKY 3x1.5 cable in conduit
- Waterproof connectors
- Cable labeling
Water Distribution
Main branches:
- Dig 30-40cm deep
- Sand bed
- Place PE pipe
- Cover with sand
- Warning tape
- Backfill
Drip lines:
Dripper spacing:
- Vegetables: 30cm
- Shrubs: 50-100cm
- Trees: around trunk
Anchoring:
- Plastic stakes every 1m
- Loop at drippers
Calibration and Tuning
Moisture Sensor Calibration
- Dry soil: Measure value
- Watered soil: Wait 30 min, measure
- Ideal moisture: Per plant type (30-60%)
Watering Time Optimization
Testing:
1. Water zone for 5 minutes
2. Wait 1 hour
3. Check penetration depth
4. Adjust time as needed
Target depth:
- Lawn: 10-15cm
- Beds: 20-30cm
- Shrubs/trees: 40-60cm
System Maintenance
Seasonal Maintenance
Spring:
- Check joint tightness
- Clean filters
- Test all zones
- Calibrate sensors
Fall:
- Drain system
- Blow out with air
- Remove valves (or use glycerin)
- Winterize electronics
Regular Maintenance
- Weekly: Visual inspection
- Monthly: Clean drippers
- Seasonal: Replace filters
- Yearly: Check wiring
Advanced Features
Water Usage Monitoring
// Flow meter on GPIO25
volatile int pulseCount = 0;
float flowRate = 0;
float totalLiters = 0;
void IRAM_ATTR pulseCounter() {
pulseCount++;
}
void setupFlowMeter() {
pinMode(25, INPUT_PULLUP);
attachInterrupt(25, pulseCounter, FALLING);
}
void calculateFlow() {
// YF-S201: 450 pulses = 1 liter
flowRate = pulseCount / 450.0 * 60; // L/min
totalLiters += pulseCount / 450.0;
pulseCount = 0;
}
Leak Detection
automation:
- alias: "Water leak detection"
trigger:
- platform: numeric_state
entity_id: sensor.flow_rate
above: 0.5
for: "00:30:00"
condition:
- condition: state
entity_id: group.all_zones
state: "off"
action:
- service: notify.mobile_app_phone
data:
title: "WARNING - Water leak!"
message: "Flow detected with no active zone!"
Troubleshooting
Valve won't open
- Check voltage (multimeter)
- Manual valve test
- Clean diaphragm
- Check solenoid coil
Uneven watering
- Check water pressure
- Clean clogged nozzles
- Add pressure regulator
- Split into more zones
Moisture sensor not working
- Clean electrodes
- Check depth
- Replace with capacitive type
- Add corrosion protection
Price Comparison with Commercial Systems
Solution | Price | Features | Maintenance |
---|---|---|---|
DIY ESP32 | $240-600 | Full control | Self |
Gardena Smart | $800-1600 | Limited | Medium |
Rain Bird | $1200-2400 | Professional | Service |
Hunter Hydrawise | $1000-2000 | Cloud | Service |
Conclusion
Building your own smart irrigation system isn't complex and provides full control over your garden. Start simple with a few zones and gradually expand. The investment pays back in water savings and healthier plants.
Tip: Start with one test zone and once you verify functionality, expand to the whole garden.