Real-Time Sports Scores w/ TeamTracker and TeamTracker-Card (Beta)

The state will be PRE, IN, POST, or NOT_FOUND.

Hi guys, is it possible to add some kind of identifier on the card for the corresponding team when there’s a power play event? I saw in the API http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/summary?event=GAMEID that the event is registered as:

    {
      "id": "401802564000006850",
      "sequenceNumber": "165",
      "type": {
        "id": "509",
        "text": "Penalty",
        "abbreviation": "penalty"
      },
      "text": "Brandon Carlo Tripping against Barrett Hayton",
      "awayScore": 1,
      "homeScore": 2,
      "period": {
        "number": 2,
        "displayValue": "2nd"
      },
      "clock": {
        "displayValue": "13:05"
      },
      "scoringPlay": false,
      "scoreValue": 0,
      "modified": "2025-11-06T02:34Z",
      "team": {
        "id": "21"
      },
      "participants": [
        {
          "athlete": {
            "id": "3904175",
            "displayName": "Brandon Carlo",
            "shortName": "B. Carlo",
            "headshot": {
              "href": "https://a.espncdn.com/i/headshots/nhl/players/full/3904175.png",
              "alt": "Brandon Carlo"
            }
          },
          "type": "penalized",
          "ytdGoals": 0
        },
        {
          "athlete": {
            "id": "4352699",
            "displayName": "Barrett Hayton",
            "shortName": "B. Hayton",
            "headshot": {
              "href": "https://a.espncdn.com/i/headshots/nhl/players/full/4352699.png",
              "alt": "Barrett Hayton"
            }
          },
          "ytdAssists": 2
        }
      ],
      "wallclock": "2025-11-06T01:30:23Z",
      "shootingPlay": true,
      "coordinate": {
        "x": -76,
        "y": 14
      },
      "strength": {
        "id": "701",
        "text": "Even Strength",
        "abbreviation": "even-strength"
      }
    },

As of last season, the scoreboard API didn’t have a PP indicator. I will check again and also look at the API you listed. If it’s not in the scoreboard API, it won’t be quick.

@timblazing recently shared a Custom Mushroom Template Badge that I wanted to pass along.

Anyone looking for a compact display should check it out.

