ESP32のMQTT入門:Mosquitto、Home Assistant、ESPHome
ESP32のMQTT入門ガイドです。Mosquittoのインストール、PubSubClientでのセンサーデータの送信とリレーの制御、Home AssistantのMQTT Discovery、ESPHomeのYAML例、QoSを解説します。
ESP32とMQTTの組み合わせは、家の中のセンサーやリレー同士、そしてHome Assistantのようなスマートホームシステムとをつなぐ、もっとも一般的な方法です。MQTTは小さなデバイス向けに設計された軽量なメッセージングプロトコルです。ESP32が測った温度を「ev/salon/sicaklik」(トルコ語で「家/リビング/温度」)のようなトピックに置くと、そのトピックを受信しているすべてのプログラムがすぐに値を受け取ります。同じように、スマホのHome Assistantアプリでボタンをタップすると、「ON」のコマンドが1秒もかからずにESP32に届き、リレーが切り替わります。HTTPとは違ってリクエストのたびに新しい接続を開くことはなく、開いたままの1本の接続の上で、わずか2バイトのヘッダーで動作します。そのため高速で、Wi-Fiにもプロセッサーにも負担がかかりません。
このガイドでは、動作するスマートホームのノードを一から作ります。まずMQTTのブローカー、トピック、パブリッシュ、サブスクライブという考え方を学び、次にHome AssistantまたはRaspberry PiにMosquittoブローカーをインストールします。続いてPubSubClientライブラリを使ってESP32からDHT22の温度と湿度を送信し、リレーのコマンドを受信し、デバイスがHome Assistantに自動で表示されるようにMQTT Discoveryを追加します。さらに、QoSとretain、接続が切れたときにデバイスを「利用不可」と表示させるラストウィル(LWT)メッセージ、C++を1行も書かずに同じことができるESPHomeという選択肢、ESP8266とRaspberry Pi Pico Wでの違い、よくあるエラーも1つずつ取り上げます。

