Sports Standings and Scores

I have been creating a Sports Standings Dashboard that utilizes the ESPN REST interfaces as well and several sensors to deliver a consolidated dashboard for Home Assistant. It is a mix of REST sensors and templates sensors based on thse REST sensors to display standings for NHL, MLB, NFL and NBA sports. Of course you could change and extend to sports that you target if you like.

It has grown to the point where others should take a look and give feedback.

The full solution is posted here:

It has both Standings as well as PRE, IN and POST game statistics through the excellent Teamtracker integration and card.

From the Readme:

Sport Standings and Scores

This project has Sports Standings and Scores Sensors and Dashboard for Home Assistant. It contains NHL, NFL, MLB and NBA standings as well as pre-game, in-game and post-game statistics. It makes use of the ESPN APIs as well as the great Teamtracker Home Assistant App and Card.

Final Result

I always like to give you a glance at what you are building first. This helps you to understand the components and what goes into the solution. What we are building is shown here in the following images:

NHL Standings by Division

img/nhl_standings_division.png

MLB Live Games

img/mlb_live.png

There are several components in this interface. Across the top it uses tabs to allow selection of what you want to see:

  • Select the Sport (NHL, MLB, NFL or NBA)
  • Standings has Divisonal, Conference and Overall standings for each sport
  • Note: the current view also has Playoff/Wildcard placeholder yet to be done
  • Each sport has post-game, in-game and pre-game display

Prerequisites

As for the GUI, there are several custom cards used. These include:

You also would need the Teamtracker integration for the game-based statistics. If you want the solution to work as is, you will need to be sure you have these installed and working in your Lovelace configuration.

Sensors

There are several sensors. The main sensors can be broken down into two types. The first is a single sensor for every sport that uses REST. These are in the GITHUB sensor.yaml file. One example is:

##
## NFL Standings
##
- platform: rest
  scan_interval: 36000
  name: NFL Standings
  unique_id: sensor.nfl_standings
  resource: https://site.web.api.espn.com/apis/v2/sports/football/nfl/standings?seasontype=2&type=0&level=3
  value_template: "{{ now() }}"
  json_attributes:
      - children

In these REST sensors, it gets the data from the ESPN API using:

  • seasontype = 2 (regular season)
  • type = 0 (full stats)
  • level = 3 (full/conference/division)

The other sensors in sensor.yaml are all teamtracker sensors for every team in all the sports. A short (snipped) example is like this:

##
##  NFL Teams
##
- platform: teamtracker
  league_id: NFL
  team_id: DET
  name: Detroit Lions
- platform: teamtracker
  league_id: NFL
  team_id: GB
  name: Green Bay
- platform: teamtracker
  league_id: NFL
  team_id: CHI
  name: Chicago Bears
- platform: teamtracker
  league_id: NFL
  team_id: MIN
  name: Minnesota Vikings
- platform: teamtracker
  league_id: NFL
  team_id: BUF
  name: Buffalo Bills
- platform: teamtracker
  league_id: NFL
  team_id: MIA
  name: Miami Dolphins

The sensor.yaml attached has teamtracker sensors to every team in all those sports.

As a side note, I made these but I not type in every team. I use a tool to go JSON to XML using the JSON output from the standings file. The repository includes an XSL that can build the set based on the sensor for a sport. And yes, I used XML and XSL … because that is what I am familar with and this is a one off.

In order to get standings for all the divisions, I implemented template sensors for all of them. These are included in template.yaml in GITHUB. A short sample is like this for the NHL:

