Tautulli REST End Point and custom-button-card

I was looking for a way to display Plex session data in HA. I came across this card, which looked great!

https://www.reddit.com/r/homeassistant/comments/zt56p0/custom_plex_tautulli_active_user_details_card/jfwjxo0/?context=3

However, I noticed an issue. It uses the official Tautulli integration, which creates sensors based on each USER of plex. We only really use a single user in my household, so whoever started a stream first - thats what was populated into the sensor.

What I wanted to replicate was the “activity” panel in Tautulli itself. This displays a card for each session that is running, even if they’re under the same user.

I’ve achieved this using the Tautulli REST API, 10 “session” template sensors, and and auto-entities / custom-button card. Here is the result:

Here’s what I did:

After some research, it looks like the official integration decided to make these sensors based on user because, well, its hard to anticipate how many sessions you’d possibly have running in your plex instance. Makes sense.

So I did some research of the Tautulli REST API, and figured out that I could pull the data into a sensor myself and parse it into templates. So I’ve got a main REST sensor that pulls in the “get_activity” end point into HA, and then 10 template sensors that parse the session array into 10 individual sensors.

Rest End Point:

  - platform: rest
    unique_id: tautulli_activity
    name: Tautulli Activity
    icon: mdi:plex
    scan_interval: 5
    force_update: true
    resource: http://[TAUTULLI SERVER:PORT]/api/v2?apikey=[YOUR API KEY HERE]&cmd=get_activity
    method: POST
    headers:
      Content-Type: application/json
    value_template: "{{ value_json.response.result }}"
    json_attributes_path: "$.response.data"
    json_attributes:
      - stream_count
      - sessions
      - stream_count_direct_play
      - stream_count_direct_stream
      - stream_count_transcode
      - total_bandwidth
      - lan_bandwidth
      - wan_bandwidth

I’ll likely switch to using a !secrets value for the whole url so that I don’t accidentally expose the key someplace.

Then the template sensors (UPDATED 07/04/24, now includes all 10!):

Please use the following gist, including all 10 was too big for the forum post!

Note that in the above, I’m referencing a different value in the ‘sessions’ attribute array for each sensor. The first session in the array is at spot [0] and the second session is in spot [1] and so forth.
I just copy/pasted this 10 times, and adjusted the unique_id, name, and array value for each instance.

UPDATE 04/28/23: Also make sure you update the state if/else with an incremented " |length >= [ x ]" value. See EDIT at the bottom for the details. So for the first session, it should be " |length >= [1]" and for the second session sensor it should be " |length >= [2]" and so forth…

UPDATE 07/04/24: Fixed a bug where when plex was doing a countdown to the next video, or had just ended playing a session, the template would be in a flux state and throw errors. So I added the same logic to count the number of sessions to each underlying attribute. I’ve also decided to just include all 10 in the example above to save everyone some effort.

Make sure you have auto-entities and custom-button-card installed from HACS!

Then, the custom-button-card. Unfortunately you can’t use !secret values in lovelace code (not that I’m aware of). So to get the poster image out, you’ll need to hard code the API key in the card.
Replace the server ip/domain/port/api key values in the card below where it resembles: http://[TAUTULLI SERVER:PORT]/api/v2?apikey=[YOUR API KEY HERE]…

I also use the “session_thumbnail” sensor from the delivered Tautulli integration to source the users icon as a fallback if the poster value doesn’t exist. So make sure that integration is set up and that all of the session_thumbnail entities are enabled (they may be disabled by default).

