Once you've finished setting up your QT Py ESP32-S2 with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.
# SPDX-FileCopyrightText: 2022 Liz Clark for Adafruit Industries # SPDX-License-Identifier: MIT import os import ssl import time import board import wifi import socketpool import fontio import neopixel import simpleio from adafruit_display_text.bitmap_label import Label from adafruit_bitmap_font import bitmap_font from displayio import Bitmap from rainbowio import colorwheel from adafruit_ticks import ticks_ms, ticks_add, ticks_diff import adafruit_requests from weather_codes import weather_codes # minimum expected temperature min_temp = 0 # maximum expected temperature max_temp = 100 # first daylight hour daytime_min = 7 # last daylight hour daytime_max = 17 # latitude lat = 42.36 # longitude long = -71.06 # temp unit for API request temperature_unit = "fahrenheit" # temp unit for display temp_unit = "F" # API request to open-meteo weather_url = "https://api.open-meteo.com/v1/forecast?" # pass latitude and longitude weather_url += "latitude=%d&longitude=%d&timezone=auto" % (lat, long) # pass temperature_unit weather_url += "¤t_weather=true&temperature_unit=%s&windspeed_unit=mph" % temperature_unit # connect to SSID wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) def get_the_weather(): # make the API request response = requests.get(weather_url) # packs the response into a JSON response_as_json = response.json() print() # prints the entire JSON print(response_as_json) print() # gets current weather code w = int(response_as_json['current_weather']['weathercode']) # gets temperature t = response_as_json['current_weather']['temperature'] temp_int = int(t) t_c = simpleio.map_range(temp_int, min_temp, max_temp, 255, 0) # gets time json_time = response_as_json['current_weather']['time'] n_t = json_time.rsplit("T", 1)[-1] n_t = int(n_t[:2]) return w, t, t_c, n_t # initial API call weather, temp, temp_color, new_time = get_the_weather() # font edit code by Jeff Epler tom_thumb = bitmap_font.load_font("tom-thumb.pcf", Bitmap) _glyph_keys = ['bitmap', 'tile_index', 'width', 'height', 'dx', 'dy', 'shift_x', 'shift_y'] def patch_glyph(base, **kw): d = {} for k in _glyph_keys: d[k] = kw.get(k, getattr(base, k)) return fontio.Glyph(**d) class PatchedFont: def __init__(self, base_font, patches): self.base_font = base_font self.patches = patches def get_glyph(self, glyph): g = self.base_font.get_glyph(glyph) patch = self.patches.get(glyph) if patch is not None: #print("patching", repr(chr(glyph)), g) g = patch_glyph(g, **patch) #print("patched", g) return g def get_bounding_box(self): return self.base_font.get_bounding_box() font = PatchedFont(tom_thumb, { 32: {'shift_x': 1, 'dx': 0}, 105: {'dx': 0, 'shift_x': 2}, 33: {'dx': 0, 'shift_x': 2}, }) # thank you Jeff for this PatchedFont() function! # temperature for scrolling text label = Label(text=" %s°%s " % (temp, temp_unit), font=font) text = label.bitmap # create 5x5 neopixels pixels = neopixel.NeoPixel(board.A3, 5*5, brightness=.08, auto_write=False) # count for pixels when drawing bitmaps count = 0 # arrays to pack assets from weather_codes helper # weather condition code codes = [] # bitmaps for daytime day_images = [] # bitmaps for nighttime night_images = [] for i in weather_codes: codes.append(i['code']) day_images.append(i['day_img']) night_images.append(i['night_img']) # checks if it's day or night based on hour def day_or_night(t): if t in range(daytime_min, daytime_max): z = day_images[weather] else: z = night_images[weather] return z # initial sprite selection img = day_or_night(new_time) # draw bitmap sprite def draw_sprite(c): for pixel in img: pixels[c] = pixel pixels.show() c += 1 time.sleep(0.001) c = 0 # ticks time tracker clock = ticks_ms() # 15 minutes in milliseconds weather_check = 900000 # display current weather sprite & scroll temperature while True: # checks the time if ticks_diff(ticks_ms(), clock) > weather_check: print("pinging Open-Meteo") # make the API request with function # return weather ID, temp, temp color & hour weather, temp, temp_color, new_time = get_the_weather() # checks if it's day or night based on hour # & returns day or night version of sprite img = day_or_night(new_time) label.text = " %s°%s " % (temp, temp_unit) # reset clock clock = ticks_add(clock, weather_check) # draw bitmap sprite draw_sprite(count) # blocking delay to hold the sprite on the display time.sleep(5) # draw scrolling text for v in range(2): for i in range(text.width): # Scoot the old text left by 1 pixel pixels[:20] = pixels[5:] # adjust color based on temperature color = colorwheel(temp_color) # Draw in the next line of text for y in range(5): # Select black or color depending on the bitmap pixel pixels[20+y] = color * text[i,y] pixels.show() time.sleep(.1)
Upload the Code and Libraries to the QT Py ESP32-S2
After downloading the Project Bundle, plug your QT Py ESP32-S2 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the QT Py ESP32-S2's CIRCUITPY drive.
- lib folder
- code.py
- weather_codes.py
Your QT Py ESP32-S2 CIRCUITPY drive should look like this after copying the lib folder, weather_codes.py file and the code.py file.