###
### NHL Divisions
###
  - name: NHL East Atlantic
    unique_id: sensor.nhl_east_atlantic
    state: "{{ now() }}"
    attributes:
      entries: "{{ state_attr('sensor.nhl_standings','children')[0]['children'][0]['standings']['entries'] }}"
  - name: NHL East Metropolitan
    unique_id: sensor.nhl_east_metropolitan
    state: "{{ now() }}"
    attributes:
      entries: "{{ state_attr('sensor.nhl_standings','children')[0]['children'][1]['standings']['entries'] }}"
  - name: NHL West Central
    unique_id: sensor.nhl_west_central
    state: "{{ now() }}"
    attributes:
      entries: "{{ state_attr('sensor.nhl_standings','children')[1]['children'][0]['standings']['entries'] }}"
  - name: NHL West Pacific
    unique_id: sensor.nhl_west_pacific
    state: "{{ now() }}"
    attributes:
      entries: "{{ state_attr('sensor.nhl_standings','children')[1]['children'][1]['standings']['entries'] }}"

The Dashboard

The complete dashboard is contained in dashboard.yaml. You can examine that and make changes you may want. It makes extensive use of decluttering to templatize things and make it easier and much shorter to write. I think I could go deeper here, but for now it has one template per sport (as the columns are different) and one for game stats. It uses flex-table to show standings. There is a help_template in a .txt file that can help you identify the fields that contain the columns you wish to show in standings.

There are a few nice things done with card-mod to implement nicer tabs, colorize active tabs and provide scrolling tables horizontally on smaller displays. Although I will say that I designed this for wall pads in the house with large screens, I would likely change things if I ever implemented it to target phone devices.

A lot of background information can be found here:

Update 3/13/2023:

Since the NHL started to return “CLINCH” indicators for some teams there are some code changes to handle this. I eliminated SEED and put in CLINCH indicator and changed to use abbreviation lookup instead of positional lookup because the positions would be different for teams as they clinch or are eliminated from playoff contention. The new code is posted in “dashboard.yaml”

10 Likes

Hello,
with the last code for dashboard.yaml I still only get the headlines under Devisional and Conference :
21e7bf0af4ca660fc5d36f25b4e7e124acf9f748_2_690x251

And under Overall ist a complete emty page.

It is not clear why this is happening in your installation assuming you have all the prerequisites installed and functioning. I personally would start to debug this way.

Your previous posts look as if the data is fine in the sensors so this below would only be what you need for a single card.

  1. Create some testing area in your installation.
  2. Create a simple one division card using the following YAML. Do this by creating a card as “Manual” and pasting the following into it:

      type: custom:flex-table-card
      title: 'Test'
      css:
        table+: 'padding: 0px; width: 1600px;'
        tbody tr td:first-child: 'width: 2%;'
        tbody tr td:nth-child(2): 'width: 20%;'
        tbody tr td:nth-child(n+3): 'width: 5%;'
        tbody tr:hover: 'background-color: green!important; color:white!important;'
        tbody tr td:nth-child(7): 'background-color: green; color: white;'
      card_mod:
        style: |
          ha-card {
              overflow: auto;
          }
          $: |
              .card-header {
                 padding: 12px 0px 8px 4px!important;
                 font-size: 16px!important;
                 line-height: 18px!important;
                 font-weight: bold!important;
               }
      entities:
        include: 
          - sensor.nhl_east_atlantic
      columns:
        - name: C
          data: entries
          modify: if(typeof x.stats.find(y=>y.abbreviation == 'CLINCH') !== 'undefined' ){x.stats.find(y=>y.abbreviation == 'CLINCH').displayValue}else{'-'}
        - name: Team
          data: entries
          modify: x.team.displayName
        - name: GP
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'GP').displayValue
        - name: W
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'W').displayValue
        - name: L
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'L').displayValue
        - name: OTL
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'OTL').displayValue
        - name: PTS
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'PTS').displayValue
        - name: RW
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'RW').displayValue
        - name: ROW
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'ROW').displayValue
        - name: SOW
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'SOW').displayValue
        - name: SOL
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'SOL').displayValue
        - name: HOME
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'HOME').displayValue
        - name: AWAY
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'AWAY').displayValue
        - name: GF
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'GF').displayValue
        - name: GA
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'GA').displayValue
        - name: DIFF
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'DIFF').displayValue
        - name: L10
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'L10').summary
        - name: STRK
          data: entries
          modify: x.stats.find(y=>y.abbreviation == 'STRK').displayValue

