automate dumb audio system

Automating My Dumb Audio System

This post may contain affiliate links. Please see the disclaimer for more information.

Recently I’ve been on somewhat of a mission to improve the integration between my various media devices and my home automation system. One part which has until now been untouched is the main living room audio system, which is still somewhat dumb. I’m not all the way to automating this yet, but I’m making progress. In this post I’m going to detail my progress so far, the issues I ran into and how I’m planning to improve the integration in future. I’ll also partially review the different components involved.

The Audio System

As detailed previously, our living room audio is provided by a Polk Signa S2 soundbar. I really like this soundbar and it was a massive step up from the TV audio we had previously. The system was very easy to set up, pretty much plug in the HDMI ARC connection, turn it on and go. It came pre-paired with it’s wireless sub, so that just worked. I’ve had a few instances (maybe five or so the last few months) where the main unit was not able to connect to the sub on startup, which it communicates by flashing an LED. Turning the unit off and on again allows it to retry, which in my experience always works.

The audio quality is great to my ear, but I don’t consider myself an expert in audio stuff. It’s enough to fill our living space with sound and shake the walls if you turn the bass up!

automate dumb audio system
The soundbar in all it’s glory

Where this device falls down is the lack of easy integration with other devices. This is of course pretty normal in this space, but it doesn’t mean we have to accept it!

Clever, Dumb Speakers

On the surface the soundbar behaves somewhat intelligently. It will turn on when the TV comes on. Presumably this is done via HDMI CEC because it will also turn on when the TV is turned on via CEC. This means it isn’t just intercepting the IR commands from the TV remote. However, all my attempts to control the unit via CEC from the Raspberry Pi connected to the TV failed. Weirdly, the soundbar also won’t switch off when the TV is turned off via CEC, whereas it does with the remote. The soundbar also integrates the volume between the TV and itself and allows adjustment via either the TV or it’s own remote.

This is all nice for the “normal” case of just using the TV. We can just sit down and watch without having to find the extra remote and everything just works. Where it falls down is anything outside of this basic use case. The soundbar has basic preset modes for different applications, “movie”, “music” and “night mode”. It also has 3 levels of what it refers to as voice adjust, which amplifies the frequencies contained in human speech to make dialogue clearer (it actually works pretty well). None of these settings are available unless using the device’s own remote.

automate dumb audio system
The soundbar remote, most of the functions are only available via this remote

IR Control

As I couldn’t control the device via CEC, I was pretty much resigned to having to build an IR remote control device. The soundbar also has bluetooth, but it’s only for audio streaming. There doesn’t appear to be any control capability and it’s turned off when the device is in standby. I’ll come back to the bluetooth later, as I do have some ideas around it for future work.

After failing to find the time to build an IR blaster (hardware is hard), I decided to buy a Broadlink device. Specifically the Broadlink RM Mini 3, since they were fairly cheap and supported in Home Assistant.

automate dumb audio system
The Broadlink RM Mini 3, sitting happily in it’s place on the bookcase

When the RM Mini arrived I was pleased with how it looked and easily found a place for it on a bookshelf where it could see the TV and soundbar. The line of sight reaches across the whole of our living/dining area, so I was hoping the range would be sufficient to reach. As it would turn out I was right, there have been no problems so far.

OMG, That App

Setting up the Broadlink was an exercise in frustration, mainly because the IHC app is truly awful. No… that doesn’t cover it: the app is a train wreck on board a sinking ship that’s just been hit by a meteor. For example, on the sign up page, it gives you 60 seconds to both enter the validation code it sent you by email and type your new (and hopefully secure) password. Unfortunately, I don’t have a screenshot of this, since I don’t want to reinstall that natural disaster of an app to get one!

Who really thinks that one minute is enough to sign into their email (or even just switch to your mail app), grab and enter the code and generate and enter a reasonable password?! That’s even assuming the email comes through in that time. Repeat after me: Email is not a synchronous communication medium! It was not designed for real time communication, messages can be delayed for any reason. I use grey-listing on my server, which would have made it impossible for me to sign up had I not been able to temporarily disable it.

Always Remember: The Cloud is Just Someone Else’s Computer

The app will also fail to find the device if you are on separate networks and gives you no way just to enter its IP address. This meant I had to put my phone onto my IoT network, on which most outgoing traffic is blocked. I then had to punch holes in the firewall for the device and my phone, because the setup process requires Internet access. The one for the device may not be required, but it definitely didn’t work if my phone couldn’t get out.

A device like this really has no business needing Internet access in the first place. It could also be configured purely over the local network and even without an app if they just put an AP+captive portal configuration page on it. Sigh.

Anyway, I got it onto the network. Eventually. However, things didn’t get better from there.

