You've setup Shortcuts to log your HomeKit sensors to Adafruit IO. You've setup a Shortcut to have an Adafruit IO feed switch between Scenes. You even setup a hack to use Adafruit IO Actions and Shortcut Automations to seamlessly control your Scenes from Adafruit IO. Surely its time to call it a day?
Before you go off and take a nap, there's one more skateboard trick for you to show off with. With all of this backend work, you can use a Feather ESP32-S3 Reverse TFT running CircuitPython to display the sensor data and switch between Scenes with the press of a physical button.
Once you've finished setting up your Feather with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries # # SPDX-License-Identifier: MIT import time import os import ssl import wifi import socketpool import microcontroller import board import digitalio import displayio from adafruit_bitmap_font import bitmap_font from adafruit_display_text import bitmap_label from adafruit_display_shapes.circle import Circle from adafruit_display_shapes.roundrect import RoundRect from adafruit_ticks import ticks_ms, ticks_add, ticks_diff import adafruit_minimqtt.adafruit_minimqtt as MQTT aio_username = os.getenv("ADAFRUIT_AIO_USERNAME") aio_key = os.getenv("ADAFRUIT_AIO_KEY") # feeds! temp_feed = aio_username + "/feeds/eve-temp" # temperature sensor humid_feed = aio_username + "/feeds/eve-humid" # humidity sensor lux_feed = aio_username + "/feeds/eve-light" # lux sensor occupy_feed = aio_username + "/feeds/eve-occupy" # occupation sensor light_feed = aio_username + "/feeds/nanoleaf" # lightstrip # buttons button0 = digitalio.DigitalInOut(board.D0) button0.direction = digitalio.Direction.INPUT button0.pull = digitalio.Pull.UP button0_state = False button1 = digitalio.DigitalInOut(board.D1) button1.direction = digitalio.Direction.INPUT button1.pull = digitalio.Pull.DOWN button1_state = False button2 = digitalio.DigitalInOut(board.D2) button2.direction = digitalio.Direction.INPUT button2.pull = digitalio.Pull.DOWN button2_state = False display = board.DISPLAY group = displayio.Group() display.root_group = group # load background bitmap bitmap = displayio.OnDiskBitmap("/tft_bg.bmp") tile_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader) group = displayio.Group() group.append(tile_grid) # bitmap font font_file = "/roundedHeavy-26.bdf" font = bitmap_font.load_font(font_file) # text elements temp_text = bitmap_label.Label(font, text="00.0°C", x=55, y=70, color=0xFFFFFF) group.append(temp_text) humid_text = bitmap_label.Label(font, text="00.0%", x=120, y=70, color=0xFFFFFF) group.append(humid_text) lux_text = bitmap_label.Label(font, text="00 lx", x=190, y=70, color=0xFFFFFF) group.append(lux_text) occupy_text = bitmap_label.Label(font, text="Occupied?", x=128, y=display.height - 12, color=0xFFFFFF) group.append(occupy_text) onOff_circ = Circle(display.width - 12, display.height - 12, 10, fill=0xcc0000) group.append(onOff_circ) scene_select = RoundRect(0, 0, 42, 40, 8, fill=None, outline=0xcccc00, stroke=6) scene_y = [0, int(display.height / 2) - int(scene_select.height / 2), display.height - scene_select.height - 1] group.append(scene_select) display.root_group = group print() print("Connecting to WiFi...") # connect to your SSID wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) print("Connected to WiFi!") # pylint: disable=unused-argument # Define callback methods which are called when events occur def connected(client, userdata, flags, rc): # pylint: disable=unused-argument # This function will be called when the client is connected # successfully to the broker. print("Connected to Adafruit IO!") # Subscribe to all changes on feeds client.subscribe(temp_feed) client.subscribe(humid_feed) client.subscribe(lux_feed) client.subscribe(occupy_feed) client.subscribe(light_feed) def disconnected(client, userdata, rc): # pylint: disable=unused-argument # This method is called when the client is disconnected print("Disconnected from Adafruit IO!") def on_message(client, topic, msg): # pylint: disable=unused-argument # This method is called when a topic the client is subscribed to # has a new message. print(f"New message on topic {topic}") def on_temp_msg(client, topic, msg): print(f"temp feed data: {msg}°C") temp_text.text = f"{float(msg):.01f}°C" def on_humid_msg(client, topic, msg): print(f"humid feed data: {msg}%") humid_text.text = f"{float(msg):.01f}%" def on_lux_msg(client, topic, msg): print(f"lux feed data: {msg} lx") lux_text.text = f"{float(msg):.00f} lx" def on_occupy_msg(client, topic, msg): print(f"occupation feed data: {msg}") if msg == "1": onOff_circ.fill = 0x00cc00 else: onOff_circ.fill = 0xcc0000 def on_light_msg(client, topic, msg): print(f"light scene selected: {msg}") scene_select.y = scene_y[int(msg)] pool = socketpool.SocketPool(wifi.radio) ssl_context = ssl.create_default_context() # Initialize an Adafruit IO HTTP API object mqtt_client = MQTT.MQTT( broker="io.adafruit.com", port=1883, username=aio_username, password=aio_key, socket_pool=pool, ssl_context=ssl_context, ) # Setup the callback methods above mqtt_client.on_connect = connected mqtt_client.on_disconnect = disconnected mqtt_client.on_message = on_message mqtt_client.add_topic_callback(temp_feed, on_temp_msg) mqtt_client.add_topic_callback(humid_feed, on_humid_msg) mqtt_client.add_topic_callback(lux_feed, on_lux_msg) mqtt_client.add_topic_callback(occupy_feed, on_occupy_msg) mqtt_client.add_topic_callback(light_feed, on_light_msg) # Connect the client to the MQTT broker. print("Connecting to Adafruit IO...") mqtt_client.connect() clock_clock = ticks_ms() clock_timer = 5 * 1000 while True: try: if ticks_diff(ticks_ms(), clock_clock) >= clock_timer: mqtt_client.loop(timeout=1) clock_clock = ticks_add(clock_clock, clock_timer) # reset button state on release if button0.value and button0_state: button0_state = False if not button1.value and button1_state: button1_state = False if not button2.value and button2_state: button2_state = False # buttons change light scenes if not button0.value and not button0_state: mqtt_client.publish(light_feed, 0) scene_select.y = scene_y[0] button0_state = True if button1.value and not button1_state: mqtt_client.publish(light_feed, 1) scene_select.y = scene_y[1] button1_state = True if button2.value and not button2_state: mqtt_client.publish(light_feed, 2) scene_select.y = scene_y[2] button2_state = True except Exception as error: # pylint: disable=broad-except print(error) mqtt_client.disconnect() time.sleep(5) microcontroller.reset()
Upload the Code and Libraries to the Feather
After downloading the Project Bundle, plug your Feather into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Feather's CIRCUITPY drive.
- lib folder
- code.py
- roundedHeavy-26.bdf
- tft_bg.bmp
Your Feather CIRCUITPY drive should look like this after copying the lib folder, bitmap graphic, font file the code.py file.
Add Your settings.toml File
As of CircuitPython 8.0.0, there is support for Environment Variables. Environment variables are stored in a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file. Add your settings.toml file as described in the Create Your settings.toml File page earlier in this guide. You'll need to include your ADAFRUIT_AIO_USERNAME
, ADAFRUIT_AIO_KEY
, CIRCUITPY_WIFI_SSID
and CIRCUITPY_WIFI_PASSWORD
.
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here" ADAFRUIT_AIO_USERNAME = "your-username-here" ADAFRUIT_AIO_KEY = "your-key-here"
How the CircuitPython Code Works
At the top of the code, you'll add your feed names that you'll be monitoring over MQTT.
aio_username = os.getenv("ADAFRUIT_AIO_USERNAME") aio_key = os.getenv("ADAFRUIT_AIO_KEY") # feeds! temp_feed = aio_username + "/feeds/eve-temp" # temperature sensor humid_feed = aio_username + "/feeds/eve-humid" # humidity sensor lux_feed = aio_username + "/feeds/eve-light" # lux sensor occupy_feed = aio_username + "/feeds/eve-occupy" # occupation sensor light_feed = aio_username + "/feeds/nanoleaf" # lightstrip
Buttons
The Feather ESP32-S3 Reverse TFT has three buttons that you can use for user input. These are setup as digitalio
inputs.
# buttons button0 = digitalio.DigitalInOut(board.D0) button0.direction = digitalio.Direction.INPUT button0.pull = digitalio.Pull.UP button0_state = False button1 = digitalio.DigitalInOut(board.D1) button1.direction = digitalio.Direction.INPUT button1.pull = digitalio.Pull.DOWN button1_state = False button2 = digitalio.DigitalInOut(board.D2) button2.direction = digitalio.Direction.INPUT button2.pull = digitalio.Pull.DOWN button2_state = False
Graphics
The tft_bg.bmp graphic is shown on the display as a Bitmap
. This graphic has icons for the sensors and buttons. There are a few text objects for the data that will be shown from the feeds. Two shapes are also used to display data. A circle is used to show occupancy by changing color from red (unoccupied) to green (occupied). A rounded rectangle is used to outline the button labels to show which Scene is selected.
display = board.DISPLAY group = displayio.Group() display.root_group = group # load background bitmap bitmap = displayio.OnDiskBitmap("/tft_bg.bmp") tile_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader) group = displayio.Group() group.append(tile_grid) # bitmap font font_file = "/roundedHeavy-26.bdf" font = bitmap_font.load_font(font_file) # text elements temp_text = bitmap_label.Label(font, text="00.0°C", x=55, y=70, color=0xFFFFFF) group.append(temp_text) humid_text = bitmap_label.Label(font, text="00.0%", x=120, y=70, color=0xFFFFFF) group.append(humid_text) lux_text = bitmap_label.Label(font, text="00 lx", x=190, y=70, color=0xFFFFFF) group.append(lux_text) occupy_text = bitmap_label.Label(font, text="Occupied?", x=128, y=display.height - 12, color=0xFFFFFF) group.append(occupy_text) onOff_circ = Circle(display.width - 12, display.height - 12, 10, fill=0xcc0000) group.append(onOff_circ) scene_select = RoundRect(0, 0, 42, 40, 8, fill=None, outline=0xcccc00, stroke=6) scene_y = [0, int(display.height / 2) - int(scene_select.height / 2), display.height - scene_select.height - 1] group.append(scene_select) display.root_group = group
MQTT Methods
MQTT is used to connect to Adafruit IO. The first method connected()
subscribes to the five feeds defined at the top of the code.
def connected(client, userdata, flags, rc): # pylint: disable=unused-argument # This function will be called when the client is connected # successfully to the broker. print("Connected to Adafruit IO!") # Subscribe to all changes on feeds client.subscribe(temp_feed) client.subscribe(humid_feed) client.subscribe(lux_feed) client.subscribe(occupy_feed) client.subscribe(light_feed)
Five custom methods are defined for each feed. The methods for the temperature, humidity and lux feeds update the text with the new value when there is new data available on the feed. The occupancy sensor method changes the color of the circle depending on the value. The lightstrip method changes the y
coordinate of the rounded rectangle to outline the selected Scene.
def on_temp_msg(client, topic, msg): print(f"temp feed data: {msg}°C") temp_text.text = f"{float(msg):.01f}°C" def on_humid_msg(client, topic, msg): print(f"humid feed data: {msg}%") humid_text.text = f"{float(msg):.01f}%" def on_lux_msg(client, topic, msg): print(f"lux feed data: {msg} lx") lux_text.text = f"{float(msg):.00f} lx" def on_occupy_msg(client, topic, msg): print(f"occupation feed data: {msg}") if msg == "1": onOff_circ.fill = 0x00cc00 else: onOff_circ.fill = 0xcc0000 def on_light_msg(client, topic, msg): print(f"light scene selected: {msg}") scene_select.y = scene_y[int(msg)]
The callback methods are setup and then a connection is established.
pool = socketpool.SocketPool(wifi.radio) ssl_context = ssl.create_default_context() # Initialize an Adafruit IO HTTP API object mqtt_client = MQTT.MQTT( broker="io.adafruit.com", port=1883, username=aio_username, password=aio_key, socket_pool=pool, ssl_context=ssl_context, ) # Setup the callback methods above mqtt_client.on_connect = connected mqtt_client.on_disconnect = disconnected mqtt_client.on_message = on_message mqtt_client.add_topic_callback(temp_feed, on_temp_msg) mqtt_client.add_topic_callback(humid_feed, on_humid_msg) mqtt_client.add_topic_callback(lux_feed, on_lux_msg) mqtt_client.add_topic_callback(occupy_feed, on_occupy_msg) mqtt_client.add_topic_callback(light_feed, on_light_msg) # Connect the client to the MQTT broker. print("Connecting to Adafruit IO...") mqtt_client.connect()
The Loop
In the loop, ticks are used for non-blocking timekeeping. Every 5 seconds, the MQTT client loops to check for any new messages on the feeds. If a new message is detected, the callback methods will run, automatically updating the values on the TFT.
Each of the three buttons sends a different value to the lightstrip feed using the publish()
function. These values correspond with the values defined in the Shortcut for setting the light Scenes (0
-2
).
while True: try: if ticks_diff(ticks_ms(), clock_clock) >= clock_timer: mqtt_client.loop(timeout=1) clock_clock = ticks_add(clock_clock, clock_timer) # reset button state on release if button0.value and button0_state: button0_state = False if not button1.value and button1_state: button1_state = False if not button2.value and button2_state: button2_state = False # buttons change light scenes if not button0.value and not button0_state: mqtt_client.publish(light_feed, 0) scene_select.y = scene_y[0] button0_state = True if button1.value and not button1_state: mqtt_client.publish(light_feed, 1) scene_select.y = scene_y[1] button1_state = True if button2.value and not button2_state: mqtt_client.publish(light_feed, 2) scene_select.y = scene_y[2] button2_state = True except Exception as error: # pylint: disable=broad-except print(error) mqtt_client.disconnect() time.sleep(5) microcontroller.reset()
Text editor powered by tinymce.