Code Walkthrough

Importing Libraries

Download: file
import board
import displayio
from adafruit_bitmap_font import bitmap_font
from adafruit_button import Button
import adafruit_touchscreen
from digitalio import DigitalInOut

import busio
import neopixel
from adafruit_esp32spi import adafruit_esp32spi
from adafruit_esp32spi import adafruit_esp32spi_wifimanager

# import lifx library
import adafruit_lifx

The code first imports all of the libraries required to run the Smart Lighting Controller. 

Note that the code imports a special adafruit_lifx library. To communicate with the LIFX API, we wrote a CircuitPython helper module called CircuitPython_LIFX. This module makes HTTP requests ( or, "talks") to the LIFX API server in order to interact with the bulbs.

Configuring the PyPortal

Download: file
# ESP32 SPI
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)

# These pins are used as both analog and digital! XL, XR and YU must be analog
# and digital capable. YD just need to be digital
ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR,
                                      board.TOUCH_YD, board.TOUCH_YU,
                                      calibration=((5200, 59000), (5800, 57000)),
                                      size=(320, 240))

The next chunk of code grabs information from a secrets.py file including wifi configuration and your LIFX Access Token. Then, it sets up the ESP32's SPI connections for use with the PyPortal. The wifi object is set up here too - it's used later in the code to communicate with the LIFX API. 

Configuring the LIFX Helper Module

Download: file
# Set this to your LIFX personal access token in secrets.py
# (to obtain a token, visit: https://cloud.lifx.com/settings)
lifx_token = secrets['lifx_token']

# Initialize the LIFX API Helper
lifx = adafruit_lifx.LIFX(wifi, lifx_token)

# Set these to your LIFX light selector (https://api.developer.lifx.com/docs/selectors)
lifx_lights = ['label:Lamp', 'label:Bedroom']
# set default light properties
current_light = lifx_lights[0]
light_brightness = 1.0

The lifx_token entry within secrets is set to a new variable, lifx_token, and passed into the LIFX helper along with the wifi object created earlier.

Then, the code sets up a list of two lights (identified by their selectors). Later in this code, these lights are linked to buttons so the code can identify which light is being toggled. 

Also, a default light brightness of 100% is set, along with a current_light variable used to track the light being addressed.

Button Setup

Download: file
# Make the display context
button_group = displayio.Group(max_size=20)
board.DISPLAY.show(button_group)
# preload the font
print('loading font...')
font = bitmap_font.load_font("/fonts/Arial-12.bdf")
glyphs = b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.: '
font.load_glyphs(glyphs)
# button properties
BUTTON_WIDTH = 60
BUTTON_HEIGHT = 60
buttons = []

Before the code can create buttons, it needs to create a displayio group to hold them. Here - the code selects a font, preloads it with glyphs, and sets button properties for BUTTON_WIDTH and BUTTON_HEIGHT.

Button Generation

Download: file
# list of color buttons and their properties
color_btn = [
    {'name':'red', 'pos':(15, 80), 'color':button_colors['red']},
    {'name':'white', 'pos':(85, 80), 'color':button_colors['white']},
    {'name':'orange', 'pos':(155, 80), 'color':button_colors['orange']},
    {'name':'yellow', 'pos':(225, 80), 'color':button_colors['yellow']},
    {'name':'pink', 'pos':(15, 155), 'color':button_colors['pink']},
    {'name':'green', 'pos':(85, 155), 'color':button_colors['green']},
    {'name':'blue', 'pos':(155, 155), 'color':button_colors['blue']},
    {'name':'purple', 'pos':(225, 155), 'color':button_colors['purple']}
]

# generate color buttons from color_btn list
for i in color_btn:
    button = Button(x=i['pos'][0], y=i['pos'][1],
                    width=BUTTON_WIDTH, height=BUTTON_HEIGHT, name=i['name'],
                    fill_color=i['color'], style=Button.ROUNDRECT)
    buttons.append(button)