MQTTとは?どのように動くのか
MQTT(Message Queuing Telemetry Transport)はパブリッシュ/サブスクライブ型のモデルを使います。中央にはブローカーと呼ばれるサーバーがあり、ESP32、Home Assistant、スマホ、パソコン上のプログラムがクライアントとしてここに接続します。クライアント同士はお互いのことも、お互いのIPアドレスも知りません。あるクライアントがトピックにメッセージをパブリッシュ(発行)すると、ブローカーがそのトピックをサブスクライブ(購読)しているすべてのクライアントに転送します。郵便局をイメージするとわかりやすいでしょう。ESP32が「ev/salon/sicaklik」宛ての箱に手紙を入れると、その箱を見張っているHome Assistantがすぐに受け取ります。接続は常にクライアント側から開くので、ESP32には固定IPもルーターのポート開放も必要ありません。知っておく必要があるのはブローカーのアドレスだけです。
| 用語 | 役割 | 例 |
|---|---|---|
| ブローカー(Broker) | メッセージを受け取り、購読者に配るサーバー | Mosquitto、Home AssistantのMosquittoブローカー |
| クライアント | ブローカーに接続するデバイスやプログラム。それぞれ固有のクライアントIDを持つ | ESP32やHome Assistant、MQTT Explorer |
| トピック(Topic) | メッセージの宛先。スラッシュで区切られた階層でできている | ev/salon/sicaklik(家/リビング/温度) |
| パブリッシュ(Publish) | トピックにメッセージを送ること | 値「23.4」をev/salon/sicaklikに送る |
| サブスクライブ(Subscribe) | トピックのメッセージを受け取るために登録すること | ESP32がev/salon/role/komutを受信する |
| ペイロード(Payload) | メッセージの中身。テキスト、数値、JSONなど | ON、23.4、{"sicaklik":23.4} |
| retain(保持) | ブローカーがトピックの最後のメッセージを保持し、新しい購読者にすぐ渡す | リレーの状態:ON |
| LWT(ラストウィル) | クライアントが予期せず切断したときに、ブローカーが代わりにパブリッシュするメッセージ | ev/salon/baglanti: offline(接続状態:オフライン) |
| QoS | メッセージの配信保証レベル(0、1、2) | リレーのコマンドにはQoS 1 |
なぜMQTT?Webサーバー、Blynk、Telegramとの比較
| 方法 | 長所 | 短所 | 向いている用途 |
|---|---|---|---|
| MQTT + Home Assistant | ローカルで高速、双方向。数百台のデバイスを1つのダッシュボードで扱え、オートメーションも組める | ブローカーを動かす必要がある | 家全体を1か所で管理する |
| ESP32のWebサーバー | 追加のサーバー不要、ブラウザで開ける | デバイスごとに別のページになり、デバイス間のオートメーションがない | 1台のデバイス用の操作パネル |
| Blynk IoT | 既製のスマホ用ダッシュボード、インターネット経由でアクセス | クラウドに依存し、無料プランには制限がある | 手早いモバイルの試作 |
| Telegramボット | 無料で即時通知、ポート転送不要 | グラフやダッシュボードがない | アラームと簡単なコマンド |
| ESP-NOW | ルーターもブローカーもなしでESPボード同士が通信 | ESPボード同士のみで、Home Assistantには直接届かない | 電池駆動のセンサーからゲートウェイへデータを運ぶ |
つまりMQTTは、デバイスの数が増えるほど力を発揮する方法です。新しいセンサーを追加するときも既存のコードには一切手を加えず、新しいトピックへのパブリッシュを始めるだけで済みます。1台のデバイスをブラウザから操作したいだけなら「ESP32の非同期WebサーバーとWebSocket」のガイドのほうが早く結果が出ます。スマホへの通知なら「ESP32のTelegramボット」、既製のモバイル用ダッシュボードなら「Blynk×ESP32入門」を参照してください。ルーターの電波が届かない庭のセンサーには、ESP-NOWでデータをMQTTにつながったゲートウェイ役のESP32に運ぶのがよい解決策です。
MQTTのトピック構成とワイルドカード
トピックはフォルダのパスに似ています。「ev/salon/sicaklik」(トルコ語で「家/リビング/温度」)は3つの階層でできていて、大文字と小文字が区別されるので、「Ev/Salon」と「ev/salon」は別のトピックです。先頭にスラッシュを付けないでください(「/ev/salon」は空の最初の階層を作ってしまいます)。スペースやアクセント付きの文字、日本語などの英語以外の文字も避け、半角英字、数字、ハイフン、アンダースコアだけを使えば十分です。サブスクライブするときは2つのワイルドカードが使えます。+はちょうど1つの階層の代わりになり(「ev/+/sicaklik」ですべての部屋の温度が届きます)、#はそれ以降のすべてを含み、末尾にしか書けません(「ev/#」で家の中のすべてのメッセージが届きます)。ワイルドカードが使えるのはサブスクライブのときだけで、メッセージは常に1つの正確なトピックにパブリッシュされます。$で始まるトピック($SYSなど)はブローカー自身の統計用に予約されていて、#のサブスクライブには含まれません。このガイドのプロジェクトでは、次の構成を使います。
| トピック | パブリッシュする側 | 受信する側 | retain |
|---|---|---|---|
| ev/salon/sicaklik(温度) | ESP32(30秒ごと) | Home Assistant | あり |
| ev/salon/nem(湿度) | ESP32(30秒ごと) | Home Assistant | あり |
| ev/salon/role/komut(リレーのコマンド) | Home Assistant、スマホ、オートメーション | ESP32 | なし |
| ev/salon/role/durum(リレーの状態) | ESP32(変化するたび) | Home Assistant | あり |
| ev/salon/baglanti(接続状態) | ESP32(online)、ブローカー(offline) | Home Assistant | あり |
| homeassistant/status(Home Assistantの状態) | Home Assistant(起動時にonline) | ESP32 | なし |
MQTTブローカーのインストール:Mosquitto
もっとも広く使われている無料のMQTTブローカーはEclipseのMosquittoです。Raspberry Piでも1秒に数百件のメッセージを余裕で処理できます。インストール方法は簡単なものが3つあります。Home Assistant OSを使っているなら公式のMosquitto brokerアプリ、別のRaspberry PiやLinuxパソコンならaptパッケージ、WindowsならMosquittoのインストーラーです。どれを選んでも、ブローカーは暗号化なしのMQTTに1883番、TLSで暗号化したMQTTに8883番のポートを使い、ESP32と同じ家庭のネットワーク上にあれば十分です。Home Assistantでの手順は次のとおりです。
- Home Assistantで設定 > アプリ(旧バージョンでは「アドオン」という名前)を開き、アプリをインストールをクリックして、一覧からMosquitto brokerを選びます。
- インストールをクリックし、完了したら開始をクリックします。起動時に開始するオプションは有効のままにしておきます。
- ESP32用に別のユーザーを作ります。設定 > 人 > ユーザーで、「esp32」という名前のユーザーを強力なパスワードで追加します。このタブが見当たらない場合は、プロフィールで詳細モードをオンにしてください。「homeassistant」と「addons」という名前はブローカー用に予約されているので使わないでください。
- 設定 > デバイスとサービスのページには、たいていMQTTの統合が「検出」として表示されます。構成をクリックして確定します。表示されない場合は統合を追加でMQTTを検索し、Mosquittoを自動で構成するオプションを選びます。
- ESP32のコードには、ブローカーのアドレスとしてHome AssistantのIPアドレス(例:192.168.1.10)、ポートとして1883、そして今作ったユーザーを入力します。
Raspberry PiやUbuntuのようなLinuxマシンなら、いくつかのコマンドで準備できます。sudo apt install mosquitto mosquitto-clientsでブローカーとテスト用ツールをインストールします。sudo mosquitto_passwd -c /etc/mosquitto/passwd esp32で「esp32」ユーザーを作り、パスワードを2回入力します。-cオプションはファイルを新しく作り直すので、2人目のユーザーを追加するときは付けないでください。付けると既存のユーザーが消えてしまいます。次に/etc/mosquitto/conf.d/フォルダにlocal.confというファイルを作り、下の3行を書き込んでから、sudo systemctl restart mosquittoでサービスを再起動します。Windowsでは、Mosquittoのインストールフォルダにあるmosquitto.confファイルに同じ行を追加し、Mosquitto Brokerサービスを再起動します。
| 行 | 意味 |
|---|---|
| listener 1883 | すべてのネットワークインターフェースで1883番ポートを待ち受けます。この行がないと、Mosquitto 2.xは同じパソコンからの接続しか受け付けません |
| allow_anonymous false | ユーザー名とパスワードのない接続を拒否します。2.xではもともと初期設定ですが、明示的に書いておくのはよい習慣です |
| password_file /etc/mosquitto/passwd(パスワードファイルの指定) | mosquitto_passwdで作ったパスワードファイルを使います。パスワードはハッシュとして保存されます |
ESP32に進む前に、パソコンからブローカーをテストしましょう。ブローカーの問題とコードの問題を最初に切り分けておくと、時間を大きく節約できます。1つ目のターミナルでmosquitto_sub -h 192.168.1.10 -u esp32 -P yourpassword -t "ev/#" -vを実行し、家の中のすべてのトピックの受信を始めます。2つ目のターミナルでmosquitto_pub -h 192.168.1.10 -u esp32 -P yourpassword -t ev/salon/test -m helloと入力します。1つ目のターミナルに「ev/salon/test hello」という行が表示されれば、ブローカーは動作していて、パスワードも正しいということです。-vオプションは各メッセージの前にトピックを表示し、-rオプションはメッセージをretain付きで送ります。
部品リストと配線図
このプロジェクトに必要なのは、ESP32開発ボード、DHT22温湿度センサー、3.3 Vで動作させられるリレーモジュール、ブレッドボード、ジャンパーワイヤーです。DHT22はDHT11より精度が高く、温度を±0.5 °C、湿度をおよそ±2–5%の精度で測定でき、-40〜80 °Cで動作します。センサーのファミリーについては「DHT11温湿度センサー」で詳しく解説しています。DHT11を持っている場合は、コードのDHT22をDHT11に変えるだけです。最初の接続テストにはESP32とUSBケーブルだけで十分で、センサーとリレーはあとから追加できます。



