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.
How the Pieces Fit Together
DHT22 --> ESP32 --(WiFi, MQTT)--> Mosquitto broker --> Home Assistant
- The ESP32 reads the DHT22.
- It publishes a JSON message such as
{"temperature":24.6,"humidity":51.2}to a state topic. - On connection it also publishes a discovery message describing each sensor to a special topic that Home Assistant watches.
- Home Assistant creates a device with a temperature entity and a humidity entity, and updates them whenever new state arrives.
- A Last Will message tells Home Assistant when the ESP32 disappears, so the entities go "unavailable" instead of showing old data.
What You Need
| Item | Notes |
|---|---|
| ESP32 development board | Any ESP32 board (see the ESP32 pinout guide) |
| DHT22 (AM2302) sensor or module | The module version includes the required pull-up resistor. A DHT11 also works but is less accurate. |
| Jumper wires, breadboard | |
| Home Assistant | Any install type, with the Mosquitto broker add-on or another MQTT broker |
| Arduino libraries | PubSubClient, 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
- 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.
- Add the MQTT integration. Home Assistant normally suggests it under Settings → Devices & Services. Accept the discovery prompt and confirm. Discovery is enabled by default.
- Create a user for the ESP32. Go to Settings → People → Users → Add user, create a user such as
esp32with a strong password, and leave it as a normal user. The Mosquitto add-on accepts Home Assistant user accounts for MQTT logins by default. - 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.
Step 2: Wire the DHT22
| DHT22 pin | ESP32 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.
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 (
trueinpublish) 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/statuslets the sketch re-announce itself when Home Assistant restarts. - Non-blocking reconnect uses
millis(), so a missing broker never freezes the loop. isnanchecks prevent publishing nonsense when the DHT22 occasionally fails a read.
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.
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.