Tautulli REST End Point and custom-button-card

Well sorry for sounding stupid but how to I add REST? I thought it was jus to add the - platform: rest… And it should be active?

I have added the platform: rest, and template in to configuration.yaml, also added a Lovelace windows (that don’t show anything ATM).

I tested my tautulli url and I get a reply back so that’s working at least.

Here is the error when I restart HA.
Don’t mind the warnings, they are for Aarlo plugin.

Logger: homeassistant.helpers.template
Source: helpers/template.py:2558
First occurred: 19:17:17 (132 occurrences)
Last logged: 19:17:17

Template variable error: None has no element 1 when rendering '{% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].relayed }}{%endif%}'
Template variable error: None has no element 1 when rendering '{% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].bandwidth }}{%endif%}'
Template variable error: None has no element 1 when rendering '{% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].video_resolution }}{%endif%}'
Template variable error: None has no element 1 when rendering '{% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].stream_video_resolution }}{%endif%}'
Template variable error: None has no element 1 when rendering '{% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].transcode_decision }}{%endif%}'
Logger: homeassistant.components.template.template_entity
Source: components/template/template_entity.py:201
integration: Template (documentation, issues)
First occurred: 19:17:17 (46 occurrences)
Last logged: 19:17:17

TemplateError('UndefinedError: None has no element 1') while processing template 'Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].relayed }}{%endif%}) renders=4>' for attribute 'relayed' in entity 'sensor.plex_session_2_tautulli'
TemplateError('UndefinedError: None has no element 1') while processing template 'Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].bandwidth }}{%endif%}) renders=4>' for attribute 'bandwidth' in entity 'sensor.plex_session_2_tautulli'
TemplateError('UndefinedError: None has no element 1') while processing template 'Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].video_resolution }}{%endif%}) renders=4>' for attribute 'video_resolution' in entity 'sensor.plex_session_2_tautulli'
TemplateError('UndefinedError: None has no element 1') while processing template 'Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].stream_video_resolution }}{%endif%}) renders=4>' for attribute 'stream_video_resolution' in entity 'sensor.plex_session_2_tautulli'
TemplateError('UndefinedError: None has no element 1') while processing template 'Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].transcode_decision }}{%endif%}) renders=4>' for attribute 'transcode_decision' in entity 'sensor.plex_session_2_tautulli'
Logger: homeassistant.helpers.event
Source: helpers/template.py:588
First occurred: 19:17:17 (46 occurrences)
Last logged: 19:17:17

