YoutubeTV - channel with ADB - channel codes

Because I’m a PHP hack? :smiley:

1 Like

I know this is an old post, but I had to reply… You are my hero! Damn fine use of automations!!!

Thanks for doing this I’m going to have to try this out!

Looks a bit involved for me though

Is this still working in 2024?

I’ve refined my setup quite a bit, but yes it works, and the codes are now stable. I have also integrated it with xmltvlistings.com. At some point if I ever get free time, I should write up the whole thing in the projects channel. :slight_smile:

Here’s the script I use for tuning channels. There’s a lot of extra bits you may not want, but it also show how I handle FireTV differently than AndroidTV (e.g. Chromecasts). Per channel, you just need to snag the code from the URL when mousing over from the web, behind “/watch/” in the URL.

alias: "TV: Tune Channel"
sequence:
  - alias: Set variables
    variables:
      channel_code: >
        {% set code = state_attr("sensor.youtubetv_channel_lookup", "details") |
        selectattr("name", "equalto", channel) | map(attribute='code') | list
        %}    

        {% if (code | count()) == 0 %}
          NOTFOUND
        {% else %}
          {{ code | first }}
        {% endif %}    
      current_code: >
        {% set cur_code = state_attr("sensor.youtubetv_channel_lookup",
        "details") | selectattr("name", "equalto", states('sensor.' ~ device ~
        '_channel')|upper) | map(attribute='code') | list %}    

        {% if (cur_code | count()) == 0 %}
          NOTFOUND
        {% else %}
          {{ cur_code | first }}
        {% endif %}
  - condition: template
    value_template: "{{ (device | upper) != 'NONE' }}"
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ channel_code != \"NOTFOUND\" }}"
            alias: If channel is valid
        sequence:
          - alias: Tune if Channel is different than the Current Channel
            if:
              - condition: template
                value_template: "{{ current_code != channel_code }}"
                alias: If channel is different than the current channel
            then:
              - choose:
                  - conditions:
                      - condition: template
                        value_template: "{{ device == \"den\" }}"
                        alias: Google TV Devices
                    sequence:
                      - alias: If Off, turn Device on and Wait
                        if:
                          - condition: template
                            value_template: >-
                              {{ is_state("media_player." + device + "_adb",
                              "off") }}
                            alias: If Device is Off
                        then:
                          - service: media_player.turn_on
                            metadata: {}
                            data: {}
                            target:
                              entity_id: media_player.{{ device }}_adb
                          - delay:
                              hours: 0
                              minutes: 0
                              seconds: 3
                              milliseconds: 0
                      - service: androidtv.adb_command
                        data:
                          command: >-
                            am start -a android.intent.action.VIEW -d
                            https://tv.youtube.com/watch?v={{ channel_code }} -n
                            com.google.android.youtube.tvunplugged/com.google.android.apps.youtube.tvunplugged.activity.MainActivity
                        target:
                          entity_id: media_player.{{ device }}_adb
                  - conditions:
                      - condition: template
                        value_template: "{{ device == \"becca_tv\" }}"
                        alias: Fire TV Devices
                    sequence:
                      - alias: If Off, turn Device on and Wait
                        if:
                          - condition: template
                            value_template: >-
                              {{ is_state("media_player." + device + "_adb",
                              "off") }}
                            alias: If Device is Off
                        then:
                          - service: media_player.turn_on
                            metadata: {}
                            data: {}
                            target:
                              entity_id: media_player.{{ device }}_adb
                          - delay:
                              hours: 0
                              minutes: 0
                              seconds: 3
                              milliseconds: 0
                      - service: androidtv.adb_command
                        data:
                          entity_id: media_player.{{ device }}_adb
                          command: >-
                            am start -a android.intent.action.VIEW -d
                            https://tv.youtube.com/watch?v={{ channel_code }} -n
                            com.amazon.firetv.youtube.tv/dev.cobalt.app.MainActivity
                default:
                  - alias: If Off, turn Device on and Wait
                    if:
                      - alias: If Device is Off
                        condition: template
                        value_template: "{{ is_state(\"remote.\" + device, \"off\") }}"
                    then:
                      - service: remote.turn_on
                        metadata: {}
                        data: {}
                        target:
                          entity_id: remote.{{ device }}
                      - delay:
                          hours: 0
                          minutes: 0
                          seconds: 3
                          milliseconds: 0
                      - service: remote.turn_on
                        data:
                          activity: https://tv.youtube.com
                        target:
                          entity_id: remote.{{ device }}
                      - delay:
                          hours: 0
                          minutes: 0
                          seconds: 4
                          milliseconds: 0
                  - service: remote.turn_on
                    data:
                      activity: https://tv.youtube.com/watch/{{ channel_code }}
                    target:
                      entity_id: remote.{{ device }}
        alias: Regular YouTube TV Channels
      - conditions:
          - condition: template
            value_template: "{{ \"PRIME\" in (channel | upper) }}"
            alias: Channel Requested is PRIME
        sequence:
          - service: remote.turn_on
            data:
              activity: https://app.primevideo.com
            target:
              entity_id: remote.{{ device }}
        alias: Amazon Prime
      - conditions:
          - condition: template
            value_template: "{{ \"ESPN+\" in (channel | upper) }}"
            enabled: true
            alias: Channel Requested is ESPN+
        sequence:
          - service: remote.turn_on
            data:
              activity: sportscenter://x-callback-url
            target:
              entity_id: remote.{{ device }}
            enabled: true
        alias: ESPN+
    default:
      - service: notify.{{ device }}
        data:
          message: "{{ channel }}"
          title: Invalid Channel!
          data:
            fontsize: max
            position: center
            duration: 15
            color: red
        alias: TV Notification of invalid channel
