NFL game sensor (scores, possession, etc)

I might go the txt file route. FWIW, a HA restart isn’t necessary, there are services for reloading rest and template sensors:

service: rest.reload

and

service: template.reload

They’re available as buttons under the Server Controls in in Configuration, or as services you can call in developer tools or via scripts/automations/etc.

If I get ambitious (and find a few hours to kill), I might still try to build a template that won’t balk at the missing situation key, but I can live with what I’ve got. I kept the 86400 seconds as scan interval and then use an automation to manually update when I care about the live info, I’ll just keep it switched off the rest of the time.

Thanks again for everything!

This should get rid of the errors. I haven’t tested it during a game yet, but I think it should parse the new info from the json feed when the game is live.

    - name: Packers Game Possession
      value_template: >
          {% for event in value_json["events"] %}
            {% if 'GB' in event.shortName %}
              {% for competition in event["competitions"] %}
                {% if competition.status.type.state == 'pre' or competition.status.type.state == 'post' %}
                  {% set situation = {'possession': 'null'} %}
                {% else %}
                  {{ competition.situation.possession }}
                {% endif %}
              {% endfor %}
            {% endif %}
          {% endfor %}
2 Likes

I added a sensor that allows you to easily define the abbreviation for all of the sensors at once.

rest:
- resource: http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard
  scan_interval: 86400
  sensor:
    - name: My Team
      value_template: >
        {% set my = { "team": 'GB' } %}
        {{ my.team }}
    [...]

Then you can replace the abbreviation in the sensors with states.sensor.my_team.state
So

{% if 'GB' in event.shortName %}

becomes

{% if states.sensor.my_team.state in event.shortName %}

We should really start a github repo to track changes.

1 Like

We should really start a github repo to track changes.

Yah, it seems like that would be useful. At this point I imagine it would be relatively easy to wrap all the work you’ve all done as a custom component as well. In the meantime, I would be happy to spin up a new repo and all all as contributors.

2 Likes

I’d personally be hesitant to release a custom component using an unofficial API. But I do think it’d be nice to have a repo to consolidate all the info and submit PRs.

Yah… I’m thinking I will require the user to enter the API endpoint, so that it isn’t my responsibility. That said, I have an initial version working with this as a sensor, where the state is the game state, and a bunch of stuff is available as attributes. I need to work on making the polling interval smart (and fix the bug with timeouts), but so far it is a nice start.

It’s a PITA to add new attributes, but if there are any that seem essential, let me know and I can add before sharing the component.

1 Like

Nice! Seems like a great start.

Essential:

  • Possession

Nice to Have:

  • Location
    (I was thinking about trying to work out a dynamic weather card based on the game location/date)
  • Broadcast Provider
  • Short Name

I’d Like:

  • Win Probability
  • Team Colors / Alt Colors

I found the repo on your github so I’ll check out the code now.

Ah yes possession, good call. Wrote this after the game last night so I missed it in the JSON.

Seems like I should just make the name field reference short name, since I think anyone using this component would know the city.

When you say location, do you mean the city it’s taking place in? If so that’s another easy one!

Yes, city and state would be good.
{{ competition.venue.address.city }}, {{ competition.venue.address.state }}

Easy enough, just added :slight_smile:

Guessing it will not be easy, but next item is to make polling smart (ie parse the kickoff time and then making polling super frequent until state=post…). Let me know if you notice anything amiss. Will also add colors/probability shortly.

edit: Added colors and probability to the repo. Let me know if you notice anything funky.

1 Like

pretty rough to pull those hex colors, but I think this would work

#{{ (states.sensor.nfl.attributes.team_colors| string).split("'")[1] }}
#{{ (states.sensor.nfl.attributes.team_colors| string).split("'")[3] }}

Oh snap, is it hard to pull from an array? It’s easy for me to just make them two different properties, I just thought an array was a bit tidier.

edit: I think either of these should work:

{{ state_attr("sensor.nfl", "team_colors")[0] }}

{{ states.sensor.nfl.attributes.team_colors[0] }}