Error while processing template: Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].relayed }}{%endif%}) renders=2>
Error while processing template: Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].bandwidth }}{%endif%}) renders=2>
Error while processing template: Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].video_resolution }}{%endif%}) renders=2>
Error while processing template: Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].stream_video_resolution }}{%endif%}) renders=2>
Error while processing template: Template<template=({% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].transcode_decision }}{%endif%}) renders=2>
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 586, in async_render
    render_result = _render_with_context(self.template, compiled, **kwargs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 2545, in _render_with_context
    return template.render(**kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/jinja2/environment.py", line 1301, in render
    self.environment.handle_exception()
  File "/usr/local/lib/python3.12/site-packages/jinja2/environment.py", line 936, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 1, in top-level template code
TypeError: object of type 'NoneType' has no len()

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 712, in async_render_to_info
    render_info._result = self.async_render(
                          ^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/template.py", line 588, in async_render
    raise TemplateError(err) from err
homeassistant.exceptions.TemplateError: TypeError: object of type 'NoneType' has no len()

Logger: homeassistant.config
Source: config.py:1324
First occurred: 19:17:07 (1 occurrences)
Last logged: 19:17:07

Invalid config for 'sensor' from integration 'rest' at configuration.yaml, line 77: template value should be a string for dictionary value 'headers->json_attributes', got ['stream_count', 'sessions', 'stream_count_direct_play', 'stream_count_direct_stream', 'stream_count_transcode', 'total_bandwidth', 'lan_bandwidth', 'wan_bandwidth'], please check the docs at https://www.home-assistant.io/integrations/rest

And my configuration.yaml file:

# Loads default set of integrations. Do not remove.
default_config:

# Load frontend themes from the themes folder
frontend:
  themes: !include_dir_merge_named themes/

automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml

aarlo:
  tfa_source: imap
  tfa_type: email
  tfa_host: imap.gmail.com
  tfa_username: !secret gmail_username
  tfa_password: !secret gmail_app_password
  username: !secret arlo_username
  password: !secret arlo_password
  backend: sse

camera:
  - platform: aarlo

binary_sensor:
  - platform: aarlo
    monitored_conditions:
      - motion
      - sound
      - ding
      - cry
      - connectivity

sensor:
  - platform: aarlo
    monitored_conditions:
      - total_cameras
      - last_capture
      - recent_activity
      - captured_today
      - battery_level
      - signal_strength
      - temperature
      - humidity
      - air_quality

  - platform: rest
    unique_id: tautulli_activity
    name: Tautulli Activity
    icon: mdi:plex
    scan_interval: 5
    force_update: true
    resource: !secret tautulli_url_api
    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

template:         
  - sensor:
      - unique_id: plex_session_1
        name: Plex Session 1 (Tautulli)
        icon: mdi:plex
        state: >
          {% if (state_attr('sensor.tautulli_activity','sessions')|length >= 1)%}{{ state_attr('sensor.tautulli_activity','sessions')[0].state }}{%else%}off{%endif %}
        attributes:
          user: > 
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].user }}{%endif%}
          progress_percent: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].progress_percent }}{%endif%}
          media_type: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].media_type }}{%endif%}
          full_title: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].full_title }}{%endif%}
          grandparent_thumb: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].grandparent_thumb }}{%endif%}
          thumb: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].thumb }}{%endif%}
          parent_media_index: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].parent_media_index }}{%endif%}
          media_index: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].media_index }}{%endif%}
          year: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].year }}{%endif%}
          product: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].product }}{%endif%}
          player: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].player }}{%endif%}
          device: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].device }}{%endif%}
          platform: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].platform }}{%endif%}
          location: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].location }}{%endif%}
          ip_address: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].ip_address }}{%endif%}
          ip_address_public: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].ip_address_public }}{%endif%}
          local: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].local }}{%endif%}
          relayed: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].relayed }}{%endif%}
          bandwidth: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].bandwidth }}{%endif%}
          video_resolution: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].video_resolution }}{%endif%}
          stream_video_resolution: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].stream_video_resolution }}{%endif%}
          transcode_decision: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[0].transcode_decision }}{%endif%}
  - sensor:
      - unique_id: plex_session_2
        name: Plex Session 2 (Tautulli)
        icon: mdi:plex
        state: >
          {% if (state_attr('sensor.tautulli_activity','sessions')|length >= 2)%}{{state_attr('sensor.tautulli_activity','sessions')[1].state }}{%else%}off{%endif %}
        attributes:
          user: > 
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].user }}{%endif%}
          progress_percent: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].progress_percent }}{%endif%}
          media_type: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].media_type }}{%endif%}
          full_title: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].full_title }}{%endif%}
          grandparent_thumb: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].grandparent_thumb }}{%endif%}
          thumb: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].thumb }}{%endif%}
          parent_media_index: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].parent_media_index }}{%endif%}
          media_index: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].media_index }}{%endif%}
          year: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].year }}{%endif%}
          product: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].product }}{%endif%}
          player: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].player }}{%endif%}
          device: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].device }}{%endif%}
          platform: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].platform }}{%endif%}
          location: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].location }}{%endif%}
          ip_address: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].ip_address }}{%endif%}
          ip_address_public: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].ip_address_public }}{%endif%}
          local: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].local }}{%endif%}
          relayed: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].relayed }}{%endif%}
          bandwidth: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].bandwidth }}{%endif%}
          video_resolution: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].video_resolution }}{%endif%}
          stream_video_resolution: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].stream_video_resolution }}{%endif%}
          transcode_decision: >
            {% if this.state !='off'%}{{ state_attr('sensor.tautulli_activity','sessions')[1].transcode_decision }}{%endif%}
 
