Add live scores to Home Assistant
One YAML package: a RESTful resource reads the league scoreboard, six template sensors pick out your team's game, and automations refresh it often during games and rarely otherwise, notify the final and (optionally) flash a light when your team scores.
- Stack
- Home Assistant, YAML, Jinja
- Plan
- Starter for live updates all season; Free for next-game and final scores only
- Code
- home-assistant/ (MIT)
Updated 2026-10-18. Sensor templates rendered against a real NHL scoreboard captured on that date. Data is aggregated from public sources and is typically 20-30 seconds behind live play (about 1 second after our source). There is no SLA.
1. Get a free API key
Sign up (no card) and copy the key from the dashboard. The Free plan gives you 1,000 calls in the first 30 days, then 125 a month, at 1 request per second. Every successful REST call counts as one call.
Check the key works with one call to the live-scores endpoint:
Terminal
export REALTIME_SPORTS_API_KEY=your_key
curl -H "Authorization: Bearer $REALTIME_SPORTS_API_KEY" \
"https://www.realtimesportsapi.com/api/v1/sports/football/leagues/nfl/events/live"2. Get the package
Everything is in the home-assistant/ folder of the examples repo (MIT). Copy packages/rsa_scores.yaml into your config's packages/ folder and add your key to secrets.yaml. Keep the word Bearer.
secrets.yaml and configuration.yaml
# secrets.yaml
rsa_api_auth: "Bearer YOUR_API_KEY"
# configuration.yaml
homeassistant:
packages: !include_dir_named packages3. One call refreshes every sensor
The rest: resource reads the league scoreboard and six template sensors choose your team's game: a live game first, then the next upcoming one, then the last final. scan_interval is a day, so the resource never polls on its own. Set the league in resource: (default NBA) and the team in input_text.rsa_team (default BOS, changeable from the UI).
The automations do the scheduling: rsa_refresh_while_live calls homeassistant.update_entity every 2 minutes while the game is on (and from 10 minutes before the start), rsa_refresh_idle every 2 hours otherwise. rsa_game_final notifies the final score; rsa_team_scored is off by default and fires on every score change, which suits NHL, NFL and soccer.
home-assistant/packages/rsa_scores.yaml
# Realtime Sports API: live score sensors for one team, as a Home Assistant package.
# Docs: https://www.realtimesportsapi.com/guides/home-assistant-live-scores
#
# 1. secrets.yaml: rsa_api_auth: "Bearer YOUR_API_KEY"
# 2. configuration.yaml: homeassistant: { packages: !include_dir_named packages }
# 3. Change the league in `resource:` (and the team in input_text below), then restart.
#
# One REST call refreshes every sensor. The resource never polls on its own (scan_interval is a
# day); the automations below refresh it every 2 minutes while your team is playing and every
# 2 hours otherwise, so the quota is spent only when it matters.
input_text:
rsa_team:
name: Team to follow (abbreviation)
initial: BOS
max: 10
rest:
- resource: https://www.realtimesportsapi.com/api/v1/sports/basketball/leagues/nba/events
headers:
Authorization: !secret rsa_api_auth
Accept: application/json
scan_interval: 86400
timeout: 20
sensor:
- name: RSA game state
unique_id: rsa_game_state
icon: mdi:scoreboard
# pre / in / post, or none when the team has no game on the current scoreboard
value_template: >-
{% set team = states('input_text.rsa_team') | upper %}
{% set all = value_json.data | default([]) %}
{% set games = (all | selectattr('homeTeam.abbreviation', 'eq', team) | list)
+ (all | selectattr('awayTeam.abbreviation', 'eq', team) | list) %}
{% set live = games | selectattr('status.state', 'eq', 'in') | list %}
{% set pre = games | selectattr('status.state', 'eq', 'pre') | sort(attribute='date') | list %}
{% set post = games | selectattr('status.state', 'eq', 'post') | sort(attribute='date', reverse=true) | list %}
{% set g = (live + pre + post) | first | default(none) %}
{{ g.status.state if g else 'none' }}
- name: RSA team score
unique_id: rsa_team_score
icon: mdi:numeric
state_class: measurement
value_template: >-
{% set team = states('input_text.rsa_team') | upper %}
{% set all = value_json.data | default([]) %}
{% set games = (all | selectattr('homeTeam.abbreviation', 'eq', team) | list)
+ (all | selectattr('awayTeam.abbreviation', 'eq', team) | list) %}
{% set live = games | selectattr('status.state', 'eq', 'in') | list %}
{% set pre = games | selectattr('status.state', 'eq', 'pre') | sort(attribute='date') | list %}
{% set post = games | selectattr('status.state', 'eq', 'post') | sort(attribute='date', reverse=true) | list %}
{% set g = (live + pre + post) | first | default(none) %}
{% if g and g.status.state != 'pre' %}
{{ g.homeTeam.score if g.homeTeam.abbreviation == team else g.awayTeam.score }}
{% else %}unknown{% endif %}
- name: RSA opponent score
unique_id: rsa_opponent_score
icon: mdi:numeric
state_class: measurement
value_template: >-
{% set team = states('input_text.rsa_team') | upper %}
{% set all = value_json.data | default([]) %}
{% set games = (all | selectattr('homeTeam.abbreviation', 'eq', team) | list)
+ (all | selectattr('awayTeam.abbreviation', 'eq', team) | list) %}
{% set live = games | selectattr('status.state', 'eq', 'in') | list %}
{% set pre = games | selectattr('status.state', 'eq', 'pre') | sort(attribute='date') | list %}
{% set post = games | selectattr('status.state', 'eq', 'post') | sort(attribute='date', reverse=true) | list %}
{% set g = (live + pre + post) | first | default(none) %}
{% if g and g.status.state != 'pre' %}
{{ g.awayTeam.score if g.homeTeam.abbreviation == team else g.homeTeam.score }}
{% else %}unknown{% endif %}
- name: RSA opponent
unique_id: rsa_opponent
icon: mdi:account-group
value_template: >-
{% set team = states('input_text.rsa_team') | upper %}
{% set all = value_json.data | default([]) %}
{% set games = (all | selectattr('homeTeam.abbreviation', 'eq', team) | list)
+ (all | selectattr('awayTeam.abbreviation', 'eq', team) | list) %}
{% set live = games | selectattr('status.state', 'eq', 'in') | list %}
{% set pre = games | selectattr('status.state', 'eq', 'pre') | sort(attribute='date') | list %}
{% set post = games | selectattr('status.state', 'eq', 'post') | sort(attribute='date', reverse=true) | list %}
{% set g = (live + pre + post) | first | default(none) %}
{% if g %}{{ ('vs ' ~ g.awayTeam.abbreviation) if g.homeTeam.abbreviation == team else ('@ ' ~ g.homeTeam.abbreviation) }}{% else %}none{% endif %}
- name: RSA game detail
unique_id: rsa_game_detail
icon: mdi:clock-outline
# "13:12 - 2nd Period", "Final", or the local start time before the game
value_template: >-
{% set team = states('input_text.rsa_team') | upper %}
{% set all = value_json.data | default([]) %}
{% set games = (all | selectattr('homeTeam.abbreviation', 'eq', team) | list)
+ (all | selectattr('awayTeam.abbreviation', 'eq', team) | list) %}
{% set live = games | selectattr('status.state', 'eq', 'in') | list %}
{% set pre = games | selectattr('status.state', 'eq', 'pre') | sort(attribute='date') | list %}
{% set post = games | selectattr('status.state', 'eq', 'post') | sort(attribute='date', reverse=true) | list %}
{% set g = (live + pre + post) | first | default(none) %}
{{ g.status.detail[:250] if g else 'No game on the current scoreboard' }}
- name: RSA game start
unique_id: rsa_game_start
device_class: timestamp
value_template: >-
{% set team = states('input_text.rsa_team') | upper %}
{% set all = value_json.data | default([]) %}
{% set games = (all | selectattr('homeTeam.abbreviation', 'eq', team) | list)
+ (all | selectattr('awayTeam.abbreviation', 'eq', team) | list) %}
{% set live = games | selectattr('status.state', 'eq', 'in') | list %}
{% set pre = games | selectattr('status.state', 'eq', 'pre') | sort(attribute='date') | list %}
{% set post = games | selectattr('status.state', 'eq', 'post') | sort(attribute='date', reverse=true) | list %}
{% set g = (live + pre + post) | first | default(none) %}
{% if g and g.date %}{{ g.date[:16] ~ ':00+00:00' }}{% else %}unknown{% endif %}
automation:
- id: rsa_refresh_while_live
alias: "Sports: refresh scores every 2 min while the game is on"
mode: single
triggers:
- trigger: time_pattern
minutes: "/2"
conditions:
- condition: template
value_template: >-
{% set s = states('sensor.rsa_game_state') %}
{% set start = states('sensor.rsa_game_start') | as_datetime(none) %}
{{ s == 'in' or (s == 'pre' and start is not none and now() >= start - timedelta(minutes=10)) }}
actions:
- action: homeassistant.update_entity
target:
entity_id: sensor.rsa_game_state
- id: rsa_refresh_idle
alias: "Sports: refresh the scoreboard every 2 hours"
mode: single
triggers:
- trigger: time_pattern
hours: "/2"
minutes: 1
- trigger: state
entity_id: input_text.rsa_team
conditions:
- condition: not
conditions:
- condition: state
entity_id: sensor.rsa_game_state
state: "in"
actions:
- action: homeassistant.update_entity
target:
entity_id: sensor.rsa_game_state
- id: rsa_game_final
alias: "Sports: notify the final score"
mode: queued
triggers:
- trigger: state
entity_id: sensor.rsa_game_state
from: "in"
to: "post"
actions:
- action: notify.notify
data:
title: "Final: {{ states('input_text.rsa_team') }} {{ states('sensor.rsa_opponent') }}"
message: >-
{{ states('input_text.rsa_team') }} {{ states('sensor.rsa_team_score') }} -
{{ states('sensor.rsa_opponent_score') }} ({{ states('sensor.rsa_game_detail') }})
# Goal light: best for low-scoring sports (NHL, NFL, soccer). Off by default; turn it on in
# Settings > Automations once you point the resource at that league.
- id: rsa_team_scored
alias: "Sports: my team scored (goal light)"
initial_state: false
mode: queued
triggers:
- trigger: state
entity_id: sensor.rsa_team_score
conditions:
- condition: state
entity_id: sensor.rsa_game_state
state: "in"
- condition: template
value_template: >-
{{ trigger.from_state.state | is_number and trigger.to_state.state | is_number
and trigger.to_state.state | int > trigger.from_state.state | int }}
actions:
- action: notify.notify
data:
title: "{{ states('input_text.rsa_team') }} scored!"
message: >-
{{ states('input_text.rsa_team') }} {{ states('sensor.rsa_team_score') }} -
{{ states('sensor.rsa_opponent_score') }} {{ states('sensor.rsa_opponent') }}
({{ states('sensor.rsa_game_detail') }})
# Flash a light, play a horn on a speaker or run a scene here.
4. Test the templates
The repo renders every sensor template with Jinja2 against a real NHL scoreboard response (a live game, a final, an upcoming game and no game). In Home Assistant, check the automations in Developer tools > Template.
Terminal
pip install jinja2 pyyaml
python -m unittest discover -s tests -v5. Quota
Every refresh is one call whatever the number of sensors. The 2-hour idle refresh is about 360 calls a month and one NBA or NHL game about 80, so one team is roughly 1,500 calls a month (Starter, 10,000 calls). On the Free plan (125 calls a month after the first month), set the idle refresh to every 12 hours and turn off the live refresh: you still get the next game and final scores. Data is typically 20-30 seconds behind live play.
Endpoints used
GET /sports/{sport}/leagues/{league}/events
Full reference: docs · OpenAPI · what each endpoint returns, by league
FAQ
- Do I need HACS or a custom integration?
- No. It uses the built-in RESTful and template integrations, configured in one YAML package.
- Why is the state "none" between games?
- The scoreboard covers the current window (roughly today and the next day), so your team's next game appears when it enters that window.
- Can I follow two teams?
- Copy the package with a second input_text and new unique_ids, or point a second rest resource at another league. Each resource refresh is one call.