Home Theatre Control!

You may remember that last year I set up a Rube Goldberg machine that allowed me to control my TV with Home Assistant. The overly complicated setup consisted of CircuitPython code running on a Feather M4 (specifically the M4 — this didn’t work right with M0, nRF, or ESP32), a web application exposing its functionality (and speaking to the Feather over a serial port), and a custom Home Assistant component that would speak to that web app. The component provided an interface both for the HDMI switch and for the infrared emitter that simulated the TV remote control — the receiver (a Yamaha RX-V475 that I bought in Dublin, right after buying the TV) already had a working Home Assistant integration.

Well, since last October we finally decided to change our TV, it was due time to go and have a bit of a cleanup of the whole setup, among other things because the Yamaha is no longer in place, having been replaced by the HT-A7000 soundbar (yes, the same as Techmoan’s — we ordered it together with the TV, with a stern warning that it could take up to three months to arrive… it arrived a week later, only a few days after the TV!)

New Landscape

The new TV is also a Sony, which funnily enough means that most of the IR control code works exactly the same as before: the TV respond to the same set of power and input commands as the old TV did, which is always fascinating to me. But between the soundbar and the TV, there’s seven different integrations in Home Assistant before adding my own!

The TV itself shows up as a DLNA Media Receiver, as a Google Cast target (it runs Google TV, duh!), and also as a HomeKit compatible target — Home Assistant supports all three of these and they are even auto-detected. From the Settings screen, you can find out it also supports Control4, which is some more entreprisey solution that does not appear to have support in HA out of the box (it appears to require some sort of controller), and RS232C over HDMI — and while looking at this particular option, I found myself going down a rabbit hole that from one side made my life much easier, but on the other it’s likely going to cause me a headache later!

I started trying to get this to work with HomeKit, but it turned out to be a nuisance: while reporting power state worked fine, source selection didn’t work for most inputs, and there was no power control. I gave up. DLNA wouldn’t have anything useful to do, and Google Cast only works when it comes to actual Cast usage, which is not what we do most of the time anyway.

So first of all, let’s exclude that RS232C over HDMI: the page from Sony already says that this is only supported with the CBX-H10/1 devices, which are discontinued. These are some sort of Serial-to-HDMI converter boxes, and I’ve got no idea how they work. I’m half tempted to buy one just to figure it out, but for now I’m okay without. These are probably used widely in hotels and hospitality in general, so they seem to surface fairly regularly on eBay and other sources.

But the same page reports not one but three other protocols for “IP Control” — and with IP Control they mean network control, obviously IPv4 only. There’s a REST API using JSON-RPC, there’s IRCC-IP which is SOAP (thus XML) based, and there’s a “Simple IP Control” (SSIP) which is a much simpler and simplistic protocol, even simpler than the RS232C option. The documentation for the Simple IP Control shows you how to test this out with netcat, which surprised me in a very positive way.

At this point, the only service that seemed to work was SSIP — except Home Assistant definitely doesn’t support it. On the other hand, Home Assistant does support a Bravia TV control, which at first I thought wouldn’t work with my TV. Turns out it does instead, but you need to manually fill in the IP address because there is no discovery. This version is not the fastest option available (among other things because it does not use asyncio), but it seems to be working reliably. It also provides access to pretty much everything that you may want to control the TV for, including power, state, and sources.

As for the soundbar, the situation is a bit better: besides the same DLNA Media Receiver and Google Cast, it supports a different IP protocol called Songpal, which is discovered via UPnP/DLNA but otherwise implemented as a standalone thing. The good thing is that this is also supported by Home Assistant, and when it works, it works very well. I say when because it turned out not to work right when I tried configuring it the first time, which was due to a bug in the async_upnp_client library that it uses behind the scene. I fixed the bug and updated the dependencies in Home Assistant, so it’s all good to go for me.

The good thing about Songpal is that it provides full control to every single knob the soundbar has. This means that, once properly exposed to Home Assistant. Unfortunately, it turns out that this was quite the task. While there is a library implementing the protocol, it was not tested with my soundbar, and found a couple of things that need to be addressed.