#- sensor:
#      - unique_id: plex_session_3
# ...etc...

This has an incorrect indentation. Try changing it to:

    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

Well… After that fix I get no errors… To late at night for me to test but will test tomorrow! Thanks for helping me out so far! I will let you know how it goes.

Wow, I can’t believe that “little” correction was all that was needed for it to work. Thanks you so much!

One question tho, I have another card mod that have little more rounded corners, how is the best possible way to move the poster little to the right (the left side is clipped by the bubble), or If I move the picture, dose the text adjust itself to the middle?

Love the cards! I made an slimmed version if anyone’s interested. I’m not really experienced with code so I just slashed until it looked good, don’t judge.

Tautulli custom card slim

I’d like to add a feature which changes the bar color depending on if the user is playing or paused. Anyone that can help me out?

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.transcode_decision
    ]]]
  product_label: <b>Client</b>
  product: |
    [[[
      return states[variables.entity].attributes.product
    ]]]
  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;
      }
    ]]]
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: 140px
    - padding: 0
  custom_fields:
    bar:
      - text-transform: capitalize
      - font-size: 13px
    user:
      - text-align: end
      - font-size: 18px
    title:
      - text-align: start
      - font-size: 25px
    stream:
      - text-transform: capitalize
      - text-align: start
      - font-size: 15px
    product:
      - text-transform: capitalize
      - text-align: start
      - font-size: 15px
    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 title title"
        "picture product_label product"
        "picture stream_label stream"
        "picture bar bar"
        "picture media_detail user"
    - grid-template-columns: 1fr 60px 3fr
    - grid-gap: 5px 10px

Hi buddy. I did the same

  color: |
    [[[
      if (states[variables.entity].state == 'playing') {
        return '#2986cc';
      } else if (states[variables.entity].state == 'paused') {
        return '#e49f29';
      } else {
        return '#000000'; // Default color if neither playing nor paused
      }
    ]]]

Sorry to bother ou all again but suddenly my picture one show, any idea what could be the issue? I tried adding same code again, adding the API with out luck, any idea what I can do to troubleshoot this?

External the pic won’t load. Internal it does

I have my local ip in the config, so no external ip. It worked before but now I noticed that I see an error on the photo.

Oooh so, I can’t use HA cloud service because then it will always show as broken image…?! When I’m on local it works, I thought it was just the local IP in the config/setup.

So there was no work around to this to work on HA cloud? I meant, technically I’m connected to my computer via HA, just not on the local network… :thinking:

Right - so if its using an internal IP address and you’re internal to the network, you should be able to see the image.
If it’s using an internal Tautulli IP address, and you leave the house / network, then it wont be able to pull and serve the image to you.

Daaamn just noticed it when I tried on my computer via local URL instead… But is there a way to get it to work? Save in tmp or likewise? Would be nice for it to always work.

You could use Tailscale to connect to your local computers using the tailscale IP address. Any device on the tailscale network would then use the tailscale IP address to serve up the image.
You could also do what I do and host Tautulli externally. I do this using a reverse proxy domain name and letsencrypt SSL certificates.
Tailscale is probably the quickest, easiest and most secure to set up though.

I do have wireguard installed and if I have that up with web app for HA it works, but I use duckdns for DNS and think it is kind of unstable from time to time. I tried tailscale (on my AdGuard server within the network) now but didn’t work when I tried it. But I assume I might need to install it on my tautulli server also, but… Sad it don’t work via home assistant out if the Box (when HA talks with Tautulli via network).

this is my modified version I’ve been working on over the last few days

Great work. This is awesome!!! Any reason why its showing just 2 streams?

The code above only has examples for two streams. You need to copy past and modify as stated above to add more streams. I’ve done 15 so hopefully never need to alter it again

Yup, I copy/pasted up to 10 myself.

Do you get errors in your logs ? I currently do and not sure how to fix. Everything’s working great though.

Has blinking orange when paused too.

Thank You!!! Dunno how I missed that