How to make an automatic sprinkler controlled by Telegram with the ESP8266 NodeMCU

Today, information technology and electronics pervade almost every aspect of our lives. There are automatic electronic systems that have many different functions, sometimes they are a little invasive but they are often very useful and help us in our daily activities. One of the fields where certain devices are extremely suitable is gardening, in particular in the automatic control of the irrigation of plants. Personally, I have had the need to remotely control the irrigation of some plants, monitoring some parameters such as the temperature and humidity of the air and the humidity of the soil. I could easily have bought an automatic commercial device ready for this purpose but I preferred to try to make it myself because it’s more fun and instructive and because I could implement the functions I needed.
So I decided to propose it in this blog.

Brief description of the implemented features

Let’s start describing the various features that will be implemented in this project.
The device is able to command a solenoid valve in order to close or open it to interrupt or not the water flow of a garden hose. It is able to detect the temperature and humidity of the air through a special sensor. It can measure the amount of moisture in the soil where the plant is. It is also capable of connecting to the Internet via a WiFi connection on our home modem. What is the Internet connection for? First of all, the device is able to connect to an NTP server in order to automatically set the date and time. Furthermore the Internet connection is used to monitor and control the device via a specially created Telegram bot. Through this bot we can remotely read the temperature and humidity of the air, the humidity of the soil, the status of the solenoid valve (if it is open or closed). We can also read the current date and time and whether the device is set to automatic or manual operation. However, we can also send commands: we can make it work manually (by turning the solenoid valve ON or OFF by giving a command on Telegram) or automatically (by setting, again from Telegram, the solenoid valve ON and OFF times).

Of course, there are many possibilities. The user can develop new features or modify existing ones. For example, it could add more timers so that the device can automatically activate several times a day (or only on certain days of the week). Or decide to activate watering automatically if the soil humidity is lower than a certain value and so on.

What components do we need for the automatic sprinkler?

The list of components is not particularly long:

An example of a twin optoisolated relay module used in this project
An example of a twin optoisolated relay module used in this project

An example of a 9V bistable solenoid valve used in this project
An example of a 9V bistable solenoid valve used in this project
9V battery clip
9V battery clip

We need the 9V battery to power the solenoid valve.

Let’s briefly see how a solenoid valve works

Mainly there are two types of solenoid valves, one monostable and one bistable.
The monostable solenoid valve is fed with an alternating voltage while the bistable with a DC voltage.
In this project we will use a bistable solenoid valve. The bistable solenoid valve has two stable states, one for the closed valve and one for the open valve. Feeding the solenoid, the valve opens and remains open even cutting off the power.
To close it we have to feed the solenoid reversing the polarity, also in this case by removing power the valve remains closed. The main advantage is a minimum electrical consumption, given that to open and close the valve it is necessary to feed it only for a few milliseconds, this allows to feed it using a battery.
The solenoid valve needs one twin optoisolated relay module. The two relay are properly driven by two NodeMCU ESP8266’s digital outputs to apply a direct or inverse voltage to the solenoid valve, depending on whether you want to open or close it.

How is this tutorial structured?

To explain the project as simply as possible, the tutorial will proceed in incremental steps. We’ll start with the simplest implementation and gradually add new features:

  • as a first step we will connect the solenoid valve to the NodeMCU ESP8266 and we will command it with an alternation of cyclical opening and closing commands defined in the loop function;
  • as a second step we will connect the DHT22 humidity and temperature sensor and cyclically read the measured values ​​via the Serial Monitor;
  • as a third step we will add wireless connectivity and Telegram features and we will see how to connect our device to the Internet and how to command/monitor it via the Telegram bot;
  • then we will implement the soil moisture sensor;
  • finally we will use the EEPROM of the NodeMCU to be able to save some settings so that they are maintained even in the event of a power failure.

Let’s connect and test the solenoid valve

First of all let’s connect the ESP8266 NodeMCU, the relay module and the solenoid valve using the breadboard and the wires following the Fritzing diagram below:

Fritzing schematic for the first step
Fritzing schematic for the first step

Currently, the twin relays module is not present in Fritzing but, if you want to use it in your own projects, you can dowload it from here.

As you can see, this schematic is very simple: the two pins Vin and GND from the NodeMCU ESP8266 are used to power the twin relays module, connecting the Vin pin (NodeMCU side) to the VCC pin (twin relays module side) and the two GND (ground) pins.

Two other connections are used to control the relay module and are the orange wire and the brown wire which connect the two digital outputs D1 and D2 of the NodeMCU to the inputs IN1 and IN2 of the relay module.

You will notice that on the relay module there is a jumper (drawn in light blue on the left connector) that connects the JD-VCC and VCC terminals. This jumper is used to power the relay module through the VCC and GND terminals on the right connector. Without this jumper, we are forced to power the module with an external power supply.

Finally, connected to the relay contacts, we have the 9V battery and the solenoid valve so that it can be powered with opposite polarity depending on whether we want to open or close it.

Let’s see now the software to control the solenoid valve. We will use for this step the well-known IDE PlatformIO. If you don’t know how to create a project for NodeMCU ESP8266 with PlatformIO and how to add external libraries to the project, you can read the tutorial How to create a project for NodeMCU ESP8266 with PlatformIO in this blog.

To download the code for this first step of the project, click below:

At the moment the code is only in the main.cpp file. Then, in your newly created project, replace the main.cpp file with the one you downloaded from the link above.
Now let’s see how the code looks like:

#include <Arduino.h>

#define  RELAY_1  5
#define  RELAY_2  4

At the beginning we have the inclusion of the Arduino library (this library was automatically included by PlatformIO) and the definition of the digital pins that we will use to drive the relay module. These pins are GPIO4 and GPIO5 and correspond, on the NodeMCU, to outputs D2 and D1, as we can see from the pinout image below:

The NodeMCU ESP8266 pinout
The NodeMCU ESP8266 pinout

Below we have the implementation of the functions that open/close the solenoid valve (open_valve and close_valve) and those that cut off its power (reset_valve_high and reset_valve_low) putting the two pin at the same potential:

void reset_valve_high() {
  // Sends 0V to the valve setting HIGH both control pins.
  digitalWrite(RELAY_1, HIGH);
  digitalWrite(RELAY_2, HIGH);  
}

void reset_valve_low() {
  // Sends 0V to the valve setting LOW both control pins.
  digitalWrite(RELAY_1, LOW);
  digitalWrite(RELAY_2, LOW);  
}


void open_valve() {
  // Sends a 10ms impulse to open the valve.
  digitalWrite(RELAY_1, LOW);
  digitalWrite(RELAY_2, HIGH);
  delay(10); 
  Serial.println("Valve opened. Water Flowing\n");
}


void close_valve() {
  // Sends a 10ms impulse to open the valve.
  digitalWrite(RELAY_1, HIGH);
  digitalWrite(RELAY_2, LOW);
  delay(10);
  Serial.println("Valve closed. Water not Flowing\n");
}

The solenoid valve is opened by setting pin RELAY_2 to HIGH (i.e. 1) and pin RELAY_1 to LOW (i.e. 0) for 10 ms (line delay(10); ) and is closed by inverting the status of the two pins.

Depending on the solenoid valve model, you may need to change this time interval.

In the setup function we initialize the serial port, define the RELAY_1 and RELAY_2 pins as digital outputs and give the closing command to the solenoid valve:

void setup() {
  Serial.begin(9600);
  pinMode(RELAY_1, OUTPUT);
  pinMode(RELAY_2, OUTPUT);

  close_valve();
  reset_valve_high();
}

void loop() {
  open_valve();
  reset_valve_low();
  delay(5000);
  

  close_valve();
  reset_valve_high();
  delay(5000);
}

In the loop function we open and close the solenoid valve cyclically (since we are in an infinite loop). Every time we open or close the solenoid valve, we have to cut off its power supply (reset_valve_low and reset_valve_high functions) to avoid unnecessarily discharging the 9V battery. The change of state occurs every 5 seconds (delay(5000);)

Now connect the board to the computer via the USB cable and flash the firmware using the Upload button. Then open the Serial Monitor to see the messages coming from the board.

Upload and Serial Monitor buttons on PlatformIO
Upload and Serial Monitor buttons on PlatformIO

If everything works correctly you will have to hear the relays and the solenoid valve click every 5 seconds and see, on the Serial Monitor window, the messages coming from the board:

Serial monitor messages
Serial monitor messages

Let’s connect and test the DHT22 sensor

Now let’s go one step further by adding the DHT22 sensor and reading the ambient temperature and humidity. So let’s take a look at the following wiring diagram:

Fritzing schematic for the second step
Fritzing schematic for the second step

As you can see, the power for the DHT22 is taken from the 3.3V output of the NodeMCU (pin 3V3). It is necessary to power the sensor with 3.3V so that its output is also 3.3V since the digital pins of the NodeMCU do not accept voltages higher than 3.3V.

WARNING: in the NodeMCU ESP8266 the maximum voltage tolerated by the digital inputs is equal to 3.3V. Any higher voltage would damage it irreparably!!

The output pin is connected to the power supply via a 4.7kΩ pull-up resistor and then, via the white wire, to the pin D5 (which corresponds to GPIO14).

Now let’s look at the code part.
First of all let’s add the DHT sensor library for ESPx by Bernd Giesecke to the project as we have already done in the article How to create a project for NodeMCU ESP8266 with PlatformIO.

Then download the project from the link below:

Replace the contents of the main.cpp file of the project with the one in the main.cpp you just downloaded. Now let’s see the changes.

#include <Arduino.h>
#include "DHTesp.h"

DHTesp dht;

#define  RELAY_1  5
#define  RELAY_2  4
#define  DHT22_PIN 14

The DHTesp.h library that we just added to the project has been included. The dht object of type DHTesp was instantiated and then the GPIO for reading the sensor data was defined (GPIO14 which corresponds to pin D5 of the board, please see the pinout figure above).

void setup() {
  Serial.begin(9600);
  pinMode(RELAY_1, OUTPUT);
  pinMode(RELAY_2, OUTPUT);

  close_valve();
  reset_valve_high();

  dht.setup(DHT22_PIN, DHTesp::DHT22); // Connect DHT sensor to GPIO 14 (D5)
}

The setup function is not very different from the previous case. It differs in the presence of the DHT22 initialization command which connects the sensor to the GPIO14 (pin D5).

void loop() {
  open_valve();
  reset_valve_low();
  delay(5000);
  

  close_valve();
  reset_valve_high();
  delay(5000);


  delay(dht.getMinimumSamplingPeriod());

  float humidity = dht.getHumidity();
  float temperature = dht.getTemperature();

  Serial.print("Status: ");
  Serial.print(dht.getStatusString());
  Serial.print("\thumidity: ");
  Serial.print(humidity, 1);
  Serial.print("%\t\t");
  Serial.print("temperature: ");
  Serial.print(temperature, 1);
  Serial.print("°C\n");
}

