I was looking for a way to display Plex session data in HA. I came across this card, which looked great!
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. 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.
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â.