Add Your settings.toml File
As of CircuitPython 8.0.0, there is support for Environment Variables. These Environmental Variables are stored in a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file. Add your settings.toml file as described in the Create Your settings.toml File page earlier in this guide. You'll need to include your CIRCUITPY_WIFI_SSID
and CIRCUITPY_WIFI_PASSWORD
.
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here"
The weather_codes.py File
The weather_codes.py file is a helper file that contains the sprite information and assigns the sprites to the weather condition codes that will be returned from Open-Meteo.
The file begins by defining RGB color values that will be used to create the sprites.
y = (255, 125, 0) o = (0, 0, 0) a = (0, 75, 125) w = (255, 255, 255) v = (127, 0, 255) b = (0, 0, 255) z = (0, 0, 25) g = (25, 25, 25)
The sprites are defined as five by five arrays. In total, there are eleven sprite arrays.
sun_bitmap = [ y,a,y,a,y, a,y,y,y,a, y,y,y,y,y, a,y,y,y,a, y,a,y,a,y, ] cloud_bitmap = [ a,a,a,w,a, a,w,w,w,a, a,w,w,w,a, a,a,w,w,a, a,a,a,w,a, ] # etc
weather_codes
is a dictionary that contains the weather codes and the associated daytime and nighttime sprite. The codes are WMO Codes as defined by NOAA. There are 100 possible codes, ranging from 0
to 99
. In code.py, these weather codes are used as indexes to access the appropriate sprite.
weather_codes = [ {"code" : 0, "day_img" : sun_bitmap, "night_img" : night_bitmap}, {"code" : 1, "day_img" : sun_bitmap, "night_img" : night_bitmap}, {"code" : 2, "day_img" : sun_bitmap, "night_img" : night_bitmap}, ... {"code" : 99, "day_img" : thunder_bitmap, "night_img" : nightThunder_bitmap} ]
How the CircuitPython Code Works
At the top of the code are variables that can be edited to customize your code.py file for your needs. min_temp
and max_temp
are used in the map_range()
function that determine the color of the temperature text. The min_temp
number can be negative if you are in a colder climate. daytime_min
and daytime_max
are used to determine the hours in which a daytime sprite is shown versus a nighttime sprite. lat
and long
hold your location's latitude and longitude. Finally, temperature_unit
and temp_unit
hold either Fahrenheit or Celsius.
# minimum expected temperature min_temp = 0 # maximum expected temperature max_temp = 100 # first daylight hour daytime_min = 7 # last daylight hour daytime_max = 17 # latitude lat = 42.36 # longitude long = -71.06 # temp unit for API request temperature_unit = "fahrenheit" # temp unit for display temp_unit = "F"
The Request URL
The API request to Open-Meteo is passed as a URL. lat
, long
and temperature_unit
are passed to the URL string. Open-Meteo has documentation with more information on building a URL for an API request.
# API request to open-meteo weather_url = "https://api.open-meteo.com/v1/forecast?" # pass latitude and longitude weather_url += "latitude=%d&longitude=%d&timezone=auto" % (lat, long) # pass temperature_unit weather_url += "¤t_weather=true&temperature_unit=%s&windspeed_unit=mph" % temperature_unit
Get the Weather
The function get_the_weather()
is used to make the API request and return values for the current weather condition, temperature, the color mapped to the current temperature and the current time.
def get_the_weather(): # make the API request response = requests.get(weather_url) # packs the response into a JSON response_as_json = response.json() print() # prints the entire JSON print(response_as_json) print() # gets current weather code w = int(response_as_json['current_weather']['weathercode']) # gets temperature t = response_as_json['current_weather']['temperature'] temp_int = int(t) t_c = simpleio.map_range(temp_int, min_temp, max_temp, 255, 0) # gets time json_time = response_as_json['current_weather']['time'] n_t = json_time.rsplit("T", 1)[-1] n_t = int(n_t[:2]) return w, t, t_c, n_t # initial API call weather, temp, temp_color, new_time = get_the_weather()
Import the Sprites
A for
statement packs the list entries from weather_codes
into three different arrays: codes
for the weather code numbers, day_images
for the daytime sprites and night_images
for the nighttime sprites.
for i in weather_codes: codes.append(i['code']) day_images.append(i['day_img']) night_images.append(i['night_img'])
Day or Night?
The function day_or_night()
determines which sprite is shown based on the weather condition and time of day.
# checks if it's day or night based on hour def day_or_night(t): if t in range(daytime_min, daytime_max): z = day_images[weather] else: z = night_images[weather] return z # initial sprite selection img = day_or_night(new_time)
# draw bitmap sprite def draw_sprite(c): for pixel in img: pixels[c] = pixel pixels.show() c += 1 time.sleep(0.001) c = 0
Tick, tick, tick...
For tracking time, the ticks
library is used. ticks
manages time in milliseconds, rather than seconds like in time.monotonic()
. The API will be called every fifteen minutes, or every 900000
milliseconds.
# ticks time tracker clock = ticks_ms() # 15 minutes in milliseconds weather_check = 900000
The Loop
In the loop, the API is called every 15 minutes. The sprite and scrolling temperature text is updated depending on the data that is returned.
# checks the time if ticks_diff(ticks_ms(), clock) > weather_check: print("pinging Open-Meteo") # make the API request with function # return weather ID, temp, temp color & hour weather, temp, temp_color, new_time = get_the_weather() # checks if it's day or night based on hour # & returns day or night version of sprite img = day_or_night(new_time) label.text = " %s°%s " % (temp, temp_unit) # reset clock clock = ticks_add(clock, weather_check)
Scroll the Weather
The core functionality of the display loops continuously. The sprite is drawn to the 5x5 grid and is shown for five seconds. Then, the temperature text is scrolled across the display. Once the text finishes scrolling, the process begins again by drawing the sprite.
# draw bitmap sprite draw_sprite(count) # blocking delay to hold the sprite on the display time.sleep(5) # draw scrolling text for v in range(2): for i in range(text.width): # Scoot the old text left by 1 pixel pixels[:20] = pixels[5:] # adjust color based on temperature color = colorwheel(temp_color) # Draw in the next line of text for y in range(5): # Select black or color depending on the bitmap pixel pixels[20+y] = color * text[i,y] pixels.show() time.sleep(.1)
Page last edited January 22, 2025
Text editor powered by tinymce.