ESP32 Smart Thermostat

Smart HomeBeginnerIntermediateAdvanced

A Wi-Fi smart thermostat that reads temperature and humidity, switches a relay for heating or cooling, and serves a live browser dashboard — all running on a single ESP32.

Overview

In this beginner project you build a temperature-controlled relay switch using an ESP32 DevKit and a DHT22 sensor. Every five seconds the ESP32 reads the current temperature. When it climbs above your chosen threshold — default 25 °C — GPIO 26 goes HIGH and energises a 5 V relay module, which can switch a fan, air conditioner, or space heater.

This project teaches the three core pillars of every embedded system: sense, decide, act. You will learn how to use the Adafruit DHT library, compare float values against a threshold, and drive a digital output to switch a real-world load. The relay is an electromagnetically operated switch that lets your low-power ESP32 safely control mains-voltage appliances without any electrical contact between the control and power circuits.

By the end you will have a fully automatic temperature controller. Adjust TEMP_THRESHOLD at the top of the sketch to set your desired cut-in temperature without touching any other code. The project is a foundation for the intermediate and advanced levels, where you add an OLED display, web dashboard, and MQTT smart-home integration.

What you will learn: DHT22 wiring with pull-up resistor, reading temperature and humidity, basic relay control, digital output logic, serial debugging.

Components
  • 1× ESP32 DevKit V1 (30-pin or 38-pin) — Any standard ESP32 board
  • 1× DHT22 Temperature & Humidity Sensor — More accurate than DHT11 — ±0.5 °C
  • 1× 5 V Single-Channel Relay Module — Optocoupler-isolated type for safety
  • 1× 10 kΩ Resistor — Pull-up for DHT22 data line
  • 1× Breadboard (400-tie or larger)
  • 1× Jumper Wires (male-to-male) — ~20 assorted
  • 1× USB Micro-B Cable — Programming and power
Wiring
Component PinESP32 PinNotes
DHT22 Pin 1 (VCC)3.3 VDo NOT use 5 V — DHT22 is 3.3 V tolerant
DHT22 Pin 2 (DATA)GPIO 4Place 10 kΩ pull-up between DATA and 3.3 V
DHT22 Pin 4 (GND)GND
Relay INGPIO 26Active-HIGH on most modules; check your datasheet
Relay VCCVIN (5 V)Relay coil needs 5 V — use VIN when powered via USB
Relay GNDGND
Arduino Code
esp32-smart-thermostat_beginner.ino
/*
 * ESP32 Smart Thermostat — Beginner
 * Switches a relay ON when temperature exceeds TEMP_THRESHOLD.
 * Reads DHT22 every 5 s. Logs readings to Serial Monitor.
 *
 * Wiring:
 *   DHT22 DATA → GPIO 4  (+ 10 kΩ pull-up to 3.3 V)
 *   Relay IN   → GPIO 26
 *
 * Library: "DHT sensor library" by Adafruit (Library Manager)
 */
#include <DHT.h>

#define DHT_PIN        4
#define DHT_TYPE       DHT22
#define RELAY_PIN      26
#define TEMP_THRESHOLD 25.0f   // Change this to your desired cut-in temp (°C)

DHT dht(DHT_PIN, DHT_TYPE);

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW);   // Relay OFF at boot
  dht.begin();
  Serial.printf("Thermostat started — threshold: %.1f °Cn", TEMP_THRESHOLD);
}

void loop() {
  delay(5000);   // DHT22 min sample interval: 2 s; 5 s is safe

  float h = dht.readHumidity();
  float t = dht.readTemperature();   // Default: Celsius

  if (isnan(h) || isnan(t)) {
    Serial.println("Sensor read failed — check wiring and pull-up resistor.");
    return;
  }

  Serial.printf("Temp: %.1f °C  |  Hum: %.1f %%  |  ", t, h);

  if (t >= TEMP_THRESHOLD) {
    digitalWrite(RELAY_PIN, HIGH);
    Serial.println("Relay ON  (cooling active)");
  } else {
    digitalWrite(RELAY_PIN, LOW);
    Serial.println("Relay OFF");
  }
}
How It Works
01

