ESP32-C3 SHT40 Weather Server

by greend88 in Circuits > Sensors

28 Views, 0 Favorites, 0 Comments

ESP32-C3 SHT40 Weather Server

screenshot webserver.jpeg

In this project, we will create a web server using an ESP32-C3 microcontroller running CircuitPython. The server will:

  1. Display real-time indoor temperature and humidity using the SHT40 sensor.
  2. Fetch and display outdoor weather data (temperature and humidity) from the OpenWeatherMap API.
  3. Dynamically update the data on the webpage without requiring a manual refresh.

Supplies

Materials Required

  1. ESP32-C3 Seeedstudio XIAO microcontroller
  2. SHT40 temperature and humidity sensor
  3. USB-C cable
  4. Access to Wi-Fi
  5. OpenWeatherMap API key

Setting Up the Environment

Pinout.png
  1. Wire up your sensor to the ESP32-C3 using attached image as a guide.
  2. Install CircuitPython
  3. Download CircuitPython for ESP32-C3 from the Adafruit website.
  4. Follow the instructions to flash CircuitPython onto your ESP32-C3.
  5. Download Libraries
  6. Download the latest CircuitPython library bundle from CircuitPython Libraries.
  7. Copy the following libraries to the lib folder of your ESP32-C3:
  8. adafruit_sht4x.mpy
  9. adafruit_httpserver
  10. adafruit_requests.mpy
  11. adafruit_ntp.mpy
  12. Prepare the settings.toml File
  13. Create a file named settings.toml in the root directory of your ESP32-C3.
  14. 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:

  1. Connects to Wi-Fi and syncs time using NTP.
  2. Reads temperature and humidity from the SHT40 sensor.
  3. Fetches outdoor weather data from the OpenWeatherMap API.
  4. 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}} &deg;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}} &deg;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} &deg;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

  1. Go to OpenWeatherMap and sign up for a free account.
  2. Navigate to the API section and generate an API key.
  3. Note down your location’s latitude and longitude (you can find these on Google Maps).
  4. 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

  1. Save the code.py file and reset the ESP32-C3.
  2. Open the serial monitor to ensure the ESP32-C3:
  3. Connects to Wi-Fi.
  4. Syncs time successfully.
  5. Fetches weather data without errors.
  6. 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:

  1. Current date and time (updated every second).
  2. Real-time indoor temperature and humidity.
  3. Outdoor temperature and humidity fetched every 5 minutes from OpenWeatherMap.

The interface includes:

  1. A clean dark mode design with tiles for each data type.
  2. Dynamic updates using JavaScript to ensure the webpage refreshes data without requiring a manual reload.

Customizing the Project

  1. Change Update Intervals
  2. Adjust the JavaScript setInterval values to modify how often data is refreshed.
  3. Add More Sensors
  4. Expand the project by connecting additional sensors to the ESP32-C3 and adding their data to the webpage.
  5. Host on a Local Network
  6. Keep the ESP32-C3 running on your local network to access real-time data from multiple devices.