The loop function defines the two float variables temperature and humidity and fills them with the values ​​read by the sensor using the two functions getHumidity and getTemperature. After which it cyclically prints the reading carried out on the Serial Monitor.

Now connect the board to the computer via the USB cable and flash the firmware using the Upload button. Then open the Serial Monitor to see the messages coming from the board:

Serial monitor messages
Serial monitor messages

More and more difficult!! Let’s add the connection to the Internet and to Telegram.

For this step we need to add two other libraries to our project: WiFiManager (which is used to manage the Internet connection via WiFi) and UniversalTelegramBot (which is used to interface our project to Telegram bots). If you have followed the procedure described in the article How to create a project for NodeMCU ESP8266 with PlatformIO you should have already installed the necessary libraries. The ArduinoJson library has also been downloaded because it is a dependency of the UniversalTelegramBot library.

Then, let’s download the code from the link below:

Also in this case we must take the main.cpp file just downloaded and replace it with the one present in the project. We also need to copy the mylibrary.h file in the include folder into the include folder of the project.

Before commenting the code and seeing how it works, we will do two preliminary operations: connect the board to the Internet and create the Telegram bot to interface it with.

Connecting the board to the Internet

Connect the board to the computer via the USB cable and flash the firmware using the Upload button. Then open the Serial Monitor to see the messages coming from the board.

First, the card goes into Access Point mode and will give us an IP address that we will use shortly. This operation is used to connect the board to the Internet without having to enter the parameters of the WiFi network (SSID and password) in the code.

The board gives us its current IP address
The board gives us its current IP address

In this case the IP address is 192.168.4.1.

At this point the NodeMCU is in Access Point mode (with SSID AutoConnectAP) and we need to connect our computer to the NodeMCU network. If we go to the networks menu of our computer we should also see the AutoConnectAP network in the list of wireless networks.

List of available WiFi networks
List of available WiFi networks

Connect the computer to the AutoConnectAP network. Then go to your browser and enter the IP previously provided by the NodeMCU: 192.168.4.1

You will see a screen like this:

Browser screen to choose the network
Browser screen to choose the network

Click the ConfigureWiFi button. It will show the available networks:

List of available networks
List of available networks

Choose the SSID of your network:

Choose your network
Choose your network

Enter the password of the network and click the save button:

Enter the password
Enter the password

The reply of the board
The reply of the board

If the connection is successful, you should see a message like that on the Serial Monitor:

Connection successful message
Connection successful message

The NodeMCU stores the access parameters so even if you turn it off, it will remember them when restarted and will automatically reconnect without having to redo this procedure. Only if you reset it by uncommenting this line

// wm.resetSettings();

it will lose the connection parameters.

Please note: the device can store only one network. If you later connect it to another network, it will forget the settings of the previous network.

How to create the Telegram bot

Telegram is an instant messaging and VoIP application that can be installed in your smartphone (Android and iPhone) or computer (PC, Mac and Linux). Telegram allows you to create bots for our device to interact with.

Let’s create our bot!

If you don’t already have Telegram, install it and then search for botFather bot. Click on the item that appears. The following screen will appear:

First Telegram screen of botFather
First Telegram screen of botFather

Type the command /start to read the instructions:

The instructions related the creation of the bot
The instructions related the creation of the bot

Now type the command /newbot to create your bot. Give it a name and username:

The creation of the new bot
The creation of the new bot

If your bot was created successfully, you will receive a message with a link to access the bot and the bot token. Save the bot token because you will need it later for the ESP8266 to interact with the bot.

This is how the screen where the bot token is written looks like:

The bot token
The bot token

Anyone who knows your bot’s username can interact with it. In order to filter messages to ignore those that don’t come from your Telegram account, you need to use your Telegram User ID. Thus, when your Telegram bot receives a message, our ESP8266 will know if it comes from us (and therefore process it) or from others (and therefore ignore it). But…..how do we find this ID?

In your Telegram account, search for IDBot and start a conversation with that bot:

The first screen of the IDBot
The first screen of the IDBot

Then type the command /getid and it will reply with your ID:

The result of the /getid command
The result of the /getid command

At this point we have created our bot and we have all the elements to interface it with our device: the username, the token and the userid.

Let’s now take a look at the code

We will now look at the new parts of the code and comment them step by step.

#include <Arduino.h>
#include "DHTesp.h"

#include <WiFiManager.h>
#include <WiFiClientSecure.h>

#include <UniversalTelegramBot.h>   // Universal Telegram Bot Library written by Brian Lough: https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot

#include <ArduinoJson.h>

#include "time.h"

#include "mylibrary.h"

We initially include the libraries we need. We use them to manage the DHT22, the WiFi connection and the connection to the Telegram bot. The ArduinoJson library is a dependence of the UniversalTelegramBot library. The time.h is used to handle the current date and hour. We have already seen some of them in the previous steps. We have added the mylibrary.h library in the include folder that contains a support function (splitString) which is used to split the strings at each occurrence of a given separator character. We use it here to split commands from Telegram on the “:” separator character. The function takes three parameters: the given string to split, the separator character and the index of the word we want to extract.

Examples:

  Serial.println(splitString("pippo:pluto:paperino", ':', 0));
  Serial.println(splitString("pippo:pluto:paperino", ':', 1));
  Serial.println(splitString("pippo:pluto:paperino", ':', 2));

