ガイド · 読了まで23分

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でHome Assistantに接続したESP32開発ボード
ESP32はWi-Fiを内蔵しているので、家庭のネットワーク上のMQTTブローカーに直接つながります。センサーデータもリレーのコマンドも同じ接続を流れます。

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での手順は次のとおりです。

  1. Home Assistantで設定 > アプリ(旧バージョンでは「アドオン」という名前)を開き、アプリをインストールをクリックして、一覧からMosquitto brokerを選びます。
  2. インストールをクリックし、完了したら開始をクリックします。起動時に開始するオプションは有効のままにしておきます。
  3. ESP32用に別のユーザーを作ります。設定 > 人 > ユーザーで、「esp32」という名前のユーザーを強力なパスワードで追加します。このタブが見当たらない場合は、プロフィールで詳細モードをオンにしてください。「homeassistant」と「addons」という名前はブローカー用に予約されているので使わないでください。
  4. 設定 > デバイスとサービスのページには、たいていMQTTの統合が「検出」として表示されます。構成をクリックして確定します。表示されない場合は統合を追加でMQTTを検索し、Mosquittoを自動で構成するオプションを選びます。
  5. 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で作ったパスワードファイルを使います。パスワードはハッシュとして保存されます
設定でlistenerが定義されていない場合、Mosquitto 2.0以降は127.0.0.1だけで待ち受け、ログに「Starting in local only mode. Connections will only be possible from clients running on this machine」(ローカル専用モードで起動、このマシン上のクライアントからのみ接続可能)という警告を書き出します。この状態ではESP32は接続できず、rc=-2のエラーになります。一方で、ルーターで1883番ポートをインターネットに開放すること(ポート転送)は絶対にしないでください。暗号化されていないMQTTの通信は途中で読み取られる可能性があり、家のリレーが他人に操作されかねません。外からアクセスする必要がある場合は、VPNかHome Assistantのリモートアクセスの仕組みを使ってください。

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付きで送ります。

コマンドラインが苦手なら、無料のMQTT Explorerというプログラムで、ブローカー上のすべてのトピックを最新の値やretainフラグと一緒にツリー表示で確認できます。Home Assistantの中でも、設定 > デバイスとサービス > MQTT > 構成を開き、「トピックをリッスン」の欄にev/#と入力して受信を始められます。「パケットを公開」の欄からは、ESP32に手動でコマンドを送れます。

部品リストと配線図

このプロジェクトに必要なのは、ESP32開発ボード、DHT22温湿度センサー、3.3 Vで動作させられるリレーモジュール、ブレッドボード、ジャンパーワイヤーです。DHT22はDHT11より精度が高く、温度を±0.5 °C、湿度をおよそ±2–5%の精度で測定でき、-40〜80 °Cで動作します。センサーのファミリーについては「DHT11温湿度センサー」で詳しく解説しています。DHT11を持っている場合は、コードのDHT22をDHT11に変えるだけです。最初の接続テストにはESP32とUSBケーブルだけで十分で、センサーとリレーはあとから追加できます。

ESP32
ESP32
DHT22
DHT22
リレーモジュール
リレーモジュール
接続ESP32ESP8266(NodeMCU)Raspberry Pi Pico W
DHT22 VCC3V33V33V3(OUT)
DHT22 DATAGPIO 4D2 (GPIO 4)GP15
DHT22 GNDGNDGNDGND
リレーのVCCVIN (5V)VIN (5V)VBUS (5V)
リレーのINGPIO 26D1 (GPIO 5)GP14
リレーのGNDGNDGNDGND
ESP32、ESP8266、Picoのピンは3.3 Vで動作し、5 V非対応です。そのためDHT22は3V3ピンから給電します。3ピンのDHT22モジュールには、たいていボード上にプルアップ抵抗が付いています。4本足の単体センサーを使う場合は、DATAとVCCの間に4.7–10 kΩの抵抗を追加してください。リレーで230 Vの商用電源の負荷を切り替える場合(日本の商用電源は100V(50/60Hz)です)は、すべての配線をプラグを抜いた状態で行い、大人に手伝ってもらってください。詳しくは「リレーモジュールとは?」にあります。ボードのすべてのピンは「ピン配置」ツールで確認できます。

