ESP32 MicroPython Weather Web Server: Live DHT22 Readings with Auto-Refresh
An ESP32 can host a small web page that displays temperature and relative humidity measured by a DHT22 sensor. A phone or computer on the same local network opens the ESP32's address, and JavaScript refreshes the displayed values without reloading the entire page. This is a useful step between reading a sensor in the REPL and building a connected monitoring project.
This SolderHub guide covers wiring, MicroPython setup, a lightweight HTTP server, a JSON endpoint, a browser dashboard, sensible sensor polling intervals, and troubleshooting. The example is intended for a trusted local network, not direct exposure to the public internet. A simple web server without authentication should not be forwarded through a router or placed on an untrusted network.
1. Parts and prerequisites
You need an ESP32 development board, a DHT22/AM2302 sensor or module, jumper wires, a USB data cable, and MicroPython firmware compatible with your exact ESP32 variant. Use Thonny or another serial REPL tool to install and run scripts. The computer used for setup and the ESP32 should be connected to the same Wi-Fi network for the simplest test.
A bare DHT22 sensor often requires a pull-up resistor on the data line; many breakout modules include one. Check the specific module. Power the sensor according to its datasheet and the module design. ESP32 GPIO is 3.3 V logic: do not feed 5 V into a GPIO. If a sensor module is powered from 5 V and its data pull-up also rises to 5 V, that output can damage the ESP32. Prefer a module arrangement whose data signal is pulled up to 3.3 V or use a suitable level-shifting circuit.
DHT22 readings are relatively slow compared with many digital sensors. Do not poll the device continuously at browser refresh speed. The server should cache the latest successful reading and refresh the sensor at a reasonable interval.
2. Wiring the DHT22
For a common four-pin bare DHT22, identify VCC, DATA, NC, and GND from the exact datasheet and package orientation. Pin order can vary between bare sensors and modules, so never rely only on the shape of the part. Connect VCC to a suitable supply, GND to ESP32 GND, and DATA to a GPIO that is available on your board. The example below uses GPIO 4, but you must verify that this pin is safe and exposed on your specific board.
If your bare sensor needs an external pull-up, connect a suitable resistor between DATA and 3.3 V according to the sensor documentation. A common value is around 4.7–10 kΩ, but follow the sensor and module specifications. Keep wiring short while testing. Do not connect DATA to a pin that is reserved for flash, strapping, or other onboard functions on your particular board.
Before connecting USB, check for shorts between supply and ground. If readings fail, disconnect power and recheck pin orientation and common ground before changing software.
3. Install and test the sensor driver
MicroPython firmware does not necessarily include every sensor driver. Use a DHT driver supported by your firmware; the standard MicroPython distribution commonly provides the dht module on supported builds. Test the sensor independently before adding networking.
import dht
from machine import Pin
from time import sleep
sensor = dht.DHT22(Pin(4)) # verify GPIO 4 for your board
while True:
try:
sensor.measure()
print("Temperature C:", sensor.temperature())
print("Humidity %:", sensor.humidity())
except OSError as exc:
print("DHT read failed:", exc)
sleep(3)
The pause is deliberate. DHT22 devices need a minimum interval between measurements; follow the datasheet and driver documentation, typically allowing at least about two seconds between readings. A read can fail due to timing, wiring, supply stability, or a sensor that has not settled. Catching OSError lets the test continue rather than terminating at the first failed measurement.
Do not interpret one successful reading as proof of accuracy. Compare the sensor in a stable environment and understand that inexpensive sensors have specified tolerances and response times. Avoid placing it against a warm regulator, in direct sunlight, or where condensation can form.
4. Connect the ESP32 to Wi-Fi
Use station mode to join an existing access point. Replace the placeholders with local credentials; do not publish real passwords in a public repository or tutorial screenshot.
import network
import time
SSID = "YOUR_WIFI_NAME"
PASSWORD = "YOUR_WIFI_PASSWORD"
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(SSID, PASSWORD)
deadline = time.ticks_add(time.ticks_ms(), 15000)
while not wlan.isconnected() and time.ticks_diff(deadline, time.ticks_ms()) > 0:
time.sleep_ms(250)
if wlan.isconnected():
print("ESP32 IP:", wlan.ifconfig()[0])
else:
raise RuntimeError("Wi-Fi connection timed out")
A bounded wait is better than an infinite loop because a wrong password or unavailable access point should not hang the board forever. For a deployed device, implement a recovery policy rather than crashing permanently: retry with a delay, show an offline status, or restart according to the needs of the application. Most ESP32 Wi-Fi projects use 2.4 GHz networks; confirm the capabilities of your exact board and router.
5. Build the HTTP server and cached reading
The following example uses a small blocking socket server to keep the core concept easy to inspect. It caches a measurement and refreshes the sensor only when the minimum interval has passed. This avoids taking a new DHT22 reading for every browser request. Save the file to the ESP32 as main.py after testing, or use a separate filename while developing.
import network, socket, time, dht, ujson
from machine import Pin
SSID = "YOUR_WIFI_NAME"
PASSWORD = "YOUR_WIFI_PASSWORD"
sensor = dht.DHT22(Pin(4)) # verify your board pin
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
deadline = time.ticks_add(time.ticks_ms(), 15000)
while not wlan.isconnected() and time.ticks_diff(deadline, time.ticks_ms()) > 0:
time.sleep_ms(250)
if not wlan.isconnected():
raise RuntimeError("Wi-Fi unavailable")
last_read_ms = None
last_data = {"temperature_c": None, "humidity_percent": None, "ok": False, "error": "No reading yet"}
MIN_INTERVAL_MS = 2500
def update_reading():
global last_read_ms, last_data
now = time.ticks_ms()
if last_read_ms is not None and time.ticks_diff(now, last_read_ms) < MIN_INTERVAL_MS:
return
last_read_ms = now
try:
sensor.measure()
last_data = {"temperature_c": sensor.temperature(), "humidity_percent": sensor.humidity(), "ok": True, "error": None}
except OSError as exc:
last_data = {"temperature_c": None, "humidity_percent": None, "ok": False, "error": str(exc)}
addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(2)
s.settimeout(1)
print("Open http://%s/" % wlan.ifconfig()[0])
This block prepares the network, state, and listening socket. The next section adds request handling and browser output. The server is deliberately local and minimal; it is not a hardened internet-facing web framework.
6. Add a JSON endpoint and dashboard
Append the following code after the previous block. It serves /api as JSON and / as a simple page that fetches fresh cached data periodically. It sends a Content-Length header so the browser knows where the response ends.
PAGE = """<!doctype html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'><title>ESP32 Weather</title></head><body><h1>ESP32 Weather</h1><p>Temperature: <strong id='t'>--</strong> °C</p><p>Humidity: <strong id='h'>--</strong> %</p><p id='status'>Waiting for data…</p><script>
async function refresh(){
const status=document.getElementById('status');
try {
const r=await fetch('/api',{cache:'no-store'});
if(!r.ok) throw new Error('HTTP '+r.status);
const d=await r.json();
if(!d.ok) throw new Error(d.error || 'Sensor read failed');
document.getElementById('t').textContent=d.temperature_c;
document.getElementById('h').textContent=d.humidity_percent;
status.textContent='Updated locally';
} catch(e) { status.textContent='Data unavailable: '+e.message; }
}
refresh(); setInterval(refresh,5000);
</script></body></html>"""
def send_response(client, status, content_type, body):
if isinstance(body, str):
body = body.encode()
header = "HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" % (status, content_type, len(body))
client.send(header.encode())
client.send(body)
while True:
try:
client, remote = s.accept()
except OSError:
continue
try:
client.settimeout(2)
request = client.recv(1024).decode()
first = request.split("\r\n", 1)[0]
path = first.split(" ")[1] if len(first.split(" ")) > 1 else "/"
if path == "/api":
update_reading()
send_response(client, "200 OK", "application/json", ujson.dumps(last_data))
elif path == "/":
send_response(client, "200 OK", "text/html; charset=utf-8", PAGE)
else:
send_response(client, "404 Not Found", "text/plain", "Not found")
except Exception as exc:
print("Request error:", exc)
finally:
client.close()
The server is intentionally simple. A single blocking client can delay other requests, and a real application may need more robust parsing, concurrency, authentication, and request limits. Keep it on a trusted LAN.
7. Open the dashboard
Run the script and read the IP address printed in the REPL. On a phone or computer connected to the same local network, open http://ESP32_IP/, replacing the example with the address printed by your board. The page displays the latest successful temperature and humidity values. Its JavaScript calls /api every five seconds without reloading the entire document.
The browser refresh interval is intentionally longer than the sensor's minimum measurement interval. The server cache means several browser requests can receive the same recent reading rather than repeatedly polling the sensor. The ok flag lets the interface distinguish a valid measurement from an error, and the status line makes failures visible instead of silently showing stale numbers.
If the page does not load, first ping or otherwise check reachability of the ESP32 address if your network permits it. Confirm that the computer and board are on the same LAN and that client isolation is not enabled on the Wi-Fi access point. Guest networks often block communication between clients. Check the IP address again after reconnecting because DHCP can assign a different one.
8. Improve stale-data and sensor-error handling
A production dashboard should make freshness explicit. The example above records the latest attempt time but does not provide a full historical timestamp. You can extend the cached object with a last-success time and a monotonically increasing reading counter. When a read fails, retain the last good value only if the interface clearly labels it as stale; otherwise display unavailable values. Never present a failed measurement as a current, valid reading.
DHT sensors can occasionally return an error because of timing, long wires, noisy supply rails, incorrect pull-up wiring, or an overly short polling interval. Keep the data wire short, use a stable supply, share ground, and respect the sensor's sampling interval. If the sensor repeatedly fails, test the standalone sensor script first rather than debugging the web server and sensor simultaneously.
Temperature and humidity values should not be presented with false precision. Sensor tolerance is not improved by printing many decimal places. Round for display if useful, but retain enough information for the application. For a serious environmental monitor, compare readings with a known reference and account for placement, airflow, radiant heat, and condensation.
9. Security and reliability
This example has no login, TLS, or authorization. Anyone who can reach the server on the local network may be able to view the page, and a future control endpoint would create additional risk. Do not port-forward the device to the internet. For remote access, use a properly secured gateway or authenticated service rather than exposing a tiny raw socket server.
Do not embed real Wi-Fi credentials in a public repository. Store them in a local configuration file that is excluded from version control, or enter them during setup. Avoid printing passwords in logs. Consider how the device behaves when the access point goes down: a useful server should fail visibly, retry with a bounded policy, and avoid a tight loop that consumes power.
The minimal parser reads only the first request line and is suitable for a controlled learning exercise, not arbitrary hostile traffic. Keep request sizes bounded, handle malformed paths, and avoid accepting user-supplied commands. If adding relay or actuator control later, require authentication, validate every parameter, and design a safe default state for resets and network loss.
10. Troubleshooting checklist
No DHT module: confirm that your firmware includes the module or install a compatible driver. Sensor read errors: verify pin orientation, pull-up requirements, GPIO choice, supply, common ground, and minimum sampling interval. Wi-Fi timeout: check SSID, password, 2.4 GHz support, signal, and whether the network blocks embedded devices. Browser cannot connect: confirm the printed IP, same-LAN access, and client-isolation settings. Page loads but values do not change: inspect the browser status message and /api response; verify the server loop is running and sensor polling is not failing. Board resets: remove peripherals and test the sensor-only program, then reintroduce networking.
Use one diagnostic step at a time. First prove the sensor can be read locally, then prove Wi-Fi connectivity, then serve a static page, and finally add the JSON endpoint and auto-refresh. This order makes failures easier to isolate.
Final checklist and next steps
- Confirmed the exact ESP32 board and safe GPIO pin.
- Wired DHT22 with correct supply, common ground, and any required pull-up.
- Tested the sensor independently and respected its sampling interval.
- Connected to Wi-Fi with a bounded timeout and private credentials.
- Served
/and/apifrom the local network. - Cached readings instead of polling the sensor on every request.
- Displayed an explicit error state when the sensor read fails.
- Kept the unauthenticated server off the public internet.
For current details, consult the official MicroPython ESP32 and DHT documentation, the sensor datasheet, and your board's pinout. APIs and firmware modules can vary by release, so verify examples against your installed build.
SolderHub takeaway: test the sensor first, add networking second, and serve cached readings through a small JSON endpoint. Auto-refresh makes the dashboard feel live without forcing the DHT22 to sample too frequently. Keep the device on a trusted local network and make errors visible.