We start by importing the library modules we need:

import time
import json
from secrets import secrets
import board
from adafruit_pyportal import PyPortal
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label
from digitalio import DigitalInOut, Direction, Pull
import analogio
import displayio
import adafruit_logging as logging

Notably, the file contains a dictionary containing hidden information (like api keys, wifi network credentials, etc.) and some general configuration information like location strings for the time and weather services. It's not in the repo, so copy the template below into a text editor and replace the placeholders with your information.

secrets = {
    'ssid' : 'your wifi ssid',
    'password' : 'your wifi password',
    'timezone' : None,
    'openweather_token': 'get it from',
    'celcius': True,
    'timezone': 'your timezone string',
    'city_id': 'your openweather city id'

You can find a list of cities and their IDs in

Save the file to the main (root) directory of your PyPortal CIRCUITPY flash drive.

Configuration and Variables

The data source and location for the PyPortal data fetch support needs to be set up to get the weather information:

# Set up where we'll be fetching data from
DATA_SOURCE = ''+secrets['city_id']
DATA_SOURCE += '&appid='+secrets['openweather_token']
# You'll need to get a token from, looks like 'b6907d289e10d714a6e88b30761fae22'

With that set up, we can create the PyPortal instance as well as the light sensor and snooze button input:

pyportal = PyPortal(url=DATA_SOURCE,

light = analogio.AnalogIn(board.LIGHT)

snooze_button = DigitalInOut(board.D3)
snooze_button.direction = Direction.INPUT
snooze_button.pull = Pull.UP

There is a variety of global variables having to do with managing the alarm, including snoozing:

alarm_background = 'red_alert.bmp'
alarm_file = 'alarm.wav'
alarm_enabled = True
alarm_armed = True
alarm_interval = 10.0
alarm_hour = 7
alarm_minute = 00
snooze_time = None
snooze_interval = 600.0

Finally there are variables relating to weather display, update timing, and tracking the light level (to avoid repeatedly updating the display).

icon_file = None
icon_sprite = None
celcius = secrets['celcius']

refresh_time = None
update_time = None
weather_refresh = None
current_time = None

low_light = False

To provide some visual variation as well as fit more information on the screen, three fonts are used in this project:

  1. a large font for displaying the current time,
  2. a medium font for displaying the alarm time, and
  3. a small font for displaying the temperature.

The time is the main piece of information so we want a big, bold, highly visible font for that. Having the alarm time visible is nice to have, and it also provides a clear indication of whether the alarm is turned on. The weather and temperature aren't crucial, so a smaller font suffices, as well as being sized in keeping with the weather graphic.

# Load the fonts

time_font = bitmap_font.load_font('/fonts/Anton-Regular-104.bdf')
time_font.load_glyphs(b'0123456789:') # pre-load glyphs for fast printing

alarm_font = bitmap_font.load_font('/fonts/Helvetica-Bold-36.bdf')

temperature_font = bitmap_font.load_font('/fonts/Arial-16.bdf')


The logger module (see this guide for information on it) is used for debug and error output. It's initial included in the repository, but will be in the bundle eventually. It needs to be initialized and the logging level set. If you want to see debug information, set this to logging.DEBUG instead.

logger = logging.getLogger('alarm_clock')
logger.setLevel(logging.ERROR)            # change as desired

Support Functions

In addition to the state classes, there are a handful of support functions.

create_text_areas created a list of TextArea instances based on a list of dictionaries that define the position, font, color, and text length of each area:

def create_text_areas(configs):
    """Given a list of area specifications, create and return test areas."""
    text_areas = []
    for cfg in configs:
        textarea = Label(cfg['font'], text=' '*cfg['size'])
        textarea.x = cfg['x']
        textarea.y = cfg['y']
        textarea.color = cfg['color']
    return text_areas

We have a clear_splash function that removes display elements from the PyPortal, leaving only the background. Since the background is set up first by the PyPortal code, this can simply pop off elements that have been added since then.

def clear_splash():
    for _ in range(len(pyportal.splash) - 1):

Finally we have a function that takes a touch (which is a tuple of x, y, and pressure values) and a button definition (a dictionary with left, right, top, and bottom keys) and returns whether the touch is in the area covered by the button.

def touch_in_button(t, b):
    in_horizontal = b['left'] <= t[0] <= b['right']
    in_vertical = b['top'] <= t[1] <= b['bottom']
    return in_horizontal and in_vertical

This guide was first published on Mar 27, 2019. It was last updated on Jul 17, 2024.

This page (Setup) was last updated on Mar 08, 2024.

Text editor powered by tinymce.