Arduino IDEにPubSubClientをインストールする

  1. Arduino IDEにESP32のボードパッケージがまだ入っていない場合は、先にインストールします。手順は「ESP32とは?」にあります。
  2. 「ツール」>「ライブラリを管理」を開き、検索欄にPubSubClientと入力して、作者がNick O'Learyのライブラリをインストールします。このガイドのコードはバージョン2.8でコンパイルしました。
  3. 同じウィンドウでDHT sensor libraryを検索し、Adafruitのライブラリをインストールします。依存ライブラリについて聞かれたら「全てをインストール」を選び、Adafruit Unified Sensorも一緒にインストールします。
  4. ライブラリのインストールでつまずいたら、「Arduinoにライブラリを追加する方法」のガイドを参照してください。

最初のテスト:ESP32をMQTTブローカーにつなぐ

センサーを追加する前に、Wi-Fiの情報、ブローカーのアドレス、パスワードが正しいことを、いちばん簡単なコードで確認しましょう。下のコードはブローカーに接続し、「test/esp32/durum」トピックに「merhaba」(トルコ語で「こんにちは」)をパブリッシュして、「test/esp32/komut」に届いたメッセージをすべてシリアルモニタに表示します。Wi-Fiの名前、パスワード、ブローカーのIP、MQTTユーザーを自分のものに置き換えてコードを書き込み、シリアルモニタを115200ボーで開いてください。

C++ (Arduino IDE)
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秒後に再試行します。これらのコードを見れば、問題がどこにあるかがすぐにわかります。

rcPubSubClientでの名前意味と対処法
-4MQTT_CONNECTION_TIMEOUTブローカーが時間内に応答しなかった。ネットワークが弱いか、ブローカーの負荷が高い
-3MQTT_CONNECTION_LOST確立した接続が切れた。たいていはloop()が長時間呼ばれなかったか、Wi-Fiが切れたのが原因
-2MQTT_CONNECT_FAILEDTCP接続がそもそも開けなかった。IPやポートの誤り、ブローカーが停止中、Mosquittoがローカル専用モード、またはファイアウォールが1883をブロックしている
-1MQTT_DISCONNECTED接続が正常に閉じられた(disconnectが呼ばれた)か、まだ一度も接続していない
2MQTT_CONNECT_BAD_CLIENT_IDクライアントIDが拒否された。空または無効なクライアントIDを使っている
4MQTT_CONNECT_BAD_CREDENTIALSユーザー名またはパスワードの誤り(このコードを返すブローカーもある)
5MQTT_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でも変更なしでコンパイルできます。

C++ (Arduino IDE)
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()の違い」で詳しく解説しています。

コマンド用のトピックには、決してretainフラグ付きでメッセージを送らないでください。「ev/salon/role/komut」にretain付きの「ON」が残っていると、ESP32は停電やWi-Fiの切断のあとに再接続するたびにその古いコマンドを受け取り、真夜中に勝手にリレーをオンにしてしまいます。うっかりretain付きのコマンドを送ってしまったら、mosquitto_pub -r -n -t ev/salon/role/komutで空のretainメッセージをパブリッシュすれば消去できます。Home AssistantのMQTTスイッチは、初期設定ではretainなしでコマンドを送ります。

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_classmeasurementを指定すると、長期の統計とグラフが記録されるmeasurement
availability_topiconline/offlineの状態を読み取るトピック。初期設定のペイロードはonlineとofflineev/salon/baglanti(接続状態)
deviceエンティティを1枚のデバイスカードにまとめるidentifiers、name、model(識別子、名前、型番)