The Home Assistant integration, on the other hand, needed quite a bit more work. Not only it didn’t implement any of the knobs (despite the library being able to discover them), but it was completely missing the input source selection, and couldn’t report anything but the power status. This was an interesting deep dive in the code to just make sure it all worked right, particularly when losing and restoring connection, but the end result was being able to turn night mode on when it got late and we still watched TV, and to make sure the sound mode would switch back to standard when playing videogames.

Since Songpal didn’t originally properly report the state of the bar, particularly when playing Spotify with Spotify Connect, I found myself also relying on the DLNA integration, since that already reports everything it needs.

At the end of all of this there’s also the HDMI switcher for which I wrote the original custom components in the first place. It’s still there, because between the TV and the soundbar, I only get five inputs total. Even considering that I’m not needing Chromecasts, FireTV, or AppleTV for it, and that I failed at securing a PS5 (I did, though, order a gaming PC to share with my wife!), that’s not enough ports.

Stringing Multiple Media Players Together

So having now half a dozen integration-provided entities, I found myself wondering how I can string these together into a single “Home Theatre” media player entity. My original idea first was to figure out if there was a template-based integration, like there is for fans, lights, and others. There isn’t, but there is the Universal Media Player integration, which is… nearly good.

The idea behind this is that you can have a linear integration of media players, but it turns out to be a lot more complicated than that, at least for myself, and the fact that it appears to predate most of modern Home Assistant best practices doesn’t make it easy to work with.

Let’s start with the first problem: commands and attributes are not actions and templates. The commands only allow calling a single service: you cannot add multiple actions like you can in a template switch turn_on and turn_off, so to make sure that both the TV and the soundbar turn off at the same time, I had to wrap those around in a template switch instead. Similarly because the only templatized attribute is the state, if you want to provide something more complicated than a single list of source selections, you need to do so with an intermediate select entity.

It might sound like a minimal problem, since it’s not something you expect to run into very often, to have multiple inputs, but it does make things complicated for me since, first of all, I have a two-levels selection of inputs, and secondly I don’t want to have every single app on the TV to show up as a valid source.

The end result is that my select input for this is defined as such:

template:
  - select:
      - name: "Home Theatre Source"
        state: >-
          {% if is_state_attr("media_player.sony_bravia_tv", "source", "HDMI 2") %}
            {{ states("select.hdmi_switch_input") }}
          {% else %}
          {{ state_attr("media_player.sony_bravia_tv", "source") or "none" }}
          {% endif %}
        optimistic: true
        select_option:
          - service: media_player.turn_on
            target:
              entity_id: media_player.sony_bravia_tv
          - wait_for_trigger:
            - platform: state
              entity_id: media_player.sony_bravia_tv
              to: 'playing'
              timeout: 
            timeout: '00:00:03'
          - choose:
              - conditions:
                - condition: template
                  value_template: >-
                    {{ option in (state_attr("select.hdmi_switch_input", "options") or []) }}
                sequence:
                  - service: media_player.select_source
                    target:
                      entity_id: media_player.sony_bravia_tv
                    data:
                      source: "HDMI 2"
                  - service: select.select_option
                    target:
                      entity_id: select.hdmi_switch_input
                    data:
                      option: "{{option}}"
            default:
              service: media_player.select_source
              target:
                entity_id: media_player.sony_bravia_tv
              data:
                source: "{{option}}"
        options: >-
          {% set blocked_inputs = ["AV", "PortalTV", "Recorder_1", "Youtube Music", "Recorded Title List", "TV Control with Smart Speakers", "Help", "Timers & Clock", "Internet Browser", "BT Sport", "deezer"] %}
          [
          {%- for input in state_attr("select.hdmi_switch_input", "options") or [] %}
          "{{ input }}",
          {%- endfor %}
          {%- for input in state_attr("media_player.sony_bravia_tv", "source_list") or [] %}
          {%- if not input.startswith("HDMI ") and input not in blocked_inputs %}
          "{{ input }}",
          {%- endif %}
          {%- endfor %}
          ]