I get this:

This will eliminate anything outside of flex-table and card-mod as a test

with this code I get this error:

Konfigurationsfehler erkannt:
bad indentation of a mapping entry (2:12)

 1 | type: custom:flex-table-card
 2 |       title: 'Test'
----------------^
 3 |       css:
 4 |         table+: 'padding: 0px; width: 1600px;'

You copied it verbatim but did not indent as I have, notice in mine that type: is not indented with title or the remaining. I will edit the above with indents but you need to understand YAML and indentation rules.

I have copy your code with the copy button in the right corner.

But now I have copy your code with the mouse.

Now I don´t get an error. But only the headlines:

OK, good start. This means your sensors are wrong or something. Please go to Developer Tools → Templates and enter this:

{{ state_attr('sensor.nhl_east_atlantic','entries')[0].stats }}

You should see something like what I have on the right:

with this code in the developer tool I got this error:

UndefinedError: None has no element 0

OK, and this?

{{ state_attr('sensor.nhl_east_atlantic','entries') }}

If this is not existent then you are creating the sensors incorrectly. I would guess you are not using a broken up YAML config like I do.

I got this:

Ergebnistyp: dict
null
Dieses Template ßberwacht die folgenden Ereignisse, die einen Zustand ändern:

Entität: sensor.nhl_east_atlantic

My German is rough but that I thin means you have (1) either no sensor named “sensor.nhl_east_atlantic” or it has no attribute “entries”.

If you just copied my sensors, how did you put them into Home Assistant?
Do you use includes?

My code assumes you have a broken up YAML configuration in which you would do this in configuration.yaml:

sensor: !include sensor.yaml
template: !include template.yaml

Then you would have separate files named “sensor.yaml” and “template.yaml” where you put in my code for these files. Is this what you have or?

Best to show the relevant parts of how you actually put in the sensors.

sorry for the german part in my home assistant.

I have an sensor with this name:

and it have entries:

But I dont have template: !include template.yaml in my configuration.

I have this part direktly in the configuration:

template:

###
### NHL Divisions
###
  - binary_sensor:
    - name: NHL East Atlantic
      unique_id: sensor.nhl_east_atlantic
      state: "{{ now() }}"
      attributes:
        entries: "{{ state_attr('sensor.nhl_standings','children')[0]['children'][0]['standings']['entries'] }}"
    - name: NHL East Metropolitan
      unique_id: sensor.nhl_east_metropolitan
      state: "{{ now() }}"
      attributes:
        entries: "{{ state_attr('sensor.nhl_standings','children')[0]['children'][1]['standings']['entries'] }}"
    - name: NHL West Central
      unique_id: sensor.nhl_west_central
      state: "{{ now() }}"
      attributes:
        entries: "{{ state_attr('sensor.nhl_standings','children')[1]['children'][0]['standings']['entries'] }}"
    - name: NHL West Pacific
      unique_id: sensor.nhl_west_pacific
      state: "{{ now() }}"
      attributes:
        entries: "{{ state_attr('sensor.nhl_standings','children')[1]['children'][1]['standings']['entries'] }}"

I have it directly in the configuration.yaml because I have an other part in it.

Well. They are not binary sensors. That is your issue, your code should be:


template:
###
### NHL Divisions
###
  sensor:
    - name: NHL East Atlantic
      unique_id: sensor.nhl_east_atlantic
      state: "{{ now() }}"
      attributes:
        entries: "{{ state_attr('sensor.nhl_standings','children')[0]['children'][0]['standings']['entries'] }}"
    - name: NHL East Metropolitan
      unique_id: sensor.nhl_east_metropolitan
      state: "{{ now() }}"
      attributes:
        entries: "{{ state_attr('sensor.nhl_standings','children')[0]['children'][1]['standings']['entries'] }}"
    - name: NHL West Central
      unique_id: sensor.nhl_west_central
      state: "{{ now() }}"
      attributes:
        entries: "{{ state_attr('sensor.nhl_standings','children')[1]['children'][0]['standings']['entries'] }}"
    - name: NHL West Pacific
      unique_id: sensor.nhl_west_pacific
      state: "{{ now() }}"
      attributes:
        entries: "{{ state_attr('sensor.nhl_standings','children')[1]['children'][1]['standings']['entries'] }}"