コードを書き込んで数秒たつと、設定 > デバイスとサービス > MQTTに「Salon ESP32」というデバイスが現れます。デバイスカードには温度と湿度のセンサー、そしてリレーのスイッチがあります。温度をクリックすると履歴のグラフが開き、スイッチを切り替えるとリレーがカチッと動きます。これらのエンティティはダッシュボードに追加したり、音声アシスタントに公開したり、オートメーションで使ったりできます。たとえば「リビングが27 °Cを超えたら扇風機をつないだリレーをオンにし、25 °Cを下回ったらオフにする」というルールも、数回のクリックで作れます。ESP32のプラグを抜くと、遅くとも約22秒後にエンティティが「利用不可」に切り替わり、ラストウィルのメッセージが機能していることがわかります。Discoveryを使いたくない場合は、configuration.yamlのmqtt:の下に、sensorとswitchとして同じエンティティを手動で定義することもできます。

デバイスの名前やトピックを変更して、Home Assistantに古いエンティティが残ってしまった場合は、該当するconfigトピックに空のretainメッセージを送ります:mosquitto_pub -r -n -t homeassistant/sensor/esp32-salon/sicaklik/config。Home Assistantはエンティティを削除し、デバイスにほかのエンティティが残っていなければデバイスも削除します。ESP32ごとに別のISTEMCI_ID(トルコ語で「クライアントID」)を付ければ、unique_idも自動的に別々になるので、複数の部屋を混乱なく追加できます。

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_sifresi

1つ注意すべき違いがあります。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という名前で保存すると、ボードの電源を入れるたびに実行されます。

MicroPython
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を追加する
トラブルシューティングは順番を守って進めましょう。まずシリアルモニタでWi-Fiの接続とIPアドレスを確認し、次にパソコンのmosquitto_sub -v -t "#"でブローカーに実際に何が届いているかを見て、最後にHome Assistantを確認します。メッセージがブローカーには届いているのにHome Assistantに表示されないなら、問題はDiscoveryのJSONにあります。ブローカーにまったく届かないなら、ESP32側の問題です。Wi-Fiの段階でつまずいている場合は「ESP32がWi-Fiにつながらないとき」を、リレーが動いたときにボードがリセットされる場合は「ESP32のブラウンアウトとGuru Meditation」のガイドを参照してください。

ブローカーといくつかのトピック、そして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を点滅
#103初級

ESP32でLEDを点滅

ESP32のGPIO出力の基本を学びます。デジタルピンからLEDを点灯・消灯させて、点滅させます。Arduino UNOの定番のLED点滅プロジェクトのESP32版です。Arduino環境で使う基本的な点滅コマンドは同じですが、ESP32のピン配置と3.3 VのGPIO電圧レベルはArduino UNOとは異なります。

4.7(37)
99
ESP32 Wi-Fi STA接続(LCDディスプレイ付き)
#127上級

ESP32 Wi-Fi STA接続(LCDディスプレイ付き)

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

4.4(21)
34
ESP32 DHT22温湿度計(LCDディスプレイ付き)
#117中級

ESP32 DHT22温湿度計(LCDディスプレイ付き)

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

4.8(38)
56
ESP32スマート照明(PIR + LDR + リレー + OLED)
#141上級
プレミアム

ESP32スマート照明(PIR + LDR + リレー + OLED)

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

4.3(6)
11
ESP32 WebSocketカウンター(2つのボタン+LCD+Webリアルタイム表示)
#130上級
プレミアム

ESP32 WebSocketカウンター(2つのボタン+LCD+Webリアルタイム表示)

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

4.5(14)
23
ESP32 mDNS:bluegrays-esp.localでデバイスにアクセス(LCD付き)
#131上級
プレミアム

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に標準対応しています。

4.8(14)
23
ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)
#145上級
プレミアム

ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)

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

0.0(0)
9