Skip to content

Project: Weather Station

Is it warmer inside or out? Your weather station knows. A DHT11 sensor measures the temperature and humidity in your room, the ESP32 fetches the weather outside from the internet, and the OLED shows them side by side.

Objective

  • Show indoor temperature and humidity from the DHT11, updated every 2 seconds.
  • Show outdoor temperature and humidity from the free Open-Meteo weather API, updated every 10 minutes.
  • Button A switches between °C and °F.
  • If something goes wrong (no Wi-Fi, sensor unplugged), show -- instead of crashing.

You'll need:

  • 1 × DHT11 temperature & humidity module
  • 1 × SSD1306 OLED screen (128 × 64, I2C)
  • 1 × push button
  • Jumper wires
  • A 2.4 GHz Wi-Fi network

Before you start

This project uses what you learned in Temperature & Humidity, OLED Screens, Wi-Fi & APIs and Buttons. Make sure button.py, ssd1306.py, wifi.py and secrets.py (with your Wi-Fi name and password) are saved on your ESP32 (see Saving Programs to the Board).

Wiring

Part Pin Connects to
DHT11 module + / VCC 3V3
OUT / DATA GPIO 19
– / GND GND
OLED GND GND
VCC 3V3
SCL GPIO 22
SDA GPIO 21
Button A one leg GPIO 14, other leg to GND

Set your location

Near the top of the code are two lines for your location:

LATITUDE = 40.71
LONGITUDE = -74.01

These are New York's. Search the web for "latitude longitude" and the name of your town, and paste in your own numbers. North and east are positive; south and west are negative.

Plan it out

  1. Set up each component. The OLED on I2C, the DHT11, and Button A. Then connect to Wi-Fi.
  2. Break the job into functions.
    • read_indoor() asks the DHT11 for a new measurement.
    • read_outdoor() asks Open-Meteo for the current weather.
    • format_temp() and format_humidity() turn numbers into short text, in °C or °F, or -- if there's no reading.
    • draw() lays everything out on the screen.
  3. Call the functions from the main loop, each on its own schedule. Check the button every loop, the DHT11 every 2 seconds, and the internet every 10 minutes.

Code

Save this to your ESP32 as main.py so it runs every time the board powers up. Or just click Run in Thonny to try it out.

weather_station.py
# Weather Station: shows the temperature and humidity indoors (DHT11)
# and outdoors (from the internet) on the OLED screen.
# Button A switches between °C and °F.
import time
import dht
import requests
from machine import Pin, I2C
import ssd1306
import wifi
from button import Button

DHT_PIN = 19
BUTTON_PIN = 14   # Button A

# Change these to where you live (search "latitude longitude <your town>")
LATITUDE = 40.71
LONGITUDE = -74.01

INDOOR_EVERY_MS = 2000          # the DHT11 can only measure every ~1-2 s
OUTDOOR_EVERY_MS = 10 * 60000   # ask the internet every 10 minutes

URL = ("https://api.open-meteo.com/v1/forecast"
       "?latitude=" + str(LATITUDE) + "&longitude=" + str(LONGITUDE) +
       "&current=temperature_2m,relative_humidity_2m")

# Step 1: set up each component
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
display = ssd1306.SSD1306_I2C(128, 64, i2c)
sensor = dht.DHT11(Pin(DHT_PIN))
button = Button(BUTTON_PIN)

# None means "no reading yet": shown on screen as --
indoor_temp = None
indoor_humidity = None
outdoor_temp = None
outdoor_humidity = None
fahrenheit = False


# Step 2: small functions for each job
def read_indoor():
    global indoor_temp, indoor_humidity
    try:
        sensor.measure()
        indoor_temp = sensor.temperature()
        indoor_humidity = sensor.humidity()
    except OSError:
        print("Couldn't read the DHT11 - check its wiring")


