Automation
Intermediate
ESP32 Telegram Bot: Relay Control and Motion Alerts
Switch a light with /on and /off from anywhere in the world and get a Telegram message when a PIR sensor detects motion. No public IP or port forwarding needed.
Why Telegram
Controlling a device over the internet usually needs a public IP, port forwarding or a cloud service. Telegram makes it simpler: the ESP32 reaches out to Telegram's servers and polls for new messages. There are no incoming connections, so it works behind any router and even on mobile data.
What the bot does:
/on,/off— switches the relay;/status— relay state, Wi-Fi signal and uptime;- messages you by itself when the PIR sensor detects motion (at most once a minute).
Parts
- ESP32 DevKit
- Relay module (or an LED for testing) — GPIO 26
- HC-SR501 or AM312 PIR motion sensor — GPIO 27
| Module | ESP32 |
|---|---|
| Relay IN / VCC / GND | GPIO 26 / 5V / GND |
| PIR OUT / VCC / GND | GPIO 27 / 5V (HC-SR501) or 3V3 (AM312) / GND |
The HC-SR501 output is 3.3 V even when powered from 5 V, so it connects directly.
Libraries
- UniversalTelegramBot (Brian Lough)
- ArduinoJson (Benoît Blanchon) — dependency
Create the bot
- Message @BotFather in Telegram →
/newbot→ pick a name. You'll get a token like123456789:AA.... - Message @userinfobot — it replies with your chat ID (a number).
- Open your new bot and press Start, otherwise it can't message you.
Code
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";
#define BOT_TOKEN "123456789:AAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
const String OWNER_CHAT_ID = "123456789"; // your chat ID
const int RELAY_PIN = 26;
const int PIR_PIN = 27;
const unsigned long MOTION_COOLDOWN = 60000; // 1 min between alerts
WiFiClientSecure client;
UniversalTelegramBot bot(BOT_TOKEN, client);
unsigned long lastPoll = 0;
unsigned long lastMotionAlert = 0;
void handleMessages(int count) {
for (int i = 0; i < count; i++) {
const String chatId = bot.messages[i].chat_id;
const String text = bot.messages[i].text;
// Only answer the owner — anyone can find your bot
if (chatId != OWNER_CHAT_ID) {
bot.sendMessage(chatId, "Access denied", "");
continue;
}
if (text == "/on") {
digitalWrite(RELAY_PIN, HIGH);
bot.sendMessage(chatId, "💡 On", "");
} else if (text == "/off") {
digitalWrite(RELAY_PIN, LOW);
bot.sendMessage(chatId, "🌑 Off", "");
} else if (text == "/status") {
String s = "Relay: " + String(digitalRead(RELAY_PIN) ? "ON" : "OFF");
s += "\nWi-Fi: " + String(WiFi.RSSI()) + " dBm";
s += "\nUptime: " + String(millis() / 60000) + " min";
bot.sendMessage(chatId, s, "");
} else {
bot.sendMessage(chatId, "Commands: /on /off /status", "");
}
}
}
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
pinMode(PIR_PIN, INPUT);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println(WiFi.localIP());
client.setCACert(TELEGRAM_CERTIFICATE_ROOT); // verify api.telegram.org's certificate
bot.sendMessage(OWNER_CHAT_ID, "🤖 ESP32 online", "");
}
void loop() {
// Poll Telegram once a second
if (millis() - lastPoll > 1000) {
lastPoll = millis();
int n = bot.getUpdates(bot.last_message_received + 1);
while (n) {
handleMessages(n);
n = bot.getUpdates(bot.last_message_received + 1);
}
}
// Motion + cooldown between alerts
if (digitalRead(PIR_PIN) == HIGH && millis() - lastMotionAlert > MOTION_COOLDOWN) {
lastMotionAlert = millis();
bot.sendMessage(OWNER_CHAT_ID, "🚶 Motion detected!", "");
}
}
Testing
- After boot the bot sends you "🤖 ESP32 online".
- Send
/status, then/on— the relay clicks. - Wave in front of the PIR sensor — you get an alert.
Good to know
- Tune the HC-SR501: the left pot sets sensitivity, the right one the output duration; set the jumper to H (retrigger mode).
- The PIR sensor needs ~30–60 s to settle after power-up — expect false triggers during that time.
- Add menu buttons via BotFather →
/setcommandsso you don't have to type commands. - The token is a password. Don't publish the sketch with it on GitHub. If it leaks —
/revokein BotFather.
Ideas
- Send a photo from an ESP32-CAM on motion (
bot.sendPhotoByBinary). - A daily report with the temperature from the "Mini Weather Station" project.
- Multiple users — an array of allowed chat IDs.