I just wanted to share a football dashboard I made that lets me track 3 teams that I follow along with a few specific players as well. All the data is being pulled from the ESPN api
sensor.yaml code
- platform: rest
name: EPL Standings
unique_id: epl_standings
resource: https://site.api.espn.com/apis/v2/sports/soccer/eng.1/standings
scan_interval: 3600
value_template: "{{ value_json.children[0].standings.entries | count }}"
json_attributes_path: "$.children[0].standings"
json_attributes:
- entries
- platform: rest
name: Championship Standings
unique_id: championship_standings
resource: https://site.api.espn.com/apis/v2/sports/soccer/eng.2/standings
scan_interval: 3600
value_template: "{{ value_json.children[0].standings.entries | count }}"
json_attributes_path: "$.children[0].standings"
json_attributes:
- entries
- platform: rest
name: Wrexham Squad
unique_id: wrexham_squad
resource: https://site.api.espn.com/apis/site/v2/sports/soccer/eng.2/teams/352/roster
scan_interval: 21600
value_template: "{{ value_json.athletes | count }}"
json_attributes:
- athletes
- platform: rest
name: Charlton Squad
unique_id: charlton_squad
resource: https://site.api.espn.com/apis/site/v2/sports/soccer/eng.2/teams/372/roster
scan_interval: 21600
value_template: "{{ value_json.athletes | count }}"
json_attributes:
- athletes
- platform: rest
name: Rotherham Squad
unique_id: rotherham_squad
resource: https://site.api.espn.com/apis/site/v2/sports/soccer/eng.4/teams/402/roster
scan_interval: 21600
value_template: "{{ value_json.athletes | count }}"
json_attributes:
- athletes
Standing tables
type: horizontal-stack
cards:
- type: custom:flex-table-card
title: Premier League
card_mod:
style: |
ha-card {
position: relative;
overflow: hidden;
}
ha-card::before {
content: "";
position: absolute;
inset: 0;
background: url("https://a.espncdn.com/i/leaguelogos/soccer/500-dark/23.png")
no-repeat center 55% / 46%;
opacity: 0.06;
filter: grayscale(1) brightness(2.2);
pointer-events: none;
z-index: 0;
}
ha-card > div {
position: relative;
z-index: 1;
}
#flextbl tr {
background: transparent;
}
#flextbl tr:nth-child(even) {
background: rgba(255, 255, 255, 0.03);
}
#flextbl tr { position: relative; }
#flextbl tr td { overflow: visible; position: static; }
#flextbl tr:has(.zone) td { padding-bottom: 18px; }
#flextbl tr td,
thead th {
padding-left: 0.25em;
padding-right: 0.25em;
}
#flextbl tr td:nth-child(n + 3),
thead th:nth-child(n + 3) {
width: 2.2em;
white-space: nowrap;
}
.zone {
position: absolute;
left: 8px;
right: 8px;
bottom: 3px;
display: flex;
align-items: center;
gap: 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
pointer-events: none;
}
.zone::before,
.zone::after {
content: "";
flex: 1;
border-top: 1px solid currentColor;
opacity: 0.4;
}
#flextbl tr:has([data-team="NEW"]) td {
background: rgba(45, 175, 250, 0.15);
font-weight: 700;
}
entities:
include: sensor.epl_standings
columns:
- name: "#"
data: entries
modify: >-
(() => { const all = this.entity.attributes.entries; const z = e =>
e.note ? e.note.description.toLowerCase() : ''; const i =
all.findIndex(e => e.team.id === x.team.id); const rank =
x.stats.find(s => s.name === 'rank').displayValue; if (i < 0 || i ===
all.length - 1 || z(all[i]) === z(all[i + 1])) return rank; const n =
all[i].note || all[i + 1].note; return rank + `<div class="zone"
style="color:#${n.color.replace(/#/g,
'')}"><span>${n.description}</span></div>`; })()
- name: Club
data: entries
modify: >-
`<span data-team="${x.team.abbreviation}"><img
src="${x.team.logos[0].href}" height="20"
style="vertical-align:middle;margin-right:8px">${x.team.shortDisplayName}</span>`
- name: GP
data: entries
align: center
modify: x.stats.find(s => s.name === 'gamesPlayed').displayValue
- name: W
data: entries
align: center
modify: x.stats.find(s => s.name === 'wins').displayValue
- name: D
data: entries
align: center
modify: x.stats.find(s => s.name === 'ties').displayValue
- name: L
data: entries
align: center
modify: x.stats.find(s => s.name === 'losses').displayValue
- name: GD
data: entries
align: center
modify: x.stats.find(s => s.name === 'pointDifferential').displayValue
- name: P
data: entries
align: center
modify: x.stats.find(s => s.name === 'points').displayValue
- type: custom:flex-table-card
title: Championship
card_mod:
style: |
ha-card {
position: relative;
overflow: hidden;
}
ha-card::before {
content: "";
position: absolute;
inset: 0;
background: url("https://a.espncdn.com/i/leaguelogos/soccer/500-dark/24.png")
no-repeat center 55% / 46%;
opacity: 0.06;
filter: grayscale(1) brightness(2.2);
pointer-events: none;
z-index: 0;
}
ha-card > div {
position: relative;
z-index: 1;
}
#flextbl tr {
background: transparent;
}
#flextbl tr:nth-child(even) {
background: rgba(255, 255, 255, 0.03);
}
#flextbl tr { position: relative; }
#flextbl tr td { overflow: visible; position: static; }
#flextbl tr:has(.zone) td { padding-bottom: 18px; }
#flextbl tr td,
thead th {
padding-left: 0.25em;
padding-right: 0.25em;
}
#flextbl tr td:nth-child(n + 3),
thead th:nth-child(n + 3) {
width: 2.2em;
white-space: nowrap;
}
.zone {
position: absolute;
left: 8px;
right: 8px;
bottom: 3px;
display: flex;
align-items: center;
gap: 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
pointer-events: none;
}
.zone::before,
.zone::after {
content: "";
flex: 1;
border-top: 1px solid currentColor;
opacity: 0.4;
}
#flextbl tr:has([data-team="WXM"]) td {
background: rgba(214, 30, 45, 0.15);
font-weight: 700;
}
#flextbl tr:has([data-team="WHU"]) td {
background: rgba(122, 38, 58, 0.18);
font-weight: 700;
}
entities:
include: sensor.championship_standings
columns:
- name: "#"
data: entries
modify: >-
(() => { const all = this.entity.attributes.entries; const z = e =>
e.note ? e.note.description.toLowerCase() : ''; const i =
all.findIndex(e => e.team.id === x.team.id); const rank =
x.stats.find(s => s.name === 'rank').displayValue; if (i < 0 || i ===
all.length - 1 || z(all[i]) === z(all[i + 1])) return rank; const n =
all[i].note || all[i + 1].note; return rank + `<div class="zone"
style="color:#${n.color.replace(/#/g,
'')}"><span>${n.description}</span></div>`; })()
- name: Club
data: entries
modify: >-
`<span data-team="${x.team.abbreviation}"><img
src="${x.team.logos[0].href}" height="20"
style="vertical-align:middle;margin-right:8px">${x.team.shortDisplayName}</span>`
- name: GP
data: entries
align: center
modify: x.stats.find(s => s.name === 'gamesPlayed').displayValue
- name: W
data: entries
align: center
modify: x.stats.find(s => s.name === 'wins').displayValue
- name: D
data: entries
align: center
modify: x.stats.find(s => s.name === 'ties').displayValue
- name: L
data: entries
align: center
modify: x.stats.find(s => s.name === 'losses').displayValue
- name: GD
data: entries
align: center
modify: x.stats.find(s => s.name === 'pointDifferential').displayValue
- name: P
data: entries
align: center
modify: x.stats.find(s => s.name === 'points').displayValue
grid_options:
columns: full
rows: auto
Player tables
type: horizontal-stack
cards:
- type: custom:flex-table-card
title: Wrexham Squad
card_mod:
style: |
ha-card {
position: relative;
overflow: hidden;
}
ha-card::before {
content: "";
position: absolute;
inset: 0;
background: url("https://a.espncdn.com/i/teamlogos/soccer/500/352.png")
no-repeat center 55% / 42%;
opacity: 0.07;
filter: grayscale(1) brightness(1.8);
pointer-events: none;
z-index: 0;
}
ha-card > div {
position: relative;
z-index: 1;
}
#flextbl tr {
background: transparent;
}
#flextbl tr {
position: relative;
}
#flextbl tr td {
position: static;
overflow: visible;
}
#flextbl tr:has(.zone) td {
padding-top: 18px;
}
.zone {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 16px;
padding: 3px 8px 0;
box-sizing: border-box;
background: var(--ha-card-background, var(--card-background-color));
display: flex;
align-items: center;
gap: 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--secondary-text-color);
pointer-events: none;
}
.zone::before,
.zone::after {
content: "";
flex: 1;
border-top: 1px solid currentColor;
opacity: 0.4;
}
#flextbl tr:nth-child(even) {
background: rgba(255, 255, 255, 0.03);
}
#flextbl tr td,
thead th {
padding-left: 0.25em;
padding-right: 0.25em;
}
#flextbl tr td:first-child,
thead th:first-child {
width: 2.2em;
}
#flextbl tr td:nth-child(n + 3),
thead th:nth-child(n + 3) {
width: 2.6em;
white-space: nowrap;
}
#flextbl tr:has([data-player="189605"]) td,
#flextbl tr:has([data-player="182338"]) td,
#flextbl tr:has([data-player="17861"]) td {
background: rgba(214, 30, 45, 0.15);
font-weight: 700;
}
entities:
include: sensor.wrexham_squad
columns:
- name: "#"
data: athletes
align: center
modify: >-
(() => { const all = this.entity.attributes.athletes; const L = { G:
'Goalkeepers', D: 'Defenders', M: 'Midfielders', F: 'Forwards' };
const pos = a => a.position.abbreviation; const i = all.findIndex(a =>
a.id === x.id); const n = x.jersey || 'β'; if (i > 0 && pos(all[i -
1]) === pos(x)) return n; return `<div class="zone"><span>${L[pos(x)]
|| x.position.displayName}</span></div>${n}`; })()
- name: Player
data: athletes
modify: >-
(() => { const f = x.flag ? `<img src="${x.flag.href}" height="12"
style="vertical-align:middle;margin-right:6px;border-radius:2px">` :
''; return `<span data-player="${x.id}">${f}${x.displayName}</span>`;
})()
- name: Age
data: athletes
align: center
modify: x.age || 'β'
- name: APP
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories || []).flatMap(c
=> c.stats).find(s => s.name === 'appearances'); return s ?
s.displayValue : 'β'; })()
- name: G
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories || []).flatMap(c
=> c.stats).find(s => s.name === 'totalGoals'); return s ?
s.displayValue : 'β'; })()
- name: A
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories || []).flatMap(c
=> c.stats).find(s => s.name === 'goalAssists'); return s ?
s.displayValue : 'β'; })()
- name: SHOT
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories || []).flatMap(c
=> c.stats).find(s => s.name === 'totalShots'); return s ?
s.displayValue : 'β'; })()
- name: SOG
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories || []).flatMap(c
=> c.stats).find(s => s.name === 'shotsOnTarget'); return s ?
s.displayValue : 'β'; })()
- name: YC
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories || []).flatMap(c
=> c.stats).find(s => s.name === 'yellowCards'); return s ?
s.displayValue : 'β'; })()
- name: RC
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories || []).flatMap(c
=> c.stats).find(s => s.name === 'redCards'); return s ?
s.displayValue : 'β'; })()
- type: vertical-stack
cards:
- type: custom:flex-table-card
title: Arthur Okonkwo
strict: true
card_mod:
style: |
ha-card {
position: relative;
overflow: hidden;
}
ha-card::before {
content: "";
position: absolute;
inset: 0;
background: url("https://a.espncdn.com/i/teamlogos/soccer/500/372.png")
no-repeat right -10% center / auto 130%;
opacity: 0.08;
filter: grayscale(1) brightness(1.8);
pointer-events: none;
z-index: 0;
}
ha-card > div {
position: relative;
z-index: 1;
}
#flextbl tr {
position: relative;
background: transparent;
}
#flextbl tr td {
position: static;
overflow: visible;
padding-left: 0.25em;
padding-right: 0.25em;
}
thead th {
padding-left: 0.25em;
padding-right: 0.25em;
}
#flextbl tr td,
thead th {
width: 2.2em;
white-space: nowrap;
}
#flextbl tr td:nth-child(2),
thead th:nth-child(2),
#flextbl tr td:nth-child(3),
thead th:nth-child(3) {
width: 3.6em;
}
#flextbl tr:has(.zone) td {
padding-top: 20px;
}
.zone {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 16px;
padding: 3px 8px 0;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--secondary-text-color);
pointer-events: none;
}
.zone::before,
.zone::after {
content: "";
flex: 1;
border-top: 1px solid currentColor;
opacity: 0.4;
}
entities:
include: sensor.charlton_squad
columns:
- name: Age
data: athletes
align: center
modify: >-
(() => { if (x.id !== '280554') return null; const f = x.flag ?
`<img src="${x.flag.href}" height="11"
style="vertical-align:middle;margin-right:5px;border-radius:2px">`
: ''; return `<div class="zone"><span>${f}#${x.jersey} Β·
${x.position.displayName} Β· Charlton Athletic</span></div>${x.age
|| 'β'}`; })()
- name: HT
data: athletes
align: center
modify: "x.height ? (x.height * 0.0254).toFixed(2) + ' m' : 'β'"
- name: WT
data: athletes
align: center
modify: "x.weight ? Math.round(x.weight * 0.45359237) + ' kg' : 'β'"
- name: APP
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'appearances');
return s ? s.displayValue : 'β'; })()
- name: G
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'totalGoals');
return s ? s.displayValue : 'β'; })()
- name: A
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'goalAssists');
return s ? s.displayValue : 'β'; })()
- name: SHOT
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'totalShots');
return s ? s.displayValue : 'β'; })()
- name: SOG
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'shotsOnTarget');
return s ? s.displayValue : 'β'; })()
- name: YC
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'yellowCards');
return s ? s.displayValue : 'β'; })()
- name: RC
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'redCards'); return
s ? s.displayValue : 'β'; })()
- type: custom:flex-table-card
title: Paul Mullin
strict: true
card_mod:
style: |
ha-card {
position: relative;
overflow: hidden;
}
ha-card::before {
content: "";
position: absolute;
inset: 0;
background: url("https://a.espncdn.com/i/teamlogos/soccer/500/402.png")
no-repeat right -10% center / auto 130%;
opacity: 0.08;
filter: grayscale(1) brightness(1.8);
pointer-events: none;
z-index: 0;
}
ha-card > div {
position: relative;
z-index: 1;
}
#flextbl tr {
position: relative;
background: transparent;
}
#flextbl tr td {
position: static;
overflow: visible;
padding-left: 0.25em;
padding-right: 0.25em;
}
thead th {
padding-left: 0.25em;
padding-right: 0.25em;
}
#flextbl tr td,
thead th {
width: 2.2em;
white-space: nowrap;
}
#flextbl tr td:nth-child(2),
thead th:nth-child(2),
#flextbl tr td:nth-child(3),
thead th:nth-child(3) {
width: 3.6em;
}
#flextbl tr:has(.zone) td {
padding-top: 20px;
}
.zone {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 16px;
padding: 3px 8px 0;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--secondary-text-color);
pointer-events: none;
}
.zone::before,
.zone::after {
content: "";
flex: 1;
border-top: 1px solid currentColor;
opacity: 0.4;
}
entities:
include: sensor.rotherham_squad
columns:
- name: Age
data: athletes
align: center
modify: >-
(() => { if (x.id !== '11547') return null; const f = x.flag ?
`<img src="${x.flag.href}" height="11"
style="vertical-align:middle;margin-right:5px;border-radius:2px">`
: ''; return `<div class="zone"><span>${f}#${x.jersey} Β·
${x.position.displayName} Β· Rotherham United</span></div>${x.age
|| 'β'}`; })()
- name: HT
data: athletes
align: center
modify: "x.height ? (x.height * 0.0254).toFixed(2) + ' m' : 'β'"
- name: WT
data: athletes
align: center
modify: "x.weight ? Math.round(x.weight * 0.45359237) + ' kg' : 'β'"
- name: APP
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'appearances');
return s ? s.displayValue : 'β'; })()
- name: G
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'totalGoals');
return s ? s.displayValue : 'β'; })()
- name: A
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'goalAssists');
return s ? s.displayValue : 'β'; })()
- name: SHOT
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'totalShots');
return s ? s.displayValue : 'β'; })()
- name: SOG
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'shotsOnTarget');
return s ? s.displayValue : 'β'; })()
- name: YC
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'yellowCards');
return s ? s.displayValue : 'β'; })()
- name: RC
data: athletes
align: center
modify: >-
(() => { const s = (x.statistics?.splits?.categories ||
[]).flatMap(c => c.stats).find(s => s.name === 'redCards'); return
s ? s.displayValue : 'β'; })()
grid_options:
columns: full
1 Like