For example: all these statements split the string “pippo:pluto:paperino” on the separator character “:”. The first result will be “pippo” as we have given the index 0 in the function, the second will be “pluto” because the given index is 1, the third will be “paperino” because the given index is 2.

// Initialize Telegram BOT
#define BOTtoken "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"  // your Bot Token (Get from Botfather)

// Use @myidbot to find out the chat ID of an individual or a group
// Also note that you need to click "start" on a bot before it can
// message you
#define CHAT_ID "1111111111"

Here we have to put the token and Telegram User ID parameters that we got in the previous step when we created the Telegram bot.

#ifdef ESP8266
  X509List cert(TELEGRAM_CERTIFICATE_ROOT);
#endif

WiFiClientSecure client;
UniversalTelegramBot bot(BOTtoken, client);

Here we manage the certificate for the Telegram bot and we define the WiFi client and the Telegram bot.

// Checks for new messages every 1 second.
int botRequestDelay = 1000;
unsigned long lastTimeBotRan;

int devid = 3;

int valveStatus, lastStatus, timerStatus = 0;

String dd = "0";
String mm = "0";
String yyyy = "0";
String hh = "0";
String mnts = "0";
String ss = "0";
String dweek = "0";

String hhon = "NaN";
String mmon = "NaN";
String hhoff = "NaN";
String mmoff = "NaN";

Here we initialize some of the variables used in the code.

const char* ntpServer = "pool.ntp.org";

const long  gmtOffset_sec = 0;
const int   daylightOffset_sec = 3600;
int ntpStatus = 0;

Here we define:

  • the NTP server name
  • the gmtOffset_sec which adjusts the UTC offset for your timezone (in seconds). Refer to the list of UTC time offsets
  • the daylightOffset_sec variable which changes the Daylight offset (in seconds). Set it to 3600 if your country observes Daylight saving time; otherwise, set it to 0
  • the ntpStatus variable to store the status of the NTP server (if it is available or not).

Next, there is the function

void handleNewMessages(int numNewMessages) 

which is used to manage the messages coming from the Telegram bot.

This part

