ESP32 综合教程:通过 MQTT 自动发现接入 Home Assistant
本文由 AI 协助整理,示例以 Arduino-ESP32、PubSubClient 和 ArduinoJson 为主。Home Assistant 与库的界面、字段会随版本变化,本文依据发布时的官方 MQTT Discovery 规则整理;实际部署请以当前官方文档为准。

这次不是“发出 MQTT 就算完成”
ESP32 能把温湿度发到 MQTT,只说明数据已经进入消息服务器。要在 Home Assistant 里变成有名称、有单位、有设备归属、能画历史曲线的实体,还需要配置这些数据应该如何解释。
最省事的方式是 MQTT Discovery。ESP32 启动后向特定主题发布一份配置,告诉 Home Assistant:“我是一台设备,我有温度、湿度和一个开关,这些实体的状态和命令分别在哪些主题。”Home Assistant 收到后自动创建设备和实体,不需要为每块板手写 YAML。
自动发现很方便,也容易留下“幽灵实体”:设备名改了、唯一编号变了、旧配置仍保留在 Broker 中。本文不仅讲怎样让实体出现,也讲清楚稳定编号、保留消息、在线状态和删除旧实体。
最终功能
完成后,ESP32 会在 Home Assistant 中显示为一台设备,包含:
- 温度传感器,单位为摄氏度。
- 湿度传感器,单位为百分比。
- 一个 LED 开关,可从 Home Assistant 下发开关命令。
- 在线或离线状态。
- 设备名称、型号和固件版本。
硬件用普通 ESP32 DevKit 即可。传感器可选 DHT22、SHT30 或 BME280。教程把传感器读取封装成两个变量,便于替换;LED 使用开发板可用的输出引脚,具体 GPIO 要按自己的板型确认。
服务端准备
Home Assistant 需要先配置 MQTT Broker。使用 Home Assistant OS 时,常见做法是安装 Mosquitto Broker 应用,然后在“设置 → 设备与服务”中添加 MQTT 集成。也可以使用自己维护的兼容 Broker。
为 ESP32 创建独立 MQTT 账号,不要让设备使用管理员账号。Broker 不应直接暴露到公网;跨网络访问应使用 VPN、TLS 或经过安全设计的入口。
先用 MQTT 客户端验证以下信息:
| 配置 | 示例 |
|---|---|
| Broker 地址 | 192.168.1.20 |
| 端口 | 1883,仅局域网示例 |
| 设备用户名 | esp32_node |
| 密码 | 使用独立强密码 |
| Discovery 前缀 | 默认 homeassistant |
先确认电脑客户端能连上 Broker,再调 ESP32。否则 WiFi、用户名、端口、Broker 防火墙和代码问题会同时站在门口,排查起来很有团队气氛。
主题规划
本项目使用:
home/lab/esp32-ha-01/state
home/lab/esp32-ha-01/set
home/lab/esp32-ha-01/availability
homeassistant/sensor/esp32_ha_01_temperature/config
homeassistant/sensor/esp32_ha_01_humidity/config
homeassistant/switch/esp32_ha_01_led/config
前三个是业务主题,后三个是自动发现配置主题。默认发现主题格式是:
<discovery_prefix>/<component>/<object_id>/config
每个实体还必须有稳定且唯一的 unique_id。不要每次启动都生成新的随机编号,也不要让两块设备使用同一个编号,否则 Home Assistant 会把它们认成同一个实体,像给两户人家发了同一把门牌。

