For Christmas, I received a Google Home and some Philips Hue lightbulbs. Within 15 minutes, I had every light in my apartment responding to voice controls! Naturally rather than accepting that life is awesome and that I live in the future, I was immediately annoyed that I still had to use a remote to change inputs on my TV. Here’s how I went about fixing that.

Google Home

The Google Home is an interface to the Google Assistant, which currently only supports a integration through Actions on Google. Currently the only way to integrate as a random developer is to implement a Conversation Action, which means controlling my TV would sound a little bit like this:

“Ok Google, I’d like to talk to my TV” - Me

Home - “Here’s your TV!”

“Let’s play Wii” - Me

Home - “Setting your TV to Wii”

“Exit” - Me

Yikes, that’s not going to fly. Fortunately, Google has allowed IFTTT to implement Direct Actions, which let a developer specify special phrases to trigger an IFTTT recipe. Even more fortunately, IFTTT finally implemented a Maker channel, which allows arbitrary web requests. With that part sorted, let’s look at how we can control the TV.

I ordered a USB IR blaster from IguanaWorks, and jumped at the chance to use one of my fancy new Raspberry Pi 3s. I loaded up a fresh copy of Raspbian, installed LIRC from the repository, copied in an LG remote profile from this list, and tried my first command:

$ irsend send_once lg KEY_POWER

irsend: hardware does not support sending

Hmm. It turns out there’s quite a process left here, as detailed in the IguanaWorks Getting Started page. I had to build LIRC from source including the iguanaIR driver, and configure my hardware.conf. However with that finished, and the end of my IR blaster artfully taped over my TV’s IR sensor, I could toggle the TV power!

So let’s take stock here: We have a raspberry pi that can trigger IR commands on my TV, and the ability to call a website with a voice command. Clearly we’re missing some glue here.At this point there are many ways to solve the problem, the easiest of which is to open a hole in my router, and let IFTTT call my raspberry pi directly. Since I’m a glutton for punishment though, and have been interested in AWS for a while, lets try to solve this problem with AWS IoT.

The plan is simple. I’ll set up an AWS IoT device to represent my TV, and have my raspberry pi subscribe to the device state, and update the actual TV whenever the state changes on AWS. Then I can have Amazon API Gateway trigger a lambda function to trigger updates to the AWS IoT device.

AWS IoT

Configuring devices in AWS IoT is actually deceptively simple for home use. Amazon pre-defines tons of types with pre-defined attributes, but for my purposes, I really just want to track the current input and the current power state, so I created my own Thing without a type. I created two attributes for input and power.

IoT configuration

Once the Thing (I called mine TV) is created, it will be visible on the AWS IoT dashboard.

IoT dashboard

AWS IoT manages the online state of objects through what it calls Shadows. The shadow represents the most recent online state, which the device will update itself to reflect at the next online check-in. In order to update an IoT shadow from a lambda function, you must grant the lambda function a special IAM permission. I used the following inline IAM code to allow my lambda function to publish to my Amazon IoT devices:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "iot:Publish" ], "Resource": [ "*" ] } ] }

And the following python code is my lambda function itself. It contains a mapping from devices to inputs, and updates the input and power state accordingly. In addition, this lambda function takes a password key, that it requires before updating any IoT state. This lambda function is going to be publicly accessible, and I don’t really want strangers fiddling with my TV!

from __future__ import print_function import boto3 import json print('Loading function') def respond(err, res=None): return { 'statusCode': '400' if err else '200', 'body': err.message if err else json.dumps(res), 'headers': { 'Content-Type': 'application/json', }, } def inputMap(name): m = { "xbox": "HDMI1", "chromecast": "HDMI2", "netflix": "HDMI2", "youtube": "HDMI2", "wii": "HDMI3", "nintendo": "AV1", "n64": "AV1" } if name.lower() not in m: return "HDMI2" return m[name.lower()] def lambda_handler(event, context): print("Received event: " + json.dumps(event, indent=2)) payload = json.loads(event['body']) if "password" not in payload or payload["password"] != "SECRET_CODE": return respond(True) client = boto3.client('iot-data', region_name='us-east-1') update = { "state": { "desired": {} } } if "input" in payload: print("Received input selection " + payload["input"] + " which maps to " + inputMap(payload["input"])) update["state"]["desired"]["input"] = inputMap(payload["input"]) if "power" in payload: print("Received power selection " + str(payload["power"]) ) update["state"]["desired"]["power"] = payload["power"] # Change topic, qos and payload response = client.publish( topic='$aws/things/TV/shadow/update', qos=1, payload=json.dumps(update) ) return respond(False, "")