type: custom:auto-entities
filter:
  exclude:
    - state: unknown
    - state: unavailable
    - state: 'off'
  include:
    - entity_id: '*plex*session*'
      options:
        entity: this.entity_id
        type: custom:button-card
        variables:
          entity: this.entity_id
        custom_fields:
          picture:
            card:
              type: picture
              image: |
                [[[
                  if (states[variables.entity].attributes.grandparent_thumb != ''){
                    return "https://[TAUTULLI SERVER:PORT]/api/v2?apikey=[YOUR API KEY HERE]&cmd=pms_image_proxy&img=" + states[variables.entity].attributes.grandparent_thumb + "&width=300&height=450&fallback=poster&refresh=true";
                  } else {
                    if (states[variables.entity].attributes.thumb != ''){
                      return "https://[TAUTULLI SERVER:PORT]/api/v2?apikey=[YOUR API KEY HERE]&cmd=pms_image_proxy&img=" + states[variables.entity].attributes.thumb + "&width=300&height=450&fallback=poster&refresh=true"                    
                    } else {
                      return states['sensor.' + states[variables.entity].attributes.user + '_session_thumbnail'].state
                    }
                  } 
                ]]]
              card_mod:
                style: |
                  ha-card {
                    box-shadow: 0;
                    border-radius: 0;
                    margin: 5px 0 0 -5px;
                  }
                  ha-card img {
                    min-height: 100px;
                    min-width: 100px;
                  }
          bar:
            card:
              type: custom:bar-card
              entities:
                - entity: this.entity_id
              attribute: progress_percent
              unit_of_measurement: '%'
              positions:
                icon: 'off'
                indicator: 'off'
                name: inside
              height: 19px
              color: '#e49f29'
              name: |
                [[[
                  return states[variables.entity].state
                ]]]
              card_mod:
                style: |-
                  ha-card {
                    --ha-card-background: none;
                    border: none;
                    box-shadow: none;
                  }
                  ha-card #states {
                    padding: 0;
                  }
                  bar-card-currentbar, bar-card-backgroundbar {
                    border-radius: 5px;
                    left: 0;
                  }
                  bar-card-name {
                    margin-left: 3%;
                    text-shadow: 1px 1px 1px #0003;
                  }
                  bar-card-value {
                    margin-right: 3%;
                    text-shadow: 1px 1px 1px #0003;
                  }
          user: |
            [[[
              return "<b>" + states[variables.entity].attributes.user + "</b>"
            ]]]
          title: |
            [[[
              if (states[variables.entity].state == 'playing') {
                return "<ha-icon icon='mdi:play' style='width: 15px; height: 15px; position: relative; top: -2px;'></ha-icon> " + states[variables.entity].attributes.full_title;
              } else {
                if (states[variables.entity].state == 'paused') {
                  return "<ha-icon icon='mdi:pause' style='width: 15px; height: 15px; position: relative; top: -2px;'></ha-icon> " + states[variables.entity].attributes.full_title;
                } else {
                  return states[variables.entity].attributes.full_title;
                }
              }

            ]]]
          stream_label: <b>Stream</b>
          stream: |
            [[[
              return states[variables.entity].attributes.video_resolution + " > " + states[variables.entity].attributes.transcode_decision + " > " +states[variables.entity].attributes.stream_video_resolution + ""
            ]]]
          product_label: <b>Product</b>
          product: |
            [[[
              return states[variables.entity].attributes.product
            ]]]
          player_label: <b>Player</b>
          player: |
            [[[
              return states[variables.entity].attributes.player
            ]]]
          location_label: <b>Location</b>
          location: |
            [[[
              return states[variables.entity].attributes.location + ": " + states[variables.entity].attributes.ip_address
            ]]]
          media_detail: |
            [[[
              if(states[variables.entity].attributes.media_type == 'movie') {
                return "<ha-icon icon='mdi:filmstrip' style='width: 15px; height: 15px; position: relative; top: -2px;'></ha-icon> (" + states[variables.entity].attributes.year + ")";
              } else {
                return "<ha-icon icon='mdi:television-classic' style='width: 15px; height: 15px; position: relative; top: -2px;'></ha-icon> S" + states[variables.entity].attributes.parent_media_index + " • E" + states[variables.entity].attributes.media_index;
              }
            ]]]
          bandwidth_label: <b>Bandwidth</b>
          bandwidth: |
            [[[
              var bytes = states[variables.entity].attributes.bandwidth * 1000;
              var sizes = ['Bytes', 'Kbps', 'Mbps', 'Gbps', 'Tbps'];
              if (bytes == 0) return 'n/a';
              var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)));
              if (i == 0) return bytes + ' ' + sizes[i];
              return (bytes / Math.pow(1000, i)).toFixed(1) + ' ' + sizes[i];
            ]]]
        card_mod:
          style: |
            ha-card {
              box-shadow: 0;
              padding: 0;
              margin: 0;
              border: 0;
            }
            ha-card #container {
            margin: 5px 0 0 0;
            }
            #name {
              display:none;
            }
        styles:
          card:
            - height: 100x
            - padding: 0
          custom_fields:
            bar:
              - text-transform: capitalize
              - font-size: 13px
            user:
              - text-align: end
              - font-size: 15px
            title:
              - text-align: start
              - font-size: 13px
            stream:
              - text-transform: capitalize
              - text-align: start
              - font-size: 13px
            product:
              - text-transform: capitalize
              - text-align: start
              - font-size: 13px
            player:
              - text-transform: capitalize
              - text-align: start
              - font-size: 13px
            location:
              - text-transform: uppercase
              - text-align: start
              - font-size: 13px
            media_detail:
              - text-transform: uppercase
              - text-align: start
              - font-size: 13px
            bandwidth:
              - text-transform: capitalize
              - text-align: start
              - font-size: 13px
            product_label:
              - text-transform: uppercase
              - text-align: end
              - font-size: 10px
            player_label:
              - text-transform: uppercase
              - text-align: end
              - font-size: 10px
            stream_label:
              - text-transform: uppercase
              - text-align: end
              - font-size: 10px
            location_label:
              - text-transform: uppercase
              - text-align: end
              - font-size: 10px
            bandwidth_label:
              - text-transform: uppercase
              - text-align: end
              - font-size: 10px
          grid:
            - grid-template-areas: |
                "picture product_label product"
                "picture player_label player"
                "picture stream_label stream"
                "picture location_label location"
                "picture bandwidth_label bandwidth"
                "picture bar bar"
                "picture title title"
                "picture media_detail user"
            - grid-template-columns: 1fr 60px 3fr
            - grid-gap: 5px 10px
