Football dashboard

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