Natalie Pendragon The Intergalactic Blog

Building a Plant Watering Robot

Due to some holiday travel, some big, unwieldy plants in the apartment, and a pandemic making all of humanity skittish around itself (eek!), we found ourselves brainstorming an automated way to water said plants. We eventually came to the conclusion that it would be easy enough and cheap enough to build a little waterbot for this with a Raspberry Pi.

As usual, the process began with a search for prior art. I came across several examples of people having done this, and having documented the process. My favorite was Alan Constantino’s Writeup, and we ended up using a very similar design. They have great pictures, and you should check out their writeup as well. The three notable pieces of my writeup that diverge are:

  • thoughts on pros and cons of two popular schematics for this project, and reasoning for choosing one over the other
  • a nice wiring diagram
  • revamped application code - I think I’ve managed to simplify the implementation a bit.

Hardware

The first thing I noticed was that there seemed to be two schools of thought on how to design the circuit.

  • get a 5v pump, and run that using the the 5v output from rpi gpio pins.
    • pro: you don’t need a dedicated power source for the pump, as the rpi itself can serve as the power source.
    • con: you have to complicate the circuit around the pump, adding things like a diode to protect the rpi from any voltage weirdness that the pump could cause, potentially damaging the rpi.
  • get a 12v pump, and a 12v power source, then turn it on and off with a 5v relay controlled using the 5v output from rpi gpio pins.
    • pro: the circuit is simple - just a relay opening and closing the circuit of a power source connected to a pump.
    • con: you need an additional power source - one for the rpi, one for the pump.

We decided to go for the second option, since both of our electronics experience is a bit, ahem, stale, at this point. And the additional 12v power source was only $5. With that decision out of the way, we could now finalize our component list!

Components

  • Raspberry Pi Zero
  • 5v relay
  • 12v peristaltic pump
  • 12v power supply
  • Assorted wires and connecty bits

Wiring

				  peristaltic
				     pump
	 __                           __
	 | \   ---------             /  \----
	 |  \__|       |-----   ----|->  |   |
5V     ----    |       |    |   |    \__/  ----
power   --     |  rPi  |    D|\ o           --   12V
source ----    | Zero  |    D| \           ----  power
	--     |       |    D|  o           --   source
	 |     |       |    |   |            |
	 ------|       |-----   --------------
	       ---------      ^
			      |
			   5V relay

The circuit should be fairly self-explanatory from the above wiring diagram, with the exception of pin handling on the rpi. You have to do a couple things here - first, solder the 40-pin connector block to the rpi, and second, figure out which pins to use.

rpi-zero-pinout.png

I gather you can use any of the gpio pins for INPUT or OUTPUT, but some of them can also be used for more specialized tasks, as per the diagram1. All we are doing is simple OUTPUT to engage the relay, so I chose one of the pins that only does simple gpio, and left the more specialized pins free for other things. Maybe after version one is complete I can add a soil moisture sensor!

Pins connected to the relay are as follows:

  • pin 2: 5v power
  • pin 34: ground
  • pin 12: high/low signal (note that this pin is pin 12 for gpio purposes, but on the pin diagram, it is also referenced as pin 32)

Software

Next up came the software - both a small amount of application code, as well as some configuration management for the rpi itself, which runs Raspbian2. The software only needs to set high and low states appropriately for pin 12, as pin 2 (power) and pin 34 (ground) will always be doing the same thing.

In addition to the salient snippets embedded directly into this writeup, you can find the complete code here.

Application Code

There is a good Python library for handling rpi gpio called gpiozero., so we whipped up a quick little Python program to handle everything. The biggest source of complexity in this ~80-line program is tracking the watering times via serializing and deserializing to a file. This seemed like a good approach to manage the risk of the program dying for any reason - if it does3, whenever it restarts, it will naturally pick up where it left off by reading whatever data it last saved to file.

import os
import time
from datetime import datetime, timedelta
from pathlib import Path

from gpiozero import OutputDevice

DATA_FILENAME = "waterbot.data"
SECONDS_TO_WATER = 120
SECONDS_TO_SLEEP = 60 * 60 # 1 hour
WATER_INTERVAL = timedelta(days=3)
DATETIME_LOG_FORMAT = "%Y-%m-%d %H:%M:%S"

class Relay(OutputDevice):
    def __init__(self, pin, active_high):
        super(Relay, self).__init__(pin, active_high)

RELAY = Relay(12, False)

def water_plant(relay=RELAY, seconds=SECONDS_TO_WATER):
    relay.on()
    log_message("💧 Starting water")
    time.sleep(seconds)
    log_message("💧 Stopping water")
    relay.off()


