Replacing my AVR's faulty Spotify Connect implementation with a Raspberry Pi

Posted on Sun 07 February 2021 in raspberrypi

For the second time in a row, upon returning from a few weeks away from home my Marantz AVR stopped showing up in the list of available Spotify Connect on all my devices. The alternative would be to connect via Bluetooth, but that's not an option since it requires device to stay connected the whole time, while also draining extra battery via the BT connection.

A quick look around and I found raspotify, a wrapper around librespot that promises a Spotify Connect client for the Raspberry Pi that Just Works™.

My initial idea was to install raspotify on my RPi with LibreELEC but, due to the just-enough-os-for-kodi nature of the OS, that would require more effort than I was willing to invest. I then decided to install it on an old RPi 2 I had laying around, which would become a dedicated Spotify Connect device.

Turns out that the raspotify slogan is spot-on: after the installation I was immediately able to find the new Spotify Connect device on all my playback devices. I did introduce a small config change in order to enable playback through my HiFiBerry Digi+ Pro:

OPTIONS="--device plughw:CARD=sndrpihifiberry"

I did however immediately start missing a feature that the AVR provided: whenever I selected it as the playback device on Spotify Connect, it would turn on automatically and switch to "Spotify" mode. I was aware of the CEC protocol (and its intricacies) through the magic of controlling my Kodi installation with my TV remote, and I decided to see if I could wield such protocol to emulate the missing functionality.

The first hurdle was to be able to trigger commands upon connecting to the Spotify Connect server on the RPi. Turns out that librespot offers such functionality through the --onevent argument, which allows you to execute an arbitrary command when it detects an event.

The cec-utils package in raspbian provides the cec-client command, which allows for the execution of CEC commands. The first thing was to get a list of available commands:

# echo h | cec-client -s -d 1
opening a connection to the CEC adapter...

================================================================================
Available commands:

...
[tx] {bytes}              transfer bytes over the CEC line.
...
[on] {address}            power on the device with the given logical address.
...
[as]                      make the CEC adapter the active source.
...
[scan]                    scan the CEC bus and display device info
...
================================================================================

The second step was to discover the CEC-enabled devices on the network:

$ echo scan | cec-client -s -d 1

opening a connection to the CEC adapter...
requesting CEC bus information ...
CEC bus information
===================
...
device #3: Tuner 1
address:       2.0.0.0
active source: no
vendor:        Marantz
osd string:    AV Receiver
CEC version:   unknown
power status:  standby
language:      ???
...

currently active source: Recorder 1 (1)

Interestingly, the RPi itself didn't show up. There's a suggestion here to solve that (by adding hdmi_force_hotplug=1 to `/boot/config.txt), but since I would not need to interact with the RPi directly I didn't try it.

The first goal was to turn on the AVR. This was rather straightforward. You can do it using either the device ID or its address.

echo "on 3" | cec-client RPI -s -d 1

or

echo "on 2.0.0.0" | cec-client RPI -s -d 1

The next step was to set the RPi running raspotify as the active source in the AVR. I initially didn't want to connect the raspotify RPi with an HDMI cable, so I though about using ssh to execute CEC commands on my Kodi RPi. With this setup, the Kodi RPi had to tell the AVR to set the raspotify RPi as the active source.

By accident I discovered that running cec-client with no parameters acts as a sort of active CEC scanner. I saw a couple of addresses flying around in its output, and I decided to try switching the AVR output from the RPi to something else and back a couple of times. Sure enough an interesting bit of informations started popping up:

# cec-client
...
TRAFFIC: [           25407]     >> 5f:80:22:00:21:00
DEBUG:   [           25408]     >> Audio (5) -> Broadcast (F): routing change (80)
...
TRAFFIC: [           25407]     >> 5f:80:21:00:22:00
DEBUG:   [           25408]     >> Audio (5) -> Broadcast (F): routing change (80)
...

The debug messages gave me a few keys to understand the CEC frames, which were basically showing a routing change (i.e. changing the output) from address 2.1.0.0 to address 2.2.0.0. I was hoping I could find an equivalent command that instead of a routing change with two addresses, would take a single address and activate it.

After a bit of digging into an intro to CEC frames, a list of CEC codes, and most importantly a CEC message encoder and decoder, I found out that "82" was the code I was after. So I gave it a shot:

echo "tx 5f:82:21:00" | cec-client RPI -s -d 1

And voilà, it worked: the AVR switched its output to the RPi.

I now had all the commands I needed to run, so I built a script to pass to librespot through the --onevent argument:

if [ "$PLAYER_EVENT" = "start" ]; then # this makes the code run only upon the initial connection to raspotify
    echo "on 3" | cec-client RPI -s -d 1
    echo "tx 5f:82:21:00" | cec-client RPI -s -d 1
fi

This worked well, but had a side effect: after the code run for the first time, I lost the ability to control Kodi using my TV remote, ergo, the Kodi CEC capabilities died whenever I used cec-client on that RPi. I then decided to cave in and connect the raspotify RPi to the AVR using an HDMI cable and doing everything locally.

The first consequence of this was that now the RPi running the CEC commands was the one that I wanted to become the active source, so the code could be simplified by using the CEC "active source request" command:

if [ "$PLAYER_EVENT" = "start" ]; then # this makes the code run only upon the initial connection to raspotify
    echo "on 3" | cec-client RPI -s -d 1
    echo "as" | cec-client RPI -s -d 1
fi

And there it was. All functionality replicated, good riddance to Marantz's crappy Spotify Connect implementation. But...

Turns out that the AVR didn't always turn on upon the first raspotify connection. Maybe 1 out of 5 times I had to switch the source to something else, and then try again. This was rather annoying, so I decided to look for a Python library that could help me solve this by giving me a bit more control over things. I eventually settled for https://github.com/trainman419/python-cec.

I then came up with this Python code, which makes sure the AVR is on before continuing:

#!/usr/bin/python
import cec
from time import sleep

cec.init("RPI")

avr = cec.Device(cec.CECDEVICE_AUDIOSYSTEM)

for i in range(10): # try 10 times, otherwise give up
    if not avr.is_on():
        print("AVR is off, sending poweron signal")
        avr.power_on()
        sleep(6) # wait for the AVR to finish powering on
    else:
        cec.set_active_source()
        break

Instead of the couple of cec-client calls, the bash script now calls this Python script.

Unfortunately CEC commands require root privileges, so I had to allow the raspotify user to execute the Python script as root via the sudoers file. A small price to pay for a finally robust, working Spotify Connect implementation.