Formula 1 Racing sensor

I have made a Formula 1 Sensor, a custom integration specifically designed to leverage Home Assistant for automations, notifications, and advanced setups based around Formula 1 race events.

What does F1 Sensor provide?

This integration fetches detailed and timely data from the Jolpica-F1 API, creating sensors:

  • sensor.f1_next_race – Next race information (location, schedule, timings).
  • sensor.f1_season_calendar – Complete current season calendar.
  • sensor.f1_driver_standings – Current driver championship standings.
  • sensor.f1_constructor_standings – Current constructor championship standings.
  • sensor.f1_weather - Current weather and race-time forecast at the next race location.
14 Likes

New pre-release adding result sensors

1 Like

Hey!
Those separate sensors are really powerful, they give you a lot of freedom to build your own dashboard cards. I built the Results card shown here and I’m working on the rest, all without any issues so far. You can click the chevron icon to expand the card and reveal the full race results. The dashboard cards update automatically, even if a race finishes after midnight. The current race is highlighted with a glow effect, and the cards automatically adapt their layout when a sprint race weekend is detected.

Here’s an overview of all the cards I’ve built so far:

  1. Next Grand Prix — shows the upcoming race with session schedule, countdown timer, circuit map and live weather for both current conditions and race time forecast
  2. Season Calendar — full 2026 race calendar with sprint indicators, next race highlight and expandable to show all 24 rounds
  3. Driver Standings — top 10 with team colors and points bar, expandable to all 22 drivers
  4. Constructor Standings — same style as driver standings, expandable to all 11 teams
  5. Last Race Results — full results with team colors, points, DNF status and expandable to all 22 drivers

Required custom cards
custom button card
TailwindCSS Template Card
Vertical Stack In Card
Calendar Card Pro
card-mod 4

1. Next Grand Prix
Shows the upcoming race with a session schedule, countdown timer, circuit map and live weather forecast for both current conditions and race time. Sessions that are live are highlighted in yellow. Past sessions are greyed out. The card automatically adapts when a sprint race weekend is detected, sprint sessions only appear when scheduled.