mode: queued
max: 10
fields:
  device:
    selector:
      select:
        options:
          - living_room_tv
          - bedroom_android_tv
          - den
    required: true
    name: Device
  channel:
    selector:
      text: null
    name: Channel
    required: true

Yeah, screenshot is a showoff, but a lot of effort went into obtaining that high WAF. :stuck_out_tongue:

1 Like

Well now that you’ve shown off. You know we need to see some yaml for that dashboard card. :slightly_smiling_face: Also, I’d love more info on how you’re populating sensor.youtubetv_channel_lookup. This would be a really well received integration if you packaged it all together.

I have PHP files hosted in Synology Web Station doing all the downloads and parsing all the files into a single JSON string, and then that sensor is a rest sensor to pull it into HA. It’s more than most want to deal with, but we are into TV.

Then the card allows us to change the channel with a single click, or add events to calendar which will change the channel for us automatically.

It really should be a HA integration but that would take a bit of a lift at this point. If I get time I’ll put all the parts in git, but since you asked here is the yaml for the card.

    card:
      type: vertical-stack
      cards:
        - type: horizontal-stack
          cards:
            - type: entities
              card_mod:
                style: |
                  .card-content, .card-content > div {
                    margin: 6px 5px 0px 3px !important;
                    padding: 0px !important;                    
                  }              
              entities:
                - input_select.tv_channel_category
            - type: entities
              card_mod:
                style: |
                  .card-content, .card-content > div {
                    margin: 0px 5px 0px 3px !important;
                    padding: 0px !important;                    
                  }               
              entities:
                - entity: sensor.youtubetv_channel_lookup
                  name: Channels
                  icon: mdi:youtube-tv
                - entity: script.tv_download_channels_and_listing
                  icon: mdi:refresh-circle
                  name: Channels
                  action_name: Reload
        - type: custom:auto-entities
          card:
            type: entities
            title: YouTube TV Guide
            icon: mdi:youtube-tv
            card_mod:
              style:
                .: |
                  ha-icon {
                    color: red;
                  }              
                  ha-card {
                    height: 80vh !important;
                    overflow: auto;
                  }
          filter:
            template: >
              {% set ns = namespace(results = []) %}

              {% set channels =
              state_attr("sensor.youtubetv_channel_lookup","details") |
              selectattr("is_alias", "equalto", false) %} {% if not
              is_state("input_select.tv_channel_category", "All") %}
                {% set filtered_channels = namespace(channels = []) %}
                {% for c in channels %}
                  {% if (c.category == states("input_select.tv_channel_category") or c.on_now.category == states("input_select.tv_channel_category")) %}
                    {% set filtered_channels.channels = filtered_channels.channels + [c] %}
                  {% endif %}
                {% endfor %}
                {% set channels = filtered_channels.channels %}
              {% else %}
                {% set channels = channels | list %}
              {% endif %}

              {% for channel in channels %}
                {% set ns.results = ns.results + [{
                    "type": "custom:template-entity-row",
                    "card_mod": {
                      "style": ("#wrapper {border-radius: 6px; background-image: linear-gradient(to right, rgba(255,193,7,1) 0%, rgba(255,193,7,0) 100%);} " if states("sensor.[[entity_id]]_channel").upper() == channel.name else "") + "div.info {font-weight: bold; font-size: 12pt;} state-badge {flex: 0 0 80px !important; background-size: contain; background-position: center; background-repeat: no-repeat; border-radius: 0px;} div.secondary {border: 1px solid var(--app-header-background-color);border-radius: 5px;position: relative;} div.secondary:after {padding: 2px;content: '" + channel.on_now.start + " - " + channel.on_now.end + "'; background: var(--app-header-background-color); position: absolute; top: 0; bottom: 0; left: 0; width: " + channel.on_now.pct|string + "%; }"
                    },
                    "name": channel.on_now.title + (" ["  + channel.on_now.sub_title + "]" if channel.on_now.sub_title is defined else "") or channel.name,
                    "secondary": "-",
                    "image": channel.icon,
                    "icon":"mdi:youtube-tv",
                    "state": "",
                    "tap_action": {
                      "action": "fire-dom-event",
                      "browser_mod": {
                        "service": "browser_mod.popup",
                        "data": {
                          "title": channel.name,
                          "content": "<img style='float: right' src='" + channel.icon + "' /><b><u>" + channel.on_now.title + "</u></b>" + (": "  + channel.on_now.sub_title if channel.on_now.sub_title is defined else "") + "<br/>" + ("<ha-icon icon='mdi:new-box' style='color: green; padding-right: 3px;'></ha-icon>" if channel.on_now.new is defined and channel.on_now.new else "") + "<i>" + channel.on_now.start + " - " + channel.on_now.end +"</i>" +  "<br/>" + ("<img style='border-radius: 8px; max-width: 100%; height: auto;' src='" + channel.on_now.icon + "' />" if channel.on_now.icon is defined else "") + ("<br/><br/>" + channel.on_now.desc if channel.on_now.desc is defined else "")  + ("<br/><br/>Staring: " + channel.on_now.credits.actor|join(", ") if channel.on_now.credits is defined and channel.on_now.credits.actor is defined else "")  + ("<br/><br/>Guests: " + channel.on_now.credits.guest|join(", ") if channel.on_now.credits is defined and channel.on_now.credits.guest is defined else "") + ("<br/><br/>Directed By: " + channel.on_now.credits.director|join(", ") if channel.on_now.credits is defined and channel.on_now.credits.director is defined else ""),
                          "card_mod": {                        
                            "style": {
                              "ha-dialog$":".mdc-dialog__actions {overflow: hidden;}",
                              "": ".header {margin-bottom: -10px;} .main-title {display: inline; position: relative; top: 17px; font-weight: bold; font-size: larger; margin-top: 10px !important;} .content {padding: 0px 20px !important;}"
                            }                        
                          },
                          "right_button": "Tune now",
                          "right_button_action": {
                            "service": "script.tv_tune_channel",
                            "data": {
                              "device": "[[entity_id]]",
                              "channel": channel.name
                            }                      
                          }
                        }
                      }
                    },
                    "hold_action": {
                      "action": "call-service",
                      "service": "script.tv_tune_channel",
                      "data": {
                        "device": "[[entity_id]]",
                        "channel": channel.name
                      }
                    },
                  }]
                %}
                {% set ns.results = ns.results + [{
                    "type": "custom:template-entity-row",
                    "card_mod": {
                      "style": "#wrapper {min-height: 30px !important; height: 30px; font-style: italic; border-bottom: 1px solid silver; " + ("border-radius: 6px; background-image: linear-gradient(to right, rgba(101,255,194,1) 0%, rgba(101,255,194,0) 100%);" if state_attr("calendar.tv_auto_tune", "description") | upper == channel.name and state_attr("calendar.tv_auto_tune", "start_time")|as_datetime|as_local == channel.on_next.start_full|as_datetime else "") + " } state-badge {flex: 0 0 80px !important; text-align: right;} "
                    },
                    "name": "" if channel.on_next.title == "" else "Next: " + channel.on_next.title + (" ["  + channel.on_next.sub_title + "]" if channel.on_next.sub_title is defined else ""),
                    "icon": "" if channel.on_next.title == "" else "mdi:arrow-right-bottom",
                    "state": "",
                    "secondary": "" if channel.on_next.title == "" else channel.on_next.start + " - " + channel.on_next.end,
                    "tap_action": {
                      "action": "fire-dom-event",
                      "browser_mod": {
                        "service": "browser_mod.popup",
                        "data": {
                          "title": channel.name,
                          "content": "<img style='float: right' src='" + channel.icon + "' /><b><u>" + channel.on_next.title + "</u></b>" + (": "  + channel.on_next.sub_title if channel.on_next.sub_title is defined else "") + "<br/>" + ("<ha-icon icon='mdi:new-box' style='color: green; padding-right: 3px;'></ha-icon>" if channel.on_next.new is defined and channel.on_next.new else "") + "<i>" + channel.on_next.start + " - " + channel.on_next.end +"</i>" +  "<br/>" + ("<img style='border-radius: 8px; max-width: 100%; height: auto;' src='" + channel.on_next.icon + "' />" if channel.on_next.icon is defined else "") + ("<br/><br/>" + channel.on_next.desc if channel.on_next.desc is defined else "")  + ("<br/><br/>Staring: " + channel.on_next.credits.actor|join(", ") if channel.on_next.credits is defined and channel.on_next.credits.actor is defined else "")  + ("<br/><br/>Guests: " + channel.on_next.credits.guest|join(", ") if channel.on_next.credits is defined and channel.on_next.credits.guest is defined else "") + ("<br/><br/>Directed By: " + channel.on_next.credits.director|join(", ") if channel.on_next.credits is defined and channel.on_next.credits.director is defined else ""),
                          "card_mod": {                        
                            "style": {
                              "ha-dialog$":".mdc-dialog__actions {overflow: hidden;}",
                              "": ".header {margin-bottom: -10px;} .main-title {display: inline; position: relative; top: 17px; font-weight: bold; font-size: larger;} .content {padding: 0px 20px !important;}"
                            }                        
                          },
                          "left_button": "Tune at " + channel.on_next.start,
                          "left_button_action": {
                            "service": "calendar.create_event",
                            "data": {
                              "entity_id": "calendar.tv_auto_tune",                          
                              "summary": channel.on_next.title,
                              "description": channel.name,
                              "start_date_time": channel.on_next.start_full,
                              "end_date_time": channel.on_next.end_full
                            }                      
                          },           
                          "right_button": "Tune now",
                          "right_button_action": {
                            "service": "script.tv_tune_channel",
                            "data": {
                              "device": "[[entity_id]]",
                              "channel": channel.name
                            }                      
                          }
                        }
                      }
                    },                
                    "hold_action": {
                      "action": "call-service",
                      "service": "script.tv_tune_channel",
                      "data": {
                        "device": "[[entity_id]]",
                        "channel": channel.name
                      }
                    }
                  }]
                %}            
              {% endfor %}

              {{ ns.results }} 