card:
  type: vertical-stack
card_param: cards

UPDATE 04/28/23: Make sure you add “- state: ‘off’” to the exclude filter. See the EDIT at the bottom for the details.
UPDATE 01/08/24: If you used this previously, card_mod has been updated with a breaking change for the style: elements in the button card. I’ve updated the YAML above, and much thanks for @derailius for the update!

In closing, I was able to mostly recreate the Tautulli cards that are in the main Tautulli interface inside of HA using the button card and auto-entities! Even grabbed the posters using the Tautulli rest end point. :slight_smile: I didn’t do a 1-1 recreation because I only wanted certain data points, but I expect that its possible to 100% recreate the interface layout like this.

HTH and let me know if you have any questions about this. This was a fun weekend project. :slight_smile:

EDIT 04-28-23: Noticed I was getting a ton of template errors in my log due to the sessions entity not having anything in its array. So I’ve corrected the templates and the auto-entities card above to check to see if it has a session for that array value active in the list.

EDIT 01-08-24: Updated button card YAML for breaking change with card_mod, thanks @derailius !

EDIT 07-04-24: Updated the template sensors to fix null errors in the home assistant log when a plex session ends or when it’s doing a countdown to “play next”.

12 Likes

you my sir are a legend! This is great! Thx very much!

You’re going to want to implement the edits above - noticed a template issue in the logs!

@stratotally

First I have to give you your flowers for all your hard work on this project. I looked for a card like this about a year ago and there wasn’t anything out there, now you’ve raised the bar, so THANK YOU!

I wanted to note that the bar-card is also needed for this; you only noted auto-entities and button-card.

I followed your instructions, but a few things on my card appear to be off and I was hoping you might be able to assist.

  1. There’s an outline box around the Playing bar that I can’t figure out how to get rid of.
  2. The User it’s displaying the session for doesn’t appear in the bottom right corner like your user does in your example.
  3. In the bottom right corner it’s showing Plex Session # for me where it shows the User for you.
  4. The Plex icon is appearing in the bottom right corner for some reason.

I appreciate any assistance you can provide on this.

Thanks in advance!

The issue appears to have been resolved and hasn’t been occurring. Thanks again for the amazing card and all your work.

Oh thats great to hear, just saw this. Usually when you see things floating in the bottom right of the button card like that, its that a variable has be declared, but it doesn’t exist in the CSS grid layout. Weird that it was popping up like that, but glad its resolved now.

I love this . i’d love to implement it but i’m notive. can you list step by step instructions on adding this?

PLEASE!!!

@stratotally I made an account to message you and unfortunately couldn’t figure out how to private message or contact you other than a reply to this thread, but I wanted to let you know your card example in this post has your tautulli URL and API key exposed in it. Seems like you forgot to redact it last time you updated the post. Wanted to let you know!

Also great write-up! It’s working beautifully for me!

1 Like

Shoutout to @stratotally, this is amazing. Also as noted above, your plex API key are still in the code. You might want to redact that.

For anyone else who runs into issues similar to @thajuice. I had the exact same problem! In order to use CSS elements in bar-card you need card-mod. After installing card-mod, reloading, and a hard refresh everything looked perfect!

Hope that helps anyone else who stumbles upon this. Thanks again!

1 Like