type: custom:vertical-stack-in-card
cards:
  - type: custom:tailwindcss-template-card
    content: |
      {% set s = 'sensor.f1_next_race' %}
      {% set race = state_attr(s, 'race_name') %}
      {% set circuit = state_attr(s, 'circuit_name') %}
      {% set locality = state_attr(s, 'circuit_locality') %}
      {% set country = state_attr(s, 'circuit_country') %}
      {% set flag = state_attr(s, 'country_flag_url') %}
      {% set race_start = state_attr(s, 'race_start') %}

      {% set now_ts = now().timestamp() %}
      {% set race_ts = as_timestamp(race_start) %}
      {% set diff = race_ts - now_ts %}
      {% set days = (diff / 86400) | int %}
      {% set hours = ((diff % 86400) / 3600) | int %}
      {% set mins = ((diff % 3600) / 60) | int %}

      {% if diff > 0 %}
        {% set status = "⏳ Over " ~ days ~ "d " ~ hours ~ "u " ~ mins ~ "m" %}
        {% set status_color = "#FFF200" %}
      {% elif diff > -5400 %}
        {% set status = "🏁 Race is bezig!" %}
        {% set status_color = "#E10600" %}
      {% else %}
        {% set status = "✅ Race afgelopen" %}
        {% set status_color = "#aaaaaa" %}
      {% endif %}

      <div style="padding: 2px;">

        <!-- Header -->
        <div style="text-align: center; border-bottom: 1px solid rgba(225,6,0,0.25); padding-bottom: 6px; margin-bottom: 12px;">
          <div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">🏎️ Volgende Grand Prix</div>
          <div style="display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 4px;">
            <img src="{{ flag }}" style="height: 18px; border-radius: 2px;">
            <span style="font-size: 14px; font-weight: bold; color: #ffffff;">{{ race }}</span>
          </div>
          <div style="font-size: 13px; color: #888888; margin-top: 2px;">{{ circuit }} · {{ locality }}, {{ country }}</div>
          <div style="font-size: 13px; font-weight: bold; margin-top: 4px; color: {{ status_color }};">{{ status }}</div>
        </div>

        <!-- Sessieschema -->
        <div style="display: flex; flex-direction: column; gap: 6px;">
          {% set sessions = [
            ('⚙️ VT1', state_attr(s, 'first_practice_start')),
            ('⚙️ VT2', state_attr(s, 'second_practice_start')),
            ('⚙️ VT3', state_attr(s, 'third_practice_start')),
            ('⚡ Sprint Kwal.', state_attr(s, 'sprint_qualifying_start')),
            ('⚡ Sprint', state_attr(s, 'sprint_start')),
            ('⏱️ Kwalificatie', state_attr(s, 'qualifying_start')),
            ('🏁 Race', state_attr(s, 'race_start')),
          ] %}

          {% for label, dt in sessions %}
            {% if dt %}
              {% set ts = as_timestamp(dt) %}
              {% set is_live = now_ts >= ts and now_ts <= (ts + 4500) %}
              {% set is_past = now_ts > (ts + 4500) %}
              <div style="display: flex; justify-content: space-between; align-items: center;
                          padding: 6px 10px; border-radius: 4px; height: 50px;
                          {% if is_live %}background: rgba(255,242,0,0.08); border: 1px solid #FFF200;
                          {% elif is_past %}background: rgba(255,255,255,0.04); border: 1px solid transparent;
                          {% else %}background: rgba(255,255,255,0.08); border: 1px solid transparent;{% endif %}">
                <span style="font-size: 15px; color: {% if is_live %}#FFF200{% elif is_past %}#555555{% else %}#cccccc{% endif %}; font-weight: {% if is_live %}bold{% else %}normal{% endif %};">
                  {{ label }}{% if is_live %} 🔴 LIVE{% endif %}
                </span>
                <span style="font-size: 13px; font-weight: bold; color: {% if is_live %}#FFF200{% elif is_past %}#555555{% else %}#888888{% endif %};">
                  {{ as_timestamp(dt) | timestamp_custom('%a %d %b · %H:%M') }}
                </span>
              </div>
            {% endif %}
          {% endfor %}
        </div>

      </div>
    card_mod:
      style: |
        ha-card {
          background: transparent !important;
          border: none !important;
          box-shadow: none !important;
        }
  - type: map
    entities:
      - entity: sensor.f1_circuit_locatie
    hours_to_show: 0
    default_zoom: 10
    theme_mode: auto
    aspect_ratio: "2"
    card_mod:
      style: |
        ha-card {
          height: 220px !important;
          border-radius: 0 !important;
          border: none !important;
          border-top: 1px solid rgba(225,6,0,0.25) !important;
          border-bottom: 1px solid rgba(225,6,0,0.25) !important;
          box-shadow: none !important;
        }
        .card-header { display: none !important; }
  - type: custom:tailwindcss-template-card
    content: >
      {% set s = 'sensor.f1_weather' %}

      {% set c_temp = state_attr(s, 'current_temperature') %}

      {% set c_hum = state_attr(s, 'current_humidity') %}

      {% set c_wind = state_attr(s, 'current_wind_speed') %}

      {% set c_gust = state_attr(s, 'current_wind_gusts') %}

      {% set c_dir = state_attr(s, 'current_wind_direction') %}

      {% set c_rain = state_attr(s, 'current_precipitation_probability') %}

      {% set c_cloud = state_attr(s, 'current_cloud_cover') %}

      {% set r_temp = state_attr(s, 'race_temperature') %}

      {% set r_hum = state_attr(s, 'race_humidity') %}

      {% set r_wind = state_attr(s, 'race_wind_speed') %}

      {% set r_gust = state_attr(s, 'race_wind_gusts') %}

      {% set r_dir = state_attr(s, 'race_wind_direction') %}

      {% set r_rain = state_attr(s, 'race_precipitation_probability') %}

      {% set r_cloud = state_attr(s, 'race_cloud_cover') %}


      {% macro beaufort(mps) %}{% set mps = mps | float %}{% if mps <= 0.3 %}0{%
      elif mps <= 1.5 %}1{% elif mps <= 3.3 %}2{% elif mps <= 5.4 %}3{% elif mps
      <= 7.9 %}4{% elif mps <= 10.7 %}5{% elif mps <= 13.8 %}6{% elif mps <=
      17.1 %}7{% elif mps <= 20.7 %}8{% else %}9+{% endif %}{% endmacro %}


      <div style="padding: 12px;">

        <!-- Header -->
        <div style="text-align: center; border-bottom: 1px solid rgba(225,6,0,0.25); padding-bottom: 6px; margin-bottom: 12px;">
          <div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">🌤️ Circuit Weer</div>
          <div style="font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 4px;">
            {{ state_attr('sensor.f1_next_race', 'circuit_locality') }},
            {{ state_attr('sensor.f1_next_race', 'circuit_country') }}
          </div>
        </div>

        <!-- Twee kolommen -->
        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">

          <!-- NU -->
          <div style="border-radius: 4px; padding: 10px; background: rgba(255,255,255,0.04); border: 1px solid transparent;">
            <div style="font-size: 13px; font-weight: bold; text-transform: uppercase; text-align: center; color: #888888; margin-bottom: 8px;">Nu</div>
            <div style="text-align: center; margin-bottom: 8px;">
              <span style="font-size: 28px; font-weight: bold; color: #ffffff;">{{ c_temp | round(1) }}</span>
              <span style="font-size: 14px; color: #888888;">°C</span>
            </div>
            <div style="display: flex; flex-direction: column; gap: 4px;">
              {% for icon, label, val in [
                ('💧', 'Vochtigheid', c_hum ~ '%'),
                ('🌧️', 'Regen', c_rain ~ '%'),
                ('☁️', 'Bewolking', c_cloud ~ '%'),
                ('💨', 'Wind', beaufort(c_wind) | trim ~ ' Bft ' ~ c_dir),
                ('💨', 'Windstoten', c_gust ~ ' m/s'),
              ] %}
              <div style="display: flex; justify-content: space-between; font-size: 13px;">
                <span style="color: #888888;">{{ icon }} {{ label }}</span>
                <span style="color: #cccccc; font-weight: bold;">{{ val }}</span>
              </div>
              {% endfor %}
            </div>
          </div>

          <!-- RACE -->
          <div style="border-radius: 4px; padding: 10px; background: rgba(225,6,0,0.06); border: 1px solid rgba(225,6,0,0.25);">
            <div style="font-size: 13px; font-weight: bold; text-transform: uppercase; text-align: center; color: #E10600; margin-bottom: 8px;">Race</div>
            <div style="text-align: center; margin-bottom: 8px;">
              <span style="font-size: 28px; font-weight: bold; color: #ffffff;">{{ r_temp | round(1) }}</span>
              <span style="font-size: 14px; color: #888888;">°C</span>
            </div>
            <div style="display: flex; flex-direction: column; gap: 4px;">
              {% for icon, label, val, warn in [
                ('💧', 'Vochtigheid', r_hum ~ '%', false),
                ('🌧️', 'Regen', r_rain ~ '%', r_rain | int > 30),
                ('☁️', 'Bewolking', r_cloud ~ '%', false),
                ('💨', 'Wind', beaufort(r_wind) | trim ~ ' Bft ' ~ r_dir, false),
                ('💨', 'Windstoten', r_gust ~ ' m/s', false),
              ] %}
              <div style="display: flex; justify-content: space-between; font-size: 13px;">
                <span style="color: #888888;">{{ icon }} {{ label }}</span>
                <span style="color: {% if warn %}#E10600{% else %}#cccccc{% endif %}; font-weight: bold;">{{ val }}</span>
              </div>
              {% endfor %}
            </div>
          </div>

        </div>

        <div style="text-align: center; font-size: 11px; color: #555555; margin-top: 12px;">Bron: open-meteo</div>
      </div>
    card_mod:
      style: |
        ha-card {
          background: transparent !important;
          border: none !important;
          box-shadow: none !important;
        }
