Postlad/Guides/Projects

Build a WiFi room monitor with an ESP32 and a DHT22

One ESP32, one DHT sensor, three wires, and about fifteen minutes. What you end up with is the temperature and humidity of a room drawn on a live chart at a link you can send to somebody, without making them sign up for anything. Under $15 of parts, and the same pattern carries every other sensor you are likely to buy next.

Postlad is live. api.postlad.com answers today, so the sketch below sends to a real endpoint. What you need first is a write key: create a free account, click the link we email you, and it is waiting on your streams page.


What you end up with

Two lines on one chart: temperature in degrees Celsius and relative humidity as a percentage, a new point every 30 seconds. The chart has a share link that a viewer opens with no account, an embed snippet if you want it in a write-up, and a CSV export button when you want the numbers out.

The free tier keeps every reading at full detail for 30 days, then an hourly minimum, maximum and average for 12 months. That is what makes this project worth leaving plugged in: next February you can look at what the room actually did last winter, rather than at the last week of it.

Parts

Part Rough price Notes
ESP32 dev board $5 to $8 A classic WROOM-32 DevKit is the safe buy. Any ESP32 with a USB socket works.
DHT11 or DHT22 sensor $1 to $2, or $4 to $5 Buy the 3-pin module rather than the bare 4-pin part if you have the choice.
Breadboard and jumper wires about $5 Female-to-male jumpers let you skip the breadboard if the sensor is a module.
USB cable and a phone charger probably in a drawer Any 5 V charger. The board wants about 500 mA at its noisiest.

Rough street prices in August 2026 from the usual marketplaces. They move, and a starter kit with all of it in one box is often cheaper than the parts separately.

DHT11 or DHT22, honestly

They speak the same protocol, use the same library, and need the same three wires. The sketch on this page runs either one with a single word changed. The difference is what the numbers are worth.

DHT11 DHT22 (AM2302)
Temperature range0 to 50 °C-40 to 80 °C
Temperature accuracy±2 °C±0.5 °C
Humidity range20 to 80 %RH0 to 100 %RH
Humidity accuracy±5 %RH±2 to 5 %RH
Resolution1 °C, 1 %RH (whole numbers)0.1 °C, 0.1 %RH
Fastest sample rateone reading per secondone reading per two seconds
Typical price$1 to $2$4 to $5

Either sensor answers "is this room stuffy" perfectly well, and if a DHT11 came in a starter kit you already own, use it. Buy the DHT22 when the number itself is the point: anything that goes below zero, a loft or a greenhouse in winter, a fridge, or any comparison between two rooms where 2 °C of sensor error would be the whole answer. A DHT11 reporting whole degrees also draws a very flat, stepped chart, which is a small thing until you have looked at it for a week.

Neither one is a lab instrument. Both drift, both dislike condensation, and both read a couple of degrees high if you mount them touching the ESP32's voltage regulator.

The circuit

Three connections. Power, ground, and one data line.

An ESP32 wired to a DHT22 sensor on a breadboard A pictorial wiring diagram. An ESP32 dev board sits on the left of a breadboard with a header strip along its right edge, and a DHT22 sensor module sits on the right. Three wires run between them: the ESP32's 3V3 pin to the sensor's VCC pin, the ESP32's GPIO 4 pin to the sensor's DATA pin, and the ESP32's GND pin to the sensor's GND pin. Each wire carries its own label. A note below reads: bare four-pin sensors need a 10 kilohm resistor between DATA and 3V3, and most modules already have one. ESP32 and DHT22 on a breadboard ESP32 dev board 3V3 GPIO 4 GND DHT22 or DHT11 3V3 to VCC GPIO 4 to DATA GND to GND Bare 4-pin sensor? Add a 10 kΩ resistor between DATA and 3V3. Most modules already have one.
Three wires, and that is the whole circuit. The pin names are what matter, not where they sit on your board.
DHT pinESP32 pin
VCC (+)3V3
DATA (out)GPIO 4
GND (-)GND

The same circuit, drawn the way a datasheet would draw it. If you have never read a schematic before, this is a gentle one: a line means a wire, a dot means two wires joined, and the little stack of bars at the bottom means ground.

Schematic of an ESP32 connected to a DHT22 A schematic. Two boxes, ESP32 on the left and DHT22 or DHT11 on the right, joined by three horizontal nets: 3V3 to VCC, GPIO 4 to DATA, and GND to GND. A dashed branch runs from the DATA net up to the 3V3 net through a 10 kilohm resistor, marked as needed only for bare sensors. The GND net carries a ground symbol. The same three connections, as a schematic ESP32 dev board, 3.3 V logic DHT22 or DHT11 3V3 GPIO 4 GND VCC DATA GND 10 kΩ Both boards share one 3.3 V supply and one ground. Nothing else is connected. The dashed 10 kΩ pull-up is only for bare 4-pin sensors. Modules have one fitted.
The dashed branch is the pull-up resistor, and on a 3-pin module it is already fitted.

