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:

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 }} 

1 Like

…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
1 Like

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.
1 Like