card_mod:
  style: |
    ha-card {
      background: var(--ha-card-background, var(--card-background-color, #1e1e1e)) !important;
      border: 0px solid rgba(225,6,0,0.19) !important;
      border-radius: 12px !important;
      box-shadow: 0px 4px 24px rgba(225,6,0,0.12) !important;
      overflow: hidden;
    }

Required template sensor
add this to your configuration.yaml or a template package file

template:
  - sensor:
      - name: "F1 Circuit Locatie"
        unique_id: 39899dcb-87b4-4aef-8e7f-83741475e9e5
        state: "{{ state_attr('sensor.f1_next_race', 'circuit_name') }}"
        attributes:
          latitude: "{{ state_attr('sensor.f1_next_race', 'circuit_lat') }}"
          longitude: "{{ state_attr('sensor.f1_next_race', 'circuit_long') }}"
          country: "{{ state_attr('sensor.f1_next_race', 'circuit_country') }}"
          icon: mdi:map-marker

2. Season Calendar
Full 2026 race calendar powered by calendar-card-pro. Shows all sessions per race weekend with colour coded event types (race, qualifying, sprint, practice). The current race is highlighted with a pulse indicator. Tap the card to expand it to full screen.

entities:
  - entity: calendar.f1_season_calendar
    allowlist: Race$
    color: "#FFFFFF"
    accent_color: "#E10600"
    label: mdi:flag-checkered
    show_location: true
  - entity: calendar.f1_season_calendar
    allowlist: Qualifying$
    color: "#FFFFFF"
    accent_color: "#FFF200"
    label: mdi:timer-outline
    show_location: false
  - entity: calendar.f1_season_calendar
    allowlist: Sprint$
    color: "#FFFFFF"
    accent_color: "#FF8C00"
    label: mdi:lightning-bolt
    show_location: false
  - entity: calendar.f1_season_calendar
    allowlist: Sprint Qualifying$
    color: "#FFFFFF"
    accent_color: "#FFA500"
    label: mdi:timer-sand
    show_location: false
  - entity: calendar.f1_season_calendar
    allowlist: Practice
    color: "#AAAAAA"
    accent_color: "#444444"
    label: mdi:wrench-outline
    show_location: false
start_date: today
days_to_show: 275
compact_events_to_show: 5
compact_events_complete_days: true
filter_duplicates: true
language: nl
title: 🏎️ Formula 1 — 2026 Season
title_font_size: 22px
title_color: "#E10600"
background_color: var(--ha-card-background, var(--card-background-color,
vertical_line_width: 4px
event_spacing: 6px
additional_card_spacing: 8px
max_height: 650px
day_separator_width: 1px
day_separator_color: "#333333"
month_separator_width: 1px
month_separator_color: "#E1060060"
today_indicator: pulse
today_indicator_color: "#E10600"
weekday_font_size: 11px
weekday_color: "#888888"
day_font_size: 28px
day_color: "#FFFFFF"
month_font_size: 10px
month_color: "#E10600"
weekend_weekday_color: "#AAAAAA"
weekend_day_color: "#FFFFFF"
today_weekday_color: "#E10600"
today_day_color: "#E10600"
today_month_color: "#E10600"
event_background_opacity: 12
show_countdown: true
show_progress_bar: true
progress_bar_color: "#E10600"
event_font_size: 13px
event_color: "#FFFFFF"
time_24h: true
time_font_size: 11px
time_color: "#888888"
remove_location_country: true
location_font_size: 11px
location_color: "#666666"
weather:
  position: date
  date:
    show_conditions: true
    show_high_temp: true
    show_low_temp: false
    icon_size: 14px
    font_size: 12px
    color: var(--primary-text-color)
  event:
    show_conditions: true
    show_temp: true
    icon_size: 14px
    font_size: 12px
    color: var(--primary-text-color)
  entity: weather.buienradar
tap_action:
  action: expand
refresh_interval: 60
type: custom:calendar-card-pro
card_mod:
  style: |
    ha-card {
      background: var(--ha-card-background, var(--card-background-color, #1e1e1e)) !important;
      border: 0px solid rgba(225,6,0,0.19) !important;
      border-radius: 12px !important;
      box-shadow: 0px 4px 24px rgba(225,6,0,0.12) !important;
      overflow: hidden;
    }
    ha-card .header-container h1.card-header {
      font-size: 15px !important;
      font-weight: bold !important;
      letter-spacing: 2px !important;
      text-transform: uppercase !important;
      text-align: center !important;
      float: none !important;
      border-bottom: 1px solid rgba(225,6,0,0.25) !important;
      padding-bottom: 8px !important;
    }

3. Driver Standings
Shows the current driver standings with team colours, driver name and points. Displays the top 10 by default. Tap the card to expand and show all 22 drivers.

Required boolean

input_boolean:
  f1_drivers_toggle:
    name: Show all drivers
    initial: false

type: custom:button-card
entity: sensor.f1_driver_standings
show_name: false
show_state: false
show_icon: false
layout: custom
tap_action:
  action: call-service
  service: input_boolean.toggle
  service_data:
    entity_id: input_boolean.f1_drivers_toggle
styles:
  grid:
    - grid-template-areas: |
        "header"
        "standings"
    - row-gap: 12px
  card:
    - padding: 12px
    - border-radius: 12px
    - background: var(--ha-card-background, var(--card-background-color,
    - color: white
    - font-family: sans-serif
    - box-shadow: 0px 4px 24px rgba(225,6,0,0.12)
    - border: 0px solid rgba(225,6,0,0.19)
  custom_fields:
    header:
      - padding-bottom: 6px
      - border-bottom: 1px solid rgba(225,6,0,0.25)
    standings:
      - display: flex
      - flex-direction: column
      - gap: 6px
custom_fields:
  header: |
    [[[
      if (!entity?.attributes) return "Geen data";
      const expanded = states['input_boolean.f1_drivers_toggle']?.state === 'on';
      const icon = expanded ? "mdi:chevron-up" : "mdi:chevron-down";
      return '<div style="width: 100%; text-align: center;">'
        + '<div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">👤 Rijdersklassement</div>'
        + '<div style="font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 4px;">' + entity.attributes.season + ' Season</div>'
        + '<div style="font-size: 13px; color: #888888; margin-top: 2px;">Na ronde ' + entity.attributes.round + '</div>'
        + '<div style="display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; cursor: pointer;">'
        + '<span style="font-size: 12px; color: #555555;">' + (expanded ? '▲ Toon minder' : '▼ Toon alle ' + entity.attributes.driver_standings.length + ' rijders') + '</span>'
        + '<ha-icon icon="' + icon + '" style="--mdc-icon-size: 16px; color: #555555;"></ha-icon>'
        + '</div></div>';
    ]]]
  standings: |
    [[[
      if (!entity?.attributes?.driver_standings) return "Geen data";
      const expanded = states['input_boolean.f1_drivers_toggle']?.state === 'on';
      const standings = expanded
        ? entity.attributes.driver_standings
        : entity.attributes.driver_standings.slice(0, 10);
      const teamColors = {
        "Mercedes":         "#00D2BE",
        "Ferrari":          "#E8002D",
        "McLaren":          "#FF8000",
        "Red Bull":         "#3671C6",
        "Haas F1 Team":     "#B6BABD",
        "RB F1 Team":       "#6692FF",
        "Audi":             "#B5B5B5",
        "Alpine F1 Team":   "#FF87BC",
        "Williams":         "#64C4FF",
        "Cadillac F1 Team": "#D9042B",
        "Aston Martin":     "#229971"
      };
      return standings.map(function(d, idx) {
        const pos = d.positionText !== '-' ? parseInt(d.positionText) : (idx + 1);
        const code = d.Driver.code;
        const name = d.Driver.givenName + ' ' + d.Driver.familyName;
        const team = d.Constructors[0]?.name || '';
        const color = teamColors[team] || "#888";
        const points = d.points;
        const isTop3 = pos <= 3;
        const medal = pos == 1 ? '🥇' : pos == 2 ? '🥈' : pos == 3 ? '🥉' : pos + '.';
        const nameDisplay = pos == 1 ? '<b style="color: gold">' + name + '</b>' : name;
        const bg = idx % 2 === 0 ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.08)';
        return '<div style="display: flex; align-items: center; background: ' + bg + '; padding: 6px 10px; border-radius: 4px; height: 38px;">'
          + '<div style="min-width: 28px; font-size: ' + (isTop3 ? '18px' : '15px') + '; font-weight: bold; text-align: center; color: ' + (pos == 1 ? '#FFF200' : isTop3 ? '#ffffff' : '#555555') + ';">' + medal + '</div>'
          + '<div style="width: 4px; height: 26px; background: ' + color + '; border-radius: 2px; margin: 0 10px; flex-shrink: 0;"></div>'
          + '<div style="flex: 1;">'
          + '<div style="font-size: 15px; color: ' + (isTop3 ? '#ffffff' : '#cccccc') + '; font-weight: ' + (isTop3 ? 'bold' : 'normal') + ';">' + nameDisplay + ' <span style="font-size: 12px; color: #666666;">' + code + '</span></div>'
          + '<div style="font-size: 11px; color: #666666;">' + team + ' • ' + points + ' pt' + (points == 1 ? '' : 's') + '</div>'
          + '</div></div>';
      }).join('');
    ]]]

4. Constructor Standings
Shows the current constructor standings with team colours and points. Displays the top 10 by default. Tap the card to expand and show all 11 teams.

Required boolean

input_boolean:
  f1_race_toggle:
    name: Show all teams
    initial: false

type: custom:button-card
entity: sensor.f1_constructor_standings
show_name: false
show_state: false
show_icon: false
layout: custom
tap_action:
  action: call-service
  service: input_boolean.toggle
  service_data:
    entity_id: input_boolean.f1_race_toggle
styles:
  grid:
    - grid-template-areas: |
        "header"
        "standings"
    - row-gap: 12px
  card:
    - padding: 12px
    - border-radius: 12px
    - background: var(--ha-card-background, var(--card-background-color,
    - color: white
    - font-family: sans-serif
    - box-shadow: 0px 4px 24px rgba(225,6,0,0.12)
    - border: 0px solid rgba(225,6,0,0.19)
  custom_fields:
    header:
      - padding-bottom: 6px
      - border-bottom: 1px solid rgba(225,6,0,0.25)
    standings:
      - display: flex
      - flex-direction: column
      - gap: 6px
custom_fields:
  header: |
    [[[
      if (!entity?.attributes) return "Geen data";
      const expanded = states['input_boolean.f1_race_toggle']?.state === 'on';
      const icon = expanded ? "mdi:chevron-up" : "mdi:chevron-down";
      return '<div style="width: 100%; text-align: center;">'
        + '<div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">🏭 Constructeursklassement</div>'
        + '<div style="font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 4px;">' + entity.attributes.season + ' Season</div>'
        + '<div style="font-size: 13px; color: #888888; margin-top: 2px;">Na ronde ' + entity.attributes.round + '</div>'
        + '<div style="display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; cursor: pointer;">'
        + '<span style="font-size: 12px; color: #555555;">' + (expanded ? '▲ Toon minder' : '▼ Toon alle ' + entity.attributes.constructor_standings.length + ' teams') + '</span>'
        + '<ha-icon icon="' + icon + '" style="--mdc-icon-size: 16px; color: #555555;"></ha-icon>'
        + '</div></div>';
    ]]]
  standings: |
    [[[
      if (!entity?.attributes?.constructor_standings) return "Geen data";
      const expanded = states['input_boolean.f1_race_toggle']?.state === 'on';
      const standings = expanded
        ? entity.attributes.constructor_standings
        : entity.attributes.constructor_standings.slice(0, 10);
      const teamColors = {
        "Mercedes":         "#00D2BE",
        "Ferrari":          "#E8002D",
        "McLaren":          "#FF8000",
        "Red Bull":         "#3671C6",
        "Haas F1 Team":     "#B6BABD",
        "RB F1 Team":       "#6692FF",
        "Audi":             "#B5B5B5",
        "Alpine F1 Team":   "#FF87BC",
        "Williams":         "#64C4FF",
        "Cadillac F1 Team": "#D9042B",
        "Aston Martin":     "#229971"
      };
      return standings.map(function(c, idx) {
        const pos = c.positionText !== '-' ? parseInt(c.positionText) : (idx + 1);
        const name = c.Constructor.name;
        const nationality = c.Constructor.nationality;
        const points = c.points;
        const wins = c.wins;
        const color = teamColors[name] || "#888";
        const isTop3 = pos <= 3;
        const medal = pos == 1 ? '🥇' : pos == 2 ? '🥈' : pos == 3 ? '🥉' : pos + '.';
        const nameDisplay = pos == 1 ? '<b style="color: gold">' + name + '</b>' : name;
        const bg = idx % 2 === 0 ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.08)';
        return '<div style="display: flex; align-items: center; background: ' + bg + '; padding: 6px 10px; border-radius: 4px; height: 38px;">'
          + '<div style="min-width: 28px; font-size: ' + (isTop3 ? '18px' : '15px') + '; font-weight: bold; text-align: center; color: ' + (pos == 1 ? '#FFF200' : isTop3 ? '#ffffff' : '#555555') + ';">' + medal + '</div>'
          + '<div style="width: 4px; height: 26px; background: ' + color + '; border-radius: 2px; margin: 0 10px; flex-shrink: 0;"></div>'
          + '<div style="flex: 1;">'
          + '<div style="font-size: 15px; color: ' + (isTop3 ? '#ffffff' : '#cccccc') + '; font-weight: ' + (isTop3 ? 'bold' : 'normal') + ';">' + nameDisplay + '</div>'
          + '<div style="font-size: 11px; color: #666666;">' + nationality + ' • ' + points + ' pt' + (points == 1 ? '' : 's') + ' • ' + wins + ' win' + (wins == 1 ? '' : 's') + '</div>'
          + '</div></div>';
      }).join('');
    ]]]

5. Last Race Results
Shows the full results of the most recent race with team colours, points scored, and DNF/DNS status in red. Displays the top 10 by default. Tap the card to expand and show all 22 drivers.

Required boolean

input_boolean:
  f1_race_results:
    name: Race results
    initial: false

type: custom:button-card
entity: sensor.f1_last_race_results
show_name: false
show_state: false
show_icon: false
layout: custom
tap_action:
  action: call-service
  service: input_boolean.toggle
  service_data:
    entity_id: input_boolean.f1_race_results
styles:
  grid:
    - grid-template-areas: |
        "header"
        "results"
    - row-gap: 12px
  card:
    - padding: 12px
    - border-radius: 12px
    - background: var(--ha-card-background, var(--card-background-color,
    - color: white
    - font-family: sans-serif
    - box-shadow: 0px 4px 24px rgba(225,6,0,0.12)
    - border: 0px solid rgba(225,6,0,0.19)
  custom_fields:
    header:
      - padding-bottom: 6px
      - border-bottom: 1px solid rgba(225,6,0,0.25)
    results:
      - display: flex
      - flex-direction: column
      - gap: 6px
custom_fields:
  header: |
    [[[
      if (!entity?.attributes) return "Geen data";
      const expanded = states['input_boolean.f1_race_results']?.state === 'on';
      const icon = expanded ? "mdi:chevron-up" : "mdi:chevron-down";
      const date = new Date(entity.attributes.race_start_utc).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' });
      return '<div style="width: 100%; text-align: center;">'
        + '<div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">🏁 Laatste Race</div>'
        + '<div style="font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 4px;">' + entity.attributes.race_name + '</div>'
        + '<div style="font-size: 13px; color: #888888; margin-top: 2px;">' + entity.attributes.circuit_locality + ', ' + entity.attributes.circuit_country + ' · ' + date + '</div>'
        + '<div style="display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; cursor: pointer;">'
        + '<span style="font-size: 12px; color: #555555;">' + (expanded ? '▲ Toon minder' : '▼ Toon alle ' + entity.attributes.results.length + ' rijders') + '</span>'
        + '<ha-icon icon="' + icon + '" style="--mdc-icon-size: 16px; color: #555555;"></ha-icon>'
        + '</div></div>';
    ]]]
  results: |
    [[[
      if (!entity?.attributes?.results) return "Geen data";
      const expanded = states['input_boolean.f1_race_results']?.state === 'on';
      const results = expanded
        ? entity.attributes.results
        : entity.attributes.results.slice(0, 10);
      const teamColors = {
        "mercedes":     "#00D2BE",
        "ferrari":      "#E8002D",
        "mclaren":      "#FF8000",
        "red_bull":     "#3671C6",
        "haas":         "#B6BABD",
        "rb":           "#6692FF",
        "audi":         "#B5B5B5",
        "alpine":       "#FF87BC",
        "williams":     "#64C4FF",
        "cadillac":     "#D9042B",
        "aston_martin": "#229971"
      };
      return results.map(function(r, idx) {
        const pos = parseInt(r.position);
        const code = r.driver.code;
        const name = r.driver.givenName + ' ' + r.driver.familyName;
        const team = r.constructor.name;
        const teamId = r.constructor.constructorId;
        const color = teamColors[teamId] || "#888";
        const points = parseInt(r.points);
        const status = r.status;
        const isPoints = points > 0;
        const isDNF = status === 'Retired' || status === 'Did not start';
        const statusColor = isDNF ? '#E10600' : '#aaaaaa';
        const statusText = isPoints ? '+' + points + ' pt' + (points == 1 ? '' : 's') : isDNF ? status : status;
        const isTop3 = pos <= 3;
        const medal = pos == 1 ? '🥇' : pos == 2 ? '🥈' : pos == 3 ? '🥉' : pos + '.';
        const nameDisplay = pos == 1 ? '<b style="color: gold">' + name + '</b>' : name;
        const bg = idx % 2 === 0 ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.08)';
        return '<div style="display: flex; align-items: center; background: ' + bg + '; padding: 6px 10px; border-radius: 4px; height: 38px;">'
          + '<div style="min-width: 28px; font-size: ' + (isTop3 ? '18px' : '15px') + '; font-weight: bold; text-align: center; color: ' + (pos == 1 ? '#FFF200' : isTop3 ? '#ffffff' : '#555555') + ';">' + medal + '</div>'
          + '<div style="width: 4px; height: 26px; background: ' + color + '; border-radius: 2px; margin: 0 10px; flex-shrink: 0;"></div>'
          + '<div style="flex: 1;">'
          + '<div style="font-size: 15px; color: ' + (isTop3 ? '#ffffff' : '#cccccc') + '; font-weight: ' + (isTop3 ? 'bold' : 'normal') + ';">' + nameDisplay + ' <span style="font-size: 12px; color: #666666;">' + code + '</span></div>'
          + '<div style="font-size: 11px; color: #666666;">' + team + '</div>'
          + '</div>'
          + '<div style="font-size: 13px; font-weight: bold; color: ' + statusColor + '; text-align: right; min-width: 60px;">' + statusText + '</div>'
          + '</div>';
      }).join('');
    ]]]

Here is the link where everything is neatly in one post.
Share: Full F1 Dashboard — 10 cards with live timing, standings, weather, calendar and more · Nicxe/f1_sensor · Discussion #445

9 Likes

Could you please share the code.
I have installed both the card and the generation of the sensors, but I am not good with cards, and I have not yet found a rewarding card

I have post the code in my first post

1 Like

I reeaalllyy want to be able to get the current session’s current flag state (green, yellow, red) so I can turn the light behind my TV the corresponding colour. I don’t suppose this can get that information can it?

1 Like

have you checked if this is available in the underlying API? Unless it’s there then it cannot be surfaced in this Integration (or any other using the same API)

Unfortunately, this data is not available in jolpica-f1 api, which this integration uses.

As far as I understand, it should be available in the Fast-F1 API. However, I haven’t looked into it in detail, so I’m not sure how easy it would be to integrate into an integration.

I took a closer look at this after the question came up.

Technically, it is possible to extract that kind of data using Fast-F1, but it’s quite complex. You’d need to run a live data capture client, handle raw SignalR streams, and manually parse and forward status changes into Home Assistant. It’s not designed for real-time usage and would require a significant workaround to make it reliable.

However, I did find a cleaner alternative. OpenF1, a separate project that provides real-time race control data (including flags), with only ~2–3 seconds delay. This would be relatively easy to hook into Home Assistant.

The only catch is that live access requires a paid account to support their infrastructure.

So the question is
Is there enough interest in this feature that some of us would be willing to chip in to cover the cost of access?

If there is, I’d be happy to explore building support for it into the integration.

Let me know what you think.

2 Likes

Damn, thanks for looking into it.

Not quite sure if it’s worth spending money on, but I may look a bit more into OpenF1 at some point. Thanks again for that tip.

You’re very welcome, glad it was helpful!

Best of luck if you decide to dive into Fast-F1 – it’s a powerful tool, even if it takes a bit of work to bend it to real-time use.

If you do manage to get something working, feel free to share it back here, I’m sure others (myself included) would be really interested to see what you come up with!

1 Like

There’s been some interest in adding live Formula 1 data to this integration – such as flag status and race control messages – and I’ve decided to take a closer look at it.

I’m now working on integrating OpenF1 into the f1_sensor integration to bring real-time race data into Home Assistant. This could open up fun automation possibilities like syncing your lights with track flags, announcing safety car events, or triggering “race mode” scenes.

I’ve started a GitHub discussion to gather feedback and ideas from the community – you can join the conversation here:

Let me know what you’d love to automate during a race!

6 Likes

Hi everyone!

I’ve decided to move away from OpenF1-lite and instead implement F1’s official (but undocumented) Live Timing API. I’ve started by adding flag status and Safety Car functionality, and the first version is now running without any errors.

The real test will be this upcoming race weekend when live data starts flowing, fingers crossed everything works as intended! If it does, this could open up some really fun automation possibilities, like ambient lighting changes or notifications based on track events.

I’m currently looking for a few interested beta testers who’d like to help test during the sessions during upcoming weekends. Let me know if you’re up for it

16 Likes

Hello
Excellent integration, thx U
I don’t know how to configure waether API, can someone help me please ?

You do not need an API key for the weather

1 Like

Live F1 Data Directly in Home Assistant

:rocket: This is the biggest update so far

F1 Sensor now brings live data straight from Formula 1’s Live Timing API into your smart home.

Want your lights to turn red when a red flag is thrown? Or get instant notifications when the Safety Car is deployed? Now it’s possible. :racing_car::bulb:


:sparkles: What’s New

:red_circle: Live session sensors

Three brand new live entities are introduced, updating in real-time:

  • sensor.f1_session_status
    Shows the current phase of a session. Possible states:
    pre, live, suspended, finished, finalised, ended

  • sensor.f1_track_status
    Mirrors the official TrackStatus feed. Possible states:
    CLEAR, YELLOW, VSC, SC, RED

  • binary_sensor.f1_safety_car
    A simple boolean sensor that is on whenever VSC or SC is active.


:gear: New configuration options

  • Enable live F1 API – toggle creation of the new live sensors.
  • Live update delay (seconds) – add a configurable delay to sync with your broadcast or stream.
    (Useful since live TV and streaming have built-in delays ranging from 5 to 60 seconds.)

:crystal_ball: What’s Next

This lays the foundation for even richer live race data in your automations. Expect more to come as the integration evolves!


Disclaimer: F1 Sensor is an unofficial project and not affiliated with Formula 1.

5 Likes

Awesome, I will look into using this in the card on Friday

2 Likes

Nice looking card mate, I had a quick go before with setting it up and created the Boolean.
How come you say create the f1_race_results Boolean but the one in the code is f1_drivers_toggle.
For some reason mine doesn’t show any data or do anything when I click on it.
I’ve not had chance to fully check where I’ve gone wrong.
Have you created any more cards ?
Thanks

Yes, of course, it should be input_boolean.f1_drivers_toggle, not input_boolean.f1_race_results. I mixed it up with the other maps I have. I’ll share all my other maps this weekend.

I updated my post with all the cards I have including the code

3 Likes