PROJECT 12 · CLOUD / MQTT

ESP32 Telegram Bot

Chat with your board: read the sensor and switch the light from Telegram, anywhere.

⬇ Download the sketch Save Project_12_ESP32_Telegram_Bot.ino. Needs UniversalTelegramBot, ArduinoJson (v6+), and the DHT libraries, plus your Wi-Fi and a bot token.

🎯 Objective

By the end of this project, you will:

1 · How it works

Telegram runs a bot API over HTTPS. The ESP32 asks Telegram "any new messages?" once a second and acts on them, and it can also send you a message when something happens (motion).

You (Telegram app) <-- HTTPS --> Telegram servers <-- HTTPS --> ESP32

Because it is just outbound HTTPS, this reaches your phone even on networks where a self-hosted web server would not.

CommandWhat it does
/statustemperature, humidity, light state
/light_on · /light_offswitch the LED or relay
/helplist the commands

2 · Create your bot

  1. In Telegram, chat with @BotFather and send /newbot.
  2. Pick a name and a username ending in bot. Copy the HTTP API token.
  3. Optional: open @userinfobot to get your numeric chat id for motion alerts.

3 · Libraries and wiring

Install: UniversalTelegramBot (Brian Lough), ArduinoJson (v6+), DHT sensor library, Adafruit Unified Sensor.

QtyPartNotes
1DHT11S to GPIO 4, + to 3.3 V, - to GND
1LED + 220 Ohm or relayto GPIO 26
1PIR (optional)VCC to 5 V, OUT to GPIO 27, GND to GND

4 · Edit, upload, chat

Edit the marked block: WIFI_SSID, WIFI_PASS, BOT_TOKEN, and optionally CHAT_ID. Upload, open Serial Monitor at 115200, then send /start to your bot in Telegram.

==============================================
 Project 12: ESP32 Telegram Bot
==============================================
Connecting to Wi-Fi: MyNetwork
....
Wi-Fi connected!
Bot ready. Open Telegram and send /start to your bot.

5 · Code Walkthrough: Understanding the Sketch

More moving parts than anything before it, but each one is something you have already met.

Step 1: A secure client, and a bot that rides on it

#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>

WiFiClientSecure secured_client;
UniversalTelegramBot bot(BOT_TOKEN, secured_client);

Two objects, stacked. WiFiClientSecure is a TCP connection with TLS on top: the https:// part. UniversalTelegramBot sits above it and knows Telegram's API, turning sendMessage() into the right HTTPS request. ArduinoJson is underneath both, decoding the replies, which is why the library needs it even though your code never mentions it.

The token identifies your bot to Telegram. Anyone with it can send messages as your bot, so it belongs in the edit block and nowhere public.

Step 2: Trusting Telegram, in setup()

secured_client.setCACert(TELEGRAM_CERTIFICATE_ROOT);

HTTPS is only useful if the client can tell the real server from an impostor, and that check works by trusting a root certificate. Your laptop ships with hundreds. An ESP32 ships with none, so the library bundles the one Telegram's certificate chains up to and this line installs it. Skip it and every request fails to connect, even with a perfect token.

Step 3: handleNewMessages() reads the text and dispatches

for (int i = 0; i < numNewMessages; i++) {
  String chat_id = bot.messages[i].chat_id;
  String text    = bot.messages[i].text;
  String from    = bot.messages[i].from_name;

  if (text == "/light_on") {
    digitalWrite(LED_PIN, HIGH);
    bot.sendMessage(chat_id, "Light is ON", "");
  }
  ...
  else {
    bot.sendMessage(chat_id, "Unknown command. Send /help.", "");
  }
}

Several messages can be waiting, so the function takes a count and loops. Each message carries who sent it (from_name), what they typed (text), and which conversation it came from (chat_id). Replying to chat_id rather than a fixed id is what lets one bot serve several people at once.

The chain of if/else if is a command dispatcher, the same job the URL parsing did in Project 5. The final else matters: a bot that ignores what it does not understand feels broken, while one that says "send /help" teaches its own interface.

/status reads the sensor at the moment you ask, with the same isnan() guard as Project 9, and builds the reply with String(t, 1) so 24.63 prints as 24.6.

Step 4: loop() pushes on motion, then polls for commands

bool motion = digitalRead(PIR_PIN);
if (motion && !lastMotion && String(CHAT_ID).length() > 0) {
  bot.sendMessage(CHAT_ID, "Motion detected!", "");
  Serial.println("[PIR] motion -> alert sent");
}
lastMotion = motion;

if (millis() - lastBotCheck > BOT_CHECK_INTERVAL) {
  int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
  while (numNewMessages) {
    handleNewMessages(numNewMessages);
    numNewMessages = bot.getUpdates(bot.last_message_received + 1);
  }
  lastBotCheck = millis();
}

motion && !lastMotion is Project 4's edge detection, doing real work here: without it, standing in front of the sensor would send a message every few milliseconds until Telegram rate-limited the bot.

The second half is polling. getUpdates(last_message_received + 1) asks "anything newer than the last one I saw?", and the + 1 is what stops the board replying to the same message forever. The inner while drains the queue, since a burst may not arrive in one response.

Note who does the asking: the ESP32 always opens the connection outwards, which is exactly why this works behind a router that would never let an incoming request through.

Key concepts: what each line really does

CodeWhat it does
WiFiClientSecure secured_clientA TCP connection with TLS on top. The s in https.
setCACert(TELEGRAM_CERTIFICATE_ROOT)Installs the root certificate the board needs to trust Telegram's server.
UniversalTelegramBot bot(TOKEN, client)Wraps the Telegram API: your bot's identity plus the secure connection it speaks over.
bot.getUpdates(bot.last_message_received + 1)Asks for messages newer than the last one handled, and returns how many arrived.
bot.messages[i].chat_idWhich conversation a message came from. Reply here to answer the right person.
bot.messages[i].textWhat the user typed, ready to compare against your commands.
bot.sendMessage(chat_id, text, "")Sends a reply. The third argument is the parse mode, empty for plain text.
motion && !lastMotionFires once on the rising edge, so one person walking past sends one alert.
String(CHAT_ID).length() > 0Makes the push feature optional: an empty id disables it cleanly.
String(t, 1)Formats a float to one decimal place for a readable reply.
millis() - lastBotCheck > BOT_CHECK_INTERVALThe non-blocking timer again, now pacing how often you poll Telegram.

6 · Wrapping Up: What You've Learned

You have built a device with a user interface you did not have to design, reachable from any network in the world. The key takeaways:

7 · Troubleshooting

SymptomLikely causeFix
Bot never repliesWrong tokenRe-copy from BotFather
Error on TELEGRAM_CERTIFICATE_ROOTWrong libraryUse UniversalTelegramBot by Brian Lough
JSON compile errorOld ArduinoJsonUpdate to v6 or newer
No motion alertsCHAT_ID empty or wrongGet your id from @userinfobot

8 · Going further

← Project 11 · MQTT + Ubidots Project 13 · Capstone →