DHT22 Protocol: The DHT22 uses a single-wire protocol requiring a 10 kΩ pull-up resistor to keep the data line HIGH when idle. Without it, readings fail. The library handles all timing automatically.

02

5-Second Sample Interval: delay(5000) prevents reading the sensor faster than its 2-second minimum interval, which causes "nan" errors. It also gives the ESP32 CPU a rest during which it draws less power.

03

isnan() Guard: If a read fails (bad wiring, noise), the library returns NaN (Not a Number). The isnan() check catches this and skips the bad sample instead of accidentally switching the relay on corrupted data.

04

Threshold Comparison: A simple >= comparison decides relay state. Adjust TEMP_THRESHOLD once at the top of the file. The relay activates for cooling loads (fan, AC) when above threshold, or for heating loads (heater) when below — swap HIGH/LOW in the if/else branches to invert.

05

Active-HIGH vs Active-LOW Relays: Most relay modules with an optocoupler activate when IN is HIGH. Some older "active-LOW" modules activate when IN is LOW. If your relay clicks on at boot or behaves inverted, swap HIGH and LOW in the sketch and set the initial state accordingly.

Applications
  • Automatic fan controller for 3D printer enclosure or electronics cabinet
  • Server rack over-temperature emergency shutdown
  • Reptile vivarium basking-spot heat lamp controller
  • Home brewing fermentation temperature regulation
  • Seedling germination heating mat controller
Troubleshooting

Serial Monitor prints "Sensor read failed" on every cycle

1) Verify DHT22 is on 3.3 V, not 5 V. 2) Confirm the 10 kΩ pull-up is between DATA and 3.3 V. 3) Try a 4.7 kΩ resistor — some clone sensors prefer it. 4) Check the DHT_PIN definition matches your physical GPIO.

Relay chatters ON/OFF rapidly when temperature is near threshold

Add a 2 °C hysteresis band: turn ON at threshold + 1 and OFF at threshold - 1. This avoids the relay toggling every sample when temperature hovers at the setpoint.

Temperature reads 5–10 °C higher than actual room temperature

The ESP32 radiates heat during operation. Move the DHT22 at least 5 cm away from the board. If using Wi-Fi (later levels), the RF transceiver adds additional heat; enclosing both in a box makes this worse.

Upgrades
  • Add hysteresis (2 °C dead-band) to prevent relay chattering
  • Wire an LED indicator so relay state is visible without a computer
  • Add a push button on GPIO 27 to force-override the relay manually
  • Log readings to SD card for a week of temperature history
FAQ

You need an ESP32 DevKit, DHT22 Pin 2 (DATA), Relay IN, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

At the intermediate level you add a 0.96-inch OLED display for local status readout and a Wi-Fi web dashboard so you can monitor readings and change the setpoint from any browser on your network — no app required, no cloud required, just a local IP address.

The dashboard auto-refreshes every 3 seconds using a small JavaScript fetch() loop. A number input lets you type a new threshold; the ESP32 saves it instantly to NVS flash via the Preferences library so the setting survives power cuts. The OLED shows temperature, humidity, relay state, and current threshold locally even when no browser is open.

You will learn: the ESP32 WebServer library, serving HTML/CSS/JS from PROGMEM, handling HTTP GET routes, building a JSON data endpoint, and storing persistent settings without needing an EEPROM chip. You will also learn non-blocking timing with millis() so the web server stays responsive while sensor reads happen in the background.

Components
  • 1× ESP32 DevKit V1
  • 1× DHT22 Sensor + 10 kΩ resistor — Same wiring as beginner level
  • 1× 5 V Relay Module
  • 1× 0.96" OLED Display (SSD1306, I2C) — 128×64 px, I2C address 0x3C (most common)
  • 1× Jumper wires
