Chat with your board: read the sensor and switch the light from Telegram, anywhere.
Project_12_ESP32_Telegram_Bot.ino. Needs UniversalTelegramBot, ArduinoJson (v6+), and the DHT libraries, plus your Wi-Fi and a bot token.
By the end of this project, you will:
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).
Because it is just outbound HTTPS, this reaches your phone even on networks where a self-hosted web server would not.
| Command | What it does |
|---|---|
/status | temperature, humidity, light state |
/light_on · /light_off | switch the LED or relay |
/help | list the commands |
/newbot.bot. Copy the HTTP API token.Install: UniversalTelegramBot (Brian Lough), ArduinoJson (v6+), DHT sensor library, Adafruit Unified Sensor.
| Qty | Part | Notes |
|---|---|---|
| 1 | DHT11 | S to GPIO 4, + to 3.3 V, - to GND |
| 1 | LED + 220 Ohm or relay | to GPIO 26 |
| 1 | PIR (optional) | VCC to 5 V, OUT to GPIO 27, GND to GND |
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.
More moving parts than anything before it, but each one is something you have already met.
#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.
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.
handleNewMessages() reads the text and dispatchesfor (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.
loop() pushes on motion, then polls for commandsbool 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.
| Code | What it does |
|---|---|
WiFiClientSecure secured_client | A 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_id | Which conversation a message came from. Reply here to answer the right person. |
bot.messages[i].text | What 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 && !lastMotion | Fires once on the rising edge, so one person walking past sends one alert. |
String(CHAT_ID).length() > 0 | Makes 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_INTERVAL | The non-blocking timer again, now pacing how often you poll Telegram. |
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:
/help.+ 1 is the whole trick. Asking for updates newer than the last one handled is how a polling client avoids replaying its own history.| Symptom | Likely cause | Fix |
|---|---|---|
| Bot never replies | Wrong token | Re-copy from BotFather |
Error on TELEGRAM_CERTIFICATE_ROOT | Wrong library | Use UniversalTelegramBot by Brian Lough |
| JSON compile error | Old ArduinoJson | Update to v6 or newer |
| No motion alerts | CHAT_ID empty or wrong | Get your id from @userinfobot |