Real-time football data in Home Assistant via ESPN, with optional API-Football support for users who provide their own API key.
Built on ideas from Calcio Live by @Bobsilvio
HACS default store: submission pending — once approved, search for Soccer Live directly in HACS.
Until then, add as a custom repository:
- In HACS → ⋮ → Custom repositories → add
https://github.com/rononline/soccerlive, category: Integration - Install Soccer Live via HACS
- Restart Home Assistant
- Go to Settings → Integrations → Add Integration and search for
Soccer Live
Also install the companion cards: Soccer Live Card
Sensors are created automatically depending on your selection:
| Sensor type | Name pattern | Description |
|---|---|---|
team_match |
soccer_live_next_{competition}_{team} |
Next / current match for a team |
team_matches |
soccer_live_all_{competition}_{team} |
All matches for a team (competition-specific) |
team_matches_mixed |
soccer_live_all_mixed_{team} |
All matches for a team (all competitions) |
match_day |
soccer_live_all_{competition} |
All matches in a competition |
standings |
soccer_live_standings_{competition} |
League standings |
top_scorers |
soccer_live_scorers_{competition} |
Top scorers for a competition (auto-created) |
bracket |
soccer_live_bracket_{competition} |
Knockout bracket (auto-created for cup competitions) |
all_matches_today |
soccer_live_all_today |
All matches worldwide today |
news |
soccer_live_news_{competition} |
News feed for a competition |
Configure via Settings → Devices & Services → Soccer Live → Configure:
| Option | Default | Description |
|---|---|---|
scan_interval |
3 minutes |
Normal polling interval when no match is live. |
live_scan_interval |
60 seconds |
Extra refresh interval while a match is live (30 / 45 / 60 / 90 / 120 seconds). Use 30 seconds for faster goal/card updates when your API quota allows it. |
enable_summary_enrichment |
true |
Fetch extra match details. ESPN uses the summary endpoint; API-Football uses fixture events, statistics and lineups. Disable to reduce API calls. |
include_friendlies |
true |
Include friendlies when using API-Football fixture data. |
api_football_season |
0 (auto) |
API-Football season to query. For standings/top scorers, auto mode uses the previous season before August. |
max_matches |
0 (unlimited) |
Limit the number of matches stored per sensor (5 / 10 / 15 / 20 / 30). Useful to reduce state size on large sensors. |
ESPN is the default provider and does not require an API key. API-Football can be selected during setup and requires your own API-Football key.
Current API-Football support:
- Team fixtures:
team_match,team_matches,team_matches_mixed - Competition fixtures:
match_day all_matches_todaystandingstop_scorers- Optional match enrichment for
team_match,team_matchesandteam_matches_mixedvia fixture events, statistics and lineups
API-Football news and knockout brackets are not supported yet. News and bracket sensors remain ESPN-only.
The integration caches API-Football calls to reduce quota use:
- Main fixture/standings/scorers responses are shared by URL for up to 60 seconds. While a match is live, this cache follows
live_scan_intervalwhen that value is lower than 60 seconds. - Fixture events are cached for 30 seconds.
- Fixture statistics and lineups are cached for 5 minutes.
- Predictions are cached for 6 hours, standings for 6 hours, injuries for 3 hours, and odds for 1 hour.
- The
api_football_quotadiagnostic attribute is refreshed through API-Football/statusevery 30 minutes.
The config-entry diagnostics (Settings → Devices & services → the entry → Download diagnostics) include an api_football section for monitoring API usage: per-endpoint call counts and cache hits, the last successful update and last HTTP status per endpoint, the endpoint cache size, and a rate_limited_at marker (set on an HTTP 429). This helps explain when a section is temporarily missing.
For API-Football team sensors, search teams directly by name during setup. Labels include the API-Football ID, because these IDs are different from ESPN IDs.
The large, high-churn attributes (matches, previous_matches, upcoming_matches, next_match, the schedule_* lists, standings_groups, scorers, articles, rounds, head_to_head, league_info and the last_*_event payloads) are automatically excluded from the recorder history — the integration marks them as unrecorded, so the Home Assistant database stays small out of the box. The sensor state (the score summary) and small scalar attributes are still recorded, so state history keeps working.
If you'd rather drop the sensors from history entirely, you can still add this to configuration.yaml:
recorder:
exclude:
entity_globs:
- sensor.soccer_live_*Usually not needed: the ID is filled in automatically when you select a competition and team. Only required for manual entry.
- Via ESPN website: open the team page on
espn.com— the ID is the number in the URL:
espn.com/soccer/team/_/id/9723/portland-timbers→ Team ID = 9723 - Via ESPN API:
https://site.api.espn.com/apis/site/v2/sports/soccer/all/teams
The integration can automatically send push notifications when goals, cards, or match events occur. Configure this in the integration options:
- Go to Settings → Devices & Services → Soccer Live
- Click Options (gear icon)
- Set Notify Service to your desired notification service:
notify.mobile_app_yourphone— iOS/Android Home Assistant appnotify.telegram— Telegram (requires notify.telegram service)notify.pushbullet— PushBullet (requires notify.pushbullet service)
- Save
Notifications sent for:
- ⚽ Goal scored
- 🟨 Yellow card issued
- 🟥 Red card issued
- 🔄 Substitution made
- 🏁 Match finished
Example notification: "⚽ GOAL! Kramer (34') — Feyenoord 1 - 0 Sparta Rotterdam"
If you prefer more control, use Home Assistant automations with the exposed events instead:
alias: Football - Match starting soon
trigger:
- platform: template
value_template: >
{{ state_attr('sensor.soccer_live_next_ned_1_feyenoord_rotterdam', 'next_match_minutes_until') == 15 }}
condition:
- condition: template
value_template: >
{{ state_attr('sensor.soccer_live_next_ned_1_feyenoord_rotterdam', 'next_match_status') == 'pre' }}
action:
- service: notify.mobile_app_my_phone
data:
title: "⚽ Match starts in 15 min!"
message: >
{{ state_attr('sensor.soccer_live_next_ned_1_feyenoord_rotterdam', 'next_match_home_team') }}
vs {{ state_attr('sensor.soccer_live_next_ned_1_feyenoord_rotterdam', 'next_match_away_team') }}
mode: singlealias: Football - Match started
trigger:
- platform: event
event_type: soccer_live_match_started
condition:
- condition: template
value_template: >
{{ trigger.event.data.home_team == 'Feyenoord Rotterdam'
or trigger.event.data.away_team == 'Feyenoord Rotterdam' }}
action:
- service: notify.mobile_app_my_phone
data:
title: "🟢 Match started!"
message: >
{{ trigger.event.data.home_team }} vs {{ trigger.event.data.away_team }}
— {{ trigger.event.data.venue }}
mode: singlealias: Football - Goal notification
trigger:
- platform: event
event_type: soccer_live_goal
event_data:
team: Feyenoord Rotterdam # omit to receive for all teams
action:
- service: notify.mobile_app_my_phone
data:
title: "⚽ GOAL!"
message: >
{% set m = trigger.event.data.minute %}
{{ trigger.event.data.player }}{{ " (" ~ m ~ "')" if m and m != 'N/A' else '' }} —
{{ trigger.event.data.home_team }} {{ trigger.event.data.home_score }}
- {{ trigger.event.data.away_score }} {{ trigger.event.data.away_team }}
mode: queuedalias: Football - Card notification
trigger:
- platform: event
event_type: soccer_live_yellow_card
- platform: event
event_type: soccer_live_red_card
action:
- service: notify.mobile_app_my_phone
data:
title: >
{{ '🟥 RED CARD!' if trigger.event_type == 'soccer_live_red_card' else '🟨 Yellow card' }}
message: >
{{ trigger.event.data.player }} ({{ trigger.event.data.minute }}')
— {{ trigger.event.data.home_team }} vs {{ trigger.event.data.away_team }}
mode: queuedalias: Football - Substitution
trigger:
- platform: event
event_type: soccer_live_substitution
action:
- service: notify.mobile_app_my_phone
data:
title: "🔄 Substitution"
message: >
{{ trigger.event.data.player }} ({{ trigger.event.data.team }})
minute {{ trigger.event.data.minute }}'
— {{ trigger.event.data.home_team }} vs {{ trigger.event.data.away_team }}
mode: queuedalias: Football - Final score
trigger:
- platform: event
event_type: soccer_live_match_finished
condition:
- condition: or
conditions:
- condition: template
value_template: "{{ trigger.event.data.home_team == 'Feyenoord Rotterdam' }}"
- condition: template
value_template: "{{ trigger.event.data.away_team == 'Feyenoord Rotterdam' }}"
action:
- service: notify.mobile_app_my_phone
data:
title: "⏹️ Full time"
message: >
{{ trigger.event.data.home_team }} {{ trigger.event.data.home_score }}
- {{ trigger.event.data.away_score }} {{ trigger.event.data.away_team }}
{% if trigger.event.data.goal_scorers_str != 'N/A' %}
· Scorers: {{ trigger.event.data.goal_scorers_str }}
{% endif %}
mode: single# Eredivisie events only
condition:
- condition: template
value_template: "{{ trigger.event.data.competition_code == 'ned.1' }}"
# Or by league name
condition:
- condition: template
value_template: "{{ trigger.event.data.league_name == 'Dutch Eredivisie' }}"alias: Football - High scoring match
trigger:
- platform: event
event_type: soccer_live_goal
condition:
- condition: template
value_template: >
{{ (trigger.event.data.home_score | int(0)) + (trigger.event.data.away_score | int(0)) >= 4 }}
action:
- service: notify.mobile_app_my_phone
data:
title: "🔥 High-scoring match!"
message: >
{{ trigger.event.data.home_team }} {{ trigger.event.data.home_score }}
- {{ trigger.event.data.away_score }} {{ trigger.event.data.away_team }}
mode: queuedalias: Football - Match ended in draw
trigger:
- platform: event
event_type: soccer_live_match_finished
condition:
- condition: template
value_template: "{{ trigger.event.data.home_score == trigger.event.data.away_score }}"
action:
- service: notify.mobile_app_my_phone
data:
title: "⚽ Draw!"
message: >
{{ trigger.event.data.home_team }} {{ trigger.event.data.home_score }}
- {{ trigger.event.data.away_score }} {{ trigger.event.data.away_team }}
mode: singlealias: Football - Today's fixtures
trigger:
- platform: time
at: "10:00:00"
condition:
- condition: template
value_template: "{{ state_attr('sensor.soccer_live_all_today', 'upcoming_matches_count') | int(0) > 0 }}"
action:
- service: notify.mobile_app_my_phone
data:
title: "⚽ Today's matches"
message: >
{{ (state_attr('sensor.soccer_live_all_today', 'matches') or [])[0:3] | map(attribute='home_team') | list | join(', ') }}
mode: singlealias: Football - Feyenoord goal by Giménez
trigger:
- platform: event
event_type: soccer_live_goal
event_data:
team: Feyenoord Rotterdam
condition:
- condition: template
value_template: "{{ 'Gimenez' in trigger.event.data.player }}"
action:
- service: notify.mobile_app_my_phone
data:
title: "⚽ GIMÉNEZ SCORES!"
message: >
{{ trigger.event.data.player }} ({{ trigger.event.data.minute }}')
— {{ trigger.event.data.home_team }} {{ trigger.event.data.home_score }}
- {{ trigger.event.data.away_score }} {{ trigger.event.data.away_team }}
mode: queuedalias: Football - Red card alert
trigger:
- platform: event
event_type: soccer_live_red_card
action:
- service: notify.mobile_app_my_phone
data:
title: "🟥 RED CARD!"
message: >
{{ trigger.event.data.team }} down to 10 men
{{ trigger.event.data.player }} sent off ({{ trigger.event.data.minute }}')
{{ trigger.event.data.home_team }} {{ trigger.event.data.home_score }}
- {{ trigger.event.data.away_score }} {{ trigger.event.data.away_team }}
mode: queuedNext match (next_match_*):
home_team, away_team, home_score, away_score, date, datetime_iso, minutes_until, status (pre/in/post), clock, period, venue, home_logo, away_logo, home_form, away_form
Live match (live_match_*): same fields as next_match_*
Last match (last_match_*):
home_team, away_team, home_score, away_score, date, venue
Top-level next match extras:
next_match_week — competition week/round number (e.g. "Round 30")
Counters & booleans:
total_matches, live_matches_count, upcoming_matches_count, finished_matches_count, has_live_match, has_upcoming_match, has_recent_match
Schedule summary:
schedule_match_count, schedule_live_count, schedule_upcoming_count, schedule_recent_count, schedule_live_matches, schedule_upcoming_matches, schedule_recent_matches
Automation-friendly last event attributes:
last_event, last_event_type, last_event_timestamp, last_goal_event, last_card_event, last_match_started_event, last_match_finished_event
Health/debug attributes:
api_status, last_successful_update, last_error, request_count, last_request_time, sensor_type, start_date, end_date, provider, provider_capabilities, api_football_season, api_football_quota
Provider capabilities: the provider_capabilities attribute lists what the selected provider can supply, so cards and automations can adapt. ESPN: fixtures, scores, standings, top_scorers, news, brackets, lineups, statistics, head_to_head. API-Football also adds top_assists, predictions, odds, injuries and xg (but not news/brackets).
Each config entry also creates a calendar entity (calendar.soccer_live_<team>) with the team's/competition's fixtures as events (kick-off time, Home - Away title, venue as location, competition as description; finished matches include the score). It reuses the match data the sensors already fetch — no extra polling — so the fixtures show up in the Home Assistant calendar and can drive time-based automations (e.g. a Calendar trigger a set time before an event starts).
| Event | Fired when | Key fields |
|---|---|---|
soccer_live_match_started |
Kick-off (pre → in) | home_team, away_team, venue, date, league_name, competition_code |
soccer_live_goal |
Goal scored | team, player, minute, home_score, away_score, league_name, competition_code |
soccer_live_yellow_card |
Yellow card | player, minute, team, home_team, away_team, league_name |
soccer_live_red_card |
Red card | player, minute, team, home_team, away_team, league_name |
soccer_live_substitution |
Substitution | player, minute, team, home_team, away_team, league_name |
soccer_live_match_finished |
Full time | home_score, away_score, goal_scorers, goal_scorers_str, league_name |
Example automation blueprints are available in blueprints/automation:
goal, yellow card, red card, substitution, match started, full time (final score)
and a configurable kick-off reminder (choose how many minutes before kick-off).
These attributes are guaranteed to be present when available. Card developers can rely on this structure.
| Attribute | Type | Description |
|---|---|---|
home_team / away_team |
string | Team names |
home_abbrev / away_abbrev |
string | Team abbreviations |
home_logo / away_logo |
string | Logo URLs |
home_color / away_color |
string | ESPN team colors when available |
home_score / away_score |
int|str | Score or N/A |
home_form / away_form |
string | Recent form string, e.g. WDWLW |
state |
string | pre / in / post |
date |
string | DD-MM-YYYY HH:MM (localized display) |
date_iso |
string | Raw ISO kickoff timestamp (used for kickoff-time weather) |
venue / venue_city |
string | Stadium info |
league_name / league_logo |
string | Resolved league identity for mixed/all sensors |
competition_name / competition_logo |
string | Competition identity |
season_info |
string | Season phase slug, e.g. round-of-16 |
broadcasts |
list | TV/streaming channels |
neutral_site |
bool | Neutral venue |
attendance |
int | Stadium attendance |
links |
dict | ESPN links: stats, commentary, video, summary |
has_stats |
bool | Boxscore available |
has_commentary |
bool | Play-by-play available |
clock |
string | Match clock (live) |
league_info |
list | Competition metadata: name, abbreviation, logo_href, startDate, endDate |
home_statistics / away_statistics |
dict | Per-team stats; includes expectedGoals (xG) when the provider supplies it |
These are attached to the nearest upcoming match, and only when real data exists (they populate close to competitive matches — generally not friendlies or far-out fixtures):
| Attribute | Type | Description |
|---|---|---|
prediction |
dict | percent_home / percent_draw / percent_away (int %), advice, winner_name, winner_comment |
injuries_home / injuries_away |
list | Absentees: player, reason, type, suspended (bool) |
odds |
dict | Averaged Match-Winner odds: home / draw / away (float), bookmaker_count |
home_rank / away_rank |
int | League position (structured, so cards can localize the label) |
home_points / away_points |
int | League points |
The 10 most-recently finished matches for a team sensor. Subset of the full match object:
| Attribute | Type | Description |
|---|---|---|
home_team / away_team |
string | Team names |
home_abbrev / away_abbrev |
string | Abbreviations |
home_logo / away_logo |
string | Logo URLs |
home_color / away_color |
string | Team colors |
home_score / away_score |
int|str | Final scores |
state |
string | Always post |
date |
string | DD-MM-YYYY HH:MM |
league_name |
string | Competition name |
season_info |
string | Season phase slug (e.g. round-of-16) |
Up to 4 upcoming/live matches after the primary next match. Subset of the full match object:
| Attribute | Type | Description |
|---|---|---|
home_team / away_team |
string | Team names |
home_abbrev / away_abbrev |
string | Abbreviations |
home_logo / away_logo |
string | Logo URLs |
home_color / away_color |
string | Team colors |
home_score / away_score |
int|str | Scores (live) |
state |
string | pre or in |
date |
string | DD-MM-YYYY HH:MM |
clock |
string | Match clock (live) |
event_id |
string | ESPN event ID |
head_to_head |
list | Last 3 H2H matches |
home_form / away_form |
string | Recent form string, e.g. WDWLW — empty string when ESPN does not supply form data |
league_name |
string | Competition name |
ESPN uses different data shapes per endpoint. The parser resolves per-match league identity in this order:
competition.leagueor event-levelleague- top-level
leagues[]matched by league ID - the
l:part from competition/eventuid altGameNotebefore the comma, e.g.FIFA World Cup, Group F- curated logo overrides for common international competitions
The integration intentionally does not guess arbitrary ESPN CDN logo URLs from league IDs, because many IDs do not match the logo file number.
Available after kick-off when enable_summary_enrichment is on:
| Attribute | Type | Description |
|---|---|---|
home_statistics / away_statistics |
dict | Raw ESPN stat keys → values |
key_events |
list | Goals, cards, subs with clock, type, team, athletes |
lineup_home / lineup_away |
list | Players with position, jersey, headshot |
head_to_head |
list | Recent H2H matches |
home_standing_summary / away_standing_summary |
string | League position |
home_record_summary / away_record_summary |
string | Season record |
last_five_home / last_five_away |
string | Form string (e.g. WDWLW) |
next_match_home_team, next_match_away_team, next_match_home_abbrev, next_match_away_abbrev, next_match_home_color, next_match_away_color, team_colors, next_match_date, next_match_datetime_iso, next_match_minutes_until, next_match_status, next_match_venue, next_match_broadcasts, next_match_broadcast_count, next_match_event_id, next_match_event_count, next_match_h2h_count, next_match_has_stats, next_match_has_commentary, next_match_links, next_match_attendance, next_match_neutral_site
live_match_home_team, live_match_away_team, live_match_home_abbrev, live_match_away_abbrev, live_match_home_color, live_match_away_color, team_colors, live_match_date, live_match_status, live_match_venue, live_match_clock, live_match_event_id, live_match_event_count, live_match_h2h_count
schedule_live_matches, schedule_upcoming_matches and schedule_recent_matches contain compact match objects with: event_id, date, state, clock, team names/abbreviations/logos/colors, scores, venue, league_name, league_logo, season_info, and broadcasts.
last_event always contains the latest fired Soccer Live event payload plus event_type and timestamp. Typed convenience attributes are populated for the latest matching event category: last_goal_event, last_card_event, last_match_started_event, last_match_finished_event.
GPL-3.0 — data via ESPN public APIs or API-Football when configured with your own key.