ESP32でTelegramボットを作る:通知、LED、リレーの制御
ESP32でTelegramボットを作ります。BotFatherのトークン、チャットID、UniversalTelegramBotの設定、人感センサーとガスの通知、コマンドでのリレー制御、自分にだけ返信する設定まで解説します。
ESP32のTelegramボットを使うと、自宅のESP32ボードからスマホにメッセージを送ったり、スマホから送ったコマンドをボードに実行させたりできます。玄関で動きがあったときや台所でガスを検知したときには、すぐにポケットの中のスマホに通知が届きます。「/ac」(トルコ語で「オン」の意味)と送ればリレーでランプがつき、「/kapat」(「オフ」)と送れば消えます。スマホアプリを自作する必要も、ルーターのポートを開ける必要も、クラウドサービスにお金を払う必要もありません。Telegramの無料のBot APIと、ESP32に内蔵されたWi-Fiだけで実現できます。
このガイドでは、実際に動くシステムを一から作ります。BotFatherでボットを作ってトークンを取得し、自分のチャットIDを調べ、UniversalTelegramBotとArduinoJsonのライブラリをインストールして、ESP32から最初のメッセージを送ります。続いて、PIR人感センサーとMQ-2ガスセンサーによる動きとガスの通知、そしてTelegramのコマンドによるLEDとリレーの制御を加えます。さらに、ボットが自分のIDにだけ返信するようにする方法、証明書の設定、ESP8266とRaspberry Pi Pico Wでの違い、よくあるエラーも1つずつ取り上げます。

