Heatwaves are becoming more common, and very hot rooms are a real danger, especially for babies, older people, people who are ill, and pets. The temperature alone doesn't tell the whole story. Our body cools down by sweating, and sweat only cools us when it can evaporate. In humid air it evaporates slowly, so 35 °C with high humidity feels, and is, much more dangerous than 35 °C in dry air. Weather services combine the two into the heat index, the "feels like" temperature. In this lesson the ESP32 measures temperature and humidity with the DHT11, calculates the heat index with the official formula, shows the risk level with three LEDs, and sends a message to the family's phones when a room becomes dangerous.
What you'll learn
- Why humidity makes heat more dangerous, and what the heat index is
- How to turn a weather service's formula into code
- How to smooth readings with a running average
- How to send each warning once, and again only when the danger stays
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- DHT11 module, KY-015 (ESP32 IoT Starter Kit)
- Green, yellow and red LEDs and 3 resistors, 220 Ω (ESP32 IoT Starter Kit)
- Passive buzzer (ESP32 IoT Starter Kit)
- A phone with the free ntfy app
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
The heat index levels
| Heat index | Level | What can happen | LEDs |
|---|---|---|---|
| below 27 °C | Comfortable | No extra risk | Green |
| 27–32 °C | Caution | Tiredness with long activity | Green and yellow |
| 32–41 °C | Extreme caution | Heat cramps and heat exhaustion are possible | Yellow |
| 41–54 °C | Danger | Heat exhaustion is likely, heatstroke possible | Red |
| 54 °C and more | Extreme danger | Heatstroke is likely | Red, blinking, with beeps |
These are the levels used by the US National Weather Service. Babies, older people and pets are at risk earlier. The DHT11 measures from 0 to 50 °C, to within about 2 °C, so treat its readings as a good guide, not an exact value.
Wiring