Ugh, thank you for the heads up!

Hi everybody,

i have the problem that the cover was not show

how can i solve it?

It may be the URL to the image? Are you on your local network? Maybe take a look at the link to the image and see if the API key is correct? If you copy/paste it into a browser, you should see the image.

hi i gave my tautulli a domain and then i get the covers, with the local IP not. But when i put the url with the ip in my browser i become the cover.

But with the domain for tautulli i solved the problem for me.
Thanks for your help.

1 Like

Unfortunately, this doesn’t work for me at all. Just get the error message: REST result could not be parsed as JSON

:frowning:

I fixed this to work with the new changes that were made in card-mod
Thank you for your excellent work @stratotally this is definitely one of my favorite additions to HA.

entity: this.entity_id
type: custom:button-card
variables:
  entity: this.entity_id
custom_fields:
  picture:
    card:
      type: picture
      image: |
        [[[
          if (states[variables.entity].attributes.grandparent_thumb != ''){
            return "http://[TAUTULLI SERVER:PORT]/api/v2?apikey=[YOUR API KEY HERE]&cmd=pms_image_proxy&img=" + states[variables.entity].attributes.grandparent_thumb + "&amp;width=300&amp;height=450&amp;fallback=poster&amp;refresh=true";
          } else {
            if (states[variables.entity].attributes.thumb != ''){
              return "http://[TAUTULLI SERVER:PORT]/api/v2?apikey=[YOUR API KEY HERE]&cmd=pms_image_proxy&img=" + states[variables.entity].attributes.thumb + "&amp;width=300&amp;height=450&amp;fallback=poster&amp;refresh=true"
            } else {
              return states['sensor.' + states[variables.entity].attributes.user + '_session_thumbnail'].state
            }
          }
        ]]]
      card_mod:
        style: |
          ha-card {
          box-shadow: 0;
          border-radius: 0;
          margin: 5px 0 0 -5px;
          }
          ha-card img {
          min-height: 100px;
          min-width: 100px;
          }
  bar:
    card:
      type: custom:bar-card
      entities:
        - entity: this.entity_id
      attribute: progress_percent
      unit_of_measurement: "%"
      positions:
        icon: "off"
        indicator: "off"
        name: inside
      height: 19px
      color: "#e49f29"
      name: |
        [[[
          return states[variables.entity].state
        ]]]
      card_mod:
        style: |
          ha-card {
          --ha-card-background: none;
          border: none;
          box-shadow: none;
          }
          ha-card #states {
            padding: 0;
          }
          bar-card-currentbar, bar-card-backgroundbar {
            border-radius: 5px;
            left: 0;
          }
          bar-card-name {
            margin-left: 3%;
            text-shadow: 1px 1px 1px #0003;
          }
          bar-card-value {
            margin-right: 3%;
            text-shadow: 1px 1px 1px #0003;
          }
  user: |
    [[[
      return "<b>" + states[variables.entity].attributes.user + "</b>"
    ]]]
  title: |
    [[[
      if (states[variables.entity].state == 'playing') {
        return "<ha-icon icon='mdi:play' style='width: 15px; height: 15px; position: relative; top: -2px;'></ha-icon> " + states[variables.entity].attributes.full_title;
      } else {
        if (states[variables.entity].state == 'paused') {
          return "<ha-icon icon='mdi:pause' style='width: 15px; height: 15px; position: relative; top: -2px;'></ha-icon> " + states[variables.entity].attributes.full_title;
        } else {
          return states[variables.entity].attributes.full_title;
        }
      }

    ]]]
  stream_label: <b>Stream</b>
  stream: |
    [[[
      return states[variables.entity].attributes.video_resolution + " > " + states[variables.entity].attributes.transcode_decision + " > " +states[variables.entity].attributes.stream_video_resolution + ""
    ]]]
  product_label: <b>Product</b>
  product: |
    [[[
      return states[variables.entity].attributes.product
    ]]]
  player_label: <b>Player</b>
  player: |
    [[[
      return states[variables.entity].attributes.player
    ]]]
  location_label: <b>Location</b>
  location: |
    [[[
      return states[variables.entity].attributes.location + ": " + states[variables.entity].attributes.ip_address
    ]]]
  media_detail: |
    [[[
      if(states[variables.entity].attributes.media_type == 'movie') {
        return "<ha-icon icon='mdi:filmstrip' style='width: 15px; height: 15px; position: relative; top: -2px;'></ha-icon> (" + states[variables.entity].attributes.year + ")";
      } else {
        return "<ha-icon icon='mdi:television-classic' style='width: 15px; height: 15px; position: relative; top: -2px;'></ha-icon> S" + states[variables.entity].attributes.parent_media_index + "•E" + states[variables.entity].attributes.media_index;
      }
    ]]]
  bandwidth_label: <b>Bandwidth</b>
  bandwidth: |
    [[[
      var bytes = states[variables.entity].attributes.bandwidth * 1000;
      var sizes = ['Bytes', 'Kbps', 'Mbps', 'Gbps', 'Tbps'];
      if (bytes == 0) return 'n/a';
      var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)));
      if (i == 0) return bytes + ' ' + sizes[i];
      return (bytes / Math.pow(1000, i)).toFixed(1) + ' ' + sizes[i];
    ]]]
