Introduction: Irrigation Using Google Calendar!
This Instructable is about making a Google-Calendar-controlled irrigation programmer based on a ESP8266.
How to use it?Add a task to your Google Calendar and the irrigation of your garden/lawn will start when the event begins and stop when it ends!
You love this Ible? Vote here: https://www.instructables.com/contest/Microcontroller2017/
Why I invented that? I don't like the interface of cheap irrigation programmers, the irrigation parameters are not flexible enough and it can't be controlled remotely. I wanted a cheap wifi irrigation programmer but I didn't want to build the entire UI for the ESP8266, I preferred to be able to change the irrigation planning from anywhere with simple means. I decided to have the ESP polling one of my Google Calendars, with events to turn on and off the valve at the correct timing.
How is this made possible? A ESP8266 connects to your Google Calendar via your home Wifi (using a Google Script) and commands a solenoid valve.
What skills are needed? Basic electronics and software programming skills.
What tools are needed? Basic electronics tools, a RS232 USB programmer and a computer.
I had a basic irrigation programmer that was useless because its display was broken. I scavenged the solenoid valve and the enclosing for this project.
Step 1: Parts
We will need several electronic parts:
- 3 x Capacitors (22µF electrolytic, 0.1µF ceramic, 4700µF electrolytic)
- 1 x DC motor driver (DRV8872 in this case)
- 1 x ESP8266 (version ESP07)
- 1 x 3.3V Voltage regulator (LT1529-3.3 for instance)
- 1 x 12V CC 1.5A charger
- 1 x Female plug (same size as the charger's plug)
- 1 x Prototyping board
- 1 x Latching solenoid valve (scavenged from an Orbit 58874N)
- 3 x Momentary push buttons
- 1 x Rocker switch
- 3 x Straight male headers
- Some wires
Some basic tools: USB RS232 programmer, pliers, soldering iron, etc.
And a Google Account.
The fact that the valve is a "latching" type (also known as "bi-stable") is important. We can drive it with a DC motor driver/H-bridge. Main advantage: it doesn't require energy to remain open or closed. Main drawback: it is not fail-safe, if for some reason the power goes off then the water will continue to flow if the valve was open (we will add a safety procedure in the ESP's firmware to mitigate that risk).
Step 2: Build an Adapter for the 8-pin SOP Circuit
The DC motor driver I have is in a 8-pin SOP package, it is very tiny and I need to build a way to connect it to my prototype board. This is a very delicate process so take your time and be patient.
- Flatten the chip's pins
- Flatten the tip of a resistor wire
- Bend the flattened end
- Solder it to the heat dissipator/sink of the chip
- Cut and solder the wire to the GND of your board
- For each chip's pin, repeat the process
Step 3: Build the Circuit
Solder the parts as illustrated (explanations below).
Connect the voltage regulator (datasheet) with basic components as follow:
- VIN pin to 12V CC power source
- OUTPUT pin to SENSE pin
- GND pin to board's GND
Add a 22µF capacitor between OUTPUT and GND as close to the regulator as possible.
Connect the driver (datasheet) with basic components as follow:
- GND pin to board's GND
- PPAD (heat sink) to board's GND
- VM pin to 12V CC power source
- OUT1 pin to Solenoid red wire
- OUT2 pin to Solenoid black wire
- ISEN to board's GND
Add the bulk capacitor (4700µF) between VM and GND (watch out the polarity) as close to driver's VM pin as possible. Add the 0.1µF capacitor in parallel to the bulk one.
Connect the ESP (documentation) with the switch and buttons as follow:
- One push button between GPIO12 and 3.3V
- One push button between GPIO13 and 3.3V
- One rocker switch between GPIO00 and GND
Use the headers to have an easy access to ESP's Rx, Tx and GND.
Connect the ESP with the voltage regulator as follow:
ESP8266 | LT1521CST-3.3 |
---|---|
VCC | OUTPUT |
GND | GND |
Connect the ESP with the driver as follow:
ESP8266 | DRV8872 |
---|---|
GND | GND |
GPIO04 | nFAULT |
GPIO12 | IN1 |
GPIO13 | IN2 |
Don't forget to connect ESP's GPIO16 pin to its RST pin (internal wakeup).
Step 4: Do Some Preliminary Tests
Connect the charger and push one by one the momentary buttons. You should hear a click, that is the solenoid actuating on the valve.
As we probably don't know in which state the valve is closed or open we will need to perform some tests. Try to blow some air through the valve after having pushed and released one of the two buttons. If the air goes through it means that this button is the one that opens the valve. Repeat the process with the other button to clear any assumption.
Take note of the ESP pin to which the open button is connected, and the one that closes the valve too. We will use it to adjust the ESP's firmware.
Step 5: Create a Dedicated Google Calendar
Many people have a Google Account. This includes the Google Calendar viewer. There you can create as many calendar as you want to.
Create a new calendar dedicated to our irrigation system:
- go to https://calendar.google.com and log in
- click on the arrow close to "My calendars" and select "Create new calendar"
- name this calendar as you wish but keep that name somewhere as we will need it in the next step. "Irrigation" is a good name
- make sure that the time zone is correct according to your irrigation system location
- click on "Create Calendar"
Step 6: Create a Google Script
A Google Script is a way to create an bridge between a Google product, here your calendar, and outside world.
The Script will be the service we are going to poll to obtain the next events date and duration.
To have access to Google Scripts App you need to:
- Go to https://drive.google.com and log in
- Click on New > More > + connect more apps
- In the search box type "scripts" and hit return
- Click the "connect" button of the first result
- Back in Drive, click on New > More > Google Apps Script
- Copy and past the script from my Github: https://github.com/ClemRz/GCalendar-Irrigation
- Change the value of _calendarName with your irrigation-dedicated calendar's name
- Click on save and name this script as you wish
- Click on Run > doGet
- An authorisation popup should show up, click on "Review Permissions" then "Allow"
- Click on Publish > Deploy as web app...
- Fill the fields as in the screenshot and hit "Deploy"
- Copy the url that is provided
You can also change the _checkInRate value which corresponds to the polling rate.
If you want to have a more reactive system, for instance that would detect a newly added event in the minute, change this variable to a lower value. The main drawback of having low values is that your ESP is going to have to poll more frequently i.e. using more bandwidth and more energy.In the other hand, if you don't care about reactiveness but want a more energy efficient system, increase this value.
Note: if you change something in your script after publishing you can test using the dev. url in the Deploy window ("Test web app for your latest code."). But the changes won't be reflected to the public url until you update the deploy parameters with a new version (choose "new" instead of the version number).
Step 7: [Optional] Explanation of the Script
Here are some details about the Google Script.
this is where you adjust the name of your calendar and the polling rate:
var _calendarName = 'Irrigation', _checkinRate = 5 * MINUTE;
doGet is the function that's going to be called when you access the URL. It returns a text file (not an HTML one) with the result of the getOuput function:
return ContentService.createTextOutput(getOutput(_calendarName, _checkInRate));
The only goal of this function is to transform a JavaScript plain object (obtained by calling getResponse) into a JSON string:
return JSON.stringify(getResponse(calendarName, offset));
getResponse has the responsibility to create the plain object depending on several situations.
We first try to access to the calendar:
var cal = CalendarApp.getCalendarsByName(calendarName)[0];
If the calendar is not accessible for any reason (typo in the name for instance) then we return an error object:
if (!cal) { return {status: 'error', message: 'Error, calendar "' + calendarName + '" does not exist.'}; }
If we could open it, then we will get the events (items) that are soon to happen:
var now = new Date(), later = new Date(); later.setSeconds(now.getSeconds() + offset); var items = cal.getEvents(now, later), length = items.length, response = {};
If there are some events then we format the object accordingly:
if (length){ response = getEvent(items[0]); if (response.nextCheckIn > offset) response.nextCheckIn = offset;
We also adjust the next checkin rate so it is never greater than the polling rate.
If not, then we let the callee know that it needs to close the valve. Apart from indicating that an event has possibly ended, this also covers the case when the valve could have remained unintentionally open.
} else { response = {closeValve: true, nextCheckIn: offset}; }
We also let the callee know that everything went well by wrapping the response this way:
return {status: 'success', data: response};
The getEvent function basically do some maths and logic to determine when should happen the next checkin and if the valve needs to be opened or closed.
var now = new Date(), title = item.getTitle(), startTime = item.getStartTime(), endTime = item.getEndTime(), fromNow = startTime - now, startsWithin = fromNow < 0 ? 0 : Math.round((fromNow)/1000), lasts = Math.round((endTime - (fromNow < 0 ? now : startTime))/1000), response = {title: title};
If the event already started then fromNow will be negative, we let the callee know that it needs to open the valve and the next checkin will correspond to the end of the event:
if (fromNow <= 0) { response.openValve = true; response.nextCheckIn = lasts;
If the event is not started yet then we let the callee know that the valve needs to remain closed and the next checkin will be the starting date of the next event:
} else { response.closeValve = true; response.nextCheckIn = startsWithin; }
We increment the nextCheckIn to prevent returning zero. If the ESP try to deep-sleep for 0 microseconds it would never wakeup by itself and it would be catastrophic:
response.nextCheckIn++; return response;
Step 8: [Optional] Log the Interactions in a Spreadsheet in Order to Have Visual Control
If you want to make sure that your irrigation system is actually calling your Google Apps Script I recommend to do the following:
- Create a new spreadsheet in your Drive, name it "Irrigation"
- Rename the first sheet to "data"
- Add some headers to the first row: "Time" and "Data"
- Copy the ID of your spreadsheet, you will find it in the URL
- Add this function to your Script:
function logToSpreadsheet(data) { var ss = SpreadsheetApp.openById('paste_here_your_spreadsheets_id'), sheet = ss.getSheetByName('data'); sheet.appendRow([new Date(), data]); }
- Modify the getOutput function:
function getOutput(calendarName, offset) { var response = JSON.stringify(getResponse(calendarName, offset)); logToSpreadsheet(response); return response; }
- Publish a new version of your Script (Publish > Deploy as webapp > Project version: new > Update)
Wait for the ESP to call the Script, you should see a new row of data appended to the "data" sheet. This is the proof that your system is actually reaching the Script.
Step 9: Upload the Firmware to the ESP
There are tons of tutorials in the Internet on how to upload firmwares to the ESP. I also wrote a DIY which contains this part here: https://www.instructables.com/id/IoT-Door-Alarm-UPGRADED/
- Download and install those libraries in Arduino/libraries folder:
- ArduinoJson: https://github.com/bblanchon/ArduinoJson
- HTTPS redirect: https://github.com/electronicsguy/ESP8266/tree/master/HTTPSRedirect
- Download the firmware from my github: https://github.com/ClemRz/GCalendar-Irrigation/tree/master/software%20design/ESP8266_gcalendar_irrigation
- Open it with the Arduino IDE
- Change the following parameters:
- SSID: your home wifi's network name
- PASSWORD: your home wifi's password
- SCRIPT_ID: your Google Script ID (from the URL you copied previously)
- CLOSE (pins allocation section): the ESP's input connected to the close button (you took note of it previously)
- OPEN (pins allocation section): the ESP's input connected to the open button (you took note of it previously)
- Set the ESP in programming mode (GPIO0 connected to GND)
- Hit the upload arrow
- Set the ESP in running mode (GPIO0 disconnected from GND)
I wrote this code getting inspiration from https://github.com/wilda17/ESP8266-Google-Calendar... and https://coertvonk.com/technology/embedded/esp8266...
Step 10: [Optional] Explanation of the ESP Firmware - Main Functions
The main sketch of the ESP's firmware is explained here.
In the setup function we initialise the Serial port, the pins (IO stands for Input Output) and the connection to the Wifi network (see next steps for more details):
void setup() { #if DEBUG initSerial(); #endif initIO(); initWiFi(); }
In the loop function we try to contact the Google Script.
We first check if we are connected to the network:
void loop() { if (_attempts <= MAX_WIFI_ATTEMPTS) {
In the case we are connected, we first reset the attempts counter and then try to get the Google Script's response (more details about httpsGet in next steps):
_attempts = 0; String response = httpsGet();
Then we test if the response is not empty (successful answer):
if (response != "") {
And we process the message returned by the Google Script (more details about process below):
process(response);
If the answer was empty then we close the valve as a safety measure (more details about closeValve in next steps):
} else closeValve();
If we are not connected to the network then we close the valve as a safety measure:
} else closeValve();
We finish by setting the ESP asleep (more details about sleep below):
sleep(); }
The process function will decide what should be done depending on the Google Scrip's answer.
We first deserialise/parse the response as a JSON object and check the consistency of that object:
void process(String response) { StaticJsonBuffer<200> jsonBuffer; JsonObject& root = jsonBuffer.parseObject(response); if (root.success()) {
If the object is consistent then we check the status returned by the Google Script:
String status = root["status"]; if (status == "success") {
If the answer is a success then we extract the polling rate:
_pollingRate = root["data"]["nextCheckIn"];
We do a sanity check on the polling rate, the ESP cannot sleep for more than 71 minutes:
if (_pollingRate > MAX_SLEEP_TIME) _pollingRate = MAX_SLEEP_TIME;
If the answer indicates that the valve needs to be opened then we do so (more details about openValve in next steps):
if (root["data"]["openValve"]) openValve();
If the answer indicates that the valve needs to be closed then we do so:
if (root["data"]["closeValve"]) closeValve();
If the status is not a success then, as a safety measure then we close the valve:
} else { #if DEBUG Serial.print(F("Unsuccessful response: ")); Serial.println(status); #endif closeValve(); }
If the object is not consistent then, as a safety measure then we close the valve:
} else { #if DEBUG Serial.println(F("Failed to parse JSON")); #endif closeValve(); } }
The sleep function puts the ESP into deep sleep mode for a specific amount of time, in this case the polling rate. If the value of _pollingRate is equals to zero then the ESP will never wakeup by itself, this is the reason why we do a sanity check on it.
void sleep() { #if DEBUG Serial.print(F("Go to sleep for ")); Serial.print(_pollingRate); Serial.println(F("s.")); #endif //DEBUG if (_pollingRate == 0) _pollingRate = 1; ESP.deepSleep(_pollingRate * MICROSEC); }
Step 11: [Optional] Explanation of the ESP Firmware - Initialisation Functions
The initialisation sketch of the ESP's firmware is explained here.
In initSerial we initialise the Serial communication for debug messages:
void initSerial(void) { Serial.begin(9600); Serial.println(); //Serial.setDebugOutput(true); }
In initIO we set the OPEN and CLOSE pins as outputs and the N_FAULT pin as an input, using the internal pullup resistor (see driver's datasheet):
void initIO(void) { pinMode(N_FAULT, INPUT_PULLUP); pinMode(OPEN, OUTPUT); pinMode(CLOSE, OUTPUT); }
Step 12: [Optional] Explanation of the ESP Firmware - Valve Functions
The initialisation sketch of the ESP's firmware is explained here.
The openValve and closeValve functions call the actuateValve passing a boolean as an argument:
void openValve(void) { actuateValve(true); } void closeValve(void) { actuateValve(false); }
The actuateValve simply decides which pin needs is concerned depending on the argument. As this is a latching (bi-stable) valve we set the pin high for a small amount of time (10ms here). In between we check if the driver indicates a fault:
void actuateValve(bool openValve) { #if DEBUG Serial.print(openValve ? F("Open") : F("Close")); Serial.println(F(" the valve.")); #endif int pin = openValve ? OPEN : CLOSE; digitalWrite(pin, HIGH); delay(5); lookForFault(); delay(5); digitalWrite(pin, LOW); }
To detect if there is a fault we read the N_FAULT pin. Looking a the driver's datasheet we know that there is a fault when this pin is low. We log a message when we detect a fault. You can add some logic here to send an email for instance:
void lookForFault(void) { bool isInFault = digitalRead(N_FAULT) == LOW; #if DEBUG if (isInFault) { Serial.println(F("Driver is showing a fault")); } #endif }
Step 13: [Optional] Explanation of the ESP Firmware - Wifi Functions
The wifi sketch of the ESP's firmware is explained here.
We first enable the persistent mode. This allows the ESP to "memorise" the network it was connected to previously and speeds up the connection process. Then we check if we are allready connected to a network:
void initWiFi(void) { #if DEBUG Serial.println(F("Start WiFi")); #endif WiFi.persistent(true); if (WiFi.status() != WL_CONNECTED) {
If we are not yet connected to any network then we request the connection. We periodically check the status of the connection. This loop is limited by a max attempts amount to prevent lockups. The yield function allows task to be ran on background and prevents other types of lockups.
WiFi.begin(SSID, PASSWORD); while (WiFi.status() != WL_CONNECTED && _attempts <= MAX_WIFI_ATTEMPTS) { yield(); delay(500); #if DEBUG Serial.print(F(".")); #endif _attempts++; } }
Once we either are connected to the network, or reached the max attempts amount, we log some information:
#if DEBUG Serial.println(); if (_attempts > MAX_WIFI_ATTEMPTS) { Serial.print(F("Failed to connect to ")); Serial.println(SSID); } else { Serial.print(F("Connected to ")); Serial.println(SSID); Serial.print(F("IP address: ")); Serial.println(WiFi.localIP()); Serial.print(F("Mac addresss: ")); Serial.println(WiFi.macAddress()); } #endif }
Step 14: [Optional] Explanation of the ESP Firmware - Client Functions
The client sketch of the ESP's firmware is explained here.
The httpsGet function has the responsibility to get the response from the Google Script.
We try to connect to the Google Script's host several times before giving up:
String httpsGet(void) { HTTPSRedirect* client = new HTTPSRedirect(HTTPS_PORT); bool connected = false; for (int i=0; i<MAX_HTTPS_ATTEMPTS; i++){ yield();
We check the https connection status:
int retval = client->connect(HOST, HTTPS_PORT); if (retval == 1) {
If we are connected we set up a flag and exit the loop:
connected = true; break;
If we are not connected we output some debug information and loop again after a small delay:
#if DEBUG } else { Serial.println(F("Connection failed. Retrying...")); #endif } delay(HTTPS_REINTENT_DELAY*MILLISEC); }
If we couldn't connect, after several attempts, then we return an empty string:
if (!connected) { #if DEBUG Serial.print(F("Could not connect to server")); #endif return ""; }
If we could connect to the host then we request the url:
if (client->GET(URL, HOST)) {
If the server answered successfully we get the response it sent:
String payload = client->getResponseBody(); #if DEBUG Serial.println(F("HTTP Response: ")); Serial.println(payload); #endif
If the response contains the success phrasing then we return the message:
if (payload.indexOf(SUCCESS) >= 0) return payload;
If not, we return an empty string:
#if DEBUG Serial.print(F("Script returned failure message: ")); Serial.print(payload); #endif return "";
In the case we couldn't connect to the URL then we return an empty string:
} else { #if DEBUG Serial.print(F("Couldn't get the body, code: ")); Serial.print(client->getStatusCode()); Serial.print(F(", message: ")); Serial.println(client->getReasonPhrase()); #endif return ""; }
Finally, we free the memory from the connection information:
delete client; client = NULL; }
Step 15: Test the Calendar Connectivity
Set up an event which starts within a few minutes and last 2 minutes and check that the valve reacts properly according to that timing.
Step 16: Protect the Circuit in an Enclosure
I reused the original programmer's enclosure.
Step 17: Final Thoughts
This project was a real challenge to figure out. But it was fun to build and is quite cheap and reliable!
Here are some nice features:
- You can add recurring events in your calendar for quick setup
- We can think of controlling multiple irrigation systems in one single place
- The calendar can be shared to be collaborative if you need more people to control the irrigation system
- Using an automation third party system like IFTTT we can add events automatically depending on the weather for instance
- ...
The main drawbacks of this device are:
- Useless when the power or the Internet/network goes off
- Needs a Wifi signal close/strong enough
- Needs a power source
Some enhancements can be added (power on batteries, hall-effect sensor to have a feedback on the position of the valve, etc.) so feel free to comment and post your own project below! :)
Enjoy.

Runner Up in the
Microcontroller Contest 2017
45 Comments
10 months ago
Thank you for sharing. It's a very good idea. But alas, the code is not operational at all. The httpsGet function is missing. Although it is explained in the project, it is not in the code. Functions
initSerial , initWifi and initIO are not declared. No comment in the code to have an explanation. Unfortunately, an unusable share as it is.
Reply 10 months ago
Are you sure you're using the ESP8266 libraries / Arduino configuration? You must select it in "boards".
Those functions are supposed to be part of the native API.
Question 3 years ago on Step 10
Hi, thanks for sharing. I have try your code and get problem at setup function to initialize /* initSerial (); initIO (); initWifi (); */ that it was not declared in the scope. If you know this problem that would be very helpful for me. Thanks
Answer 3 years ago
It seems that there is something wrong with the parenthesis. initSerial is a Arduino core function and should work no mater what. Let me know.
5 years ago
Hi, ClemRz. I like this tutorial and try to make it. I can't understand where did you get the 'offset' value in Google Script?!
Reply 5 years ago
Hi, The latest version is located at https://github.com/ClemRz/GCalendar-Irrigation/blo...
offset is a parameter that the functions getOutput and getResponse are expecting. In the function doGet, we make a call to getOutput passing two values: _calendarName and _checkinRate, those values are then accessible within getOutput function under the name of calendarName and offset. The same applies to the getResponse function.
This code is written in JavaScript. If you want to know more about this programming language I suggest you do some search on the Internet ;)
Reply 5 years ago
Noted and thanks for your quick reply~
This project is really amazing! Thanks for sharing ~
Reply 5 years ago
Thank you!
5 years ago
will this work with the newest version of google calendars?
Reply 5 years ago
AFAIK the newest version brought changes to the UI only. The API embedded in the Google App Script should not be impacted.
5 years ago
I understand your interest in making this project. And applaud you for the interface of both hardware and software to accomplish the results. I have used a varied number of sprinkler timers and for the most part they are rock solid and work for years on end. They are for the most part multiple timers with SCR gates to channel the AC voltages to the electric valves. Easy to make and easy to program. In fact the one I am presently using is decades old and still working perfectly, as well. Can I change the setting remotely, NO. but I have no need to do that once everything is programmed. I been using the same program for decades as well. All that stated, you did make a great project and if anybody wants to duplicate your setup, you gave equally great instructions.
Reply 5 years ago
Thanks!
Reply 5 years ago
Im amazed that you actually accomplished this. im trying to make a button who is "red" the days on an "ordering day"(when a calender event is there) and green if you pressed a button that day. do you think this is possible with this code as a base?
Reply 5 years ago
Thanks. I think your use-case is very close to what I accomplished. You should be able to reuse great part of the code.
You are building a kind of reminder? Sounds interesting! =D
5 years ago
hi,
perfect for me! this is what I am looking for. but I have a problem, and I am not able to fix it. I need to pilot 3 relay mono-stable, and I don't know how to modify your project. may you help me?
thanks
Reply 5 years ago
Hi, thank you! Please could you post a functional diagram of what you aim to do?
Reply 5 years ago
hi, thanks for your time. differences are that calendar need 3 different events, that pilot 3 relay. relays are monostable so I don't need close command, but when put down out, relay turn off.
Reply 5 years ago
Hi, I guess what you can do is use a naming convention for your events that you would be able to use in your arduino script to decide which relay to de/activate, the member "title" is already sent by the Google Script.
Modify the code of the "process" function:
if (root["data"]["title"] == "Relay 1") openRelay1();
if (root["data"]["title"] == "Relay 2") openRelay2();
etc.
It would actually be better to use a "switch" structure instead of the "ifs".
You know what I mean?
Feel free to comment if you have more questions!
If you like, vote here: https://www.instructables.com/contest/Microcontroller2017/
Thanks ;)
Reply 5 years ago
Hi ClemRz great job. I'm studying your project as my fist approach to Arduino. I found the helferb suggestion interesting and I'm asking why you don't think to realease an updated version considering different irrigation zones. This will probably also needs to modify the parser in the Google Calendar events in order to associate each calendar event with a specific zone/valve (i.e. Zone1, Zone2, ....). Thanks for your time.
Reply 5 years ago
Hi, finally i had time to tried it, but i had some problems.
In esp skecth, deepSleep func make a soft reset, but i need to maintain gpio status, so i must try somethings else. Have you a tip?
In Google script (sorry but i never use script), i don't know how to modify it in order to use your suggestion. How to change open/close with only open procedure.
i hope you may understand what i try to explain. (sorry for my english :) )