← До всіх проєктів
IoT / Веб Середній

ESP32 сам з'являється в Home Assistant: MQTT Discovery без YAML

Пишемо скетч, який при підключенні сам реєструє в Home Assistant пристрій із сенсорами та перемикачем. Жодних записів у configuration.yaml — лише MQTT-брокер.

Складність
Середній
Час
1–2 h
Категорія
IoT / Веб

Ідея

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);
  }
}

Перевірка

  1. Прошийте плату, у Serial Monitor має з'явитися MQTT connected.
  2. Home Assistant → Settings → Devices & services → MQTT — з'явиться пристрій ESP32 xxxxxx з трьома сутностями.
  3. Перемкніть Relay в інтерфейсі HA — реле клацне, а стан синхронізується в обидва боки.
  4. Вимкніть плату — через ~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.