2 Likes

…and the YAML for the sensor attributes looks like this:

details: 
- name: USA
  category: Favorites
  code: jsh6k-d901M
  icon: https://cdn.tvpassport.com/image/station/76x28/v2/s11207_h15_af.png
  on_now:
    title: 'Law & Order: Special Victims Unit'
    start: 3:00 PM
    end: 4:00 PM
    pct: 80
    sub_title: Hammered
    desc: >-
      After a night of drinking, a man (Scott Foley) wakes up with a head injury
      and a dead woman in his bed; Detectives Benson and Stabler try to piece
      together the crime by returning to the bar where the night began.
    category: Drama
    icon: https://cdn.tvpassport.com/image/show/960x540/v2/p7857446_e_h10_ac.jpg
    credits:
      actor:
        - Christopher Meloni
        - Mariska Hargitay
        - Richard Belzer
  on_next:
    title: 'Law & Order: Special Victims Unit'
    start: 4:00 PM
    end: 5:00 PM
    start_full: '2024-07-11T20:00:00+00:00'
    end_full: '2024-07-11T21:00:00+00:00'
    sub_title: Hardwired
    desc: >-
      After a young boy is raped, detectives find out their suspect is the
      leader of a civil rights group that supports adult-child relationships.
    category: Drama
    icon: https://cdn.tvpassport.com/image/show/960x540/v2/p184536_i_s4_ab.jpg
    credits:
      actor:
        - Christopher Meloni
        - Mariska Hargitay
        - Richard Belzer
  is_alias: false
