India loses nearly 40% of its irrigation water to inefficiency, according to the Central Water Commission. If you have a terrace garden in Bengaluru, a balcony full of pots in Chennai, or a kitchen garden in Pune, you have probably experienced the two extremes: forgetting to water for days during a busy week, then overcompensating by flooding the soil. Plants suffer either way.
A smart irrigation system fixes this permanently. A soil moisture sensor reads the ground, an ESP32 decides whether to water, a relay switches a pump on or off, and you get a Telegram message on your phone. The entire system costs under Rs. 2,000 — roughly what you would spend on two dead plants and their replacements.
This guide walks through every step: hardware, wiring, calibration, code with hysteresis logic, a web dashboard, weather-aware scheduling, multi-zone support, and scaling with LoRa for larger plots.
Why Smart Irrigation Matters in India
Water scarcity is real. Bengaluru, Chennai, Hyderabad, and Delhi regularly face water crises during summer months. Manual watering is inconsistent — you either forget or overwater. Both kill plants and waste water.
Seasonal extremes demand flexibility. Indian gardens face three distinct seasons:
- Monsoon (June-September): Daily rainfall means irrigation should be disabled entirely. Running a pump during monsoon wastes water and drowns roots.
- Summer (March-May): Temperatures cross 40 degrees C in many cities. Soil dries out within hours. Aggressive, frequent watering is essential.
- Winter (November-February): Cooler temperatures mean slower evaporation. Reduced watering keeps roots healthy without waterlogging.
A smart system adapts to all three automatically.
Midday watering is wasteful. Water evaporates fastest between 10 AM and 4 PM. Watering early morning or late evening lets the soil absorb moisture properly. Our system enforces time-based watering windows so the pump never runs during peak heat.
System Overview
The logic flow is straightforward:
Soil Moisture Sensor → ESP32 (ADC Read) → Decision Logic → Relay Module → Water Pump
↓
Web Dashboard + Telegram Alerts
- The capacitive soil moisture sensor outputs an analog voltage proportional to moisture.
- The ESP32 reads this voltage on its ADC, converts it to a percentage.
- Decision logic checks: Is moisture below 30%? Is it within the watering window? Is rain forecast? If all conditions are met, water.
- A 5V relay module switches the 12V pump circuit.
- A web dashboard hosted on the ESP32 itself shows live data and a manual override button.
- Telegram alerts notify you of watering events and anomalies.
Components List
| Component | Purpose | Approx. Price (Rs.) |
|---|---|---|
| ESP32 DevKitC (38-pin) | Main controller, WiFi, web server | 450 |
| Capacitive Soil Moisture Sensor v1.2 | Reads soil moisture (analog) | 120 |
| 5V Single-Channel Relay Module | Switches pump power | 60 |
| 12V DC Submersible Mini Pump | Pushes water through tubing | 180 |
| 12V 2A DC Power Supply | Powers pump and ESP32 (via regulator) | 200 |
| LM7805 Voltage Regulator or Buck Converter | Steps 12V down to 5V for ESP32 | 40 |
| Silicone Tubing (2m) | Water delivery | 80 |
| Jumper Wires (M-M, M-F) | Connections | 50 |
| Breadboard or PCB | Prototyping | 80 |
| Total | Rs. 1,260 |
Optional additions:
| Component | Purpose | Approx. Price (Rs.) |
|---|---|---|
| 12V Solenoid Valve | Precise flow control, multi-zone | 250 |
| YF-S201 Water Flow Sensor | Measure consumption in litres | 180 |
| LoRa SX1278 Module (pair) | Long-range multi-zone without WiFi | 350 each |
| Solar Panel (6W) + Charge Controller | Off-grid terrace/farm operation | 500 |
Even with every optional component, you stay well under Rs. 3,000. Compare this to commercial smart irrigation systems that start at Rs. 8,000 and go up to Rs. 25,000 for multi-zone setups.
Why Capacitive Sensors Beat Resistive Sensors
You will find two types of soil moisture sensors in the market. The cheap resistive type has two exposed metal probes that pass current through the soil. The capacitive type (v1.2) has no exposed metal — it measures moisture through changes in capacitance.
Resistive sensors corrode. The exposed probes undergo electrolysis in wet soil. Within 2-3 weeks of continuous use, the probes develop a green patina, readings drift, and accuracy drops. In Indian monsoon humidity, they can fail in under a week.
Capacitive sensors last months to years. No exposed metal means no corrosion. The v1.2 version has a sealed PCB with a voltage regulator onboard, giving stable readings across a wide voltage range (3.3V-5V).
Always buy the v1.2 version specifically. Earlier versions had exposed components that corroded. The v1.2 has a smooth, coated surface.
Wiring Diagram
ESP32 DevKitC Pinout:
-----------------------------------------
GPIO 34 (ADC1_CH6) ←── Sensor AOUT
GPIO 26 ──→ Relay IN
3.3V ──→ Sensor VCC
GND ──→ Sensor GND, Relay GND
5V (Vin) ──→ Relay VCC
Relay Module:
-----------------------------------------
COM ──→ 12V Power Supply (+)
NO ──→ Pump (+) terminal
Pump (-) ──→ Power Supply (-)
Important: The relay's NO (Normally Open) terminal ensures the pump is OFF when the ESP32 is not actively driving the relay. This is a safety measure — if the ESP32 crashes or loses power, the pump stops.
Calibrating the Soil Moisture Sensor
Calibration is the single most important step. Skip it and your readings will be meaningless.
Step 1: Record the dry air reading. Hold the sensor in open air, not touching anything. Note the ADC value. This is your 0% moisture baseline. Typical value: 3200-3500 on ESP32's 12-bit ADC.
Step 2: Record the water reading. Submerge the sensor (up to the marked line, never above the electronics) in a glass of water. Note the ADC value. This is your 100% moisture baseline. Typical value: 1200-1600.
Step 3: Map to percentage. Notice that the reading goes DOWN as moisture increases. This is normal for capacitive sensors — higher moisture means higher capacitance, which pulls the voltage lower.
// Calibration values — measure YOUR sensor and update these
const int DRY_VALUE = 3400; // ADC reading in open air
const int WET_VALUE = 1400; // ADC reading submerged in water
int readMoisturePercent() {
int raw = analogRead(34);
// Constrain to calibration range
raw = constrain(raw, WET_VALUE, DRY_VALUE);
// Map inversely: low ADC = wet, high ADC = dry
int percent = map(raw, DRY_VALUE, WET_VALUE, 0, 100);
return percent;
}
Run this calibration routine when you first set up the system:
void calibrate() {
Serial.println("=== Calibration Mode ===");
Serial.println("Hold sensor in dry air. Reading in 5 seconds...");
delay(5000);
int drySum = 0;
for (int i = 0; i < 20; i++) {
drySum += analogRead(34);
delay(100);
}
int dryAvg = drySum / 20;
Serial.printf("Dry air average: %d\n", dryAvg);
Serial.println("Now place sensor in water. Reading in 10 seconds...");
delay(10000);
int wetSum = 0;
for (int i = 0; i < 20; i++) {
wetSum += analogRead(34);
delay(100);
}
int wetAvg = wetSum / 20;
Serial.printf("Water average: %d\n", wetAvg);
Serial.printf("\nUpdate your code:\n");
Serial.printf("const int DRY_VALUE = %d;\n", dryAvg);
Serial.printf("const int WET_VALUE = %d;\n", wetAvg);
}
Core Code: Pump Control with Hysteresis
Hysteresis prevents rapid on-off cycling. Without it, when moisture hovers around a single threshold (say 30%), the pump would switch on and off every few seconds — damaging the relay and pump motor.
Our logic: Turn ON the pump when moisture drops below 30%. Keep it running until moisture reaches 60%. Then turn OFF.
This creates a comfortable dead band. The pump runs in longer, healthier cycles instead of frantic toggling.
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>
// --- Pin Configuration ---
#define MOISTURE_PIN 34
#define RELAY_PIN 26
// --- Calibration ---
const int DRY_VALUE = 3400;
const int WET_VALUE = 1400;
// --- Hysteresis Thresholds ---
const int MOISTURE_LOW = 30; // Start watering below this
const int MOISTURE_HIGH = 60; // Stop watering above this
// --- Watering Windows (24h format) ---
// Only water between 6-8 AM and 5-7 PM to avoid midday evaporation
const int WINDOW_1_START = 6;
const int WINDOW_1_END = 8;
const int WINDOW_2_START = 17;
const int WINDOW_2_END = 19;
// --- State ---
bool pumpRunning = false;
bool manualOverride = false;
int currentMoisture = 0;
unsigned long lastWaterTime = 0;
unsigned long pumpStartTime = 0;
unsigned long totalWaterToday = 0; // milliseconds of pump runtime
// --- Season Mode ---
enum Season { MONSOON, SUMMER, WINTER };
Season currentSeason = SUMMER;
// --- WiFi ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
WebServer server(80);
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // Relay module is active LOW; HIGH = OFF
analogReadResolution(12);
analogSetAttenuation(ADC_11db);
// Connect WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nConnected! IP: %s\n", WiFi.localIP().toString().c_str());
// Configure NTP for IST (UTC+5:30)
configTime(19800, 0, "pool.ntp.org", "time.nist.gov");
setupWebServer();
}
void loop() {
server.handleClient();
// Read moisture every 2 seconds
static unsigned long lastRead = 0;
if (millis() - lastRead > 2000) {
lastRead = millis();
currentMoisture = readMoisturePercent();
updatePump();
}
}
int readMoisturePercent() {
// Average 10 readings for stability
long sum = 0;
for (int i = 0; i < 10; i++) {
sum += analogRead(MOISTURE_PIN);
delay(10);
}
int raw = sum / 10;
raw = constrain(raw, WET_VALUE, DRY_VALUE);
return map(raw, DRY_VALUE, WET_VALUE, 0, 100);
}
bool isWateringWindow() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return false;
int hour = timeinfo.tm_hour;
return (hour >= WINDOW_1_START && hour < WINDOW_1_END) ||
(hour >= WINDOW_2_START && hour < WINDOW_2_END);
}
void updatePump() {
// Monsoon mode: never water
if (currentSeason == MONSOON && !manualOverride) {
stopPump();
return;
}
// Adjust thresholds by season
int lowThreshold = MOISTURE_LOW;
int highThreshold = MOISTURE_HIGH;
if (currentSeason == SUMMER) {
lowThreshold = 35; // More aggressive in summer
highThreshold = 65;
} else if (currentSeason == WINTER) {
lowThreshold = 25; // Less aggressive in winter
highThreshold = 55;
}
// Manual override bypasses all logic
if (manualOverride) {
startPump();
return;
}
// Time window check
if (!isWateringWindow()) {
if (pumpRunning) stopPump();
return;
}
// Hysteresis logic
if (!pumpRunning && currentMoisture < lowThreshold) {
startPump();
} else if (pumpRunning && currentMoisture >= highThreshold) {
stopPump();
}
// Safety: stop pump if it has been running more than 10 minutes
if (pumpRunning && (millis() - pumpStartTime > 600000)) {
stopPump();
sendTelegramAlert("WARNING: Pump ran for 10+ minutes. Stopped automatically. Check for leaks.");
}
}
void startPump() {
if (!pumpRunning) {
digitalWrite(RELAY_PIN, LOW); // Active LOW relay
pumpRunning = true;
pumpStartTime = millis();
lastWaterTime = millis();
Serial.println("Pump ON");
sendTelegramAlert("Watering started. Soil moisture: " + String(currentMoisture) + "%");
}
}
void stopPump() {
if (pumpRunning) {
digitalWrite(RELAY_PIN, HIGH);
unsigned long runtime = millis() - pumpStartTime;
totalWaterToday += runtime;
pumpRunning = false;
manualOverride = false;
Serial.printf("Pump OFF. Ran for %lu seconds\n", runtime / 1000);
}
}
Telegram Alerts
Telegram bots are free, require no server, and work on every phone. Create a bot through @BotFather on Telegram, get your bot token and chat ID.
#include <HTTPClient.h>
const char* telegramToken = "YOUR_BOT_TOKEN";
const char* chatId = "YOUR_CHAT_ID";
void sendTelegramAlert(String message) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
String url = "https://api.telegram.org/bot" + String(telegramToken)
+ "/sendMessage?chat_id=" + String(chatId)
+ "&text=" + urlEncode(message);
http.begin(url);
int httpCode = http.GET();
if (httpCode != 200) {
Serial.printf("Telegram send failed: %d\n", httpCode);
}
http.end();
}
String urlEncode(String str) {
String encoded = "";
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
encoded += c;
} else {
encoded += "%" + String(c < 16 ? "0" : "") + String(c, HEX);
}
}
return encoded;
}
Alert scenarios to implement:
- "Watering started. Soil moisture: 28%"
- "Watering complete. Duration: 3 min 42 sec"
- "ALERT: Soil critically dry (12%). Check sensor and water supply."
- "WARNING: Pump ran for 10+ minutes. Possible leak or empty tank."
- "Rain forecast for today. Skipping scheduled watering."
Web Dashboard
The ESP32 hosts a web server directly. No external hosting needed — just open the ESP32's IP address in your browser.
void setupWebServer() {
server.on("/", handleDashboard);
server.on("/api/status", handleApiStatus);
server.on("/api/override", handleManualOverride);
server.on("/api/season", handleSetSeason);
server.begin();
}
void handleApiStatus() {
String json = "{";
json += "\"moisture\":" + String(currentMoisture) + ",";
json += "\"pump\":" + String(pumpRunning ? "true" : "false") + ",";
json += "\"override\":" + String(manualOverride ? "true" : "false") + ",";
json += "\"season\":\"" + getSeasonName() + "\",";
json += "\"lastWater\":" + String((millis() - lastWaterTime) / 60000) + ",";
json += "\"totalToday\":" + String(totalWaterToday / 1000);
json += "}";
server.send(200, "application/json", json);
}
void handleManualOverride() {
manualOverride = !manualOverride;
if (!manualOverride && pumpRunning) stopPump();
server.send(200, "text/plain", manualOverride ? "Override ON" : "Override OFF");
}
void handleSetSeason() {
String s = server.arg("mode");
if (s == "monsoon") currentSeason = MONSOON;
else if (s == "summer") currentSeason = SUMMER;
else if (s == "winter") currentSeason = WINTER;
server.send(200, "text/plain", "Season set to: " + s);
}
String getSeasonName() {
switch (currentSeason) {
case MONSOON: return "Monsoon";
case SUMMER: return "Summer";
case WINTER: return "Winter";
default: return "Unknown";
}
}
void handleDashboard() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Garden Monitor</title>
<style>
body { font-family: -apple-system, sans-serif; background: #0F1A2E; color: #f1f5f9; margin: 0; padding: 16px; }
.card { background: #1a2744; border-radius: 12px; padding: 20px; margin-bottom: 12px; }
.value { font-size: 2.5rem; font-weight: 700; color: #FF6B35; }
.label { font-size: 0.85rem; color: rgba(255,255,255,0.5); text-transform: uppercase; letter-spacing: 0.1em; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.btn { background: #FF6B35; color: white; border: none; padding: 14px 24px; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; width: 100%; }
.btn:active { opacity: 0.8; }
.status-on { color: #10B981; }
.status-off { color: rgba(255,255,255,0.4); }
h1 { font-size: 1.5rem; margin: 0 0 16px 0; }
</style>
</head>
<body>
<h1>Garden Irrigation</h1>
<div class="grid">
<div class="card">
<div class="label">Soil Moisture</div>
<div class="value" id="moisture">--</div>
</div>
<div class="card">
<div class="label">Pump Status</div>
<div class="value" id="pump">--</div>
</div>
</div>
<div class="card">
<div class="label">Last Watered</div>
<div id="lastWater" style="font-size:1.2rem;margin-top:4px;">--</div>
</div>
<div class="card">
<div class="label">Season Mode</div>
<div id="season" style="font-size:1.2rem;margin-top:4px;">--</div>
</div>
<div style="margin-top:16px;">
<button class="btn" onclick="toggleOverride()">Manual Override</button>
</div>
<script>
async function refresh() {
try {
const r = await fetch('/api/status');
const d = await r.json();
document.getElementById('moisture').textContent = d.moisture + '%';
document.getElementById('pump').textContent = d.pump ? 'ON' : 'OFF';
document.getElementById('pump').className = d.pump ? 'value status-on' : 'value status-off';
document.getElementById('lastWater').textContent = d.lastWater + ' min ago';
document.getElementById('season').textContent = d.season + ' Mode';
} catch(e) { console.error(e); }
}
async function toggleOverride() { await fetch('/api/override'); refresh(); }
refresh();
setInterval(refresh, 3000);
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
The dashboard auto-refreshes every 3 seconds, shows moisture percentage with a large readable number, pump on/off status, time since last watering, current season mode, and a manual override button for when you want to water outside the scheduled windows.
Weather-Aware Watering
Why water if it is going to rain in 3 hours? The OpenWeatherMap free tier gives you 1,000 API calls per day — more than enough for checking every 30 minutes.
#include <ArduinoJson.h>
const char* weatherApiKey = "YOUR_OPENWEATHERMAP_KEY";
// Bengaluru coordinates; change for your city
const float latitude = 12.9716;
const float longitude = 77.5946;
bool rainForecast = false;
void checkWeather() {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
String url = "https://api.openweathermap.org/data/2.5/forecast?lat="
+ String(latitude, 4) + "&lon=" + String(longitude, 4)
+ "&cnt=4&appid=" + String(weatherApiKey);
http.begin(url);
int httpCode = http.GET();
if (httpCode == 200) {
String payload = http.getString();
DynamicJsonDocument doc(4096);
deserializeJson(doc, payload);
rainForecast = false;
JsonArray list = doc["list"];
for (JsonObject entry : list) {
int weatherId = entry["weather"][0]["id"];
// Weather IDs 200-622 cover all rain, drizzle, thunderstorm, snow
if (weatherId >= 200 && weatherId < 700) {
rainForecast = true;
sendTelegramAlert("Rain forecast detected. Skipping scheduled watering.");
break;
}
}
}
http.end();
}
Add a rainForecast check in your updatePump() function:
// Inside updatePump(), before the hysteresis logic:
if (rainForecast && !manualOverride) {
if (pumpRunning) stopPump();
return;
}
Check weather every 30 minutes by adding a timer in your loop().
Multi-Zone Support
Most Indian terrace gardens have separate areas — a row of pots along the railing, a raised bed in the corner, a vertical garden on the wall. Each zone has different soil, different sun exposure, and different water needs.
Hardware for multi-zone:
- Use a 4-channel relay module (Rs. 150) instead of a single channel
- Add one capacitive moisture sensor per zone
- Use ESP32 ADC pins: GPIO 34, 35, 32, 33 (all on ADC1; avoid ADC2 pins as they conflict with WiFi)
struct Zone {
const char* name;
int sensorPin;
int relayPin;
int moisture;
bool pumpOn;
int lowThreshold;
int highThreshold;
};
Zone zones[] = {
{"Terrace Pots", 34, 26, 0, false, 30, 60},
{"Raised Bed", 35, 27, 0, false, 25, 55},
{"Vertical Garden", 32, 14, 0, false, 35, 65},
};
const int NUM_ZONES = 3;
void updateAllZones() {
for (int i = 0; i < NUM_ZONES; i++) {
zones[i].moisture = readMoisturePin(zones[i].sensorPin);
if (!zones[i].pumpOn && zones[i].moisture < zones[i].lowThreshold) {
digitalWrite(zones[i].relayPin, LOW);
zones[i].pumpOn = true;
} else if (zones[i].pumpOn && zones[i].moisture >= zones[i].highThreshold) {
digitalWrite(zones[i].relayPin, HIGH);
zones[i].pumpOn = false;
}
}
}
Each zone gets its own thresholds. Vertical gardens dry out faster and need more aggressive watering (lower threshold). Raised beds retain moisture longer and need gentler treatment.
Water Flow Measurement
The YF-S201 flow sensor (Rs. 180) uses a Hall effect sensor inside a small turbine. As water flows through, it generates pulses. Each pulse corresponds to approximately 2.25 mL.
#define FLOW_PIN 25
volatile int pulseCount = 0;
float totalLitres = 0;
void IRAM_ATTR flowPulseISR() {
pulseCount++;
}
void setupFlowSensor() {
pinMode(FLOW_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FLOW_PIN), flowPulseISR, RISING);
}
void calculateFlow() {
// Called every second
noInterrupts();
int pulses = pulseCount;
pulseCount = 0;
interrupts();
// YF-S201: ~450 pulses per litre
float litres = pulses / 450.0;
totalLitres += litres;
}
Tracking water consumption over weeks reveals patterns. You might discover your raised bed uses 3x more water than the pots, prompting you to add mulch or shade cloth. Data drives better gardening decisions.
Seasonal Adjustment
Instead of manually changing thresholds every few months, automate it based on the month:
void autoDetectSeason() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return;
int month = timeinfo.tm_mon + 1; // tm_mon is 0-indexed
if (month >= 6 && month <= 9) {
currentSeason = MONSOON; // June-September
} else if (month >= 3 && month <= 5) {
currentSeason = SUMMER; // March-May
} else {
currentSeason = WINTER; // October-February
}
}
Monsoon mode disables all automatic watering. The weather check still runs, so if there is an unusual dry spell during monsoon, you can use the manual override from the dashboard.
Summer mode raises the low threshold to 35% and checks more frequently. It also enables an emergency watering override: if moisture drops below 15% at any time of day (even outside watering windows), the pump kicks in to save your plants.
Winter mode lowers the low threshold to 25% and extends the time between checks. Overwatering in winter causes root rot, a common killer of Indian terrace garden plants.
Data Logging
The ESP32 has limited memory, but you can log daily summaries to SPIFFS (the onboard flash filesystem):
#include <SPIFFS.h>
void logDailySummary() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return;
char filename[32];
strftime(filename, sizeof(filename), "/log_%Y%m.csv", &timeinfo);
File f = SPIFFS.open(filename, FILE_APPEND);
if (!f) return;
char line[128];
snprintf(line, sizeof(line), "%04d-%02d-%02d,%d,%lu,%.2f\n",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
currentMoisture,
totalWaterToday / 1000, // seconds of pump runtime
totalLitres
);
f.print(line);
f.close();
// Reset daily counters
totalWaterToday = 0;
totalLitres = 0;
}
Add a /api/logs endpoint to download the CSV through the web dashboard. Import it into Google Sheets for charts and trend analysis.
Scaling with LoRa for Larger Plots
WiFi works perfectly for a terrace garden or balcony. But if you have a farm plot or community garden spread across hundreds of metres, WiFi will not reach. This is where LoRa shines.
LoRa (Long Range) communication reaches 2-5 km in open areas using minimal power. The SX1278 LoRa module costs around Rs. 350 per unit and connects to ESP32 via SPI.
Architecture for LoRa-based multi-zone:
- Remote nodes: ESP32 + soil sensor + relay + pump + LoRa transmitter. Each node is self-contained and makes local watering decisions.
- Base station: ESP32 + LoRa receiver + WiFi. Collects data from all remote nodes and hosts the unified web dashboard.
Remote nodes send periodic updates (moisture level, pump status, water used) to the base station via LoRa. The base station aggregates this data and pushes Telegram alerts. If a remote node loses communication, the base station sends a "Node offline" alert.
This architecture works beautifully for small farms in rural India where WiFi infrastructure does not exist but mobile connectivity (for Telegram) is available at one central point.
Indian Garden Considerations
Terrace gardens (Bengaluru, Chennai, Hyderabad): Most common setup. Place the ESP32 and relay in a waterproof enclosure under a covered area. Run tubing along the parapet. Use drip nozzles (Rs. 5 each) at each pot for efficient water delivery. A single 12V pump handles 20-30 pots easily.
Balcony pots (Mumbai, Delhi apartments): Space is tight. Use a small 5L bucket as a water reservoir under a table. The submersible pump sits inside the bucket. Keep the ESP32 indoors near a power socket and run the sensor wire out to the balcony through the window gap.
Kitchen gardens (backyard plots): Larger area means more tubing and possibly multiple zones. A solenoid valve per zone (Rs. 250 each) gives better control than individual pumps. One main pump feeds a manifold that splits into zone-controlled lines.
Farm plots (rural/peri-urban): LoRa-based multi-node system. Solar-powered remote nodes with large 12V batteries. Drip irrigation lines. Water flow sensors on each line to detect blockages (common with bore well water in Karnataka and Tamil Nadu due to mineral deposits).
Power Options
Mains powered (most setups): A 12V 2A adapter powers the pump directly and the ESP32 through a buck converter. Stable, reliable, always-on. Best for terrace and balcony setups.
Solar powered (terrace/farm): A 6W solar panel (Rs. 350-500), charge controller (Rs. 150), and 12V 7Ah lead-acid battery (Rs. 500) gives you a fully off-grid system. The battery alone can run the ESP32 for 3-4 days without sun. The pump only runs for minutes per day, so power consumption is minimal. This is essential for farm plots without nearby power outlets.
Cost Breakdown
Basic system (single zone, no frills):
| Item | Cost |
|---|---|
| ESP32 DevKitC | Rs. 450 |
| Capacitive Soil Sensor v1.2 | Rs. 120 |
| 5V Relay Module | Rs. 60 |
| 12V Submersible Pump | Rs. 180 |
| 12V 2A Power Supply | Rs. 200 |
| Tubing + Connectors | Rs. 100 |
| Jumper Wires + Breadboard | Rs. 100 |
| Total | Rs. 1,210 |
Full-featured system (3 zones, flow sensor, weather-aware):
| Item | Cost |
|---|---|
| ESP32 DevKitC | Rs. 450 |
| 3x Capacitive Soil Sensors | Rs. 360 |
| 4-Channel Relay Module | Rs. 150 |
| 12V Submersible Pump | Rs. 180 |
| 2x Solenoid Valves | Rs. 500 |
| Water Flow Sensor | Rs. 180 |
| 12V 2A Power Supply | Rs. 200 |
| Tubing + Connectors + Drip Nozzles | Rs. 250 |
| Waterproof Enclosure | Rs. 150 |
| Total | Rs. 2,420 |
A comparable commercial system with app control and multi-zone support starts at Rs. 8,000 and goes up to Rs. 25,000. You save 60-90% while getting a system you fully understand and can customize.
Troubleshooting Common Issues
Sensor reads 0% or 100% constantly: Check wiring. Ensure the sensor is connected to an ADC1 pin (GPIO 32-39), not ADC2 (GPIO 0-15) which conflicts with WiFi.
Pump chatters (rapid on-off): Your hysteresis band is too narrow. Increase the gap between low and high thresholds. A 30-point gap (30% to 60%) works well.
WiFi disconnects frequently: Indian routers sometimes drop connections under load. Add a reconnection check in your loop:
if (WiFi.status() != WL_CONNECTED) {
WiFi.reconnect();
delay(5000);
}
Moisture reading drifts over weeks: Soil salt buildup affects capacitive readings. Pull the sensor out monthly, wipe it clean, and recalibrate if needed.
Pump does not start: Verify relay wiring. Most relay modules are active LOW — you need digitalWrite(pin, LOW) to activate. Check the pump's power supply with a multimeter; 12V pumps will not run on a 5V USB supply.
What to Build Next
Once your basic system is running, consider these enhancements:
- OLED display (Rs. 200) mounted near the garden showing live moisture and pump status
- Temperature and humidity sensor (DHT22, Rs. 180) to factor ambient conditions into watering decisions
- MQTT integration with Home Assistant for unified smart home control
- OTA updates so you can push new firmware over WiFi without physically accessing the ESP32
- Mobile app using ESP32's BLE to control the system when WiFi is unavailable
The ESP32's dual-core processor, WiFi, Bluetooth, and generous GPIO make it the ideal brain for garden automation. Start with a single sensor and pump, get comfortable with the code, then expand zone by zone. Your plants — and your water bill — will thank you.
All components mentioned in this guide are available at wavtron.in. We ship across India from Bengaluru with same-day dispatch for orders placed before 2 PM.



