Automate Your Home Theater Lights From The Cloud

Written by markbrouch | Published 2017/05/01
Tech Story Tags: plex | philips-hue | aws-lambda | side-project | javascript

TLDRvia the TL;DR App

Connecting Plex Media Server to Hue Lighting System using AWS API Gateway and Lambda

This weekend I set out to do something cool with my Phillips Hue Lights that I had installed in my basement’s home theater. The goal: Automatically dim the lights to ‘theater mode’ when I play a movie.

The Setup

I live in a split-level apartment in Chicago with a finished basement area that I use as a home theater. Last October I installed 6 Color Hue Can Lights, a Hue Bloom, and a Hue LED Strip in the basement which I can control with my iPhone or Amazon Echo Dot. I set up several Hue lighting “scenes” so that I can easily change the brightness and color of the lights in the basement, including a “Theater Mode” which turns off all of the lights except the LED strip as a backlight behind the TV stand.

For movie streaming, I have a computer in my office running Plex Media Server which I use to stream to a variety of devices on or off my local network. I also have an Apple TV hooked up to my TV in the basement with the Plex app installed.

This setup has worked pretty well for awhile now, and I especially love that I can tell Alexa to turn the lights on/off without even getting off the couch, but I felt we could go even lazier.

“If necessity is the mother of invention, then laziness is the father.”

The Plan

I wanted a way for the lights to automatically go into Theater Mode (lights off) when I play a movie, and then come back to Dimmed Mode (lights on) when the movie ends or when I pause to use the bathroom or make some more popcorn. (In the future, popcorn will be delivered automatically from the cloud.)

Plex recently added webhooks for several events, including play/pause. Hue lights have always been sold with hobbyist programmers in mind and include a robust API (although it could be much better — more on that later). With these two endpoints in place, all I had to do was get them to talk to each other.

Given that I already had a server on my network to host Plex, it would make sense to quickly spin up an Express server on Node.js to forward webhook events to my Hue lighting hub, but baremetal servers are sooo 2015. No, this project sounded like a good opportunity to go serverless in the AWS cloud!

Data flow diagram: Plex Webhook → AWS API Gateway → AWS Lambda → Hue Lights

Disclaimer: I’m an AWS novice. Send me a message if any of my configuration could be improved!

Setting up the Lambda function

In order to get the Plex server talking to the Hue lights, we’ll need to implement a small amount of webhook parsing logic — a perfect job for AWS Lambda. For the uninitiated — a Lambda function is basically a function in the cloud. They’re great for building out microservices, or in this case, connecting APIs.

We’re essentially going to build an “If This Then That”-style function with the following logic:

  • 📺 Check Plex webhook payload if the action was triggered by my AppleTV.
  • 📽 Check Plex webhook payload if the triggered action came from a movie.
  • 🌑 Set Hue lights to Theater Mode if Plex event is Play or Resume.
  • 🌕 Set Hue lights to Dimmed Mode if Plex event is Pause or Stop.

To get started, we’ll need to create a new Lambda. We can do this from the AWS dashboard, but I prefer to use the NPM package node-lambda for configuration, deployment, and testing. You can see the full source for my Lambda function on GitHub.

I won’t go into the details of setting up a new Lambda function, other than that we’ll need to provide the lambda-access role so that we can connect to our API Gateway later on. Lambda also (finally) supports Node v6.10, and we’ll be using that as our execution environment. Once our Lambda is configured, we’ll be able to setup API Gateway.

Configuring API Gateway

AWS API Gateway is the interface we’ll use to connect our Plex webhook to our Lambda function. API Gateway is capable of a highly sophisticated API architecture, but for our purposes we’ll need only a single POST endpoint that will forward on to the Lambda function.

After we create a new API, we’ll create a single POST resource at / that will handle our webhook request. Next, we’ll configure the POST resource’s Integration Request as such:

  • Integration type: Lambda function
  • Lambda Region: The region of our Lambda function
  • Lambda Function: The name of our Lambda function

The Plex webhook doesn’t deliver its payload in the application/json content-type that our Lambda function needs, so we’ll need to massage the request into a Lambda-friendly format. Since there’s no way to parse the multipart form data coming from the Plex webhook in API Gateway, we’re just going to pass it through as Base64 encoded binary instead and then deconstruct it later in our Lambda function.

We’ll need to enable binary support on our API and add the binary media type multipart/form-data so that API Gateway doesn’t try to parse JSON from our webhook. Then, back in our POST resource’s Integration Request tab, we’ll need to add the following Body Mapping Template for multipart/form-data content-type:

{"body": "$input.body","headers": {#foreach($param in $input.params().header.keySet())"$param":"$util.escapeJavaScript($input.params().header.get($param))"#if($foreach.hasNext),#end#end}}

API Gateway uses Apache’s Velocity Templating Language (VTL) for body mapping templates

This will wrap our raw encoded request in body and also forward all headers coming from the Plex webhook in headers. That’s all the configuration needed for API Gateway, we can now click Actions → Deploy API. Create a new Deployment stage and hit Deploy. If successful, you will be taken to the Stage Editor for the stage you just created and deployed to. Copy the Invoke URL to a new Plex webhook under Account Settings.

Unpacking the payload in Lambda

Our Lambda function exposes a handler which our API Gateway will invoke with the forwarded Plex webhook as the event argument. We’ll need to unpack the event body, which will be a multipart form, and then parse it to get the payload JSON. To do this, we’ll use an NPM package called Busboy.

Busboy loves parsing your multipart form data as much as this one loves bussing tables.

Busboy is a low-level writeable stream used by several Express middlewares like Multer for parsing multipart form data. Usage is simple — we’ll just pass the headers to the Busboy initializer and then pipe in the Base64 encoded body. Busboy will then fire events for each form field it encounters. Here’s what our handler code looks like:

const Busboy = require('busboy');

exports.handler = (event, context, callback) => {// Busboy expects headers to be lower-caseconst headers = {};Object.keys(event.headers).forEach(key => (headers[key.toLowerCase()] = event.headers[key]));

const busboy = new Busboy({headers});

// For each field in the requestbusboy.on('field', (fieldname, value) => {// Check for the Plex webhook's payload fieldif (fieldname === 'payload') {const payload = JSON.parse(value);

  // Read the payload to control Hue lights  
  readPayload(payload);  
    
  // Send payload in response for testing & debugging  
  callback(null, {  
    payload  
  });  
}  

});

// Pipe Base64 encoded body from API Gateway to Busboybusboy.write(Buffer.from(event.body, 'base64'));};

The readPayload() function will contain our logic for connecting to Hue, but you could use this handler code as a boilerplate for connecting a Plex webhook to any external service.

We can now test our event handler by creating a mockevent.json if you’re using node-lambda, or you can console.log() to the AWS CloudWatch logs.

Controlling Hue lights with Lambda

With our Plex webhook payload in proper JSON format, we’re ready to talk to our Hue lights, but first we’ll need to prepare our Lambda for the Hue API. We’ll add some environment variables for the various Hue IDs we’ll need, and perform a rather hacky authentication to connect to our Hue Bridge from our Lambda function that exists outside our local network.

Phillips provides a fairly robust RESTful API for controlling most aspects of a Hue lighting system from within the same local network, but does not provide any documentation for controlling the lights from the Internet, though it is possible. Paul Shi wrote an excellent blog post on how to “hack” into a Hue Bridge from outside its local network. Use Paul’s guide to get the BRIDGEID and ACCESSTOKEN.

We’re going to store the BRIDGEID and ACCESSTOKEN as environment variables in our Lambda, along with several other variables:

  • HUE_TOKEN: ACCESSTOKEN
  • HUE_BRIDGE_ID: BRIDGEID
  • HUE_SCENE_THEATER: The scene ID for our Theater scene
  • HUE_SCENE_DIMMED: The scene ID for our Dimmed scene
  • HUE_GROUP_ID: The group ID for the group containing our lights
  • PLAYER_UUID: The UUID of the AppleTV device from the Plex webhook

You can find the scene IDs and group ID by making a GET [https://www.meethue.com/api/getbridge](https://www.meethue.com/api/getbridge) request as explained in Paul’s blog post.

Our readPayload() function will perform the checks on the payload to see what, if any, action should be sent to the Hue API, and will then construct and send the appropriate request:

const https = require('https');

const readPayload = payload => {// Plex webhook event constantsconst PLAY = 'media.play';const PAUSE = 'media.pause';const RESUME = 'media.resume';const STOP = 'media.stop';

const { event, Player, Metadata } = payload;

// https optionsconst options = {hostname: 'www.meethue.com',path: `/api/sendmessage?token=${process.env.HUE_TOKEN}`,method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded'}};

if (process.env.PLAYER_UUID === Player.uuid && // Event came from the correct playerMetadata.type === 'movie' && // Event type is from a movie(event === PLAY || event === STOP || event === PAUSE || event === RESUME) // Event is a valid type) {const scene = event === PLAY || event === RESUME? process.env.HUE_SCENE_THEATER // Turn the lights off because it's playing: process.env.HUE_SCENE_DIMMED; // Turn the lights on because it's not playing

// Construct Hue API body  
_const_ body = \`clipmessage={ bridgeId: "${process.env.HUE\_BRIDGE\_ID}", clipCommand: { url: "/api/0/groups/${process.env.HUE\_GROUP\_ID}/action", method: "PUT", body: { scene: "${scene}" } } }\`;

// Send request to Hue API  
_const_ req = https.request(options);  
req.write(body);

req.end();  

}};

And that’s it! The lights should now go into Theater Mode when a movie starts playing on the AppleTV and will come back on in Dimmed Mode when the movie is paused or stops.


Published by HackerNoon on 2017/05/01