Wiring
Component PinESP32 PinNotes
DHT22 DATAGPIO 410 kΩ pull-up to 3.3 V
Relay INGPIO 26
OLED SDAGPIO 21I2C data — default ESP32 I2C bus
OLED SCLGPIO 22I2C clock
OLED VCC3.3 V
OLED GNDGND
Arduino Code
esp32-smart-thermostat_intermediate.ino
/*
 * ESP32 Smart Thermostat — Intermediate
 * Adds: OLED display, Wi-Fi web dashboard, persistent setpoint (NVS)
 * Libraries: DHT (Adafruit), Adafruit_SSD1306, Adafruit_GFX
 *            WebServer & Preferences (ESP32 built-in)
 */
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <DHT.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ── CONFIGURE THESE ──────────────────────────────────────────────
const char* SSID     = "YOUR_WIFI_SSID";
const char* PASSWORD = "YOUR_WIFI_PASSWORD";
// ────────────────────────────────────────────────────────────────

#define DHT_PIN   4
#define DHT_TYPE  DHT22
#define RELAY_PIN 26

DHT          dht(DHT_PIN, DHT_TYPE);
WebServer    server(80);
Preferences  prefs;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);

float g_temp = 0, g_hum = 0, g_threshold = 25.0f;
bool  g_relay = false;

/* ── Dashboard HTML (stored in flash) ─────────────────────────── */
const char HTML[] PROGMEM = R"HTML(<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32 Thermostat</title>
<style>
 body{font-family:sans-serif;background:#0f172a;color:#e2e8f0;margin:0;padding:20px}
 h1{color:#60a5fa;margin-bottom:20px}
 .card{background:#1e293b;border-radius:12px;padding:24px;margin:12px 0;max-width:420px}
 .big{font-size:3.5rem;font-weight:700;color:#60a5fa;line-height:1}
 .sub{color:#94a3b8;margin-top:6px}
 .on{color:#4ade80}.off{color:#f87171}
 input[type=number]{background:#334155;border:1px solid #475569;color:#e2e8f0;
   padding:8px 12px;border-radius:8px;width:90px;font-size:1rem}
 button{background:#2563eb;color:#fff;border:none;padding:10px 20px;
   border-radius:8px;cursor:pointer;font-size:1rem;margin-left:10px}
 button:hover{background:#1d4ed8}
</style></head><body>
<h1>ESP32 Thermostat</h1>
<div class="card">
 <div class="big" id="t">--</div>
 <div class="sub" id="h">Humidity: --%</div>
 <div class="sub" id="r" style="margin-top:10px">Relay: --</div>
</div>
<div class="card">
 <label>Setpoint (°C):
  <input type="number" id="sp" step="0.5" min="5" max="45">
 </label>
 <button onclick="save()">Save</button>
</div>
<script>
function refresh(){
 fetch("/data").then(r=>r.json()).then(d=>{
  document.getElementById("t").textContent=d.temp.toFixed(1)+"°C";
  document.getElementById("h").textContent="Humidity: "+d.hum.toFixed(1)+"%";
  const r=document.getElementById("r");
  r.textContent="Relay: "+(d.relay?"ON — cooling active":"OFF");
  r.className=d.relay?"on":"off";
  document.getElementById("sp").value=d.threshold;
 });
}
function save(){
 const v=document.getElementById("sp").value;
 fetch("/set?threshold="+v).then(refresh);
}
setInterval(refresh,3000);refresh();
</script></body></html>)HTML";

void updateOLED() {
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(2); oled.setCursor(0,0);
  oled.printf("%.1fC", g_temp);
  oled.setTextSize(1); oled.setCursor(0,20);
  oled.printf("Hum: %.1f%%", g_hum);
  oled.setCursor(0,32);
  oled.print(g_relay ? "RELAY: ON " : "RELAY: OFF");
  oled.setCursor(0,44);
  oled.printf("Set: %.1fC", g_threshold);
  oled.display();
}

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW);
  dht.begin();
  Wire.begin();
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  oled.clearDisplay(); oled.display();

  prefs.begin("thermo", false);
  g_threshold = prefs.getFloat("sp", 25.0f);

  WiFi.begin(SSID, PASSWORD);
  Serial.print("Wi-Fi connecting");
  while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); }
  Serial.printf("nDashboard: http://%sn", WiFi.localIP().toString().c_str());

  server.on("/", [](){
    server.send_P(200, "text/html", HTML);
  });
  server.on("/data", [](){
    String j = "{"temp":" + String(g_temp,1) +
               ","hum":"  + String(g_hum,1)  +
               ","relay":" + (g_relay?"true":"false") +
               ","threshold":" + String(g_threshold,1) + "}";
    server.send(200, "application/json", j);
  });
  server.on("/set", [](){
    if (server.hasArg("threshold")) {
      g_threshold = server.arg("threshold").toFloat();
      prefs.putFloat("sp", g_threshold);
    }
    server.send(200, "text/plain", "OK");
  });
  server.begin();
}

void loop() {
  server.handleClient();
  static unsigned long last = 0;
  if (millis() - last >= 5000) {
    last = millis();
    float h = dht.readHumidity(), t = dht.readTemperature();
    if (!isnan(h) && !isnan(t)) {
      g_temp = t; g_hum = h;
      g_relay = (t >= g_threshold);
      digitalWrite(RELAY_PIN, g_relay ? HIGH : LOW);
      updateOLED();
    }
  }
}
How It Works
01

Non-blocking Loop: server.handleClient() is called on every loop iteration to process incoming HTTP requests. Sensor reading uses a millis() timer instead of delay() so the web server never blocks mid-request.

02

PROGMEM HTML: The HTML/CSS/JS dashboard is stored in flash memory using the PROGMEM keyword, not RAM. With 520 KB RAM on the ESP32 this matters less than on Arduino, but the pattern keeps heap free for the web server buffers.

03

JSON Data Endpoint: GET /data returns a small JSON string. The browser JavaScript calls this every 3 seconds and updates the DOM without reloading the page. This pattern (separate HTML and API endpoints) is the same approach used by full-stack web applications.

04

Persistent Setpoint: Preferences wraps ESP32's built-in NVS (Non-Volatile Storage) flash. putFloat("sp", value) writes in under 1 ms with no wear concern for this use case (100,000+ write cycles rated). The threshold survives firmware resets, power cuts, and watchdog reboots.

05

OLED Update: updateOLED() clears the display buffer, renders four lines of text at two different font sizes, then calls display() to push the buffer to the screen via I2C. Calling display() is the expensive step; always buffer all drawing before committing.

Applications
  • Field trial with visible OLED feedback
  • Manual override for maintenance or testing
  • Calibrated setup for daily use
Troubleshooting

Cannot reach the dashboard in a browser

1) Confirm SSID and PASSWORD are correct. 2) Check Serial Monitor for the assigned IP. 3) Ensure your computer is on the same network (2.4 GHz — ESP32 does not support 5 GHz). 4) Temporarily disable Windows Firewall to rule out port blocking.

