The Time state is the most complex. It:

  • displays the current time, syncing it occasionally from a time service,
  • displays the current weather condition and temperature fetched occasionally from a weather service,
  • triggers the alarm when the alarm time is reached,
  • triggers the alarm when snooze timeout expires, and
  • provides access to the settings and mugsy states.

It also has the busiest screen.

__init__

The constructor sets up the background files, timers, and screen elements. Notice that each button includes the left, right, top, and bottom of the area it covers, as well as the state to transition to if that button is touched.

    def __init__(self):
        super().__init__()
        self.background_day = 'main_background_day.bmp'
        self.background_night = 'main_background_night.bmp'
        self.refresh_time = None
        self.update_time = None
        self.weather_refresh = None
        text_area_configs = [dict(x=88, y=170, size=5, color=0xFFFFFF, font=time_font),
                             dict(x=210, y=50, size=5, color=0xFF0000, font=alarm_font),
                             dict(x=88, y=90, size=6, color=0xFFFFFF, font=temperature_font)]
        self.text_areas = create_text_areas(text_area_configs)
        self.weather_icon = displayio.Group()
        self.weather_icon.x = 88
        self.weather_icon.y = 20
        self.icon_file = None

        self.snooze_icon = displayio.Group()
        self.snooze_icon.x = 260
        self.snooze_icon.y = 70
        self.snooze_file = None

        # each button has it's edges as well as the state to transition to when touched
        self.buttons = [dict(left=0, top=50, right=80, bottom=120, next_state='settings'),
                        dict(left=0, top=155, right=80, bottom=220, next_state='mugsy')]

The time state has a helper method to adjust the screen for lighting level. At low levels, it will dim the display backlight and switch to a red-based background. At higher light levels, it sets the display to full brightness and uses a blue-based background.

    def adjust_backlight_based_on_light(self, force=False):
        """Check light level. Adjust the backlight and background image if it's dark."""
        global low_light
        if light.value <= 1000 and (force or not low_light):
            pyportal.set_backlight(0.01)
            pyportal.set_background(self.background_night)
            low_light = True
        elif force or (light.value >= 2000 and low_light):
            pyportal.set_backlight(1.00)
            pyportal.set_background(self.background_day)
            low_light = False

tick

This is a rather complex method and we'll work through it one piece at a time.

This state has two things to do with snoozing:

  1. Check if the snooze button is pressed. If so turn off snooze. If snooze was active, remove the snooze indicator from the screen.
  2. Check if the snooze timer has timed out. If so transition to the alarm state.
# is the snooze button pushed? Cancel the snooze if so.
        if not snooze_button.value:
            if snooze_time:
                self.snooze_icon.pop()
            snooze_time = None
            alarm_armed = False

        # is snooze active and the snooze time has passed? Transition to alram is so.
        if snooze_time and ((now - snooze_time) >= snooze_interval):
            change_to_state('alarm')
            return

Once snoozing is dealt with, the background is adjusted using the method described earlier.

        # check light level and adjust background & backlight
        self.adjust_backlight_based_on_light()

Every hour the time is synced with a time service. This keeps the time accurate enough without requiring real time clock (RTC) hardware. It only syncs hourly so as to not consume too much time/power doing the WiFi communication as well as keeping use of the service minimal. This is handled by the PyPortal class, so the code here is short:

# only query the online time once per hour (and on first run)
        if (not self.refresh_time) or ((now - self.refresh_time) > 3600):
            try:
                pyportal.get_local_time(location=secrets['timezone'])
                self.refresh_time = now
            except RuntimeError as e:
                self.refresh_time = now - 3000   # delay 10 minutes before retrying
                logger.error('Some error occured, retrying! - %s', str(e))

Similarly, the weather data is fetched periodically. In this case. every 10 minutes. Even though this uses the PyPortal data fetch and extract support, it is a bit more involved: In addition to formatting and displaying the temperature, the appropriate weather icon needs to be selected and displayed.

