An evapotranspiration sensor network for water-efficient farming measures and calculates the actual water loss from soil evaporation and plant transpiration — enabling precise irrigation scheduling that replaces only what is lost, nothing more. ET-based irrigation is the gold standard for water efficiency, used by advanced fruit and vegetable growers worldwide. This guide covers building a distributed ET sensor network with ESP32, BME280, pyranometer, and automated irrigation control for Indian farms.
Table of Contents
- What is Evapotranspiration and Why It Matters
- ET Calculation Methods
- Sensor Network Design
- Components Required
- ESP32 ET Station Code
- Irrigation Scheduling from ET Data
- Indian Agriculture ET Reference Values
- Frequently Asked Questions
What is Evapotranspiration and Why It Matters
Evapotranspiration (ET) is the combined process of water evaporation from soil surfaces and transpiration from plant leaves. It represents the total water demand of the field:
- Reference ET (ET0): Water demand of a standardised grass reference surface (mm/day)
- Crop ET (ETc): Actual crop water demand = ET0 x Crop Coefficient (Kc)
- Actual ET: What actually occurred, limited by available soil water
Irrigating based on ET replaces only what the crop used, preventing both water waste and crop stress. In Indian conditions, ET-based scheduling vs calendar scheduling reduces water use by 30-45% for most crops — critical with declining groundwater in Punjab, Haryana, and Rajasthan.
ET Calculation Methods
Four methods, from simple to most accurate:
- Blaney-Criddle: Uses temperature and daylight hours only. Simple but less accurate in humid regions.
- Hargreaves-Samani: Temperature + solar radiation. Good accuracy with basic sensors.
- Priestley-Taylor: Temperature + net radiation. Better for humid climates.
- FAO Penman-Monteith: Full method using temperature, humidity, wind, and radiation. Most accurate but requires more sensors.
For DIY farm stations, the Hargreaves-Samani method provides good accuracy with just temperature (BME280) and solar radiation (pyranometer or BH1750 as proxy). For research-grade accuracy, use the full Penman-Monteith with wind speed and net radiation sensors.
Sensor Network Design
Each ET station measures:
- Air temperature (max and min): BME280 in a Stevenson screen (radiation-shielded housing)
- Relative humidity: BME280
- Solar radiation: BH1750 light sensor as proxy, or ML8511 UV sensor + calibration
- Wind speed: Hall-effect anemometer (optional for Hargreaves method, required for PM method)
- Rainfall: Tipping bucket rain gauge (RG-11 or DIY with Hall sensor)
Network topology: 1-3 ET stations per 50 acres, connected via WiFi or LoRa to central Raspberry Pi server running ET calculations and irrigation scheduling software.
Components Required
Weather Sensors from Zbotic
- GY-BME280 3.3V Temperature, Humidity, Pressure Sensor — core ET station sensor
- GY-BME280 5V variant — for 5V Arduino systems
- Raindrops Detection Sensor Module — rainfall detection for ET correction
- Capacitive Soil Moisture Sensor — for actual soil water balance verification
Per ET station:
- ESP32 WROOM-32
- BME280 in Stevenson screen (white louvred radiation shield)
- BH1750 light intensity sensor (for solar radiation proxy)
- Reed switch + magnet anemometer (wind speed measurement)
- Tipping bucket rain gauge (1 tip = 0.2mm rain)
- DS3231 RTC
- Solar power: 5W panel + 5000mAh LiPo
- LoRa SX1278 for data transmission to gateway
ESP32 ET Station Code
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <BH1750.h>
#include <LoRa.h>
#include <SPI.h>
#include <math.h>
Adafruit_BME280 bme;
BH1750 lightMeter;
#define LORA_SS 5
#define LORA_RST 14
#define LORA_DIO0 2
#define RAIN_PIN 13 // Tipping bucket (interrupt)
#define WIND_PIN 12 // Anemometer reed switch
volatile int rainTips = 0; // Rain gauge tipping bucket count
volatile int windPulses = 0; // Wind speed pulse count
unsigned long windCheckStart = 0;
void IRAM_ATTR onRainTip() { rainTips++; }
void IRAM_ATTR onWindPulse() { windPulses++; }
float hargreaves_et0(float tmax, float tmin, float ra_mj_m2_day) {
float tmean = (tmax + tmin) / 2.0;
return 0.0023 * (tmean + 17.8) * sqrt(tmax - tmin) * ra_mj_m2_day;
}
float extraterrestrial_radiation(float lat_deg, int doy) {
float lat = lat_deg * M_PI / 180.0;
float dr = 1 + 0.033 * cos(2 * M_PI * doy / 365.0);
float delta = 0.409 * sin(2 * M_PI * doy / 365.0 - 1.39);
float ws = acos(-tan(lat) * tan(delta));
return (24.0 * 60.0 / M_PI) * 0.0820 * dr * (
ws * sin(lat) * sin(delta) +
cos(lat) * cos(delta) * sin(ws)
);
}
struct DailyData {
float tmax, tmin, tmean, humid;
float lightLux; // Proxy for solar radiation
float windSpeed; // m/s
float rainfallMm;
};
DailyData daily = {0, 99, 0, 0, 0, 0, 0}; // Init tmin high so any reading sets it
RTC_DATA_ATTR int bootCount = 0;
void setup() {
Serial.begin(115200);
bootCount++;
Wire.begin(21, 22);
bme.begin(0x76);
lightMeter.begin();
pinMode(RAIN_PIN, INPUT_PULLUP);
pinMode(WIND_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(RAIN_PIN), onRainTip, FALLING);
attachInterrupt(digitalPinToInterrupt(WIND_PIN), onWindPulse, RISING);
// Read current values
float temp = bme.readTemperature();
float humid = bme.readHumidity();
float lux = lightMeter.readLightLevel();
// Update daily min/max (using RTC_DATA_ATTR to persist across sleep cycles)
if (temp > daily.tmax) daily.tmax = temp;
if (temp < daily.tmin) daily.tmin = temp;
daily.tmean = (daily.tmax + daily.tmin) / 2.0;
daily.humid = humid;
daily.lightLux = lux;
// Wind speed: count pulses over 5 seconds
windCheckStart = millis();
windPulses = 0;
delay(5000);
detachInterrupt(digitalPinToInterrupt(WIND_PIN));
float windHz = windPulses / 5.0;
daily.windSpeed = windHz * 2.4; // Anemometer calibration: 1Hz = 2.4 km/h; convert to m/s
daily.windSpeed /= 3.6;
// At end of day (23:55), calculate daily ET and transmit
// For simplicity, calculate ET each cycle with accumulated data
int doy = 91; // Day of year - replace with RTC day-of-year calculation
float lat = 20.5; // Farm latitude
float ra = extraterrestrial_radiation(lat, doy);
// Convert lux to MJ/m2/day (rough: 1 MJ/m2/day ~= 27,778 lux-hours)
float ra_from_sensor = daily.lightLux * 0.0864 / 1000.0 / 27778.0 * 24; // Very rough!
float ra_mj = min(ra, max(ra * 0.3, ra_from_sensor)); // Use RA as cap, sensor as indicator
float et0 = hargreaves_et0(daily.tmax, daily.tmin, ra_mj);
float rainMm = rainTips * 0.2; // 0.2mm per tip
Serial.printf("T:%.1f-%.1fC | RH:%.0f%% | Wind:%.1fm/s | Rain:%.1fmm | ET0:%.1fmm/dayn",
daily.tmin, daily.tmax, humid, daily.windSpeed, rainMm, et0);
// Transmit via LoRa
SPI.begin(18, 19, 23, LORA_SS);
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
if (LoRa.begin(433E6)) {
LoRa.setSpreadingFactor(9);
LoRa.beginPacket();
LoRa.write(1); // Station ID
int16_t tmaxI = (int16_t)(daily.tmax * 100);
int16_t tminI = (int16_t)(daily.tmin * 100);
uint8_t humI = (uint8_t)humid;
uint8_t et0I = (uint8_t)(et0 * 10); // 0-25mm as 0-250
uint8_t rainI = (uint8_t)(rainMm * 10); // 0-25mm as 0-250
LoRa.write((uint8_t*)&tmaxI, 2);
LoRa.write((uint8_t*)&tminI, 2);
LoRa.write(humI);
LoRa.write(et0I);
LoRa.write(rainI);
LoRa.endPacket();
}
// Sleep 30 minutes
esp_sleep_enable_timer_wakeup(30ULL * 60 * 1000000);
esp_deep_sleep_start();
}
void loop() {} // Never reached
Irrigation Scheduling from ET Data
On the central server (Raspberry Pi), the ET data drives irrigation decisions:
def schedule_irrigation(et0_mm, crop_kc, soil_moisture_pct,
field_capacity_pct, rainfall_mm):
"""Calculate irrigation need based on ET water balance."""
etcrop = et0_mm * crop_kc
effective_rain = rainfall_mm * 0.8 # 80% effectiveness factor
# Soil water balance
available_water_mm = (soil_moisture_pct / 100) * field_capacity_pct * 300
fc_water_mm = field_capacity_pct / 100 * 300 # 300mm root zone
net_depletion = etcrop - effective_rain
new_moisture = available_water_mm - net_depletion
if new_moisture < fc_water_mm * 0.5: # 50% MAD reached
irrigation_need_mm = fc_water_mm - new_moisture
return irrigation_need_mm
return 0
# Example: Wheat at mid-season (Kc=1.15), Punjab, March
et0 = 5.2 # mm/day (typical March Punjab)
kc = 1.15 # Wheat mid-season
need = schedule_irrigation(et0, kc, 55, 70, 0) # 55% current, 70% FC
print(f"Irrigation needed: {need:.1f} mm = {need*10:.0f} litres per sq metre")
Indian Agriculture ET Reference Values
Reference ET0 values for major Indian agricultural regions:
| Region | Season | ET0 (mm/day) | Critical Crops |
|---|---|---|---|
| Punjab/Haryana | Rabi (Nov-Apr) | 2.5-5.5 | Wheat, mustard |
| Maharashtra | Rabi | 3.5-6.0 | Sorghum, onion |
| AP/Telangana | Kharif | 4.0-6.5 | Rice, cotton |
| Rajasthan | Summer | 6.0-10.0 | Pearl millet, groundnut |
Frequently Asked Questions
How accurate is ET calculation from a BME280 alone?
Using Hargreaves method with BME280 temperature + BH1750 as radiation proxy gives ET0 accuracy within 15-25% of Penman-Monteith. For practical irrigation scheduling this is sufficient — any accuracy better than calendar-based scheduling translates to water savings. For research purposes, use a proper pyranometer (Kipp and Zonen, Rs 15,000-80,000).
Do I need a separate weather station for each field?
One station per 10-50 acres is sufficient if terrain is relatively uniform. In hilly or coastal areas with complex microclimates, use one station per 5 acres. IMD agrometeorology stations also provide free daily ET0 data for major Indian districts — use these as a baseline to calibrate your field station.
What is the crop coefficient (Kc) for common Indian crops?
FAO Irrigation and Drainage Paper 56 provides Kc values: Rice Kc=1.05-1.20, Wheat Kc=0.7-1.15 (season-varying), Cotton Kc=0.45-1.15, Sugarcane Kc=0.4-1.25, Maize Kc=0.3-1.2. These vary by growth stage — initial, development, mid-season, and late-season values differ significantly.
Add comment