安装 Arduino 库
在 Arduino IDE 库管理器安装:
PubSubClientArduinoJson- 自己传感器所需的库,例如 Adafruit BME280 Library
PubSubClient 默认消息缓冲区可能放不下较长的 Discovery JSON,可以在代码中调用 mqtt.setBufferSize(1024)。若配置继续扩展,应检查序列化后的实际长度,而不是一味把缓冲区调大。
程序配置与主题
下面先准备基础对象:
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_PASSWORD";
const char* MQTT_HOST = "192.168.1.20";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = "esp32_node";
const char* MQTT_PASS = "CHANGE_ME";
const char* DEVICE_ID = "esp32_ha_01";
const char* DEVICE_NAME = "书房环境节点";
const char* STATE_TOPIC = "home/lab/esp32-ha-01/state";
const char* COMMAND_TOPIC = "home/lab/esp32-ha-01/set";
const char* AVAIL_TOPIC = "home/lab/esp32-ha-01/availability";
constexpr uint8_t LED_PIN = 2; // 按开发板修改
WiFiClient networkClient;
PubSubClient mqtt(networkClient);
正式项目不要把真实密码提交到公开代码仓库。可以放入未纳入版本控制的 secrets.h,或者通过配置页面写入 NVS。
发布自动发现配置
为了便于理解,下面采用“每个实体一条配置”的方式。官方文档也支持把多组件放在一个设备发现消息中;项目实体很多时,可以再升级成设备级发现。
先写公共设备信息:
void addDeviceInfo(JsonDocument& doc) {
JsonObject device = doc["device"].to<JsonObject>();
device["identifiers"][0] = DEVICE_ID;
device["name"] = DEVICE_NAME;
device["manufacturer"] = "DIY";
device["model"] = "ESP32 DevKit";
device["sw_version"] = "1.0.0";
}
温度实体:
void publishTemperatureDiscovery() {
JsonDocument doc;
doc["name"] = "温度";
doc["unique_id"] = "esp32_ha_01_temperature";
doc["state_topic"] = STATE_TOPIC;
doc["value_template"] = "{{ value_json.temperature }}";
doc["device_class"] = "temperature";
doc["unit_of_measurement"] = "°C";
doc["state_class"] = "measurement";
doc["availability_topic"] = AVAIL_TOPIC;
addDeviceInfo(doc);
char payload[768];
size_t len = serializeJson(doc, payload, sizeof(payload));
mqtt.publish(
"homeassistant/sensor/esp32_ha_01_temperature/config",
reinterpret_cast<const uint8_t*>(payload), len, true);
}
湿度实体只需调整名称、唯一编号、模板、设备类型和单位:
void publishHumidityDiscovery() {
JsonDocument doc;
doc["name"] = "湿度";
doc["unique_id"] = "esp32_ha_01_humidity";
doc["state_topic"] = STATE_TOPIC;
doc["value_template"] = "{{ value_json.humidity }}";
doc["device_class"] = "humidity";
doc["unit_of_measurement"] = "%";
doc["state_class"] = "measurement";
doc["availability_topic"] = AVAIL_TOPIC;
addDeviceInfo(doc);
char payload[768];
size_t len = serializeJson(doc, payload, sizeof(payload));
mqtt.publish(
"homeassistant/sensor/esp32_ha_01_humidity/config",
reinterpret_cast<const uint8_t*>(payload), len, true);
}
最后一个参数 true 表示保留 Discovery 配置。这样 Home Assistant 或 Broker 重启后仍能收到配置。保留消息也意味着旧配置不会自动消失,修改 unique_id 前要计划如何清理。
增加可控制的 LED 开关
开关既有命令主题,也有状态字段:
void publishLedDiscovery() {
JsonDocument doc;
doc["name"] = "状态灯";
doc["unique_id"] = "esp32_ha_01_led";
doc["command_topic"] = COMMAND_TOPIC;
doc["state_topic"] = STATE_TOPIC;
doc["value_template"] = "{{ value_json.led }}";
doc["payload_on"] = "ON";
doc["payload_off"] = "OFF";
doc["availability_topic"] = AVAIL_TOPIC;
addDeviceInfo(doc);
char payload[768];
size_t len = serializeJson(doc, payload, sizeof(payload));
mqtt.publish(
"homeassistant/switch/esp32_ha_01_led/config",
reinterpret_cast<const uint8_t*>(payload), len, true);
}
接收命令时必须验证主题和内容:
bool ledOn = false;
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
if (strcmp(topic, COMMAND_TOPIC) != 0) return;
String command;
for (unsigned int i = 0; i < length; ++i) {
command += static_cast<char>(payload[i]);
}
if (command == "ON") {
ledOn = true;
digitalWrite(LED_PIN, HIGH);
} else if (command == "OFF") {
ledOn = false;
digitalWrite(LED_PIN, LOW);
} else {
Serial.printf("Unknown command: %s\n", command.c_str());
return;
}
publishState();
}
有些开发板板载 LED 是低电平点亮,HIGH 与 LOW 需要反过来。接继电器时还要考虑上电默认状态,不要让设备启动时产生危险动作。
发布状态数据
温度、湿度和 LED 状态共用一个 JSON:
void publishState() {
float temperature = 24.3f; // 替换为真实读数
float humidity = 52.6f;
if (!isfinite(temperature) || !isfinite(humidity)) {
Serial.println("Invalid sensor value");
return;
}
JsonDocument doc;
doc["temperature"] = temperature;
doc["humidity"] = humidity;
doc["led"] = ledOn ? "ON" : "OFF";
char payload[256];
size_t len = serializeJson(doc, payload, sizeof(payload));
mqtt.publish(STATE_TOPIC,
reinterpret_cast<const uint8_t*>(payload), len, false);
}
状态数据通常不必保留。Home Assistant 会保存历史,设备离线后也不应让 Broker 用一条很旧的读数假装它刚刚还在测量。若业务确实需要 retain,要同时正确维护 availability 和数据过期逻辑。
在线、离线与遗嘱消息
连接 MQTT 时设置 Last Will:
bool connectMqtt() {
bool ok = mqtt.connect(
DEVICE_ID,
MQTT_USER,
MQTT_PASS,
AVAIL_TOPIC,
1,
true,
"offline");
if (!ok) return false;
mqtt.publish(AVAIL_TOPIC, "online", true);
mqtt.subscribe(COMMAND_TOPIC);
publishTemperatureDiscovery();
publishHumidityDiscovery();
publishLedDiscovery();
publishState();
return true;
}
设备异常断电或网络丢失后,Broker 会发布 offline;正常连接时设备发布 online。Home Assistant 便能把实体显示为不可用,而不是永久保留最后一个温度,看起来仿佛拔掉电源以后房间气候也被冻结了。
官方 MQTT 集成默认还会在 homeassistant/status 发布 Birth 消息。更完善的设备可以订阅它,收到 online 后重新发送发现配置。本文已经使用保留发现消息,入门阶段能够应对常见重启场景。
主循环与重连节流
不要在 loop() 中无间隔疯狂重连:
unsigned long lastReconnectAttempt = 0;
unsigned long lastPublishAt = 0;
void loop() {
if (!mqtt.connected()) {
if (millis() - lastReconnectAttempt >= 5000) {
lastReconnectAttempt = millis();
connectMqtt();
}
} else {
mqtt.loop();
}
if (mqtt.connected() && millis() - lastPublishAt >= 30000) {
lastPublishAt = millis();
publishState();
}
}
WiFi 也需要独立处理断线恢复。正式项目可以用状态机管理 WiFi 与 MQTT,确保传感器采样不被网络重连阻塞。
验证顺序
- 用桌面 MQTT 客户端确认 Broker 用户名和密码可用。
- ESP32 只连接 WiFi,打印 IP。
- 连接 MQTT,查看 Broker 日志或订阅
home/#。 - 发布温度发现配置,确认 Home Assistant 出现一个实体。
- 增加湿度与 LED,检查它们是否归到同一设备。
- 从 Home Assistant 切换 LED,确认 ESP32 收到命令并回报状态。
- 直接拔掉 ESP32,观察实体是否变成不可用。
- 重启 Home Assistant,确认发现配置仍可恢复。
常见问题
| 现象 | 排查方向 |
|---|---|
| MQTT 已连接但没有实体 | Discovery 前缀、配置主题格式、JSON 和 retain |
| 出现多个重复设备 | unique_id 或设备 identifiers 不稳定 |
| 实体存在但没有数值 | state_topic、JSON 字段和 value_template 不一致 |
| 控制按钮能点但设备不动作 | 命令主题、订阅、payload 大小写和引脚逻辑 |
| 设备断电仍显示在线 | Last Will 或 availability 配置不正确 |
| 配置发布失败 | PubSubClient 缓冲区太小,或序列化数组容量不足 |
删除旧的自动发现实体
如果之前发布了错误配置,向旧的配置主题发布空消息并保留,即可清除对应保留配置。随后在 Home Assistant 中检查设备和实体是否仍有其他引用。
不要只改 unique_id 重新发布。那会创建新实体,旧实体仍然留着,几轮测试后界面里便会出现“温度”“温度 2”“温度 3”,很像一个没人愿意认领的家族群。
安全和工程化建议
为每台设备使用独立账号与最小主题权限;生产网络启用 TLS,并正确验证 Broker 证书;不要把管理界面和 1883 端口直接暴露到公网;密码和 WiFi 凭据不要提交到公开仓库。
设备端还可以增加 NVS 配置、Web 配网、OTA、看门狗和错误计数。Home Assistant 端则可以基于温度、湿度和在线状态创建自动化,例如节点离线十分钟后提醒,而不是只把数值摆在仪表盘上欣赏。
总结
ESP32 接入 Home Assistant 的关键不是“MQTT 能发消息”,而是为设备建立稳定身份,并把配置、状态、命令和可用性分开。unique_id 决定实体身份,Discovery 配置告诉平台如何解释数据,availability 让平台知道设备是否真的在线。
把这些基础打牢后,增加光照、空气质量、继电器或门磁只是继续添加实体。设备会从一个能发 JSON 的开发板,变成 Home Assistant 中可管理、可自动化、能明确显示在线状态的智能家居节点。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号