# only query the weather every 10 minutes (and on first run)
        if (not self.weather_refresh) or (now - self.weather_refresh) > 600:
            logger.debug('Fetching weather')
            try:
                value = pyportal.fetch()
                weather = json.loads(value)

                # set the icon/background
                weather_icon_name = weather['weather'][0]['icon']
                try:
                    self.weather_icon.pop()
                except IndexError:
                    pass
                filename = "/icons/"+weather_icon_name+".bmp"

                if filename:
                    # CircuitPython 6 & 7 compatible
                    if self.icon_file:
                        self.icon_file.close()
                    self.icon_file = open(filename, "rb")
                    icon = displayio.OnDiskBitmap(self.icon_file)

                    icon_sprite = displayio.TileGrid(icon,
                                                     pixel_shader=getattr(icon, 'pixel_shader', displayio.ColorConverter()),
                                                     x=0, y=0)

                    # # CircuitPython 7+ compatible
                    # icon = displayio.OnDiskBitmap(filename)
                    # icon_sprite = displayio.TileGrid(icon, pixel_shader=icon.pixel_shader)

                    self.weather_icon.append(icon_sprite)

                temperature = weather['main']['temp'] - 273.15 # its...in kelvin
                if celcius:
                    temperature_text = '%3d C' % round(temperature)
                else:
                    temperature_text = '%3d F' % round(((temperature * 9 / 5) + 32))
                self.text_areas[2].text = temperature_text
                self.weather_refresh = now
                try:
                    board.DISPLAY.refresh(target_frames_per_second=60)
                except AttributeError:
                    board.DISPLAY.refresh_soon()
                    board.DISPLAY.wait_for_frame()


            except RuntimeError as e:
                self.weather_refresh = now - 540   # delay a minute before retrying
                logger.error("Some error occured, retrying! - %s", str(e))

The final part of this method checks to see if it's time to update the displayed time. If so, it does.

If we are not snoozing. we check whether it is time to sound the alarm (remember that snoozing is handled earlier by using a timer). If so, and the alarm is armed we transition to the alarm state. If it's not alarm time. we arm the alarm (if it's enabled) so that it will sound the next day when the time again reaches the set time.

        if (not update_time) or ((now - update_time) > 30):
            # Update the time
            update_time = now
            current_time = time.localtime()
            time_string = '%02d:%02d' % (current_time.tm_hour,current_time.tm_min)
            self.text_areas[0].text = time_string
            board.DISPLAY.refresh_soon()
            board.DISPLAY.wait_for_frame()

            # Check if alarm should sound
        if current_time is not None and not snooze_time:
            minutes_now = current_time.tm_hour * 60 + current_time.tm_min
            minutes_alarm = alarm_hour * 60 + alarm_minute
            if minutes_now == minutes_alarm:
                if alarm_armed:
                    change_to_state('alarm')
            else:
                alarm_armed = alarm_enabled

touch

This is only concerned with the alarm settings and mugsy buttons. For each button, it checks if the touch (if there is one) is in its area. If so the associated state is transitioned to and the loop terminates.

    def touch(self, t, touched):
        if t:
            logger.debug('touched: %d, %d', t[0], t[1])
        if t and not touched:             # only process the initial touch
            for button_index in range(len(self.buttons)):
                b = self.buttons[button_index]
                if touch_in_button(t, b):
                    change_to_state(b['next_state'])
                    break
        return bool(t)

enter

This is another busy method. It starts by updating the background, forcing the update regardless of what it thinks the light level is.

The text areas created in the constructor are added to the display, as is the weather icon and, if appropriate, the snooze indicator.

If the alarm is enabled, the alarm time is displayed.

Finally the display is updated.

def enter(self):
        self.adjust_backlight_based_on_light(force=True)
        for ta in self.text_areas:
            pyportal.splash.append(ta)
        pyportal.splash.append(self.weather_icon)
        if snooze_time:
            # CircuitPython 6 & 7 compatible
            if self.snooze_file:
                self.snooze_file.close()
            self.snooze_file = open('/icons/zzz.bmp', "rb")
            icon = displayio.OnDiskBitmap(self.snooze_file)
            icon_sprite = displayio.TileGrid(icon,
                                             pixel_shader=getattr(icon, 'pixel_shader', displayio.ColorConverter()))

            # # CircuitPython 7+ compatible
            # icon = displayio.OnDiskBitmap("/icons/zzz.bmp")
            # icon_sprite = displayio.TileGrid(icon, pixel_shader=icon.pixel_shader)

            self.snooze_icon.append(icon_sprite)
            pyportal.splash.append(self.snooze_icon)
        if alarm_enabled:
            self.text_areas[1].text = '%2d:%02d' % (alarm_hour, alarm_minute)
        else:
            self.text_areas[1].text = '     '
        try:
            board.DISPLAY.refresh(target_frames_per_second=60)
        except AttributeError:
            board.DISPLAY.refresh_soon()
            board.DISPLAY.wait_for_frame()

exit

The exit method calls the super to clear out the views, as well as clearing out the snooze indicator.

    def exit(self):
        super().exit()
        for _ in range(len(self.snooze_icon)):
            self.snooze_icon.pop()

This guide was first published on Mar 27, 2019. It was last updated on Nov 30, 2023.

This page (The Time State) was last updated on Mar 02, 2019.

Text editor powered by tinymce.