~/jackwent.co.uk
jack@jackwent:~$ cat garden-irrigation.md

Garden Irrigation System

by published 26 March 2024updated 11 October 20264 min read

I got fed up with plants either drowning or drying out, so I built a Raspberry Pi to water them for me. It uses the Pi’s GPIO pins to read soil moisture sensors and switch a pump and valves, so each plant gets watered when it actually needs it and not on a guess. It works just as well for a few pots on the patio as it does in a greenhouse, and it takes a daily job off your list.

Why automate watering?

Watering by hand usually means too much or too little, and both are bad for plants. This system fixes that with soil moisture sensors that watch how dry each pot is and turn on a pump or valve when it’s needed. The soil stays at the right moisture level and the plants are much happier for it.

How it works

It’s easy to adapt and grow, whether you’ve got a small garden or a big greenhouse. Moisture sensors sit in the soil and report when it’s getting dry. When a pot needs water, the Pi switches on the pump or opens the right valve and waters just that spot. Because it only waters when it has to, you use a lot less water too.

Getting started

Everything you need is below: the kit list, which GPIO pins to use for the sensors and relays, and the Python script that runs the whole thing. There’s a wiring diagram too, so it’s very doable even if you’re new to this.

Why it’s worth it

The biggest win is peace of mind. Your plants get looked after properly every day, even when you’re not there, and you can stop worrying about who’s going to water them while you’re on holiday.

Kit list

  • 1x Raspberry Pi 4 running Ubuntu 22.04
  • 2x ultrasonic water tank sensors (HC-SR04)
  • 2x soil moisture sensors (LM393)
  • 3x 5V single channel relay modules
  • 2x DC 4.5V inlet water solenoid valves, 1/4” high pressure
  • 1x breadboard

Raspberry Pi pinout

  • Plant pot 1 valve relay: GPIO 26
  • Plant pot 2 valve relay: GPIO 27
  • Water pump 1 relay: GPIO 22
  • Plant pot 1 moisture sensor DO pin: GPIO 23
  • Plant pot 2 moisture sensor DO pin: GPIO 6
  • Ultrasonic water tank sensor trigger pin: GPIO 16
  • Ultrasonic water tank sensor echo pin: GPIO 25

Benefits

  • Uses less water. Watering based on how dry the soil is means no overwatering and no wasted water.
  • Hands free. Once it’s running you don’t have to think about watering at all.
  • Healthier plants. Constant monitoring and watering at the right time keeps plants growing well.

How it’s gone

Mine waters two plant pots, and it’s still running. The one thing that caught me out was the water tank running dry.

When the tank is empty, the pump has nothing to push, so the plants don’t get watered and the pump runs dry, which isn’t good for it either. That’s why the ultrasonic tank sensor matters more than it first looks. It measures the gap between the top of the tank and the water, so the script knows when the level is getting low and stops watering when it’s empty, instead of switching the pump on for nothing.

While writing this up I found three bugs in the version of the script on this page, and I’ve fixed them below:

  • The tank always read “Empty”. The level check tested the empty case first, against a range with no upper limit, so it matched every reading. It now checks from full down to empty.
  • The empty tank check never kicked in. The script had a water_tank_empty flag for pausing until you refill, but nothing ever set it. It now updates from the tank sensor on every loop.
  • The overnight pause never paused. “Between 22:00 and 06:00” was written as one comparison, which can never be true because it crosses midnight. It’s now two checks joined with “or”.

If you’ve copied the script before, it’s worth grabbing it again.

#!/usr/bin/env python3
import RPi.GPIO as GPIO
import time

# GPIO pin definitions
PLANT_POT_1_VALVE_PIN = 26
PLANT_POT_2_VALVE_PIN = 27
WATER_PUMP_1_PIN = 22
PLANT_POT_1_MOISTURE_PIN = 23
PLANT_POT_2_MOISTURE_PIN = 6
WATER_TANK_TRIGGER_PIN = 16
WATER_TANK_ECHO_PIN = 25

# Ultrasonic water tank sensor ranges
FULL_RANGE = (1, 40)
MEDIUM_RANGE = (41, 81)
LOW_RANGE = (82, 100)
EMPTY_RANGE = (101, float('inf'))