2 Likes

Thank you! That’s enough for me to run with for sure. If you’d be inclined to upload what you have to github somewhere, I’d be willing to take a crack at an integration for the masses. Thanks again.

I have a lots of bits and pieces so I’m not sure I got them all but this is my trove: GitHub - TarheelGrad1998/ha-tv: Home Assistant integrations with YoutubeTV and XMLTVListings

Included there:

Required Input for the json output of channels.php:

  • channel_aliases.json: Some aliases that I use for easier voice control
  • channel_ids.json: A mapping of channel name to the XMLTVListings id. Also where I categorize channels for filtering in the card
  • The downloaed XMLTVListings files (downloaded nightly by fetch_listings.php). Note that I download 2 listings to completely cover our channels. YMMV
  • browse.json from YouTubeTV, for getting the YTTV channel codes. I couldn’t figure out how to automate this, and for a while the YTTV channel codes were changing regularly, but they haven’t in some time. Anyway, I just go to tv.youtube.com and find the data in “browse” file and save those contents.

Other Notes:

  • There are my local networks included here. Change those out as desired.
  • I also subscribe to google calendars for some sports teams and use the Team Tracker integration to grab which channel the games are on. You’ll see that in the auto tune automation.
  • There is one bug in the card, where rarely it will not render listings for a brief while. My guess is something in a program name or some such breaks the string built code, but I’ve never been able to track it down because as soon as that program passes, it works again.