| 接続 | ESP32 | ESP8266(NodeMCU) | Raspberry Pi Pico W |
|---|---|---|---|
| DHT22 VCC | 3V3 | 3V3 | 3V3(OUT) |
| DHT22 DATA | GPIO 4 | D2 (GPIO 4) | GP15 |
| DHT22 GND | GND | GND | GND |
| リレーのVCC | VIN (5V) | VIN (5V) | VBUS (5V) |
| リレーのIN | GPIO 26 | D1 (GPIO 5) | GP14 |
| リレーのGND | GND | GND | GND |
Arduino IDEにPubSubClientをインストールする
- Arduino IDEにESP32のボードパッケージがまだ入っていない場合は、先にインストールします。手順は「ESP32とは?」にあります。
- 「ツール」>「ライブラリを管理」を開き、検索欄にPubSubClientと入力して、作者がNick O'Learyのライブラリをインストールします。このガイドのコードはバージョン2.8でコンパイルしました。
- 同じウィンドウでDHT sensor libraryを検索し、Adafruitのライブラリをインストールします。依存ライブラリについて聞かれたら「全てをインストール」を選び、Adafruit Unified Sensorも一緒にインストールします。
- ライブラリのインストールでつまずいたら、「Arduinoにライブラリを追加する方法」のガイドを参照してください。
最初のテスト:ESP32をMQTTブローカーにつなぐ
センサーを追加する前に、Wi-Fiの情報、ブローカーのアドレス、パスワードが正しいことを、いちばん簡単なコードで確認しましょう。下のコードはブローカーに接続し、「test/esp32/durum」トピックに「merhaba」(トルコ語で「こんにちは」)をパブリッシュして、「test/esp32/komut」に届いたメッセージをすべてシリアルモニタに表示します。Wi-Fiの名前、パスワード、ブローカーのIP、MQTTユーザーを自分のものに置き換えてコードを書き込み、シリアルモニタを115200ボーで開いてください。
1#include <WiFi.h>
2#include <PubSubClient.h>
3
4const char* WIFI_SSID = "AgAdi"; // Wi-Fi adın (yalnız 2,4 GHz) / your Wi-Fi name (2.4 GHz only)
5const char* WIFI_PASS = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
6const char* MQTT_SUNUCU = "192.168.1.10"; // broker'ın IP adresi / broker IP address
7const int MQTT_PORT = 1883; // şifresiz MQTT portu / unencrypted MQTT port
8const char* MQTT_KULLANICI = "esp32"; // broker kullanıcı adı / broker username
9const char* MQTT_SIFRE = "mqttSifre"; // broker şifresi / broker password
10
11WiFiClient wifiIstemci; // TCP bağlantısı / TCP connection
12PubSubClient mqtt(wifiIstemci); // MQTT istemcisi / MQTT client
13
14// Abone olunan konuya mesaj gelince çalışır / runs when a subscribed topic receives a message
15void mesajGeldi(char* topic, byte* payload, unsigned int length) {
16 Serial.print("Mesaj [");
17 Serial.print(topic);
18 Serial.print("] ");
19 for (unsigned int i = 0; i < length; i++) {
20 Serial.print((char)payload[i]); // payload sonunda \0 yoktur / payload is not null-terminated
21 }
22 Serial.println();
23}
24
25void setup() {
26 Serial.begin(115200);
27 WiFi.mode(WIFI_STA);
28 WiFi.begin(WIFI_SSID, WIFI_PASS);
29 while (WiFi.status() != WL_CONNECTED) {
30 delay(500);
31 Serial.print(".");
32 }
33 Serial.print("\nIP: ");
34 Serial.println(WiFi.localIP());
35
36 mqtt.setServer(MQTT_SUNUCU, MQTT_PORT); // broker adresi / broker address
37 mqtt.setCallback(mesajGeldi); // gelen mesaj fonksiyonu / incoming message handler
38}
39
40void loop() {
41 if (!mqtt.connected()) {
42 Serial.print("MQTT baglaniyor... ");
43 if (mqtt.connect("esp32-test", MQTT_KULLANICI, MQTT_SIFRE)) { // istemci kimliği benzersiz olmalı / client ID must be unique
44 Serial.println("baglandi");
45 mqtt.subscribe("test/esp32/komut"); // bu konuyu dinle / listen to this topic
46 mqtt.publish("test/esp32/durum", "merhaba"); // ilk mesaj / first message
47 } else {
48 Serial.print("hata, rc=");
49 Serial.println(mqtt.state()); // hata kodu / error code
50 delay(5000);
51 return;
52 }
53 }
54 mqtt.loop(); // mesajları işle, bağlantıyı canlı tut / process messages, keep the connection alive
55}シリアルモニタに「baglandi」(トルコ語で「接続した」)と表示されたら、パソコンのターミナルからmosquitto_pub -h 192.168.1.10 -u esp32 -P yourpassword -t test/esp32/komut -m hiを送ります。数ミリ秒のうちに、シリアルモニタに「Mesaj [test/esp32/komut] hi」という行が現れます。接続に失敗すると、コードはmqtt.state()でエラーコードを表示し、5秒後に再試行します。これらのコードを見れば、問題がどこにあるかがすぐにわかります。
| rc | PubSubClientでの名前 | 意味と対処法 |
|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT | ブローカーが時間内に応答しなかった。ネットワークが弱いか、ブローカーの負荷が高い |
| -3 | MQTT_CONNECTION_LOST | 確立した接続が切れた。たいていはloop()が長時間呼ばれなかったか、Wi-Fiが切れたのが原因 |
| -2 | MQTT_CONNECT_FAILED | TCP接続がそもそも開けなかった。IPやポートの誤り、ブローカーが停止中、Mosquittoがローカル専用モード、またはファイアウォールが1883をブロックしている |
| -1 | MQTT_DISCONNECTED | 接続が正常に閉じられた(disconnectが呼ばれた)か、まだ一度も接続していない |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID | クライアントIDが拒否された。空または無効なクライアントIDを使っている |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS | ユーザー名またはパスワードの誤り(このコードを返すブローカーもある) |
| 5 | MQTT_CONNECT_UNAUTHORIZED | 権限がない。Mosquittoはパスワードが間違っているときにも、認証情報なしで接続しようとしたときにもこのコードを返す |
ESP32でセンサーデータを送信し、リレーのコマンドを受信する
いよいよ本番のプロジェクトを作ります。下のコードは30秒ごとにDHT22を読み取り、温度と湿度をretainフラグ付きでパブリッシュします。「ev/salon/role/komut」に届くON、OFF、TOGGLEのコマンドでリレーを制御し、リレーの新しい状態を「ev/salon/role/durum」に報告します。接続時にはブローカーにラストウィルを登録し、MQTT DiscoveryでHome Assistantに自分を知らせます。接続が切れた場合は、loop()を止めずに5秒ごとに再接続を試みます。先頭の#if defined(ESP8266)ブロックのおかげで、同じコードがNodeMCUでも変更なしでコンパイルできます。
1#if defined(ESP8266)
2#include <ESP8266WiFi.h>
3#else
4#include <WiFi.h>
5#endif
6#include <PubSubClient.h>
7#include <DHT.h>
8
9// ---- Ayarlar / Settings ----
10const char* WIFI_SSID = "AgAdi";
11const char* WIFI_PASS = "Sifre123";
12const char* MQTT_SUNUCU = "192.168.1.10"; // broker IP (Home Assistant ya da Raspberry Pi) / broker IP (Home Assistant or Raspberry Pi)
13const int MQTT_PORT = 1883;
14const char* MQTT_KULLANICI = "esp32";
15const char* MQTT_SIFRE = "mqttSifre";
16const char* ISTEMCI_ID = "esp32-salon"; // her cihazda farklı olmalı / must be different on every device
17
18#if defined(ESP8266)
19const int DHT_PIN = 4; // NodeMCU D2
20const int ROLE_PIN = 5; // NodeMCU D1
21#else
22const int DHT_PIN = 4; // ESP32 GPIO 4
23const int ROLE_PIN = 26; // ESP32 GPIO 26
24#endif
25const bool ROLE_AKTIF_LOW = true; // çoğu röle modülü LOW ile çeker / most relay modules switch on LOW
26const unsigned long OLCUM_ARALIGI = 30000; // 30 saniyede bir ölçüm / one reading every 30 s
27
28// ---- Konular (topic) / Topics ----
29const char* T_SICAKLIK = "ev/salon/sicaklik";
30const char* T_NEM = "ev/salon/nem";
31const char* T_KOMUT = "ev/salon/role/komut"; // ON, OFF ya da TOGGLE / ON, OFF or TOGGLE
32const char* T_DURUM = "ev/salon/role/durum";
33const char* T_BAGLANTI = "ev/salon/baglanti"; // online / offline (son vasiyet / last will)
34const char* T_HA_DURUM = "homeassistant/status"; // HA açılınca "online" yayınlar / HA publishes "online" when it starts
35
36WiFiClient wifiIstemci;
37PubSubClient mqtt(wifiIstemci);
38DHT dht(DHT_PIN, DHT22);
39
40bool roleAcik = false;
41unsigned long sonOlcum = 0;
42unsigned long sonDeneme = 0;
43
44// Röleyi sür ve yeni durumu kalıcı (retain) yayınla / drive the relay and publish the new state as retained
45void roleAyarla(bool acik) {
46 roleAcik = acik;
47 bool seviye = ROLE_AKTIF_LOW ? !acik : acik;
48 digitalWrite(ROLE_PIN, seviye ? HIGH : LOW);
49 mqtt.publish(T_DURUM, acik ? "ON" : "OFF", true);
50 Serial.println(acik ? "Role: ON" : "Role: OFF");
51}
52
53// Tek bir Home Assistant keşif mesajı gönder / send one Home Assistant discovery message
54void kesifYayinla(const char* tur, const char* nesne, const char* ad, const char* ekAlanlar) {
55 char konu[96];
56 snprintf(konu, sizeof(konu), "homeassistant/%s/%s/%s/config", tur, ISTEMCI_ID, nesne);
57 char json[600];
58 snprintf(json, sizeof(json),
59 "{\"name\":\"%s\",\"unique_id\":\"%s_%s\",%s,\"availability_topic\":\"%s\","
60 "\"device\":{\"identifiers\":[\"%s\"],\"name\":\"Salon ESP32\",\"model\":\"ESP32 + DHT22\"}}",
61 ad, ISTEMCI_ID, nesne, ekAlanlar, T_BAGLANTI, ISTEMCI_ID);
62 if (!mqtt.publish(konu, json, true)) { // false: tampon küçük ya da bağlantı yok / false: buffer too small or not connected
63 Serial.println("Kesif mesaji gonderilemedi");
64 }
65}
66
67// Sıcaklık, nem ve röleyi Home Assistant'a tanıt / announce temperature, humidity and relay to Home Assistant
68void haKesifGonder() {
69 char ek[200];
70 snprintf(ek, sizeof(ek), "\"state_topic\":\"%s\",\"device_class\":\"temperature\",\"unit_of_measurement\":\"°C\",\"state_class\":\"measurement\"", T_SICAKLIK);
71 kesifYayinla("sensor", "sicaklik", "Sıcaklık", ek);
72 snprintf(ek, sizeof(ek), "\"state_topic\":\"%s\",\"device_class\":\"humidity\",\"unit_of_measurement\":\"%%\",\"state_class\":\"measurement\"", T_NEM);
73 kesifYayinla("sensor", "nem", "Nem", ek);
74 snprintf(ek, sizeof(ek), "\"state_topic\":\"%s\",\"command_topic\":\"%s\"", T_DURUM, T_KOMUT);
75 kesifYayinla("switch", "role", "Röle", ek);
76}
77
78// DHT22'yi oku ve değerleri yayınla / read the DHT22 and publish the values
79void olcumGonder() {
80 float t = dht.readTemperature();
81 float h = dht.readHumidity();
82 if (isnan(t) || isnan(h)) {
83 Serial.println("DHT22 okunamadi");
84 return;
85 }
86 char deger[12];
87 snprintf(deger, sizeof(deger), "%.1f", t); // 23.4 gibi / like 23.4
88 mqtt.publish(T_SICAKLIK, deger, true);
89 snprintf(deger, sizeof(deger), "%.0f", h); // 45 gibi / like 45
90 mqtt.publish(T_NEM, deger, true);
91 Serial.printf("Sicaklik %.1f C, nem %.0f %%\n", t, h);
92}
93
94// Abone olunan konulara mesaj gelince çalışır / runs when a subscribed topic receives a message
95void mesajGeldi(char* topic, byte* payload, unsigned int length) {
96 // Önce kopyala: yayın yapınca kütüphane aynı tamponu kullanır / copy first: publishing reuses the same buffer
97 String konu = topic;
98 String mesaj;
99 for (unsigned int i = 0; i < length; i++) mesaj += (char)payload[i];
100 mesaj.trim();
101
102 if (konu == T_KOMUT) {
103 if (mesaj == "ON") roleAyarla(true);
104 else if (mesaj == "OFF") roleAyarla(false);
105 else if (mesaj == "TOGGLE") roleAyarla(!roleAcik);
106 } else if (konu == T_HA_DURUM && mesaj == "online") {
107 haKesifGonder(); // HA yeniden başladı, tanıtımı yenile / HA restarted, announce again
108 mqtt.publish(T_DURUM, roleAcik ? "ON" : "OFF", true);
109 }
110}
111
112bool mqttBaglan() {
113 Serial.print("MQTT baglaniyor... ");
114 // Bağlantı koparsa broker T_BAGLANTI'ya "offline" yazar / if we drop, the broker publishes "offline" to T_BAGLANTI
115 if (!mqtt.connect(ISTEMCI_ID, MQTT_KULLANICI, MQTT_SIFRE, T_BAGLANTI, 1, true, "offline")) {
116 Serial.print("hata, rc=");
117 Serial.println(mqtt.state());
118 return false;
119 }
120 Serial.println("baglandi");
121 mqtt.publish(T_BAGLANTI, "online", true);
122 mqtt.subscribe(T_KOMUT, 1); // komutları QoS 1 ile dinle / listen to commands with QoS 1
123 mqtt.subscribe(T_HA_DURUM);
124 haKesifGonder();
125 mqtt.publish(T_DURUM, roleAcik ? "ON" : "OFF", true);
126 return true;
127}
128
129void setup() {
130 Serial.begin(115200);
131 pinMode(ROLE_PIN, OUTPUT);
132 roleAyarla(false); // güvenli başlangıç: röle kapalı / safe start: relay off
133 dht.begin();
134
135 WiFi.mode(WIFI_STA);
136 WiFi.begin(WIFI_SSID, WIFI_PASS);
137 Serial.print("Wi-Fi");
138 while (WiFi.status() != WL_CONNECTED) {
139 delay(500);
140 Serial.print(".");
141 }
142 Serial.print(" IP: ");
143 Serial.println(WiFi.localIP());
144
145 mqtt.setServer(MQTT_SUNUCU, MQTT_PORT);
146 mqtt.setCallback(mesajGeldi);
147 mqtt.setBufferSize(1024); // keşif JSON'u varsayılan 256 bayta sığmaz / discovery JSON doesn't fit the default 256 bytes
148}
149
150void loop() {
151 if (!mqtt.connected()) {
152 unsigned long simdi = millis();
153 if (sonDeneme == 0 || simdi - sonDeneme >= 5000) { // 5 sn'de bir dene, loop'u kilitleme / retry every 5 s without blocking
154 sonDeneme = simdi;
155 if (mqttBaglan()) sonOlcum = simdi - OLCUM_ARALIGI; // bağlanınca hemen ölç / measure right after connecting
156 }
157 return;
158 }
159 mqtt.loop(); // gelen komutlar ve keep-alive / incoming commands and keep-alive
160
161 if (millis() - sonOlcum >= OLCUM_ARALIGI) {
162 sonOlcum = millis();
163 olcumGonder();
164 }
165}| 行 | 役割 |
|---|---|
| mqtt.connect(ISTEMCI_ID, ..., T_BAGLANTI, 1, true, "offline")(ラストウィル付きで接続) | 接続と同時にラストウィルを登録します。ESP32が予期せず切断すると、ブローカーがev/salon/baglantiにretain付きで「offline」を書き込みます |
| mqtt.publish(T_BAGLANTI, "online", true)(オンラインを通知) | 接続直後に状態を「online」にします。Home Assistantはデバイスを再び利用可能と表示します |
| mqtt.subscribe(T_KOMUT, 1)(コマンドを購読) | リレーのコマンドをQoS 1で受信します。PubSubClientがサブスクライブで対応しているのはQoS 0と1だけです |
| mqtt.setBufferSize(1024)(バッファの拡大) | パケットのバッファを初期値の256バイトから大きくします。いちばん大きいDiscoveryのパケットは約355バイトです |
| mqtt.loop()(受信とキープアライブ) | 届いたメッセージを読み、キープアライブ(PINGREQ)のパケットを送ります。loopの周回ごとに必ず呼び出す必要があります |
| ROLE_AKTIF_LOW | 多くのリレーモジュールはLOWの信号で動作します。HIGHで動作するモジュールならfalseにします |
| OLCUM_ARALIGI(測定間隔) | 測定の頻度です。DHT22は最短2秒ごとにしか読み取れず、室温なら30–60秒が理想的です |
コードのいくつかの細部は意図的にそうしています。mesajGeldi()(トルコ語で「メッセージが届いた」)関数は、最初にトピックとペイロードをStringにコピーします。PubSubClientは受信と送信のメッセージに1つのバッファを共用しているため、コールバックの中でpublish()を呼んだ瞬間に、topicとpayloadのポインタが指すデータが上書きされてしまうからです。また、ペイロードの末尾にはC言語の文字列のような\0がないので、ちょうどlengthの文字数だけ読み取ります。測定値とリレーの状態にretainを使うのは、Home Assistantが再起動したときに30秒待たずに最後の値を表示できるようにするためです。タイミングにもmillis()を使っています。loop()の中に長いdelay()があるとキープアライブのパケットが遅れ、ブローカーが接続を切ってしまいます。この手法は「millis()とdelay()の違い」で詳しく解説しています。
Home Assistantでの表示:MQTT Discovery
Home Assistantは、homeassistant/で始まるトピックを常に受信しています。デバイスがhomeassistant/[component]/[node]/[object]/configという形のトピックにJSONの設定をパブリッシュすると、Home Assistantがそのエンティティを自動的に作成します。これをMQTT Discoveryと呼びます。今回のコードは3つのメッセージを送ります。センサー(温度と湿度)用の2つはhomeassistant/sensor/esp32-salon/...に、リレー用の1つはhomeassistant/switch/esp32-salon/role/configに送ります。retain付きなので、Home Assistantを再起動しても消えません。さらにコードはhomeassistant/statusもサブスクライブしています。Home Assistantは起動時にここへ「online」と書き込み、それを受けてESP32が改めて自分を知らせます。デバイスが多い場合は、この再通知の前に少しランダムな遅延を入れることがHome Assistantのドキュメントで推奨されています。
| JSONのフィールド | 意味 | 今回の値 |
|---|---|---|
| name | エンティティの表示名。前にデバイス名が付く | トルコ語で「温度」を意味する名前(前に「Salon ESP32」が付いて表示される) |
| unique_id | エンティティの変わらないID。ないと画面からエンティティの名前やエリアを編集できない | esp32-salon_sicaklik |
| state_topic | 値を読み取るトピック | ev/salon/sicaklik(温度) |
| command_topic | スイッチのコマンドを送るトピック | ev/salon/role/komut(リレーのコマンド) |
| device_class | アイコン、単位の選択肢、グラフの種類を決める | temperature、humidity(温度、湿度) |
| unit_of_measurement | 測定の単位 | °Cと% |
| state_class | measurementを指定すると、長期の統計とグラフが記録される | measurement |
| availability_topic | online/offlineの状態を読み取るトピック。初期設定のペイロードはonlineとoffline | ev/salon/baglanti(接続状態) |
| device | エンティティを1枚のデバイスカードにまとめる | identifiers、name、model(識別子、名前、型番) |
コードを書き込んで数秒たつと、設定 > デバイスとサービス > MQTTに「Salon ESP32」というデバイスが現れます。デバイスカードには温度と湿度のセンサー、そしてリレーのスイッチがあります。温度をクリックすると履歴のグラフが開き、スイッチを切り替えるとリレーがカチッと動きます。これらのエンティティはダッシュボードに追加したり、音声アシスタントに公開したり、オートメーションで使ったりできます。たとえば「リビングが27 °Cを超えたら扇風機をつないだリレーをオンにし、25 °Cを下回ったらオフにする」というルールも、数回のクリックで作れます。ESP32のプラグを抜くと、遅くとも約22秒後にエンティティが「利用不可」に切り替わり、ラストウィルのメッセージが機能していることがわかります。Discoveryを使いたくない場合は、configuration.yamlのmqtt:の下に、sensorとswitchとして同じエンティティを手動で定義することもできます。
QoSとretain:どれをいつ使う?
QoS(Quality of Service)は、メッセージが相手に届くことをどこまで確実にしたいかを決めるものです。大事な点として、QoSは2つの区間で別々に適用されます。パブリッシャーとブローカーの間ではパブリッシャーのQoS、ブローカーと購読者の間ではサブスクライブのQoSが使われ、購読者が受け取る保証はその2つのうち低いほうになります。PubSubClientは、パブリッシュではQoS 0のみ、サブスクライブではQoS 0と1に対応しています。家庭のWi-FiでQoS 0のメッセージが失われることはごくまれですが、それでもコマンドのサブスクライブをQoS 1にしておくのは手軽な保険になります。
| QoS | 保証 | しくみ | 使いどころ |
|---|---|---|---|
| 0 | 最大1回 | 確認なしで1回だけ送る。その瞬間に接続が切れると失われることがある | 頻繁なセンサーの測定値 |
| 1 | 最低1回 | 受信側がPUBACKで確認し、確認が届かなければ再送する。2回届くことがある | リレーのコマンド、アラーム |
| 2 | ちょうど1回 | PUBREC、PUBREL、PUBCOMPによる4段階のハンドシェイク。もっとも遅いレベル | 重複すると困るカウンターなど。PubSubClientは非対応 |
retainフラグは配信ではなく記憶に関するものです。ブローカーは各トピックの最後のretainメッセージだけを保持し、新しくサブスクライブしたクライアントにすぐ送ります。センサーの値、リレーの状態、オンライン状態のような「現在の状態」のメッセージには最適ですが、コマンドや1回限りのイベントには危険です。ラストウィル(LWT)はこれを補うものです。ブローカーは、クライアントからキープアライブの時間の1.5倍のあいだパケットが届かないと接続が切れたと判断し、ラストウィルのメッセージをパブリッシュします。PubSubClientのキープアライブの初期値は15秒なので、プラグを抜いたESP32は遅くとも約22秒後に「offline」と表示されます。この時間はmqtt.setKeepAlive(30)のような1行で変えられますが、長くするほど切断に気づくのが遅くなります。
コードを書かない選択肢:ESPHome
ESPHomeは、YAMLファイルで記述したデバイスを、そのまま書き込めるESP32用のファームウェアに変換してくれる無料のツールです。C++を1行も書く必要はありません。Home Assistantで設定 > アプリからESPHome Device Builderをインストールし、画面を開いてNew device(新しいデバイス)のウィザードで設定を作ります。最初の書き込みだけはUSBで1回行います。ChromeかEdgeならボードに直接書き込めます。うまくいかない場合は、ウィザードがダウンロードしたファイルをESPHome Webのページで書き込めます。以降の更新はすべてWi-Fi経由の無線(OTA)で行われます。下の設定は、上のArduinoのプロジェクトと同じ内容です。GPIO 4のDHT22を30秒ごとに読み取り、GPIO 26のアクティブLOWのリレーをスイッチとして公開します。Wi-Fiの情報とパスワードは、!secretで別のsecrets.yamlファイルから読み込みます。
1esphome:
2 name: salon-sensor # küçük harf, rakam ve tire / lowercase, digits and hyphens
3 friendly_name: Salon Sensör
4
5esp32:
6 board: esp32dev
7
8logger:
9
10api: # Home Assistant ile doğrudan bağlantı / direct link to Home Assistant
11 encryption:
12 key: !secret api_anahtari
13
14ota:
15 - platform: esphome
16 password: !secret ota_sifresi
17
18wifi:
19 ssid: !secret wifi_ssid
20 password: !secret wifi_sifresi
21 ap: # Wi-Fi yoksa kendi ağını açar / opens its own network if Wi-Fi fails
22 ssid: "Salon-Sensor"
23 password: "yedek1234"
24
25captive_portal:
26
27sensor:
28 - platform: dht
29 pin: GPIO4
30 model: DHT22
31 update_interval: 30s
32 temperature:
33 name: "Sıcaklık"
34 humidity:
35 name: "Nem"
36
37switch:
38 - platform: gpio
39 name: "Röle"
40 pin:
41 number: GPIO26
42 inverted: true # aktif LOW röle / active-LOW relay
43 restore_mode: ALWAYS_OFF # açılışta kapalı / off at boot
44
45# MQTT ile kullanmak için api: bloğunu sil, bunu aç / to use MQTT, delete api: and uncomment this
46# mqtt:
47# broker: 192.168.1.10
48# username: esp32
49# password: !secret mqtt_sifresi1つ注意すべき違いがあります。ESPHomeは初期設定ではMQTTではなく、独自の暗号化されたネイティブAPIを使います。Home Assistantはネットワーク上のデバイスを自動で見つけ、ブローカーはまったく必要ありません。Node-REDなどほかのシステムでも同じデータを読みたい場合や、Home Assistantを使わない場合にだけMQTTを選んでください。そのためには、ファイル末尾のmqtt:の行のコメントを外し、api:ブロックを削除します。削除しないと、15分間ネイティブAPIに誰も接続しなかったときにデバイスが自分で再起動してしまいます(この時間はreboot_timeoutの設定です)。MQTTモードでは、ESPHomeがHome AssistantのDiscoveryのメッセージと、salon-sensor/statusのonline/offlineのメッセージを自分で送ります。つまり、Arduinoのコードで手作業で行ったことをすべて自動でやってくれます。
| 項目 | PubSubClient(Arduinoのコード) | ESPHome(YAML) |
|---|---|---|
| コードを書く | すべてC++で自分で書く | YAMLだけ。ファームウェアは自動でコンパイルされる |
| Home Assistantへの追加 | DiscoveryのJSONを自分で送る | 自動で検出される |
| 柔軟性 | 制限なし。どんなライブラリやロジックも使える | 既製のコンポーネントに限られるが、数百種類のセンサーに対応。ラムダでC++を追加できる |
| 無線アップデート | ArduinoOTAを自分でコードに追加する | 標準で搭載 |
| ブローカーの必要性 | 必要 | ネイティブAPIなら不要 |
| 学習効果 | MQTTのプロトコルとプログラミングが身につく | すぐに結果が出るが、内部で起きていることは見えない |
ESP8266とRaspberry Pi Pico WでのMQTT
上の完全なコードは、ESP8266 NodeMCUやWemos D1 Miniのボードでもそのまま動きます。「ツール」>「ボード」でNodeMCU 1.0を選ぶと、#if defined(ESP8266)のブロックがWiFi.hの代わりにESP8266WiFi.hを、ピンもD2とD1を自動で選びます。Raspberry Pi Pico Wのほうでは、MicroPythonのumqtt.simpleモジュールを使います。このモジュールはPico WのMicroPythonファームウェアには含まれていません。Thonnyの「ツール」>「パッケージを管理」でumqtt.simpleを検索してインストールするか、Wi-Fiにつないだ状態でREPLにimport mip; mip.install("umqtt.simple")と入力してください。一方、DHT22に必要なdhtモジュールはファームウェアに付属しています。下のコードで、キッチンに置いたPico Wが同じ測定とリレーの仕事をこなします。短くするためにDiscoveryのメッセージは省きました。Pico WをHome Assistantに表示するには、ESP32のコードと同じDiscoveryのJSONをev/mutfak(トルコ語で「家/キッチン」)のトピックでパブリッシュするか、configuration.yamlのmqtt:の下でエンティティを手動で定義してください。ファイルをmain.pyという名前で保存すると、ボードの電源を入れるたびに実行されます。
1import network, time, dht
2from machine import Pin
3from umqtt.simple import MQTTClient # önce mip ile kur / install with mip first
4
5WIFI_SSID = "AgAdi" # Wi-Fi adın / your Wi-Fi name
6WIFI_PASS = "Sifre123" # Wi-Fi şifren / your Wi-Fi password
7MQTT_SUNUCU = "192.168.1.10" # broker IP / broker IP
8ISTEMCI_ID = "pico-mutfak" # benzersiz olmalı / must be unique
9
10T_SICAKLIK = b"ev/mutfak/sicaklik"
11T_NEM = b"ev/mutfak/nem"
12T_KOMUT = b"ev/mutfak/role/komut"
13T_DURUM = b"ev/mutfak/role/durum"
14T_BAGLANTI = b"ev/mutfak/baglanti"
15
16sensor = dht.DHT22(Pin(15)) # DHT22 DATA -> GP15
17role = Pin(14, Pin.OUT, value=1) # aktif LOW röle kapalı başlar / active-LOW relay starts off
18led = Pin("LED", Pin.OUT) # Pico W dahili LED / Pico W onboard LED
19
20wlan = network.WLAN(network.STA_IF)
21wlan.active(True)
22wlan.connect(WIFI_SSID, WIFI_PASS)
23while not wlan.isconnected():
24 time.sleep(0.5)
25print("IP:", wlan.ifconfig()[0])
26
27def mesaj_geldi(konu, mesaj): # ikisi de bytes gelir / both arrive as bytes
28 if konu == T_KOMUT and mesaj in (b"ON", b"OFF"):
29 acik = mesaj == b"ON"
30 role.value(0 if acik else 1)
31 led.value(1 if acik else 0)
32 istemci.publish(T_DURUM, mesaj, retain=True)
33
34istemci = MQTTClient(ISTEMCI_ID, MQTT_SUNUCU, user="esp32", password="mqttSifre", keepalive=60) # Mosquitto kullanıcısı / Mosquitto user
35istemci.set_callback(mesaj_geldi)
36istemci.set_last_will(T_BAGLANTI, b"offline", retain=True) # son vasiyet / last will
37
38def baglan():
39 while True:
40 try:
41 if not wlan.isconnected(): # Wi-Fi koptuysa yeniden bağlan / reconnect Wi-Fi if it dropped
42 wlan.connect(WIFI_SSID, WIFI_PASS)
43 time.sleep(5)
44 istemci.connect()
45 istemci.publish(T_BAGLANTI, b"online", retain=True)
46 istemci.publish(T_DURUM, b"OFF" if role.value() else b"ON", retain=True)
47 istemci.subscribe(T_KOMUT)
48 print("MQTT baglandi")
49 return
50 except Exception as e: # MQTTException(5): yetki yok / not authorized
51 print("MQTT hatasi:", e)
52 time.sleep(5)
53
54baglan()
55son = time.ticks_add(time.ticks_ms(), -30000) # ilk ölçüm hemen / first reading right away
56while True:
57 try:
58 istemci.check_msg() # bekleyen komut var mı? / any pending command?
59 if time.ticks_diff(time.ticks_ms(), son) >= 30000:
60 son = time.ticks_ms()
61 try:
62 sensor.measure()
63 except OSError:
64 print("DHT22 okunamadi")
65 istemci.ping() # veri yoksa da bağlantıyı canlı tut / keep the link alive without data
66 else:
67 istemci.publish(T_SICAKLIK, "{:.1f}".format(sensor.temperature()).encode(), retain=True)
68 istemci.publish(T_NEM, "{:.0f}".format(sensor.humidity()).encode(), retain=True)
69 except OSError:
70 print("Baglanti koptu, yeniden baglaniliyor")
71 time.sleep(5)
72 baglan()
73 time.sleep_ms(100)ESP32とMQTTのプロジェクトアイデア
- 部屋ごとの温度マップ:各部屋にDHT22かBME280付きのESP32を置き、ev/+/sicaklikのサブスクライブで家全体を1つのグラフで確認します。
- スマートランプとコンセント:リレーをHome Assistantのオートメーションにつなぎ、日没でオン、夜12時でオフにします。
- ドアと窓のセンサー:リードスイッチを使ってドアが開いたらMQTTメッセージを送り、留守中ならスマホに通知を受け取ります。
- 人が通ると点灯する廊下の照明:PIRセンサーに動きをパブリッシュさせ、Home Assistantのオートメーションで照明をつけます。
- 植物の水やりステーション:土壌水分センサーの値をパブリッシュし、土が乾いたらMQTTのコマンドでポンプを動かします。
- 電力の見える化:ACS712電流センサーで家電の消費電流を測り、1日の消費量をグラフにします。
- 電池で動くセンサー:測定してMQTTで送信し、またスリープに戻るノードを作ります。詳しくは「ESP32のディープスリープ」にあります。
ESP32とMQTTのよくあるエラーと対処法
| 症状またはメッセージ | 原因 | 対処法 |
|---|---|---|
| rc=-2が続く | ブローカーのIPやポートの誤り、ブローカーが停止中、またはMosquittoがローカル専用モード | IPを確認し、設定にlistener 1883を追加し、ファイアウォールで1883番ポートを許可する |
| rc=5 | ユーザー名かパスワードの誤り、または認証情報なしで接続しようとしている | パソコンからmosquitto_subでパスワードを試す。必要ならユーザーを作り直す |
| 接続しても数秒で切れ、それを繰り返す | 2台のデバイスが同じクライアントIDを使っている。Mosquittoのログに「Client esp32-salon already connected, closing old connection」(すでに接続中のため古い接続を閉じる)と出る | デバイスごとに固有のISTEMCI_IDを付ける |
| Discoveryのメッセージが届かず、publish()がfalseを返す | PubSubClientのバッファが初期値で256バイトしかなく、JSONが収まらない | setup()内でmqtt.setBufferSize(1024)を呼び出す |
| rc=-3が頻繁に出る | loop()内の長いdelay()でキープアライブのパケットが止まっている、またはWi-Fiの電波が弱い | タイミングはmillis()で処理し、ボードをルーターに近づける |
| コマンドがESP32に届かない | トピック名の大文字・小文字やつづりが違う、または再接続後にsubscribe()をやり直していない | mosquitto_subでトピックを確認し、subscribe()の行を接続用の関数の中に入れる |
| 起動時にリレーが勝手にオンになる | コマンド用のトピックに古いretainメッセージが残っている | mosquitto_pub -r -n -t ev/salon/role/komutで消去する |
| Home Assistantでエンティティが「利用不可」と表示される | availabilityのトピックがofflineになっている、またはonlineのつづりが違う | ev/salon/baglantiを受信して確認し、接続後にコードがonlineをパブリッシュしていることを確かめる |
| 値の代わりに文字化けした文字が出る | ペイロードは\0で終わらないのに、lengthを使わずに読み取っている | ループでlengthの分だけペイロードをコピーする |
| ESPHomeのデバイスが15分ごとに再起動する | MQTTモードなのにapi:ブロックが残っていて、Home Assistantが一度も接続しない | api:ブロックを削除するか、reboot_timeout: 0sを追加する |
ブローカーといくつかのトピック、そしてPubSubClientで、ESP32は家のほかの機器と同じ言葉を話すスマートホームデバイスになりました。同じしくみに新しいセンサーやほかの部屋、Pico Wのような別のボードを追加し、すべてをHome Assistantの1つのダッシュボードで管理できます。BlueGraysアプリでは、ESP32のWi-Fi接続、DHT22での温度と湿度の測定、PIRとリレーによるスマート照明、WebSocketによるリアルタイム制御、スマートな植物の水やりといったプロジェクトを、回路図とコード付きでステップごとに紹介しています。
よくある質問
MQTTは何に使うのですか?
MQTTは、デバイスがブローカーを介し、トピック名を使ってメッセージを送受信する軽量なメッセージングプロトコルです。通信量も電力も少なくて済むので、ESP32のような小さなデバイスがセンサーデータを送ったりコマンドを受け取ったりするのに最適です。Home Assistant、Node-RED、そして産業用IoTシステムの多くがMQTTに対応しています。
MQTTは何番のポートを使いますか?
初期設定では、暗号化なしのMQTTは1883番、TLSで暗号化したMQTTは8883番のポートを使います。ブラウザからの接続に使うWebSocketのポートはブローカーによって異なり、Home AssistantのMosquittoブローカーでは1884番です。家庭のネットワークでは、ESP32はたいてい1883番に接続します。
MQTTにはインターネット接続が必要ですか?
いいえ。ブローカーが自宅のRaspberry PiやHome Assistantで動いていれば、ESP32とブローカーの間の通信はすべてローカルネットワーク内で完結します。インターネットが切れてもセンサーやリレーは動き続けます。インターネットが必要なのは、外からシステムにアクセスしたいときやクラウドのブローカーを使いたいときだけです。
ESP32がHome Assistantに表示されないのはなぜですか?
よくある原因は、DiscoveryのメッセージがPubSubClientの256バイトのバッファに収まらないこと、configトピックのつづりの誤り、JSONの引用符のミスです。mosquitto_subでhomeassistant/#を受信して、メッセージがブローカーに届いているかを確認し、コードにmqtt.setBufferSize(1024)の行があることを確かめてください。Home AssistantのMQTT統合が同じブローカーに接続していることも必要です。
ESPHomeとArduinoのMQTT、どちらを使えばいいですか?
動作するスマートホームデバイスを早く用意したいなら、ESPHomeのほうが手間がかかりません。YAMLを書くだけで、デバイスがHome Assistantに自動で表示され、無線でアップデートできます。MQTTのプロトコルを学びたい、独自のロジックを書きたい、Home Assistant以外のシステムと連携したいなら、ArduinoとPubSubClientのほうが柔軟です。同じ家の中で両方を問題なく併用できます。
MQTTのQoS 0、1、2の違いは何ですか?
QoS 0は確認を待たずにメッセージを1回送ります。QoS 1は確認が届くまで再送しますが、2回届くことがあります。QoS 2は4段階のハンドシェイクで、ちょうど1回届くことを保証します。センサーの測定値にはQoS 0、コマンドにはQoS 1で十分です。PubSubClientはQoS 0でのパブリッシュと、QoS 0または1でのサブスクライブしかできません。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
ESP32でLEDを点滅
ESP32のGPIO出力の基本を学びます。デジタルピンからLEDを点灯・消灯させて、点滅させます。Arduino UNOの定番のLED点滅プロジェクトのESP32版です。Arduino環境で使う基本的な点滅コマンドは同じですが、ESP32のピン配置と3.3 VのGPIO電圧レベルはArduino UNOとは異なります。

