Secrets File Setup

Open the secrets.py file on your CircuitPython device using Mu or your favorite text editor. If you don't have one, copy the generic one below. You're going to edit this file to enter your wifi and location info.

  • Change aio_username to your Adafruit IO username
  • Change aio_key to your Adafruit IO active key. Don't use the one in the example below, it's just a placeholder
  • Change timezone to your time zone. Find the correct one here.
  • Change latitude and longitude to your location. Get this info by looking up your house in Google Maps and right-clicking on that spot

Your secrets.py file should look something like this: 

# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it

secrets = {
    'ssid' : 'Wifi_Network',
    'password' : 'Wifi_Password',
    'timezone' : "America/Los_Angeles", # http://worldtimeapi.org/timezones
    'aio_username'  :  'Username',
    'aio_key' : '36e7bd431d4lwd9mx21x8b10d9be7a77d8d5',
    'latitude'  : 38.86965735043201, 
    'longitude' : -121.07998478906876,
	}

Installing Project Code

To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.

Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory Shadow_Box/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.

Your CIRCUITPY drive should now look similar to the following image:

CIRCUITPY
# SPDX-FileCopyrightText: 2021 Erin St Blaine for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
Clock & sky colorbox for Adafruit MagTag: displays current time while
NeoPixels provide theme lighting for the time of day. Requires WiFi
internet access -- configure credentials in secrets.py. An Adafruit IO
user name and API key are also needed there, plus timezone and
geographic coords.
"""

# pylint: disable=import-error
import time
import json
import board
import neopixel
from adafruit_magtag.magtag import MagTag
import adafruit_fancyled.adafruit_fancyled as fancy

# UTC offset queries require some info from the secrets table...
try:
    from secrets import secrets
except ImportError:
    print('Please set up secrets.py with network credentials.')


# CONFIGURABLE SETTINGS ----------------------------------------------------

USE_AMPM_TIME = True # Set to False to use 24-hour time (e.g. 18:00)
NUM_LEDS = 22        # Length of NeoPixel strip
BRIGHTNESS = 0.9     # NeoPixel brightness: 0.0 (off) to 1.0 (max)
SPIN_TIME = 10 * 60  # Seconds for NeoPixels to complete one revolution
# Default spin time is 10 minutes. It should be very slow...imperceptible
# really...as there will be pauses when network activity is occurring.

DAY_PALETTE = [               # Daylight colors
    fancy.CRGB(0.5, 0, 1.0),  # Purplish blue
    fancy.CRGB(0, 0.5, 1.0),  # Blue
    fancy.CRGB(0, 0.5, 1.0),  # Blue
    0x1B90FF,                 # Cyan
    fancy.CRGB(0, 0.8, 0.2),  # Green
    fancy.CRGB(0, 0.8, 0.2),  # Green
    0xFFEA0A,                 # Yellow
    0xFFEA0A,                 # Yellow
    0xFFEA0A,                 # Yellow
    0xFFEA0A,                 # Yellow
    0x30FEF2,                 # Sky blue
    0x0C69FC,                 # Sky blue
    0x1A82FF,
    fancy.CRGB(0, 0.8, 0.8),  # Green
    fancy.CRGB(0, 0.8, 0.2),  # Green
    fancy.CRGB(0, 0.8, 0.2),  # Green
    fancy.CRGB(0.5, 0, 1.0),] # Purplish blue

NIGHT_PALETTE = [ # Starlight colors
    fancy.CRGB(0, 0, 1.0),
    fancy.CRGB(0, 0.2, 1.0),
    fancy.CRGB(0, 0.1, 1.0),
    fancy.CRGB(0, 0, 1.0),
    0x000000,
    0x000000,
    0x000000,
    fancy.CRGB(1.0, 1.0, 0.8),
    0x000000,
    fancy.CRGB(0.3, 0.3, 0.3),
    fancy.CRGB(0.2, 0.2, 0.2),
    fancy.CRGB(0.3, 0.3, 0.3),
    0x000000,
    0x000000,
    0x000000,
    0x000000]

HORIZON_PALETTE = [            # Dawn & dusk colors
    fancy.CHSV(0.8),           # Purple
    fancy.CHSV(1.0),           # Red
    fancy.CHSV(1.0),           # Red
    fancy.CRGB(1.0, 0.5, 0.0), # Orange
    fancy.CRGB(1.0, 0.5, 0.0), # Orange
    fancy.CRGB(1.0, 0.8, 0.0), # Yellow
    0xFFFFFF,                  # White
    fancy.CRGB(1.0, 0.8, 0.0), # Yellow
    fancy.CRGB(1.0, 0.5, 0.0)] # Orange


# SOME UTILITY FUNCTIONS ---------------------------------------------------

def hh_mm(time_struct, twelve_hour=True):
    """ Given a time.struct_time, return a string as H:MM or HH:MM, either
        12- or 24-hour style depending on twelve_hour flag.
    """
    postfix = ""
    if twelve_hour:
        if time_struct.tm_hour > 12:
            hour_string = str(time_struct.tm_hour - 12) # 13-23 -> 1-11 (pm)
            postfix = "pm"
        elif time_struct.tm_hour > 0:
            hour_string = str(time_struct.tm_hour) # 1-12
            postfix = "am"
        else:
            hour_string = '12' # 0 -> 12 (am)
            postfix = "pm"
    else:
        hour_string = '{hh:02d}'.format(hh=time_struct.tm_hour)
    return hour_string + ':{mm:02d}'.format(mm=time_struct.tm_min) + postfix

def parse_time(timestring):
    """ Given a string of the format YYYY-MM-DDTHH:MM:SS.SS-HH:MM (and
        optionally a DST flag), convert to and return a numeric value for
        elapsed seconds since midnight (date, UTC offset and/or decimal
        fractions of second are ignored).
    """
    date_time = timestring.split('T') # Separate into date and time
    hour_minute_second = date_time[1].split('+')[0].split('-')[0].split(':')
    return (int(hour_minute_second[0]) * 3600 +
            int(hour_minute_second[1]) * 60 +
            int(hour_minute_second[2].split('.')[0]))

def blend(palette1, palette2, weight2, offset):
    """ Given two FancyLED color palettes and a weighting (0.0 to 1.0) of
        the second palette, plus a positional offset (where 0.0 is the start
        of each palette), fill the NeoPixel strip with an interpolated blend
        of the two palettes.
    """
    weight2 = min(1.0, max(0.0, weight2)) # Constrain input to 0.0-1.0
    weight1 = 1.0 - weight2               # palette1 weight (inverse of #2)
    for i in range(NUM_LEDS):
        position = offset + i / NUM_LEDS
        color1 = fancy.palette_lookup(palette1, position)
        color2 = fancy.palette_lookup(palette2, position)
        # Blend the two colors based on weight1&2, run through gamma func:
        color = fancy.CRGB(
            color1[0] * weight1 + color2[0] * weight2,
            color1[1] * weight1 + color2[1] * weight2,
            color1[2] * weight1 + color2[2] * weight2)
        color = fancy.gamma_adjust(color, brightness=BRIGHTNESS)
        PIXELS[i] = color.pack()
    PIXELS.show()


# ONE-TIME INITIALIZATION --------------------------------------------------

MAGTAG = MagTag()

MAGTAG.graphics.set_background("/background.bmp")

MAGTAG.add_text(
    text_font="Lato-Regular-74.pcf",
    text_position=(MAGTAG.graphics.display.width // 2, 30),
    text_anchor_point=(0.5, 0),
    is_data=False,
)

# Declare NeoPixel object on pin D10 with NUM_LEDS pixels, no auto-write.
# Set brightness to max as we'll be using FancyLED's brightness control.
PIXELS = neopixel.NeoPixel(board.D10, NUM_LEDS, brightness=0.1,
                           auto_write=False)
PIXELS.show() # Off at start

LAST_SYNC = time.monotonic() - 5000 # Force initial clock sync
LAST_MINUTE = -1                    # Force initial display update
LAST_DAY = -1                       # Force initial sun query
SUNRISE = 6 * 60 * 60               # Sunrise @ 6am by default
SUNSET = 18 * 60 * 60               # Sunset @ 6pm by default
UTC_OFFSET = '+00:00'               # Gets updated along with time
SUN_FLAG = False                    # Triggered at midnight

# MAIN LOOP ----------------------------------------------------------------

while True:
    if (time.monotonic() - LAST_SYNC) > 3600: # Sync time once an hour
        MAGTAG.network.get_local_time()
        LAST_SYNC = time.monotonic()
        # Sun API requires a valid UTC offset. Adafruit IO's time API
        # offers this, but get_local_time() above (using AIO) doesn't
        # store it anywhere. I’ll put in a feature request for the
        # PortalBase library, but in the meantime this just makes a
        # second request to the time API asking for that one value.
        # Since time is synced only once per hour, the extra request
        # isn't particularly burdensome.
        try:
            RESPONSE = MAGTAG.network.requests.get(
                'https://io.adafruit.com/api/v2/%s/integrations/time/'
                'strftime?x-aio-key=%s&tz=%s' % (secrets.get('aio_username'),
                                                 secrets.get('aio_key'),
                                                 secrets.get('timezone')) +
                '&fmt=%25z')
            if RESPONSE.status_code == 200:
                # Arrives as sHHMM, convert to sHH:MM
                print(RESPONSE.text)
                UTC_OFFSET = RESPONSE.text[:3] + ':' + RESPONSE.text[-2:]
        except: # pylint: disable=bare-except
            # If query fails, prior value is kept until next query.
            # Only changes 2X a year anyway -- worst case, if these
            # events even align, is rise/set is off by an hour.
            pass

    NOW = time.localtime() # Current time (as time_struct)

    # If minute has changed, refresh display
    if LAST_MINUTE != NOW.tm_min:
        MAGTAG.set_text(hh_mm(NOW, USE_AMPM_TIME), index=0)
        LAST_MINUTE = NOW.tm_min

    # If day has changed (local midnight), set flag for later sun query
    # (it's not done at midnight, see below).
    if LAST_DAY != NOW.tm_mday:
        SUN_FLAG = True
        LAST_DAY = NOW.tm_mday

    # If the sun flag is set, and if the time is 3:05 am or thereabouts,
    # query the sun API for new rise and set times for today. It's done
    # this way (rather than at midnight) to allow for DST time jumps
    # (which occur at 2am) and slight clock drift (corrected hourly),
    # but still before dawn.
    if SUN_FLAG and (NOW.tm_hour * 60 + NOW.tm_min > 185):
        try:
            URL = ('https://api.met.no/weatherapi/sunrise/2.0/.json?'
                   'lat=%s&lon=%s&date=%s-%s-%s&offset=%s' %
                   (secrets.get('latitude'), secrets.get('longitude'),
                    str(NOW.tm_year), '{0:0>2}'.format(NOW.tm_mon),
                    '{0:0>2}'.format(NOW.tm_mday), UTC_OFFSET))
            print('Fetching sun data via', URL)
            FULL_DATA = json.loads(MAGTAG.network.fetch_data(URL))
            SUN_DATA = FULL_DATA['location']['time'][0]
            SUNRISE = parse_time(SUN_DATA['sunrise']['time'])
            SUNSET = parse_time(SUN_DATA['sunset']['time'])
        except: # pylint: disable=bare-except
            # If any part of the sun API query fails (whether network or
            # bad inputs), just repeat the old sun rise/set times and we'll
            # try again tomorrow. These only shift by seconds or minutes
            # daily, and the LEDs are just for mood, not like we're
            # launching a Mars rocket, errors here are not catastrophic.
            # Very worst case is a query error on a DST time change day,
            # in which case rise/set lights will be off by about an hour
            # until next successful query.
            pass
        SUN_FLAG = False # Pass or fail, don't query again until tomorrow

    # Convert NOW into elapsed seconds since midnight
    NOW = time.mktime(NOW) - time.mktime((NOW.tm_year, NOW.tm_mon,
                                          NOW.tm_mday, 0, 0, 0,
                                          NOW.tm_wday, NOW.tm_yday,
                                          NOW.tm_isdst))
    # Compare current time (in seconds since midnight) against sun rise/set
    # times and do color fades within +/- 30 minutes of each.
    if SUNRISE < NOW < SUNSET:                  # Day (ish)
        if NOW - SUNRISE < (30 * 60):           # Between sunrise & daylight
            PALETTE1, PALETTE2 = HORIZON_PALETTE, DAY_PALETTE
            INTERP = (NOW - SUNRISE) / (30 * 60)
        elif SUNSET - NOW < (30 * 60):          # Between daylight & sunset
            PALETTE1, PALETTE2 = HORIZON_PALETTE, DAY_PALETTE
            INTERP = (SUNSET - NOW) / (30 * 60)
        else:                                   # Full daylight
            PALETTE1 = PALETTE2 = DAY_PALETTE   # Day sky
            INTERP = 0.0                        # No fade
    else:                                       # Night (ish)
        if 0 < SUNRISE - NOW < (30 * 60):       # Between night & sunrise
            PALETTE1, PALETTE2 = HORIZON_PALETTE, NIGHT_PALETTE
            INTERP = (SUNRISE - NOW) / (30 * 60)
        elif 0 < NOW - SUNSET < (30 * 60):      # Between sunset & night
            PALETTE1, PALETTE2 = HORIZON_PALETTE, NIGHT_PALETTE
            INTERP = (NOW - SUNSET) / (30 * 60)
        else:                                   # Full night
            PALETTE1 = PALETTE2 = NIGHT_PALETTE # Night sky
            INTERP = 0.0                        # No fade

    # Update NeoPixels based on time of day
    blend(PALETTE1, PALETTE2, INTERP, time.monotonic() / SPIN_TIME)
If you're having difficulty running this example, it could be because your MagTag CircuitPython firmware or library needs to be upgraded! Please be sure to follow https://learn.adafruit.com/adafruit-magtag/circuitpython to install the latest CircuitPython firmware and then also replace/update ALL the MagTag-specific libraries mentioned here https://learn.adafruit.com/adafruit-magtag/circuitpython-libraries-2

Troubleshooting

If you're still having trouble, head over to our main MagTag guide for some troubleshooting tips.

Customizing your Code

There are a few configurable settings to look at near the top of the code:

# CONFIGURABLE SETTINGS ----------------------------------------------------

USE_AMPM_TIME = True # Set to False to use 24-hour time (e.g. 18:00)
NUM_LEDS = 22        # Length of NeoPixel strip
BRIGHTNESS = 0.9     # NeoPixel brightness: 0.0 (off) to 1.0 (max)
SPIN_TIME = 10 * 60  # Seconds for NeoPixels to complete one revolution
# Default spin time is 10 minutes. It should be very slow...imperceptible
# really...as there will be pauses when network activity is occurring.

Change NUM_LEDS to match the number of pixels in your strip. You can also adjust the global brightness here. Change SPIN_TIME to speed up or slow down the animation speed.

Color Palettes

There are three color palettes in the code: a daytime palette, a night time palette, and a horizon palette (for sunrise and sunset). You can customize these to your heart's content. FancyLED makes it really easy to create custom palettes. You can add as many or as few colors as you like, and rearrange the order to suit your design. Learn more about FancyLED here.

DAY_PALETTE = [               # Daylight colors
    fancy.CRGB(0.5, 0, 1.0),  # Purplish blue
    fancy.CRGB(0, 0.5, 1.0),  # Blue
    fancy.CRGB(0, 0.5, 1.0),  # Blue
    0x1B90FF,                 # Cyan
    fancy.CRGB(0, 0.8, 0.2),  # Green
    fancy.CRGB(0, 0.8, 0.2),  # Green
    0xFFEA0A,                 # Yellow
    0xFFEA0A,                 # Yellow
    0xFFEA0A,                 # Yellow
    0xFFEA0A,                 # Yellow
    0x30FEF2,                 # Sky blue
    0x0C69FC,                 # Sky blue
    0x1A82FF,
    fancy.CRGB(0, 0.8, 0.8),  # Green
    fancy.CRGB(0, 0.8, 0.2),  # Green
    fancy.CRGB(0, 0.8, 0.2),  # Green
    fancy.CRGB(0.5, 0, 1.0),] # Purplish blue

NIGHT_PALETTE = [ # Starlight colors
    fancy.CRGB(0, 0, 1.0),
    fancy.CRGB(0, 0.2, 1.0),
    fancy.CRGB(0, 0.1, 1.0),
    fancy.CRGB(0, 0, 1.0),
    0x000000,
    0x000000,
    0x000000,
    fancy.CRGB(1.0, 1.0, 0.8),
    0x000000,
    fancy.CRGB(0.3, 0.3, 0.3),
    fancy.CRGB(0.2, 0.2, 0.2),
    fancy.CRGB(0.3, 0.3, 0.3),
    0x000000,
    0x000000,
    0x000000,
    0x000000]

HORIZON_PALETTE = [            # Dawn & dusk colors
    fancy.CHSV(0.8),           # Purple
    fancy.CHSV(1.0),           # Red
    fancy.CHSV(1.0),           # Red
    fancy.CRGB(1.0, 0.5, 0.0), # Orange
    fancy.CRGB(1.0, 0.5, 0.0), # Orange
    fancy.CRGB(1.0, 0.8, 0.0), # Yellow
    0xFFFFFF,                  # White
    fancy.CRGB(1.0, 0.8, 0.0), # Yellow
    fancy.CRGB(1.0, 0.5, 0.0)] # Orange

This guide was first published on Mar 03, 2021. It was last updated on Mar 03, 2021.

This page (Code Setup) was last updated on Jun 07, 2023.

Text editor powered by tinymce.