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()
Page last edited January 21, 2025
Text editor powered by tinymce.