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.
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
- Flash the board; the Serial Monitor should print
MQTT connected. - Home Assistant → Settings → Devices & services → MQTT — a device ESP32 xxxxxx appears with three entities.
- Toggle Relay in the HA UI — the relay clicks and the state stays in sync both ways.
- 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_idmust 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),numberandbutton— see the HA MQTT integration docs.