This shows a little bit more of the complexity of selecting a source: while you cannot choose a source on a media_player entity until it is on, the Universal Media Player allows you to consider the media player as a whole “on” when the TV is still off — which I want for things like Spotify, so that for sure it turns off when we go to bed or leave the apartment.

Finally, the other annoying part of the Universal Media Player as implemented at the time of writing, is that it does not allow you to specify additional attributes. You can set the state of the media player to “Playing”, but there is no way to copy over the attributes of the media playing from the Soundbar, Kodi, or PlayStation 4. And this in turn means you cannot know how to dispatch the various media play/pause/stop/previous/next commands — unless you write a whole lot of scripts and trigger those on each one of the commands. Not something I’m looking forward to: I don’t need it urgently enough, I’ll try to get it fixed upstream sooner or later.

You may have noticed two interesting points in the definition above: the first is that the HDMI switch is now a select entity rather than a media_player — I’ll get to that in a moment, there’s a whole section on that. The other one is that there is a three seconds timeout while waiting for the TV to turn on. This is because there’s a bit of a problem with the current implementation of the BraviaTV integration: it uses the requests library, which is not asynchronous (which means most operations end up slowing down the whole of Home Assistant).

While it seems like to support every possible feature needed, the BraviaTV integration is required to implement both JSON and SOAP protocol, it does not look like they ever tried implementing the SSIP protocol — which would probably solve a few of the current idiosyncrasies with the delays. It also would allow fetching the MAC address to enable Wake-up-on-LAN, except I use WiFi so it’s very unlikely it’s going to be working (I tried using USB3-to-Gigabit, but it ended up being slower than WiFi, so I gave up.)

So, About The HDMI Switcher…

Back when I started the work automating the home theatre, I was very early in my adoption of Home Assistant. The end result was that instead of a drop-down selection of sources, I ended up with a bunch of scripts that set up the various media players the way I expected them to be. Now that I set up the Universal Media Player integration instead, I have a lot more flexibility in how I handle those scripts, as the complexity is hidden below a different wrapping layer (or two, as noted above.)

I also wasn’t deeply in the bowels of additional integrations, so, as an example, I didn’t have a MQTT broker running on my local network. Nowadays I do, because I run zigbee2mqtt for controlling the Zigbee lights and buttons — yeah I should possibly reconsider ZHA but that might happen only when Home Assistant Yellow is released. And while Srdjan had suggested me to look into using ESPHome for this I didn’t have enough experience to figure out how to do it properly.

Well, now things are quite different. So the obvious long term strategy is to remove the need for the whole Home Assistant-to-NUC-to-Feather machinery, and use an ESP32 devkit to connect to the HDMI switch. To be honest, if I had the expertise and experience I’d rather have a custom switcher that includes an ESP32 as the main controller, and I’m actually half-tempted, because then I would also know the state of the various ports at all time… but that’s probably well above my abilities right now. I might play with it later.

I actually sent a PCB to manufacture for that before writing this blog, which also included the IR blaster for the TV — before I realized the Bravia integration worked fine, it just means I will not be populating that part of the board. It hasn’t arrived yet, and there might be more to write when it does, but until then I needed something to keep working.

While practically there was no reason for me to replace the old custom component at this point, I also decided to see if I could simplify the whole handling of it, because it has happened before that a Python update stopped it from starting, and gave me headaches when I couldn’t figure out why Home Assistant was just failing to start. It also required me to change the list of sources in two places, because of bad design on my part.

So instead I decided to use MQTT. Instead of running a web application on the NUC to bridge to the Feather, I then just need to publish a selector for it, and react on change requests from Home Assistant. Much better architecturally. Originally I expected to publish also a switch for the TV (to go with the IR blaster), together with a few commands to send to change settings on the TV itself. But as I said above, that wasn’t needed.

The story of how I ended up implementing the whole thing is worthy of another blog post, since it has nothing to do with Home Assistant. Suffice to say that the MQTT-based implementation is not just a lot nicer to use with HA, but also a lot simpler to write. Among other things, since messages are processed sequentially, there’s no need to worry about protecting access to the serial port, which simplifies the code significantly. Of course, this wouldn’t have mattered when I implemented it the first time: at the time I didn’t have a broker running, and setting one up would have taken me more time than doing the custom component dance I did.

