Water your plants with a Raspberry Pi
Cyber Gardener
An automated watering system comprising a Raspberry Pi Zero W, an analog-to-digital converter, and an inexpensive irrigation kit can help keep your potted plants from dying of thirst.
Inspired by an earlier article in Linux Pro Magazine [1], I had been thinking for some time about the idea of using a computer to measure the moisture of three potted plants in my office and watering them automatically when needed. When I came across an inexpensive kit with sensors and pumps, the time had come to tackle the subject.
The first idea for watering the three flower pots was to use a pump with a valve system to regulate which pot was watered. However, online research did not reveal any low-cost systems, so the project ended up back in the drawer.
A later search took me to an irrigation kit by the Chinese company WayinTop that contains four individual pumps, four humidity sensors, a relay module, and a matching hose [2], all for about $30 (EUR30, £34). This was well within the price range I had in mind.
The controller was to be a Raspberry Pi, but that was one thing I had forgotten to look into in my planning. The sensors in the WayinTop kit deliver analog signals. This is not a problem if you want to use it with an Arduino Uno, as intended by the manufacturer, but using it with a Raspberry Pi requires an analog-to-digital (A/D) converter. My choice here was an MCP3008, which only costs around $4 (EUR2.50, £3). You can find a Python library on GitHub for programming this chip [3].
Divide and Conquer
Implementing the planned scenario for automatic irrigation involves the following three tasks:
- Set up the hardware so that the sensors provide measured values and the pumps can be controlled individually.
- Acquire and understand the measured values (e.g., determine how watering changes the measured values).
- Derive an algorithm that supports automatic watering.
The Raspberry Pi has to control two classes of devices: the sensors that provide moisture data and the pumps, which the Pi switches on and off as required.
In addition to controlling and reading out the measured values, step 1 also includes setting up the power supply. The pumps and sensors only need 3.3V, which the Raspberry Pi can supply. The A/D converter also needs a power supply, as does the controller of the relay module.
In principle, the Raspberry Pi can supply the pumps with power directly. To do so, you need to connect their ground terminals to the ground pin on the Raspberry Pi. The positive pump terminals are then connected to one GPIO port on the Raspberry Pi, so the pumps can be switched on and off. However, their motors do not necessarily have the same current draw when starting up. The approach of controlling the pumps with an external power supply provided by the relay module seemed more promising.
The sensors also require 3.3V to run. Budding electricians need to bear in mind that, depending on the quality and coating of the sensors, keeping them permanently live will cause them to corrode because a chemical reaction takes place in the ground. Measuring humidity with a resistor also causes electrolysis, so it makes sense to supply the sensors with power only when they are supposed to provide measurement data. As has been shown in practice, this is not often the case. I used a breadboard for the assembly and testing steps (Figure 1). To implement the entire design as economically as possible, I also chose a Pi Zero W [4] as the control computer.
Hands On
The first step was to connect the sensor and A/D converter. The MCP3008 has connections on two sides. One side is for the control and power supply and the other side has the input channels. Figure 2 illustrates how the MCP3008 is wired to the Raspberry Pi.
The A/D converter is controlled over the SPI interface, which must be activated in raspi-config
. Alternatively, you can set the appropriate kernel parameters manually in the bootloader and restart the Raspberry Pi.
The next step is to connect the data ports on the MCP3008 with the matching pins on the Raspberry Pi – not all pins provide serial peripheral interface (SPI) functions (Figure 3). The two ground (GND) connections and the 3.3V the A/D converter needs are all wired on the breadboard, which keeps from using too many cables and pins on the Pi Zero W.
The data lines of the humidity sensors are now connected to the input channels of the A/D converter one after the other. The sensors are grounded on one of the breadboard's power rails, to which the Raspberry Pi is also wired. The 3.3V is taken from GPIOs 4, 17, and 22 (header pins 7, 11, and 15), allowing the Raspberry Pi to switch the sensors on before and off after measurements are taken.
The power for the pumps and the relay module is provided by a separate power supply module that is plugged in to the breadboard (Figure 4) and receives its input current from a power supply or over USB. In the setup with the breadboard, the 5V output is switched off and a voltage of 3.3V is available on the breadboard's other power rail.
The relay module needs 3.3V, plus ground and a wire to a GPIO for each relay to be controlled. Power is supplied by the matching module, and three control wires are routed to the Raspberry Pi.
The pumps' ground connections are connected to ground on the breadboard's power supply module (Figure 1, green wires). The positive terminal on each pump is connected to the center terminal of the respective relay. From the right connector on the relay, one wire goes to the 3.3V line on the power supply module (Figure 4).
To turn on the relay, the program sets the connected GPIO port to
, and to turn off the relay, the program sets it to 1
. Figure 5 shows the setup for a single sensor and pump.
Software
Controlling the irrigation system turns out to be somewhat of a trial, because the interaction between irrigation and a measurable change in soil moisture is not actually binary. In fact, the sensor – depending on the position of the end of the tube and the sensor in the pot – sometimes measures more moisture than the plant has available and sometimes less. An algorithm that continues to water until the sensor reports moisture could drown the plant. At the same time, digital gardeners need to take into account that different plants consume different amounts of water.
The sensor readings determined with the program in Listing 1 [8], which simply switches a sensor on and reads the value, provide a starting point. If the soil is dry, the return value is around 840. If the sensor is in water, the value is 500. The next step is to investigate the behavior of the sensor when the plant is watered while the sensor is in the soil and a program reads the values regularly.
Listing 1
First Test
01 import datetime 02 import time 03 import Adafruit_GPIO.SPI as SPI 04 import Adafruit_MCP3008 05 import RPi.GPIO as GPIO 06 07 SPI_PORT = 0 08 SPI_DEVICE = 0 09 mcp = Adafruit_MCP3008. MCP3008(spi=SPI.SpiDev(SPI_PORT, SPI_DEVICE)) 10 11 port=4 12 GPIO.setmode(GPIO.BCM) 13 GPIO.setup(port, GPIO.OUT) 14 while True: 15 GPIO.output(port,1) 16 time.sleep(5) 17 hum = mcp.read_adc(0) 18 print (str(datetime.datetime.now())+"Sensor: "+str(hum)) 19 GPIO.output(port,0) 20 time.sleep(55)
In the setup, I first plugged the sensor in at the edge of the flowerpot and put the end of the hose in the middle. After the pump had been running for five seconds, the reading jumped to 620, but the water seeped away quickly: After only a few minutes, the reading was back to 820, indicating dryness. After two further pump strokes of five seconds, the measured value leveled off at 800 and then stopped falling for a while.
For the ficus houseplants I used in the experiments, the official care recommendation is: "Water once a day, the plant needs less in winter." From this, I derived the watering algorithm shown in Listing 2. For each flower pot, the program has to set the values for the unit of time, the threshold value, and X and Y individually. Preferably these values are read from a configuration file.
Listing 2
Watering Algorithm
01 Measure the humidity once per unit of time. 02 If the measured value is greater than the threshold value, then: 03 Water for X seconds. 04 Measure again after Y seconds (Y is smaller than the unit of time). 05 If the threshold value is not reached, then: 06 Add an extra shot of water 07 Goto 01
On this basis, I developed the Python code in Listing 3, which runs in multiple threads. Each thread serves a plant pot with a sensor and relay. The config.yml
configuration file in Listing 4 contains the time and threshold values, as well as the GPIO ports for switching the relays on and off and the channels on which the sensors are connected to the A/D converter.
Listing 3
Watering Program
001 import datetime 002 import time 003 import Adafruit_GPIO.SPI as SPI 004 import Adafruit_MCP3008 005 import RPi.GPIO as GPIO 006 import threading 007 import yaml 008 import pprint 009 import smtplib 010 from influxdb import InfluxDBClient 011 012 class PotThread(threading.Thread): 013 # args is a pot-dict 014 def __init__(self, group=None, monitoronly=False, influxclient=None, target=None, threadname=None, debug=None, args=()): 015 threading.Thread.__init__(self, group=group, target=target, name=threadname) 016 if 'pot' in args: 017 self.potconfig=args['pot'] 018 else: 019 self.potconfig={} 020 if threadname: 021 self.threadname=threadname 022 else: 023 if self.potconfig and "name" in self.potconfig: 024 self.threadname = self.potconfig['name'] 025 else: 026 self.threadname = "Unknown" 027 self.debug = debug 028 self.active = True 029 self.influxclient = influxclient 030 self.monitoronly = monitoronly 031 032 def run(self): 033 measurements = [] 034 while True: 035 if self.active: 036 humidity = self.get_sync_humidity(self.potconfig['sensorchannel'], self.potconfig['sensorgpio']) 037 if self.debug: 038 print (str(datetime.datetime.now())+" "+self.threadname+" Humidity: "+str(humidity)) 039 if self.influxclient: 040 measurement = { 041 'measurement': 'humidity', 042 'tags': { 043 'name': self.threadname 044 }, 045 'time' : time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), 046 'fields': { 047 'level':humidity 048 } 049 } 050 measurements.append(measurement) 051 try: 052 self.influxclient.write_points(measurements) 053 measurements=[] 054 except: 055 print ("Influx failed for "+self.threadname) 056 057 if humidity > int(self.potconfig['limitval']): 058 if self.debug: 059 print (str(datetime.datetime.now())+" "+self.threadname+" Pump on ") 060 self.pump_on(self.potconfig['pumpseconds'], self.potconfig['relaygpio']) 061 if self.debug: 062 print (str(datetime.datetime.now())+" "+self.threadname+" Pump off ") 063 064 time.sleep(self.potconfig['measuringbreak2']) 065 humidity2 = self.get_sync_humidity(self.potconfig['sensorchannel'], self.potconfig['sensorgpio']) 066 if humidity2 > int(self.potconfig['limitval2']): 067 if self.debug: 068 print (str(datetime.datetime.now())+" "+self.threadname+" Pump on ") 069 self.pump_on(self.potconfig['pumpseconds2'], self.potconfig['relaygpio']) 070 if self.debug: 071 print (str(datetime.datetime.now())+" "+self.threadname+" Pump off ") 072 073 time.sleep(self.potconfig['measuringbreak']) 074 075 def pump_on(self, seconds, gpio): 076 if not self.monitoronly: 077 GPIO.setmode(GPIO.BCM) 078 GPIO.setwarnings(False) 079 GPIO.setup(gpio, GPIO.OUT) 080 GPIO.output(gpio, 0) 081 time.sleep(int(seconds)) 082 GPIO.output(gpio, 1) 083 084 def get_sync_humidity(self, sensorchannel, sensorgpio): 085 SPI_PORT = 0 086 SPI_DEVICE = 0 087 lock = threading.RLock() 088 lock.acquire() 089 mcp = Adafruit_MCP3008.MCP3008(spi=SPI.SpiDev(SPI_PORT, SPI_DEVICE)) 090 GPIO.setmode(GPIO.BCM) 091 GPIO.setwarnings(False) 092 GPIO.setup(sensorgpio, GPIO.OUT) 093 GPIO.output(sensorgpio, 1) 094 time.sleep(3) 095 hum = mcp.read_adc(sensorchannel) 096 GPIO.output(sensorgpio, 0) 097 mcp._spi.close() 098 lock.release() 099 return(hum) 100 101 def set_active(self, active): 102 if self.debug: 103 print (self.threadname+" Setting active to: "+str(active)) 104 self.active = active 105 106 if __name__ == '__main__': 107 configfile = open("config.yml", "r") 108 configyml = configfile.read() 109 configfile.close() 110 config=yaml.load(configyml, Loader=yaml.Loader) 111 influxclient = None 112 if "influx" in config: 113 influxclient = InfluxDBClient(config['influx']['server'], 8086, config['influx']['user'], config['influx']['password'], config['influx']['database']) 114 debug = config['debug'] 115 monitoronly = False 116 if "monitoronly" in config: 117 monitoronly = config["monitoronly"] 118 children = [] 119 for pot in config['pots']: 120 pt = PotThread(debug=debug, influxclient=influxclient, monitoronly=monitoronly, args=(pot)) 121 pt.start() 122 children.append(pt) 123 124 tankpot = {} 125 tankthread = PotThread(monitoronly=True, debug=debug, args=(tankpot)) 126 while True: 127 tankhum = tankthread.get_sync_humidity(config['tank']['sensorchannel'], config['tank']['sensorgpio']) 128 if debug: 129 print ("Tank: "+str(tankhum)) 130 if tankhum > config['tank']['limitval']: 131 mailserver = smtplib.SMTP(config['mail']['server']) 132 mailserver.sendmail(config['mail']['from'], config['mail']['to'], "Please fill water tank") 133 mailserver.quit() 134 print ("Please fill tank") 135 for pot in children: 136 pot.set_active(False) 137 else: 138 for pot in children: 139 pot.set_active(True) 140 141 time.sleep(config['tank']['measuringbreak'])
Listing 4
config.yml
01 pots: 02 - pot: 03 sensorchannel: 1 04 sensorgpio: 19 05 pumpseconds: 15 06 pumpseconds2: 5 07 limitval: 825 08 limitval2: 805 09 measuringbreak: 60 10 measuringbreak2: 1200 11 relaygpio: 16 12 name: ficus 13 - pot: 14 sensorchannel: 2 15 sensorgpio: 26 16 pumpseconds: 10 17 pumpseconds2: 5 18 limitval: 825 19 limitval2: 785 20 measuringbreak: 60 21 measuringbreak2: 1200 22 relaygpio: 20 23 name: orchid 24 - pot: 25 sensorchannel: 3 26 sensorgpio: 13 27 pumpseconds: 10 28 pumpseconds2: 5 29 limitval: 825 30 limitval2: 785 31 measuringbreak: 60 32 measuringbreak2: 1200 33 relaygpio: 21 34 name: dickblatt 35 36 tank: 37 sensorchannel: 0 38 sensorgpio: 17 39 measuringbreak: 7200 40 limitval: 830 41 42 mail: 43 server: 1.2.3.4 44 from: watering@local 45 to: gaertner@local 46 47 influx: 48 server: 2.3.4.5 49 user: garten 50 password: gartenpw 51 database: gartendb 52 53 debug: True 54 monitoronly: True
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.
-
Fedora 41 Released with New Features
If you're a Fedora fan or just looking for a Linux distribution to help you migrate from Windows, Fedora 41 might be just the ticket.
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.
-
VirtualBox 7.1.4 Includes Initial Support for Linux kernel 6.12
The latest version of VirtualBox has arrived and it not only adds initial support for kernel 6.12 but another feature that will make using the virtual machine tool much easier.
-
New Slimbook EVO with Raw AMD Ryzen Power
If you're looking for serious power in a 14" ultrabook that is powered by Linux, Slimbook has just the thing for you.
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.