Overview
This desktop local weather monitor is the latest in a series of updates to John Park’s original openweathermap-based PyPortal weather station project. It’s a free weather information service that doesn’t require a credit card paywall account. Thanks to some investigative work by @DJDevon3, this latest version uses the free non-commercial Open-Meteo (OM) URL API service. The best part? You don’t need an account or key to access the API if you’re using it for non-commercial projects.
The OM Weather Display periodically updates and displays the following local weather conditions:
- Day, date, and time (AM/PM)
- Tomorrow's sunrise and sunset times
- Temperature
- Relative Humidity
- Wind speed and direction
- Wind gust speed
- Condition description and graphic icon
Location, time zone, and measurement units settings are stored in the settings.toml file. Either "METRIC" or "IMPERIAL" measurement units can be specified.
Weather condition query and internet clock refresh interval rates are set by parameters in the code.py module. The default interval for updating weather conditions is 5 minutes. The local time is updated from the Adafruit Network Time Protocol (NTP) server hourly.
The CircuitPython code runs on an ESP32-S3 4MB/2MB Feather attached to a 2.4-inch TFT FeatherWing. An optional ALS-PT19 ambient light sensor can be used to automatically adjust display brightness.
CircuitPython Code
The Weather Display's CIRCUITPY root directory contains the following files and folders.
- files:
- settings.toml -- Wi-Fi and location parameters
- om_query.py -- OM API URL query string builder
- who_to_map_icon.py -- parses the WMO weather code for descriptions and icons
- code.py -- the primary code module
- folders:
- fonts -- contains the font files
- icons_160x160 -- the weather icon bitmap graphics files
- lib -- the CircuitPython library modules
- sd -- a placeholder for the unused SD storage drive
A downloadable bundle of code files and folders can be found in the OM Weather Display GitHub repository.
Wi-Fi Connect and Local Information (settings.toml)
Besides the WiFi AP SSID and password, the settings.toml file contains the location, latitude, longitude, time zone, time zone offset from GMT (hours), and measurement units parameter settings needed to build the OM query.
CIRCUITPY_WIFI_SSID = "wi-fi_ssid" CIRCUITPY_WIFI_PASSWORD = "averysecurepassword" LOCATION = "Seattle, WA, US" LATITUDE = "47.6061" LONGITUDE = "-122.3328" TIMEZONE = "US/Pacific" TIMEZONE_OFFSET = -8 MEASUREMENT_UNITS = "IMPERIAL"
Build the Open-Meteo Web API Query (om_query.py)
This module builds the Open-Meteo (OM) API URL query string from a list of key words and variables. After retrieving local settings from settings.toml, the DATA_SOURCE string is built by concatenating API keywords to form a URL query. The DATA_SOURCE variable is then used by code.py to retrieve the resultant data payload from OM.
Some of the OM keywords are not used by this version of the Weather Display. The unused keywords are commented-out but left in the oil_query.py file in case they are needed in a future version.
Thanks to @DJDevon3 for the initial URL query builder code that became the foundation for this version.
# SPDX-FileCopyrightText: 2025 JG for Cedar Grove Maker Studios
# SPDX-License-Identifier: MIT
"""
om_query.py
Query builder for Open_Meteo weather conditions Web API.
Location information is extracted from the settings.toml file.
Inspired by DJDevon3's Open-Meteo query builder.
"""
import os
LATITUDE = os.getenv("LATITUDE")
LONGITUDE = os.getenv("LONGITUDE")
TIMEZONE = os.getenv("TIMEZONE")
TIMEZONE_OFFSET = os.getenv("TIMEZONE_OFFSET")
MEASUREMENT_UNITS = os.getenv("MEASUREMENT_UNITS")
if MEASUREMENT_UNITS == "METRIC":
UNITS = ["mm", "celsius", "kmh"]
else:
UNITS = ["inch", "fahrenheit", "mph"]
# Open-Meteo Free API
DATA_SOURCE = "http://api.open-meteo.com/v1/forecast?"
DATA_SOURCE += "latitude=" + LATITUDE
DATA_SOURCE += "&longitude=" + LONGITUDE
DATA_SOURCE += "¤t=temperature_2m,"
DATA_SOURCE += "relative_humidity_2m,"
# DATA_SOURCE += "apparent_temperature,"
# DATA_SOURCE += "dew_point_2m,"
DATA_SOURCE += "is_day,"
# DATA_SOURCE += "precipitation,"
# DATA_SOURCE += "rain,"
# DATA_SOURCE += "showers,"
# DATA_SOURCE += "snowfall,"
DATA_SOURCE += "weather_code,"
# DATA_SOURCE += "cloud_cover,"
# DATA_SOURCE += "pressure_msl,"
# DATA_SOURCE += "surface_pressure,"
DATA_SOURCE += "wind_speed_10m,"
DATA_SOURCE += "wind_direction_10m,"
DATA_SOURCE += "wind_gusts_10m"
DATA_SOURCE += "&daily=sunrise,sunset"
DATA_SOURCE += "&temperature_unit=" + UNITS[1]
DATA_SOURCE += "&wind_speed_unit=" + UNITS[2]
# DATA_SOURCE += "&precipitation_unit=" + UNITS[0]
DATA_SOURCE += "&timeformat=unixtime"
DATA_SOURCE += "&timezone=" + TIMEZONE
Translate WMO Condition Codes (wmo_to_map_icon.py)
When the Open-Meteo data payload is received, it contains a World Meteorological Organization (WMO) Weather Interpretation Code that is used to report weather conditions. This module consists of a dictionary that provides a short and long condition description as well as a two-digit bitmap file prefix used to display the associated weather icon.
Weather icon bitmaps are contained in the icon_160x160 folder.
# SPDX-FileCopyrightText: 2025 JG for Cedar Grove Maker Studios
# SPDX-License-Identifier: MIT
"""
wmo_to_map_icon.py
Decoder for WMO Weather interpretation codes (WW) commonly used by OpenMateo.
Provides a short description, long description, and the openweathermap.org
("map") icon designation.
"""
# WMO code: [description, long_description, map_icon_designation]
wmo_to_map_icon = {
"0": ["Clear", "Clear sky.", "01"],
"1": ["MainlyClear", "Mainly clear sky.", "02"],
"2": ["PartlyCloudy", "Partly cloudy sky.", "02"],
"3": ["Overcast", "Overcast sky.", "03"],
"45": ["Fog", "Foggy.", "50"],
"48": ["Fog", "Depositing rime fog.", "50"],
"51": ["Drizzle", "Light drizzle.", "10"],
"53": ["Drizzle", "Moderate drizzle.", "09"],
"55": ["Drizzle", "Dense drizzle.", "09"],
"56": ["FreezingDrizzle", "Light freezing drizzle.", "09"],
"57": ["FreezingDrizzle", "Dense freezing drizzle.", "09"],
"61": ["Rain", "Slight rain.", "10"],
"63": ["Rain", "Moderate rain.", "09"],
"65": ["Rain", "Heavy rain.", "09"],
"66": ["FreezingRain", "Light freezing rain.", "09"],
"67": ["FreezingRain", "Heavy freezing rain.", "09"],
"71": ["Snow", "Slight snow fall.", "13"],
"73": ["Snow", "Moderate snow fall.", "13"],
"75": ["Snow", "Heavy snow fall.", "13"],
"77": ["Snow", "Snow grains falling.", "13"],
"80": ["Rain", "Slight rain showers.", "10"],
"81": ["Rain", "Moderate rain showers.", "09"],
"82": ["Rain", "Violent rain showers.", "09"],
"85": ["Snow", "Slight snow showers.", "13"],
"86": ["Snow", "Heavy snow showers.", "13"],
"95": ["Thunderstorm", "Thunderstorm.", "07"],
"96": ["Thunderstorm", "Thunderstorm with slight hail.", "07"],
"99": ["Thunderstorm", "Thunderstorm with heavy hail.", "07"],
}
The Primary Code Module (code.py)
The code.py module queries Open-Meteo and fills the display with weather condition values and a graphic icon. The module is also used to control the screen update rate, refreshes the local time, and optionally controls display screen brightness.
The OM query rate in seconds is set by the SAMPLE_INTERVAL parameter. Since OM updates weather conditions a few times each hour, setting this parameter to 10 or 20 minutes should suffice. If you need a shorter interval rate, consider signing up for one of OM's paid API plans.
The local clock is updated from the Adafruit NTP server each hour. The clock refresh rate is set by the NTP_INTERVAL parameter.
The BRIGHTNESS parameter is used to control the TFT display backlight and NeoPixel intensity. It can be set with a value between 0.0 (completely dark) and 1.0 (brightest setting). When the LIGHT_SENSOR is enabled, BRIGHTNESS represents the maximum brightness value.
If automatic display brightness is enabled (LIGHT_SENSOR = True), the code will periodically read the analog value of pin A3 to calculate a target brightness for the display and NeoPixel. Of course that means that a light sensor should be connected. See the Optional Display Brightness Control section for how to attach the sensor.
# SPDX-FileCopyrightText: 2025 JG for Cedar Grove Maker Studios
# SPDX-License-Identifier: MIT
"""
om_weather_display_code.py
Receives Open-Meteo local weather conditions.
Designed for the Adafruit ESP32-S3 4MB/2MB Feather (#5477) and
2.4" TFT FeatherWing (#3315).
"""
import time
import board
import os
import gc
import rtc
import displayio
import fourwire
import wifi
import ssl
from digitalio import DigitalInOut, Direction
import pwmio
import analogio
import supervisor
from simpleio import map_range
import neopixel
import adafruit_ntp
import adafruit_connection_manager
import adafruit_requests
import adafruit_ili9341
from adafruit_display_text.label import Label
from adafruit_bitmap_font import bitmap_font
from adafruit_display_shapes.roundrect import RoundRect
from wmo_to_map_icon import wmo_to_map_icon
from om_query import DATA_SOURCE
# Weather Display Parameters
SAMPLE_INTERVAL = 600 # Check conditions (sec): typically 600 to 1200
NTP_INTERVAL = 3600 # Update local time from NTP server (sec): typically 3600
BRIGHTNESS = 0.75 # TFT and NeoPixel brightness setting
LIGHT_SENSOR = False # True when ALS-PT19 sensor is connected to board.A3
# fmt: off
# Month and weekday lookup tables
WEEKDAY = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
MONTH = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
# fmt: on
# Default colors
BLACK = 0x000000
RED = 0xFF0000
GREEN = 0x00FF00
ORANGE = 0xFF8811
YELLOW = 0xFFFF00
VIOLET = 0x9900FF
PURPLE = 0xFF00FF
WHITE = 0xFFFFFF
DK_BLUE = 0x000080
# Define a few state/mode pixel colors
STARTUP = PURPLE
NORMAL = DK_BLUE
FETCH = YELLOW
ERROR = RED
# Set start-up values
old_brightness = BRIGHTNESS
clock_tick = False
# Instantiate the display and darken during startup process
# Adafruit 2.4" TFT FeatherWing with LITE connected to TX
lite = pwmio.PWMOut(board.TX, frequency=500)
lite.duty_cycle = 0 # Reduce display brightness during startup
displayio.release_displays() # Release display resources
display_bus = fourwire.FourWire(
board.SPI(), command=board.D10, chip_select=board.D9, reset=None
)
display = adafruit_ili9341.ILI9341(display_bus, width=320, height=240)
display.rotation = 0
# Instantiate the red LED
led = DigitalInOut(board.LED)
led.direction = Direction.OUTPUT
led.value = False
# Instantiate the NeoPixel
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel[0] = STARTUP
pixel.brightness = BRIGHTNESS
# Instantiate the ALS-PT19 light sensor
light_sensor = analogio.AnalogIn(board.A3)
# ### Helper Methods ###
def adjust_brightness():
"""Acquire the ALS-PT19 light sensor value and gradually adjust display
brightness based on ambient light. The display brightness ranges from 0.05
to BRIGHTNESS when the ambient light level falls between 5 to 200 lux.
Full-scale raw light sensor value (65535) is approximately 1500 Lux."""
global old_brightness
if not LIGHT_SENSOR:
return
raw = 0
for i in range(2000):
raw = raw + light_sensor.value
target_brightness = round(
map_range(raw / 2000 / 65535 * 1500, 5, 200, 0.05, BRIGHTNESS), 3
)
new_brightness = round(
old_brightness + ((target_brightness - old_brightness) / 5), 3
)
disp_brightness(new_brightness)
pixel.brightness = new_brightness
old_brightness = new_brightness
def alert(text=""):
"""Flash text in the message area and print to REPL.
:param str text: Message text limited to 20 characters. Default is
an empty string (no message)."""
if not text or text == "":
return
text = text[:20]
print("ALERT: " + text)
display_message.color = RED
display_message.text = text
time.sleep(0.1)
display_message.color = YELLOW
time.sleep(0.1)
display_message.color = RED
time.sleep(0.1)
display_message.color = YELLOW
time.sleep(0.5)
display_message.color = None
def am_pm(hour):
"""Provide an adjusted hour and AM/PM string to create to a
12-hour time string.
:param int hour: The clock hour. No default."""
if hour < 12:
if hour == 0:
hour = 12
return hour, "AM"
if hour == 12:
return 12, "PM"
if hour > 12:
hour = hour - 12
return hour, "PM"
def disp_brightness(new_brightness):
"""Set the TFT display brightness.
:param float new_brightness: The display brightness 0.0 to 1.0. No default."""
new_brightness = min(max(new_brightness, 0), 1.0)
lite.duty_cycle = int(new_brightness * 0xFFFF)
def display_local_time(repl=True):
"""Show the local time on-screen and in the REPL.
:param bool repl: Print time string in the REPL. Default is True (print
in the REPL)."""
hour, suffix = am_pm(time.localtime().tm_hour)
display_time = f"{hour:2d}:{time.localtime().tm_min:02d} {suffix}"
clock_digits.text = display_time
if repl:
print(f" Local Time: {display_time}")
def get_local_time():
"""Update the local time from the NTP server."""
pixel[0] = FETCH
alert("UPDATE TIME")
try:
ntp = adafruit_ntp.NTP(pool, tz_offset=os.getenv("TIMEZONE_OFFSET"))
rtc.RTC().datetime = ntp.datetime
except Exception as time_err:
print(f" ERROR: Fetch local time: {time_err}")
alert(" READY")
pixel[0] = NORMAL
def toggle_clock_tick():
"""Toggle the clock tick indicator and red LED."""
global clock_tick
if clock_tick:
clock_tick_mask.fill = RED
led.value = True
else:
clock_tick_mask.fill = None
led.value = False
clock_tick = not clock_tick
def update_display():
"""Fetch latest weather condition values from Open-Mateo and
update the display."""
pixel[0] = FETCH
alert("UPDATE CONDITIONS")
# Update day and date
wday = time.localtime().tm_wday
month = time.localtime().tm_mon
day = time.localtime().tm_mday
year = time.localtime().tm_year
clock_day_mon_yr.text = f"{WEEKDAY[wday]} {MONTH[month - 1]} {day:02d}, {year:04d}"
# Get weather conditions from Open-Meteo free API
gc.collect() # Prepare to use memory for query result
# print(f" mem_free() before fetch: {gc.mem_free() / 1000:.3f}kB")
try:
with requests.get(DATA_SOURCE) as payload:
payload = requests.get(DATA_SOURCE)
om_json = payload.json()
gc.collect() # Cleans up 10kB of json payload rubbish
pixel[0] = NORMAL
except Exception as data_source_err:
pixel[0] = ERROR
print(f"ERROR: Fetch data from data source: {data_source_err}")
print(" MCU will soft reset in 30 seconds.")
time.sleep(30)
supervisor.reload() # Soft reset: keeps the terminal session alive
# print(f" mem_free() after fetch: {gc.mem_free() / 1000:.3f}kB")
# Calculate and update sunrise/sunset
sset = time.localtime(om_json["daily"]["sunset"][0] + om_json["utc_offset_seconds"])
srise = time.localtime(
om_json["daily"]["sunrise"][0] + om_json["utc_offset_seconds"]
)
sunrise.text = f"rise {am_pm(srise.tm_hour)[0]:2d}:{srise.tm_min:02d} {am_pm(srise.tm_hour)[1]}"
sunset.text = (
f"set {am_pm(sset.tm_hour)[0]:2d}:{sset.tm_min:02d} {am_pm(sset.tm_hour)[1]}"
)
wind_dir = f"{wind_direction(om_json['current']['wind_direction_10m'])}"
if om_json["current_units"]["wind_speed_10m"] == "mp/h":
wind_units = "MPH"
else:
wind_units = "km/H"
windspeed.text = (
f"{wind_dir} {om_json['current']['wind_speed_10m']:.0f} {wind_units}"
)
windgust.text = f"gusts {om_json['current']['wind_gusts_10m']:.0f} {wind_units}"
# Update weather description
description.text = wmo_to_map_icon[f"{om_json['current']['weather_code']}"][0]
long_desc.text = wmo_to_map_icon[f"{om_json['current']['weather_code']}"][1]
# Create icon filename
if om_json["current"]["is_day"]:
icon_suffix = "d" # Day
else:
icon_suffix = "n" # Night
icon = wmo_to_map_icon[f"{om_json['current']['weather_code']}"][2]
icon_file = f"/icons_160x160/{icon}{icon_suffix}.bmp"
# print(f"Icon filename: {icon_file}")
# Update icon graphic
image_group.pop(0)
icon_image = displayio.OnDiskBitmap(icon_file)
icon_bg = displayio.TileGrid(
icon_image,
pixel_shader=icon_image.pixel_shader,
x=(WIDTH // 2) - 80,
y=(HEIGHT // 2) - 80,
)
image_group.insert(0, icon_bg)
# Update temperature and humidity
temperature.text = f"{om_json['current']['temperature_2m']:.0f}{om_json['current_units']['temperature_2m']}"
humidity.text = f"{om_json['current']['relative_humidity_2m']}{om_json['current_units']['relative_humidity_2m']} RH"
gc.collect() # Clean up displayio rendering rubbish
alert(" READY")
pixel[0] = NORMAL
def wind_direction(heading):
"""Provide a one or two character string representation of the compass
heading value. Returns '--' if heading is None.
:param int heading: The compass heading. No default."""
if heading is None:
return "--"
return ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][
int(((heading + 22.5) % 360) / 45)
]
# ### Primary Initialization Process ###
# Connect to the Wi-Fi AP specified in settings.toml
pixel[0] = FETCH
print("Connecting to Wi-Fi...")
while not wifi.radio.ipv4_address:
try:
wifi.radio.connect(
os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)
except ConnectionError as connect_err:
pixel[0] = ERROR
print("ERROR: Wi-Fi Connection Error:", connect_err)
print(" retrying in 10 seconds")
time.sleep(10)
continue
print(" Wi-Fi Connected")
pixel[0] = NORMAL
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
pixel[0] = STARTUP
# Load the text fonts from the fonts folder
SMALL_FONT = bitmap_font.load_font("/fonts/Arial-12.bdf")
MEDIUM_FONT = bitmap_font.load_font("/fonts/Arial-16.bdf")
LARGE_FONT = bitmap_font.load_font("/fonts/Arial-Bold-24.bdf")
# Define the TFT's display size
WIDTH = display.width
HEIGHT = display.height
# Define the display group for text and graphic icon
image_group = displayio.Group()
display.root_group = image_group # Select the group
disp_brightness(BRIGHTNESS) # Watch it build (for the fun of it)
# ### Define display graphic icon, label, and value areas ###
# Create a replaceable icon background layer as image_group[0]
icon_image = displayio.OnDiskBitmap("/icons_160x160/01d.bmp")
icon_bg = displayio.TileGrid(
icon_image,
pixel_shader=icon_image.pixel_shader,
x=(WIDTH // 2) - 80,
y=(HEIGHT // 2) - 80,
)
image_group.append(icon_bg)
# Define the project messaging label
display_message = Label(SMALL_FONT, text=" ", color=YELLOW)
display_message.anchor_point = (0.5, 0.5)
display_message.anchored_position = (display.width // 2, 231)
image_group.append(display_message)
alert("STARTUP")
# Define the day/date and clock labels; update and display local time
clock_day_mon_yr = Label(MEDIUM_FONT, text=" ")
clock_day_mon_yr.anchor_point = (0, 0)
clock_day_mon_yr.anchored_position = (10, 15)
clock_day_mon_yr.color = PURPLE
image_group.append(clock_day_mon_yr)
clock_digits = Label(MEDIUM_FONT, text=" ")
clock_digits.anchor_point = (1.0, 0)
clock_digits.anchored_position = (display.width - 10, 15)
clock_digits.color = WHITE
image_group.append(clock_digits)
get_local_time()
display_local_time()
# Define wind speed and gust labels
windspeed = Label(MEDIUM_FONT, text=" ")
windspeed.anchor_point = (0, 0)
windspeed.anchored_position = (10, 44)
windspeed.color = WHITE
image_group.append(windspeed)
windgust = Label(SMALL_FONT, text=" ")
windgust.anchor_point = (0, 0)
windgust.anchored_position = (10, 65)
windgust.color = RED
image_group.append(windgust)
# Define sunrise and sunset labels
sunrise = Label(SMALL_FONT, text=" ")
sunrise.anchor_point = (1.0, 0.0)
sunrise.anchored_position = (display.width - 10, 44)
sunrise.color = YELLOW
image_group.append(sunrise)
sunset = Label(SMALL_FONT, text=" ")
sunset.anchor_point = (1.0, 0.0)
sunset.anchored_position = (display.width - 10, 60)
sunset.color = ORANGE
image_group.append(sunset)
# Define the short and long description labels
description = Label(LARGE_FONT, text=" ")
description.anchor_point = (0, 0)
description.anchored_position = (10, 178)
description.color = WHITE
image_group.append(description)
long_desc = Label(SMALL_FONT, text=" ")
long_desc.anchor_point = (0, 0)
long_desc.anchored_position = (10, 210)
long_desc.color = PURPLE
image_group.append(long_desc)
# Define the temperature and humidity labels
temperature = Label(LARGE_FONT, text=" ")
temperature.anchor_point = (1.0, 0)
temperature.anchored_position = (display.width - 10, 178)
temperature.color = WHITE
image_group.append(temperature)
humidity = Label(SMALL_FONT, text=" ")
humidity.anchor_point = (1.0, 0)
humidity.anchored_position = (display.width - 10, 210)
humidity.color = PURPLE
image_group.append(humidity)
# Define the clock tick indicator shap
clock_tick_mask = RoundRect(310, 4, 7, 8, 1, fill=VIOLET, outline=None, stroke=0)
image_group.append(clock_tick_mask)
gc.collect() # Clean up displayio rendering rubbish
# Initialize interval timing variables
last_weather_update = time.monotonic()
last_time_update = time.monotonic()
# Initially display the conditions
update_display() # Fetch initial data from Open-Mateo
# ### Main Loop ###
while True:
current_time = time.monotonic()
# Update weather every SAMPLE_INTERVAL seconds
if current_time - last_weather_update > SAMPLE_INTERVAL:
update_display()
display_local_time()
last_weather_update = current_time
# Update network time every NTP_INTERVAL seconds
if current_time - last_time_update > NTP_INTERVAL:
get_local_time()
display_local_time()
last_time_update = current_time
# Update time display every second
toggle_clock_tick()
display_local_time(repl=False)
# Watch for and adjust to ambient light changes
adjust_brightness()
# Adjust wait time to as close to 1 sec as possible
time.sleep(max(min(1.0 - (time.monotonic() - current_time), 1.0), 0))
Optional Display Brightness Control
(Some soldering required.)
If TFT display brightness control is desired, the TFT FeatherWing will require a modification to permit the ESP32-S3 Feather's TX pin to adjust the TFT backlight intensity. Also, the Weather Display code is designed to use a light sensor to automatically adjust brightness proportional to ambient light near the display.
With the power cable unplugged and any batteries removed, solder a short length of wire between the TFT FeatherWing's LITE jumper pad and the pad next to the Feather socket's TX pin. The wire provides a path to control the TFT backlight brightness using PWM from the TX pin. This modification is required for setting brightness even if a light sensor is not attached for automatic control.
For automatic brightness control, the ALS-PT19 ambient light sensor is attached to the FeatherWing's GND, +3V, and A3. The easiest method to make the sensor "connectable" is to solder three wires with header pins attached to the -, +, and sig pads on the sensor breakout board. I used three Soft Silicon Wires with pin connections, cut to length but leaving one of the wire's pin connectors intact. It's recommended that a red wire be used for the +3V power, black for GND, and some other color (yellow is nice) for the signal connection to A3.
Plug the red wire into the 3V pin of the Feather socket (the third pin when counting from the left), black into the GND pin (the fourth pin), and the yellow signal wire into A3 (the eighth pin).
The final step for automatic control is to edit the LIGHT_SENSOR parameter in code.py (line 39) so that it's set to True. Save code.py, position the sensor to detect ambient light, and the weather display will be ready to go.
Planned Enhancements and Wish List
- Increase the number of icons for weather conditions. (planned)
- Use a 3.5-inch TFT FeatherWing to simultaneously display more data categories such as barometric pressure and precipitation totals. (planned)
- Add touch-sensitive selection of forecasts and graphical data trends.
- Would like future TFT FeatherWing and breakout display board designs to provide mounting screw holes that are equidistant from the center of the TFT viewing area rather than the exterior of the TFT module. Would make the display look more centered in an enclosure where the mounting screws are exposed.
- I'll be patiently wishing for an Adafruit ESP32-S3 8MB/2MB (or 16MB/8MB) PyPortal family with all the bells and whistles of the original PyPortal --connectors, on-board temperature sensor, light sensor, audio speaker and in all three sizes. Plug and play. Fingers are crossed that a revised PyPortal will also have an integrated on-off power switch.
Resources and Adafruit Products
- The free Open-Meteo (OM) API service web site.
- A NOAA archive listing of WMO Interpretation Codes.
- For some helpful hints for attaching header pins to the ESP32-S3 Feather, see How to Solder Headers.
- Tap Plastics polycarbonate 1/16-inch and 1/8-inch sheets.
This page (Weather Display Using Open-Meteo's API) was last updated on August 17, 2025.
Text editor powered by tinymce.