type: custom:mushroom-template-badge
entity: sensor.pittsburgh_steelers
icon: mdi:football
content: >
  {% set raw_date = state_attr(entity, 'date') %}
  {% set team_score = (state_attr(entity, 'team_score') | int(0)) %}
  {% set opp_score = (state_attr(entity, 'opponent_score') | int(0)) %}
  {% set winner = state_attr(entity, 'team_winner') %}
  {% set clock = state_attr(entity, 'clock') %}
  {% set quarter = state_attr(entity, 'quarter') %}

  {# ----- POST ----- #}
  {% if clock == 'Final' %}
    {% if winner %}
      W {{ team_score }}-{{ opp_score }}
    {% elif winner == false %}
      L {{ team_score }}-{{ opp_score }}
    {% else %}
      {{ team_score }}-{{ opp_score }}
    {% endif %}

  {# ----- IN ----- #}
  {% elif team_score > 0 or opp_score > 0 %}
    {% if clock %}
      {% if 'half' in clock | lower %}
        {{ team_score }}-{{ opp_score }} | HALFTIME
      {% else %}
        {{ team_score }}-{{ opp_score }} | {{ clock }}
      {% endif %}
    {% else %}
      {% if quarter %}
        {% set q = quarter | int %}
        {% if q == 1 %}
          {{ team_score }}-{{ opp_score }} | 1st
        {% elif q == 2 %}
          {{ team_score }}-{{ opp_score }} | 2nd
        {% elif q == 3 %}
          {{ team_score }}-{{ opp_score }} | 3rd
        {% elif q == 4 %}
          {{ team_score }}-{{ opp_score }} | 4th
        {% else %}
          {{ team_score }}-{{ opp_score }} | OT
        {% endif %}
      {% else %}
        {{ team_score }}-{{ opp_score }}
      {% endif %}
    {% endif %}

  {# ----- PRE ----- #}
  {% elif raw_date %}
    {% set dt = as_datetime(raw_date) %}
    {% set local = as_local(dt) %}
    {% set today = as_local(now()).date() %}
    {% if local.date() == today %}
      Today at {{ local.strftime('%-I:%M %p') }}
    {% else %}
      {{ local.strftime('%A at %-I:%M %p') }}
    {% endif %}

  {# ----- BYE / NOT_FOUND ----- #}
  {% else %}
    BYE
  {% endif %}
label: >
  {%- set event = state_attr(entity, 'event_name') or '' -%}
  {%- set raw_date = state_attr(entity, 'date') -%}
  {%- if raw_date -%}
    {{ event }}
  {%- else -%}
    {{ '' }}
  {%- endif -%}
color: |
  {% set winner = state_attr(entity, 'team_winner') %}
  {% if winner == true %}
    green
  {% elif winner == false %}
    red
  {% else %}
    grey
  {% endif %}
picture: |
  {{ state_attr(entity, 'team_logo') }}

I added possession using an asterisk before the home team or after the away team.

{{ '<p style="text-align: start"; "font-weight: bold"><tt>' }}
{%- set ns = namespace(holdleague='') -%}
{%- set teams = integration_entities("teamtracker") | expand | selectattr('state', 'eq', 'IN') | selectattr('attributes.team_homeaway', 'eq', 'away') | sort(attribute='attributes.league,attributes.date,attributes.team_abbr') | map(attribute='entity_id') | list -%}
{%- for team in teams -%}
  {%- if state_attr(team, 'league') != ns.holdleague -%}
    {{ '' if loop.first else '<br>' }}
    {%- set ns.holdleague = state_attr(team, 'league') %}
    {{ state_attr(team, 'league')~'<br>' }}
  {%- endif %}
  {%- set team_possession = '*' if state_attr(team, 'possession') == state_attr(team, 'team_id') else '&nbsp;' -%}
  {%- set opponent_possession = '*' if state_attr(team, 'possession') == state_attr(team, 'opponent_id') else '&nbsp;' -%}
  {%- set teamscore = ('&nbsp;' if state_attr(team, 'team_score') | length < 2 else '')~state_attr(team, 'team_score') %}
  {%- set opponentscore = state_attr(team, 'opponent_score')~('&nbsp;' if state_attr(team, 'opponent_score') | length < 2 else '') %}
  {%- set teamabbr = state_attr(team, 'team_abbr')~('&nbsp;' if state_attr(team, 'team_abbr') | length < 3 else '') %}
  {%- set opponentabbr = state_attr(team, 'opponent_abbr')~('&nbsp;' if state_attr(team, 'opponent_abbr') | length < 3 else '') %}
  {{ '&nbsp;'~team_possession~teamabbr~' @ '~opponentabbr~opponent_possession~' '~' '~teamscore~'-'~opponentscore~'&nbsp;'~state_attr(team, 'clock') | replace('Rain Delay', 'RD') | replace('Delayed', 'Del')~'<br>' }}
{%- endfor %}
{{ '</tt></p>' }}

Admittedly, since it’s a lot of copy/paste, I used AI to clean up this YAML code. However, I can’t seem to get it to only show the home game instead of showing home and away team, duplicating the card. What am I missing, any suggestions? Code and screenshot below.

type: horizontal-stack
cards:
  - type: vertical-stack
    cards:
      - type: markdown
        content: |
          ## šŸ’ Atlantic Division šŸ„…šŸšØ
      - type: custom:teamtracker-card
        entity: sensor.bos_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.buf_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.det_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.fla_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.mtl_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.ott_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.tbl_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.tor_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
  - type: vertical-stack
    cards:
      - type: markdown
        content: |
          ## šŸ’ Metropolitan Division šŸ„…šŸšØ
      - type: custom:teamtracker-card
        entity: sensor.car_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.cbj_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.njd_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.nyi_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.nyr_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.phi_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.pit_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.wsh_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
  - type: vertical-stack
    cards:
      - type: markdown
        content: |
          ## šŸ’ Central Division šŸ„…šŸšØ
      - type: custom:teamtracker-card
        entity: sensor.ari_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.chi_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.col_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.dal_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.min_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.nsh_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.stl_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.wpg_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
  - type: vertical-stack
    cards:
      - type: markdown
        content: |
          ## šŸ’ Pacific Division šŸ„…šŸšØ
      - type: custom:teamtracker-card
        entity: sensor.ana_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.cgy_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.edm_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.lak_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.sjs_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.sea_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.van_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
      - type: custom:teamtracker-card
        entity: sensor.vgk_team_tracker
        can_interval: 1
        filter:
          - attribute: team_homeaway
            value: home
grid_options:
  columns: 48
  rows: auto

I would use the auto-entitites card.

If you rename your sensors using a naming convention similar to the following you should be able to filter and group them the way you want.

sensor.tt_nhl_metro_car
sensor.tt_nhl_metro_cbj
etc.

This should get you started.

type: custom:auto-entities
unique: true
show_empty: false
card:
  type: custom:layout-card
  layout_type: masonry
card_param: cards
filter:
  template: |
    {%- for team in integration_entities("teamtracker") -%}
       {%- if "sensor.tt_nhl_metro" in team -%}
            {%- if not is_state(team, "NOT_FOUND")
                  and state_attr(team, "team_homeaway") == "home" -%}
                {{{"type": "custom:teamtracker-card",
                   "show_league": true,
                   "entity": team }}},
            {%- endif -%}
        {%- endif -%}
    {%- endfor -%}
sort:
  method: attribute
  attribute: date

Hello, and thanks for this beautiful integration. One question, how to know who is on serve in Tennis Sports? It’s important for me to know and to show in the card.

Thanks!

I just checked and the API doesn’t return this info. Sorry.

Is there a way to calculate it based on the score and who is considered Home and Away?

@vasqued2 as always fantastic work on the tt and card.

I don’t know if this is a feature request or if something that doesn’t make sense to do and we should just wait for the ESPN Olympic scoreboard API to update with 2026 data.

My question is does a feature request make sense to add dates? Default if no date range is there to just use scoreboard but if it does exist append. Specifically something like this:

## Group C
- platform: teamtracker
  league_id: XXX
  sport_path: hockey
  league_path: olympics-mens-ice-hockey
  team_id: USA
  name: United States
  date_range: 20260201-20260222

Background

I know that I can get to mens & womens Olympic hockey data through these links: [https://site.api.espn.com/apis/site/v2/sports/hockey/olympics-mens-ice-hockey/scoreboard]

[https://site.api.espn.com/apis/site/v2/sports/hockey/olympics-womens-ice-hockey/scoreboard]

but they show 2022 results

I also know that I can get the upcoming 2026 games if I append the dates to the scoreboard link [https://site.api.espn.com/apis/site/v2/sports/hockey/olympics-mens-ice-hockey/scoreboard?dates=20260201-20260222] and the status within the data returned is correct with pre -

          "status": {
            "clock": 0,
            "displayClock": "0:00",
            "period": 0,
            "type": {
              "id": "1",
              "name": "STATUS_SCHEDULED",
              "state": "pre",
              "completed": false,
              "description": "Scheduled",
              "detail": "Wed, February 11th at 10:40 AM EST",
              "shortDetail": "2/11 - 10:40 AM EST"
            }

and if I use my python script I can get it to display in HA just not using your TT card.

but if I try to show USA I get nothing because TT is going to the scoreboard link and it doesn’t contain that information

I also assume that if I wanted to display previous years I could then just append previous year dates so for example if I wanted to see results from 2022 or 2018 etc. I could just create a new tt sensor from the 2022 games as well: ?dates=20220209-20220220 which then tt would use the whole link: https://site.api.espn.com/apis/site/v2/sports/hockey/olympics-mens-ice-hockey/scoreboard?dates=20220209-20220220

Not a burning need, but thought I would ask.

If would also be interesting if we could use that for other sports like NFL.
https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?dates=20250904-20260208

Can anyone help me with the Data for Millwall in the English championship I’ve tried MIL and the league as. eng.2 and the sport as soccer ??

Basically, choose Custom on the first screen and enter mil for the team, then on the second screen, sport path will be soccer and league path is eng.2

Hi

Yes that’s what I did and it says API error

It’s working for me. What is your api_url and api_message.

Hi

It’s {ā€œcodeā€:400,ā€œmessageā€:ā€œFailed to get events endpoint.ā€}

try lower case and congrats on the win

This is an interesting idea and I like the thought. I do some date logic to look about a week into the future but generally assumed the API defaulted to the future dates once the future dates were added to the schedule. That’s clearly not the case.

There’s a couple things I need to think through.

First and most important, I honor ESPN’s limit on the number of competitions returned by the API. I do this for a couple reasons. First, while a competition is in progress, the API gets called every 5 seconds. I don’t want to call the API that frequently and get the results of tons of competitions and run the risk of getting ESPN’s attention and triggering some sort of block, either at a client level or the sensor itself attracting attention. Secondly, there is also a limit to the amount of data HA can practically parse before the next call 5 seconds later. As the number of competitions returned increase, this would become more of a concern.

Since the number of competitions in a date range would likely exceed ESPN’s limit. The practical reality is this would work for the first couple competitions in the range, and then the later ones would get truncated off the API response and you wouldn’t get the most recent results.

I could probably get around this by using either the start of the date range or today’s date, whichever is farther in the future. This wouldn’t allow the back-dating for past events like you talked about though.

I’m wondering if I could do something where if the event returned by default is more than like a month old, I look much farther into the future than I do today. The API limit would prevent a ton of responses and using a larger future date range would pick up the future events that I didn’t realize were populated and available for retrieval.

I will play around with it. I think the biggest problem is the future competitions that aren’t pulled back by default. Let me know your thoughts and suggestions.

What is the api_url? Either you have a bad URL, which could be because you have a typo or are using caps instead of lower case, or the URL is being blocked by your firewall or something else.

Cheers mate, I could have sworn I did it all in Lowercase but it was on my phone so maybe it defaulted to capital at the start. It’s working now cheers

Glad to hear it!!! See you in 2 weeks when we beat y’all!!! :wink: