jameslittle.me

Festival of (API-controlled) Lights

December 15, 2023

If you run the following command in your terminal, the light in my office will turn blue:

1curl -X POST https://api.jameslittle.me/home/light -d "color=blue"

A photo of a blue smart lamp

A photo of the light in my office, being blue

I have a smart lightbulb in my office that I'll use as a backlight to make myself pop on video calls. For a while, I've had some iOS shortcuts that I run from my phone (or my computer) that I would run when I was starting or stopping a video call, to turn on and off that light, respectively.

I started using an app called Pushcut recently. It does a lot of different things, but the one feature I use is the Automation Server: in short, if you have a spare iOS device and you're willing to run the Pushcut app in the foreground constantly, Pushcut will start an HTTP server on your device and expose your shortcuts via a web API.

1https://api.pushcut.io/[secret]/execute?shortcut=[shortcut-name]

[secret] is a generated authentication key that routes the API request to my iCloud account, and [shortcut-name] is the name of a Shortcut I've put together in the app.

You can see where this is going.

Exposing a Public API#

I've had that light for a few years now, and despite being capable of representing a full color spectrum, I only ever set it to "off" or "white". That seemed like a sad existence for the light, so I wanted to make it colorful sometimes.

Instead of deciding when it should be different colors myself, I decided to use Pushcut to set up a system where the internet could decide which color the light should be.

The naive solution is to publish the Pushcut URL for that shortcut somewhere, but I don't want my Pushcut secret to be publicly available -- it makes the API less pretty, and my Pushcut account can only accept so many requests per day. Instead, I decided to write a little proxy API.

I run a web service exposed at api.jameslittle.me. Nothing terribly critical happens on that service, it's mostly just fun to write and run something. I added a new endpoint at /home/light that proxies requests to my Pushcut API endpoint, with a few caviats to keep my Pushcut account from being slammed:

  • There's a global rate limit so my office doesn't turn into a rave
  • It validates the color you pass in (and requires that you pass in a color), rejecting requests that don't specify a valid color

From there, it's a matter of decoding the incoming form-encoded data and crafting the request to the Pushcut servers:

1let response = reqwest::Client::new()
2 .post(format!(
3 "https://api.pushcut.io/{}/execute",
4 std::env::var("PUSHCUT_KEY").unwrap()
5 ))
6 .query(&[
7 ("shortcut", "Set Light Color"),
8 ("input", &shortcut_input.to_string()),
9 ])
10 .send()
11 .await
12 .context("Failed to send request to Pushcut")?;

I might not be in the room when you send your API request! No fear, I also set up the API endpoint to send me a notification on my phone (via a private Slack organization) so that even if I'm not there to see it, I can appreciate the intention while I'm out and about.

Error Handling#

There are lots of things that could fail in this system. Pushcut could reject the request if that service is down or if I've hit my request limit for the day. My spare iPhone-turned-web-server might be stuck in some weird state (which unfortunately seems to happen a few times a week). If Pushcut can't run the shortcut on my device, it will respond with a non-200 HTTP code.

I also have a kill switch in case you internet weirdos get too adventurous, and if the kill switch is activated, the shortcut will respond with a Dictionary with an error message:

A screenshot of the Shortcuts app on my iPhone

Early-returning a dictionary from a shortcut

If a shortcut responds with a dictionary, Pushcut will turn it into a JSON object and return it in the body of its HTTP request.

My API endpoint has logic to find errors from all those situations and return them in its own response. For an API that's as tempermental as this one (just look at how many things can go wrong!), the experience will only be delightful if folks can trust that when they see a successful response the light in my room has actually changed color, and when the response has an error it explains what went wrong.

That API endpoint, once again#

1curl -X POST https://api.jameslittle.me/home/light -d "color=blue"

Blue isn't the only color that the endpoint accepts. There are six valid colors as I write this - can you find them all?

N.B.: The title is a Hanukkah thing.