About the pull-up. The data line needs a resistor of about 10 kΩ to 3V3 to hold it high between bits. Almost every 3-pin DHT module has one fitted on its little PCB already, and adding a second does no harm but is not needed. A bare 4-pin sensor has nothing fitted, so put a 10 kΩ resistor between the DATA pin and 3V3 yourself. If you are not sure which you have: a module with three pins and a small circuit board has it, a naked blue or white plastic brick with four pins does not.

Use 3V3, not VIN. The ESP32's GPIO pins are 3.3 V parts. Both sensors will happily run from 5 V and then answer on a 5 V data line, which is a slow way to damage a pin. Take power from 3V3 and everything on the wire stays at 3.3 V.

Wherever those pins sit on your board. ESP32 dev boards do not agree on pin order, so read the silkscreen rather than counting from a picture. You want the pin marked 3V3 (sometimes 3.3V), any pin marked GND, and any free GPIO for the data line. GPIO 4 is a good default; avoid GPIO 0, 2, 12 and 15, which the boot process cares about.

Unplug the board while you wire it. Not because a DHT22 is dangerous, but because a jumper that lands in the wrong hole while the power is on is how GPIO pins die.

The sketch

Libraries: install DHT sensor library by Adafruit from the Arduino IDE's Library Manager, and accept the prompt to install Adafruit Unified Sensor alongside it. WiFi.h and HTTPClient.h already ship with the ESP32 core, so there is nothing else to add.

The whole cloud half of this project is one line, and it is the URL:

cpp
String url = String("http://api.postlad.com/u?k=") + WRITE_KEY
           + "&f1=" + String(temperature, 1)
           + "&f2=" + String(humidity, 1);

f1 and f2 are the two field slots on your stream. Both readings go up in one request, so they share a timestamp and cost one point against your monthly quota rather than two.

Reviewed, not yet bench-tested on this exact board and sensor. If it misbehaves for you, tell us and we'll fix the page.

cpproom_monitor.ino
#include <WiFi.h>
#include <HTTPClient.h>
#include <DHT.h>

// ---- fill these in -------------------------------------------------------
const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* WRITE_KEY     = "YOUR_KEY";
// --------------------------------------------------------------------------

// Plain HTTP on purpose: api.postlad.com answers it directly and never
// redirects, so the board needs no TLS stack at all. https:// works too.
const char* HOST_URL = "http://api.postlad.com/u";

// 30 s. The free tier's floor is one reading every 5 s, but its other limit is
// 100,000 stored points a month, which works out at one reading every 26 s
// sustained. 30 s runs all month with room to spare, and the sensor cannot be
// read faster than once every 2 s anyway.
const unsigned long SEND_INTERVAL_MS = 30000;

#define DHTPIN  4
#define DHTTYPE DHT22          // change to DHT11 if that is the sensor you have
DHT dht(DHTPIN, DHTTYPE);

WiFiClient client;             // one plain TCP client, reused for every request