OLED shows nothing or garbled output

Run an I2C scanner sketch to find your display's address. Some SSD1306 modules use 0x3D instead of 0x3C. Change the address in oled.begin(SSD1306_SWITCHCAPVCC, 0x3D).

Setpoint resets to 25 °C after every restart

The Preferences namespace must match between putFloat and getFloat. Both must use "thermo" (the first argument to prefs.begin()). If you cloned from beginner and renamed the namespace, the stored key is not found and falls back to 25.0.

Upgrades
  • Assign a static IP or use mDNS (WiFi.setHostname("thermostat")) to access via http://thermostat.local
  • Add a 7-day temperature history graph using Chart.js fetched from a /history endpoint
  • Add a second relay for heating so the board can control both cooling and heating automatically
FAQ

You need an ESP32 DevKit, DHT22 Pin 2 (DATA), Relay IN, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The advanced level turns your thermostat into a proper smart-home device: it publishes temperature, humidity, and relay state to an MQTT broker every 30 seconds; subscribes to a command topic so automations in Home Assistant, Node-RED, or Apple HomeKit (via Homebridge) can change the setpoint remotely; announces itself as unavailable via a Last Will Testament if it drops offline; and supports over-the-air firmware updates so you never need to physically access the device again.

You will integrate with Home Assistant using the standard MQTT sensor platform, and optionally enable MQTT Auto-Discovery so the thermostat appears in Home Assistant automatically without any YAML configuration. The sketch also adds an NTP time client so log messages carry real timestamps for debugging.

