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.

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:
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.
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.
Let’s be honest, this is software… “when it does”