ESP32 to Home Assistant via MQTT

Connect your Arduino-programmed ESP32 to Home Assistant using MQTT auto-discovery

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| A

The 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:

  1. Go to Settings > Add-ons > Add-on Store.
  2. Search for Mosquitto broker.
  3. Click Install, then Start.
  4. Enable Start on boot.

🔗Create an MQTT User

Mosquitto requires authentication. Create a dedicated user:

  1. Go to Settings > People > Users (tab at the top).
  2. Click Add User.
  3. Set the name and username to mqtt_user (or whatever you prefer).
  4. Set a password (e.g., mqtt_pass). Remember these -- you will use them in your Arduino code.
  5. Toggle "Can only log in from the local network" to on (this user does not need full HA access).

🔗Configure the MQTT Integration

  1. Go to Settings > Devices & Services.
  2. Home Assistant should auto-discover the Mosquitto broker. Click Configure.
  3. If it does not appear, click Add Integration, search for MQTT, and configure it with localhost as 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>/config

Where:

  • <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):

  1. PubSubClient by Nick O'Leary -- MQTT client
  2. Adafruit BME280 Library by Adafruit -- sensor driver
  3. Adafruit Unified Sensor by Adafruit -- dependency
  4. ArduinoJson by Benoit Blanchon -- JSON generation

🔗Wiring

BME280 PinESP32 PinNotes
VIN / VCC3.3VPower
GNDGNDGround
SDAGPIO 21I2C data
SCLGPIO 22I2C 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

  1. Connects to WiFi and the Mosquitto MQTT broker.
  2. Sends MQTT auto-discovery messages for three sensor entities (temperature, humidity, pressure).
  3. Every 30 seconds, reads the BME280 and publishes the values.
  4. 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:

  1. Go to Settings > Devices & Services > MQTT.
  2. Click on the MQTT integration. You should see a new device called "Office Sensor."
  3. Click on it to see the three entities: Temperature, Humidity, and Pressure.
  4. The entities will have the device_class set 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

ComponentPinESP32 Pin
Relay moduleINGPIO 26
Relay moduleVCC5V (Vin)
Relay moduleGNDGND

🔗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 ClassUnitIcon
temperature°C or °FThermometer
humidity%Water droplet
pressurehPaGauge
illuminancelxBrightness
battery%Battery
voltageVLightning bolt
currentACurrent
powerWFlash
energykWhLightning 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

ProblemCauseSolution
Entities do not appear in HADiscovery JSON too large for bufferCall mqtt.setBufferSize(512) before connecting
Entities do not appear in HAMQTT integration not configuredGo to Settings > Devices & Services and set up MQTT
"Connection failed (rc=-2)"Wrong broker IP or portVerify the IP address of your HA instance
"Connection failed (rc=4)"Wrong MQTT credentialsCheck username and password
Relay does not respondNot subscribed to command topicEnsure mqtt.subscribe() is called after each reconnect
Stale data after rebootNot republishing discoveryCall publishAllDiscovery() on every MQTT reconnect
Switch state out of syncState not published after commandAlways 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.