# GPIO setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(PLANT_POT_1_VALVE_PIN, GPIO.OUT)
GPIO.setup(PLANT_POT_2_VALVE_PIN, GPIO.OUT)
GPIO.setup(WATER_PUMP_1_PIN, GPIO.OUT)
GPIO.setup(WATER_TANK_TRIGGER_PIN, GPIO.OUT)
GPIO.setup(WATER_TANK_ECHO_PIN, GPIO.IN)
GPIO.setup(PLANT_POT_1_MOISTURE_PIN, GPIO.IN)
GPIO.setup(PLANT_POT_2_MOISTURE_PIN, GPIO.IN)

# Global variables
water_tank_empty = False
time_restriction_enabled = True

def check_moisture(sensor_pin):
    return GPIO.input(sensor_pin)

def check_water_tank():
    GPIO.output(WATER_TANK_TRIGGER_PIN, True)
    time.sleep(0.00001)
    GPIO.output(WATER_TANK_TRIGGER_PIN, False)
    
    pulse_start = time.time()
    pulse_end = time.time()
    
    while GPIO.input(WATER_TANK_ECHO_PIN) == 0:
        pulse_start = time.time()
    
    while GPIO.input(WATER_TANK_ECHO_PIN) == 1:
        pulse_end = time.time()
    
    pulse_duration = pulse_end - pulse_start
    distance = pulse_duration * 17150
    return int(distance)  # Convert to integer before returning

def water_tank_status():
    # The sensor measures the gap from the top of the tank down to the water,
    # so a bigger distance means less water. Check from full to empty.
    distance = int(check_water_tank())
    if distance <= FULL_RANGE[1]:
        return "Full"
    elif distance <= MEDIUM_RANGE[1]:
        return "Medium"
    elif distance <= LOW_RANGE[1]:
        return "Low"
    else:
        return "Empty"

def run_irrigation_system():
    global water_tank_empty
    
    while True:
        now = int(time.strftime('%H%M'))
        # No watering overnight, between 22:00 and 06:00
        if time_restriction_enabled and (now >= 2200 or now <= 600):
            print("Time Restriction: Script paused due to time restriction.")
            time.sleep(60)
            continue
        
        plant_pot_1_moisture = check_moisture(PLANT_POT_1_MOISTURE_PIN)
        plant_pot_2_moisture = check_moisture(PLANT_POT_2_MOISTURE_PIN)
        
        tank = water_tank_status()
        water_tank_empty = tank == "Empty"  # never run the pump with no water
        print("Ultra Sonic Water Tank Sensor:", tank)
        print("Plant Pot 1 Moisture Sensor:", plant_pot_1_moisture)
        print("Plant Pot 1 Valve Relay:", GPIO.input(PLANT_POT_1_VALVE_PIN))
        print("Plant Pot 2 Moisture Sensor:", plant_pot_2_moisture)
        print("Plant Pot 2 Valve Relay:", GPIO.input(PLANT_POT_2_VALVE_PIN))
        
        if water_tank_empty:
            print("Water Tank Empty: Refill required.")
            time.sleep(60)
            continue
        
        if not plant_pot_1_moisture:
            GPIO.output(WATER_PUMP_1_PIN, True)
            GPIO.output(PLANT_POT_1_VALVE_PIN, True)
            print("Watering Plant Pot 1...")
            time.sleep(30)
            GPIO.output(PLANT_POT_1_VALVE_PIN, False)
            GPIO.output(WATER_PUMP_1_PIN, False)
            print("Plant Pot 1 watered.")
        elif GPIO.input(PLANT_POT_1_VALVE_PIN):
            GPIO.output(WATER_PUMP_1_PIN, False)
        
        if not plant_pot_2_moisture:
            GPIO.output(WATER_PUMP_1_PIN, True)
            GPIO.output(PLANT_POT_2_VALVE_PIN, True)
            print("Watering Plant Pot 2...")
            time.sleep(30)
            GPIO.output(PLANT_POT_2_VALVE_PIN, False)
            GPIO.output(WATER_PUMP_1_PIN, False)
            print("Plant Pot 2 watered.")
        elif GPIO.input(PLANT_POT_2_VALVE_PIN):
            GPIO.output(WATER_PUMP_1_PIN, False)
        
        if plant_pot_1_moisture and plant_pot_2_moisture:
            time.sleep(30)
            if not GPIO.input(PLANT_POT_1_VALVE_PIN) and not GPIO.input(PLANT_POT_2_VALVE_PIN):
                GPIO.output(WATER_PUMP_1_PIN, False)
        
        time.sleep(1)

try:
    run_irrigation_system()
except KeyboardInterrupt:
    print("\nExiting...")
finally:
    GPIO.cleanup()
[jackwent]