RisalDash RisalDash

Project · Climate · Automation

A thermostat that heats and cools

Most DIY thermostats stop at “heater on, heater off”. A room that actually feels right needs the other half too: a cooling fan that kicks in automatically when the temperature overshoots, and an overheat alert for the day the heater relay sticks. One DHT22, two relays and an ESP32 do all three — with the setpoint, history chart and an event log on a dashboard the chip serves itself.

ProjectClimateDHT22RelayESP32
ESP32 smart thermostat project: DHT22, hysteresis, auto-cooling fan and overheat alerts

Why hysteresis, not a plain threshold

Switch the heater at exactly the setpoint and it chatters on/off every few seconds around the boundary — relays hate that. A hysteresis band (heat below −0.3 °C, stop above +0.3 °C) makes each cycle long and calm. Auto-cooling is a second, wider band on the other side: fan on at +1.5 °C over, off once the room recovers. The overheat alert sits further out still and fires even in Hold mode, because a stuck relay doesn't care about your automation:

if (!hold) {
  // Heating hysteresis: heat below setpoint-0.3, stop above setpoint+0.3.
  if (roomT < setpoint - 0.3f) heating = true;
  else if (roomT > setpoint + 0.3f) heating = false;
  // Auto-cooling: fan on when the room runs 1.5 C past the setpoint, off on recovery.
  if (roomT > setpoint + 1.5f) { if (!fan && evlog) evlog->print("Overshoot - fan ON"); fan = true; }
  else if (roomT < setpoint + 0.5f) fan = false;
}
// Overheat alert regardless of hold: something is wrong (sun on the sensor, heater stuck).
bool hot = roomT > setpoint + 4.0f;
if (hot && !overheat && evlog) evlog->print("OVERHEAT - check the heater");
overheat = hot;

The dashboard

A live setpoint (number widget), both relay states, the alert LED and the trend chart — the whole control panel is a dozen declarations:

dash.layout("Climate", RICON_THERMOMETER);
dash.gauge("Room temp", &roomT, 0, 40, "C");
dash.gauge("Humidity", &roomH, 0, 100, "%").variant("semi");
dash.chart("Trend", &roomT, "C");            // the history everyone wants

dash.layout("Control", RICON_POWER);
dash.number("Setpoint", &setpoint, 5, 35, 1);
dash.led("Heating", &heating);               // relay 1
dash.led("Cooling fan", &fan);               // relay 2 — auto-cooling
dash.led("Overheat!", &overheat);            // buzzer + red LED
dash.toggle("Hold", &hold);
dash.beginAP("RisalDash-Thermostat", "12345678");

The shipped example (examples/04_Templates/Thermostat) runs on the fake day/night room from RisalFakeEnv, so both sides of the hysteresis fire while you watch: mornings heat, afternoons overshoot and the fan spins up. Swap one line for a real dht.readTemperature() and wire two relays when you're happy with the bands.

Making it a product

Persist the setpoint in NVS so it survives reboots; add MQTT + Home Assistant discovery and the thermostat shows up as a climate entity; or let an AI agent read the room and nudge the setpoint via MCP. The full room version — with motion and gas — is the room monitor build.

Tune the hysteresis bands on a fake room tonight — wire the relays later.