void connectWiFi() {
  Serial.printf("Connecting to %s", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nConnected. IP address: %s\n", WiFi.localIP().toString().c_str());
}

void setup() {
  Serial.begin(115200);
  delay(200);
  dht.begin();
  delay(2000);                 // the sensor needs a moment after power-up
  connectWiFi();
}

void loop() {
  float temperature = dht.readTemperature();   // °C; readTemperature(true) for °F
  float humidity    = dht.readHumidity();

  if (isnan(temperature) || isnan(humidity)) {
    Serial.println("DHT read failed - check the wiring and the pull-up");
    delay(2000);
    return;
  }

  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  HTTPClient http;
  String url = String(HOST_URL) + "?k=" + WRITE_KEY
             + "&f1=" + String(temperature, 1)
             + "&f2=" + String(humidity, 1);

  if (http.begin(client, url)) {
    int status = http.GET();
    if (status > 0) {
      // A stored point answers 200 with the plain text "ok 2": two fields taken.
      Serial.printf("%.1f C  %.1f %%RH  -> HTTP %d %s\n",
                    temperature, humidity, status, http.getString().c_str());
    } else {
      Serial.printf("request failed: %s\n", http.errorToString(status).c_str());
    }
    http.end();
  } else {
    Serial.println("http.begin() failed");
  }

  delay(SEND_INTERVAL_MS);
}

Open the serial monitor at 115200 baud. After the dotted connection line you should get one line every 30 seconds:

text
21.4 C  47.8 %RH  -> HTTP 200 ok 2
21.4 C  47.9 %RH  -> HTTP 200 ok 2
21.5 C  48.1 %RH  -> HTTP 200 ok 2

ok 2 is the whole response: the point was stored, and it took two fields. Anything else is worth reading rather than ignoring. error bad_key means the key in the sketch is not one we know, error no_fields means the URL arrived with nothing numeric on it, and a negative number where the HTTP status should be is a client-side failure rather than an answer from the server at all.

Open your stream page and the chart already has points on it.

Name the fields

Out of the box the chart calls your two lines f1 and f2, which is fine for ten minutes and useless a week later. On the stream page, open the field settings and give each slot a name, a unit and a number of decimal places:

Slot Name Unit Decimals
f1Temperature°C1
f2Humidity%0

Decimals are a display setting, not a storage one. The sketch sends humidity with one decimal place and that is what is kept; showing it to zero places just stops the chart from claiming a precision the sensor does not have.

If you already have a pile of readings in a CSV, the importer sets the names for you: upload the file, and the header row becomes your field names and units, with the old readings landing at their original timestamps rather than all at the moment you uploaded them.

Where to go from here

The pattern does not change when the sensor does: read a number, put it in an f slot, send the URL. A stream carries up to 16 fields, f1 through f16, so one board in one room can report a great deal more than temperature and humidity before you need a second stream. The quota counts readings, not fields, so a request carrying six numbers costs exactly what a request carrying one costs.

DS18B20, for temperature where water is. A waterproof stainless probe on a lead, about $3, and the one to use for a hot tank, a pond, a compost heap, or anything outside in the rain. It is a 1-Wire part: VCC to 3V3, GND to GND, DATA to a free GPIO, and a 4.7 kΩ resistor between DATA and 3V3, which is required here rather than optional. The OneWire and DallasTemperature libraries do the talking, and several probes can share one pin. Send it as f3, next to the room temperature you already have, because the difference between the two lines is usually the interesting part.

Capacitive soil moisture, for plants. The cheap $2 boards that answer with a voltage. Get the capacitive kind rather than the two-pronged resistive kind, which corrodes away after a few weeks of being wet. VCC to 3V3, GND to GND, and the analog output to an ADC pin: on an ESP32, use one of GPIO 32 to 39, because the other ADC block is unavailable while the WiFi radio is on. Send the raw reading as f4 and calibrate it yourself by noting the value in dry air and in a glass of water. The number is not a percentage, and any tutorial that presents it as one has skipped the calibration.

BME280, when you want pressure too. About $5, and the natural upgrade from a DHT: temperature, humidity and barometric pressure from one part, over I²C rather than a one-wire protocol, and more accurate than either DHT. Four wires: 3V3, GND, SDA to GPIO 21 and SCL to GPIO 22 on most dev boards. If it replaces the DHT entirely, keep temperature on f1 and humidity on f2 so your history stays continuous, and put pressure in hPa on f5. Pressure is the field that makes a room monitor start predicting the weather, because it is the trend over a few hours, rather than the value, that tells you anything.

Then put the rooms together. One stream per room, then a Board that draws them on the same chart: the bedroom against the loft, the greenhouse against outside, four flats in the same building. That is the point at which a room monitor stops being a gadget and starts answering questions.

FAQ

Should I buy a DHT11 or a DHT22?

The DHT22 if the number matters, the DHT11 if you already own one. The DHT22 is accurate to about ±0.5 °C, works below freezing, and reports tenths; the DHT11 is accurate to about ±2 °C, stops at 0 °C, and reports whole numbers. For $3 more, the DHT22 removes a whole class of "is that real, or is that the sensor" questions, which is the only reason to prefer it.

Why are my DHT readings NaN, or obviously wrong?

NaN means the read failed, and the causes are dull in a helpful way: no pull-up resistor on a bare sensor, a jumper in the wrong breadboard hole, the wrong GPIO number in the sketch, or the sensor being read faster than once every two seconds. If readings arrive but look wrong, check where the sensor is sitting. Mounted on top of the ESP32 it reads two or three degrees high, because the board's regulator is a small heater, and a sensor in direct sun measures the sun.

How often should an ESP32 send temperature readings?

Every 30 seconds is a good default for a room, and it is what the sketch does. Room temperature does not move quickly, the free tier's 100,000 points a month works out at one reading every 26 seconds sustained, and the sensor itself cannot be read faster than once every two seconds. If you want faster sampling for something that does move quickly, sample on the board and send the average.

Can I run this on a battery?

Not well, if you leave it as written. An ESP32 with its radio on draws roughly 80 to 150 mA, which empties a decent power bank in about a day. The fix is deep sleep between readings, waking every 10 or 15 minutes rather than every 30 seconds, which turns hours into weeks. It works far better on a bare ESP32 module than on a dev board, whose USB chip and regulator keep drawing current while the chip itself sleeps. For a room monitor indoors, a USB charger is the honest answer, and battery operation is a project of its own.

Should I use one stream with lots of fields, or one stream per sensor?

One stream per device, with the sensors on that device sharing its fields. A stream holds up to 16 fields and the quota counts readings rather than fields, so one request with six numbers on it is cheaper and tidier than six requests. Use separate streams for separate places, which is also what the free tier's two streams push you towards, and use a Board when you want two places on one chart.

Create your free account

The endpoint this sketch points at is live: one-URL writes, a live chart, a share link that needs no account, and 30 days at full detail plus hourly summaries for 12 months on the free tier. What you need is a write key. Enter an email, click the link we send back, and it is waiting on your streams page.

No password, no credit card. If you are pointing the sketch at something else in the meantime, the wiring and the code above work either way.

Create your free account →