Water your plants with a Raspberry Pi

Cyber Gardener

© Lead Image © lightwise, 123rf.com

© Lead Image © lightwise, 123rf.com

Article from Issue 236/2020
Author(s):

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:

  1. Set up the hardware so that the sensors provide measured values and the pumps can be controlled individually.
  2. Acquire and understand the measured values (e.g., determine how watering changes the measured values).
  3. 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.

Figure 1: The detailed circuit diagram for setting up the irrigation system (Fritzing [5]).

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.

Figure 2: The first order of business is to get the Raspberry Pi talking to the MCP3008 A/D converter.

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.

Figure 3: Raspberry Pi 40-pin GPIO header (Raspberry Pi Foundation CC BY-SA 4.0 [6] [7]).

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.

Figure 4: The circuit diagram for connecting the pumps to the relay module.

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.

Figure 5: The system setup for a single plant irrigation system.

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

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Garden Irrigation

    With a Pi Zero and a few components, you can build an inexpensive and reliable automatic watering system for your plants in next to no time.

  • Perl: Linux-based Gardening

    In this month’s Perl column, we will introduce a system to water your plants while you are away from home. With a little help from Perl, a friendly, Linux-based irrigation system waters your plants twice a day.

  • Gnublin

    Embedded Linux doesn’t have to be rocket science, which the Gnublin board clearly shows. If you want to learn how to read sensors, flip switches, and switch LEDs on ARM Linux, this tiny machine is perfect for your research.

  • Charly's Column – Mi Flora

    Columnist Charly Kühnast recently attached Mi Flora humidity sensors to his potted plants. At first, they only transmitted junk on Bluetooth, but armed with the right tools and a Rasp Pi, Charly now reaps a rich harvest of data.

  • Home Assistant with MQTT

    Automating your four walls does not necessarily require commercial solutions. With a little skill, you can develop your own projects on a low budget.

comments powered by Disqus
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.

Learn More

News