for (int i=0; i<numNewMessages; i++) {
    // Chat id of the requester
    String chat_id = String(bot.messages[i].chat_id);
  //  Serial.println(chat_id);
    if (chat_id != CHAT_ID){
      bot.sendMessage(chat_id, "Unauthorized user", "");
      continue;
    }

filters the user. If it is not authorized, the message will be dropped.

From here on there is the real management of Telegram commands. Let’s see it in more detail.

The /help command causes the Telegram bot to display the list of available commands and a brief explanation of each command:

if (text == "/help") {

      String welcome = "Welcome, " + from_name + ".\n";
      welcome += "Use the following commands to control your outputs:\n\n";
      welcome += "/devid to request the ID of the device\n\n";
      welcome += "/von to turn valve ON\n\n";
      welcome += "/voff to turn valve OFF\n\n";      
      welcome += "/vstate to request the current valve state\n\n";
      welcome += "/tstate to request the current timer state\n\n";
      welcome += "/ton to turn timer ON\n\n";
      welcome += "/toff to turn timer OFF\n\n";
      welcome += "/onoffstate to request the current ON or OFF state\n\n";
      welcome += "/date to request the current date and hour\n\n";
      welcome += "/th to request the temperature and humidity\n\n";
      // welcome += "/sdh to set the current date and hour\n";
      // welcome += "format /sdh:dd:mm:yyyy:hh:mm:ss\n\n";
      welcome += "/seton to set the ON hour and minutes\n";
      welcome += "format /seton:hh:mm\n\n";
      welcome += "/setoff to set the OFF hour and minutes\n";
      welcome += "format /setoff:hh:mm\n\n";

      bot.sendMessage(chat_id, welcome, "");
    } 

If you type /help on Telegram you will obtain this reply:

The reply to the /help command
The reply to the /help command

Now let’s see in more detail the commands:

  • /devid returns the identifier of our device, defined by the user in the variable devid
  • /von turns ON the solenoid valve when the device is running in manual mode (when no timer is set)
  • /voff turns OFF the solenoid valve when the device is running in manual mode (when no timer is set)
  • /vstate returns the current state (open or closed) of the solenoid valve
  • /tstate returns the state of the timer (if the device is running in manual or automatic mode)
  • /ton sets the operating mode to automatic. In this case the /von and /voff commands are not operative and the solenoid valve is opened or closed by the timer set by the user
  • /toff sets the operating mode to manual. In this case the /von and /voff commands are operative and the solenoid valve is opened or closed manually by the user, not by the timer
  • /onoffstate returns the opening and closing hours set by the user, active when the device is set to automatic mode. Initially the hours and minutes are set to the value Nan, which means that the user must set them before setting the automatic mode
  • /date returns the current date and time (received from the NTP server)
  • /th returns the temperature and humidity of the air
  • /seton sets the hour and minutes when the device will turn ON the solenoid valve (when set in automatic mode). The format is /seton:hh:mm (for example /seton:16:30)
  • /setoff sets the hour and minutes when the device will turn OFF the solenoid valve (when set in automatic mode). The format is /setoff:hh:mm (for example /setoff:18:15)

Let’s see the code:

else if (text == "/devid") {
         bot.sendMessage(chat_id, "The device ID is: " + (String) devid, "");
    } 

Simply sends a message to the bot containing the device ID.

else if (text == "/von") {
      if(timerStatus == 0) {
        bot.sendMessage(chat_id, "Valve state set to OPEN", "");
        valveStatus = 1;
        Serial.println(valveStatus);
      } else {
        bot.sendMessage(chat_id, "The timer is active, I'm working in authomatic mode.", "");
      }
      
    } 

Checks if the device is set in manual or automatic mode. In the first case sets the valveStatus to 1 (true). In both cases it sends a proper message to the bot.

else if (text == "/voff") {
      if(timerStatus == 0) {
        bot.sendMessage(chat_id, "Valve state set to CLOSED", "");
        valveStatus = 0;
        Serial.println(valveStatus);
      } else {
        bot.sendMessage(chat_id, "The timer is active, I'm working in authomatic mode.", "");
      }
      
    } 

Checks if the device is set in manual or automatic mode. In the first case sets the valveStatus to 0 (false). In both cases it sends a proper message to the bot.

else if (text == "/vstate") {
      Serial.println(valveStatus);
      if(valveStatus == 0) {
        bot.sendMessage(chat_id, "The valve is CLOSED", "");
      } else if(valveStatus == 1) {
        bot.sendMessage(chat_id, "The valve is OPEN", "");
      }         
    } 

Checks the valveStatus variable: if it equals 0, the valve is CLOSED, if it equals 1, the valve is OPEN. Then it sends the valve status message to the bot.

else if(text == "/tstate") {
      if(timerStatus == 0) {
        bot.sendMessage(chat_id, "The timer is set to OFF (manual commands).", "");
      } else {
        bot.sendMessage(chat_id, "The timer is set to ON.", "");
      }
    }

Checks the timerStatus variable: if it equals 0, the timer is set to OFF (the device operates in manual mode), if it equals 1, the timer is set to ON (the device operates in automatic mode). Then it sends the timer status message to the bot.

else if (text == "/ton") {                       // sets the timer to ON
        if(hhon == "NaN" or mmon == "NaN" or hhoff == "NaN" or mmoff == "NaN"){
          bot.sendMessage(chat_id, "The hours and minutes of the timer for automatic operation have not yet been set. Set them first using the /seton and /setoff commands.", "");
        } else {
          timerStatus = 1;
          bot.sendMessage(chat_id, "Timer set to ON (authomatic mode).", "");
        }        
    } 

Sets the timer to ON (the device operates in automatic mode). If the ON hour and minutes or the OFF hours and minutes are not set (NaN values) it sends a warning message to the bot, otherwise it sets the timerStatus to 1 (to operate in automatic mode).

else if (text == "/toff") {                       // sets the timer to OFF
        timerStatus = 0;
        bot.sendMessage(chat_id, "Timer set to OFF (manual mode).", "");
    } 

Sets the operating mode to manual setting the timerStatus to 0 and sends a status message to the bot.

else if(text == "/onoffstate") {
      bot.sendMessage(chat_id, "ON: " + hhon + ":" + mmon + "  OFF: " + hhoff + ":" + mmoff, "");
    }

Sends a message to the bot containing the ON hour and minutes and the OFF hours and minutes.

else if (text == "/date") {

          // Print current date

          char dateHour[29];
        
 //          = "Date: %02d/%02d/%4d %02d:%02d:%02d\n", day(t_unix_date), month(t_unix_date), year(t_unix_date), hour(t_unix_date), minute(t_unix_date), second(t_unix_date);

   //       snprintf_P(dateHour, sizeof(dateHour), PSTR("%02d/%02d/%4d %02d:%02d:%02d"), dd, mm, yyyy, hh, mnts, ss);
   
            snprintf_P(dateHour, sizeof(dateHour), PSTR("%4s %02s/%02s/%4s   %02s:%02s:%02s"), dweek, dd, mm, yyyy, hh, mnts, ss);
        
//          printf("Date: %02d/%02d/%4d %02d:%02d:%02d\n", day(t_unix_date), month(t_unix_date), year(t_unix_date), hour(t_unix_date), minute(t_unix_date), second(t_unix_date));
      
        if(ntpStatus == 1) {
          bot.sendMessage(chat_id, "Today is: " + (String) dateHour, "");
        } else {
          bot.sendMessage(chat_id, "Cannot connect to NTP time server", "");
        }
         
    }

If the NTP server is available (ntpStatus variable set to 1), it returns the current date and hour, otherwise it sends a warning message to the bot.

else if (text == "/th") {
      bot.sendMessage(chat_id, "The temperature is: " + (String) temperature + "°C, the humidity is: " + humidity + "%", "");
    }

Sends a message containing the current temperature and humidity of the air to the bot.

else if (splitString(text, ':', 0) == "/seton") {                        // sets the ON hour in format hh:mm  for example  /sdh:hh:mm
        hhon = splitString(text, ':', 1);
        mmon = splitString(text, ':', 2);
        bot.sendMessage(chat_id, "Time set ON: " + hhon + ":" + mmon, "");
    } 

Sets the hour and minutes when the device will turn ON the solenoid valve (when set in automatic mode). The format is /seton:hh:mm. It extracts the hour and the minutes from the command, splitting it using the splitString function on the separator character “:”.

else if (splitString(text, ':', 0) == "/setoff") {                       // sets the OFF hour in format hh:mm  for example  /sdh:hh:mm
        hhoff = splitString(text, ':', 1);
        mmoff = splitString(text, ':', 2);
        bot.sendMessage(chat_id, "Time set OFF: " + hhoff + ":" + mmoff, "");
    } 

Sets the hour and minutes when the device will turn OFF the solenoid valve (when set in automatic mode). The format is /setoff:hh:mm. It extracts the hour and the minutes from the command, splitting it using the splitString function on the separator character “:”.

} else {
       bot.sendMessage(chat_id, "Unrecognized message. Please retry...", "");
    }

