ESP32-C3 SHT40 Weather Server
In this project, we will create a web server using an ESP32-C3 microcontroller running CircuitPython. The server will:
- Display real-time indoor temperature and humidity using the SHT40 sensor.
- Fetch and display outdoor weather data (temperature and humidity) from the OpenWeatherMap API.
- Dynamically update the data on the webpage without requiring a manual refresh.
Supplies
Materials Required
- ESP32-C3 Seeedstudio XIAO microcontroller
- SHT40 temperature and humidity sensor
- USB-C cable
- Access to Wi-Fi
- OpenWeatherMap API key
Setting Up the Environment
- Wire up your sensor to the ESP32-C3 using attached image as a guide.
- Install CircuitPython
- Download CircuitPython for ESP32-C3 from the Adafruit website.
- Follow the instructions to flash CircuitPython onto your ESP32-C3.
- Download Libraries
- Download the latest CircuitPython library bundle from CircuitPython Libraries.
- Copy the following libraries to the lib folder of your ESP32-C3:
- adafruit_sht4x.mpy
- adafruit_httpserver
- adafruit_requests.mpy
- adafruit_ntp.mpy
- Prepare the settings.toml File
- Create a file named settings.toml in the root directory of your ESP32-C3.
- Add the following content, replacing your_ssid and your_password with your Wi-Fi credentials:
CIRCUITPY_WIFI_SSID="your_ssid"
CIRCUITPY_WIFI_PASSWORD="your_password"
Writing the Code
Copy the provided Python code into a file named code.py on your ESP32-C3.
This code:
- Connects to Wi-Fi and syncs time using NTP.
- Reads temperature and humidity from the SHT40 sensor.
- Fetches outdoor weather data from the OpenWeatherMap API.
- Hosts a web server that dynamically updates the webpage with sensor and weather data.
import wifi
import socketpool
import adafruit_ntp
import rtc
import time
import board
import adafruit_sht4x
from adafruit_httpserver import Server, Request, Response
import os
import json
import adafruit_requests
# Read Wi-Fi credentials from settings.toml
SSID = os.getenv("CIRCUITPY_WIFI_SSID")
PASSWORD = os.getenv("CIRCUITPY_WIFI_PASSWORD")
# OpenWeatherMap API key and location
API_KEY = "your_openweathermap_api_key"
LATITUDE = "your_latitude"
LONGITUDE = "your_longitude"
# Timezone offset in seconds (e.g., UTC+2 -> 2 * 3600)
TIMEZONE_OFFSET = -21600
# Connect to Wi-Fi
print("Connecting to Wi-Fi...")
wifi.radio.connect(SSID, PASSWORD)
print(f"Connected to Wi-Fi, IP Address: {wifi.radio.ipv4_address}")
# Set up NTP client
pool = socketpool.SocketPool(wifi.radio)
ntp = adafruit_ntp.NTP(pool, server="time.google.com", tz_offset=TIMEZONE_OFFSET // 3600)
# Sync time with NTP server
print("Syncing time...")
rtc_instance = rtc.RTC()
rtc_instance.datetime = ntp.datetime # Set the RTC time
print("Time synced!")
# Set up SHT40 sensor
i2c = board.I2C()
sht = adafruit_sht4x.SHT4x(i2c)
print("Found SHT4x with serial number", hex(sht.serial_number))
sht.mode = adafruit_sht4x.Mode.NOHEAT_HIGHPRECISION
print("Current mode is: ", adafruit_sht4x.Mode.string[sht.mode])
# Create an HTTP server
server = Server(pool, "/static")
requests = adafruit_requests.Session(pool)
# Fetch weather data
def get_weather():
url = f"http://api.openweathermap.org/data/2.5/weather?lat={LATITUDE}&lon={LONGITUDE}&appid={API_KEY}&units=imperial"
try:
print("Fetching weather data...")
response = requests.get(url)
data = response.json()
# Check if the response contains "main" key
if "main" in data:
outside_temp = data["main"]["temp"]
outside_humidity = data["main"]["humidity"]
print(f"Outside Temperature: {outside_temp} °F")
print(f"Outside Humidity: {outside_humidity} %")
return outside_temp, outside_humidity
else:
print("Error fetching weather data: 'main' key not found in API response")
return None, None
except Exception as e:
print(f"Error fetching weather data: {e}")
return None, None
# Fetch the weather data initially
outside_temp, outside_humidity = get_weather()
# Route for the root page
@server.route("/")
def root(request: Request):
now = time.localtime()
current_time = "{:04}-{:02}-{:02} {:02}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec
)
temperature_c, relative_humidity = sht.measurements
temperature_f = (temperature_c * 9 / 5) + 32
# Check for None values and provide defaults
outside_temp_display = f"{outside_temp:.1f} °F" if outside_temp is not None else "N/A"
outside_humidity_display = f"{outside_humidity:.1f} %" if outside_humidity is not None else "N/A"
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ESP32-C3 Web Server</title>
<style>
body {{
font-family: Arial, sans-serif;
background-color: #121212;
color: #e0e0e0;
text-align: center;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
box-sizing: border-box;
}}
h1 {{ color: #bb86fc; margin-bottom: 20px; }}
.container {{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
max-width: 600px;
width: 100%;
}}
.tile {{
background-color: #1f1f1f;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
}}
.tile h2 {{ margin: 0 0 10px 0; font-size: 1.2rem; color: #03dac6; }}
.tile p {{ margin: 0; font-size: 1rem; color: #e0e0e0; }}
.tile.large {{ grid-column: span 2; }}
</style>
<script>
function updateInsideData() {{
fetch('/inside-data')
.then(response => response.json())
.then(data => {{
document.getElementById("inside-temp").innerHTML = `${{data.temperature}} °F`;
document.getElementById("inside-humidity").innerHTML = `${{data.humidity}} %`;
}});
}}
function updateOutsideData() {{
fetch('/outside-data')
.then(response => response.json())
.then(data => {{
document.getElementById("outside-temp").innerHTML = `${{data.temperature}} °F`;
document.getElementById("outside-humidity").innerHTML = `${{data.humidity}} %`;
}});
}}
function updateTime() {{
fetch('/time')
.then(response => response.text())
.then(data => {{
document.getElementById("time").innerHTML = data;
}});
}}
setInterval(updateInsideData, 10000); // Update inside data every 10 seconds
setInterval(updateOutsideData, 300000); // Update outside data every 5 minutes
setInterval(updateTime, 1000); // Update time every second
</script>
</head>
<body>
<h1>ESP32-C3 Web Server</h1>
<div class="container">
<div class="tile large">
<h2>Time</h2>
<p id="time"><strong>{current_time}</strong></p>
</div>
<div class="tile">
<h2>Inside Temperature</h2>
<p id="inside-temp"><strong>{temperature_f:.1f} °F</strong></p>
</div>
<div class="tile">
<h2>Inside Humidity</h2>
<p id="inside-humidity"><strong>{relative_humidity:.1f} %</strong></p>
</div>
<div class="tile">
<h2>Outside Temperature</h2>
<p id="outside-temp"><strong>{outside_temp_display}</strong></p>
</div>
<div class="tile">
<h2>Outside Humidity</h2>
<p id="outside-humidity"><strong>{outside_humidity_display}</strong></p>
</div>
</div>
</body>
</html>
"""
return Response(request, html, content_type="text/html")
# Route for the /time endpoint
@server.route("/time")
def time_endpoint(request: Request):
now = time.localtime()
current_time = "{:04}-{:02}-{:02} {:02}:{:02}:{:02}".format(
now.tm_year, now.tm_mon, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec
)
return Response(request, current_time, content_type="text/plain")
# Route for the /inside-data endpoint
@server.route("/inside-data")
def inside_data_endpoint(request: Request):
temperature_c, relative_humidity = sht.measurements
temperature_f = (temperature_c * 9 / 5) + 32
return Response(
request,
json.dumps({
"temperature": f"{temperature_f:.1f}",
"humidity": f"{relative_humidity:.1f}"
}),
content_type="application/json"
)
# Route for the /outside-data endpoint
@server.route("/outside-data")
def outside_data_endpoint(request: Request):
return Response(
request,
json.dumps({
"temperature": f"{outside_temp:.1f}" if outside_temp is not None else "N/A",
"humidity": f"{outside_humidity:.1f}" if outside_humidity is not None else "N/A"
}),
content_type="application/json"
)
# Start the server
print("Starting server...")
server.start(str(wifi.radio.ipv4_address), port=5000)
print(f"Server is running at: http://{wifi.radio.ipv4_address}:5000")
last_weather_update = time.monotonic()
while True:
try:
# Poll the server for incoming requests
server.poll()
# Update weather data every 5 minutes
current_time = time.monotonic()
if current_time - last_weather_update > 300: # 5 minutes
outside_temp, outside_humidity = get_weather()
last_weather_update = current_time
except Exception as e:
print(f"An error occurred: {e}")
Time Zone
Edit the time zone code to account for your local time zone.
Example: -6 Central Time zone ( 6 * 3600 = 21600 ). Since it's -6, we put -21600. If it was +6, we would just put 21600.
# Timezone offset in seconds (e.g., UTC+2 -> 2 * 3600)
TIMEZONE_OFFSET = -21600
Setting Up OpenWeatherMap
- Go to OpenWeatherMap and sign up for a free account.
- Navigate to the API section and generate an API key.
- Note down your location’s latitude and longitude (you can find these on Google Maps).
- Add the API key, latitude, and longitude to the code.
Ensure you replace the OpenWeatherMap API key and coordinates in the code with your own:
API_KEY = "your_openweathermap_api_key"
LATITUDE = "your_latitude"
LONGITUDE = "your_longitude"
Running the Project
- Save the code.py file and reset the ESP32-C3.
- Open the serial monitor to ensure the ESP32-C3:
- Connects to Wi-Fi.
- Syncs time successfully.
- Fetches weather data without errors.
- Access the web server by entering the ESP32-C3’s IP address in your browser (displayed in the serial monitor).
Exploring the Web Interface
The webpage will display:
- Current date and time (updated every second).
- Real-time indoor temperature and humidity.
- Outdoor temperature and humidity fetched every 5 minutes from OpenWeatherMap.
The interface includes:
- A clean dark mode design with tiles for each data type.
- Dynamic updates using JavaScript to ensure the webpage refreshes data without requiring a manual reload.
Customizing the Project
- Change Update Intervals
- Adjust the JavaScript setInterval values to modify how often data is refreshed.
- Add More Sensors
- Expand the project by connecting additional sensors to the ESP32-C3 and adding their data to the webpage.
- Host on a Local Network
- Keep the ESP32-C3 running on your local network to access real-time data from multiple devices.