Controlling AV Equipment with Python and Google Home

Connect your Google Home to your AV system with IFTTT, Adafruit-IO and Python!

Controlling AV Equipment with Python and Google Home

I wanted a simple way to have basic control over my AV system without keeping track of more than one remote. In fact, with this solution I don't need a remote at all!

Although, I probably shouldn't loose them lest I need to change their settings at some point.

This mini project started when I realized that both of my peices of equipment (my panasonic projector & denon AVR) had network ports which allowed them to be controlled remotely. The projector even had some sort of flash-based crestron UI, which was pretty neat but unnecessary. I thought that I could control these units with a simple python script, and at worst send raw packets to a control port.

After poking and prodding for about 15 minutes, trying to figure out what kind of POST request needs to be made just to control the power of the device, I realized I should look up any existing python libraries designed to leverage some of the many available control protocols these devices support.

Here are the two that I found:

https://github.com/scarface-4711/denonavr
https://github.com/gaetano-guerriero/pypjlink

Both of these libraries can be installed through pip directly from PyPi, and that made this project a whole lot easier.

IF you have different hardware, you will of course need to find alternate libraries and write your code differently. I don't expect many people to have the exact same setup as me

Connection to the devices in these libraries is simple; instantiate a class by feeding it the IP address of your device and possibly a password or so.

from pypjlink import Projector
from denonavr import DenonAVR

projector = Projector.from_address("<projector IP address>")
projector.authenticate("panasonic") 
# My projector was a panasonic, and the default password for panasonic projectors is just 'panasonic'

avr = DenonAVR("<av reciever IP address>")

At that point, you can use built-in methods to perform actions against your device(s). I wrote a simple wrapper class for myself that summarized each device's power on/off functions into a single method I can call from another location in my code. (I also put the connection code in the constructor, and handle configuration through a settings class)

class SystemController:
    def __init__(self, settings: SystemSettings):
        self.settings = settings
        self.projector = Projector.from_address(self.settings.proj_hostname)
        self.projector.authenticate(self.settings.proj_password)
        self.avr = DenonAVR(self.settings.avr_hostname)

    def power_on(self):
        self.projector.set_power('on')
        self.avr.power_on()
        return self.power_status()

    def power_off(self):
        self.projector.set_power('off')
        self.avr.power_off()
        return self.power_status()

    def power_status(self):
        proj_status = self.projector.get_power()
        avr_status = self.avr.state
        return f"Projector power state: {proj_status}\nAVR power state: {avr_status}"

After a little bit of testing, I had success! The python script was able to power on/off both my devices and I spent the rest of my time trying to figure out how I could connect this to my Google Home.

Google Home Integration

I ended up using IFTTT and Adafruit-IO to connect the Google Assistant to my python program without forwarding any ports. The alternative solution was to use webhooks from IFTTT to a webserver running on my home network in order to send commands to my controller program. This is less than ideal, as it requires having a static IP (or dynamic DNS service) to ensure that IFTT could always reach my network.

Instead, I can use an MQTT service to capture and send messages in a publisher/subscriber fashion. IFTTT is my publisher, and my python program is my subscriber. My program simply initiates a connection to Adafruit-IO's MQTT service and just sits there and listens. No port forwarding, no dynamic-dns and most of all I don't have to worry about someone discovering an open API on my public IP and playing with my AV system like a fascinated toddler.

Setting up the IFTTT recipes is simple. First, go on over to https://io.adafruit.com/ and sign up for an account. Their MQTT service is free, and they will not ask you to fill in payment information upon sign-up. Next, login to IFTTT, head over to https://ifttt.com/adafruit and connect your adafruit-io account. (if you haven't already, connect your alexa or google assistant account too)
Configure a feed in Adafruit-IO, and give it a descriptive name like "hometheatercontroller".

Screen-Shot-2018-08-30-at-8.36.07-PM

After that, you're ready to define some applets in IFTTT.

Now, the Google Assistant service in IFTTT is a bit better featured than Alexa. Alexa only allows you to configure trigger phrases, and doesn't allow you to specify text or number ingredients in the middle of your phrase. Plus, in order to trigger your action from Alexa, you have to say "Alexa, trigger <my fancy phrase>".
Right off the bat, that's a bit less natrual than saying "Hey Google, turn on my AV system" or "Hey Google, change volume to 50". But, I'm biased. You could just go make your own Alexa Skill and achieve the same results. But, thanks to IFTTT, I don't have to write any more code than I already have to.

Screen-Shot-2018-08-30-at-8.37.13-PM

As you see above, you can define multiple variations of your trigger phrase, and even include a text ingredient. For my power on/off example, your text ingredient is that on/off word. We'll send this word to the adafruit feed so that our program knows whether you want to turn things on or off.

Screen-Shot-2018-08-30-at-8.37.58-PM

I also have other applets to change inputs, and set the volume on the AV receiver, and it's as simple as making a another trigger phrase with number/text ingredient, like "set volume to #" or "change input to $".

Now, we'll setup our python code to receive these messages from the MQTT feed and figure out what you said.

import Adafruit_IO

AIO_USERNAME = "my adafruit username"
AIO_KEY = "my adafruit AIO key"
AIO_FEED_NAME = "my mqtt feed"

mqtt = Adafruit_IO.MQTTClient(AIO_USERNAME, AIO_KEY)

# Define callback functions which will be called when certain events happen.
def connected(client):
    """Connected function will be called when the client is connected to
    Adafruit IO.This is a good place to subscribe to feed changes.  The client
    parameter passed to this function is the Adafruit IO MQTT client so you
    can make calls against it easily.
    """
    # Subscribe to changes on a feed named Counter.
    print('Subscribing to Feed {0}'.format(AIO_FEED_NAME))
    client.subscribe(AIO_FEED_NAME)
    print('Waiting for feed data...')

def disconnected(client):
    """Disconnected function will be called when the client disconnects."""
    sys.exit(1)


def message(client, feed_id, payload):
    """Message function will be called when a subscribed feed has a new value.
    The feed_id parameter identifies the feed, and the payload parameter has
    the new value.
    """
    print('Feed {0} received new value: {1}'.format(feed_id, payload))
    sc = SystemController(settings)

    def set_power(mode):
        if mode == 'on':
            sc.power_on()
        elif mode == 'off':
            sc.power_off()

    payload = payload.split()
    action = payload[0].lower()
    # the first word of the "Data to save" field in the IFTTT applet is the action.
    # If you define different actions, put a different word at the beginning of "Data to save" and the variable data afterwards.
    
    value = ' '.join(payload[1:]).lower()
    print("action:", action)
    print("value:", value)
    print("running actions...")
    try:
        if action == "power":
            set_power(value)
    except Exception as e:
        # Silently fail in case we say a phrase that crashes our program
        # This way, the program continues to receive commands
        print(e)
        pass

mqtt.on_connect = connected
mqtt.on_disconnect = disconnected
mqtt.on_message = message

mqtt.connect()
mqtt.loop_blocking()

That last part is important, as it registers your callback functions to the events, connects to the adafruit mqtt broker, and loops the program to listen for incoming messages. Anything after mqtt.loop_blocking() will not be ran during the program.

At this point, talk to your Google Home or Google Assistant (This works on your phone too!) and try out your new commands. You should be able to see console output in your python program if everything works accordingly.

Related Article