If the command received is not recognized (it is none of the previous commands), the device sends an error message to the bot.

bool getLocalTime(struct tm * info, uint32_t ms)
{
    uint32_t start = millis();
    time_t now;
    while((millis()-start) <= ms) {
        time(&now);
        localtime_r(&now, info);
        if(info->tm_year > (2016 - 1900)){
            return true;
        }
        delay(10);
    }
    return false;
}

void updateLocalTime()
{
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
    ntpStatus = 0;
    return;
  }

  ntpStatus = 1;

  dd = String(timeinfo.tm_mday);
  mm = String(timeinfo.tm_mon + 1);
  yyyy = String(timeinfo.tm_year + 1900);
  hh = String(timeinfo.tm_hour);
  if(hh.length() == 1) {
    hh = "0" + hh;
  }
  mnts = String(timeinfo.tm_min);
  if(mnts.length() == 1) {
    mnts = "0" + mnts;
  }
  ss = String(timeinfo.tm_sec);
  if(ss.length() == 1) {
    ss = "0" + ss;
  }

  switch(timeinfo.tm_wday){

    case 1:
        dweek = "Mon";
        break;

    case 2:
        dweek = "Tue";
        break;

    case 3:
        dweek = "Wed";
        break;

    case 4:
        dweek = "Thu";
        break;

    case 5:
        dweek = "Fri";
        break;
    
    case 6:
        dweek = "Sat";
        break;

    case 7:
        dweek = "Sun";
        break;

  }

} 

The getLocalTime and updateLocalTime are used to get the current date and time from the NTP server.

WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP
  // it is a good practice to make sure your code sets wifi mode how you want it.

  Serial.begin(9600);

  //WiFiManager, Local intialization. Once its business is done, there is no need to keep it around
  WiFiManager wm;

  // reset settings - wipe stored credentials for testing
  // these are stored by the esp library
  // wm.resetSettings();

  // Automatically connect using saved credentials,
  // if connection fails, it starts an access point with the specified name ( "AutoConnectAP"),
  // if empty will auto generate SSID, if password is blank it will be anonymous AP (wm.autoConnect())
  // then goes into a blocking loop awaiting configuration and will return success result

  bool res;
  // res = wm.autoConnect(); // auto generated AP name from chipid
  // res = wm.autoConnect("AutoConnectAP"); // anonymous ap
  res = wm.autoConnect("AutoConnectAP","password"); // password protected ap

  if(!res) {
      Serial.println("Failed to connect");
      // ESP.restart();
  } 
  else {
      //if you get here you have connected to the WiFi    
      Serial.println("Connected...yeey :)");
  }

  #ifdef ESP8266
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);      // get UTC time via NTP
    client.setTrustAnchors(&cert); // Add root certificate for api.telegram.org
  #endif

  bot.sendMessage(CHAT_ID, "Hi! I'm online!", "");

The setup function contains, in addition to the various settings seen in the previous steps (for example the setting of the Serial Monitor and the DHT22), the various settings related to the WiFi server to first configure the device as an Access Point and then, once entered the network credentials, to connect it to our wireless network.

Finally let’s see what has changed in the loop function:

if(timerStatus == 0) {              // timer deactivated
    switch(valveStatus)
    {
      case 0:             // if valve state is CLOSED, closes the valve
        if(lastStatus != valveStatus) {
          close_valve();
          reset_valve_high();
        }      
        lastStatus = 0;
        break;
    
      case 1:             // if valve state is OPEN, opens the valve
        if(lastStatus != valveStatus) {
          open_valve();
          reset_valve_low();
        }
        lastStatus = 1;
        break;
    }
  } else {                            // timer activated
      updateLocalTime();
      if((hh == hhon) and (mnts == mmon)) {
        valveStatus = 1;
        if(lastStatus != valveStatus) {
          open_valve();
          reset_valve_low();
          Serial.println("Valve authomatically opened at " + hh + ":" + mnts + ".");
          bot.sendMessage(CHAT_ID, "Valve authomatically opened at " + hh + ":" + mnts + ".", "");
        }      
        lastStatus = 1;
      } else {
          if((hh == hhoff) and (mnts == mmoff)) {
            if(lastStatus != valveStatus) {
              close_valve();
              reset_valve_high();
              Serial.println("Valve authomatically closed at " + hh + ":" + mnts + ".");
              bot.sendMessage(CHAT_ID, "Valve authomatically closed at " + hh + ":" + mnts + ".", "");
            }   
            valveStatus = 0;         
          }
          lastStatus = 0;
        }
  }

The if block checks if the device is operating in manual or automatic mode (checking the timerStatus variable). If in manual mode, it closes the valve if the valveStatus variable equals 0 or opens the valve if the valveStatus variable equals 1.

If in automatic mode, the current hour is updated and compared to the hour set for the automatic timer. If it equals the previously set ON hour, the valve will be open; if it equals the previously set OFF hour, the valve will be closed.

if (millis() > lastTimeBotRan + botRequestDelay)  {
    int numNewMessages = bot.getUpdates(bot.last_message_received + 1);

    while(numNewMessages) {
      Serial.println("got response");
      handleNewMessages(numNewMessages);
      numNewMessages = bot.getUpdates(bot.last_message_received + 1);
    }
    lastTimeBotRan = millis();
  }