What you will learn: MQTT publish/subscribe patterns, Last Will Testament (LWT), MQTT retain flags, ArduinoOTA for wireless firmware updates, NTP time synchronisation, JSON payload design, Home Assistant integration.

Components
  • 1× ESP32 DevKit V1
  • 1× DHT22 + 10 kΩ resistor
  • 1× 5 V Relay Module
  • 1× 0.96" OLED (SSD1306) — Optional — shows MQTT connection status locally
  • 1× MQTT Broker — Mosquitto on Raspberry Pi, or cloud broker (HiveMQ free tier)
Wiring
Component PinESP32 PinNotes
DHT22 DATAGPIO 410 kΩ pull-up to 3.3 V
Relay INGPIO 26
OLED SDA / SCLGPIO 21 / 22Optional
Arduino Code
esp32-smart-thermostat_advanced.ino
/*
 * ESP32 Smart Thermostat — Advanced
 * MQTT publish/subscribe + ArduinoOTA + NTP timestamps
 * Libraries: DHT (Adafruit), PubSubClient (Nick O'Leary), ArduinoOTA (built-in)
 */
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoOTA.h>
#include <Preferences.h>
#include <DHT.h>
#include <time.h>

const char* SSID      = "YOUR_WIFI_SSID";
const char* PASSWORD  = "YOUR_WIFI_PASSWORD";
const char* BROKER    = "192.168.1.100";   // Your broker IP
const int   MQTT_PORT = 1883;
const char* DEV_ID    = "thermostat-esp32-01";

#define DHT_PIN   4
#define DHT_TYPE  DHT22
#define RELAY_PIN 26
#define PUB_MS    30000UL   // Publish interval

// Topic helpers
String T(const char* sub) { return String("esp32engine/thermostat/") + DEV_ID + "/" + sub; }

DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient net;
PubSubClient mqtt(net);
Preferences prefs;

float g_temp=0,g_hum=0,g_threshold=25.0f;
bool  g_relay=false;

void onMqttMessage(char* topic, byte* pl, unsigned int len) {
  String msg; for(unsigned int i=0;i<len;i++) msg+=(char)pl[i];
  if(String(topic)==T("set/threshold")){
    float v=msg.toFloat();
    if(v>0&&v<60){ g_threshold=v; prefs.putFloat("sp",v); }
  }
  if(String(topic)==T("set/relay")){
    // Manual override: "ON" or "OFF"
    bool on=(msg=="ON"||msg=="1");
    g_relay=on;
    digitalWrite(RELAY_PIN,on?HIGH:LOW);
  }
}

void mqttConnect(){
  while(!mqtt.connected()){
    if(mqtt.connect(DEV_ID,"","",T("avail").c_str(),0,true,"offline")){
      mqtt.publish(T("avail").c_str(),"online",true);
      mqtt.subscribe(T("set/threshold").c_str());
      mqtt.subscribe(T("set/relay").c_str());
      Serial.println("MQTT connected");
    } else {
      Serial.printf("MQTT rc=%d retry in 5sn",mqtt.state());
      delay(5000);
    }
  }
}

void publishState(){
  char ts[20]="";
  struct tm ti; if(getLocalTime(&ti)) strftime(ts,sizeof(ts),"%H:%M:%S",&ti);
  String j = "{"temperature":" + String(g_temp,2) +
             ","humidity":"     + String(g_hum,2)  +
             ","relay":"        + (g_relay?"true":"false") +
             ","threshold":"    + String(g_threshold,1) +
             ","time":"" + ts + ""}";
  mqtt.publish(T("state").c_str(), j.c_str(), true);
}

