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.

Lovelace Multi-room Audio Contols

Quick Project: Lovelace Multi-Room Audio Controller

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

Welcome to a new segment on my blog! Here I’m going to do quick write ups of some of the little projects that I complete. These projects often come between the bigger ones that I usually write about. This is going to be bit of an experiment, but I’m aiming to publish these in addition to my usual blogging schedule. However, they will be done as time permits so may not be as regular.

I wanted to start doing posts like this, since I realised I do quite a few small projects which never make it onto the blog. Primarily this is because I just forget about them once they’re complete. Let’s see how it goes and please let me know if you like these posts in the comments or via Twitter.

Today’s Project: a Lovelace Multi-Room Audio Controller

This post is about a bit of playing around I was doing in Lovelace (the new Home Assistant UI) over the weekend. I started out installing the Home Assistant Community Store (HACS) and came across the Mini Media Player card. This struck me as the perfect thing for making better controls for my multi-room audio system. Without further ado, here are the finished Lovelace multi-room audio controller in all its glory:

Lovelace Multi-room Audio Contols
I love the beautifully minimalist look of this

Basically, we have controls of the main Mopidy music player. This is followed by the volume controls for the overall Snapcast group and the individual Snapcast clients. This is pretty much a copy of the layout in the Snapcast Android client. However, that lacks any way to control the player.

Get to the YAML!

Below is the YAML I used to create my Lovelace multi-room audio controller. Is should be noted that this can be entered through the GUI editor, by just switching to the raw editor. You don’t need to be using YAML mode.

type: vertical-stack
cards:
  - artwork: cover
    entity: media_player.mopidy
    group: true
    hide:
      source: true
      volume: true
    icon: 'mdi:music-circle'
    name: Multi-Room Audio
    type: 'custom:mini-media-player'
  - entity: media_player.snapcast_group_mopidy
    group: true
    hide:
      controls: true
      power: true
      power_state: true
      source: true
      volume: false
    icon: 'mdi:speaker-multiple'
    name: Master Volume
    type: 'custom:mini-media-player'
  - entity: media_player.snapcast_client_outdoor_speakers
    group: true
    hide:
      controls: true
      power: true
      power_state: true
      source: true
      volume: false
    icon: 'mdi:speaker-multiple'
    type: 'custom:mini-media-player'
  - entity: media_player.snapcast_client_soundbar
    group: true
    hide:
      controls: true
      power: true
      power_state: true
      source: true
      volume: false
    icon: 'mdi:speaker-multiple'
    type: 'custom:mini-media-player'
  - entity: media_player.snapcast_client_bedroom_tv
    group: true
    hide:
      controls: true
      power: true
      power_state: true
      source: true
      volume: false
    icon: 'mdi:speaker-multiple'
    type: 'custom:mini-media-player'

This consists of a vertical stack card containing several mini media player cards with the group setting set to true to make them nest fine into a what appears visually to be a single control panel. I then just use the hide parameters to get rid of the controls I don’t need on each one and add a custom icon for each. Done!

Conclusion

I really like this layout. The only improvement I can think of would be if the cover artwork for the currently playing track was displayed. You can see I’ve tried this in the YAML above, but it didn’t work for me. This might be more of a limitation of the MPD integration in HASS than of mini media player.

That’s it for now. Again, please let me know if you like this post format and I’ll keep doing them in between my other posts.

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.

Multi-Room Audio Client

Multi-Room Audio System: Indoor and Outdoor Audio with Snapcast and Mopidy

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

One of the projects I really wanted to do when moving into our new house was build a multi-room audio system. Traditional multi-room audio systems, such as Sonos, cost a massive amount for the functionality they provide. It looks like a cheaper alternative is now available using Chromecasts, but you are still at the mercy of what the manufacturer wants to do (like discontinuing the Chromecast Audio).

In this post I’m going to detail my multi-room audio setup, which plays perfectly synced audio across three sets of speakers, both indoors and outdoors. This system is 100% DIY and uses Free Software throughout. It’s also cheaper than even a single Sonos speaker.

System Overview

My system is comprised of a central server running Mopidy and Snapcast (the snapserver portion) and three audio players, each running the Snapcast client (snapclient).

Two of the clients are resident on the Raspberry Pi systems we use for Kodi on our TVs. One of these is located in the Living Room and connects into our soundbar. The other is located in the master bedroom and currently just uses the TV speakers.

The third client is located on a Raspberry Pi in our loft space, which is connected via an amplifier to speakers mounted outdoors by our patio.

The parts list for this setup is as follows:

The Fusion speakers listed above are outdoor/marine rated and certainly seem fine in the New Zealand climate (warm humid summers, wet cool winters). They are definitely not the best speakers in the world (the price reflects that). However, the quality is sufficient for my application of background/work music in an outdoor environment.