ESP32のTelegramボットとは?どのように動くのか
Telegramのボットとは、人ではなくプログラムが動かす特別なアカウントです。このプロジェクトでは、そのプログラムをESP32が動かします。ESP32とスマホが直接つながることはなく、どちらもTelegramのサーバーとやり取りします。スマホでボットに「/ac」と送ると、そのメッセージはサーバーで待機します。ESP32は1秒ごとにapi.telegram.orgへ暗号化されたHTTPSリクエスト(getUpdates)を送って「新しいメッセージはある?」と問い合わせ、メッセージを受け取って実行します。通知を送るときは、逆方向にsendMessageリクエストを送ります。接続を開くのは常にESP32の側なので、ルーターのポート転送や固定IPは必要ありません。インターネットにつながる2.4 GHzのWi-Fiネットワークなら、どこでもボードが動きます。
| 用語 | 役割 | 例 |
|---|---|---|
| BotFather | ボットを作成・管理するためのTelegramのボット | /newbot、/setcommands、/revoke |
| ボットのトークン | ボットのパスワード。コードはこれを使ってTelegramに接続する | 1234567890:AAE...(数字、コロン、長い文字列) |
| チャットID | メッセージを送る相手(個人またはグループ)の番号 | 個人:123456789、グループ:-1001234567890 |
| getUpdates | ボットに届いた新しいメッセージをサーバーから取得する | bot.getUpdates(...) |
| sendMessage | 指定したチャットIDにメッセージを送る | bot.sendMessage(CHAT_ID, "Hello") |
| コマンド | /で始まる、ボットが理解できる短いメッセージ | /ac、/kapat、/durum |
なぜTelegramなのか?ほかの通知方法との比較
| 方法 | 長所 | 短所 | 向いている用途 |
|---|---|---|---|
| Telegramボット | 無料、即時通知、双方向のコマンド、ポート開放が不要 | グラフや既製のダッシュボードがない | アラームと簡単な遠隔操作 |
| Blynk IoT | 既製のボタン、メーター、グラフのダッシュボード | 無料プランではメッセージ数とデバイス数に制限がある | センサー監視のダッシュボード |
| メール(SMTP) | どのスマホでも使え、長いレポートも送れる | 届くのが遅く、コマンドを受け取りにくい | 毎日のレポート |
| SMS(SIM800L) | Wi-Fiもインターネットも不要 | SIMカードとSMSの料金、追加のモジュールが必要 | インターネットのない山小屋や物置 |
| ESP32のWebサーバー | 完全にローカルで無料 | 通常は同じネットワークからしか使えず、通知もない | 家の中の操作パネル |
まとめると、「何かが起きたら知らせてほしい、コマンドを送ったらこれをしてほしい」というタイプのプロジェクトには、Telegramがいちばん簡単で安上がりな選択肢です。センサーのデータをグラフで見たいなら、Blynk×ESP32入門のほうが合っているかもしれません。Wi-Fiのない場所から通知を受けたいなら、SIM800L GSMモジュールのガイドを見てください(日本ではGSM(2G)のサービスがないため、実際には技適を取得したLTEモジュールが必要です)。
ステップ1:BotFatherでボットを作り、トークンを取得する
- スマホかパソコンでTelegramを開き、検索欄にBotFatherと入力して、名前の横に青いチェックマークが付いた公式アカウントを選びます。似た名前の偽アカウントに注意してください。
- Start(開始)をタップし、/newbotコマンドを送ります。
- BotFatherはまず、ボットの表示名を尋ねてきます。「自宅アラーム」など、好きな名前を入力してください。
- 次に、ユーザー名を尋ねられます。ユーザー名はほかと重複せず、半角英字、数字、アンダースコアだけを使い、最後がbotで終わる必要があります(例:my_home_alarm_bot)。すでに使われている場合は、末尾に数字を付けてもう一度試してください。
- BotFatherから「Done! Congratulations on your new bot」(完了しました。新しいボットの作成おめでとうございます)というメッセージと、ボットへのリンク、そしてトークンが届きます。トークンは1234567890:AAE...のような形で、数字、コロン、長い文字列からできています。全体をコピーして、安全な場所に保管してください。
- BotFatherから届いたリンクをタップしてボットを開き、Startを押します。この手順は欠かせません。Telegramでは、ボットからユーザーに最初のメッセージを送ることはできないので、会話は必ず自分から始める必要があります。
ステップ2:TelegramのチャットIDを調べる方法
ボットのトークンが「どのボットか」を表すのに対し、チャットIDは「誰にメッセージを送るか」を表します。Telegramのユーザー、グループ、チャンネルにはそれぞれ変わらない数字のIDがあり、これは@で始まるユーザー名とは別物です。自分のチャットIDを調べる簡単な方法は3つあります。
- IDボットを使う:Telegramで@userinfobotなどのIDボットを探して、Startを押します。Idの行に自分の番号が送られてきます。これらのボットは自分のボットではなく、IDを表示するだけです。
- ブラウザを使う:まず、自分のボットに何かメッセージを送ります。次に、パソコンのブラウザでapi.telegram.org/botTOKEN/getUpdatesを開きます(TOKENの部分に自分のトークンを貼り付け、その前の「bot」という文字は残します)。表示されたテキストの中の"chat":{"id":123456789にある数字が、チャットIDです。結果のリストが空なら、もう一度ボットにメッセージを送ってから、ページを再読み込みしてください。
- ESP32を使う:下のコマンド用のコードを書き込むと、許可されていない人のIDが、シリアルモニタの「unauthorized ID」(許可されていないID)の行に表示されます。家族を追加したいときは、ボットにメッセージを送ってもらい、そこで番号を確認してください。
ステップ3:UniversalTelegramBotとArduinoJsonをインストールする
- Arduino IDEにまだESP32のボードパッケージを入れていない場合は、先にインストールしてください。手順はESP32とは?にあります。
- 「ツール」>「ライブラリを管理」を開き、検索欄にUniversalTelegramBotと入力して、Brian Loughのライブラリをインストールします。このガイドのコードは、UniversalTelegramBot 1.3.0、ArduinoJson 7、ESP32ボードパッケージのバージョン2.0.11でコンパイルしました。
- 依存ライブラリについて聞かれたら、全てをインストールを選びます。聞かれなかった場合は、同じ画面でArduinoJsonを検索し、Benoit Blanchonのライブラリを別にインストールしてください。
- 「ファイル」>「スケッチ例」>「UniversalTelegramBot」>「ESP32」には、FlashLEDなどの既製の例があります。このガイドのコードは、それらを簡略化し、セキュリティの対策を加えたものです。
UniversalTelegramBotはArduinoJsonのバージョン6の書き方で作られていますが、ArduinoJson 7でも問題なくコンパイルできます。Arduino IDEでコンパイラの警告をオンにしていると、「'DynamicJsonDocument' is deprecated: use JsonDocument instead」(非推奨なのでJsonDocumentを使う)のような行が表示されます。これはエラーではなく警告なので、プログラムは普通に動きます。ライブラリのインストールでつまずいた場合は、Arduinoにライブラリを追加する方法を確認してください。
部品と配線
プロジェクト全体には、ESP32開発ボード、HC-SR501 PIR人感センサー、MQ-2ガスセンサーモジュール、5 Vのリレーモジュール、10 kΩと20 kΩの抵抗を1本ずつ、ブレッドボード、ジャンパーワイヤーが必要です。LEDには、GPIO 2にあるESP32内蔵の青色LEDを使います。最初の2つのステップを試すだけなら、ESP32とUSBケーブルがあれば十分です。センサーはあとから追加できます。




| 接続 | ESP32 | ESP8266(NodeMCU) | Raspberry Pi Pico W |
|---|---|---|---|
| PIR VCC | VIN(5V) | VIN(5V) | VBUS(5V) |
| PIR OUT | GPIO 27 | D5(GPIO 14) | GP16 |
| MQ-2 VCC | VIN(5V) | VIN(5V) | VBUS(5V) |
| MQ-2 AO(10 kΩ / 20 kΩの分圧回路を経由) | GPIO 34 | A0 | GP26(ADC0) |
| リレーのVCC | VIN(5V) | VIN(5V) | VBUS(5V) |
| リレーのIN | GPIO 26 | D1(GPIO 5) | GP15 |
| LED | GPIO 2(内蔵) | D4(内蔵、LOWで点灯) | "LED"(内蔵) |
| すべてのGND | GND | GND | GND |
電源にも注意してください。MQ-2のヒーターだけで約150 mA、リレーのコイルで約70 mAを消費し、ESP32はWi-Fiで送信するときに一瞬だけ最大250 mAに達することがあります。パソコンのUSBポートではこの合計をいつもまかなえるとは限らず、メッセージを送る瞬間にボードが再起動することがあります。1 A以上の5 V ACアダプターと品質のよいケーブルを使ってください。それでも問題が続く場合は、ESP32のブラウンアウトとGuru Meditationを読んでください。ボードのすべてのピンはピン配置ツールで確認できます。
最初のテスト:ESP32からTelegramにメッセージを送る
センサーに進む前に、いちばんシンプルなスケッチでトークンとチャットIDを確認しましょう。下のコードのWi-Fi名、パスワード、BOT_TOKEN、CHAT_IDの行を自分の情報に書き換えて、ボードに書き込みます。次に、シリアルモニタを115200ボーで開きます。
1#include <WiFi.h>
2#include <WiFiClientSecure.h>
3#include <UniversalTelegramBot.h>
4#include <ArduinoJson.h>
5
6const char* WIFI_SSID = "AgAdi"; // Wi-Fi adın (yalnız 2,4 GHz) / your Wi-Fi name (2.4 GHz only)
7const char* WIFI_PASS = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
8#define BOT_TOKEN "1234567890:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // BotFather'ın verdiği token / token from BotFather
9#define CHAT_ID "123456789" // kendi Telegram ID'n / your own Telegram ID
10
11WiFiClientSecure client; // şifreli (HTTPS) bağlantı / encrypted (HTTPS) connection
12UniversalTelegramBot bot(BOT_TOKEN, client);
13
14void setup() {
15 Serial.begin(115200);
16 WiFi.mode(WIFI_STA);
17 WiFi.begin(WIFI_SSID, WIFI_PASS);
18 Serial.print("Wi-Fi'ye baglaniyor / connecting to Wi-Fi");
19 while (WiFi.status() != WL_CONNECTED) {
20 delay(500);
21 Serial.print(".");
22 }
23 Serial.println();
24 Serial.println(WiFi.localIP());
25
26 client.setCACert(TELEGRAM_CERTIFICATE_ROOT); // api.telegram.org kök sertifikası / root certificate of api.telegram.org
27
28 if (bot.sendMessage(CHAT_ID, "Merhaba! ESP32 çevrimiçi / Hello! ESP32 is online")) {
29 Serial.println("Mesaj gonderildi / message sent");
30 } else {
31 Serial.println("Mesaj gonderilemedi / message failed");
32 }
33}
34
35void loop() {
36}数秒以内に、ボットからTelegramに「Hello! ESP32 is online」(ESP32が起動しました)というメッセージが届き、シリアルモニタに「Mesaj gonderildi / message sent」(トルコ語と英語で「送信しました」)と表示されるはずです。sendMessage()はメッセージが届くとtrueを返します。「message failed」(送信失敗)と表示された場合、原因はWi-Fi接続、トークン、チャットID、証明書の設定のどれかです。コードを疑う前に、ブラウザでapi.telegram.org/botTOKEN/getMeを開いてみてください。トークンが正しければ"ok":trueとボットの名前が、間違っていれば「Unauthorized」(認証失敗)が表示されます。
証明書の設定:setCACertとsetInsecureのどちらを使う?
TelegramはHTTPSしか受け付けないため、コードでは普通のWiFiClientではなくWiFiClientSecureを使います。暗号化された接続では、ESP32は通信相手のサーバーが本当にapi.telegram.orgであることを確かめなければなりません。そのために、証明書チェーンの最上位にあるルート証明書を使って、サーバーの証明書を検証します。UniversalTelegramBotには、このルート証明書がTELEGRAM_CERTIFICATE_ROOTとして、すぐに使える形で入っています。Telegramの証明書チェーンは、2037年末まで有効なGo Daddy Root Certificate Authority - G2までさかのぼります。Telegramが証明書の発行元を変えた場合は、ライブラリも更新が必要になります。
| 設定 | 役割 | 安全性 | 使う場面 |
|---|---|---|---|
| client.setCACert(TELEGRAM_CERTIFICATE_ROOT) | 接続を暗号化し、サーバーの身元を確認する | 高 | 推奨の設定。常にこれを使う |
| client.setInsecure() | 接続は暗号化するが、サーバーの身元は確認しない | 中:間に入り込んだ偽のサーバーに気づけない | 証明書の問題かどうかを切り分けるための簡単なテスト |
| どちらも使わない | 現在のESP32コアでは、接続がまったく確立されない | 動作しない | 使わない:メッセージは送られず、シリアルモニタにもエラーが出ない |
| ESP8266:setTrustAnchors(&cert) | ESP8266での同等の設定。証明書はX509Listとして渡す | 高 | NodeMCUとWemos D1 Mini。正しい時刻が必要 |
Telegramのコマンド(/ac、/kapat)でLEDとリレーを制御する
次は、ボットを双方向にしましょう。下のコードは1秒に1回、Telegramに新しいメッセージを問い合わせます。テキストが/ac(トルコ語で「オン」)ならリレーとLEDをオンにし、/kapat(「オフ」)ならオフにし、/durum(「状態」)なら現在の状態を報告し、/startならコマンドの一覧を送ります。コマンド名は/onや/offに変えてもかまいませんが、Telegramのコマンドに使えるのは半角の英小文字、数字、アンダースコアだけで、長さは32文字までという点に注意してください。「/aç」のようなアクセント付きの文字を含むコマンドは、タップできるコマンドとして認識されません。日本語の文字も、同じくコマンドには使えません。
1#include <WiFi.h>
2#include <WiFiClientSecure.h>
3#include <UniversalTelegramBot.h>
4#include <ArduinoJson.h>
5
6const char* WIFI_SSID = "AgAdi";
7const char* WIFI_PASS = "Sifre123";
8#define BOT_TOKEN "1234567890:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
9#define CHAT_ID "123456789" // yalnız bu ID'ye yanıt verilir / only this ID gets answers
10
11const int LED_PIN = 2; // çoğu DevKit'te dahili LED / onboard LED on most DevKits
12const int ROLE_PIN = 26; // röle modülünün IN pini / relay module IN pin
13const bool ROLE_AKTIF_LOW = true; // çoğu modül LOW'da çeker / most modules switch on with LOW
14
15const unsigned long KONTROL_ARALIGI = 1000; // Telegram'a saniyede bir sor / ask Telegram once per second
16unsigned long sonKontrol = 0;
17bool roleAcik = false;
18
19WiFiClientSecure client;
20UniversalTelegramBot bot(BOT_TOKEN, client);
21
22void roleYaz(bool acik) {
23 roleAcik = acik;
24 if (ROLE_AKTIF_LOW) digitalWrite(ROLE_PIN, acik ? LOW : HIGH);
25 else digitalWrite(ROLE_PIN, acik ? HIGH : LOW);
26 digitalWrite(LED_PIN, acik ? HIGH : LOW); // LED röle durumunu gösterir / LED mirrors the relay
27}
28
29void mesajlariIsle(int adet) {
30 for (int i = 0; i < adet; i++) {
31 String chatId = bot.messages[i].chat_id;
32 String metin = bot.messages[i].text;
33
34 // Güvenlik: tanımadığın kişiye cevap verme / security: never answer strangers
35 if (chatId != CHAT_ID) {
36 Serial.println("Yetkisiz ID / unauthorized ID: " + chatId);
37 continue;
38 }
39
40 int at = metin.indexOf('@'); // gruptaki "/ac@bot_adi" -> "/ac" / "/ac@bot_name" in groups -> "/ac"
41 if (at > 0) metin = metin.substring(0, at);
42
43 if (metin == "/start") {
44 String yardim = "Merhaba " + bot.messages[i].from_name + "!\n";
45 yardim += "/ac - röleyi aç / relay ON\n";
46 yardim += "/kapat - röleyi kapat / relay OFF\n";
47 yardim += "/durum - son durum / status";
48 bot.sendMessage(chatId, yardim);
49 } else if (metin == "/ac") {
50 roleYaz(true);
51 bot.sendMessage(chatId, "Röle AÇIK / relay ON");
52 } else if (metin == "/kapat") {
53 roleYaz(false);
54 bot.sendMessage(chatId, "Röle KAPALI / relay OFF");
55 } else if (metin == "/durum") {
56 bot.sendMessage(chatId, roleAcik ? "Röle AÇIK / relay ON" : "Röle KAPALI / relay OFF");
57 } else {
58 bot.sendMessage(chatId, "Bilinmeyen komut, /start yaz / unknown command, send /start");
59 }
60 }
61}
62
63void setup() {
64 Serial.begin(115200);
65 pinMode(LED_PIN, OUTPUT);
66 pinMode(ROLE_PIN, OUTPUT);
67 roleYaz(false); // açılışta röle kapalı / relay off at boot
68
69 WiFi.mode(WIFI_STA);
70 WiFi.begin(WIFI_SSID, WIFI_PASS);
71 while (WiFi.status() != WL_CONNECTED) delay(500);
72 Serial.println(WiFi.localIP());
73
74 client.setCACert(TELEGRAM_CERTIFICATE_ROOT);
75 bot.sendMessage(CHAT_ID, "Bot hazır, /start yaz / bot ready, send /start");
76}
77
78void loop() {
79 if (millis() - sonKontrol > KONTROL_ARALIGI) {
80 int adet = bot.getUpdates(bot.last_message_received + 1); // yeni mesaj var mı? / any new messages?
81 while (adet) {
82 mesajlariIsle(adet);
83 adet = bot.getUpdates(bot.last_message_received + 1);
84 }
85 sonKontrol = millis();
86 }
87}| 行 | 役割 |
|---|---|
| bot.getUpdates(bot.last_message_received + 1) | 最後に処理したメッセージより後のメッセージを要求し、届いた件数を返す。+1で同じメッセージが戻ってこないようにする |
| bot.messages[i].chat_id | メッセージが届いたチャットのID。返信はここに送られ、セキュリティチェックにも使う |
| bot.messages[i].text(本文) | 届いたテキスト。これとコマンドを比較する |
| metin.indexOf('@') | グループでTelegramがコマンドに付け足す@bot_nameの部分を切り取る |
| ROLE_AKTIF_LOW | 多くのリレーモジュールはLOW信号でオンになる。HIGHでオンになるモジュールならfalseにする |
| KONTROL_ARALIGI | 問い合わせの間隔。1000 msなら反応が速く、3000〜5000 msならネットワークへの負荷が少ない |
セキュリティ:自分のIDにだけ返信させる
Telegramのボットのユーザー名は公開されていて、検索もできます。誰かがmy_home_alarm_botを推測したり、スクリーンショットで見つけたりして/acを送ると、チェックのないESP32はそのコマンドにも従ってしまいます。だからこそ、コードでいちばん大事な行はchatId != CHAT_IDのチェックです。チャットIDは秘密にしておく必要はありません。守りの仕組みは、Telegram自身が各メッセージの送信者を確認していることにあり、誰も他人のIDを偽ることはできません。知らない相手にまったく返信しないのも、意図的な選択です。ボットが黙っている限り、動いていることさえ誰にもわかりません。
- 許可リストを使う:許可するIDを配列にまとめ、それ以外のメッセージはすべて無視して、家族を1人ずつ追加します。
- グループでは人を確認する:グループではチャットIDがグループのIDになります。特定の人だけがコマンドを送れるようにしたいなら、chat_idではなくfrom_idを比較します。
- トークンは別のファイルに:トークンとWi-Fiのパスワードはsecrets.hのようなファイルに入れ、コードを共有するときはそのファイルを除外します。
- 古いコマンドは飛ばす:停電中に送った/acが、ボードが復帰したときに思わぬタイミングで実行されてはいけません。完全版のコードでは、起動時のクリーンアップがこれを防ぎます。
- 起動時は安全な状態に:ボードが起動するたびに、リレーがオフの状態から始めます。ヒーターやポンプのような負荷では、これがとても重要です。
- 重要な仕事をボットだけに任せない:ドアロックや給湯器のようなものでは、間違ったコマンドで何が起こりうるかを考え、確認メッセージや時間制限を加えてください。
人感とガスのアラーム:Telegramに通知を送る完全版プロジェクト
では、これまでの部品を組み合わせましょう。下のコードは、PIRセンサーが動きを検知したときと、MQ-2のガスの値がしきい値を超えたときに、時刻付きの通知をTelegramで送ります。同時に/ac、/kapat、/alarm_ac、/alarm_kapat(アラームのオン/オフ)、/durumのコマンドを待ち受け、/startと入力すると画面下にタップで送れるボタンのメニューを表示し、コマンドをTelegramの「Menu」ボタン(メニュー)にも追加します。各センサーの詳細は、PIR人感センサーとMQ-2ガスセンサーのガイドを見てください。
1#include <WiFi.h>
2#include <WiFiClientSecure.h>
3#include <UniversalTelegramBot.h>
4#include <ArduinoJson.h>
5#include <time.h>
6
7const char* WIFI_SSID = "AgAdi";
8const char* WIFI_PASS = "Sifre123";
9#define BOT_TOKEN "1234567890:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
10#define CHAT_ID "123456789" // bildirimlerin gideceği ID / the ID that receives alerts
11
12// Komut verebilecek kişiler; aile üyesi eklemek için: { CHAT_ID, "222222222" }
13// People allowed to send commands; to add a family member: { CHAT_ID, "222222222" }
14const char* YETKILI[] = { CHAT_ID };
15const int YETKILI_SAYISI = sizeof(YETKILI) / sizeof(YETKILI[0]);
16
17const int LED_PIN = 2; // dahili LED / onboard LED
18const int ROLE_PIN = 26; // röle modülü IN / relay module IN
19const int PIR_PIN = 27; // PIR sensörü OUT / PIR sensor OUT
20const int GAZ_PIN = 34; // MQ-2 AO, gerilim bölücü üzerinden / MQ-2 AO through a voltage divider
21const bool ROLE_AKTIF_LOW = true; // çoğu modül LOW'da çeker / most modules switch on with LOW
22
23const int GAZ_ESIK = 1800; // temiz havadaki değere göre ayarla / tune it from your clean-air reading
24const int GAZ_FARK = 200; // histerezis payı / hysteresis margin
25const unsigned long HAREKET_BEKLEME = 60000; // iki hareket bildirimi arası en az 60 sn / at least 60 s between motion alerts
26const unsigned long GAZ_ISINMA = 120000; // MQ-2 ilk 2 dakika ısınır / MQ-2 warms up for the first 2 minutes
27const unsigned long KONTROL_ARALIGI = 1000; // Telegram'a saniyede bir sor / poll Telegram once per second
28
29WiFiClientSecure client;
30UniversalTelegramBot bot(BOT_TOKEN, client);
31
32bool alarmKurulu = true; // hareket alarmı açık mı / is the motion alarm armed
33bool roleAcik = false;
34bool gazVar = false;
35unsigned long sonKontrol = 0;
36unsigned long sonHareket = 0;
37
38String saat() { // "14:05:31" biçiminde saat / time as "14:05:31"
39 struct tm t;
40 if (!getLocalTime(&t, 100)) return "--:--:--";
41 char buf[9];
42 strftime(buf, sizeof(buf), "%H:%M:%S", &t);
43 return String(buf);
44}
45
46bool yetkiliMi(const String& id) {
47 for (int i = 0; i < YETKILI_SAYISI; i++) {
48 if (id == YETKILI[i]) return true;
49 }
50 return false;
51}
52
53void roleYaz(bool acik) {
54 roleAcik = acik;
55 if (ROLE_AKTIF_LOW) digitalWrite(ROLE_PIN, acik ? LOW : HIGH);
56 else digitalWrite(ROLE_PIN, acik ? HIGH : LOW);
57}
58
59String durumMetni() {
60 String s = "Röle / relay: ";
61 s += roleAcik ? "AÇIK / ON" : "KAPALI / OFF";
62 s += "\nAlarm: ";
63 s += alarmKurulu ? "KURULU / ARMED" : "KAPALI / DISARMED";
64 s += "\nGaz / gas: " + String(analogRead(GAZ_PIN));
65 s += "\nWi-Fi: " + String(WiFi.RSSI()) + " dBm";
66 return s;
67}
68
69void mesajlariIsle(int adet) {
70 for (int i = 0; i < adet; i++) {
71 String chatId = bot.messages[i].chat_id;
72 String metin = bot.messages[i].text;
73
74 if (!yetkiliMi(chatId)) { // yabancıya sessiz kal / stay silent to strangers
75 Serial.println("Yetkisiz ID / unauthorized ID: " + chatId);
76 continue;
77 }
78 int at = metin.indexOf('@'); // "/ac@bot_adi" -> "/ac"
79 if (at > 0) metin = metin.substring(0, at);
80
81 if (metin == "/ac") {
82 roleYaz(true);
83 bot.sendMessage(chatId, "Röle AÇIK / relay ON");
84 } else if (metin == "/kapat") {
85 roleYaz(false);
86 bot.sendMessage(chatId, "Röle KAPALI / relay OFF");
87 } else if (metin == "/alarm_ac") {
88 alarmKurulu = true;
89 bot.sendMessage(chatId, "Hareket alarmı kuruldu / motion alarm armed");
90 } else if (metin == "/alarm_kapat") {
91 alarmKurulu = false;
92 bot.sendMessage(chatId, "Hareket alarmı kapatıldı / motion alarm disarmed");
93 } else if (metin == "/durum") {
94 bot.sendMessage(chatId, durumMetni());
95 } else { // /start ve diğer her şey: tuş menüsü / /start and anything else: button menu
96 String klavye = "[[\"/ac\",\"/kapat\"],[\"/alarm_ac\",\"/alarm_kapat\"],[\"/durum\"]]";
97 bot.sendMessageWithReplyKeyboard(chatId, "Komut seç / choose a command", "", klavye, true);
98 }
99 }
100}
101
102void setup() {
103 Serial.begin(115200);
104 pinMode(LED_PIN, OUTPUT);
105 pinMode(ROLE_PIN, OUTPUT);
106 pinMode(PIR_PIN, INPUT);
107 roleYaz(false); // açılışta röle kapalı / relay off at boot
108
109 WiFi.mode(WIFI_STA);
110 WiFi.begin(WIFI_SSID, WIFI_PASS);
111 while (WiFi.status() != WL_CONNECTED) {
112 delay(500);
113 Serial.print(".");
114 }
115 Serial.println(WiFi.localIP());
116
117 client.setCACert(TELEGRAM_CERTIFICATE_ROOT); // api.telegram.org kök sertifikası / root certificate of api.telegram.org
118 bot.maxMessageLength = 4000; // büyük mesajlarda takılmayı önler / prevents hanging on big messages
119 configTime(3 * 3600, 0, "pool.ntp.org"); // Türkiye UTC+3; kendi saat dilimini yaz / Turkey is UTC+3, set your own zone
120
121 // Telegram'daki Menü düğmesi; komutlar küçük harf, Türkçe karakter yok
122 // The Menu button in Telegram; commands are lowercase with no special letters
123 bot.setMyCommands("[{\"command\":\"ac\",\"description\":\"Röleyi aç\"},"
124 "{\"command\":\"kapat\",\"description\":\"Röleyi kapat\"},"
125 "{\"command\":\"alarm_ac\",\"description\":\"Hareket alarmını kur\"},"
126 "{\"command\":\"alarm_kapat\",\"description\":\"Hareket alarmını kapat\"},"
127 "{\"command\":\"durum\",\"description\":\"Durumu göster\"}]");
128
129 // Kart kapalıyken gelen eski komutları atla / skip old commands sent while the board was off
130 while (bot.getUpdates(bot.last_message_received + 1)) { }
131
132 bot.sendMessage(CHAT_ID, "Sistem açıldı / system started " + saat());
133}
134
135void loop() {
136 unsigned long simdi = millis();
137
138 // 1) Hareket alarmı / motion alarm
139 if (alarmKurulu && digitalRead(PIR_PIN) == HIGH && simdi - sonHareket > HAREKET_BEKLEME) {
140 sonHareket = simdi;
141 digitalWrite(LED_PIN, HIGH);
142 bot.sendMessage(CHAT_ID, "Hareket algılandı! / motion detected! " + saat());
143 digitalWrite(LED_PIN, LOW);
144 }
145
146 // 2) Gaz alarmı: ısınmadan sonra, susturulamaz / gas alarm: after warm-up, cannot be muted
147 if (simdi > GAZ_ISINMA) {
148 int gaz = analogRead(GAZ_PIN); // 0-4095
149 if (!gazVar && gaz > GAZ_ESIK) {
150 gazVar = true;
151 bot.sendMessage(CHAT_ID, "GAZ ALARMI! / GAS ALARM! " + String(gaz) + " " + saat());
152 } else if (gazVar && gaz < GAZ_ESIK - GAZ_FARK) {
153 gazVar = false;
154 bot.sendMessage(CHAT_ID, "Gaz normale döndü / gas back to normal: " + String(gaz));
155 }
156 }
157
158 // 3) Telegram komutları / Telegram commands
159 if (simdi - sonKontrol > KONTROL_ARALIGI && WiFi.status() == WL_CONNECTED) {
160 int adet = bot.getUpdates(bot.last_message_received + 1);
161 while (adet) {
162 mesajlariIsle(adet);
163 adet = bot.getUpdates(bot.last_message_received + 1);
164 }
165 sonKontrol = millis();
166 }
167}| 設定 | 値 | 説明 |
|---|---|---|
| GAZ_ESIK | 1800 | 0〜4095の生のADC値。きれいな空気での読み取り値をもとに設定する |
| GAZ_FARK | 200 | 「正常に戻りました」の通知を送る前に、値がしきい値からこの分だけ下がる必要がある。境界付近で通知が何度も切り替わるのを防ぐ |
| HAREKET_BEKLEME | 60000 ms | 2回の動きの通知の間を、最低60秒空ける |
| GAZ_ISINMA | 120000 ms | MQ-2は予熱中に高い値を示すため、最初の2分間はガスをチェックしない |
| KONTROL_ARALIGI | 1000 ms | Telegramに新しいメッセージを問い合わせる間隔 |
| bot.maxMessageLength | 4000 | ライブラリが読み取る応答の最大長。初期値は1500 |
コードの2つのアラームは、あえて異なる動きをするようにしています。人感アラームはクールダウンで守られています。部屋の中を人が歩き回っている間、PIRセンサーは何度も反応するので、クールダウンがないとスマホが1分間に何十件ものメッセージであふれてしまいます。クールダウンのタイマーはゼロから始まるので、最初の通知が届くのはボードの起動から早くても60秒後です。これは、PIRセンサーが電源投入後に安定するまでにかかる約1分ともちょうど合います。一方、ガスアラームではヒステリシスを使っています。値がしきい値を超えたときに1回だけ警告し、しきい値より200下がって初めて「正常に戻りました」と知らせます。また、安全のため/alarm_kapatでは止められません。Telegram側も、あまり頻繁なメッセージは許可していません。同じチャットに1秒に1件より多く、またはグループに1分に20件より多く送ろうとすると、「Too Many Requests: retry after」(リクエストが多すぎるので時間をおいて再試行)エラーになります。
ESP8266(NodeMCU)でTelegramボットを作る
同じライブラリはESP8266 NodeMCUやWemos D1 Miniのボードでも動きます。コマンドとアラームのロジックはまったく同じで、変わるのは接続の部分だけです。WiFi.hの代わりにESP8266WiFi.hを使い、ルート証明書をX509ListとしてsetTrustAnchors()に渡します。また、ESP8266は証明書の有効期間をチェックするので、接続の前にNTPで時刻を取得する必要があります。時刻がないと、すべての接続がエラーも出さずに失敗します。NodeMCUの内蔵LEDはD4ピンにあってLOWで点灯し、アナログ入力はA0の1つだけです。
1#include <ESP8266WiFi.h>
2#include <WiFiClientSecure.h>
3#include <UniversalTelegramBot.h>
4#include <ArduinoJson.h>
5#include <time.h>
6
7const char* WIFI_SSID = "AgAdi";
8const char* WIFI_PASS = "Sifre123";
9#define BOT_TOKEN "1234567890:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
10#define CHAT_ID "123456789"
11
12X509List cert(TELEGRAM_CERTIFICATE_ROOT); // ESP8266'da kök sertifika böyle verilir / how ESP8266 takes the root certificate
13WiFiClientSecure client;
14UniversalTelegramBot bot(BOT_TOKEN, client);
15
16void setup() {
17 Serial.begin(115200);
18 configTime(0, 0, "pool.ntp.org"); // sertifika kontrolü için saat şart / the clock is needed to check the certificate
19 client.setTrustAnchors(&cert);
20
21 WiFi.mode(WIFI_STA);
22 WiFi.begin(WIFI_SSID, WIFI_PASS);
23 while (WiFi.status() != WL_CONNECTED) delay(500);
24
25 time_t simdi = time(nullptr); // NTP saatini bekle / wait for the NTP time
26 while (simdi < 24 * 3600) {
27 delay(100);
28 simdi = time(nullptr);
29 }
30 bot.sendMessage(CHAT_ID, "ESP8266 çevrimiçi / ESP8266 is online");
31}
32
33void loop() {
34}Raspberry Pi Pico WとMicroPythonでTelegramボットを作る
Raspberry Pi Pico Wには、別のライブラリは必要ありません。MicroPythonのrequestsモジュールで、Telegram Bot APIに直接リクエストを送れます。下のコードは、動きを検知すると通知を送り、/acと/kapatのコマンドでGP15のリレーとボードの内蔵LEDを切り替え、自分のチャットIDにだけ返信します。ThonnyでPico Wに最新のMicroPythonを書き込み、コードをmain.pyとして保存してください。MicroPythonのrequestsモジュールは接続を暗号化しますが、サーバー証明書は検証しないので、安全性のレベルはESP32のsetInsecure()と同じです。
1import network, time
2import requests
3from machine import Pin
4
5WIFI_SSID = "AgAdi" # Wi-Fi adın / your Wi-Fi name
6WIFI_PASS = "Sifre123" # Wi-Fi şifren / your Wi-Fi password
7TOKEN = "1234567890:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
8CHAT_ID = 123456789 # sayı olarak yaz / write it as a number
9API = "https://api.telegram.org/bot" + TOKEN
10
11led = Pin("LED", Pin.OUT) # Pico W dahili LED / Pico W onboard LED
12role = Pin(15, Pin.OUT, value=1) # aktif LOW röle kapalı başlar / active-LOW relay starts off
13pir = Pin(16, Pin.IN) # PIR OUT
14
15wlan = network.WLAN(network.STA_IF)
16wlan.active(True)
17wlan.connect(WIFI_SSID, WIFI_PASS)
18while not wlan.isconnected():
19 time.sleep(0.5)
20print("IP:", wlan.ifconfig()[0])
21
22def gonder(metin):
23 try:
24 r = requests.post(API + "/sendMessage", json={"chat_id": CHAT_ID, "text": metin})
25 r.close()
26 except Exception as e:
27 print("Gonderilemedi / send failed:", e)
28
29def yeni_mesajlar(offset):
30 try:
31 r = requests.get(API + "/getUpdates?timeout=0&limit=5&offset=%d" % offset)
32 sonuc = r.json().get("result", [])
33 r.close()
34 return sonuc
35 except Exception as e:
36 print("Okunamadi / read failed:", e)
37 return []
38
39# Kart kapalıyken gelen eski komutları atla / skip old commands sent while the board was off
40offset = 0
41eski = yeni_mesajlar(offset)
42while eski:
43 offset = eski[-1]["update_id"] + 1
44 eski = yeni_mesajlar(offset)
45
46gonder("Pico W çevrimiçi / Pico W is online")
47son_hareket = time.ticks_ms() # ilk 60 sn PIR dengelenir / the PIR settles in the first 60 s
48
49while True:
50 if pir.value() and time.ticks_diff(time.ticks_ms(), son_hareket) > 60000:
51 son_hareket = time.ticks_ms()
52 gonder("Hareket algılandı! / motion detected!")
53
54 for u in yeni_mesajlar(offset):
55 offset = u["update_id"] + 1
56 mesaj = u.get("message", {})
57 if mesaj.get("chat", {}).get("id") != CHAT_ID: # yalnız sana yanıt ver / answer only you
58 continue
59 komut = mesaj.get("text", "").split("@")[0]
60 if komut == "/ac":
61 role.value(0) # aktif LOW: 0 = açık / active LOW: 0 = on
62 led.on()
63 gonder("Röle AÇIK / relay ON")
64 elif komut == "/kapat":
65 role.value(1)
66 led.off()
67 gonder("Röle KAPALI / relay OFF")
68 elif komut == "/durum":
69 gonder("Röle / relay: " + ("AÇIK / ON" if role.value() == 0 else "KAPALI / OFF"))
70 time.sleep(1)ESP32のTelegramボットのプロジェクトアイデア
- ドア・窓のアラーム:リードスイッチやPIRセンサーを使い、誰もいない家で動きがあったらスマホにメッセージを送ります。
- 写真付きの防犯カメラ:動きを検知したらESP32-CAMで写真を撮り、Telegramに送ります。手順はESP32-CAMとTelegramのガイドにあります。
- 部屋や温室のモニター:DHT11で測った温度が設定値を超えたら警告を受け取り、/durumで現在の値を問い合わせます。
- 植物の水やりリマインダー:土壌水分センサーが乾燥を示したら通知を受け取り、/sula(トルコ語で「水をやる」)のようなコマンドでポンプを数秒間動かします。
- 火災の警告:炎センサーとMQ-2を組み合わせて、両方の危険を同時に見張ります。
- 電池で動く郵便受けセンサー:ESP32をディープスリープにしておき、ふたが開いたら起こして、メッセージを1件送ってからまたスリープに戻します。
- 遠隔操作のコンセント:リレーを通して、卓上ランプや扇風機を世界中のどこからでも/acと/kapatで切り替えます。
ESP32のTelegramボットでよくあるエラーと対処法
| 症状またはメッセージ | 原因 | 対処 |
|---|---|---|
| sendMessageがfalseを返し、シリアルモニタにエラーが出ない | setCACertかsetInsecureの行がない、トークンに空白が入っている、またはWi-Fiにつながっていない | 証明書の行を追加し、トークンをコピーし直し、まずWi-Fi接続を確認する |
| ブラウザでのテストで「Forbidden: bot can't initiate conversation with a user」(ボットからは会話を始められない) | ボットでStartを押していない、またはボットをブロックしている | Telegramでボットを開いてStartを押す。ブロックしていたら解除する |
| エラー:「Bad Request: chat not found」(チャットが見つからない) | チャットIDの間違い、グループIDのマイナス記号の付け忘れ、またはボットがグループから削除された | getUpdatesでIDを確認し直し、グループIDはマイナス記号付きで書く |
| エラー:「Unauthorized」(認証失敗) | トークンが間違っているか、/revokeで変更された | BotFatherから現在のトークンをコピーし、コードを書き込み直す |
| メッセージは送れるが、コマンドに反応しない | loop()内の長いdelay()、チャットIDの間違い、またはグループでコマンドに@bot_nameが付いている | 待ち時間にはmillis()を使い、@以降を切り取り、シリアルモニタの「unauthorized ID」の行を確認する |
| 写真やスタンプのあと、ボットがまったく反応しなくなった | ライブラリは応答の最初の1500文字しか読まないため、長い応答を解析できず、同じメッセージが何度も戻ってくる | setup()にbot.maxMessageLength = 4000;の行を追加する |
| 起動時にボードが古いコマンドを実行する | Telegramは未読のメッセージを最大24時間保持する | 起動時に保留中のメッセージを読み取って破棄する |
| エラー:「Conflict: terminated by other getUpdates request」(ほかのgetUpdatesと競合) | 同じトークンが2台のデバイスで同時に、またはパソコンのスクリプトでも動いている | デバイスごとにBotFatherで別のボットを作る |
| エラー:「Too Many Requests: retry after 30」(送信が多すぎる) | メッセージの送信間隔が短すぎる | クールダウンを加え、同じチャットへの送信は1秒に1件までにする |
| コンパイル時に「ArduinoJson.h: No such file or directory」(ファイルが見つからない) | ArduinoJsonライブラリがインストールされていない | ライブラリマネージャからArduinoJsonをインストールする |
| メッセージの送信中にボードが再起動する | Wi-Fi送信時の電流の急増に電源が耐えられない(ブラウンアウト) | 1 AのACアダプター、短く品質のよいケーブル、3V3とGNDの間に100〜470 µFのコンデンサ |
トークンとチャットID、そして数行のコードだけで、ESP32は家の様子を報告し、コマンドを受け付ける小さなアシスタントになりました。同じ仕組みに温度、水位、ドアのセンサーを加えたり、通知を家族のグループに送ったりして、さらに広げることもできます。BlueGraysアプリには、PIR、ブザー、LEDでTelegramに通知を送るESP32の人感アラームシステム、MQ-2のガス・煙検知アラーム、ESP32のWi-Fiプロジェクトが、回路図とコード付きでステップごとに揃っています。
よくある質問
ESP32のTelegramボットは無料ですか?
はい。Telegramでのボットの作成、Bot APIの利用、メッセージの送信はすべて無料で、月ごとのメッセージ数の上限もありません。あるのは送信頻度の制限だけで、同じチャットへは1秒に約1件、グループへは1分に20件までです。家庭のプロジェクトなら、この制限で十分すぎるほどです。
TelegramのチャットIDはどうやって調べますか?
いちばん簡単なのは、Telegramで@userinfobotなどのIDボットを開いてStartを押す方法です。ID番号が送られてきます。または、自分のボットにメッセージを送り、ブラウザでapi.telegram.org/botTOKEN/getUpdatesを開いて、"chat"の部分にあるidの値を読み取ります。グループのIDはマイナス記号で始まります。
ESP32からTelegramにメッセージが送られないのはなぜですか?
よくある原因は、ボットでStartを押していない、チャットIDが間違っている、トークンに空白が入っている、コードにsetCACertかsetInsecureの行がない、のいずれかです。getMeとsendMessageのアドレスをブラウザで開けば、コードとは関係なくトークンとチャットIDをテストできます。また、ボードが2.4 GHzのWi-Fiネットワークにつながっていることも確認してください。
ほかの人にTelegramボットを操作されることはありますか?
ボットのユーザー名は公開されているので、コードにIDのチェックを入れていなければ、ボットを見つけた人は誰でもコマンドを送れてしまいます。届いたチャットIDを毎回自分のIDと比較し、それ以外を無視すれば、これを防げます。Telegramが送信者を確認しているからです。トークンも絶対に共有しないでください。漏れた場合は、BotFatherの/revokeで新しいものに替えてください。
ESP8266でもTelegramボットは作れますか?
はい。UniversalTelegramBotライブラリは、NodeMCUやWemos D1 MiniなどのESP8266ボードでも動きます。ルート証明書はX509ListとしてsetTrustAnchors()に渡し、接続の前にNTPで時刻を取得する必要があります。ESP8266はメモリが少ないので、長いメッセージはESP32のほうが余裕をもって扱えます。
ESP32からTelegramに写真を送れますか?
はい。ライブラリのsendPhotoByBinary()関数を使えば、ESP32-CAMで撮影した画像をそのままTelegramに送れます。カメラのないESP32でも、同じ方法でSDカードに保存した画像を送ったり、sendPhoto()でネット上の画像のアドレスを送ったりできます。動きを検知したら写真を送るシステムは、防犯プロジェクトの中でも特に人気があります。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
ESP32でLEDを点滅
ESP32のGPIO出力の基本を学びます。デジタルピンからLEDを点灯・消灯させて、点滅させます。Arduino UNOの定番のLED点滅プロジェクトのESP32版です。Arduino環境で使う基本的な点滅コマンドは同じですが、ESP32のピン配置と3.3 VのGPIO電圧レベルはArduino UNOとは異なります。

ESP32の内蔵LEDを点滅
ESP32 DevKit V1に内蔵された青色LEDを点滅させます。外付け部品は不要です!ESP32を購入したら、最初に試すプロジェクトです。GPIO 2が内蔵LEDにつながっています。

ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)
PIRセンサーが動きを検知するとブザーとLEDを作動させ、同時にTelegramで持ち主に即座に通知するスマート防犯アラームです。アラームが繰り返される頻度を抑えるため、30秒のクールダウン(待ち時間)を設けています。この仕組みはPIRの検知が正しいかを確認したり、誤検知をなくしたりするものではありません。家の防犯、ガレージの監視、倉庫や作業部屋などに使えます。ESP32-CAMなら写真付きの通知を追加でき、通常のESP32 DevKit V1ではテキストで通知します。