card_mod:
  style: |
    ha-card {
    box-shadow: 0;
    padding: 0;
    margin: 0;
    border: 0;
    }
    ha-card #container {
    margin: 5px 0 0 0;
    }
    #name {
    display:none;
    }
styles:
  card:
    - height: 100x
    - padding: 0
  custom_fields:
    bar:
      - text-transform: capitalize
      - font-size: 13px
    user:
      - text-align: end
      - font-size: 15px
    title:
      - text-align: start
      - font-size: 13px
    stream:
      - text-transform: capitalize
      - text-align: start
      - font-size: 13px
    product:
      - text-transform: capitalize
      - text-align: start
      - font-size: 13px
    player:
      - text-transform: capitalize
      - text-align: start
      - font-size: 13px
    location:
      - text-transform: uppercase
      - text-align: start
      - font-size: 13px
    media_detail:
      - text-transform: uppercase
      - text-align: start
      - font-size: 13px
    bandwidth:
      - text-transform: capitalize
      - text-align: start
      - font-size: 13px
    product_label:
      - text-transform: uppercase
      - text-align: end
      - font-size: 10px
    player_label:
      - text-transform: uppercase
      - text-align: end
      - font-size: 10px
    stream_label:
      - text-transform: uppercase
      - text-align: end
      - font-size: 10px
    location_label:
      - text-transform: uppercase
      - text-align: end
      - font-size: 10px
    bandwidth_label:
      - text-transform: uppercase
      - text-align: end
      - font-size: 10px
  grid:
    - grid-template-areas: |
        "picture product_label product"
        "picture player_label player"
        "picture stream_label stream"
        "picture location_label location"
        "picture bandwidth_label bandwidth"
        "picture bar bar"
        "picture title title"
        "picture media_detail user"
    - grid-template-columns: 1fr 60px 3fr
    - grid-gap: 5px 10px

1 Like

@stratotally thanks for this!
I cant seem to figure out why, but all my sessions are stuck as “unavailable”

I was able to reproduce your issue when i purposely malformed the rest api sensor in my configuration.yaml

Here is mine.

sensor:
  - platform: rest
    unique_id: tautulli_activity
    name: Tautulli Activity
    icon: mdi:plex
    scan_interval: 5
    force_update: true
    resource: http://YOURIP/api/v2?apikey=YOURKEY&cmd=get_activity
    method: POST
    headers:
      Content-Type: application/json
    value_template: "{{ value_json.response.result }}"
    json_attributes_path: "$.response.data"
    json_attributes:
      - stream_count
      - sessions
      - stream_count_direct_play
      - stream_count_direct_stream
      - stream_count_transcode
      - total_bandwidth
      - lan_bandwidth
      - wan_bandwidth

Hope this helps.

2 Likes

@derailius Thanks! Yes I see my issue. I was missing the sensor: line.

Now Im trying to add the custom card and getting various errors. I see where you updated the YAML for the card-mod breaking change, but I guess you copy/pasted from a coupld lines in on the YAML? It broke the indentation and I cant seem to get it right

If I add in the first few lines and attempt to fix the indentation, I get “No card type specified.” but if I dont add in the lines I get:

ButtonCardJSTemplateError: TypeError: Cannot read properties of undefined (reading 'attributes') in 'if (states[variables.entity].attributes.grandparent_thumb != ''){ return "

Have you tried the Tautulli end point directly in a browser? It may be a cert issue or a key issue. When you view it in a browser it should return JSON on the page.

1 Like

I think I know what your problem is. The code they posted was for the button card updates, but the button card is wrapped in an auto_entities card. I’ve updated the example in the top post - can you try that?

2 Likes