← All projects
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.

Difficulty
Intermediate
Time
1–2 h
Category
Automation

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

  1. Message @BotFather in Telegram → /newbot → pick a name. You'll get a token like 123456789:AA....
  2. Message @userinfobot — it replies with your chat ID (a number).
  3. 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

  1. After boot the bot sends you "🤖 ESP32 online".
  2. Send /status, then /on — the relay clicks.
  3. 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 → /setcommands so you don't have to type commands.
  • The token is a password. Don't publish the sketch with it on GitHub. If it leaks — /revoke in 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.