Overall, the total cost for the components ordered for this project was less than NZ$250. This comes in at less than the price of a single Sonos speaker. I’ve not included the Raspberry Pis in this, since I already had them and only one was specifically installed for this project.

Software Setup

The software setup is a pretty standard for this kind of project – basically just Mopidy feeding audio to Snapcast. As such I’m not going to give a full installation guide, since there are plenty of resources available. Take a look at the links below for full instructions (these are the resources I used when setting this up):

Multi-Room Audio UI
The Web UI Via Iris

In terms of client/remote control software, I’m using Iris as a web interface for Mopidy. On the Android side I’m using M.A.L.P. as well as the Snapcast app. M.A.L.P. seems to be a reasonable MPD client and supports multiple servers, which may come in useful in future. The main issue I have with it is that it gets the album art wrong frequently and there seems to be no way to override it’s choices (or use the correct album art from the server).

Of course, I also have both Mopidy and Snapcast integrated with Home Assistant!

Outdoor Speaker Hardware Setup

So far, so easy. Here is where I ran into issues. I mounted the speakers to the brick wall of our house just fine, but ran into problems running the cables up through the roof space to the amplifier. This was mainly due to one speaker being on the corner of the house where the roof is low. In this corner the steel supports for the roof were too close together for me to squeeze through. Also the level of the soffit where the cable came in was lower than ceiling height, so that the soffit forms a well around the outside of the house. All this made it nearly impossible to grab the cable.

Multi-Room Audio Speakers
Left Speaker (the one with the tricky cable)
Multi-Room Audio Speakers
Right Speaker

I fashioned a makeshift tool from an old mop handle and reacher grabber with a line attached to the handle so that I could actuate it from the end of the pole. I even went as far as installing the Android IP Webcam app on an old phone and mounting that on the far end. With this I could then view the image on my phone and use the light on the camera end to see better. This helped, until the battery on the phone died! Eventually I managed to grab the cable by pushing the whole length of it up through the soffit. The resulting bundle was much easier to grab.

Phew, now we’re getting somewhere…

Overall, getting the speakers installed took most of a day, with several hours spent laying on my front in the (hot) loft space trying to grab the cable. The provided speaker cables also had to be lengthened with some extra speaker cable from my local DIY store. Luckily I knew this before I installed them and didn’t have to pull them back.

Multi-Room Audio Client
The loft RPi with relays, sitting on top of the amplifier
Multi-Room Audio Client
The LED on the amplifier is unnecessarily bright

The remainder of the install was pretty much plug and play. I connected one of the USB soundcards to the Raspberry Pi and connected it’s output via audio cable to the amplifier. I spliced the relay into the 12V power line from the power supply to the amplifier to allow me to remotely control it’s power. Both the RPi and the amp are powered from the mains sockets I previously had installed in the loft.

Node-RED Relay Control

Multi-Room Audio Power Control
Power Control is done via Home Assistant

As with the relay power control for my room sensors, I used Node-RED to turn the relay on and off via MQTT. The flow uses my Home Assistant MQTT Discovery approach to be automatically added to HASS. There’s not much to say about this since it’s pretty much identical to the setup for the room sensors. Here’s the flow:

Multi-Room Audio Power Control
The relay power control flow

And here’s the corresponding JSON:

[{"id":"cd2cc1ba.21df68","type":"rpi-gpio out","z":"47863444.0972b4","name":"Relay 1","pin":"11","set":true,"level":"0","freq":"","out":"out","x":740,"y":60,"wires":[]},{"id":"b3084312.29b44","type":"mqtt in","z":"47863444.0972b4","name":"Outdoor Speakers Command","topic":"homeassistant/switch/outdoor_speakers/cmd","qos":"2","broker":"65d3656f.217c14","x":170,"y":80,"wires":[["72ed2721.9aa348"]]},{"id":"72ed2721.9aa348","type":"switch","z":"47863444.0972b4","name":"On or Off?","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"ON","vt":"str"},{"t":"eq","v":"OFF","vt":"str"}],"checkall":"false","repair":false,"outputs":2,"x":410,"y":80,"wires":[["36e87336.07603c","545168e2.b9a558"],["32dd96e5.5737fa","545168e2.b9a558"]]},{"id":"36e87336.07603c","type":"change","z":"47863444.0972b4","name":"ON","rules":[{"t":"set","p":"payload","pt":"msg","to":"1","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":60,"wires":[["cd2cc1ba.21df68"]]},{"id":"32dd96e5.5737fa","type":"change","z":"47863444.0972b4","name":"OFF","rules":[{"t":"set","p":"payload","pt":"msg","to":"0","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":100,"wires":[["cd2cc1ba.21df68"]]},{"id":"636d5fdd.480fc8","type":"mqtt out","z":"47863444.0972b4","name":"Send Messages","topic":"","qos":"2","retain":"true","broker":"65d3656f.217c14","x":730,"y":240,"wires":[]},{"id":"946d297a.c325","type":"function","z":"47863444.0972b4","name":"Format config messages","func":"var config = {\n    payload: {\n        name: \"Outdoor Speakers\",\n        command_topic: \"homeassistant/switch/outdoor_speakers/cmd\",\n    },\n    topic: \"homeassistant/switch/outdoor_speakers/config\"\n};\nreturn config;","outputs":1,"noerr":0,"x":390,"y":240,"wires":[["636d5fdd.480fc8"]]},{"id":"1f888f96.a08ec","type":"inject","z":"47863444.0972b4","name":"@Startup","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":"3","x":130,"y":240,"wires":[["946d297a.c325"]]},{"id":"545168e2.b9a558","type":"function","z":"47863444.0972b4","name":"Set topic","func":"msg.topic = \"homeassistant/switch/outdoor_speakers/state\";\nreturn msg;","outputs":1,"noerr":0,"x":600,"y":160,"wires":[["636d5fdd.480fc8"]]},{"id":"65d3656f.217c14","type":"mqtt-broker","z":"","name":"MQTT Broker","broker":"mqtt.example.com","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"nodered/loft/status","birthQos":"2","birthRetain":"true","birthPayload":"online","closeTopic":"nodered/loft/status","closeQos":"2","closeRetain":"true","closePayload":"offline","willTopic":"nodered/loft/status","willQos":"2","willRetain":"true","willPayload":"offline"}]

I also have a couple of automations which I use to mute/unmute the relevant Snapclient when the speakers are turned off. My completely unfounded hypothesis is that Snapcast should be intelligent enough to not send any data to muted clients, which should reduce unnecessary traffic on the network. I’ve not done any investigation to verify this however. In any case, here are the automations:

automation:
  - alias: "Mute outdoor speakers when powered down"
    trigger:
      platform: state
      entity_id: switch.outdoor_speakers
      from: "on"
      to: "off"
    action:
      service: media_player.volume_mute
      data:
        entity_id: media_player.snapcast_client_outdoor_speakers
        is_volume_muted: true

  - alias: "Unmute outdoor speakers when powered up"
    trigger:
      platform: state
      entity_id: switch.outdoor_speakers
      from: "off"
      to: "on"
    action:
      service: media_player.volume_mute
      data:
        entity_id: media_player.snapcast_client_outdoor_speakers
        is_volume_muted: false

Indoor Setup with Libreelec and Kodi

It wouldn’t be a multi-room audio setup with out multiple clients! So on to the indoor systems. These are the running on my two Libreelec systems, connected to the TVs. The first of these is the most interesting since that connects to to our Polk Signa S2 soundbar. I’m actually planning a review of this in the near future, but for now we’ll just say it sounds awesome. I didn’t include it in the hardware list above since I didn’t purchase it just for this project.

I connected to the soundbar using the second USB soundcard and audio cable. This means I can play audio without having the TV on, just by setting the soundbar to it’s AUX input. The other system in the master bedroom, just sends audio via the HDMI port to the TV.

Multi-Room Audio on Libreelec
Settings of the Libreelec Snapclient Plug-in

On the software side of these I used the excellent Libreelec Snapclient plug-in. Just install it from the official Libreelec repo and you’re good to go. In order for the auto-discovery to work, you should make sure that the Snapserver and Libreelec machine are on the same network. The only other issue I had is that sometimes the ‘list sound cards’ dialog in the plugin settings wouldn’t work. I found it easier to just list the devices on the command line with snapclient -l and put the relevant device number into the addon settings.

Conclusion and Next Steps

Overall, this system is pretty great. It’s served us well for outdoor audio all through the summer and has become our primary way of listening to our music collection. There are a few rough edges, like the issues with album art on Android.

The main other point of complaint is the profusion of volume controls. This could be a separate rant altogether, since everything has it’s own volume control for some reason. For this system I just don’t touch the volume in Mopidy and use the individual channel controls in Snapcast. It looks like there is now a plugin to provide better integration here, but I haven’t tried it yet.

The next steps for this system will be to re-build the server side system as part of my ongoing migration to Docker+VMs. At this point I’d like to add a couple more groups to the Snapserver. One of these will be for audio streaming in over Bluetooth. This will allow for us to stream audio directly from our phones to any of the speakers in the house.

The second group will be for TTS notifications from Home Assistant. I know I can move channels between groups via HASS automations to decide where the audio goes. The main stumbling block on this at the moment is how to get the audio from the HASS server to the media server, which will be separate VMs. If anyone has any ideas here, please let me know!

That’s it for now. Thanks for reading!

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.