First things first - my apologies for the length of the post. But, if you have a Yamaha receiver, and have been searching for a better media player, I promise it will be worth it. Please do read until the end!
So, before I get into the nitty gritty of it, I feel the need to provide a bit of background:
I’m still relatively new to the HA ecosystem, having only gotten into this about 6 months ago. However, I have a very technical background, so I’m not afraid to dive in and figure out how to make things work the way I want them to work. That said - I’m not a programmer by trade, so while my solutions work, they may or may not be the most efficient or the most eloquent. I am doing this for functionality, not to flex my programming prowess for all the masses to critique.
The reason this particular setup came to be is that I started, like everyone else, I’m sure, by just using the default media player in HA. I quickly came to realize that this was somewhat limited - first, in terms of the volume control (so I wrote my own for that (was a separate post/project, but is also included here since it’s easier than trying to filter it out)), and then I wasn’t happy with the way the default media player handled source selection. I was further dissatisfied with the fact that when using the tuner, I was limited to presets, and there was no way to simply punch in the radio station I wanted to listen to. So I set about to change that.
Some of my requirements: It needed to be easy enough to use that my wife, or a guest, could look at the interface and figure it out without any coaching or instruction - that was first and foremost. After that, I wanted to be able to click a button and choose Plex, or Tuner, or to sync that channel/zone with the main channel/zone on that receiver (I have 3 receivers with a total of 6 zones for music). Further, I wanted the ability to sync the three receivers with one another, so that one source will play through the entire house (useful for parties). I wanted the volume control to allow me to adjust the volume above 0dB (which the built-in media player cannot do), and I wanted the ability to type in a radio station to change the tuner to that station (something the built-in media player also cannot do). Lastly, in the interest of a clean interface, I wanted these controls to only be visible when they were needed - meaning that you shouldn’t be able to adjust the source or the volume on a channel that isn’t even powered on, and you shouldn’t be able to adjust the tuner when the selected source was not the tuner. Further - when the amp for that zone was powered off, none of the controls should be visible at all.
This is what it looks like when the media player is off:
This is what it looks like when it’s turned on, and using any source other than the tuner:
This is what it looks like when the selected source is the tuner:
Some of this was relatively easily accomplished with some programming (of varying degrees of difficulty), and some of this was accomplished using hardware to link everything together and provide the necessary functionality.
To that end, here is my hardware list, and how everything is connected:
My receivers are Yamaha TSR-700s, which are the Costco version of the RX-V6A. (Note that I have also tested all of this on an RX-V681, which is like 6-7 years old, and it worked fine - so I imagine this would all work fine on any network-connected model, although I have no way of verifying directly on anything other than the two models listed above.) Each receiver has 2 Chromecast devices connected to it - one on HDMI1, and one using an HDMI to RCA adapter from Amazon that is then connected to Audio3. Then, the HDMI out from the main receiver goes into a 1:4 HDMI splitter. Each of the other two receivers have an HDMI cable coming into HDMI2 to receive the signal for the whole house setup, and then there are two more HDMI to RCA adapters employed that go into Audio4 on each of the other receivers to provide that signal to Zone 2. The use case for these is, admittedly, limited - however, I wanted the ability to be able to sync each zone independently to the primary source, in the unlikely event that someone wished to play something else on the primary zone of that receiver. This likely could have been avoided with more careful planning about which speaker pairs were the main zone and which were zone 2 - but this way does provide some additional flexibility - albeit a fringe use case. For those of you wondering what’s with all the RCA adapters - Zone 2 does not allow the selection of HDMI inputs as sources, at least not on any of the receivers I have. I suspect this is because they only have a single HDMI out - but if you have a more expensive receiver with multiple HDMI outs, you may be able to avoid all this extra nonsense.
I’m not going to provide the config for all three receivers and all 6 of the media player cards, nor am I going to go into a whole bunch of detail around the Plex setup (there are plenty of good resources on that already, and frankly - I’m far from an expert on that topic!). The media player card I am providing is the card from Zone 2 of one of the “slave” receivers, since Zone 2 has the most complexity - it can be sync’d to either the main zone on that receiver, or it can be sync’d to the main receiver channel. The main zone on the “slave” receiver config is the same, except it lacks the button for Sync to Main Zone, since it is the main zone. The “master” receiver in the chain, the Main Zone has only two source options - Plex and Tuner - since there is nothing else for it to sync to. Zone 2 on the “master” receiver only has the “Sync to Main Zone” option, since that has the same net effect as syncing any channel on the other 2 receivers to the HDMI output from the “master” receiver.
One other thing to note: Some of the cards and interface bits that were used are custom - and they all came from HACS. Accordingly, if you see a card type that begins with “custom:”, and you don’t have that card type yet, HACS is where to get it from. Good? Good. Let’s proceed!
Media Player card:
(Note that this card relies on 4 input_booleans - laundryplex, laundrytuner, laundrysync, and layndrypartysync. These 4 booleans control the display color of the 4 shortcut buttons for the 4 various sources that are directly selectable via the interface to provide feedback to the user for which source is presently selected. Also required is an input_text, in this case named laundryradio)
type: vertical-stack
cards:
- type: media-control
entity: media_player.floor2_zone2
- type: conditional
conditions:
- entity: media_player.floor2_zone2
state: playing
card:
type: vertical-stack
cards:
- type: horizontal-stack
cards:
- type: custom:button-card
entity: input_boolean.laundryplex
icon: mdi:plex
icon_height: 50px
styles:
card:
- height: 100px
icon:
- color: |
[[[
if (entity.state == 'on') return 'lime';
return 'grey'
]]]
tap_action:
action: call-service
service: media_player.select_source
service_data:
entity_id: media_player.floor2_zone2
source: audio3
name: plex
- type: custom:button-card
entity: input_boolean.laundrytuner
icon: mdi:radio
icon_height: 50px
styles:
card:
- height: 100px
icon:
- color: |
[[[
if (entity.state == 'on') return 'lime';
return 'grey'
]]]
tap_action:
action: call-service
service: media_player.select_source
service_data:
entity_id: media_player.floor2_zone2
source: tuner
name: radio
- type: custom:button-card
entity: input_boolean.laundrysync
icon: mdi:party-popper
icon_height: 75px
styles:
card:
- height: 100px
icon:
- color: |
[[[
if (entity.state == 'on') return 'lime';
return 'grey'
]]]
tap_action:
action: call-service
service: media_player.select_source
service_data:
entity_id: media_player.floor2_zone2
source: main_sync
name: Master BR
- type: custom:button-card
entity: input_boolean.laundrypartysync
icon: mdi:party-popper
icon_height: 75px
styles:
card:
- height: 100px
icon:
- color: |
[[[
if (entity.state == 'on') return 'lime';
return 'grey'
]]]
tap_action:
action: call-service
service: media_player.select_source
service_data:
entity_id: media_player.floor2_zone2
source: audio4
name: Party
- type: entities
entities:
- type: custom:slider-entity-row
entity: number.tone_control_bass_5
name: Bass
- type: custom:slider-entity-row
entity: number.tone_control_treble_5
name: Treble
- type: section
label: Volume dB
- type: custom:slider-entity-row
entity: input_number.laundry_volume
full_row: true
- type: conditional
conditions:
- entity: input_boolean.laundrytuner
state: 'on'
card:
type: horizontal-stack
cards:
- entity: input_text.laundryradio
type: custom:text-input-row
name: WARNING - also changes Master BR radio
- entity: script.change_station_laundry
name: Send
icon: mdi:send
show_name: false
show_icon: true
size: 35%
styles:
card:
- height: 60px
- width: 60px
type: custom:button-card
tap_action:
action: call-service
service: script.change_station_laundry
hold_action:
haptic: success
action: call-service
service: script.change_station_laundry
Input slider:
laundry_volume:
icon: mdi:volume-medium
name: Laundry Volume
initial: -35
min: -80
max: 8
step: 1
mode: slider
Change station laundry script (HA script, not shell script - goes in scripts.yaml):
change_station_laundry:
alias: change_station_laundry
sequence:
- service: shell_command.yamaha_tuner
data:
ip: IP.OF.YOUR.RECEIVER
station: '{{ states(''input_text.laundryradio'') }}'
mode: single
icon: mdi:send
Automations:
(Note that in the last automation in here, the one that sets the volume - the possible values for zone are Main_Zone and Zone_2)
- id: '1643111111114'
alias: Set laundryplex
description: ''
trigger:
- platform: state
entity_id: media_player.floor2_zone2
to: playing
from: 'off'
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: audio3
from: tuner
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: audio3
from: main_sync
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: audio3
from: audio4
condition:
- condition: state
entity_id: media_player.floor2_zone2
attribute: source
state: audio3
action:
- service: input_boolean.turn_on
target:
entity_id:
- input_boolean.laundryplex
data: {}
- service: input_boolean.turn_off
data: {}
target:
entity_id: input_boolean.laundrysync
- service: input_boolean.turn_off
data: {}
target:
entity_id: input_boolean.laundrytuner
- service: input_boolean.turn_off
data: {}
target:
entity_id: input_boolean.laundrypartysync
mode: single
- id: '1643111111115'
alias: Set laundrytuner
description: ''
trigger:
- platform: state
entity_id: media_player.floor2_zone2
to: playing
from: 'off'
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: tuner
from: audio3
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: tuner
from: main_sync
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: tuner
from: audio4
condition:
- condition: state
entity_id: media_player.floor2_zone2
attribute: source
state: tuner
action:
- service: input_boolean.turn_off
target:
entity_id:
- input_boolean.laundryplex
data: {}
- service: input_boolean.turn_off
data: {}
target:
entity_id: input_boolean.laundrypartysync
- service: input_boolean.turn_off
data: {}
target:
entity_id: input_boolean.laundrysync
- service: input_boolean.turn_on
target:
entity_id:
- input_boolean.laundrytuner
data: {}
mode: single
- id: '1643111111116'
alias: Set laundrysync
description: ''
trigger:
- platform: state
entity_id: media_player.floor2_zone2
to: playing
from: 'off'
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: main_sync
from: audio3
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: main_sync
from: tuner
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: main_sync
from: audio4
condition:
- condition: state
entity_id: media_player.floor2_zone2
attribute: source
state: main_sync
action:
- service: input_boolean.turn_off
target:
entity_id:
- input_boolean.laundryplex
data: {}
- service: input_boolean.turn_on
target:
entity_id:
- input_boolean.laundrysync
data: {}
- service: input_boolean.turn_off
target:
entity_id:
- input_boolean.laundrytuner
data: {}
- service: input_boolean.turn_off
data: {}
target:
entity_id: input_boolean.laundrypartysync
mode: single
- id: '1643111111117'
alias: Set laundrypartysync
description: ''
trigger:
- platform: state
entity_id: media_player.floor2_zone2
to: playing
from: 'off'
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: audio4
from: audio3
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: audio4
from: tuner
- platform: state
entity_id: media_player.floor2_zone2
attribute: source
to: audio4
from: main_sync
condition:
- condition: state
entity_id: media_player.floor2_zone2
attribute: source
state: audio4
action:
- service: input_boolean.turn_off
target:
entity_id:
- input_boolean.laundryplex
data: {}
- service: input_boolean.turn_off
target:
entity_id:
- input_boolean.laundrysync
data: {}
- service: input_boolean.turn_off
target:
entity_id:
- input_boolean.laundrytuner
data: {}
- service: input_boolean.turn_on
data: {}
target:
entity_id: input_boolean.laundrypartysync
mode: single
- id: '1643111111118'
alias: Set volume Laundry
description: ''
trigger:
- platform: state
entity_id: input_number.laundry_volume
condition:
- condition: state
entity_id: media_player.floor2_zone2
state: playing
action:
- service: shell_command.yamaha_vol
data:
ip: IP.OF.YOUR.RECEIVER
zone: Zone_2
vol: '{{ (trigger.to_state.state) | int * 10 }}
'
mode: single
yamahatuner.sh (goes in /config/scripts):
(Note the line that begins “ent=”. This line strips the . out of the input string so that you can enter stations like “103.3”, even though the receiver is expecting “103300” (the following line multiplies it by 100 to format it correctly).)
#!/bin/sh
curl_cmd="curl --silent --output /dev/null"
tune() {
foo=${2}
ent="${foo//./}"
stat=$((${ent} * 100))
${curl_cmd} http://${1}/YamahaExtendedControl/v1/tuner/setFreq?band=fm\&tuning=direct\&num=${stat}
}
"$@"
yamahavolume.sh (goes in /config/scripts):
#!/bin/sh
curl_cmd="curl --silent --output /dev/null"
volume() {
${curl_cmd} http://${1}/YamahaRemoteControl/ctrl --data-binary "'<YAMAHA_AV cmd=\"PUT\"><${2}><Volume><Lvl><Val>${3}</Val><Exp>1</Exp><Unit>dB</Unit></Lvl></Volume></${2}></YAMAHA_AV>'"
}
"$@"
config.yaml shell_command definitions:
shell_command:
yamaha_tuner: /bin/bash /config/scripts/yamahatuner.sh tune {{ ip }} {{ station }}
yamaha_vol: /bin/bash /config/scripts/yamahavolume.sh volume {{ ip }} {{ zone }} {{ vol }}