I would think.

Please note the indentation. This is why I like to use:

template: !include template.yaml

In my configuration.yaml and then what I posted as a separate file.

You may have to also do some cleanup, not sure … now you will have binary sensors and state-based sensors with the same entity_id, not sure what will happen until a few restarts.

I think you will find yourself outgrowing a huge master configuration to using includes in no time. It is much easier to manage them in “chunks”

And the reason you are not getting data is obvious now as the code relies on this:

sensor.nhl_east_atlantic

and you created:

binary_sensor.nhl_east_atlantic

If you make them “sensor” and not “binary_sensor” everything should start working for you.
For instance, the dashboard code is doing this:

                                  - type: custom:decluttering-card
                                    template: nhl_settings
                                    variables:
                                      - title: Eastern Atlantic
                                      - entity: sensor.nhl_east_atlantic
                                      - sort: x.stats.find(y=>y.shortDisplayName == 'PTS').value

Note that the “entity” passed is “sensor.nhl_east_atlantic”. You have nothing by that name and hence no data. You created “binary_sensor.nhl_east_atlantic”.

Thank you for your help and sorry I overlooked that. I have now created sensor and also swapped out the content in template.yaml. Now tables will appear with you.

But I have three questions.

Nothing at all is displayed in the Overall menu item:

And what to watch at Playoofs? Your code currently looks like this for me:

And my last question is, why I see under Postgame, Live and Pregame some matches twice?
For example at the moment under Pregame Mapple Leafs vs. Avalanche

or under Postgame
Olers vs. Senators and Sharks vs. Blue Jackets:

As a first step, please grab the latest dashboard YAML from GITHUB. It has had a few iterations to fix issues caused because of ESPN changing the JSON for some teams. Drop that in and then check and post what issues remain. I would assume the double listing of games is because you o not use my naming convention or possibly you have two teamtrackers for each team which can happen if they do not have unique id’s.

The changes of note is that now that NHL and NBA teams are “clinching”, ESPN changes their JSON by adding a field. I removed the playoff seed and added the “clinch” indicator. There had to be logic when it did not exist. The old way was positional an assumed like PTS would always by stat(5) which is incorrect for the teams that ha a “CLINCH” flag. I plan to finish this update for MLB and NFL soon.

If you have a GIT account, you can add to follow and be notified of changes as I think there will be a few other coming … I have yet to think through “Playoff” standings and how to do them.

with the new code I can see somthing under overall.

But now in all Standings - Devisional, Conference and Overall there are no Places in the first row:

The double matches are now gone. I still had these because I created them before using your code.

I have now deleted them under integration so that each match is now only displayed once.

Ok Playoffs are under construction.

Now I follow on GIT

1 Like

Do you still have double listings in Teamtracker games? If yes, let’s fix that

No, sorry I have Edition my post

1 Like

OK, so now as a user, please by all means suggest changes. I can certainly put the Playoff Seed column back into the GUI. It is in the data. But this is not the playoff rankings.

ESPN shows WIldcard like this:

So this would be teams 1,2 and 3 in each division … that part is easy. JUst take the top three teams in each divisional view.

But then WIldcard is the the next two teams in the conference view that are not included in the previous list of top 3. I chose the image above because the Kraken have 81pts and the AVs have 80 … but Kraken are Wildcard and AVs in the top three. If I sort by ESPNs playoff SEED or by PTs, the Kraken would be higher than the AVs which is wrong.

So there we are in trying to figure out the best way.

So the Palayoff view like espn looks good, that would be a good option for me.

I also have a suggestion because I saw it now on the screenshot. Wouldn’t it also be a good idea to display the logos of the clubs in small form with all standings?