ESP32 Wi-Fi STA接続(LCDディスプレイ付き)
このプロジェクトでは、ESP32をステーション(STA)モードで家庭のWi-Fiネットワークに接続し、安全で障害に強い接続を実現します。SSIDとパスワードを別のsecrets.hファイルに保存してGitへの流出を防ぎ、自動再接続の仕組みを実装します。RSSIによる信号強度とIPアドレスはI2C LCDディスプレイに表示します。内蔵LEDは状態表示に使います。実運用を想定した接続管理の仕組みです。

ESP32 DHT22温湿度計(LCDディスプレイ付き)
DHT22センサーで周囲の温度と湿度を測定し、16x2 I2C LCDにリアルタイムで表示するプロジェクトです。Adafruit DHTライブラリを使い、1本の信号線を使うプロトコルでセンサーからデータを読み取ります。IoT気象観測装置やスマートホームの自動化、温室の環境監視システムの基礎となります。ESP32のGPIO 4ピンを介してDHT22と通信します。

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

ESP32 WebSocketカウンター(2つのボタン+LCD+Webリアルタイム表示)
このプロジェクトでは、ESP32に接続した2つのボタンでカウンターの値を変更します(GPIO 5で増加、GPIO 4で減少)。値は16x2 I2C LCDに即座に反映され、ESPAsyncWebServerのWebSocket機能により、接続中のすべてのブラウザーでもページを再読み込みせずに更新されます。HTTPポーリングの代わりに1つの接続を維持し、ESP32がws.textAll()で新しい値を全員に送信します。

ESP32 mDNS:bluegrays-esp.localでデバイスにアクセス(LCD付き)
このプロジェクトでは、ESP32にmDNS(Multicast DNS)サービスを追加し、IPアドレスの代わりにbluegrays-esp.localのような覚えやすいホスト名でアクセスできるようにします。DHCPで割り当てられるIPアドレスが変わっても、同じ名前でデバイスにアクセスできます。小さなWebサーバーも動かし、ブラウザーでhttp://bluegrays-esp.local/を開いてLEDを操作します。I2C LCDディスプレイにはホスト名、IPアドレス、LEDの状態をトルコ語と英語で表示します。iOS、macOS、LinuxはmDNSに標準対応しています。

ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)
PIRセンサーが動きを検知するとブザーとLEDを作動させ、同時にTelegramで持ち主に即座に通知するスマート防犯アラームです。アラームが繰り返される頻度を抑えるため、30秒のクールダウン(待ち時間)を設けています。この仕組みはPIRの検知が正しいかを確認したり、誤検知をなくしたりするものではありません。家の防犯、ガレージの監視、倉庫や作業部屋などに使えます。ESP32-CAMなら写真付きの通知を追加でき、通常のESP32 DevKit V1ではテキストで通知します。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

