Having plants at home is a luxury that only households that always count on someone’s presence can enjoy. If you live alone and travel for a few days, you are likely to return home and find a mummified version of your beloved basil plant.
One may argue: “Why not buy a timer on Home Depot for 5 bucks?”. That’s an excellent point, but why not make it overly complicated and learn a couple of things during the process?
In this article, I will show you how I created an IoT application that controls the irrigation of the plants in my house. This application independently irrigates 5 pots: 1 lemon tree, 2 passion fruit plants, 1 gardenia, and 1 pot with herbs (basil, fennel, and oregano). Because I’m planting these plants on the balcony of my apartment, I don’t have running water, so I’m using a 40 liters tank. This tank has an ultrasonic sensor that monitors the volume and once the water level is below 30%, my Losant app sends me an email telling me to fill the tank. Well, to be honest, it sends an email to my wife, I hate filling that tank.
Besides that, the system gets the current temperature and humidity here in Houston, TX, and based on that increases the volume of irrigation water to compensate for the extra heat. I realized that I needed to implement this feature once I was done with the hardware, so I’m pulling the temp info from a free weather API. It’s pretty cool that Losant allows you to integrate info from so many different sources.
The Hardware
The Electronic Enclosure
The electronics are very simple, these are the components of this project:
- Microprocessor: Particle Photon (https://www.particle.io)
- Power supplies (5 and 12 Volts)
- 6 Relays. Caveat: I have used mechanical relays because I’ve also used a switching power supply. SSD relays can switch as fast as your switching power supply and mess up with the output.
- Wifi antenna
The basic idea is the following:
- 5V feed the Particle Photon
- Photon’s outputs connect to the low power side of the relays driving 12V to the valve solenoids upon activation.
Similarly, the pump (underwater aquarium pump) is controlled by the Photon which drives AC current through its motor. Lets put everything together in a nice, presentable box.
Remember, if you use a metallic enclosure like I did, you need to ground the box. I can’t stress this enough!
The Water Manifold
I bought the cheapest solenoid valves I could get from AliExpress.com, I think I paid 3 bucks for each. This is not bad at all, in the US they go for 10 bucks each. I also got some valves, tubing, and an aquarium manifold. I’ve put everything together in a fancy plastic box. Note that the electrical connection from this wet module to the electronics is done by a multi-core cable and a connector. The final result is pretty neat.
Firmware
The Particle Photon, like Arduino, uses C++. It has a some conveniences like Particle.function() and Particle.publish(). These features allow the user to trigger functions from external events and publish local events respectively. In order to trigger functions remotely, you’ll need to declare them in the void setup. In my system I have 7 Particle functions, they are:
- tooHot: changes the value of the boolean variable hot to TRUE if the local temperature is greater than 30 Celsius. The data is retrieved from the weather API and the logic is executed in Losant.
- mode: activates the manual mode.
- pf1, lemon, herbs, gardenia, pf2: water each plant in manual mode.
These Particle functions only accept one argument and it must be a String. Conversely, you can publish events with Particle.publish(). Please check out my code below and next we will see how to create an interface to interact with the microcontroller.
// ports to control and read hardware
const int V1 = 0;
const int V2 = 1;
const int V3 = 2;
const int V4 = 3;
const int V5 = 4;
const int pump = 5;
const int trigger = 6;
const int echo = 7;
// tank volume measurement
int volume;
// temperature alarm
bool hot = FALSE;
// manual mode
bool manual_mode = FALSE;
void setup() {
// declaring particle functions
Particle.function("tooHot", tooHot);
Particle.function("mode", mode);
Particle.function("pf1", pf1);
Particle.function("lemon", lemon);
Particle.function("herbs", herbs);
Particle.function("gardenia", gardenia);
Particle.function("pf2", pf2);
// defining i/o's
pinMode(trigger, OUTPUT);
pinMode(echo, INPUT);
pinMode(pump, OUTPUT);
pinMode(V1, OUTPUT);
pinMode(V2, OUTPUT);
pinMode(V3, OUTPUT);
pinMode(V4, OUTPUT);
pinMode(V5, OUTPUT);
// setting time zone
Time.zone(-5);
// initiallizing actuators
digitalWrite(V1, HIGH);
digitalWrite(V2, HIGH);
digitalWrite(V3, HIGH);
digitalWrite(V4, HIGH);
digitalWrite(V5, HIGH);
digitalWrite(pump, HIGH);
Serial.begin(9600);
}
void loop() {
// selects the u.FL antenna
STARTUP(WiFi.selectAntenna(ANT_EXTERNAL));
if (manual_mode == FALSE){
// morning irrigation schedule
if(Time.hour() == 7 && Time.minute() == 00){
auto_irrigation();
}
// if it's warm, the system water the plants again at 3pm
if (hot == TRUE){
if(Time.hour() == 15 && Time.minute() == 00){
auto_irrigation();
}
}
}
// measure water tank level twice a day
if ((Time.hour() == 8 || Time.hour() == 16) && Time.minute() == 00){
volume = measure_level();
Particle.publish("tank_volume", (String)volume);
}
}
/* FUNCTIONS */
// irrigation
int auto_irrigation(){
digitalWrite(V1, LOW);
digitalWrite(pump, LOW);
delay(80000);
digitalWrite(V1, HIGH);
digitalWrite(V2, LOW);
delay(50000);
digitalWrite(V2, HIGH);
digitalWrite(V3, LOW);
delay(80000);
digitalWrite(V3, HIGH);
digitalWrite(V4, LOW);
delay(40000);
digitalWrite(V4, HIGH);
digitalWrite(V5, LOW);
delay(40000);
digitalWrite(V5, HIGH);
digitalWrite(pump, HIGH);
}
// measure the tank level, averages 100 measurements
int measure_level(){
int result;
long duration = 0;
for (int i = 0; i <= 100; i++)
{
digitalWrite(trigger, LOW);
delayMicroseconds(2);
digitalWrite(trigger, HIGH);
delayMicroseconds(10);
digitalWrite(trigger, LOW);
duration = duration + (pulseIn(echo, HIGH)/100);
delay(50);
}
result = 100*(25.4-duration*0.017)/25.4;
return result;
}
/* this function changes the boolean value of the variable hot and its logic is executed on Losant.
the weather api provides the temp and if greater than 30C, it will trigger toohot. */
int tooHot(String command){
if (command == "yes"){
hot = TRUE;
return 1;
}
else if (command == "no"){
hot = FALSE;
return 0;
}
else {
return -1;
}
}
// this function changes the logic value of manual_mode and it's triggered on losant.
int mode(String command){
if (command == "true"){
manual_mode = TRUE;
return 1;
}
else if (command == "false"){
manual_mode = FALSE;
return 0;
}
else {
return -1;
}
}
// passion fruit 1 manual irrigation
int pf1(String command){
if (manual_mode == TRUE){
if (command == "true"){
digitalWrite(V1, LOW);
digitalWrite(pump, LOW);
}
else if (command == "false"){
digitalWrite(V1, HIGH);
digitalWrite(pump, HIGH);
}
}
}
// lemon tree manual irrigation
int lemon(String command){
if (manual_mode == TRUE){
if (command == "true"){
digitalWrite(V2, LOW);
digitalWrite(pump, LOW);
}
else if (command == "false"){
digitalWrite(V2, HIGH);
digitalWrite(pump, HIGH);
}
}
}
// herbs manual irrigation
int herbs(String command){
if (manual_mode == TRUE){
if (command == "true"){
digitalWrite(V3, LOW);
digitalWrite(pump, LOW);
}
else if (command == "false"){
digitalWrite(V3, HIGH);
digitalWrite(pump, HIGH);
}
}
}
// gardenia manual irrigation
int gardenia(String command){
if (manual_mode == TRUE){
if (command == "true"){
digitalWrite(V4, LOW);
digitalWrite(pump, LOW);
}
else if (command == "false"){
digitalWrite(V4, HIGH);
digitalWrite(pump, HIGH);
}
}
}
// passion fruit 2 manual irrigation
int pf2(String command){
if (manual_mode == TRUE){
if (command == "true"){
digitalWrite(V5, LOW);
digitalWrite(pump, LOW);
}
else if (command == "false"){
digitalWrite(V5, HIGH);
digitalWrite(pump, HIGH);
}
}
}
IoT Workflow
Receive an email notification: Losant is an IoT platform that allows the developer to create a workflow logic that will receive and trigger events. It’s beyond the scope of this article to provide a detailed Losant tutorial, however, I’ll explain the intuition behind it and give you references in case you decide to develop your own IoT app. Let’s start checking how to implement an email notification to alert the user that the water level is getting low.
First, you’ll need to create a device and a particle integration in Losant. Upon doing that, go to your workflow and build the following block logic.
Let’s examine how to set the nodes to implement this logic.
1. Particle: This node establishes communication with your device. Click on it and select the integration you have created.
2. Function: Remember Particle.publish()? We will set up this node to listen for events published from the Photon. In order to do that, we need to define the structure of the payload with some javascript. The event published by the microcontroller will be transmitted in a JSON package, more precisely inside data. In our case, the variable being passed is ‘volume’, so we can retrieve this data by accessing ‘data.volume’.Click on the function node and add the following code.
var parts = payload.data.data.split(':');
payload.data.volume = parts[0];
3. Device Get: This node will attribute the data from your payload to a device and the attributes you’ve defined upon its creation. This will allow us to create a dashboard later. Select the query method to: ‘Match all tags query’ Set ‘Key Templates’ to ‘particle_device_id’ and ‘Value Templates’ to ‘{{ data.coreid }}’Set ‘Result Path’ to ‘device_to_update’
4. Device State: Stores the state in a device attribute.‘Device ID JSON path’ = ‘device_to_update.id”Attribute’ = ‘{{data.volume}}’
5. Debug: This block allows you to debug and understand what is going on with your payload. You can trigger events from the workflow and see how your payload is being constructed. This block is not necessary to make the app work.
6. Conditional: Executes a conditional logic. Remember you’ll need to refer to the variable according to the way that your JSON payload was defined, in our case {{data.volume}}.
7. Email: This one is pretty self-explanatory. Upon the activation of the condition previously defined, this block will send an email.To summarize, we will retrieve the variable ‘volume’ by setting up a logic to listen to the events being published from the IoT microcontroller. Upon getting this data from a JSON package we will compare it to a threshold and if the value is less than 30% of the total volume, an email will be sent asking the user to re-fill the tank.
Get local temperature from an API
First, you’ll need an API key, go to:
https://openweathermap.org/api
Create an account. You can use their free plan for this project. If I’m not wrong, the free plan includes 1000 API calls a day. If you’re not in the agricultural business, this should be more than enough. Build the following workflow:
The HTTP block is responsible for calling the API. Let’s check on how to set it up. In the ‘URL template’, add the following:
http://api.openweathermap.org/data/2.5/weather?q='YOUR CITY'&appid='YOUR API KEY'
The ‘request method’ should be set to GET and the API response should be stored in the ‘Payload path to store response’ field.
Set it to ‘working.weather’.Set the timer to call the API every hour so you don’t exceed the daily limit of calls.Now your payload should look like this:
Now the temperature should be accessible through:
{{working.weather.body.main.temp}}
Triggering Functions Remotely: Now it’s time to finally trigger our Particle functions previously defined in our firmware. Let’s start by building a workflow:
Basically we have a ‘Virtual Button’ node triggering a ‘Particle Call’ node for each one of our remotely operated functions.In the ‘Particle Call’ node, we need to:
- Inform the node which firmware function we will call by adding its name to the field ‘Function Name’.
- Tell the node what argument we will be passing by specifying it directly or adding a payload path. We will pass a payload path since we will be using a toggle switch to activate the system. We will create variables for each plant and add them to the payload. They will be located at the root data, so they will be accessible as data.pf1, data.lemon, data.herbs, data.gardenia and data.pf2.
Now, we need to create an interface to trigger this workflow we’ve just created. Go to the Dashboard and add an input control. Add toggles and buttons like so:
The toggle state is sent via payload to the microcontroller upon triggering the workflow via the button. Set the button like so (note, the value between the double brackets should be the same as your toggle, my toggle is also named ‘lemon’).
Do the same for all the other inputs and test it:
Data visualization
Add some charts and links to your device’s attributes.
Conclusion
I hope you guys enjoyed this article. This project has several moving parts and I know that I have covered some of them too fast without providing the level of detail that the reader deserves, for that reason do not hesitate to contact me if you’d like to get more info or simply ask a question.
Over 98% of the mass of plants comes from the air–an interesting fact isn’t it? For a step into starting optimizing fertilization and irrigation, I’d like to add a strain gauge to monitor the weight of the plant.