def read_outdoor():
    global outdoor_temp, outdoor_humidity
    try:
        response = requests.get(URL)
        current = response.json()["current"]
        response.close()   # always close, or the ESP32 runs out of memory
        outdoor_temp = current["temperature_2m"]
        outdoor_humidity = current["relative_humidity_2m"]
        print("Outside:", outdoor_temp, "C")
    except Exception as error:
        print("Couldn't get the outdoor weather:", error)
        outdoor_temp = None
        outdoor_humidity = None


def format_temp(celsius):
    if celsius is None:
        return "--"
    if fahrenheit:
        return str(round(celsius * 9 / 5 + 32)) + "F"
    return str(round(celsius)) + "C"


def format_humidity(percent):
    if percent is None:
        return "--"
    return str(round(percent)) + "%"


def draw():
    display.fill(0)
    display.text("Weather Station", 4, 0, 1)
    display.hline(0, 10, 128, 1)
    # left column: inside, right column: outside
    display.text("Inside", 4, 16, 1)
    display.text("Outside", 68, 16, 1)
    display.vline(63, 14, 50, 1)
    display.text(format_temp(indoor_temp), 12, 32, 1)
    display.text(format_temp(outdoor_temp), 76, 32, 1)
    display.text(format_humidity(indoor_humidity), 12, 48, 1)
    display.text(format_humidity(outdoor_humidity), 76, 48, 1)
    display.show()


# Step 3: connect, then update each reading on its own schedule
display.fill(0)
display.text("Connecting...", 4, 28, 1)
display.show()
try:
    wifi.connect()
except RuntimeError as error:
    print(error)   # keep going: indoor readings still work without Wi-Fi

read_indoor()
read_outdoor()
last_indoor = time.ticks_ms()
last_outdoor = time.ticks_ms()
draw()

while True:
    now = time.ticks_ms()

    if button.was_pressed():
        fahrenheit = not fahrenheit
        draw()

    if time.ticks_diff(now, last_indoor) >= INDOOR_EVERY_MS:
        last_indoor = now
        read_indoor()
        draw()

    if time.ticks_diff(now, last_outdoor) >= OUTDOOR_EVERY_MS:
        last_outdoor = now
        read_outdoor()
        draw()

    time.sleep_ms(20)

How it works

  • Different jobs, different speeds. The button needs checking all the time, the DHT11 can only take a reading every second or two, and there's no point asking the internet about the weather more than every few minutes. Each job gets its own "last time I did this" variable (last_indoor, last_outdoor), and the loop checks with time.ticks_diff() whether enough time has passed.
  • Planning for failure. None means "no reading". Both reading functions wrap their work in try/except, so a loose wire or a Wi-Fi hiccup just prints a message and shows --. The next attempt usually works. Even if Wi-Fi doesn't connect at all, the indoor readings still appear.
  • The API answer is a dictionary. The part we want sits under the key "current":

    current = response.json()["current"]
    outdoor_temp = current["temperature_2m"]
    

    response.close() frees the memory the response used. Forget it, and after a few hours the ESP32 runs out of memory. - °C to °F: multiply by 9, divide by 5, and add 32. The screen redraws as soon as you press the button, so you don't have to wait for the next reading.

Result

After Connecting…, the screen splits into two columns: Inside on the left and Outside on the right, each with the temperature and humidity.

What the Weather Station shows on the OLED

Breathe gently on the DHT11 and watch the indoor humidity climb. Press Button A to switch to Fahrenheit.

Make it your own

  • Colour-coded: add an RGB LED that glows blue when it's cold outside and orange when it's warm.
  • More weather: Open-Meteo can also tell you the wind speed (wind_speed_10m) or whether it's raining (precipitation). Add them to the URL's current= list and put them on a second screen page.
  • Min and max: keep track of the lowest and highest indoor temperature since the board started.
  • Better sensor: a DHT22 is more accurate than a DHT11 and reads tenths of a degree. Change dht.DHT11 to dht.DHT22.