void setup(){
  Serial.begin(115200);
  pinMode(RELAY_PIN,OUTPUT); digitalWrite(RELAY_PIN,LOW);
  dht.begin();
  prefs.begin("thermo",false);
  g_threshold=prefs.getFloat("sp",25.0f);

  WiFi.begin(SSID,PASSWORD);
  while(WiFi.status()!=WL_CONNECTED) delay(400);
  Serial.printf("IP: %sn",WiFi.localIP().toString().c_str());

  configTime(0,0,"pool.ntp.org");   // UTC — adjust offset for your timezone

  ArduinoOTA.setHostname(DEV_ID);
  ArduinoOTA.begin();

  mqtt.setServer(BROKER,MQTT_PORT);
  mqtt.setCallback(onMqttMessage);
  mqtt.setKeepAlive(60);
  mqttConnect();
}

void loop(){
  ArduinoOTA.handle();
  if(!mqtt.connected()) mqttConnect();
  mqtt.loop();

  static unsigned long last=0;
  if(millis()-last>=PUB_MS){
    last=millis();
    float h=dht.readHumidity(),t=dht.readTemperature();
    if(!isnan(h)&&!isnan(t)){
      g_temp=t; g_hum=h;
      bool shouldBeOn=(t>=g_threshold);
      if(shouldBeOn!=g_relay){
        g_relay=shouldBeOn;
        digitalWrite(RELAY_PIN,g_relay?HIGH:LOW);
      }
      publishState();
    }
  }
}
How It Works
01

MQTT Topic Hierarchy: All topics follow the pattern esp32engine/thermostat/{device-id}/{sub-topic}. Using a device ID in the path allows multiple thermostats on the same broker without topic collisions. The /state topic carries the full JSON payload; /avail carries "online"/"offline".

02

Last Will Testament (LWT): The MQTT CONNECT packet includes a LWT message: if the TCP connection drops unexpectedly (power cut, network loss), the broker automatically publishes "offline" to the /avail topic. Home Assistant uses this to mark the device as unavailable on its dashboard.

03

Retained Messages: mqtt.publish(..., true) sets the RETAIN flag. The broker caches the last retained message per topic and sends it immediately to any new subscriber. This means Home Assistant receives current temperature the instant it connects — even if the ESP32 last published 25 minutes ago.

04

ArduinoOTA: ArduinoOTA.begin() starts an mDNS service and a TCP listener for firmware updates. Once running, the device appears as a network port in Arduino IDE. Select it and upload as normal. The bootloader handles the swap safely — on failure it rolls back to the previous firmware.

05

NTP Timestamps: configTime() syncs the ESP32 real-time clock with pool.ntp.org. getLocalTime() fills a tm struct for formatting. Published JSON payloads include a "time" field, which makes Grafana and InfluxDB time-series graphs accurate even across broker restarts.

Applications
  • Remote monitoring from phone or laptop
  • Automated alerts when limits are crossed
  • Long-term trend logging for optimization
Troubleshooting

MQTT connection fails with state -2

rc=-2 means the broker was not found on the network. Verify BROKER IP and that Mosquitto is running: sudo systemctl status mosquitto. Test from another device: mosquitto_pub -h BROKER_IP -t test -m hello

OTA upload fails — port not visible in Arduino IDE

ESP32 must be powered and running the loop (not in a crash loop). Ensure your PC and the ESP32 are on the same subnet. Temporarily disable OS firewall. Check that ArduinoOTA.handle() is called on every loop iteration.

Home Assistant shows "unavailable" even though device is running

Check the LWT topic string matches what HA is subscribed to. If the broker was restarted, the ESP32 may not have reconnected yet — restart the ESP32 or wait for the MQTT keep-alive timeout to trigger reconnection.

Upgrades
  • Add MQTT Auto-Discovery: publish a config payload to homeassistant/sensor/{id}/config and HA adds the device automatically
  • Replace simple ON/OFF with a PID controller for smoother temperature regulation with no overshoot
  • Add multi-zone support: run multiple ESP32 thermostats reporting to one broker, view all zones in one HA dashboard
FAQ

You need an ESP32 DevKit, DHT22 Pin 2 (DATA), Relay IN, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.