Skip to content
Tutorial
16 min read
October 17, 2026

ESP32 MQTT Temperature Sensor: Send DHT22 Data to Home Assistant

Build an ESP32 temperature and humidity sensor that appears in Home Assistant automatically using MQTT Discovery. Includes DHT22 wiring, complete PubSubClient code with availability and reconnect, dashboard setup, an alert automation and troubleshooting.

A temperature sensor is the "hello world" of Home Assistant, and the ESP32 is the best way to build one. For the price of a coffee you get a device that reports temperature and humidity every thirty seconds, shows up as a proper entity with a graph, works in automations and survives router reboots without you touching it.

The part most tutorials skip is the one that makes it feel professional: MQTT Discovery. Instead of editing configuration.yaml and restarting Home Assistant for every sensor, your ESP32 announces itself over MQTT and Home Assistant creates the device and its entities on its own. If a sensor goes offline, the entity correctly shows as unavailable instead of freezing on a stale value. This guide builds exactly that, from wiring to a working dashboard card, and then adds an alert automation.

It assumes you already have a working MQTT broker. If you do not, start with the MQTT and Home Assistant beginner guide, and see the ESP32 MQTT tutorial for the publish and subscribe basics.

Image pending: Finished ESP32 and DHT22 sensor next to a Home Assistant dashboard showing live temperature and humidity cards

How the Pieces Fit Together

DHT22 --> ESP32 --(WiFi, MQTT)--> Mosquitto broker --> Home Assistant
  1. The ESP32 reads the DHT22.
  2. It publishes a JSON message such as {"temperature":24.6,"humidity":51.2} to a state topic.
  3. On connection it also publishes a discovery message describing each sensor to a special topic that Home Assistant watches.
  4. Home Assistant creates a device with a temperature entity and a humidity entity, and updates them whenever new state arrives.
  5. A Last Will message tells Home Assistant when the ESP32 disappears, so the entities go "unavailable" instead of showing old data.

What You Need

ItemNotes
ESP32 development boardAny ESP32 board (see the ESP32 pinout guide)
DHT22 (AM2302) sensor or moduleThe module version includes the required pull-up resistor. A DHT11 also works but is less accurate.
Jumper wires, breadboard
Home AssistantAny install type, with the Mosquitto broker add-on or another MQTT broker
Arduino librariesPubSubClient, DHT sensor library (Adafruit), Adafruit Unified Sensor, ArduinoJson 7

If you prefer a BME280 on I2C, the MQTT and Home Assistant parts are identical. Only the reading code changes (see the I2C guide).

Step 1: Prepare Home Assistant

  1. Install the broker. In Home Assistant go to Settings → Add-ons → Add-on Store (called Apps in the newest Home Assistant versions), install Mosquitto broker and start it.
  2. Add the MQTT integration. Home Assistant normally suggests it under Settings → Devices & Services. Accept the discovery prompt and confirm. Discovery is enabled by default.
  3. Create a user for the ESP32. Go to Settings → People → Users → Add user, create a user such as esp32 with a strong password, and leave it as a normal user. The Mosquitto add-on accepts Home Assistant user accounts for MQTT logins by default.
  4. Note your Home Assistant machine's IP address. You will enter it as the MQTT host. Give it a fixed address (see the static IP guide).

Keep MQTT Explorer or the built-in Developer Tools → MQTT page handy. Listening to # there shows you every message and makes debugging far easier.

Image pending: Home Assistant Settings page showing the Mosquitto broker add-on running and the MQTT integration configured

Step 2: Wire the DHT22

DHT22 pinESP32 pin
VCC (+)3.3V
DATA (out)GPIO 4
GND (-)GND

If you use a bare 4-pin DHT22 (not a 3-pin module), add a 10 kΩ resistor between DATA and VCC. The ESP32 is a 3.3V device, so power the sensor from 3.3V, and the data line stays at a safe level. GPIO 4 has no boot-time side effects, which makes it a safe choice.

Image pending: Wiring diagram of a DHT22 module connected to 3.3V, GND and GPIO 4 on an ESP32

Step 3: The Complete Sketch

Edit the six settings at the top, then upload.

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>

// ---------- Settings ----------
const char* WIFI_SSID  = "YOUR_WIFI_NAME";
const char* WIFI_PASS  = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST  = "192.168.1.10";     // Home Assistant / broker IP
const int   MQTT_PORT  = 1883;
const char* MQTT_USER  = "esp32";
const char* MQTT_PASS  = "YOUR_MQTT_PASSWORD";

const char* DEVICE_ID   = "esp32_livingroom";   // unique per device, no spaces
const char* DEVICE_NAME = "Living Room Sensor";

#define DHT_PIN  4
#define DHT_TYPE DHT22
const unsigned long PUBLISH_INTERVAL = 30000;    // 30 seconds