With that set up in AWS Lambda, it’s fairly trivial to set up an API gateway proxy. Just ensure to deploy your API to make sure that your changes are publicly visible!

IFTTT

IFTT

From there, I set up an IFTTT applet that listens for the magic words “let’s watch $”, “let’s play $”, or “turn the TV to $”. It fires a request at my API, which updates my IoT shadow.

Setting up the Pi

So, we could carefully try to track the current state, and send the right number of power toggles and input selects, but that sounds like a recipe for disaster. Although they aren’t on your remote control, most TVs support idempotent IR commands like POWER_ON, POWER_OFF, and INPUT_HDMI1. The trick is simply finding them! I downloaded the manual for my tv from LG.com, and read the section on IR codes for my TV. I learned 2 things:

  1. My TV definitely supports idempotent IR commands.
  2. There is no way I’ll be able to interpret these codes.

Enter the internet. A post on RemoteCentral.com contains raw IR codes for all of the commands I need, and IRScrutinizer can read these codes and output them for LIRC. If you, like me, have an LG TV from 2000-2013, these codes should work for you.

begin remote name lg bits 32 flags SPACE_ENC eps 30 aeps 100 zero 573 573 one 573 1694 header 9041 4507 ptrail 573 repeat 9041 2267 gap 36000 repeat_bit 0 frequency 38400 begin codes POWER_ON 0x20DF23DC POWER_OFF 0x20DFA35C AV1 0x20DF5AA5 AV2 0x20DF0BF4 COMP1 0x20DFFD02 COMP2 0x20DF2BD4 HDMI1 0x20DF738C HDMI2 0x20DF33CC HDMI3 0x20DF9768 end codes end remote

With this setup, I can now update my TV to a known state, even if I don’t know the current state. Win. While I was testing though, I noticed that my TV will only respond to POWER requests while in standby. If I want to change the input, I have to wait until the TV is on. However, if the TV is already on, I’d like to change inputs immediately. So, when selecting for example, HDMI1, I send the following command:

$ irsend send_once lg HDMI1; $ irsend send_once lg POWER_ON; $ sleep 10; $ irsend send_once lg HDMI1;

Powering off is easier, I just send $ irsend send_once lg POWER_OFF

Finally, the Pi needs to be connected to AWS IoT. The AWS IoT system will build you a custom python SDK for each device, complete with access keys. I downloaded that, and then wrote the following small script to actually send the IR codes.

def turnOff(): return_code = subprocess.call("irsend send_once lg POWER_OFF", shell=True) if return_code != 0: print("Error turning off TV: "+ str(return_code)) return return_code def setInput(input): return_code = subprocess.call("irsend send_once lg " + input + "; irsend send_once lg POWER_ON; sleep 11; irsend send_once lg " + input, shell=True) if return_code != 0: print("Error setting TV to "+input+": "+ str(return_code)) return return_code # Custom Shadow callback def customShadowCallback_Delta(payload, responseStatus, token): # payload is a JSON string ready to be parsed using json.loads(...) # in both Py2.x and Py3.x print(responseStatus) payloadDict = json.loads(payload) print("++++++++DELTA++++++++++") if "power" in payloadDict["state"]: print("power: " + str(payloadDict["state"]["power"])) if "input" in payloadDict["state"]: print("input: " + str(payloadDict["state"]["input"])) print("version: " + str(payloadDict["version"])) print("+++++++++++++++++++++++\n\n") if not payloadDict["state"]["power"]: turnOff() else: setInput(payloadDict["state"]["input"])

The full file is available for download: tvListener.py

And there we have it! An end-to-end system for controlling my dumb LG TV with a Google Home. I’ve been using this for a little over a week, and it’s working great, though there is occasionally a multi-second delay before the TV responds. If you have any recommendations for how I should change things, let me know in the comments below, or contact me.