Importing Libraries 

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 Philips Hue Bridge
from adafruit_hue import Bridge

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

Note that the code imports a special adafruit_hue library. To communicate with the Hue Bridge, we wrote a CircuitPython helper module called CircuitPython_Hue. This module makes HTTP requests ( or, "talks") to the Hue Bridge's IP address in order to interact with the bulbs.

Configuring the PyPortal

# Get wifi details and more from a secrets.py file
try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise

# 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)

The next chunk of code grabs information from a secrets.py file including wifi configuration. 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 bridge.

Configuring the Hue helper module

# Attempt to load bridge username and IP address from secrets.py
try:
    username = secrets['hue_username']
    bridge_ip = secrets['bridge_ip']
    my_bridge = Bridge(wifi, bridge_ip, username)
except:
    # Perform first-time bridge setup
    my_bridge = Bridge(wifi)
    print('Finding bridge address...')
    ip = my_bridge.discover_bridge()
    print('Attempting to register username, press the link button on your Hue Bridge now!')
    username = my_bridge.register_username()
    print('ADD THESE VALUES TO SECRETS.PY: \
                            \n\t"bridge_ip":"{0}", \
                            \n\t"hue_username":"{1}"'.format(ip, username))
    raise

The entire Hue system is built around the Hue Bridge. To communicate with the bridge, you'll need to register a unique username (stored on the bridge) for the PyPortal. you'll also need the Bridge's IP address to communicate with it. There's a bit of a HTTP request/reply "dance" associated with this process and it's tricky.

The CircuitPython Hue library automates the process of discovering and registering a username with the bridge - but, you'll still need to add these values to the secrets.py file. 

Hue Color Setup

print('loading colors...')
# color conversions (RGB to Philips Hue-compatible HSB)
red = my_bridge.rgb_to_hsb([255, 0, 0])
white = my_bridge.rgb_to_hsb([255, 255, 255])
orange = my_bridge.rgb_to_hsb([255, 165, 0])
yellow = my_bridge.rgb_to_hsb([255, 255, 0])
green = my_bridge.rgb_to_hsb([0, 255, 0])
blue = my_bridge.rgb_to_hsb([0, 0, 255])
purple = my_bridge.rgb_to_hsb([128, 0, 128])
pink = my_bridge.rgb_to_hsb([255, 192, 203])

hue_hsb = {'red': red, 'white': white, 'orange': orange,
           'yellow': yellow, 'green': green, 'blue': blue,
           'purple': purple, 'pink': pink}

When you want to modify the light's state from within your code, you'd use the set_light method in CircuitPython_Hue. This method mirrors Philips' set light state API endpoint - but it has some non user-friendly features.

The Hue API uses HSB (Hue, Saturation, Brightness) instead of RGB (red, green, blue) values. It also does not take in brightness percentages, or hue degrees. Instead, it takes in scaled integer values which directly relate to to the brightness percentage or hue degrees. 

To make this simpler - we added a rgb_to_hsb method within the Hue helper module. Passing a list of RGB values into this method will return a scaled HSB list.

  • If you wish to use RGB colors with your PyPortal Hue Controller - use this method before calling set_light to convert and scale your colors.

Our code predefines a few colors (red, white, orange, yellow, green, blue, purple, pink) by converting them from RGB values to scaled HSB values. using rgb_to_hsb.

Button Setup

# 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

# button fill colors
button_colors = {'red':0xFF0000, 'white':0xFFFFFF,
                 'orange':0xFF9900, 'yellow':0xFFFF00,
                 'green':0x00FF00, 'blue':0x0000FF,
                 'purple':0x9900FF, 'pink': 0xFF00FF}
# 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':'room', 'pos':(195, 15), 'label':'room'},
    {'name':'lamp', 'pos':(255, 15), 'label':'lamp'}
]

# 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=55, 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.

Light Setup

# Hue Light/Group Identifiers
hue_lights={'lamp': 1, 'livingroom': 2}
hue_selector = hue_lights['lamp']

# Default to 25% brightness
current_brightness = 25

The code sets up hue light identifiers in hue_lights and automatically selects the lamp identifier in the list. This prevents sending the bridge a None type if a button is pressed and no light is selected. It also sets a brightness default of 25%. 

  • For more information about setting up light identifiers, read the Hue Lights Setup section of this guide.
  • To enumerate all the lights connected to the bridge, you can run the following line from your code: my_lights = my_bridge.get_lights and print the JSON response from the hue bridge: my_lights

Main Loop

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 set to True - inverting the button's color until the action is fully performed (i.e: the light is changed, toggled, etc). By doing this, you are creating a visual status indicator. 

 if button.name == 'living':
                    hue_selector = hue_lights['livingroom']
                    print('Switching to ', hue_selector)
                elif button.name == 'room':
                    hue_selector = hue_lights['lamp']
                    print('Switching to ', hue_selector)

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

Whenever the code in the main loop sends data to the bridge to modify a light, it uses hue_selector  to indicate the light being modified.

  elif button.name == 'onoff':
                    print('Toggling {0}...'.format(hue_selector))
                    my_bridge.toggle_light(int(hue_selector))

If the on/off button is tapped, the code will toggle the state of the selected light.

  • The Hue Light API does not support toggling light state - we added a toggle_light method into the CircuitPython_Hue library to get the current state of the light , toggle it, and call set_light with the toggled state of the light..
elif button.name == 'up':
                    current_brightness += 25
                    print('Setting {0} brightness to {1}'.format(hue_selector, current_brightness))
                    my_bridge.set_light(int(hue_selector), bri=current_brightness)
elif button.name == 'down':
                    current_brightness -= 25
                    print('Setting {0} brightness to {1}'.format(hue_selector, current_brightness))
                    my_bridge.set_light(int(hue_selector), bri=current_brightness)

When either of the brightness buttons are pressed, they'll modify the value of current_brightness by +/-25% and send that value to the bridge using set_light.

else:
  print('Setting {0} color to {1}'.format(hue_selector, button.name))
  my_bridge.set_light(light_id=int(hue_selector),
                      hue=int(hue_hsb[button.name][0]),
                      sat=int(hue_hsb[button.name][1]),
                      bri=int(hue_hsb[button.name][2]))
button.selected = False

If any of the color buttons are pressed, the color's hue, saturation, and brightness values are individually passed into set_light along with the light (hue_selector).

Since the code is finished processing the action, the button's fill is set to its original color by de-selecting the button (button.selected = False). 

Going further!

This guide has only scratched the surface of the Philips Hue API! We've detailed out a good amount of the API actions within the Adafruit_CircuitPython_Hue library, like discovering new bulbs or setting a scene.

If you didn't see the API action you were looking for, feel free to add it to the library. You may find it useful to read through the library and the API reference in different tabs, and cross-referencing them (Docs are here, you'll need to create an account)

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 19, 2019. It was last updated on Apr 19, 2019.

This page (Code Walkthrough) was last updated on Apr 17, 2021.

Text editor powered by tinymce.