3 Likes

This is amazing information! I have been working on my own project off and on the last year and NEVER came across anything this helpful because it is so difficult to search for “Youtube TV” solutions. Also a user of home assistant but never thought to look here before.

I am trying to use python to control a few Android TV’s I have and haven’t really got anywhere so this would help.

How is the reliability over the last few years you have been using it? Has the channel lookup been working well with how often they change links? Any quirks about sending those commands to Android TV to know? Any reason you couldn’t channel scroll if links are not found because they changed them? Could take awhile and be annoying but if there is a way in the background to do it overnight or something it may work??

On the python side it would be great if it worked well enough to possibly setup a module (similar to pytube for Youtube) where it can find links and setup calls quickly. I am no developer but may see what I can do if its a reliable solution…

I think as far as you’re asking, it has been stable for me for the last year, probably more. Early on, every few months I would get a YTTV error when tuning and have to download the codes again. But my current codes file is from June 2023 so it hasn’t changed in over a year. I suspect YouTube may have done something to make the codes backwards compatible, but I can’t confirm that, just a feeling.

I could never find a way to automatically download or scrub the codes. It’s all behind the YTTV login, and I couldn’t figure out how to reproduce the authentication (and as you say, documentation is sparse).

The only quirks for sending to Android TV is that the package name is different on FireTV vs true Android TV devices. That I’ve handled conditionally in my tune script, but it could be done better.

The only issues I have now is XMLTVListings related: the card occasionally not rendering (as I covered previously), and my overnight download of the listings occasionally fails. The former, I want to track down but only have as long as the offending show breaks it to debug. The latter, I just added a control to download them again manually.

1 Like

So on the python side of things what I have been doing is using selenium and firefox. The trick that I got to work was to first setup the browser “profile” by logging in normally. Firefox uses user profiles to save this information.

Once the google login is setup the first time manually, I am then able to control and scrub YoutubeTV by sending direct links to channels or grabbing info with the profile logged in. Not sure how this could help you in the HA realm, but if there was a way to get the login information in the first time manually, then maybe there is a chance it could work better and avoid the login each time by automation.

There may not not be a need for it anymore because I can also attest to the links not breaking. The same links I saved manually for a few channels a year ago, still redirect me to the proper channels with different links today.

Seems like you have stayed on this topic and tried many things, just thought I would throw in what worked for me to see if it means anything for you or anyone else that may come across this.

Anyone know how to send these links to Apple TVs?