# light property buttons and their properties
prop_btn = [
    {'name':'onoff', 'pos':(15, 15), 'label':'on/off'},
    {'name':'up', 'pos':(75, 15), 'label':'+'},
    {'name':'down', 'pos':(135, 15), 'label':'-'},
    {'name':'lamp', 'pos':(195, 15), 'label':'lamp'},
    {'name':'room', 'pos':(245, 15), 'label':'room'}
]

# generate property buttons from prop_btn list
for i in prop_btn:
    button = Button(name=i['name'], x=i['pos'][0], y=i['pos'][1],
                    width=40, height=40, label=i['label'],
                    label_font=font, style=Button.SHADOWROUNDRECT)
    buttons.append(button)

# add buttons to the group
for b in buttons:
    button_group.append(b.group)

The next chunk of code creates buttons for the light colors and properties.

First, a list of dictionary items is created corresponding to button properties. The color_btn list contains information for the button such as the button's name, position on the display, and hex color value. The prop_btn list contains the button's name, position and text label.

Then, buttons are generated from the list and appended to a button list. When all the buttons have been added to button list, they are appended to the displayio button_group one-by-one.

Main Loop

Download: file
while True:
    touch = ts.touch_point
    if touch:
        for i, button in enumerate(buttons):
            if button.contains(touch):
              button.selected = True

The main loop checks for if the screen was touched. If it was, it searches for which button in the button list was touched.

Once the button was determined, the button's selected property is toggled - inverting the button's color until the action is performed. By doing this, you are creating a visual status indicator.

Download: file
                if button.name == 'lamp':
                    current_light = lifx_lights[0]
                    print('Switching to ', current_light)
                elif button.name == 'room':
                    current_light = lifx_lights[1]
                    print('Switching to ', current_light)

Then, the code checks the button's name against its function (what it does). If the button's name is lamp or room, current_light is toggled to the value of the button.

Whenever the code in the main loop sends data to the LIFX API to perform an action on a light, it uses the current_light variable to select the light.

Download: file
                elif button.name == 'onoff':
                    print('Toggling {0}...'.format(current_light))
                    lifx.toggle_light(current_light)
                elif button.name == 'up':
                    light_brightness += 0.25
                    print('Setting {0} brightness to {1}'.format(current_light, light_brightness))
                    lifx.set_brightness(current_light, light_brightness)
                elif button.name == 'down':
                    light_brightness -= 0.25
                    print('Setting {0} brightness to {1}'.format(current_light, light_brightness))
                    lifx.set_brightness(current_light, light_brightness)
                else:
                    print('Setting {0} color to {1}'.format(current_light, button.name))
                    lifx.set_color(current_light, 'on', button.name, light_brightness)
                button.selected = False

The rest of the code performs an action based on the button's name by using the adafruit_lifx helper module. 

When the on/off button is pressed - the code calls lifx.toggle_light and passes in the current_light.

When the brightness button (+ or -) is pressed, the code passes the current_light and light_brightness into lifx.set_brightness. Also, the light_brightness increments or decrements by 1/4 each time the button is pressed.

If the button's name doesn't correspond to a predefined name, we set the color using the button's name as the color passed into lifx.set_color.

Finally, the code sets its selected property to false, returning it to its original color to indicate an action has been performed.

Going further!

This guide has only scratched the surface of the LIFX Light Remote HTTP API. There are lots more endpoints, options, and effects to play around with. If you are interested in seeing how this works (in a deeper level) - take a look at the adafruit_lifx.py file located in the helper library's repository. It might be helpful to keep the LIFX API website open in another tab, for reference. 

If you end up adding a feature to the library - pull requests on the GitHub repository are appreciated!

This guide was first published on Apr 11, 2019. It was last updated on Apr 11, 2019. This page (Code Walkthrough) was last updated on Aug 31, 2019.