Making a dumb smart light switch with a Raspberry Pi Pico

Python Microcontrollers Electrical engineering

19 November, 2024



I have a Philips Hue smart light in my room, which is great because I love being able to modify the brightness and colour temperature throughout the day and evening, but it's also terrible because the Hue app takes about five seconds to load on my phone. This post describes how I made a "dumb" smart switch using a Raspberry Pi Pico, and integrated it with my Home Assistant deployment. The result is a simple switch with two buttons: one to turn the light off and on, and one to change the brightness and colour temperature setting.

The MicroPython code used in this project is available here.

Equipment

Approach

There are several ways you could feasibly control a smart light programatically. I first looked at the MQTT integration, as it's a pretty standard way of communicating with smart devices, but ultimately decided against it because it just isn't that simple. All I want to do is send a signal to Home Assistant to change the state of the light bulb, and MQTT seemed overkill for that.

I'm also much more comfortable with generic HTTP APIs than with specific protocols like MQTT, so I ended up going with what I believe to be the simplest approach for this sort of thing: the Home Assistant REST API.

Home Assistant configuration

The first step is to add the api integration into the Home Assistant deployment. In configuration.yaml, add the following if it's not already there:

api:

I use the File Editor add-on to edit configuration.yaml, but if you've got terminal access you can do it that way too.

Next, we need to get a long-lived access token from Home Assistant. You can create one here (assuming you're using the default host/port combination): http://homeassistant.local:8123/profile/security. Save the token.

You'll also need the official Philips Hue integration. I'm assuming that you've got that set up already, including configuration for an existing light. We'll be taking advantage of the standard light and hue API services to toggle our light on/off and change the brightness/scene, respectively.

Raspberry Pi Pico

The Raspberry Pi Pico microcontroller is the basis of the switch that we're making. It has an existing button (the bootsel button) that we can hijack for our own use, and we'll add another button hooked up to one of the GPIO pins.

We'll write a single MicroPython script that controls everything. At a high level, the script comprises the following components:

MicroPython

To start off, create a secrets.py file with the following variables assigned as strings:

In the main.py script, we start off with a few required imports, and define the WiFi object to manage connections:

from machine import Pin
from secrets import SSID, PASSWORD, TOKEN
import gc
import network
import requests
import rp2
import time

button = Pin(15, Pin.IN, Pin.PULL_UP)

class WiFi:
    """Just a small wrapper around network.WLAN"""

    def __init__(self, ssid, password):
        """Connect to the WiFi"""
        wlan = network.WLAN(network.STA_IF)
        wlan.active(True)
        wlan.connect(ssid, password)
        self.wlan = wlan

    def is_connected(self):
        """Basic back-off connection check"""
        retry_seconds = 0.5
        while not self.wlan.isconnected():
            if retry_seconds > 16:
                print("Failed to connect to WiFi.")
                return False
            print(f"Failed to connect, trying again in {retry_seconds} s...")
            time.sleep(retry_seconds)
            retry_seconds = retry_seconds * 2
        print("Connected to WiFi.")
        return True

wifi = WiFi(SSID, PASSWORD)

Hopefully the WiFi code is pretty self-explanatory. The button assignment refers to the button that we'll be hooking up; it'll be connected to GPIO pin 15.

The API calls that we make require standard HTTP headers Authorization and Content-Type; I've got a simple function to abstract that away so we aren't repeating ourselves in the different API calls. Note that we pass in the long-lived access token to the Authorization header:

def get_api_data(token=TOKEN):
    """
    Get HTTP headers and base URL for the Home Assistant API call
    :param token: string of Home Assistant long-lived access token
    :return: dict with "headers" and "url" keys
    """
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }
    url = "http://homeassistant.local:8123"
    return({
        "headers": headers,
        "url": url,
    })

And now we define the functions that will make the API calls:

def toggle_light():
    """
    Toggle the light on or off.
    :return: return value of requests.post
    """
    endpoint = "/api/services/light/toggle"
    api_data = get_api_data()
    response = requests.post(
        url=api_data["url"] + endpoint,
        headers=api_data["headers"],
        data='{"entity_id": "light.bedroom"}'
    )
    return response


def toggle_scene():
    """
    Toggle the "scene", i.e., the light setting.
    There's no out-of-the-box way to get the current scene, so instead we get
    the state of the bedroom light, and use the 'brightness' attribute to
    infer the scene.
    :return: return value of requests.post
    """
    api_data = get_api_data()
    endpoint = "/api/services/hue/activate_scene"
    current_state = requests.get(
        url=api_data["url"] + "/api/states/light.bedroom",
        headers=api_data["headers"],
    )
    if current_state.json()["attributes"]["brightness"] > 240:
        scene = "scene.bedroom_relax"
    else:
        scene = "scene.bedroom_concentrate"
    response = requests.post(
        url=api_data["url"] + endpoint,
        headers=api_data["headers"],
        data=f'{{"entity_id": "{scene}"}}'
    )
    return response

The toggle_light function is pretty simple; we take advantage of the standard Home Assistant API endpoint /api/services/light/toggle. This endpoint expects a POST request with the body including a value for entity_id; this is the Home Assistant-specific ID of the light that you want to toggle. In my case, it's light.bedroom.

The toggle_scene function is a bit more complicated, since (as of writing) there's no out-of-the-box way to obtain the currently set Hue scene of the light in question. I only want to toggle between two scenes—one bright scene for daytime, and one dimmer scene for nighttime, both of which I'd already created in the Hue app—and the simplest way to tell the difference between these scenes is their brightness setting.

So, the first thing we do is to make a request to /api/states/light.bedroom, to get the current state of the light. We then check the brightness attribute of the light, and use that to determine the currently set scene. For me, this is an arbitrary value of 240, but you could use any appropriate attribute and/or value. We then define the new scene; if we're currently on the daytime scene, then we want to switch to the nighttime scene, and vice versa. My scenes are named light.bedroom_concentrate and light.bedroom_relax, but that'll depend on whatever you've named yours.

Finally, we make a POST request to the /api/services/hue/activate_scene endpoint to change the scene.

And that's pretty much it. The logic of the main process is pretty straightforward; every 0.1 seconds, we check whether either of the buttons have been pressed, and if they have been, then send off the corresponding API request:

if wifi.is_connected():
    while True:
        # Turn light on or off on main button press
        if button.value() == 0:
            res = toggle_light()
            if not res.status_code == 200:
                print(res.text)
        # Change light brightness on bootsel button press
        elif rp2.bootsel_button() == 1:
            res = toggle_scene()
            if not res.status_code == 200:
                print(res.text)
        time.sleep(0.1)
        gc.collect()

There's some debugging and garbage collection in there (which you'll definitely need to stop the Pico from running out of memory), but nothing major. Transfer secrets.py and main.py over to the Pico, plug it in, and you're good to go.

Buttons

The Pico has one built-in button, the bootsel. We access its value with rp2.bootsel_button; that returns a value of 1 when it's pressed in, and we toggle the scene in that case. I wouldn't recommend using this button in any important or critical applications, since it's actually used to reset the Pico; but for this simple use case as a prototype, it actually works really well.

The second button is a two-terminal button switch, about as simple as they come. I've soldered one end to GPIO pin 15, and the other end to ground. Any GPIO and ground pins should work fine, you'll just need to make sure to reference the correct pin in the script.

The actual thing

So here's what it actually looks like:

dumb-smart-switch

I hot-glued it to a popsicle stick, and glued that to something that I can reach when I'm in bed. Now, instead of unlocking my phone, waiting forever for the Hue app to initialise, and toggling the specific light, I can turn it on/off or change the scene with the press of a button.

The irony of a (pretty) dumb switch for a (supposedly) smart light bulb does not escape me, but I'm pretty happy with the result nonetheless!



0 comments

Leave a comment