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 that I had installed in my basement’s home theater. The goal: Phillips Hue Lights 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 , a , and a 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. 6 Color Hue Can Lights Hue Bloom Hue LED Strip For movie streaming, I have a computer in my office running 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 installed. Plex Media Server Plex app 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 for several events, including play/pause. Hue lights have always been sold with hobbyist programmers in mind and include a (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. added webhooks robust API 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 . 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. AWS Lambda 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 for configuration, deployment, and testing. node-lambda 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 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. lambda-access Configuring 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. AWS API Gateway 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 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. application/json We’ll need to on our API and add the binary media type 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 content-type: enable binary support multipart/form-data multipart/form-data {"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 for body mapping templates Velocity Templating Language (VTL) This will wrap our raw encoded request in and also forward all headers coming from the Plex webhook in . 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 . body headers a new Plex webhook under Account Settings Unpacking the payload in Lambda Our Lambda function exposes a which our API Gateway will invoke with the forwarded Plex webhook as the 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 . handler event 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: Busboy = require('busboy'); const .handler = ( , , ) {// Busboy expects headers to be lower-case headers = {}; .keys(event.headers).forEach( (headers[key.toLowerCase()] = event.headers[key])); exports event context callback => const Object key => busboy = new Busboy({headers}); const // For each field in the requestbusboy.on('field', ( , ) {// Check for the Plex webhook's payload fieldif (fieldname === 'payload') { payload = JSON.parse(value); fieldname value => const // 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( .from(event.body, 'base64'));}; Buffer The 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. readPayload() We can now test our event handler by creating a mock if you’re using , or you can to the AWS CloudWatch logs. event.json node-lambda console.log() 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. on how to “hack” into a Hue Bridge from outside its local network. Use Paul’s guide to get the and . Paul Shi wrote an excellent blog post BRIDGEID ACCESSTOKEN We’re going to store the and as environment variables in our Lambda, along with several other variables: BRIDGEID ACCESSTOKEN : HUE_TOKEN ACCESSTOKEN : HUE_BRIDGE_ID BRIDGEID : The scene ID for our Theater scene HUE_SCENE_THEATER : The scene ID for our Dimmed scene HUE_SCENE_DIMMED : The group ID for the group containing our lights HUE_GROUP_ID : The UUID of the AppleTV device from the Plex webhook PLAYER_UUID You can find the scene IDs and group ID by making a request as explained in Paul’s blog post. GET [https://www.meethue.com/api/getbridge](https://www.meethue.com/api/getbridge) Our 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: readPayload() https = require('https'); const readPayload = {// Plex webhook event constants PLAY = 'media.play'; PAUSE = 'media.pause'; RESUME = 'media.resume'; STOP = 'media.stop'; const payload => const const const const { event, Player, Metadata } = payload; const // https options options = {hostname: 'www.meethue.com',path: `/api/sendmessage?token=${process.env.HUE_TOKEN}`,method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded'}}; const 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) { 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 const // 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.