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!

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.

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.

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.

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 w e 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.