Issues in Home Assistant

I set up the device in HASS thinking that the hard part was over and that it would be plain sailing from now on. That proved to be misguided. Upon running the broadlink.learn service via the dev-tools, nothing happened. When I checked the log via the info tab I got the message Failed to connect to device from the broadlink component.

Upon examining the code, I could see that this happens if the device fails to authenticate properly. The auth call comes from the underlying library, python-broadlink. I checked the latest version of this out (0.11.1) and tried the CLI tool with the latest version, which also didn’t work. The tool would just time out without getting any data back. Also, the little white LED on the device didn’t light up.

Fixing it… but not really

I checked out and installed the previous version (0.10) and tried the same thing and it worked! I was able to learn IR codes and send them back to control devices from the CLI. The next step was to work out what commit broke the library. I did this by git bisecting between the good version (0.10) and the bad version (0.11.1). The resulting commit was 38a40c5, where the approach to encrypting the payloads changed. By analysing the change set, I was able to come up with a patch for which I’ve submitted a PR.

I then decided to try this out by installing my version of the library inside my HASS Docker container (temporarily) to see if it resolved the issue. Weirdly, I ran into the same issue! After some debugging to make sure it’s picking up the right python module I did some packet captures with tcpdump. I could see that the authentication packet payload was different to that of the working library outside of HASS. At this point I’m a bit stumped. I’ve submitted an issue to HASS, but in the meantime I decided to come up with a workaround.

Unix to the Rescue

Since I now had a working CLI tool, I could now get the Broadlink working with HASS via the shell_command integration in HASS. I started by creating a new python virtual environment from inside my HASS container and installed my version of the python-broadlink library:

docker exec -it ha_homeassistant_1 bash
cd /config
python -m venv ./broadlink-env
source broadlink-env/bin/activate
apt update
apt install -y git
pip install -U git+https://github.com/webworxshop/python-broadlink.git@fix-pyaes
which python # save the output from this command
exit

It’s necessary to do this inside the container because the command will be called from within the container by HASS, so we want to make sure that venv gives us a compatible python environment. The virtualenv will be persisted to the home assistant config volume.

The next step was to copy the Broadlink CLI tool into my shell_commands directory and update the she-bang on the first line with the output from the which command above:

#!/config/broadlink-env/bin/python

I then wrote a simple wrapper script to save me from having to specify all the options in my HASS config files later (saved to shell_commands/broadlink_cli_wrapper.sh):

#!/bin/bash

/config/shell_commands/broadlink_cli --type 0x2737 --host IP --mac MAC --send $1

This just sends whatever comes in as argument 1 to the script with the broadlink_cli tool. Make sure to fill in the IP of your device and it’s MAC address (lowercase, bytes reversed).

HASS Configuration

Now we can move on to configuring this in HASS. I used the learning functionality of the CLI tool to grab the hex codes for each of the buttons on my soundbar remote. I then added them all to my HASS config (in a new package file):

