Voice control for a non-smart TV (with Google Home, a Raspberry Pi, LIRC, nginx, Lua and IFTTT)
Despite my privacy concerns, I could not resist the low price of the Google Home Mini. It is really convenient to control the ChromeCast with it, but turning my (non-smart) TV on/off, or switching the input between different devices still required reaching the remote…
…until I hacked a bit!
In a nutshell: IFTTT turns Google Home commands into HTTPS requests towards a Raspberry Pi. There, nginx triggers some Lua code that runs LIRC, which generates IR signals into a transistor that amplifies them to two IR LEDs. Complicated, but works!
Hardware: Raspberry Pi + IR LED (+ a few extras)
TVs with HDMI-CEC can be controlled and input-switched by Google Home via ChromeCast. Mine doesn’t, so I resorted to something that could duplicate the (IR) light signals sent by the remote control.
Sure, I could just add an IR LED to an Arduino or a Raspberry Pi (with a resistor, just like we do with regular LEDs on those “blinking LED” tutorials). But this simple circuit strengthens the signal just by adding a transistor and second resistor. I liked that, and went with it for my initial breadboard experiment:
Once I realized I also wanted to control the sound bar, the amplifier helped: just added a second LED in parallel, and it worked just as well as the single one, no changes needed to the circuit.
The final version was soldered on a tiny piece of protoboard - small enough to fit inside the Raspberry Pi case. The IR LEDs were connected with 22 AWG black wire, which stays in place when twisted, and blends well with the black TV and sound bar.
IR programming: LIRC
One reason I chose a Raspberry Pi over an Arduino was LIRC, an open-source IR remote control software. Took some time to install because most tutorials don’t include the (Raspbian-specific) step of editing boot/config.txt
, in which the following line must be uncommented and point to the pin connected to the 10K resistor:
dtoverlay=lirc-rpi,gpio_out_pin=22
Another hurdle: LIRC’s remotes database did not include either my TV or my sound bar. Had to create configuration files for them.
To do so, I added an infrared receiver (TSOP38238) to another pin (pinout here), adding it as a gpio_in_pin
at all places that I previously added the IR led as gpio_out_pin
.
With this new (and temporary) hardware setup, irrecord
guided me into pressing each button on the remote and generating a config file. The TV remote was straightforward, but the IR bar one required raw mode (-f
), then converting the results to regular codes (-a
).
It was a bit tedious, so I sent the files to the database maintainer for the benefit of future owners of those devices. Until they actually publish it (or in case that they never do), here are my remote code files:
With these in place, I could submit commands such as:
irsend SEND_ONCE Sharp_LCDTV-845-039-40B0 KEY_MENU
and see the menu appearing on the TV, just as if I had pressed the MENU
key.
The source switching was another challenge: my TV requires entering an input menu, navigating with arrows and pressing enter. But I could pack the sequence of irsend
commands (with proper sleep
s to give the slow TV time to react) in a script, like this:
irsend SEND_ONCE Sharp_LCDTV-845-039-40B0 INPUT
sleep 1.5
irsend SEND_ONCE Sharp_LCDTV-845-039-40B0 KEY_UP
sleep 0.5
irsend SEND_ONCE Sharp_LCDTV-845-039-40B0 KEY_UP
sleep 0.5
irsend SEND_ONCE Sharp_LCDTV-845-039-40B0 KEY_ENTER
It wasn’t over yet: the number of KEY_UP
s for a given input depends on which one is already selected. The final solution involved using the KEY_PC
button (which switches the VGA input) and using that as a starting point. Not super fast, but works.
Opening (safely) to the outside world: nginx
The other reason I chose a Raspberry Pi for the project was that the commands from IFTTT would come as web requests.
Security is always a concern with outside requests, so trusty nginx was the tool for the job. Once you properly secure your Raspberry Pi, it can be installed with:
sudo apt-get install nginx
I had to forward ports 80 and 443 from my router to the Pi (also giving it a permanent IP lease), then opening the same ports on ufw
(you did enable the Linux firewall when you secured it, right?), allowing requests to my current IP to reach nginx.
Adding a dynamic DNS service (such as no-ip) and an ssl certificate (either a self-signed one or - my favorite option - a fully trusted one with Let’s Encrypt) allowed the the Pi to respond at a fixed URL with full TLS (“https”) encryption.
Hardening the configuration with something like this (in /etc/nginx/sites-enabled/my.domain
, where my.domain
is the domain from the dynamic DNS provider) makes both myself and security checkers happy:
server {
listen 80;
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/my.domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/my.domain/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers on;
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security max-age=15768000;
# OCSP Stapling ---
ssl_stapling on;
ssl_stapling_verify on;
error_page 500 502 503 504 /500.html;
client_max_body_size 4G;
keepalive_timeout 10;
if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){
return 405;
}
...
Triggering the IR from the web: Lua
In the Apache days, I’d set it up CGI scripts and be done with it, but nginx doesn’t support that. To my surprise, Raspbian Stretch Lite came with Lua - a nifty scripting language, perfect for the job at hand! All I had to do was to add this module:
sudo apt-get install libnginx-mod-http-lua
and I could add some scripting in the same config file where I set up the server:
location /remote {
lua_need_request_body on;
content_by_lua_block {
local args, err = ngx.req.get_post_args()
if not args then
return
end
if args["secret-key"] ~= "some_generated_secret" then
return
end
if args["action"] == "tv_power" then
os.execute("irsend SEND_ONCE Sharp_LCDTV-845-039-40B0 KEY_POWER");
...
elseif args["action"] == "soundbar_volume_up" then
os.execute("irsend SEND_ONCE Insignia_RMC-SB314 KEY_VOLUMEUP");
...
end
}
}
With this, an HTTPS POST message containing the secret-key
and the action
triggers the remote. The secret key will be fully encrypted within the message payload, keeping it safe.
It can be tested with curl
:
curl -d "secret-key=some_generated_secret&action=tv_power" https://my.domain/remote
Putting it all together: IFTTT
IFTTT (“IF This, Then That”) is a neat website that executes actions (“that”) in response to triggers (“this”).
You can tell it to do things like “if I receive an email from this address, then post the contents on Facebook”, or “if the stocks for company XYZ change, add the value to a Google Spreadsheet”. It is only limited by the available services (but there are a lot of them).
By using Google Assistant as a trigger and Maker Webhooks as an action, it was super easy to trigger the HTTPS endpoint defined above by a voice command.
I just created a new applet (“applet” is how IFTTT calls the “if this then that” statements), picking Google Assistant as “this”. After a quick, first-time-only setup it allowed me to tell what phrase(s) will trigger the command and what Google Home should say.
For “that”, I chose Webhooks, using application/x-www-form-urlencoded
for Content Type, POST
for method and the arguments of the curl -d
command above (minus quotes) as body and URL, respectively.
Created an applet for each desired command, and that was it, done. Look ma, no hands!