← All projects
IoT / Web Intermediate

ESP32 Shows Up in Home Assistant by Itself: MQTT Discovery, No YAML

A sketch that registers a device with sensors and a switch in Home Assistant as soon as it connects. No configuration.yaml edits — just an MQTT broker.

Difficulty
Intermediate
Time
1–2 h
Category
IoT / Web

The idea

Home Assistant listens to MQTT topics like homeassistant/<type>/<id>/config. Publish a JSON entity description there and HA creates the device, sensor or switch by itself — with an icon, units and device grouping. That's how Tasmota, ESPHome and Zigbee2MQTT do it. We'll do the same in a plain Arduino sketch.

Our device will have:

  • a chip temperature sensor;
  • a Wi-Fi signal sensor (diagnostic);
  • a relay switch with two-way sync;
  • an online/offline status via Last Will — HA shows "unavailable" if the board disappears.

It all works with a bare board — you can wire the relay later.

What you need

  • ESP32 DevKit (+ optional relay module on GPIO 26)
  • Home Assistant with the MQTT integration and a broker (the Mosquitto broker add-on)
  • An MQTT user (in HA: Settings → People → Users, or a dedicated user in the Mosquitto config)

Library

PubSubClient (Nick O'Leary).

Code

#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";    // Home Assistant IP
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 restarted — announce ourselves again
    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 messages exceed the default 256 bytes
  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);
  }
}

Testing

  1. Flash the board; the Serial Monitor should print MQTT connected.
  2. Home Assistant → Settings → Devices & services → MQTT — a device ESP32 xxxxxx appears with three entities.
  3. Toggle Relay in the HA UI — the relay clicks and the state stays in sync both ways.
  4. Power the board off — after ~20 s the entities turn "Unavailable".

For debugging, watch all traffic in MQTT → Configure → Listen to a topic with the # filter.

Removing the device

Discovery messages are retained. To remove the device for good, publish an empty retained message to each .../config topic, or delete the device in the MQTT integration UI.

Good to know

  • unique_id must be unique and stable, or HA will create duplicates. That's why it's derived from the MAC.
  • Chip temperature is approximate and reads 10–20 °C high; swap in data from the DHT22, BME280 or DS18B20 projects.
  • The same pattern works for binary_sensor (motion, doors), light (with brightness), number and button — see the HA MQTT integration docs.