// ---------- Globals ----------
WiFiClient   wifiClient;
PubSubClient mqtt(wifiClient);
DHT          dht(DHT_PIN, DHT_TYPE);

String stateTopic;
String availTopic;
unsigned long lastPublish = 0;
unsigned long lastReconnect = 0;

// ---------- Home Assistant MQTT Discovery ----------
void publishDiscovery(const char* objectId, const char* name,
                      const char* deviceClass, const char* unit,
                      const char* valueTemplate) {
  JsonDocument doc;
  doc["name"]               = name;
  doc["unique_id"]          = String(DEVICE_ID) + "_" + objectId;
  doc["state_topic"]        = stateTopic;
  doc["availability_topic"] = availTopic;
  doc["device_class"]       = deviceClass;
  doc["unit_of_measurement"] = unit;
  doc["state_class"]        = "measurement";
  doc["value_template"]     = valueTemplate;

  JsonObject dev = doc["device"].to<JsonObject>();
  JsonArray ids = dev["identifiers"].to<JsonArray>();
  ids.add(DEVICE_ID);
  dev["name"]         = DEVICE_NAME;
  dev["manufacturer"] = "DIY";
  dev["model"]        = "ESP32 + DHT22";

  String payload;
  serializeJson(doc, payload);

  String topic = String("homeassistant/sensor/") + DEVICE_ID + "/" + objectId + "/config";
  mqtt.publish(topic.c_str(), payload.c_str(), true);   // retained
}

void publishAllDiscovery() {
  publishDiscovery("temperature", "Temperature", "temperature", "°C", "{{ value_json.temperature }}");
  publishDiscovery("humidity",    "Humidity",    "humidity",    "%",  "{{ value_json.humidity }}");
}

// ---------- MQTT ----------
void onMessage(char* topic, byte* payload, unsigned int length) {
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  // Home Assistant announces "online" after a restart; re-send discovery
  if (String(topic) == "homeassistant/status" && msg == "online") {
    publishAllDiscovery();
    mqtt.publish(availTopic.c_str(), "online", true);
  }
}

bool connectMqtt() {
  String clientId = String(DEVICE_ID) + "-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  // Last Will: broker publishes "offline" if we vanish
  bool ok = mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS,
                         availTopic.c_str(), 1, true, "offline");
  if (ok) {
    Serial.println("MQTT connected");
    mqtt.publish(availTopic.c_str(), "online", true);
    mqtt.subscribe("homeassistant/status");
    publishAllDiscovery();
  } else {
    Serial.printf("MQTT failed, rc=%d\n", mqtt.state());
  }
  return ok;
}

void ensureWifi() {
  if (WiFi.status() == WL_CONNECTED) return;
  Serial.println("Reconnecting WiFi...");
  WiFi.disconnect();
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) delay(250);
}

void publishReadings() {
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  if (isnan(t) || isnan(h)) {
    Serial.println("DHT read failed, skipping");
    return;
  }

  JsonDocument doc;
  doc["temperature"] = round(t * 10) / 10.0;
  doc["humidity"]    = round(h * 10) / 10.0;
  String payload;
  serializeJson(doc, payload);

  mqtt.publish(stateTopic.c_str(), payload.c_str());
  Serial.println(payload);
}

void setup() {
  Serial.begin(115200);
  dht.begin();

  stateTopic = String("esp32/") + DEVICE_ID + "/state";
  availTopic = String("esp32/") + DEVICE_ID + "/status";

  WiFi.mode(WIFI_STA);
  WiFi.setHostname(DEVICE_ID);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.print("WiFi connected, IP: ");
  Serial.println(WiFi.localIP());

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setBufferSize(1024);        // discovery messages exceed the 256-byte default
  mqtt.setCallback(onMessage);
}

void loop() {
  ensureWifi();

  if (!mqtt.connected()) {
    if (millis() - lastReconnect > 5000) {
      lastReconnect = millis();
      connectMqtt();
    }
  } else {
    mqtt.loop();
    if (millis() - lastPublish >= PUBLISH_INTERVAL) {
      lastPublish = millis();
      publishReadings();
    }
  }
}

Why each design choice matters

  • setBufferSize(1024) is the single most common reason discovery silently fails. PubSubClient's default buffer is 256 bytes and a discovery message is larger, so without this line the message is dropped and no entity ever appears.
  • Retained discovery messages (true in publish) mean the broker remembers them, so Home Assistant finds the device even if it restarts after the ESP32 connected.
  • Last Will ("offline" retained on the availability topic) marks the entities unavailable the moment the ESP32 loses power or WiFi.
  • Subscribing to homeassistant/status lets the sketch re-announce itself when Home Assistant restarts.
  • Non-blocking reconnect uses millis(), so a missing broker never freezes the loop.
  • isnan checks prevent publishing nonsense when the DHT22 occasionally fails a read.