ESP32 MQ-2ガス・煙検知アラーム
このプロジェクトでは、MQ-2ガスセンサーモジュールで可燃性ガス(LPG、メタン、プロパン)と煙を検知し、パッシブブザーでサイレンのような警報音を鳴らします。センサーのしきい値判定用デジタル出力(DO)をESP32のGPIO 4ピンに接続します。ガスや煙がしきい値を超えるとDOがLOWになり、アラームが作動します。感度はモジュール上のポテンショメーターで調整します。パッシブブザーをtone()で駆動し、2つの周波数を交互に切り替えてサイレン音を作ります。

ESP32スマート照明(PIR + LDR + リレー + OLED)
ESP32で廊下やトイレ、食品庫の照明をスマート化します!PIRモーションセンサー、LDR光センサー、リレーモジュールを使い、「暗い場所で動きを検知すると点灯する」定番の仕組みを作ります。周囲が明るければ動きを検知しても点灯しないため、日中の無駄な電力消費を抑えられます。照明は30秒間点灯し、動きが続くとタイマーがリセットされて点灯時間が延びます。SSD1306 OLEDには大きな文字で状態(ON / DAYTIME / WAITING)とカウントダウンを表示します。

ESP32 PIR人感センサーによる自動照明
このプロジェクトでは、HC-SR501 PIR人感センサーで部屋に入った人を検知し、LEDを自動で点灯させます。millis()で点灯時間を管理します。センサーの値がHIGHの間は最後に動きを検知した時刻を更新し、最後にHIGHを読み取ってから少なくとも10秒後にLEDを消灯します。現在のコードは各ループでdelay(50)により50 ms待機するため、完全にノンブロッキングではありません。階段や廊下、浴室、戸棚内の照明などに応用できます。

ESP32 Wi-Fi STA接続(LCDディスプレイ付き)
このプロジェクトでは、ESP32をステーション(STA)モードで家庭のWi-Fiネットワークに接続し、安全で障害に強い接続を実現します。SSIDとパスワードを別のsecrets.hファイルに保存してGitへの流出を防ぎ、自動再接続の仕組みを実装します。RSSIによる信号強度とIPアドレスはI2C LCDディスプレイに表示します。内蔵LEDは状態表示に使います。実運用を想定した接続管理の仕組みです。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

