Sports Standings and Scores

@DhananjayRamachandra Welcome.

I’ll try to add some of my thoughts but others hopefully will chime in.

Here’s a quick overview:

  1. Almost all of the data in this thread comes from ESPN API endpoints (some data are pulled from other sites, but the bulk is ESPN. There are different ways to call the ESPN API (pre-season, post, etc.) and some of those are addressed in my github page as well as other github pages.
  2. The Data (either through REST or Python Scripts) pulls data from ESPN and stores them in sensors.
  3. ESPN Shuts the API’s down, this thread shuts down. So we are all incredibly thankful for ESPN providing the Data.
  4. For the dashboard builds we use a couple of add-ins that we customize: TeamTracker, Flex-Table, and Card-Mod. So make sure that they are installed.
  5. Dashboards use the data in the sensors and we customize team-tracker and flex-table with card-mod and additional customization/templates.
  6. In the dashboards we mostly use the decluttering-template because of the nature of repetitive calls (leagues/conferences/etc.)

A couple places that we try to discuss what we are doing, provide details pieces of the build including yamls (I split my sports sensors into different yamls), templates, dashboards we are using, python scripts, etc.
@kbrown01 github site - GitHub - kbrown01/SportStandingsScores: Sports Standings and Scores Sensors and Dashboard for Home Assistant
@bburwell (my) github site - GitHub - bburwell/HA-Sports-Scores: Home Assistant Sports Scores, Standings, Dashboards and Yaml's for Home Assistant

In the end have fun and create dashboards (you can always delete) and look at the logs :wink:

Bob - Sorry for not getting back to youu sooner, I’ve been traveling and am just now getting back to this.

I have the python files in a python_scripts directory and they seem to work fine and the .json files in my www directory and they populate using an automation. But what I do not see are the gpt sensors. And I have no idea why they aren’t being created because the sensors in the beginning of the yaml file are being created. Somewhere I’m missing something and just need to work through it. I’m overlooking something, just not sure what it is.

Great - sounds like you are close. When the python script runs it creates a .json file which it sounds like you have. Think of this file like an ESPN endpoint that we have used for other sports/statistics. We have to get that json file data into a sensor.

For each sport I break them out into their own sensor yaml file. For example MLB it has its own mlb_sensors.yaml file in the sensors subdirectory.

You will see a couple examples in there of how data gets put into the sensors that we name.

I would always make sure to go through the developers tab and the logs - here is an example in tools:

So for python script data you will see that the json files are grabbed locally like this (you can see the local IP address in the resource):

- platform: rest
  name: MLB AL Wild Card gpt
  unique_id: sensor.mlb_al_wild_card_gpt
  resource:  http://192.168.xxx.xxx:8123/local/mlb_al_wild_card_gpt.json
  value_template: "{{ now()}}"  
  json_attributes: 
   - events
   - series

If I am pulling them from a website it would look like this:

######################## Post Season Playoffs 
- platform: rest
  scan_interval: 604800
  name: MLB Playoffs WorldSeries
  unique_id: sensor.mlb_playoffs_worldseries
  #2024 Dataresource: https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard?dates=20241001-20241030
  resource: https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard?dates=20250929-20251101
  value_template: "{{ now() }}"
  json_attributes:
      - leagues
      - events

One last thing to check. When you first start out if you create a sensor then change/delete sometimes HA creates the changes as a sensor with a _2. Take a look at my troubleshooting section in github - mostly explains - but feel free to ask. Here is the link to the section: GitHub - bburwell/HA-Sports-Scores: Home Assistant Sports Scores, Standings, Dashboards and Yaml's for Home Assistant

First, I want to thank @bburwell, he has taken what I did originally and made it much better.

Side note: I refactored the Strength or Schedule view from dailyfaceoff.com to not use the plugin route for Flex Table. Posting here the relevant tidbits:

The sensor (uses multiscrape) so an include to multiscrape.yaml in configuration.yaml as::

multiscrape: !include multiscrape.yaml

Then the sensor itself to store the data:

  - name: SOS scraper
    resource_template: "https://www.dailyfaceoff.com/nhl-weekly-schedule/{{ (now().timestamp()+(0-now().weekday())*86400) | int | timestamp_custom('%Y-%m-%d') }}"
    scan_interval: 3600
    sensor:
      - unique_id: hockey_strength_of_schedule
        name: Hockey Strength of Schedule
        select: '#__NEXT_DATA__'
        value_template: '{{ now() }}'
        attributes:
          - name: sos
            select: '#__NEXT_DATA__'
            value_template: >
                {{ (value | from_json)['props']['pageProps'] }}

Now the decluttering template:

  sos_settings:
    card:
      type: custom:flex-table-card
      title: '[[title]]'
      css:
        table+: 'padding: 0px; width: 100%;'
        tbody tr:hover: 'background-color: lightgreen!important;'
      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: '[[entity]]'
      sort_by: sos.dataRows-
      columns:
        - name: Games
          data: '[[attribute]]'
          modify: |-
            if (x[8] > 3 ){
                '<div style="display:none;">'+ x[8] + '</div>' + '<div style="background-color:lightgreen;text-align:center">' + x[8] + '</div>'; }
            else if (x[8] == 3) {
                '<div style="display:none;">'+ x[8] + '</div>' + '<div style="background-color:orange;text-align:center"">' + x[8] + '</div>'; }
            else {
                '<div style="display:none;">'+ x[8] + '</div>' + '<div style="background-color:red;color:white;text-align:center">' + x[8] +'</div>'}
        - name: Team
          data: '[[attribute]]'
          modify: >-
            '<div><img src="' + x[0].team.logo + '"
            style="height:20px;vertical-align:middle;">&nbsp;' + x[0].team.name
            + '&nbsp;' + '(' + x[0].team.wins + '-' + x[0].team.losses + '-' +
            x[0].team.overtimeLosses + ')' +'</div>'
        - name: Mon
          data: '[[attribute]]'
          modify: |-
            if (x[1] === null ){
              ''
            } else {
              if (x[0].team.strengthOfSchedule.strength > x[1].team.strengthOfSchedule.strength){
                '<div style="background-color:lightcoral;text-align:center">' + (x[1].home == true ? "" : "@&nbsp;") + x[1].team.abbreviation + '&nbsp;' + '(' + x[1].team.wins + '-' + x[1].team.losses + '-' + x[1].team.overtimeLosses + ')' + '</div>'
              } else {
                '<div style="background-color:lightgreen;text-align:center">' + (x[1].home == true ? "" : "@&nbsp;") + x[1].team.abbreviation + '&nbsp;' + '(' + x[1].team.wins + '-' + x[1].team.losses + '-' + x[1].team.overtimeLosses + ')' + '</div>'
              }
            }
        - name: Tues
          data: '[[attribute]]'
          modify: |-
            if (x[2] === null ){
              ''
            } else {
              if (x[0].team.strengthOfSchedule.strength > x[2].team.strengthOfSchedule.strength){
                '<div style="background-color:lightcoral;text-align:center">' + (x[2].home == true ? "" : "@&nbsp;") + x[2].team.abbreviation + '&nbsp;' + '(' + x[2].team.wins + '-' + x[2].team.losses + '-' + x[2].team.overtimeLosses + ')' + '</div>'
              } else {
                '<div style="background-color:lightgreen;text-align:center">' + (x[2].home == true ? "" : "@&nbsp;") + x[2].team.abbreviation + '&nbsp;' + '(' + x[2].team.wins + '-' + x[2].team.losses + '-' + x[2].team.overtimeLosses + ')' + '</div>'
              }
            }
        - name: Wed
          data: '[[attribute]]'
          modify: |-
            if (x[3] === null ){
              ''
            } else {
              if (x[0].team.strengthOfSchedule.strength > x[3].team.strengthOfSchedule.strength){
                '<div style="background-color:lightcoral;text-align:center">' + (x[3].home == true ? "" : "@&nbsp;") + x[3].team.abbreviation + '&nbsp;' + '(' + x[3].team.wins + '-' + x[3].team.losses + '-' + x[3].team.overtimeLosses + ')' + '</div>'
              } else {
                '<div style="background-color:lightgreen;text-align:center">' + (x[3].home == true ? "" : "@&nbsp;") + x[3].team.abbreviation + '&nbsp;' + '(' + x[3].team.wins + '-' + x[3].team.losses + '-' + x[3].team.overtimeLosses + ')' + '</div>'
              }
            }
        - name: Thur
          data: '[[attribute]]'
          modify: |-
            if (x[4] === null ){
              ''
            } else {
              if (x[0].team.strengthOfSchedule.strength > x[4].team.strengthOfSchedule.strength){
                '<div style="background-color:lightcoral;text-align:center">' + (x[4].home == true ? "" : "@&nbsp;") + x[4].team.abbreviation + '&nbsp;' + '(' + x[4].team.wins + '-' + x[4].team.losses + '-' + x[4].team.overtimeLosses + ')' + '</div>'
              } else {
                '<div style="background-color:lightgreen;text-align:center">' + (x[4].home == true ? "" : "@&nbsp;") + x[4].team.abbreviation + '&nbsp;' + '(' + x[4].team.wins + '-' + x[4].team.losses + '-' + x[4].team.overtimeLosses + ')' + '</div>'
              }
            }
        - name: Fri
          data: '[[attribute]]'
          modify: |-
            if (x[5] === null ){
              ''
            } else {
              if (x[0].team.strengthOfSchedule.strength > x[5].team.strengthOfSchedule.strength){
                '<div style="background-color:lightcoral;text-align:center">' + (x[5].home == true ? "" : "@&nbsp;") + x[5].team.abbreviation + '&nbsp;' + '(' + x[5].team.wins + '-' + x[5].team.losses + '-' + x[5].team.overtimeLosses + ')' + '</div>'
              } else {
                '<div style="background-color:lightgreen;text-align:center">' + (x[5].home == true ? "" : "@&nbsp;") + x[5].team.abbreviation + '&nbsp;' + '(' + x[5].team.wins + '-' + x[5].team.losses + '-' + x[5].team.overtimeLosses + ')' + '</div>'
              }
            }
        - name: Sat
          data: '[[attribute]]'
          modify: |-
            if (x[6] === null ){
              ''
            } else {
              if (x[0].team.strengthOfSchedule.strength > x[6].team.strengthOfSchedule.strength){
                '<div style="background-color:lightcoral;text-align:center">' + (x[6].home == true ? "" : "@&nbsp;") + x[6].team.abbreviation + '&nbsp;' + '(' + x[6].team.wins + '-' + x[6].team.losses + '-' + x[6].team.overtimeLosses + ')' + '</div>'
              } else {
                '<div style="background-color:lightgreen;text-align:center">' + (x[6].home == true ? "" : "@&nbsp;") + x[6].team.abbreviation + '&nbsp;' + '(' + x[6].team.wins + '-' + x[6].team.losses + '-' + x[6].team.overtimeLosses + ')' + '</div>'
              }
            }
        - name: Sun
          data: '[[attribute]]'
          modify: |-
            if (x[7] === null ){
              ''
            } else {
              if (x[0].team.strengthOfSchedule.strength > x[7].team.strengthOfSchedule.strength){
                '<div style="background-color:lightcoral;text-align:center">' + (x[7].home == true ? "" : "@&nbsp;") + x[7].team.abbreviation + '&nbsp;' + '(' + x[7].team.wins + '-' + x[7].team.losses + '-' + x[7].team.overtimeLosses + ')' + '</div>'
              } else {
                '<div style="background-color:lightgreen;text-align:center">' + (x[7].home == true ? "" : "@&nbsp;") + x[7].team.abbreviation + '&nbsp;' + '(' + x[7].team.wins + '-' + x[7].team.losses + '-' + x[7].team.overtimeLosses + ')' + '</div>'
              }
            }
        - name: Opp Strength
          data: '[[attribute]]'
          modify: x[0].team.strengthOfSchedule.averageOpponentStrength
        - name: Rank
          data: '[[attribute]]'
          modify: x[0].team.strengthOfSchedule.rank

And the result:

Perfect for NHL Fantasy Hockey players to visualize the upcoming week.

1 Like

So I have been playing with the NCAAF College Bowls. I wanted to try to use Sensors/Template, but like before Template has a problem with size. So I had to go back to using python to make an ESPN call that then separates out All Bowls, Regular Bowls, CFP Playoffs and CFP National Championship into their own json files that I then pull into their respective sensors that I have added to the ncaaf_sensors.yaml file.

Here are the adds to the ncaaf_sensors.yaml file:

###########################NCAAF Bowl Games    
### NCAAF Bowl Games
- platform: rest
  name: NCAAF Regular Bowl Games
  unique_id: sensor.ncaaf_regular_bowl_games
  resource:  http://192.168.xxx.xxx:8123/local/cfb_regular_bowls.json
  value_template: "{{ now()}}"  
  json_attributes: 
   - events   

### CFP Bowl Games
- platform: rest
  name: NCAAF CFP Playoffs Bowl Games
  unique_id: sensor.ncaaf_cfp_playoffs_bowl_games
  resource:  http://192.168.xxx.xxx:8123/local/cfb_cfp_playoffs.json
  value_template: "{{ now()}}"  
  json_attributes: 
   - events
  
### CFP Championship Bowl Game
- platform: rest
  name: NCAAF CFP Championship Bowl Game
  unique_id: sensor.ncaaf_cfp_championship_bowl_game
  resource:  http://192.168.xxx.xxx:8123/local/cfb_championship.json
  value_template: "{{ now()}}"  
  json_attributes: 
   - events

### NCAAF All bowl Games
- platform: rest
  name: NCAAF All Bowl Games
  unique_id: sensor.ncaaf_all_bowl_games
  resource:  http://192.168.xxx.xxx:8123/local/cfb_all_bowls.json
  value_template: "{{ now()}}"  
  json_attributes: 
   - events
  • The Dashboard that I am using is called: Sanbox NCAAF Bowls (with NFL for testing)
    ** I am still working on changes that I will then most likely incorporate into the other playoffs that is why I have the NFL in there for testing. I am also looking at adding bowl logos but that most like will need to be added to the python files. ESPN doesnt populate bowl pngs that I can find. I will eventually put this into the college sports dashboard once I am happy with the decluttering changes - yea I know I called it SANbox :wink:
  • You will need to create an automation because it should update during the games with live data but no way to test yet and last years are finals.
  • The sensors above have also been added to the ncaaf_sensors.yaml on github.
  • I am using expander-card as I do in other areas.
  • You will see all bowl games on the left and then on the right the layout is Regular Bowls, CFP Playoffs and then CFP National Championship.

Here is what last year looks like:

This years bowls haven’t been selected yet so this is what it looks like today:

Please make suggestions, changes and I will try to incorporate when I have time.

I’m having another problem with the output of the overall table.

With this code:

type: grid
cards:
  - type: custom:decluttering-card
    template: nhl_settings
    variables:
      - title: Overall
      - entity: sensor.nhl_*_*
      - attribute: entries
      - excluded_entities:
          - sensor.nhl_starting_goalies
          - sensor.nhl_wildcard_standings
          - binary_sensor.nhl_east_atlantic
          - binary_sensor.nhl_east_metropolitan
          - binary_sensor.nhl_west_central
          - binary_sensor.nhl_west_pacific
          - sensor.nhl_po_games
          - sensor.nhl_po_amer_games
          - sensor.nhl_po_natl_games
          - sensor.nhl_po_amer_wc_games
          - sensor.nhl_po_natl_wc_games
          - sensor.nhl_po_amer_div_games
          - sensor.nhl_po_natl_div_games
          - sensor.nhl_po_amer_leag_games
          - sensor.nhl_po_natl_leag_games
          - sensor.nhl_po_world_game
          - sensor.nhl_po_tv_coverage
          - sensor.nhl_playoff_seeds
          - sensor.nhl_po_team_games
      - sort: x.stats.find(y=>y.shortDisplayName == 'PTS').value
    grid_options:
      columns: full
column_span: 4

I got only a blank site.

With the Conferance Code:

type: grid
cards:
  - type: custom:stack-in-card
    mode: vertical
    cards:
      - type: custom:decluttering-card
        template: nhl_settings
        variables:
          - title: Eastern
          - entity: sensor.nhl_east_*
          - attribute: entries
          - excluded_entities:
              - sensor.nhl_starting_goalies
              - sensor.nhl_wildcard
              - sensor.nhl_wildcard_standings
          - sort: x.stats.find(y=>y.shortDisplayName == 'PTS').value
    grid_options:
      columns: full
column_span: 4

I got the table.

I don´t know what is wrong for the overall table.

Check in template what is returned from this. If it is has entries not listed as excluded that should be, it could lead to errors.

Something like this:

        {%
          set nhl_sensors = states.sensor
          | selectattr('entity_id', 'contains', "nhl_")
          | map(attribute='entity_id')
          | list
        %}
        {{ nhl_sensors }}

For me yields:

[
  "sensor.nhl_wildcard",
  "sensor.nhl_standings",
  "sensor.nhl_east_atlantic",
  "sensor.nhl_east_metropolitan",
  "sensor.nhl_west_central",
  "sensor.nhl_west_pacific",
  "sensor.nhl_wildcard_standings",
  "sensor.nhl_starting_goalies"
]

Now, you may have a different list using @bburwell code with many other sensors, They key is to be sure you exclude what should not be included. It could also be that you have excluded something that does not exist.

Now that said, it is unclear to me what are the binary sensors. They could be but then my code above would need to include them. I do not use binary_sensor, but maybe @bburwell does.

thanks for your quick replay.

with your code I got this list:

 "sensor.nhl_playoff_west_2nd_round_gpt",
  "sensor.nhl_playoff_east_final_gpt",
  "sensor.nhl_playoff_west_final_gpt",
  "sensor.nhl_playoff_east_2nd_round_gpt",
  "sensor.nhl_stanley_cup_final_gpt",
  "sensor.nhl_playoff_east_1st_round",
  "sensor.nhl_playoff_west_1st_round",
  "sensor.nhl_starting_goalies",
  "sensor.nhl_playoff_east_1st_round_gpt",
  "sensor.nhl_playoff_west_1st_round_gpt",
  "sensor.nhl_wildcard",
  "sensor.nhl_standings",
  "sensor.nhl_playoffs_stanleycup",
  "sensor.nhl_east_atlantic",
  "sensor.nhl_east_metropolitan",
  "sensor.nhl_west_central",
  "sensor.nhl_west_pacific",
  "sensor.nhl_wildcard_standings",
  "sensor.nhl_playoff_filtered_events",
  "sensor.nhl_po_games",
  "sensor.nhl_po_amer_games",
  "sensor.nhl_po_natl_games",
  "sensor.nhl_po_amer_wc_games",
  "sensor.nhl_po_natl_wc_games",
  "sensor.nhl_po_amer_div_games",
  "sensor.nhl_po_natl_div_games",
  "sensor.nhl_po_amer_leag_games",
  "sensor.nhl_po_natl_leag_games",
  "sensor.nhl_po_world_game",
  "sensor.nhl_po_tv_coverage",
  "sensor.nhl_playoff_seeds",
  "sensor.nhl_po_team_games"

Now I have changes my code for the overall like this:

type: grid
cards:
  - type: custom:decluttering-card
    template: nhl_settings
    variables:
      - title: Overall
      - entity: sensor.nhl_*_*
      - attribute: entries
      - excluded_entities:
          - sensor.nhl_starting_goalies
          - sensor.nhl_wildcard_standings
          - binary_sensor.nhl_east_atlantic
          - binary_sensor.nhl_east_metropolitan
          - binary_sensor.nhl_west_central
          - binary_sensor.nhl_west_pacific
          - sensor.nhl_po_games
          - sensor.nhl_po_amer_games
          - sensor.nhl_po_natl_games
          - sensor.nhl_po_amer_wc_games
          - sensor.nhl_po_natl_wc_games
          - sensor.nhl_po_amer_div_games
          - sensor.nhl_po_natl_div_games
          - sensor.nhl_po_amer_leag_games
          - sensor.nhl_po_natl_leag_games
          - sensor.nhl_po_world_game
          - sensor.nhl_po_tv_coverage
          - sensor.nhl_playoff_seeds
          - sensor.nhl_po_team_games
          - sensor.nhl_playoff_west_2nd_round_gpt
          - sensor.nhl_playoff_east_final_gpt"
          - sensor.nhl_playoff_west_final_gpt
          - sensor.nhl_playoff_east_2nd_round_gpt
          - sensor.nhl_stanley_cup_final_gpt
          - sensor.nhl_playoff_east_1st_round
          - sensor.nhl_playoff_west_1st_round
          - sensor.nhl_starting_goalies
          - sensor.nhl_playoff_east_1st_round_gpt
          - sensor.nhl_playoff_west_1st_round_gpt
          - sensor.nhl_wildcard
          - sensor.nhl_standings
          - sensor.nhl_playoffs_stanleycup
          - sensor.nhl_playoff_filtered_events
          - sensor.nhl_po_games
          - sensor.nhl_po_amer_games
          - sensor.nhl_po_natl_games
          - sensor.nhl_po_amer_wc_games
          - sensor.nhl_po_natl_wc_games
          - sensor.nhl_po_amer_div_games
          - sensor.nhl_po_natl_div_games
          - sensor.nhl_po_amer_leag_games
          - sensor.nhl_po_natl_leag_games
          - sensor.nhl_po_world_game
          - sensor.nhl_po_tv_coverage
          - sensor.nhl_playoff_seeds
          - sensor.nhl_po_team_games
      - sort: x.stats.find(y=>y.shortDisplayName == 'PTS').value
    grid_options:
      columns: full
column_span: 4

But I also got with this code the blank site.

EDIT:
I don´t know where the binary sensors from.

When I look at it they are unavailable.

Those are listed as “binary_sensor” not “sensor”.

In the list of sensors returned, they are just sensor, Try changing those 4 to “sensor” as they exist.

Yes I know there are binary_sensors. But I don’t know where they come from.

Where I must changing those 4 to “sensor”

Have you any idea, how to fix my overall table?

EDIT:
I got it. I think I forgot an exclude.

This Code works:

type: grid
cards:
  - type: custom:decluttering-card
    template: nhl_settings
    variables:
      - title: Overall
      - entity: sensor.nhl_*_*
      - attribute: entries
      - excluded_entities:
          - sensor.nhl_playoff_west_2nd_round_gpt
          - sensor.nhl_playoff_east_final_gpt
          - sensor.nhl_playoff_west_final_gpt
          - sensor.nhl_playoff_east_2nd_round_gpt
          - sensor.nhl_stanley_cup_final_gpt
          - sensor.nhl_playoff_east_1st_round
          - sensor.nhl_playoff_west_1st_round
          - sensor.nhl_starting_goalies
          - sensor.nhl_playoff_east_1st_round_gpt
          - sensor.nhl_playoff_west_1st_round_gpt
          - sensor.nhl_wildcard
          - sensor.nhl_standings
          - sensor.nhl_playoffs_stanleycup
          - sensor.nhl_wildcard_standings
          - sensor.nhl_playoff_filtered_events
          - sensor.nhl_po_games
          - sensor.nhl_po_amer_games
          - sensor.nhl_po_natl_games
          - sensor.nhl_po_amer_wc_games
          - sensor.nhl_po_natl_wc_games
          - sensor.nhl_po_amer_div_games
          - sensor.nhl_po_natl_div_games
          - sensor.nhl_po_amer_leag_games
          - sensor.nhl_po_natl_leag_games
          - sensor.nhl_po_world_game
          - sensor.nhl_po_tv_coverage
          - sensor.nhl_playoff_seeds
          - sensor.nhl_po_team_games
      - sort: x.stats.find(y=>y.shortDisplayName == 'PTS').value
    grid_options:
      columns: full
column_span: 4

Thank you so much

I do not use binary sensors for sports