Image pending: Arduino Serial Monitor showing WiFi connected, MQTT connected and a JSON temperature and humidity message published every 30 seconds

Step 4: See It in Home Assistant

Within a few seconds of the first MQTT connection, go to Settings → Devices & Services → MQTT → Devices. A new device named Living Room Sensor appears, with a Temperature and a Humidity entity, with no YAML and no restart.

To watch the traffic yourself, open Settings → Devices & Services → MQTT → Configure (or Developer Tools if available), listen to esp32/# and homeassistant/#, and you will see the discovery config and the state updates arrive.

Add the entities to a dashboard: Edit dashboard → Add card → Gauge, Sensor or History graph, pick the temperature and humidity entities, and save. A History graph card is the most useful for temperature trends over a day.

Image pending: Home Assistant device page showing the Living Room Sensor with Temperature and Humidity entities created automatically through MQTT Discovery
Image pending: Home Assistant dashboard with a gauge card for temperature, a gauge for humidity and a 24-hour history graph

Step 5: An Alert Automation

Notify yourself if the room gets too hot. In Home Assistant go to Settings → Automations & Scenes → Create automation → Create new → Edit in YAML and paste:

alias: Living room too hot
description: Notify when temperature stays above 30 C
trigger:
  - platform: numeric_state
    entity_id: sensor.living_room_sensor_temperature
    above: 30
    for:
      minutes: 5
action:
  - service: notify.notify
    data:
      title: "Heat warning"
      message: "Living room is {{ states('sensor.living_room_sensor_temperature') }} C"
mode: single

The exact entity name depends on your device name. Copy it from the entity's settings page. The for: minutes: 5 clause prevents a one-off spike from triggering an alert. Replace notify.notify with your own notification service, such as your phone's mobile app notifier.

You can also trigger an action rather than a message: switch on a fan with a smart switch built from a second ESP32.

Troubleshooting

The device never appears in Home Assistant. In order: check the broker address, user and password; confirm the sketch printed MQTT connected; make sure setBufferSize(1024) is present; verify discovery is enabled in the MQTT integration, and that the prefix is homeassistant. Listen to homeassistant/# in MQTT Explorer and look for the .../config messages.

MQTT fails with rc=5 or rc=4. Bad credentials or not authorized. Recheck the username and password you created. Other codes: -2 means the connection could not be made (wrong IP, port or firewall), -4 is a timeout, 2 is a rejected client ID.

Entities show "unavailable". The Last Will fired or the availability message was never published. Confirm esp32/<id>/status holds online (retained). A client ID shared by two devices causes constant disconnects, so keep each DEVICE_ID unique.

Values are unknown. The state topic does not match, or the value_template references a different JSON key. Compare the discovery payload with the real state message.

NaN or failed DHT readings. Check wiring and the pull-up resistor. The DHT22 should be read no more than once every 2 seconds, which the 30-second interval respects.

Temperature seems high. Keep the sensor away from the ESP32 board and its voltage regulator, which both warm the air. A few centimetres of cable makes a visible difference.

Disconnects every few minutes. Weak WiFi or a flaky power supply. See the WiFi troubleshooting guide.

Making It Battery-Friendly

Publishing every 30 seconds with WiFi always on drains a battery quickly. For battery projects, wake up, read, publish and go back to sleep. The ESP32 deep sleep guide shows how to stretch a battery from days to months. In that setup, publish the discovery message with retain set, so Home Assistant remembers the entity while the ESP32 sleeps, and set expire_after in the discovery payload (for example 1800 seconds) so a missed wake-up is eventually flagged as unavailable.

Frequently Asked Questions

Do I need the discovery code, or can I use YAML? You can define an mqtt: sensor: block in configuration.yaml instead. Discovery just saves the editing and the restart.

Can I add more sensors to the same device? Yes. Call publishDiscovery() once per sensor with a new objectId and add the field to the JSON state message.

What about a DS18B20 or BME280? Replace the DHT code with the right library. Everything else stays the same. The BME280 also gives you pressure, which has a pressure device class.

Is it safe to publish without TLS? On a trusted home network with a password, plain MQTT is common. For remote brokers, use port 8883 with WiFiClientSecure.

Does it work with other platforms? Yes. Any MQTT-aware system (Node-RED, Grafana through InfluxDB, openHAB) can consume the same state topic.

What can I build next? A controllable device. See ESP32 Home Assistant WiFi smart switch, or look at integrating without MQTT if you prefer ESPHome.

Image pending: MQTT Explorer window showing the retained discovery config message, the availability topic set to online and the live state message
Image pending: ESP32 and DHT22 mounted in a small project box on a shelf, powered by a USB adapter, as a permanent room sensor

Signal In

Built from what makers search for

The lookups piling up in SolderHub's search bar every day shape what we publish next — new component pages, board guides, and firmware notes, sent out as they go live.

By subscribing you agree to our Privacy Policy. Unsubscribe anytime.