If you prefer writing your own Arduino code instead of using ESPHome's YAML approach, MQTT is how you connect your ESP32 to Home Assistant. In the Introduction to MQTT article, you learned how MQTT publish-subscribe works. This article takes it further: you will install an MQTT broker in Home Assistant, use MQTT auto-discovery so your entities appear automatically, and build a complete sensor and actuator example.
🔗The Architecture
When using Arduino + MQTT with Home Assistant, the data flow looks like this:
graph LR
A[ESP32<br>Arduino Code] -->|publish sensor data| B[Mosquitto<br>MQTT Broker]
B -->|auto-discovery| C[Home Assistant]
C -->|commands| B
B -->|subscribe to commands| AThe ESP32 publishes sensor data to the MQTT broker, and Home Assistant subscribes to those topics. For actuators (relays, LEDs), the flow reverses: Home Assistant publishes commands, and the ESP32 subscribes.
The key ingredient that makes this seamless is MQTT auto-discovery -- a protocol where the ESP32 sends a special configuration message that tells Home Assistant exactly what entities to create, what topics to listen on, and how to display the data.
🔗Step 1: Install Mosquitto Broker
If you have not already installed the Mosquitto add-on in Home Assistant:
- Go to Settings > Add-ons > Add-on Store.
- Search for Mosquitto broker.
- Click Install, then Start.
- Enable Start on boot.
🔗Create an MQTT User
Mosquitto requires authentication. Create a dedicated user:
- Go to Settings > People > Users (tab at the top).
- Click Add User.
- Set the name and username to
mqtt_user(or whatever you prefer). - Set a password (e.g.,
mqtt_pass). Remember these -- you will use them in your Arduino code. - Toggle "Can only log in from the local network" to on (this user does not need full HA access).
🔗Configure the MQTT Integration
- Go to Settings > Devices & Services.
- Home Assistant should auto-discover the Mosquitto broker. Click Configure.
- If it does not appear, click Add Integration, search for MQTT, and configure it with
localhostas the broker address.
🔗Step 2: Understanding MQTT Auto-Discovery
MQTT auto-discovery is what makes the magic happen. Instead of manually configuring entities in Home Assistant, your ESP32 sends a JSON configuration message to a special topic, and Home Assistant creates the entity automatically.
The discovery topic format is:
homeassistant/<component>/<node_id>/<object_id>/configWhere:
<component>is the entity type:sensor,binary_sensor,switch,light, etc.<node_id>is a unique identifier for the device (e.g.,esp32_bme280)<object_id>is a unique identifier for the specific entity (e.g.,temperature)
The payload is a JSON object describing the entity:
{
"name": "Temperature",
"state_topic": "esp32_bme280/sensor/temperature/state",
"unit_of_measurement": "°C",
"device_class": "temperature",
"value_template": "{{ value }}",
"unique_id": "esp32_bme280_temperature",
"device": {
"identifiers": ["esp32_bme280"],
"name": "Office Sensor",
"model": "ESP32 + BME280",
"manufacturer": "DIY"
}
}When Home Assistant receives this message, it creates a temperature sensor entity that reads its value from esp32_bme280/sensor/temperature/state. The device block groups entities into a single device in the HA interface.
🔗Step 3: Complete Sensor Example
This example connects a BME280 sensor to Home Assistant with full auto-discovery. Three entities appear automatically: temperature, humidity, and pressure.
🔗Required Libraries
Install these through the Arduino Library Manager (Sketch > Include Library > Manage Libraries):
- PubSubClient by Nick O'Leary -- MQTT client
- Adafruit BME280 Library by Adafruit -- sensor driver
- Adafruit Unified Sensor by Adafruit -- dependency
- ArduinoJson by Benoit Blanchon -- JSON generation
🔗Wiring
| BME280 Pin | ESP32 Pin | Notes |
|---|---|---|
| VIN / VCC | 3.3V | Power |
| GND | GND | Ground |
| SDA | GPIO 21 | I2C data |
| SCL | GPIO 22 | I2C clock |
🔗Code
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <ArduinoJson.h>
// ---- Configuration ----
const char* ssid = "YourNetworkName";
const char* password = "YourPassword";
const char* mqtt_server = "192.168.1.100"; // Your Home Assistant IP
const int mqtt_port = 1883;
const char* mqtt_user = "mqtt_user";
const char* mqtt_pass = "mqtt_pass";
// Unique ID for this device (change if you have multiple devices)
const char* device_id = "esp32_office";
const char* device_name = "Office Sensor";
// ---- Objects ----
WiFiClient espClient;
PubSubClient mqtt(espClient);
Adafruit_BME280 bme;
unsigned long lastPublish = 0;
const unsigned long publishInterval = 30000; // 30 seconds
bool discoveryPublished = false;
// ---- WiFi ----
void connectWiFi() {
Serial.printf("Connecting to WiFi: %s", ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nConnected. IP: %s\n", WiFi.localIP().toString().c_str());
}
// ---- MQTT Discovery ----
void publishDiscovery(const char* component, const char* objectId,
const char* name, const char* unit,
const char* deviceClass, const char* icon) {
// Build the discovery topic
char topic[128];
snprintf(topic, sizeof(topic),
"homeassistant/%s/%s/%s/config",
component, device_id, objectId);
// Build the state topic
char stateTopic[128];
snprintf(stateTopic, sizeof(stateTopic),
"%s/%s/%s/state", device_id, component, objectId);
// Build the unique ID
char uniqueId[64];
snprintf(uniqueId, sizeof(uniqueId), "%s_%s", device_id, objectId);
// Create JSON payload
JsonDocument doc;
doc["name"] = name;
doc["state_topic"] = stateTopic;
doc["unique_id"] = uniqueId;
doc["value_template"] = "{{ value }}";
if (unit != nullptr) doc["unit_of_measurement"] = unit;
if (deviceClass != nullptr) doc["device_class"] = deviceClass;
if (icon != nullptr) doc["icon"] = icon;
// Device information (groups entities together)
JsonObject dev = doc["device"].to<JsonObject>();
dev["identifiers"][0] = device_id;
dev["name"] = device_name;
dev["model"] = "ESP32 + BME280";
dev["manufacturer"] = "DIY";
char payload[512];
serializeJson(doc, payload, sizeof(payload));
mqtt.publish(topic, payload, true); // retained message
Serial.printf("Discovery published: %s\n", topic);
}
void publishAllDiscovery() {
publishDiscovery("sensor", "temperature", "Temperature",
"°C", "temperature", nullptr);
publishDiscovery("sensor", "humidity", "Humidity",
"%", "humidity", nullptr);
publishDiscovery("sensor", "pressure", "Pressure",
"hPa", "pressure", nullptr);
discoveryPublished = true;
}
// ---- MQTT Connection ----
void connectMQTT() {
while (!mqtt.connected()) {
Serial.print("Connecting to MQTT...");
char clientId[32];
snprintf(clientId, sizeof(clientId), "%s_%04x",
device_id, (uint16_t)random(0xFFFF));
if (mqtt.connect(clientId, mqtt_user, mqtt_pass)) {
Serial.println(" connected!");
publishAllDiscovery();
} else {
Serial.printf(" failed (rc=%d). Retrying in 5s.\n", mqtt.state());
delay(5000);
}
}
}
// ---- Publish Sensor Data ----
void publishSensorData() {
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F;
char topic[128];
char payload[16];
// Temperature
snprintf(topic, sizeof(topic), "%s/sensor/temperature/state", device_id);
snprintf(payload, sizeof(payload), "%.1f", temperature);
mqtt.publish(topic, payload);
// Humidity
snprintf(topic, sizeof(topic), "%s/sensor/humidity/state", device_id);
snprintf(payload, sizeof(payload), "%.1f", humidity);
mqtt.publish(topic, payload);
// Pressure
snprintf(topic, sizeof(topic), "%s/sensor/pressure/state", device_id);
snprintf(payload, sizeof(payload), "%.1f", pressure);
mqtt.publish(topic, payload);
Serial.printf("Published: %.1f°C, %.1f%%, %.1f hPa\n",
temperature, humidity, pressure);
}
// ---- Setup & Loop ----
void setup() {
Serial.begin(115200);
delay(1000);
// Initialize BME280
if (!bme.begin(0x76)) {
Serial.println("BME280 not found! Check wiring.");
while (1) delay(10);
}
Serial.println("BME280 initialized.");
connectWiFi();
mqtt.setServer(mqtt_server, mqtt_port);
mqtt.setBufferSize(512); // Needed for discovery JSON payloads
connectMQTT();
}
void loop() {
if (!mqtt.connected()) {
connectMQTT();
}
mqtt.loop();
unsigned long now = millis();
if (now - lastPublish >= publishInterval) {
lastPublish = now;
publishSensorData();
}
}🔗What This Code Does
- Connects to WiFi and the Mosquitto MQTT broker.
- Sends MQTT auto-discovery messages for three sensor entities (temperature, humidity, pressure).
- Every 30 seconds, reads the BME280 and publishes the values.
- Home Assistant receives the discovery messages and creates three entities automatically.
Important: The
mqtt.setBufferSize(512)call is essential. The default PubSubClient buffer is only 256 bytes, which is too small for discovery JSON payloads. Without this, the discovery messages will be silently dropped and your entities will not appear.
🔗Verifying in Home Assistant
After uploading the code and letting it run for a few seconds:
- Go to Settings > Devices & Services > MQTT.
- Click on the MQTT integration. You should see a new device called "Office Sensor."
- Click on it to see the three entities: Temperature, Humidity, and Pressure.
- The entities will have the
device_classset correctly, so Home Assistant automatically uses the right icons and graph types.
🔗Step 4: Controlling an Actuator from Home Assistant
Sensors publish data to Home Assistant. Actuators work in the other direction: Home Assistant sends commands that the ESP32 acts on. Here is how to add a relay that Home Assistant can control.
🔗Additional Wiring
| Component | Pin | ESP32 Pin |
|---|---|---|
| Relay module | IN | GPIO 26 |
| Relay module | VCC | 5V (Vin) |
| Relay module | GND | GND |
🔗Adding Relay Code
Add these functions and modifications to the code above:
// ---- Relay Configuration ----
#define RELAY_PIN 26
const char* relay_command_topic = "esp32_office/switch/relay/set";
const char* relay_state_topic = "esp32_office/switch/relay/state";
// Add to setup(), after connectWiFi():
void setupRelay() {
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Start with relay off
}
// MQTT callback for receiving commands
void mqttCallback(char* topic, byte* payload, unsigned int length) {
String message;
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.printf("Received on [%s]: %s\n", topic, message.c_str());
if (String(topic) == relay_command_topic) {
if (message == "ON") {
digitalWrite(RELAY_PIN, HIGH);
mqtt.publish(relay_state_topic, "ON", true);
Serial.println("Relay ON");
} else if (message == "OFF") {
digitalWrite(RELAY_PIN, LOW);
mqtt.publish(relay_state_topic, "OFF", true);
Serial.println("Relay OFF");
}
}
}Add the relay discovery to the publishAllDiscovery() function:
void publishRelayDiscovery() {
char topic[128];
snprintf(topic, sizeof(topic),
"homeassistant/switch/%s/relay/config", device_id);
JsonDocument doc;
doc["name"] = "Office Relay";
doc["command_topic"] = relay_command_topic;
doc["state_topic"] = relay_state_topic;
doc["unique_id"] = "esp32_office_relay";
doc["icon"] = "mdi:electric-switch";
doc["payload_on"] = "ON";
doc["payload_off"] = "OFF";
JsonObject dev = doc["device"].to<JsonObject>();
dev["identifiers"][0] = device_id;
dev["name"] = device_name;
dev["model"] = "ESP32 + BME280";
dev["manufacturer"] = "DIY";
char payload[512];
serializeJson(doc, payload, sizeof(payload));
mqtt.publish(topic, payload, true);
}And in your setup() function, add the callback and relay subscription:
void setup() {
Serial.begin(115200);
delay(1000);
if (!bme.begin(0x76)) {
Serial.println("BME280 not found!");
while (1) delay(10);
}
setupRelay();
connectWiFi();
mqtt.setServer(mqtt_server, mqtt_port);
mqtt.setBufferSize(512);
mqtt.setCallback(mqttCallback); // Register the callback
connectMQTT();
}Update connectMQTT() to subscribe to the relay command topic and publish relay discovery after connecting:
if (mqtt.connect(clientId, mqtt_user, mqtt_pass)) {
Serial.println(" connected!");
publishAllDiscovery();
publishRelayDiscovery();
mqtt.subscribe(relay_command_topic);
// Publish current relay state
mqtt.publish(relay_state_topic,
digitalRead(RELAY_PIN) ? "ON" : "OFF", true);
}Now in Home Assistant, you will see a switch entity called "Office Relay" that you can toggle from the dashboard. When you flip the switch, Home Assistant publishes "ON" or "OFF" to esp32_office/switch/relay/set, and the ESP32 acts on it.
🔗Device Classes and Icons
Home Assistant uses device classes to determine how to display entities. Setting the right device class gives you appropriate icons, graphs, and unit handling automatically.
Common sensor device classes:
| Device Class | Unit | Icon |
|---|---|---|
temperature | °C or °F | Thermometer |
humidity | % | Water droplet |
pressure | hPa | Gauge |
illuminance | lx | Brightness |
battery | % | Battery |
voltage | V | Lightning bolt |
current | A | Current |
power | W | Flash |
energy | kWh | Lightning bolt |
moisture | % | Water |
If none of the built-in device classes fit, omit the device_class field and set a custom icon using Material Design Icons (prefix: mdi:).
🔗Availability and Last Will
MQTT has a "Last Will and Testament" (LWT) feature that lets the broker publish a message if a device disconnects unexpectedly. This lets Home Assistant know when a device goes offline:
// In the connect call, add a will message:
const char* availability_topic = "esp32_office/status";
if (mqtt.connect(clientId, mqtt_user, mqtt_pass,
availability_topic, 1, true, "offline")) {
// After connecting, publish "online"
mqtt.publish(availability_topic, "online", true);
// ... rest of connection setup
}Add the availability configuration to each discovery JSON:
doc["availability_topic"] = availability_topic;
doc["payload_available"] = "online";
doc["payload_not_available"] = "offline";Now Home Assistant will show entities as "Unavailable" if the ESP32 disconnects, instead of showing stale data.
🔗Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Entities do not appear in HA | Discovery JSON too large for buffer | Call mqtt.setBufferSize(512) before connecting |
| Entities do not appear in HA | MQTT integration not configured | Go to Settings > Devices & Services and set up MQTT |
| "Connection failed (rc=-2)" | Wrong broker IP or port | Verify the IP address of your HA instance |
| "Connection failed (rc=4)" | Wrong MQTT credentials | Check username and password |
| Relay does not respond | Not subscribed to command topic | Ensure mqtt.subscribe() is called after each reconnect |
| Stale data after reboot | Not republishing discovery | Call publishAllDiscovery() on every MQTT reconnect |
| Switch state out of sync | State not published after command | Always publish to state_topic after changing the relay |
🔗What is Next?
You now know how to connect Arduino-coded ESP32 devices to Home Assistant using MQTT auto-discovery. The next article puts everything together in a complete multi-sensor node with deep sleep for battery operation.