| From | To |
|---|---|
| DHT11 S (signal) | ESP32 GPIO 27 |
| DHT11 + | ESP32 3V3 |
| DHT11 − | GND |
| ESP32 GPIO 25, 26, 14 | 220 Ω resistor, then the long leg of the green, yellow and red LED |
| LEDs' short legs | GND |
| Passive buzzer + | ESP32 GPIO 33 |
| Passive buzzer − | GND |
Set up ntfy
The phone messages go through ntfy, a free notification service that needs no account. The ntfy lesson explains it step by step.
- Install the ntfy app from Google Play or the App Store.
- Make up a topic name that nobody could guess, such as
eementor-heat-9m2v5t. Use only letters, numbers, - and _. - In the app, tap +, type your topic name and tap Subscribe. Everyone who should get the messages subscribes to the same topic on their own phone.
- Test it: open
https://ntfy.sh/your-topic-namein a browser, type a message and send it. It should pop up on the phones.
Important: on the free ntfy.sh server, anyone who knows your topic name can read your messages. Keep the name long and random, and never send personal details through it.
The sketch
In the Arduino IDE, open Library Manager and install the DHT sensor library by Adafruit (say yes when it asks to install Adafruit Unified Sensor too).
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
Put your topic in TOPIC and the room's name in ROOM.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <DHT.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TOPIC = "YOUR_SECRET_TOPIC"; // for example eementor-heat-9m2v5t
const char* ROOM = "Bedroom";
const int DHT_PIN = 27, GREEN_PIN = 25, YELLOW_PIN = 26, RED_PIN = 14, BUZZER_PIN = 33;
DHT dht(DHT_PIN, DHT11);
const char* LEVELS[] = {"Comfortable", "Caution", "Extreme caution", "Danger", "Extreme danger"};
const float LIMITS[] = {27, 32, 41, 54}; // heat index (°C) where levels 1 to 4 start
const int ALERT_LEVEL = 3; // tell the phones from "Danger" up
const unsigned long REPEAT_ALERT = 3600000; // remind every hour while it stays dangerous
float temps[6], hums[6]; // the last 6 readings (30 seconds)
int stored = 0, slot = 0;
int level = 0, toldLevel = 0; // the level now, and the last one the phones heard
float heatIndex = 0, avgT = 0, avgH = 0;
unsigned long lastRead = 0, lastAlert = 0, lastSendTry = 0, lastBeep = 0, lastWifiTry = 0;
// Heat index from the US National Weather Service (Rothfusz formula), in °C
float heatIndexC(float c, float rh) {
float f = c * 9 / 5 + 32; // the formula works in °F
float hi = 0.5 * (f + 61.0 + (f - 68.0) * 1.2 + rh * 0.094); // simple version
if ((hi + f) / 2 >= 80) { // hot: use the full formula
hi = -42.379 + 2.04901523 * f + 10.14333127 * rh - 0.22475541 * f * rh
- 0.00683783 * f * f - 0.05481717 * rh * rh + 0.00122874 * f * f * rh
+ 0.00085282 * f * rh * rh - 0.00000199 * f * f * rh * rh;
if (rh < 13 && f >= 80 && f <= 112) hi -= (13 - rh) / 4 * sqrt((17 - fabs(f - 95)) / 17);
else if (rh > 85 && f >= 80 && f <= 87) hi += (rh - 85) / 10 * (87 - f) / 5;
}
return (hi - 32) * 5 / 9;
}
int levelFor(float hi) {
int l = 0;
while (l < 4 && hi >= LIMITS[l]) l++;
return l;
}
bool notify(const char* title, String message, const char* priority, const char* tags) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.setTimeout(5000);
http.begin(client, String("https://ntfy.sh/") + TOPIC);
http.addHeader("Title", title);
http.addHeader("Priority", priority);
http.addHeader("Tags", tags);
int code = http.POST(message);
http.end();
Serial.printf("ntfy answered %d\n", code);
return code == 200;
}
bool sendAlert() {
String numbers = String(ROOM) + ": feels like " + String(heatIndex, 0) + " °C (" + String(avgT, 1) + " °C, " + String(avgH, 0) + " % humidity). ";
if (level >= ALERT_LEVEL)
return notify(level == 4 ? "Extreme heat danger" : "Heat danger", numbers + "Cool the room down: close curtains, use a fan or air conditioning. Drink water, and check on babies, older people and pets. Signs of heatstroke (confusion, hot dry skin, fainting) need an emergency call.", level == 4 ? "urgent" : "high", "thermometer,warning");
return notify("Cooler again", numbers + "The heat risk is lower now.", "default", "white_check_mark");
}
void setup() {
Serial.begin(115200);
pinMode(GREEN_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(RED_PIN, OUTPUT);
dht.begin();
WiFi.begin(WIFI_NAME, WIFI_PASSWORD); // the lights work even without Wi-Fi
}
void loop() {
unsigned long now = millis();
// 1. Measure every 5 seconds and average the last 30 seconds
if (now - lastRead >= 5000) {
lastRead = now;
float t = dht.readTemperature(), h = dht.readHumidity();
if (isnan(t) || isnan(h)) {
Serial.println("No reading from the DHT11");
} else {
temps[slot] = t;
hums[slot] = h;
slot = (slot + 1) % 6;
if (stored < 6) stored++;
avgT = avgH = 0;
for (int i = 0; i < stored; i++) {
avgT += temps[i] / stored;
avgH += hums[i] / stored;
}
heatIndex = heatIndexC(avgT, avgH);
int l = levelFor(heatIndex);
if (l > level) level = l; // up straight away
else if (l < level && heatIndex < LIMITS[level - 1] - 1) level = l; // down only 1 °C below the limit
Serial.printf("%.1f °C %.0f %% feels like %.1f °C %s\n", avgT, avgH, heatIndex, LEVELS[level]);
}
}
// 2. Phone messages: once when it gets dangerous, every hour while it stays, and when it is over
bool worse = level >= ALERT_LEVEL && level > toldLevel;
bool still = level >= ALERT_LEVEL && now - lastAlert > REPEAT_ALERT;
bool over = level < 2 && toldLevel >= ALERT_LEVEL;
if ((worse || still || over) && WiFi.status() == WL_CONNECTED && (lastSendTry == 0 || now - lastSendTry > 60000)) {
lastSendTry = now;
if (sendAlert()) {
toldLevel = level;
lastAlert = now;
}
}
if (WiFi.status() != WL_CONNECTED && now - lastWifiTry > 30000) {
lastWifiTry = now;
WiFi.disconnect();
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
}
// 3. Lights and beeps
digitalWrite(GREEN_PIN, level <= 1);
digitalWrite(YELLOW_PIN, level == 1 || level == 2);
digitalWrite(RED_PIN, level == 3 || (level == 4 && (now / 300) % 2));
if (level >= ALERT_LEVEL && now - lastBeep > (level == 4 ? 60000UL : 300000UL)) {
lastBeep = now;
for (int i = 0; i < 3; i++) {
tone(BUZZER_PIN, 2000, 120);
delay(220);
}
}
}
Try it
- Upload the sketch and open the Serial Monitor at 115200 baud. Every 5 seconds it shows the temperature, the humidity and the "feels like" heat index.
- In a normal room the green LED is on. Now warm the sensor gently: cup your hands around it and breathe on it, which adds warmth and humidity. Watch the heat index rise faster than the temperature.
- To see the alarm without a heatwave, change
LIMITSto lower numbers, for example{22, 24, 26, 28}. Warm the sensor again: at "Danger" the red LED comes on, the buzzer beeps three times and the phones get the heat warning. - Let the sensor cool down. When the level falls back to "Caution" or below, the phones get "Cooler again".
- Put the real limits back, and place the guardian in the room you want to watch, at about head height, out of direct sunlight.
How it works
The heat index formula was found by fitting a curve to the results of a big study of how hot people feel at different temperatures and humidities. It is called the Rothfusz regression, after the scientist who published it. For mild conditions the National Weather Service uses a simpler formula, so heatIndexC() calculates that first, and only uses the full formula when it is hot. Two small corrections handle very dry and very humid air. The formula works in degrees Fahrenheit, so the code converts on the way in and out. (The DHT library has its own computeHeatIndex(); writing it yourself shows that there is no magic inside.)
The DHT11's readings jump a little from one measurement to the next. To stop the LEDs flickering between two levels, the sketch keeps the last six readings in a small ring buffer and uses their average. The level goes up at once, but only goes down again when the heat index is 1 °C below the limit, another example of hysteresis.
Messages follow three rules: send one when it becomes dangerous or gets worse, repeat it every hour while the danger stays, and send an "all clear" when it is over. That keeps the family informed without filling their phones with the same message every few seconds.
client.setInsecure() keeps the connection encrypted but skips checking the server's certificate. That keeps the sketch short and is fine for a learning project. For a device you leave running for years, load the server's root certificate with client.setCACert() instead.
Try this next
- Add a web page that shows the heat index and a chart of the last few hours.
- Switch a 5 V fan on at "Caution" with the relay module (low voltage only).
- Use two sensors, inside and outside, and suggest the best time to open the windows.
- Get tomorrow's forecast from Open-Meteo and warn a day before a heatwave.
Troubleshooting
- "No reading from the DHT11": check S on GPIO 27, + on 3V3 and − on GND. Some modules have the pins in a different order; follow the labels printed on yours.
- The heat index looks too high: the sensor may be in the sun, near a lamp, or warmed by the ESP32 itself. Put it a few centimetres away from the board, in the shade.
- It reads 50 °C and doesn't go higher: that is the top of the DHT11's range. For very hot places, use a DHT22 or a DS18B20 for the temperature.
- No message on the phone: look for "ntfy answered" in the Serial Monitor. 200 means it was sent: check the topic name in the app letter by letter. -1 means no internet; 429 means too many messages in a short time.
- It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.