shell_command:
  soundbar_power_toggle: /config/shell_commands/broadlink_cli_wrapper.sh 2600600000012192131213121212133614111311143513371336121213121311133712121312123713111312131212121312131113121311133712371336133613361435143513361300056a0001264914000c3f0001254a15000c3d0001284712000d050000000000000000
  soundbar_source_tv: /config/shell_commands/broadlink_cli_wrapper.sh 260060000001219313111312131113361411131213361237133614111311131212371311131214351435143514351436131114111212131213111411141112121336143514361336130005670001274813000c3d0001254914000c3c0001264814000d050000000000000000
  soundbar_source_aux: /config/shell_commands/broadlink_cli_wrapper.sh 2600600000012192141113111312133613121311133613371237131113121311133713111312133613111312133613361312131113121312123712371311131213361336133614351300056b0001264912000c400001264913000c400001264813000d050000000000000000
  soundbar_source_bluetooth: /config/shell_commands/broadlink_cli_wrapper.sh 2600500000012192141112121312133614111212133614351436131113121311143514111411123713111435141113121237121213121311133614111435143514111336143514351300056a0001254914000d050000000000000000
  soundbar_volume_up: /config/shell_commands/broadlink_cli_wrapper.sh 2600600000012093141112121312123713111312133613361336131213121212133613121311133712121336143513371237131113121312123713111312131113121336133613361400056a0001264913000c410001254915000c3e0001264913000d050000000000000000
  soundbar_volume_down: /config/shell_commands/broadlink_cli_wrapper.sh 260058000001229213111312121213361312121213371237123712121312131213361212131212371336133613361336143612121312121213121212141113121212133613361337130005680001274913000c3d0001274912000d05
  soundbar_bass_up: /config/shell_commands/broadlink_cli_wrapper.sh 260058000001229214111212131212371312131113361336143612121312131113361411131213361212141112371336133613121237131113371237121213121212133712121336140005660001264a13000c3c0001274814000d05
  soundbar_bass_down: /config/shell_commands/broadlink_cli_wrapper.sh 260050000001209313111411131213361311131213361336143513121311141114351411121214351435131214351336143514111435141112131236141113121212143514111336130005670001274814000d050000000000000000
  soundbar_mute_toggle: /config/shell_commands/broadlink_cli_wrapper.sh 260050000001209313121311141112371311131214341535143513121312121214351312131113371212131212121312121214361311141112371237133614351336141113361435130005690001264914000d050000000000000000
  soundbar_effect_movie: /config/shell_commands/broadlink_cli_wrapper.sh 26004800000122921212141113111337121213121237133613361411121213121336131212121336133613371237121214351337133612121312131113121336131212121411133613000d05
  soundbar_effect_night: /config/shell_commands/broadlink_cli_wrapper.sh 260058000001219214111212131213361410131213361336143514111411121213361411131113371336131114111336143513361336141114111336123712121312131212121435140005690001274814000c400001244914000d05
  soundbar_effect_music: /config/shell_commands/broadlink_cli_wrapper.sh 2600580000012093131113121311133712121312123712371336141112121312133613111411133613121311131213361336133613371311133614351435141113121311141112371300056a0001254913000c3f0001254913000d05
  soundbar_voice_adjust_1: /config/shell_commands/broadlink_cli_wrapper.sh 26004800000121921411131213111435141113111337133612371311131213121237131114111336131213361237133614351435143513121336141113111312121213121311133712000d05
  soundbar_voice_adjust_2: /config/shell_commands/broadlink_cli_wrapper.sh 2600500000012193131114111311133713111312133613361336141113111312133613121311133613361436133612371336143513361411131113121411131113121311141113361300056a0001264914000d050000000000000000
  soundbar_voice_adjust_3: /config/shell_commands/broadlink_cli_wrapper.sh 2600500000012192131213121212133613121311133712371336131113121411123713111312133613121212131213111411131113121333173513361336133712371336133614111200056a0001264912000d050000000000000000

Upon restarting your HASS instance this gives you a shell_command service for each button on the remote. I spent a little while testing all the commands via the dev-tools to make sure I had them all right.

Replacing the Remote

As a first step to fully automating the soundbar, I wanted to replace the remote with a card in my HASS UI. Then I don’t have to find the remote!

I started by creating several input_select entities for the source, effect and voice adjust options of the soundbar:

input_select:
  soundbar_source:
    name: Soundbar Source Select
    options:
      - TV
      - Aux
      - Bluetooth
    initial: TV
    icon: mdi:video-input-hdmi

  soundbar_effect:
    name: Soundbar Effect
    options:
      - Movie
      - Night
      - Music
    initial: Movie
    icon: mdi:movie

  soundbar_voice_adjust:
    name: Soundbar Voice Adjust
    options:
      - Level 1
      - Level 2
      - Level 3
    initial: Level 1
    icon: mdi:voice

I then mapped the options to the correct service calls using automations:

automation:
  - alias: Set soundbar source
    trigger:
      platform: state
      entity_id: input_select.soundbar_source
    action:
      service_template: >
        shell_command.{% if trigger.to_state.state == 'TV' %}soundbar_source_tv{% elif trigger.to_state.state == 'Aux' %}soundbar_source_aux{% else %}soundbar_source_bluetooth{% endif %}

  - alias: Set soundbar effect
    trigger:
      platform: state
      entity_id: input_select.soundbar_effect
    action:
      service_template: >
        shell_command.{% if trigger.to_state.state == 'Movie' %}soundbar_effect_movie{% elif trigger.to_state.state == 'Night' %}soundbar_effect_night{% else %}soundbar_effect_music{% endif %}

  - alias: Set soundbar voice adjust
    trigger:
      platform: state
      entity_id: input_select.soundbar_voice_adjust
    action:
      service_template: >
        shell_command.{% if trigger.to_state.state == 'Level 1' %}soundbar_voice_adjust_1{% elif trigger.to_state.state == 'Level 2' %}soundbar_voice_adjust_2{% else %}soundbar_voice_adjust_3{% endif %}

Here, I make good use of the service_template option and the trigger object to map the services. The main downside of this approach is that the automation doesn’t trigger if you re-select the currently selected option in the UI. Since we have no idea what the actual state of the soundbar is, it would be useful to be able to do this.

I did something similar for the power state and mute state, this time using input_boolean:

input_boolean:
  soundbar_power:
    name: Soundbar Power
    initial: off
    icon: mdi:flash

  soundbar_mute:
    name: Soundbar Mute
    initial: off
    icon: mdi:volume-mute