yep, that should do the trick! I’m not too well versed with jinja2 / HA templates. If you wanted them to be ready out of the box, it would be nice to prepend the hex values with #

edit: oh btw, for possession to be useful, you’ll also need team id.
{{ competitor.team.id }}

edit2: added a PR for those values.

1 Like

awesome job guys

If Anyone can / wants to take this and include it in the rest sensors ( i dont really understand rest template like you guys do), i added espn news headlines for a particular team. Was only able to do this in node red . Edit: the website to fetch is http://site.api.espn.com/apis/site/v2/sports/football/nfl/news


Imgur

card:

type: vertical-stack
cards:
  - type: markdown
    content: >
      {{(states('sensor.nfl_team_news'))}} 
        ___
      ![Image]({{states.sensor.nfl_team_news.attributes["entity_picture"]}})
      {{states.sensor.nfl_team_news.attributes["caption"]}}
      1).[Mezzanine]({{states.sensor.nfl_team_news.attributes["mezzanine"]}})
      2).[HD]({{states.sensor.nfl_team_news.attributes["hd"]}})
      3).[HLS]({{states.sensor.nfl_team_news.attributes["hls"]}})
  - type: markdown
    content: >-
      {{(states('sensor.nfl_team_news_alt'))}} 
        ___
      ![Image]({{states.sensor.nfl_team_news_alt.attributes["entity_picture"]}})
      {{states.sensor.nfl_team_news_alt.attributes["caption"]}}
      1).[Mezzanine]({{states.sensor.nfl_team_news_alt.attributes["mezzanine"]}})
      2).[HD]({{states.sensor.nfl_team_news_alt.attributes["hd"]}})
      3).[HLS]({{states.sensor.nfl_team_news_alt.attributes["hls"]}})

nodered import:

[{"id":"e4b286ed5577ce9c","type":"www-request","z":"8f4d3eb158748da5","name":"all news","method":"POST","ret":"obj","url":"http://site.api.espn.com/apis/site/v2/sports/football/nfl/news","follow-redirects":true,"persistent-http":true,"tls":"","x":240,"y":40,"wires":[["943fae5448eaa432"]]},{"id":"1581cefc08921ec8","type":"inject","z":"8f4d3eb158748da5","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":100,"y":40,"wires":[["e4b286ed5577ce9c"]]},{"id":"943fae5448eaa432","type":"change","z":"8f4d3eb158748da5","name":"articles","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.articles","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":380,"y":40,"wires":[["caba0f6d3b25ccd7"]]},{"id":"caba0f6d3b25ccd7","type":"function","z":"8f4d3eb158748da5","name":"CHANGE TEAM HERE","func":"msg.payload = msg.payload.filter(el => el.description.includes(\"Bears\"))\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":770,"y":40,"wires":[["036e5e3009994170"]]},{"id":"036e5e3009994170","type":"change","z":"8f4d3eb158748da5","name":"link url","rules":[{"t":"set","p":"link","pt":"msg","to":"payload.links.api.news.href","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":110,"y":100,"wires":[["1b6024f96ef113e3","dd22d5ae6bbbef0e"]]},{"id":"c58a56d1d7457836","type":"debug","z":"8f4d3eb158748da5","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":870,"y":220,"wires":[]},{"id":"1b6024f96ef113e3","type":"www-request","z":"8f4d3eb158748da5","name":"team link for article","method":"GET","ret":"obj","url":"{{{link.0}}}","follow-redirects":true,"persistent-http":true,"tls":"","x":310,"y":220,"wires":[["af345639c78e434f"]]},{"id":"af345639c78e434f","type":"change","z":"8f4d3eb158748da5","name":"videos","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.videos[0]","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":220,"wires":[["d03c7c2717517de0"]]},{"id":"d03c7c2717517de0","type":"ha-entity","z":"8f4d3eb158748da5","name":"nfl_team_news","server":"d363f6df.2ec558","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"nfl_team_news"},{"property":"device_class","value":""},{"property":"icon","value":""},{"property":"unit_of_measurement","value":""}],"state":"payload.headline","stateType":"msg","attributes":[{"property":"caption","value":"payload.caption","valueType":"msg"},{"property":"entity_picture","value":"payload.thumbnail","valueType":"msg"},{"property":"Mezzanine","value":"payload.links.source.mezzanine.href","valueType":"msg"},{"property":"HD","value":"payload.links.source.HD.href","valueType":"msg"},{"property":"HLS","value":"payload.links.source.HLS.href","valueType":"msg"}],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","outputOnStateChange":false,"outputPayload":"$entity().state ? \"on\": \"off\"","outputPayloadType":"jsonata","x":620,"y":220,"wires":[["c58a56d1d7457836"]]},{"id":"439d3c6e028842ae","type":"debug","z":"8f4d3eb158748da5","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":870,"y":320,"wires":[]},{"id":"dd22d5ae6bbbef0e","type":"www-request","z":"8f4d3eb158748da5","name":"team link for article","method":"GET","ret":"obj","url":"{{{link.1}}}","follow-redirects":true,"persistent-http":true,"tls":"","x":230,"y":340,"wires":[["4d60d80a3baea755"]]},{"id":"4d60d80a3baea755","type":"change","z":"8f4d3eb158748da5","name":"videos","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.videos[0]","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":340,"wires":[["cf9d1511ad38dcb3"]]},{"id":"cf9d1511ad38dcb3","type":"ha-entity","z":"8f4d3eb158748da5","name":"nfl_team_news_alt","server":"d363f6df.2ec558","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"nfl_team_news_alt"},{"property":"device_class","value":""},{"property":"icon","value":""},{"property":"unit_of_measurement","value":""}],"state":"payload.headline","stateType":"msg","attributes":[{"property":"caption","value":"payload.caption","valueType":"msg"},{"property":"entity_picture","value":"payload.thumbnail","valueType":"msg"},{"property":"Mezzanine","value":"payload.links.source.mezzanine.href","valueType":"msg"},{"property":"HD","value":"payload.links.source.HD.href","valueType":"msg"},{"property":"HLS","value":"payload.links.source.HLS.href","valueType":"msg"}],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","outputOnStateChange":false,"outputPayload":"$entity().state ? \"on\": \"off\"","outputPayloadType":"jsonata","x":650,"y":340,"wires":[["439d3c6e028842ae"]]},{"id":"d363f6df.2ec558","type":"server","name":"Home Assistant","version":1,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

Explain the update interval and the update timeout. I had mine updating every 2 seconds last weekend, during the game. Had the lights changing when the Chiefs had possession. Can you dump the update interval into seconds from minutes? Love your smart update interval idea.

I used Node Red to make mine update fast after PRE and it stopped on POST

The timeout shouldn’t really matter, and yah the interval is minutes. I could make it seconds, I just wanted to avoid it spamming the API too much. Even a few dozen people hitting it every two seconds would be noticeable I think. I’m hoping to try making it smarter this evening.

For those wanting to switch to the custom component, here are the template sensors, and lovelace cards I’m using.

Also, if the smart update interval isn’t completed by the weekend, I would think it would still possible to use an automation to force update sensor.nfl in the interim.

Note: Last Play & Win Probability are conditional and only appear during game time. Only shown here with null values as a preview. This has not been tested in game yet.

light mode

dark mode

Template Sensors

sensor:
################ NFL SCOREBOARD ################
  - platform: template
    sensors:
      nfl_team_timeouts:
        value_template: >
          {% if state_attr('sensor.nfl', 'team_timeouts') == 3 %}
            •••
          {% elif state_attr('sensor.nfl', 'team_timeouts') == 2 %}
            ••
          {% elif state_attr('sensor.nfl', 'team_timeouts') == 1 %}
            •
          {% else %}

          {% endif %}
        friendly_name_template: >
          {{ state_attr('sensor.nfl', 'team_name') }} Timeouts Remaining
      nfl_team_possession:
        value_template: >
          {% if state_attr('sensor.nfl', 'possession') == state_attr('sensor.nfl', 'team_id') %}
            🏈
          {% else %}

          {% endif %}
      nfl_team_score:
        value_template: >
          {{ states('sensor.nfl_team_timeouts') }} {{ state_attr('sensor.nfl', 'team_score') }}
        friendly_name_template: >
          {{ state_attr('sensor.nfl', 'team_name') }} {{ states('sensor.nfl_team_possession') }}
        entity_picture_template: >
          {{ state_attr('sensor.nfl', 'team_logo') }}
      nfl_opponent_timeouts:
        value_template: >
          {% if state_attr('sensor.nfl', 'opponent_timeouts') == 3 %}
            •••
          {% elif state_attr('sensor.nfl', 'opponent_timeouts') == 2 %}
            ••
          {% elif state_attr('sensor.nfl', 'opponent_timeouts') == 1 %}
            •
          {% else %}

          {% endif %}
        friendly_name_template: >
          {{ state_attr('sensor.nfl', 'opponent_name') }} Timeouts Remaining
      nfl_opponent_possession:
        value_template: >
          {% if state_attr('sensor.nfl', 'possession') == state_attr('sensor.nfl', 'opponent_id') %}
            🏈
          {% else %}

          {% endif %}
      nfl_opponent_score:
        value_template: >
          {{ states('sensor.nfl_opponent_timeouts') }} {{ state_attr('sensor.nfl', 'opponent_score') }}
        friendly_name_template: >
          {{ state_attr('sensor.nfl', 'opponent_name') }} {{ states('sensor.nfl_opponent_possession') }}
        entity_picture_template: >
          {{ state_attr('sensor.nfl', 'opponent_logo') }}
      nfl_sub_1:
        value_template: >
          {% if states('sensor.nfl') == 'PRE' %}
            Line: {{ state_attr('sensor.nfl', 'odds') }}
          {% elif states('sensor.nfl') == 'POST' %}
            Final
          {% else %}
            {{ state_attr('sensor.nfl', 'quarter') }} - {{ state_attr('sensor.nfl', 'clock') }}
          {% endif %}
        friendly_name_template: > 
          {{ as_timestamp(state_attr('sensor.nfl', 'date')) | timestamp_custom("%A @ %-I:%M %p") }}
      nfl_sub_2:
        value_template: >
          {% if states('sensor.nfl') == 'PRE' %}
            O/U: {{ state_attr('sensor.nfl', 'overunder') }}
          {% elif states('sensor.nfl') == 'POST' %}
            
          {% else %}
            {{ state_attr('sensor.nfl', 'down_distance_text') }}
          {% endif %}
        friendly_name_template: >
          {{ state_attr('sensor.nfl', 'venue') }}
      nfl_sub_3:
        value_template: >
          {{ state_attr('sensor.nfl', 'tv_network') }}
        friendly_name_template: >
          {{ state_attr('sensor.nfl', 'location') }}
      nfl_team_win_probability:
          value_template: >
            {{ state_attr('sensor.nfl', 'team_win_probability') | float * 100 }}
          friendly_name_template: >
            {{ state_attr('sensor.nfl', 'team_name') }}
      nfl_opponent_win_probability:
          value_template: >
            {{ state_attr('sensor.nfl', 'opponent_win_probability') | float * 100 }}
          friendly_name_template: >
            {{ state_attr('sensor.nfl', 'opponent_name') }}

Lovelace Cards
(uses card-mod, mini graph card, card templater, & card-tools)

      - type: vertical-stack
        cards:
          - type: entities
            title: Scoreboard
            card_mod:
              style: |
                div#states > div {
                  margin: 0px;
                }
            entities:
              - entity: sensor.nfl_team_score
                card_mod:
                  style: 
                    hui-generic-entity-row: 
                      $: |
                        .info {
                          font-size: 1.6em;
                          font-weight: 500;
                          padding: 6px 0;
                        }
                      .: |
                        .text-content {
                          font-size: 1.6em;
                          font-weight: 500;
                          padding: 6px 0;
                        }
              - entity: sensor.nfl_opponent_score
                card_mod:
                  style:
                    hui-generic-entity-row: 
                      $: |
                        .info {
                          font-size: 1.6em;
                          font-weight: 500;
                          padding: 4px 0 8px;
                        }
                      .: |
                        .text-content {
                          font-size: 1.6em;
                          font-weight: 500;
                          padding: 4px 0 8px;
                        }
              - type: divider
                card_mod:
                  style: |
                      div {
                        margin: 4px 0 8px !important;
                      }
                    
              - entity: sensor.nfl_sub_1
                card_mod:
                  style:
                    hui-generic-entity-row:
                      .: |
                        .text-content {
                          font-size: 1.2em;
                        }
                      $: |
                        state-badge {display:none;}
                        .text-content {
                          margin-left: 0px !important;
                          font-size: 1.2em;
                        }
              - entity: sensor.nfl_sub_2
                card_mod:
                  style:
                    hui-generic-entity-row:
                      .: |
                        .text-content {
                          font-weight: 300;
                        }
                      $: |
                        state-badge {display:none;}
                        .text-content {
                          margin-left: 0px !important;
                          font-weight: 300;
                        }
              - entity: sensor.nfl_sub_3
                card_mod:
                  style:
                    hui-generic-entity-row:
                      .: |
                        .text-content {
                          font-weight: 300;
                        }
                      $: |
                        state-badge {display:none;}
                        .text-content {
                          margin-left: 0px !important;
                          font-weight: 300;
                        }
          - type: conditional
            conditions:
              - entity: sensor.nfl
                state_not: 'PRE'
              - entity: sensor.nfl
                state_not: 'POST'
            card:
              type: 'custom:button-card'
              card_mod:
                style: 
                  div#container: |
                    div#name {
                      width: 100%;
                    }
              name: |
                [[[return `<div style='display:flex'>
                  <marquee>
                    ${states['sensor.nfl'].attributes.last_play}
                  </marquee>`]]]
              styles:
                card:
                  - padding: 8px
                  - font-size: 15px
          - type: conditional
            conditions:
              - entity: sensor.nfl
                state_not: 'PRE'
              - entity: sensor.nfl
                state_not: 'POST'
            card:
              type: 'custom:card-templater'
              entities:
               - sensor.nfl_team_win_probability
               - sensor.nfl_opponent_win_probability
               - sensor.nfl
               - input_boolean.dark_mode
              card:
                type: 'custom:mini-graph-card'
                name: Win Probability
                icon: 'mdi:football'
                hours_to_show: 2
                points_per_hour: 120
                aggregate_func: last
                unit: '%'
                entities:
                  - entity: sensor.nfl_team_win_probability
                    color_template: >
                      {% if states('input_boolean.dark_mode') == 'on' %}
                       {{ state_attr('sensor.nfl', 'team_colors')[1] }}
                      {% else %}
                       {{ state_attr('sensor.nfl', 'team_colors')[0] }}
                      {% endif %}
                  - entity: sensor.nfl_opponent_win_probability
                    color_template: >
                      {% if states('input_boolean.dark_mode') == 'on' %}
                       {{ state_attr('sensor.nfl', 'opponent_colors')[1] }}
                      {% else %}
                       {{ state_attr('sensor.nfl', 'opponent_colors')[0] }}
                      {% endif %}

Edit 1: Turned off graph animation, fixed timeout sensors.
Edit 2: change FINAL to POST

Nice, I’m not sure what the etiquette is for having cards in a custom_component but feel free to submit a PR. I believe the packages subdir is where things like this would go!

If anyone wants to check out the custom component it’s at github.com/zacs/ha-nfl — I’ll add to the parent post when it gets a bit cleaner.

Ideally, I’d like to build a custom card with a more standard scoreboard format. Something closer to this
image

I’m still working out the HTML and CSS to make it more appealing, as well as adding possession and timeouts etc. I’m not sure I can work out a custom graph in it, but I’d like to get the rest of the info including the last play. And then I have to figure out how to turn that into a custom card. If anyone has any suggestions on something existing I can fork, it would be appreciated.

Edit: got a bit further and have a decent html/css base to work from.
image
I might try to incorporate the text elements in a more creative way. I also found the boilerplate HACS card, so I’m looking into that but it’ll take quite a bit of reading / trial and error.