def read_water_time():
    """
    Read an ISO-formatted datetime from a file on disk, which represents
    the datetime at which the plant was last watered. If the file either
    does not exist, or raises an exception while being parsed, return the
    minimum possible datetime value, to trigger an immediate watering, and
    subsequent call to `persist_water_time`, which should persist a valid
    ISO-formatted datetime to the file (and thus allow this function to
    traverse a happy code path the next time it's called).
    """
    p = Path(DATA_FILENAME)
    if not p.is_file():
        return datetime.min
    text = p.read_text().replace('\n', '')
    try:
        water_time = datetime.fromisoformat(text)
        return water_time
    except:
        return datetime.min


def persist_water_time():
    """
    Write the current datetime to a file in ISO format. This should be called
    immediately after watering the plant, to update the most recent watering
    time. It will create the file if it doesn't exist yet, or overwrite it
    if it does already exist.
    """
    p = Path(DATA_FILENAME)
    p.write_text(datetime.now().isoformat())


def log_message(message):
    log_line = f"{datetime.now().strftime(DATETIME_LOG_FORMAT)}: {message}"
    print(log_line)


def main():
    while True:
        time.sleep(SECONDS_TO_SLEEP)
        log_message("Checking...")
        last_water_time = read_water_time()
        if datetime.now() - last_water_time >= WATER_INTERVAL:
            water_plant()
            persist_water_time()


if __name__ == "__main__":
    main()

Configuration Management

First, you’ll need to flash a Raspbian os image onto a microsd card. I started by trying to use the official Raspberry Pi Imager, and Etcher, but both ended up encountering errors. I was frustrated with flashing, and sd cards - I feel like every time I deal with this, it’s always a painful adventure. Then I discovered that somehow, Linux cp just magically handles all of this. You simply download the image, then copy it to the unmounted drive.

sudo cp ./raspbian.img /dev/mmcblk0

You can find the drive on your machine by running lsblk (just make sure you use the drive identifier, not the partition identifier if it has one). After that, you should be able to boot it up and go through a few housekeeping steps to set up and secure the machine. Definitely make sure to secure the machine if you plan to expose any ports to the internet. Some suggestions to do so are below, but securing Linux is outside the scope of this writeup, so you should probably search around a bit for some targeted advice on that!

  • turn on ufw
  • turn on fail2ban
  • disable ssh pw auth
  • disable ssh access for root

Since I’m using Poetry to run the application, I installed that using their installer script. Poetry was a bit confused when I first trying running something with it, and it seemed like Python2/3 errors. My hunch was that it was expecting python to be synonymous with python3, which is frankly pretty reasonable at this point, but Raspbian’s python still resolved to python2. So, I used update-alternatives to make python3 the default.

# First, create a link group for Python
sudo update-alternatives --config python

# Next set the priorities for each member
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 2
sudo update-alternatives --install /usr/bin/python python /usr/bin/python2 1

# Finally, verify the results
sudo update-alternatives --display python

# python - auto mode
#   link best version is /usr/bin/python3
#   link currently points to /usr/bin/python3
#   link python is /usr/bin/python
# /usr/bin/python2 - priority 1
# /usr/bin/python3 - priority 2

Lastly, I wanted to make sure the program automatically restarted itself in the case of an error or system reboot. For this, I turned to systemctl and made a little service unit, which lives at /etc/systemd/system/waterbot.service.

[Unit]
Description=Waterbot Service
After=network.target

[Service]
User=pi
Group=pi
WorkingDirectory=/home/pi/code/waterbot
Environment="PYTHONUNBUFFERED=1"
ExecStart=/home/pi/.poetry/bin/poetry run waterbot
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

After creating this file, test it out:

# reload units
sudo systemctl daemon-reload

# start the new unit one time
sudo systemctl start waterbot.service

# check status, and see the last few lines of log output
sudo systemctl status waterbot.service

# look at all the log lines, if you need more information to debug an issue
sudo journalctl -u waterbot

# You can stop and start the unit repeatedly, if you need to iterate...

Once you’re satisfied the unit works well, you can enable it with systemctl, so it will run whenever the appropriate conditions trigger (as written, it will start at system boot, and then try and always keep it running).

sudo systemctl enable waterbot.service
sudo systemctl start waterbot.service

That’s about it!

Footnotes:

1

In addition to the diagram I embedded in this writeup, I also found pinout.xyz quite helpful. You can click on each pin and get more information about it.

2

Raspbian is Debian-based, so if you’re accustomed to Debian or Ubuntu, you should feel right at home. Also, I just learned that they changed their name from Raspbian to Raspberry Pi os, which I’m slightly bummed about. Raspbian is such a cute name.

3

Let’s be honest, this is software… “when it does”