automation:
  - alias: Set soundbar power
    trigger:
      platform: state
      entity_id: input_boolean.soundbar_power
    action:
      service: shell_command.soundbar_power_toggle

  - alias: Set soundbar mute
    trigger:
      platform: state
      entity_id: input_boolean.soundbar_mute
    action:
      service: shell_command.soundbar_mute_toggle

This isn’t ideal, especially for the power state since because there is no state feedback it will often be out of sync. If I can get some feedback on the power state (see below) I intend to convert this to a template switch.

Creating Presets

I created a couple of scripts containing the preferred settings for both TV watching and music. These just set the settings in sequence, with some delays to allow the soundbar to react (it’s a bit slow):

script:
  soundbar_preset_movie:
    alias: Soundbar Preset Movie
    sequence:
      - service: input_boolean.turn_on
        entity_id: input_boolean.soundbar_power
      - delay:
          seconds: 15
      - service: input_select.select_option
        data:
          entity_id: input_select.soundbar_source
          option: TV
      - delay:
          seconds: 1
      - service: input_select.select_option
        data:
          entity_id: input_select.soundbar_effect
          option: Movie
      - delay:
          seconds: 1
      - service: input_select.select_option
        data:
          entity_id: input_select.soundbar_voice_adjust
          option: Level 2

  soundbar_preset_music:
    alias: Soundbar Preset Music
    sequence:
      - service: input_boolean.turn_on
        entity_id: input_boolean.soundbar_power
      - delay:
          seconds: 15
      - service: input_select.select_option
        data:
          entity_id: input_select.soundbar_source
          option: Aux
      - delay:
          seconds: 1
      - service: input_select.select_option
        data:
          entity_id: input_select.soundbar_effect
          option: Music

I think I’ll comment out the power on step and following delay until I get the power feedback working. Currently if the state is inverted in HASS the script will not do the right thing. It’s annoying to have to make sure the states line up before triggering the script.

Lovelace UI

I created a card in Lovelace to allow me to control all of this. I don’t show the power control here, because I have another card which controls power for all my media devices.

automate dumb audio system
My Soundbar Card in Lovelace

Here’s the YAML:

entities:
  - action_name: Execute
    icon: 'mdi:movie'
    name: Movie Preset
    service: script.turn_on
    service_data:
      entity_id: script.soundbar_preset_movie
    type: call-service
  - action_name: Execute
    icon: 'mdi:music'
    name: Music Preset
    service: script.turn_on
    service_data:
      entity_id: script.soundbar_preset_music
    type: call-service
  - entity: input_boolean.soundbar_mute
    name: Mute
  - entity: input_select.soundbar_source
    name: Source Select
  - entity: input_select.soundbar_effect
    name: Effect
  - entity: input_select.soundbar_voice_adjust
    name: Voice Adjust
show_header_toggle: false
title: Soundbar
type: entities

That gives me pretty much full control over the soundbar from within HASS. I haven’t integrated the volume and bass controls yet, mainly due to the lack of level feedback. The volume is controllable from the TV remote and we don’t adjust the bass much. As such these aren’t really necessary right now.

Conclusion

Wow, that turned out to be a long post. It actually turned out to be a much larger project than I expected, given all the issues I ran into. If I’d have known how much trouble I was going to have with the RM Mini, I probably would have opted to build my own IR blaster with an ESP module. That said, now that it’s working, it’s great. The device also looks pretty good, which is a plus for something which must be located prominently.

I obviously want to get this working with the native HASS integration. Hopefully the integration will be fixed soon, but my workaround gives me a functional device in the meantime.

Further Work

I’d also like to fix the main defect in the current setup, which is the power feedback issue. This stands in the way of further automation, since currently you need to check the power state closely. I have a couple of possibilities for this. The first option is to use a power monitoring smart switch to detect when the soundbar is powered up. Whether this will be successful will depend on the power usage of the soundbar and the accuracy of the power monitoring. This also requires me to invest in further hardware.

The second option is to try and detect the soundbar via its bluetooth interface. My current plan is to do this via l2ping. I haven’t had much time to look into this yet, but if I could get it working that would be great. I already have a Raspberry Pi 3 in close proximity to the soundbar so that can do the detection. This can then be fused with the IR commands using a template switch in HASS.

If you made it this far, thanks for reading and getting through my rambling thoughts! I’ll be following up this post with further updates on fixing the problems detailed here, so please follow me if you’d like to see how I solve them.

If you liked this post and want to see more, please consider subscribing to the mailing list (below) or the RSS feed. You can also follow me on Twitter. If you want to show your appreciation, feel free to buy me a coffee.