The last part is used to handle the new messages coming from the Telegram bot.

And now let’s see the soil moisture meter

We could not miss a feature that would allow us to check if the soil in which our plant is planted is quite humid or dry and needs to be watered.

But how do we measure the value of this humidity? What physical parameter can we use? The parameter we use is the electrical conductivity of the soil. In reality what we really measure is its electrical resistivity, which is the inverse of conductivity.

Soil, when wet, is a good conductor of electricity. This means that its electrical resistance is low. When it dries, however, it becomes a poor conductor of electricity, so its electrical resistance increases.

In order to measure its resistance, we insert it into a very simple circuit called a voltage divider.

A voltage divider circuit
A voltage divider circuit

As you can see, the voltage divider consists of two resistors: R1 and R2. In our case, R1 represents the electrical resistance of the soil, R2 is a normal 10kΩ resistance. A voltage, which we call Vin, is applied to the divider. At the output of the divider we have a voltage which we call Vout. The voltage Vout will be a fraction of the Vin (hence the name voltage divider) given by the formula:

The voltage divider formula
The voltage divider formula

The voltage Vin is our reference voltage and must be constant. We take it from the 3V3 pin of the NodeMCU which supplies a constant voltage of 3.3V and which we have already used to power the DHT22 sensor.

WARNING: in the NodeMCU ESP8266 the maximum voltage tolerated by the analog input is equal to 3.3V. Any higher voltage would damage it irreparably!!

The R2 resistance is fixed (we set it at 10kΩ) while the R1 resistance (that of the soil) varies according to the humidity. Therefore the voltage Vout will vary as a function of the resistance R1 (and therefore of the humidity of the soil).

We just have to measure this voltage Vout with the analog input of the NodeMCU.

Connect the new components following the diagram below:

The complete wiring diagram of the automatic sprinkler
The complete wiring diagram of the automatic sprinkler

The two green and yellow wires must be connected to two metal sticks which are then stuck into the soil near the plant, spacing them a few centimeters apart.

As you can see, the voltage Vout is measured by connecting the blue wire between the center point of the voltage divider and the NodeMCU pin A0.

At this point we can download the new firmware from the link below:

Let’s update the main.cpp file and remember to re-enter the bot token and Telegram User ID here:

// Initialize Telegram BOT
#define BOTtoken "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"  // your Bot Token (Get from Botfather)

// Use @myidbot to find out the chat ID of an individual or a group
// Also note that you need to click "start" on a bot before it can
// message you
#define CHAT_ID "111111111"

Let’s see the code related to this new feature:

#define ANALOGPIN0 A0  // ESP8266 Analog Pin ADC0 = A0
#define MAXADCRESOLUTION 1024
int soilMoistureAcquired = 0;  // value read from the pot
float soilMoistureVoltage = 0.0;
float soilMoistureThreshold = 1.0;
float powerSupply = 3.3;

Here we define the analog pin input (A0) and the max resolution of the Analog to Digital Converter (ADC) of the NodeMCU. We also define some variables and the value of the power supply we use as voltage reference (powerSupply variable).

In the handleNewMessages function we have a new command:

welcome += "/sm to request the soil moisture value\n\n";

If we type /sm in the Telegram bot, the device will reply with the voltage value corresponding to the value of the electrical resistance of the soil:


In the loop function there is the code that handles the analog measure of the soil moisture:

soilMoistureAcquired = analogRead(ANALOGPIN0);
 
  // print the readings in the Serial Monitor
  Serial.print("sensor = ");
  Serial.print(soilMoistureAcquired);
  Serial.print("\n");
  soilMoistureVoltage = (powerSupply / MAXADCRESOLUTION) * soilMoistureAcquired;
  Serial.print("voltage = ");
  Serial.print(soilMoistureVoltage);
  Serial.print("\n");


  if (soilMoistureVoltage < soilMoistureThreshold) {
      bot.sendMessage(CHAT_ID, "The soil moisture value is too LOW! The voltage associated to the soil moisture value is: " + (String) soilMoistureVoltage + "V", "");
  }

Here the code acquires the value from the ADC and stores it in the soilMoistureAcquired variable. The value coming from the ADC is in the range [0 – 1024]. This value is converted to a voltage value and stored in the soilMoistureVoltage variable.

The if block is an alarm: if the measured value il less than the soilMoistureThreshold variable it will send an alarm message to the bot. Obviously you can set the value of this threshold in a different way in order to satisfy your needs.

Hint: you could also change the functionality so that if the measured value is below the threshold, the solenoid valve is opened.

Finally, let’s see how to save some settings on the EEPROM memory

What is an EEPROM memory used for, how does it work and how is it used?

You can see it by reading our tutorial How to use the EEPROM memory on the NodeMCU ESP8266. In this project we use it to memorize the ON and OFF times of the solenoid valve and the status of the timer (i.e. whether the device is set to automatic or manual operation). It is important to save these settings on the EEPROM because they remain memorized even in the event of a power failure. If we didn’t do this, every time the power went out we would have to set these values again.

As usual you can download the complete code from the link below and follow the usual procedure with the main.cpp file. Always remember to reset the bot token and the Telegram User ID as done in the previous step.

As usual we include the EEPROM library:

#include <EEPROM.h>                // EEPROM

We then initialize some variables:

// ---------------------- Initialization variables related to the EEPROM
String hhonEE = "";
String mmonEE = "";
String hhoffEE = "";
String mmoffEE = "";

int hhonEE_address = 2;
int mmonEE_address = 4;
int hhoffEE_address = 6;
int mmoffEE_address = 8;

String timerStatusEE = "";
int timerStatusEE_address = 10;

// ---------------------------------------------------------------------

Then the variables that will contain the values ​​read from the EEPROM are defined and initialized (both as regards the ON and OFF times of the solenoid valve and as regards the status of the timer). The variables containing the addresses of the values ​​stored in the EEPROM are also defined and set.

We then modified the functions (activated by the Telegram bot) that set these parameters:

else if (splitString(text, ':', 0) == "/seton") {                        // sets the ON hour in format hh:mm  for example  /sdh:hh:mm
        hhon = splitString(text, ':', 1);
        mmon = splitString(text, ':', 2);

        // ------------------------ writes data on EEPROM
        for(int i = 0; i < hhon.length(); i++) {
          EEPROM.write(hhonEE_address + i, hhon[i]);
        }

        for(int i = 0; i < mmon.length(); i++) {
          EEPROM.write(mmonEE_address + i, mmon[i]);
        }  

        EEPROM.commit();  
        // ----------------------------------------------

        bot.sendMessage(chat_id, "Time set ON: " + hhon + ":" + mmon, "");
    } 
    
    else if (splitString(text, ':', 0) == "/setoff") {                       // sets the OFF hour in format hh:mm  for example  /sdh:hh:mm
        hhoff = splitString(text, ':', 1);
        mmoff = splitString(text, ':', 2);

        // ------------------------ writes data on EEPROM
        for(int i = 0; i < hhoff.length(); i++) {
          EEPROM.write(hhoffEE_address + i, hhoff[i]);
        }

        for(int i = 0; i < mmoff.length(); i++) {
          EEPROM.write(mmoffEE_address + i, mmoff[i]);
        }

        EEPROM.commit();
        // ----------------------------------------------


        bot.sendMessage(chat_id, "Time set OFF: " + hhoff + ":" + mmoff, "");
    }

The /seton and /setoff no longer limit themselves to setting variables but write the parameters in the EEPROM.

This is what the /ton and /toff functions also do, as seen below:

else if (text == "/ton") {                       // sets the timer to ON
        if(hhon == "NaN" or mmon == "NaN" or hhoff == "NaN" or mmoff == "NaN"){
          bot.sendMessage(chat_id, "The hours and minutes of the timer for automatic operation have not yet been set. Set them first using the /seton and /setoff commands.", "");
        } else {
          timerStatus = 1;
          
          // ------------------------ writes data on EEPROM
          timerStatusEE = "1";
          EEPROM.write(timerStatusEE_address, timerStatusEE[0]);
          EEPROM.commit();
          // ----------------------------------------------

          bot.sendMessage(chat_id, "Timer set to ON (authomatic mode).", "");
        }        
    } 

    else if (text == "/toff") {                       // sets the timer to OFF
        timerStatus = 0;

        // ------------------------ writes data on EEPROM
        timerStatusEE = "0";
        EEPROM.write(timerStatusEE_address, timerStatusEE[0]);
        EEPROM.commit();
        // ----------------------------------------------

        bot.sendMessage(chat_id, "Timer set to OFF (manual mode).", "");
    } 

In the setup function we have the initialization of the EEPROM:

EEPROM.begin(512);  //Initializes EEPROM

and reading data from EEPROM at device startup:

// -------------  Reading data saved in EEPROM
  hhonEE = char(EEPROM.read(hhonEE_address));
  hhonEE += char(EEPROM.read(hhonEE_address + 1)); 
  
  mmonEE = char(EEPROM.read(mmonEE_address));
  mmonEE += char(EEPROM.read(mmonEE_address + 1));

  hhoffEE = char(EEPROM.read(hhoffEE_address)); 
  hhoffEE += char(EEPROM.read(hhoffEE_address + 1));

  mmoffEE = char(EEPROM.read(mmoffEE_address));
  mmoffEE += char(EEPROM.read(mmoffEE_address + 1)); 


  timerStatusEE = char(EEPROM.read(timerStatusEE_address));

  hhon = hhonEE;
  mmon = mmonEE;
  hhoff = hhoffEE;
  mmoff = mmoffEE;
  timerStatus = timerStatusEE.toInt();


  Serial.println("EEPROM read!");
  // --------------------------------------------

As we have seen, with a few changes to the code we have added the possibility of saving the settings in order to keep them even in the event of a power failure.

Make the PCB of the project

For this article a project created using KiCad 6 is also available so that it can also be implemented on PCB.

Screenshot of the complete schematic
Screenshot of the complete schematic

Screenshot of the PCB
Screenshot of the PCB

3D PCB screenshot
3D PCB screenshot

Download the project made using KiCad 6 from the link below:

Final considerations

As you can see, the project is quite simple. However it already has various basic functions which allow it to function as an automatic sprinkler and which allows the user to remotely monitor/control it via Telegram.
Obviously it can be improved: functions can be added/modified by acting solely on the software or other sensors can be added (and the functions that manage them implemented via software). There is no limit to the imagination.
A useful improvement consists in the addition of an RTC (Real Time Clock) module which would allow the device to keep the date and time updated even in the absence of connection to the NTP server (in case, for example, there is no connection to the Internet).

Scroll to Top