Final Configuration

The final configuration block for the Universal Media Player looks even more complicated. Among other things, to figure out what the right status to show I needed to check the power state of both the TV and soundbar to take a decision… and in the case of the soundbar, the actual playback needs to be fetched from the DLNA entity, but that entity cannot actually tell if the soundbar is on.

media_player:
  - platform: universal
    name: "Home Theatre"
    device_class: tv
    children:
      - media_player.sony_bravia_tv
      - media_player.ht_a7000
    commands:
      turn_on:
        service: switch.turn_on
        target:
          entity_id: switch.home_theatre_power
      turn_off:
        service: switch.turn_off
        target:
          entity_id: switch.home_theatre_power
      volume_set:
        service: media_player.volume_set
        target:
          entity_id: media_player.ht_a7000
        data:
          volume_level: "{{ volume_level }}"
      volume_up:
        service: media_player.volume_up
        target:
          entity_id: media_player.ht_a7000
      volume_down:
        service: media_player.volume_down
        target:
          entity_id: media_player.ht_a7000
      volume_mute:
        service: media_player.volume_mute
        target:
          entity_id: media_player.ht_a7000
      select_source:
        service: select.select_option
        target:
          entity_id: select.home_theatre_source
        data:
          option: "{{ source }}"
    attributes:
      is_volume_muted: media_player.ht_a7000|is_volume_muted
      volume_level: media_player.ht_a7000|volume_level
      source: select.home_theatre_source
      source_list: select.home_theatre_source|options
    state_template: >-
      {% if is_state("media_player.sony_bravia_tv", "off") %}
        {% if is_state("media_player.ht_a7000", "off") %}
          off
        {% else %}
          {{ states("media_player.ht_a7000_dlna") }}
        {% endif %}
      {% else %}
        {{ states("media_player.sony_bravia_tv") }}
      {% endif %}

Next Steps

I now have a setup that appears to be working. Our night time and out-of-house routines are working fine, although I didn’t manage to get these to work as scenes, due to possibly some idiosyncrasies in the Universal Media Player integration. It’s okay, it just needs to be turned off.

From the point of view of “Me and me alone”, I need to get going replacing the Rube Goldberg machinery (now simplified with MQTT) to a dedicated ESPHome device. This is not just because of the complexity of the solution, but because with the single HDMI switcher to control, it’s not worth the complex cable management I’m using for it either. Also, the NUC might end up not being my HTPC for much longer, as me and my wife wanted to play more games (given that we’re not going anywhere it looks like), and ordered a gaming PC instead.

On the broader ecosystem, it would be a good idea to take a closer look at the BraviaTV integration, and figure it out if it could be updated to use aiohttp like most other Home Assistant integrations. It should also probably include a bit more debug logging, as there’s a problem or two that I noticed happen irregularly, when inputs are not always appearing in the list of sources. Adding support for SSIP could also help, as a number of commands can be sent through that in a much faster way than SOAP or JSON-RPC.

Getting an alternative to the Universal Media Player where more attributes can be templated would also be a significant quality of life improvement. Being able to select the playing state depending on the various inputs, instead of not showing anything at all. This definitely will require some work with the Home Assistant community, as it’s a fundamental change that I don’t expect to be able to lead alone. I’ll have to see what I can do about that.

I’m personally not going to spend time looking at the HomeKit integration: it just failed to work altogether for me, and it’s kind of pointless when the SSIP and the other protocols already provide everything I need from that. But I do want to get Songpal to be a state of the art integration, expanded to support all the various knobs in a way that is compatible with scenes, and actually implement all of the features it should.

To be honest, I would be more than happy to pay Nabu Casa an extra to just drop these as questions for them to answer or solve, but I also know that they already have a tough time prioritizing work, and that a pay-to-play project is not the best way to build a proper community. By the way, this is one more reason why analytics are important, and why I opted into them for Home Assistant: it helps the team understanding and prioritizing one type of work over another.