ESP32 сам з'являється в Home Assistant: MQTT Discovery без YAML
Пишемо скетч, який при підключенні сам реєструє в Home Assistant пристрій із сенсорами та перемикачем. Жодних записів у configuration.yaml — лише MQTT-брокер.
Ідея
Home Assistant слухає MQTT-топіки виду homeassistant/<тип>/<id>/config. Якщо опублікувати туди JSON з описом сутності, HA сам створить пристрій, сенсор чи перемикач — з іконкою, одиницями виміру та прив'язкою до пристрою. Так працюють Tasmota, ESPHome, Zigbee2MQTT. Зробимо те саме у звичайному Arduino-скетчі.
Наш пристрій матиме:
- сенсор температури чипа;
- сенсор рівня Wi-Fi сигналу (діагностичний);
- перемикач реле з двостороннім зв'язком;
- статус online/offline через Last Will — HA покаже «недоступний», якщо плата зникне.
Все це працює з голою платою — реле можна підключити пізніше.
Що знадобиться
- ESP32 DevKit (+ модуль реле на GPIO 26, за бажанням)
- Home Assistant з інтеграцією MQTT і брокером (add-on Mosquitto broker)
- Користувач для MQTT (у HA: Settings → People → Users, або окремий користувач у налаштуваннях Mosquitto)
Бібліотека
PubSubClient (Nick O'Leary).
Код
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";
const char* MQTT_HOST = "192.168.1.10"; // IP Home Assistant
const char* MQTT_USER = "mqtt-user";
const char* MQTT_PASS = "mqtt-password";
const int RELAY_PIN = 26;
WiFiClient net;
PubSubClient mqtt(net);
String deviceId; // esp32_a1b2c3
String baseTopic; // esp32/esp32_a1b2c3
unsigned long lastState = 0;
unsigned long lastReconnect = 0;
void publishRelay() {
mqtt.publish((baseTopic + "/relay").c_str(), digitalRead(RELAY_PIN) ? "ON" : "OFF", true);
}
void publishDiscovery() {
String device = "\"device\":{\"identifiers\":[\"" + deviceId + "\"],\"name\":\"ESP32 " +
deviceId.substring(6) + "\",\"manufacturer\":\"Espressif\",\"model\":\"ESP32 DevKit\"}";
String avail = "\"availability_topic\":\"" + baseTopic + "/status\"";
String state = "\"state_topic\":\"" + baseTopic + "/state\"";
String temp = "{\"name\":\"Chip temperature\",\"unique_id\":\"" + deviceId + "_temp\"," + state +
",\"value_template\":\"{{ value_json.temp }}\",\"unit_of_measurement\":\"°C\"," +
"\"device_class\":\"temperature\",\"state_class\":\"measurement\"," + avail + "," + device + "}";
mqtt.publish(("homeassistant/sensor/" + deviceId + "/temp/config").c_str(), temp.c_str(), true);
String rssi = "{\"name\":\"Wi-Fi signal\",\"unique_id\":\"" + deviceId + "_rssi\"," + state +
",\"value_template\":\"{{ value_json.rssi }}\",\"unit_of_measurement\":\"dBm\"," +
"\"device_class\":\"signal_strength\",\"entity_category\":\"diagnostic\"," + avail + "," + device + "}";
mqtt.publish(("homeassistant/sensor/" + deviceId + "/rssi/config").c_str(), rssi.c_str(), true);
String relay = "{\"name\":\"Relay\",\"unique_id\":\"" + deviceId + "_relay\"," +
"\"command_topic\":\"" + baseTopic + "/relay/set\"," +
"\"state_topic\":\"" + baseTopic + "/relay\"," + avail + "," + device + "}";
mqtt.publish(("homeassistant/switch/" + deviceId + "/relay/config").c_str(), relay.c_str(), true);
}
void onMessage(char *topic, byte *payload, unsigned int len) {
String msg;
for (unsigned int i = 0; i < len; i++) msg += (char)payload[i];
String t(topic);
if (t == baseTopic + "/relay/set") {
digitalWrite(RELAY_PIN, msg == "ON" ? HIGH : LOW);
publishRelay();
} else if (t == "homeassistant/status" && msg == "online") {
publishDiscovery(); // HA перезапустився — нагадуємо про себе
publishRelay();
}
}
bool connectMqtt() {
String willTopic = baseTopic + "/status";
if (!mqtt.connect(deviceId.c_str(), MQTT_USER, MQTT_PASS, willTopic.c_str(), 0, true, "offline")) {
return false;
}
mqtt.publish(willTopic.c_str(), "online", true);
publishDiscovery();
mqtt.subscribe((baseTopic + "/relay/set").c_str());
mqtt.subscribe("homeassistant/status");
publishRelay();
return true;
}
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(500);
String mac = WiFi.macAddress(); // AA:BB:CC:DD:EE:FF
mac.replace(":", "");
mac.toLowerCase();
deviceId = "esp32_" + mac.substring(6);
baseTopic = "esp32/" + deviceId;
Serial.println("Device ID: " + deviceId);
mqtt.setServer(MQTT_HOST, 1883);
mqtt.setBufferSize(1024); // discovery-повідомлення більші за стандартні 256 байт
mqtt.setCallback(onMessage);
}
void loop() {
if (!mqtt.connected()) {
if (millis() - lastReconnect > 5000) {
lastReconnect = millis();
Serial.println(connectMqtt() ? "MQTT connected" : "MQTT connect failed");
}
return;
}
mqtt.loop();
if (millis() - lastState > 30000) {
lastState = millis();
char json[64];
snprintf(json, sizeof(json), "{\"temp\":%.1f,\"rssi\":%d}", temperatureRead(), WiFi.RSSI());
mqtt.publish((baseTopic + "/state").c_str(), json);
}
}
Перевірка
- Прошийте плату, у Serial Monitor має з'явитися
MQTT connected. - Home Assistant → Settings → Devices & services → MQTT — з'явиться пристрій ESP32 xxxxxx з трьома сутностями.
- Перемкніть Relay в інтерфейсі HA — реле клацне, а стан синхронізується в обидва боки.
- Вимкніть плату — через ~20 с сутності стануть «Недоступні».
Для діагностики зручно дивитися всі повідомлення в MQTT → Configure → Listen to a topic з фільтром #.
Як прибрати пристрій
Discovery-повідомлення збережені (retained). Щоб видалити пристрій назавжди, опублікуйте порожнє повідомлення з прапорцем retain у кожен .../config топік, або видаліть пристрій в інтерфейсі MQTT-інтеграції.
Корисно знати
unique_idмає бути унікальним і незмінним — інакше HA створюватиме дублікати. Тому він залежить від MAC.- Температура чипа — приблизна і завищена на 10–20 °C; замініть її даними з DHT22, BME280 чи DS18B20 з інших проєктів.
- Та сама схема працює для
binary_sensor(рух, двері),light(з яскравістю),number,button— див. документацію MQTT-інтеграції HA.