Build a Smart Plant Watering System with Raspberry Pi

I killed a basil plant last month. Not through neglect, exactly — I watered it. Just not at the right times, or in the right amounts. Some weeks too much, some weeks too little. The soil would dry out completely before I remembered, or I'd overcompensate and drown the roots.

Turns out this is a solved problem. A capacitive soil moisture sensor costs about a dollar, a mini water pump costs two, and a Raspberry Pi can read one and control the other. When the soil drops below a threshold — pump runs for a few seconds. When it's wet enough — nothing happens. The Pi logs every event to a CSV file, so over time you can see exactly how your plant drinks.

The whole thing runs on three GPIO pins and about 60 lines of Python. It combines sensor reading, relay-driven actuation, cooldown logic (so you don't overwater), and persistent data logging — the same control loop architecture used in commercial irrigation systems and industrial automation.

What You'll Need

All of these components are inexpensive and available from most electronics retailers. The complete setup costs under $15 in parts.


Know Your Components

Capacitive Soil Moisture Sensor

There are two types of soil moisture sensors: resistive and capacitive. We use a capacitive sensor because it lasts far longer — resistive sensors have exposed metal pads that corrode within weeks in wet soil.

Soil Sensor and ADC

The capacitive sensor measures the dielectric constant of the soil, which changes with moisture content. Dry soil has a low dielectric constant; wet soil has a high one. The sensor converts this into a voltage: lower voltage when wet, higher when dry.

Most modules include a built-in comparator with an adjustable potentiometer and a D0 (digital output) pin. When the soil is drier than the threshold you set with the potentiometer, D0 goes HIGH. When it's wet enough, D0 goes LOW. This means we can read it directly with the Pi's GPIO — no ADC needed.

The sensor runs at 3.3V, which matches the Pi's GPIO voltage perfectly.

The Water Pump

A mini submersible pump is a small DC motor that pushes water through tubing when powered. It typically runs on 3–6V and draws 100–200 mA. We control it with the relay — the Pi tells the relay to switch on, the relay connects power to the pump, water flows.

We never connect the pump directly to a GPIO pin — the current draw would damage the Pi. The relay acts as the intermediary, safely switching the pump's power circuit.


Wiring It Up

This project has three connections to the Pi: the soil sensor, the relay module, and the pump (via the relay).

Wiring Diagram

Soil sensor (3 wires)

Red wire: Sensor VCC → Pi 3.3V (pin 1)

Green wire: Sensor D0 → Pi GPIO17 (pin 11)

Black wire: Sensor GND → Pi GND (pin 6)

Connect VCC to 3.3V, not 5V. The sensor module works at 3.3V and the D0 output will be 3.3V logic, which is safe for the Pi's GPIO.

Relay module (3 wires)

Red wire: Relay VCC → Pi 5V (pin 2)

Orange wire: Relay IN → Pi GPIO27 (pin 13)

Black wire: Relay GND → Pi GND (shared with sensor)

The relay itself needs 5V to operate its coil, but the signal input (IN) triggers at 3.3V — compatible with the Pi's GPIO.

Water pump (through relay)

Connect a wire from the Pi's 5V pin to the relay's COM terminal. Connect the relay's NO (Normally Open) terminal to one wire of the pump. Connect the other wire of the pump to GND.

When the relay activates, COM connects to NO, completing the circuit and running the pump. When it deactivates, the circuit breaks and the pump stops.


Calibrating the Sensor

Before running the automation, calibrate the sensor's threshold potentiometer:

  1. Insert the sensor probe into dry soil (or hold it in the air)
  2. Observe the LED on the sensor module — it should be off (D0 = HIGH = dry)
  3. Slowly turn the potentiometer clockwise until the LED just turns on
  4. Now dip the sensor in water or very wet soil — the LED should turn off again
  5. The threshold is now set: LED on = dry (needs water), LED off = wet (enough water)

You can fine-tune this over a few days as you observe your plant.


The Code

Create the Python script:

nano plant_water.py
import RPi.GPIO as GPIO
import time
import datetime
import csv
import os

SENSOR_PIN = 17
RELAY_PIN = 27
PUMP_DURATION = 3
CHECK_INTERVAL = 600
COOLDOWN = 1800
LOG_FILE = "watering_log.csv"

GPIO.setmode(GPIO.BCM)
GPIO.setup(SENSOR_PIN, GPIO.IN)
GPIO.setup(RELAY_PIN, GPIO.OUT, initial=GPIO.LOW)


def get_timestamp():
    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def log_event(event, soil_status):
    file_exists = os.path.isfile(LOG_FILE)
    with open(LOG_FILE, "a", newline="") as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(["Timestamp", "Event", "Soil Status"])
        writer.writerow([get_timestamp(), event, soil_status])


def water_plant():
    print(f"[{get_timestamp()}] Soil is dry — watering for {PUMP_DURATION}s")
    GPIO.output(RELAY_PIN, GPIO.HIGH)
    time.sleep(PUMP_DURATION)
    GPIO.output(RELAY_PIN, GPIO.LOW)
    print(f"[{get_timestamp()}] Pump stopped")
    log_event("WATERED", "dry")


def check_soil():
    reading = GPIO.input(SENSOR_PIN)
    if reading == GPIO.HIGH:
        return "dry"
    else:
        return "wet"


print("Smart Plant Watering System")
print(f"Check interval: {CHECK_INTERVAL}s | Pump duration: {PUMP_DURATION}s")
print(f"Cooldown between waterings: {COOLDOWN}s")
print(f"Logging to: {LOG_FILE}")
print("Press Ctrl+C to stop.\n")

last_watering = 0

try:
    while True:
        status = check_soil()
        now = time.time()
        time_since_last = now - last_watering

        if status == "dry" and time_since_last >= COOLDOWN:
            water_plant()
            last_watering = time.time()
        elif status == "dry":
            remaining = int(COOLDOWN - time_since_last)
            print(f"[{get_timestamp()}] Soil dry, but in cooldown ({remaining}s remaining)")
            log_event("DRY_COOLDOWN", "dry")
        else:
            print(f"[{get_timestamp()}] Soil is wet — no action needed")
            log_event("CHECK", "wet")

        time.sleep(CHECK_INTERVAL)

except KeyboardInterrupt:
    print("\nStopping...")

finally:
    GPIO.output(RELAY_PIN, GPIO.LOW)
    GPIO.cleanup()
    print("GPIO cleaned up. Goodbye!")

Save with Ctrl+O, Enter, exit with Ctrl+X. Run it:

python3 plant_water.py

Understanding the code

GPIO.input(SENSOR_PIN) — Reads the sensor's digital output. HIGH means dry (above threshold), LOW means wet (below threshold). The threshold is set by the sensor's physical potentiometer.

PUMP_DURATION = 3 — How many seconds the pump runs each time. Start with 3 seconds and adjust based on your pot size and pump flow rate. A small pot might only need 2 seconds; a large one might need 5–8.

CHECK_INTERVAL = 600 — Check soil every 600 seconds (10 minutes). Soil moisture doesn't change rapidly, so frequent checking is unnecessary and would wear out the pump relay.

COOLDOWN = 1800 — After watering, wait at least 30 minutes before watering again, even if the sensor still reads "dry." This prevents overwatering — water takes time to distribute through the soil and reach the sensor.

CSV logging: Every check and watering event is recorded in watering_log.csv with a timestamp. Over time, this gives you a picture of how often your plant needs water and how quickly the soil dries out.

last_watering tracking: Using time.time() to track when the last watering occurred ensures the cooldown works correctly even if the Pi restarts (though the timer resets — for persistent tracking across reboots, you'd save the timestamp to a file).


An Enhanced Version with Scheduling

The basic version checks continuously. Here's an improved version that only checks during daylight hours and provides a daily summary:

import RPi.GPIO as GPIO
import time
import datetime
import csv
import os

SENSOR_PIN = 17
RELAY_PIN = 27
PUMP_DURATION = 3
CHECK_INTERVAL = 600
COOLDOWN = 1800
LOG_FILE = "watering_log.csv"
ACTIVE_HOURS = (7, 22)

GPIO.setmode(GPIO.BCM)
GPIO.setup(SENSOR_PIN, GPIO.IN)
GPIO.setup(RELAY_PIN, GPIO.OUT, initial=GPIO.LOW)

watering_count_today = 0
current_day = datetime.date.today()


def get_timestamp():
    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def log_event(event, soil_status):
    file_exists = os.path.isfile(LOG_FILE)
    with open(LOG_FILE, "a", newline="") as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(["Timestamp", "Event", "Soil Status"])
        writer.writerow([get_timestamp(), event, soil_status])


def is_active_hours():
    hour = datetime.datetime.now().hour
    return ACTIVE_HOURS[0] <= hour < ACTIVE_HOURS[1]


def water_plant():
    global watering_count_today
    print(f"[{get_timestamp()}] Watering for {PUMP_DURATION}s...")
    GPIO.output(RELAY_PIN, GPIO.HIGH)
    time.sleep(PUMP_DURATION)
    GPIO.output(RELAY_PIN, GPIO.LOW)
    watering_count_today += 1
    log_event("WATERED", "dry")
    print(f"[{get_timestamp()}] Done. Today's count: {watering_count_today}")


print("Smart Plant Watering System (Enhanced)")
print(f"Active hours: {ACTIVE_HOURS[0]}:00 — {ACTIVE_HOURS[1]}:00")
print("Press Ctrl+C to stop.\n")

last_watering = 0

try:
    while True:
        today = datetime.date.today()
        if today != current_day:
            print(f"\n--- Daily Summary ({current_day}) ---")
            print(f"Total waterings: {watering_count_today}")
            log_event("DAILY_SUMMARY", f"waterings={watering_count_today}")
            watering_count_today = 0
            current_day = today

        if not is_active_hours():
            time.sleep(60)
            continue

        status = "dry" if GPIO.input(SENSOR_PIN) == GPIO.HIGH else "wet"
        now = time.time()

        if status == "dry" and (now - last_watering) >= COOLDOWN:
            water_plant()
            last_watering = now
        elif status == "wet":
            print(f"[{get_timestamp()}] Soil OK")

        time.sleep(CHECK_INTERVAL)

except KeyboardInterrupt:
    print("\nStopping...")

finally:
    GPIO.output(RELAY_PIN, GPIO.LOW)
    GPIO.cleanup()
    print("Cleaned up. Goodbye!")

The key additions: active hours prevent the pump from running at night (when evaporation is lowest anyway), a daily counter tracks how many times watering occurred each day, and a daily summary prints at midnight with the count.


Troubleshooting

Pump runs constantly / never stops. The sensor's D0 is stuck HIGH. Check that the sensor is powered (VCC to 3.3V, not disconnected). Adjust the potentiometer — turn it slightly until the behavior matches: dry = HIGH, wet = LOW.

Pump never activates even when soil is dry. Read the sensor manually: python3 -c "import RPi.GPIO as GPIO; GPIO.setmode(GPIO.BCM); GPIO.setup(17, GPIO.IN); print(GPIO.input(17))". If it returns 0 even with dry soil, the threshold is set too high — turn the potentiometer to make it more sensitive.

Water doesn't flow when pump is running. Make sure the pump is submerged — submersible pumps can't self-prime from dry. Check that tubing isn't kinked. Verify the relay clicks (you should hear it).

"RuntimeWarning: This channel is already in use." A previous script didn't clean up. Add GPIO.setwarnings(False) after GPIO.setmode().

Pi can't supply enough power for the pump. If the Pi resets when the pump starts, the pump draws too much current. Use an external 5V supply for the pump (routed through the relay), with its GND connected to the Pi's GND.


Going Further

Multiple plants: Add more sensors and relays on different GPIO pins. Each plant gets its own check-and-water logic with independent thresholds.

Web dashboard: Add Flask to serve a web page showing current soil status, today's watering count, and a graph of historical moisture data from the CSV log.

Push notifications: Use Python's requests library to send a Telegram or Pushover notification each time the plant is watered, or when the water reservoir is running low.

Analog precision: Replace the digital D0 approach with an MCP3008 ADC chip to get exact moisture values (0–1023) instead of just wet/dry. This lets you make more nuanced decisions ("slightly dry" vs "very dry") and graph moisture levels over time.

Water level sensor: Add a simple float switch or ultrasonic sensor to the water reservoir. If the water runs out, stop trying to pump and send an alert.

Run on boot: Add the script to the Pi's crontab so it starts automatically: crontab -e then add @reboot python3 /home/pi/plant_water.py &. Your plant care system survives power outages.


What's Really Happening Here

Zoom out from the plant for a moment. What you've built is a closed-loop control system: sense a condition, decide whether to act, act, then wait before checking again. The cooldown timer, the active-hours window, the CSV log — these aren't extras. They're the safeguards that separate "automation" from "a pump that runs until the pot floods."

Real industrial systems — HVAC controllers, chemical dosing pumps, precision agriculture rigs — use exactly the same patterns: measure, compare to a setpoint, actuate, wait, repeat. They just have more sensors and fancier enclosures.

Your basil plant doesn't care about any of that. It just knows it's getting watered on time now.


Part of the Arduino Intermediate series. Previous: Temperature MonitorWi-Fi Weather StationMotion-Activated LightDistance-Controlled ServoMelody BuzzerWeb-Controlled LED Strip