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.
11 Likes

New pre-release adding result sensors

1 Like

Hey, I find it very handy those separate sensors, a lot of freedom to make your own dashboard cards. I made the Resuls card here and will do the rest, so far without problems.
You can click on that chevron-up icon to make the map expand and show the rest of the race results. The dashboard cards will update themselves if the race ends after 00:00. The current race is indicated by a glow effect. The cards automatically adjust when there is a sprint race.
You need custom button card mod (use hacs)

Here the code for the F1 Schedule 2025
Make a “input_boolean.f1_race_toggle” so you can expand the card when you click on it.

type: custom:button-card
entity: sensor.f12025_current_season
icon: mdi:calendar-clock
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"
        "races"
    - row-gap: 12px
  card:
    - padding: 12px
    - border-radius: 8px
    - background: linear-gradient(135deg,
    - color: white
    - font-family: sans-serif
    - box-shadow: 0px 4px 10px rgba(0,0,0,0.3)
  custom_fields:
    header:
      - font-size: 17px
      - font-weight: 600
      - text-align: center
      - padding-bottom: 6px
      - border-bottom: 1px solid rgba(255,255,255,0.2)
      - cursor: pointer
    races:
      - font-size: 14px
      - line-height: 1.6
      - display: flex
      - flex-direction: column
      - gap: 6px
custom_fields:
  header: |
    [[[
      if (!entity?.attributes) return "Race calendar not available";
      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="display: inline-flex; align-items: center; gap: 8px; cursor: pointer;">
            <span style="font-weight: 600;">🏁 F1 Schedule - ${entity.attributes.season}</span>
            <ha-icon icon="${icon}" style="--mdc-icon-size: 20px;"></ha-icon>
          </div>
        </div>
      `;
    ]]]
  races: |
    [[[
      if (!entity?.attributes?.races) return "No races found";
      const toggle = states['input_boolean.f1_race_toggle']?.state === 'on';

      const races = toggle ? entity.attributes.races : entity.attributes.races.slice(0, 10);

      const now = new Date();
      let nextRace = null;
      for (const r of entity.attributes.races) {
        const raceDate = new Date(r.date + 'T' + (r.time || '00:00:00Z'));
        if (raceDate > now) {
          nextRace = r;
          break;
        }
      }

      return races.map((r, idx) => {
        const date = new Date(r.date + 'T' + (r.time || '00:00:00Z'));
        const dateString = date.toLocaleDateString('nl-NL', { weekday: 'short', day: 'numeric', month: 'short' });
        const timeString = date.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' });

        const isNextRace = r === nextRace;
        const bg = isNextRace 
          ? 'rgba(255, 215, 0, 0.12)' 
          : (idx % 2 === 0 ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.08)');

        return `
          <div style="display: flex; flex-direction: column; background: ${bg}; padding: 6px 10px; border-radius: 4px;">
            <div><strong>Ronde ${r.round}:</strong> ${r.raceName}</div>
            <div style="font-size: 12px; color: var(--secondary-text-color);">
              📍 ${r.Circuit.Location.locality}, ${r.Circuit.Location.country} • ${dateString} ${r.time ? '• ' + timeString : ''}
            </div>
          </div>`;
      }).join('');
    ]]]

Here the code for the Last Grand Prix
Make a “input_boolean.f1_race_results” so you can expand the card when you click on it.

type: custom:button-card
entity: sensor.f12025_last_race_results
icon: mdi:trophy
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: 8px
    - background: linear-gradient(135deg,
    - color: white
    - font-family: Arial
    - box-shadow: 0px 4px 10px rgba(0,0,0,0.3)
  custom_fields:
    header:
      - font-size: 17px
      - font-weight: 600
      - text-align: center
      - padding-bottom: 6px
      - border-bottom: 1px solid rgba(255,255,255,0.2)
      - cursor: pointer
    results:
      - font-size: 14px
      - line-height: 1.6
      - display: flex
      - flex-direction: column
      - gap: 6px
custom_fields:
  header: |
    [[[
      const race = entity.attributes?.race_name || 'Last Race';
      const round = entity.attributes?.round || '?';
      const icon = states['input_boolean.f1_race_results']?.state === 'on' ? "mdi:chevron-up" : "mdi:chevron-down";
      return `
        <div style="width: 100%; text-align: center;">
          <div style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer;">
            <span style="font-weight: 600;">🏁 ${race.replace('Grand Prix', 'GP')} – Round ${round}</span>
            <ha-icon icon="${icon}" style="--mdc-icon-size: 20px;"></ha-icon>
          </div>
        </div>
      `;
    ]]]
  results: |
    [[[
      const results = entity.attributes?.results || [];
      const expanded = states['input_boolean.f1_race_results']?.state === 'on';
      const displayResults = expanded ? results : results.slice(0, 10);

      const teamColors = {
        "Red Bull": "#1E41FF",
        "Ferrari": "#DC0000",
        "Mercedes": "#00D2BE",
        "McLaren": "#FF8700",
        "Aston Martin": "#006F62",
        "Alpine F1 Team": "#0090FF",
        "RB F1 Team": "#6699FF",
        "Haas F1 Team": "#B6BABD",
        "Williams": "#005AFF",
        "Sauber": "#52E252"
      };

      return displayResults.map((r, idx) => {
        const pos = r.position;
        const code = r.driver?.code || '';
        const name = `${r.driver?.givenName || ''} ${r.driver?.familyName || ''}`.trim();
        const team = r.constructor?.name || '';
        const color = teamColors[team] || '#888';
        const points = r.points || '0';

        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;">
            <div style="width: 4px; height: 28px; background: ${color}; border-radius: 2px; margin-right: 8px;"></div>
            <div style="flex: 1">
              <div style="color: var(--primary-text-color);"><strong>${pos}.</strong> <strong>${code}</strong> - ${name}</div>
              <div style="font-size: 12px; color: var(--secondary-text-color);">${team} • ${points} pt${points == 1 ? '' : 's'}</div>
            </div>
          </div>`;
      }).join('');
    ]]]

Here the code for the Next Grand Prix
Make a “input_boolean.f1_race_results” so you can expand the card when you click on it.

type: custom:mod-card
style:
  ha-card:
    border-radius: 16px
    overflow: hidden
    box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.4)
    background: linear-gradient(135deg,
    padding: 0
    color: white
card:
  type: vertical-stack
  cards:
    - type: custom:button-card
      entity: sensor.f12025_next_race
      show_name: false
      show_state: false
      show_icon: false
      layout: custom
      styles:
        grid:
          - grid-template-areas: |
              "header"
              "details"
          - row-gap: 10px
        card:
          - padding: 16px
          - background: linear-gradient(135deg,
          - color: white
          - font-family: sans-serif
          - border-radius: 16px 16px 0 0
        custom_fields:
          header:
            - font-size: clamp(16px, 2vw, 18px)
            - font-weight: 600
            - text-align: center
            - padding: 0 8px 8px 8px
            - border-bottom: 1px solid rgba(255,255,255,0.2)
            - white-space: normal
            - word-break: break-word
          details:
            - font-size: 14px
            - line-height: 1.7
            - display: flex
            - flex-direction: column
            - gap: 6px
      custom_fields:
        header: |
          [[[
            if (!entity?.attributes) return "Next GP not available";
            const race = entity.attributes.race_name;
            const start = new Date(entity.attributes.race_start);
            const end = new Date(start.getTime() + 90 * 60 * 1000);
            const now = new Date();
            const diff = start - now;
            const days = Math.floor(diff / (1000 * 60 * 60 * 24));
            const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
            const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));

            let status = "";

            if (now < start) {
              status = `⏳ In ${days}d ${hours}u ${minutes}m`;
            } else if (now >= start && now <= end) {
              status = "🏁 Race has started!";
            } else {
              status = "✅ The race is over";
            }

            return `
              <div style="font-size: clamp(16px, 2vw, 18px); font-weight: 600; text-align: center;">🏁 NEXT GRAND PRIX</div>
              <div style="font-size: clamp(14px, 1.6vw, 15px); font-weight: 500; text-align: center; margin-top: 4px; color: #ccc;">${race}</div>
              <div style="margin-top: 8px; font-size: clamp(14px, 1.8vw, 16px); font-weight: 600; text-align: center; color: #f5c518;">${status}</div>
            `;
          ]]]
        details: |
          [[[ 
            const attrs = entity.attributes;
            if (!attrs) return "";

            const now = new Date();
            const start = new Date(attrs.race_start);
            const diff = start - now;
            const days = Math.floor(diff / (1000 * 60 * 60 * 24));
            const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
            const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
            const countdown = diff > 0 
              ? `⏳ In ${days}d ${hours}u ${minutes}m`
              : "🏁 Race has started!";

            const formatSession = (label, datetime, index) => {
              if (!datetime) return '';
              const dt = new Date(datetime);
              const now = new Date();
            
              const durations = {
                '🧪 FP1': 65,
                '🧪 FP2': 65,
                '🧪 FP3': 65,
                '🛞 Qualifying': 75,
                '🏁 Race': 105,
                '📐 Sprint Quali': 75,
                '🏃 Sprint': 50,
              };
            
              const duration = durations[label] || 60;
              const end = new Date(dt.getTime() + duration * 60 * 1000);
              const isLive = now >= dt && now <= end;
            
              const bg = index % 2 === 0 ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.1)';
              const liveStyle = isLive
                ? `background: rgba(224, 195, 0, 0.2);
                   animation: pulse 1.5s infinite;
                   border: 2px solid rgba(224, 195, 0, 0.8);`
                : `background: ${bg};`;
            
              const day = dt.toLocaleDateString('nl-NL', { weekday: 'short', day: 'numeric', month: 'short' });
              const time = dt.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' });
            
              return `
                <style>
                  @keyframes pulse {
                    0% { box-shadow: 0 0 0 0 rgba(255, 223, 0, 0.4); }
                    70% { box-shadow: 0 0 0 10px rgba(255, 223, 0, 0); }
                    100% { box-shadow: 0 0 0 0 rgba(255, 223, 0, 0); }
                  }
                </style>
                <div style="${liveStyle} padding: 6px 8px; border-radius: 8px;">
                  ${label}: <strong>${day} • ${time}</strong>
                </div>`;
            };

            let index = 0;
            const rows = [
              `<div style="background: ${index++ % 2 === 0 ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.1)'}; padding: 6px 8px; border-radius: 8px;"><strong>🏟️ Circuit:</strong> ${attrs.circuit_name}</div>`,
              `<div style="background: ${index++ % 2 === 0 ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.1)'}; padding: 6px 8px; border-radius: 8px;"><strong>📍 Location:</strong> ${attrs.circuit_locality}, ${attrs.circuit_country}</div>`,
              `<div style="background: ${index++ % 2 === 0 ? 'rgba(255,255,255,0.05)' : 'rgba(255,255,255,0.1)'}; padding: 6px 8px; border-radius: 8px;"><strong>📆 Race date:</strong> ${start.toLocaleDateString('nl-NL', { weekday: 'short', day: 'numeric', month: 'long' })}</div>`
            ];

            rows.push(formatSession('🧪 FP1', attrs.first_practice_start, index++));
            if (attrs.sprint_qualifying_start || attrs.sprint_start) {
              rows.push(formatSession('📐 Sprint Quali', attrs.sprint_qualifying_start, index++));
              rows.push(formatSession('🏃 Sprint', attrs.sprint_start, index++));
            } else {
              rows.push(formatSession('🧪 FP2', attrs.second_practice_start, index++));
              rows.push(formatSession('🧪 FP3', attrs.third_practice_start, index++));
            }
            rows.push(formatSession('🛞 Qualifying', attrs.qualifying_start, index++));
            rows.push(formatSession('🏁 Race', attrs.race_start, index++));

            return rows.filter(Boolean).join('');
          ]]]
    - type: map
      aspect_ratio: 16x9
      default_zoom: 10
      entities:
        - entity: sensor.f1_circuit_locatie
          latitude: "{{ state_attr('sensor.f1_circuit_locatie', 'latitude') }}"
          longitude: "{{ state_attr('sensor.f1_circuit_locatie', 'longitude') }}"
          label_mode: icon
      style:
        ha-card:
          background: linear-gradient(135deg, rgba(30,30,30,0.6), rgba(0,0,0,0.8));
          border: none
          box-shadow: none
          border-radius: 0
    - type: custom:button-card
      entity: sensor.f12025_weather
      show_name: false
      show_state: false
      show_icon: false
      layout: custom
      styles:
        grid:
          - grid-template-areas: |
              "header"
              "current_weather"
              "race_weather"
          - row-gap: 12px
        card:
          - padding: 12px
          - border-radius: 8px
          - background: linear-gradient(135deg,
          - color: white
          - font-family: sans-serif
          - box-shadow: 0px 4px 20px rgba(0,0,0,0.2)
        custom_fields:
          header:
            - font-size: 17px
            - font-weight: 600
            - text-align: center
            - padding-bottom: 6px
            - border-bottom: 1px solid rgba(255,255,255,0.2)
          current_weather:
            - font-size: 14px
            - line-height: 1.2
            - display: flex
            - flex-direction: column
            - gap: 6px
          race_weather:
            - font-size: 14px
            - line-height: 1.2
            - display: flex
            - flex-direction: column
            - gap: 6px
      custom_fields:
        header: |
          [[[ 
            const race_name = states['sensor.f12025_next_race'].attributes.race_name;
            const circuit_locality = states['sensor.f12025_next_race'].attributes.circuit_locality;
            const circuit_country = states['sensor.f12025_next_race'].attributes.circuit_country;

            if (race_name && circuit_locality && circuit_country) {
              return `
                <div style="display: flex; flex-direction: column; align-items: center; gap: 6px;">
                  <div style="display: flex; align-items: center; gap: 8px; font-size: 22px; font-weight: 700;">
                    🏁 ${race_name} Weather
                  </div>
                  <div style="width: 60%; height: 1px; background: rgba(255, 255, 255, 0.3);"></div>
                  <div style="font-size: 16px; font-weight: 400; opacity: 0.7;">
                    ${circuit_locality} - ${circuit_country}
                  </div>
                </div>
              `;
            } else {
              return `<div style="text-align: center;">No data available</div>`;
            }
          ]]]
        current_weather: |
          [[[
            const temp = entity.attributes.current_temperature;
            const humidity = entity.attributes.current_humidity;
            const cloud_cover = entity.attributes.current_cloud_cover;
            const precipitation = entity.attributes.current_precipitation;
            const wind_speed = entity.attributes.current_wind_speed;
            const wind_dir = entity.attributes.current_wind_direction;

            function mpsToBeaufort(mps) {
              if (mps <= 0.3) return 0;
              else if (mps <= 1.5) return 1;
              else if (mps <= 3.3) return 2;
              else if (mps <= 5.4) return 3;
              else if (mps <= 7.9) return 4;
              else if (mps <= 10.7) return 5;
              else if (mps <= 13.8) return 6;
              else if (mps <= 17.1) return 7;
              else if (mps <= 20.7) return 8;
              else if (mps <= 24.4) return 9;
              else if (mps <= 28.4) return 10;
              else if (mps <= 32.6) return 11;
              return 12;
            }

            const wind_beaufort = mpsToBeaufort(wind_speed);

            const rowStyles = [
              "background: rgba(255,255,255,0.04); padding: 10px 12px; border-radius: 8px;",
              "background: rgba(255,255,255,0.08); padding: 10px 12px; border-radius: 8px;"
            ];

            return `
              <div style="display: flex; flex-direction: column; gap: 8px;">

                <div style="font-weight: 700; font-size: 18px; text-align: center; margin-bottom: 6px;">Current Weather</div>

                <div style="${rowStyles[0]} display: flex; justify-content: space-between; align-items: center;">
                  <div>🌡️ Temperature</div><div><b>${Math.round(temp)}°C</b></div>
                </div>
                <div style="${rowStyles[1]} display: flex; justify-content: space-between; align-items: center;">
                  <div>💧 Humidity</div><div><b>${humidity}%</b></div>
                </div>
                <div style="${rowStyles[0]} display: flex; justify-content: space-between; align-items: center;">
                  <div>☁️ Cloud cover</div><div><b>${cloud_cover}%</b></div>
                </div>
                <div style="${rowStyles[1]} display: flex; justify-content: space-between; align-items: center;">
                  <div>🌧️ Precipitation</div><div><b>${precipitation} mm</b></div>
                </div>
                <div style="${rowStyles[0]} display: flex; justify-content: space-between; align-items: center;">
                  <div>🌬️ Wind speed</div><div><b>${wind_beaufort} Bft</b></div>
                </div>
                <div style="${rowStyles[1]} display: flex; justify-content: space-between; align-items: center;">
                  <div>🧭 Wind direction</div><div><b>${wind_dir}</b></div>
                </div>

              </div>
            `;
          ]]]
        race_weather: |
          [[[
            const race_temp = entity.attributes.race_temperature;
            const race_humidity = entity.attributes.race_humidity;
            const race_cloud_cover = entity.attributes.race_cloud_cover;
            const race_precipitation = entity.attributes.race_precipitation;
            const race_wind_speed = entity.attributes.race_wind_speed;
            const race_wind_dir = entity.attributes.race_wind_direction;

            function mpsToBeaufort(mps) {
              if (mps <= 0.3) return 0;
              else if (mps <= 1.5) return 1;
              else if (mps <= 3.3) return 2;
              else if (mps <= 5.4) return 3;
              else if (mps <= 7.9) return 4;
              else if (mps <= 10.7) return 5;
              else if (mps <= 13.8) return 6;
              else if (mps <= 17.1) return 7;
              else if (mps <= 20.7) return 8;
              else if (mps <= 24.4) return 9;
              else if (mps <= 28.4) return 10;
              else if (mps <= 32.6) return 11;
              return 12;
            }

            const race_wind_beaufort = mpsToBeaufort(race_wind_speed);

            const rowStyles = [
              "background: rgba(255,255,255,0.04); padding: 10px 12px; border-radius: 8px;",
              "background: rgba(255,255,255,0.08); padding: 10px 12px; border-radius: 8px;"
            ];

            return `
              <div style="display: flex; flex-direction: column; gap: 8px;">

                <div style="font-weight: 700; font-size: 18px; text-align: center; margin-bottom: 6px;">Race Weekend Weather</div>

                <div style="${rowStyles[0]} display: flex; justify-content: space-between; align-items: center;">
                  <div>🌡️ Temperature</div><div><b>${Math.round(race_temp)}°C</b></div>
                </div>
                <div style="${rowStyles[1]} display: flex; justify-content: space-between; align-items: center;">
                  <div>💧 Humidity</div><div><b>${race_humidity}%</b></div>
                </div>
                <div style="${rowStyles[0]} display: flex; justify-content: space-between; align-items: center;">
                  <div>☁️ Cloud cover</div><div><b>${race_cloud_cover}%</b></div>
                </div>
                <div style="${rowStyles[1]} display: flex; justify-content: space-between; align-items: center;">
                  <div>🌧️ Precipitation</div><div><b>${race_precipitation} mm</b></div>
                </div>
                <div style="${rowStyles[0]} display: flex; justify-content: space-between; align-items: center;">
                  <div>🌬️ Wind speed</div><div><b>${race_wind_beaufort} Bft</b></div>
                </div>
                <div style="${rowStyles[1]} display: flex; justify-content: space-between; align-items: center;">
                  <div>🧭 Wind direction</div><div><b>${race_wind_dir}</b></div>
                </div>

              </div>
            `;
          ]]]

This template creates an extra sensor in Home Assistant that is used to show the Formula 1 circuit on the map

template:
  - sensor:
      - name: "F1 Circuit Locatie"
        unique_id: 08728eb3-61d5-4cec-914b-4ba5d9e74648
        state: "{{ state_attr('sensor.f12025_next_race', 'circuit_name') }}"
        attributes:
          latitude: "{{ state_attr('sensor.f12025_next_race', 'circuit_lat') }}"
          longitude: "{{ state_attr('sensor.f12025_next_race', 'circuit_long') }}"
          country: "{{ state_attr('sensor.f12025_next_race', 'circuit_country') }}"
          icon: mdi:map-marker

Here the code for the Driver Standings
Make a “input_boolean.f1_race_results” so you can expand the card when you click on it.

type: custom:button-card
entity: sensor.f12025_driver_standings
icon: mdi:trophy-variant-outline
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: 8px
    - background: linear-gradient(135deg,
    - color: white
    - font-family: sans-serif
    - box-shadow: 0px 4px 10px rgba(0,0,0,0.3)
  custom_fields:
    header:
      - padding-bottom: 6px
      - border-bottom: 1px solid rgba(255,255,255,0.2)
    standings:
      - font-size: 14px
      - line-height: 1.6
      - display: flex
      - flex-direction: column
      - gap: 6px
custom_fields:
  header: |
    [[[
      if (!entity?.attributes) return "Standings not available";
      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="display: inline-flex; align-items: center; gap: 8px; cursor: pointer;">
            <span style="font-weight: 600; font-size: 17px;">
              🏁 ${entity.attributes.season} Driver Standings - Round ${entity.attributes.round}
            </span>
            <ha-icon icon="${icon}" style="--mdc-icon-size: 20px;"></ha-icon>
          </div>
        </div>
      `;
    ]]]
  standings: |
    [[[
      if (!entity?.attributes?.driver_standings) return "No 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 = {
        "Red Bull": "#1E41FF",
        "Ferrari": "#DC0000",
        "Mercedes": "#00D2BE",
        "McLaren": "#FF8700",
        "Aston Martin": "#006F62",
        "Alpine F1 Team": "#0090FF",
        "RB F1 Team": "#6699FF",
        "Haas F1 Team": "#B6BABD",
        "Williams": "#005AFF",
        "Sauber": "#52E252"
      };

      return standings.map((d, idx) => {
        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 highlight = idx === 0 
          ? '<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;">
            <div style="width: 4px; height: 26px; background: ${color}; border-radius: 2px; margin-right: 10px;"></div>
            <div style="flex: 1">
              <div style="color: var(--primary-text-color); font-weight: 500;">
                <strong>${d.position}.</strong> <strong>${code}</strong> - ${highlight}
              </div>
              <div style="font-size: 12px; color: var(--secondary-text-color);">${team} • ${points} pt${points == 1 ? '' : 's'}</div>
            </div>
          </div>`;
      }).join('');
    ]]]

Here the code for the Team Standings.

type: custom:button-card
entity: sensor.f12025_constructor_standings
icon: mdi:car-cog
show_name: false
show_state: false
show_icon: false
layout: custom
styles:
  grid:
    - grid-template-areas: |
        "header"
        "standings"
    - row-gap: 12px
  card:
    - padding: 12px
    - border-radius: 8px
    - background: linear-gradient(135deg,
    - color: white
    - font-family: sans-serif
    - box-shadow: 0px 4px 10px rgba(0,0,0,0.3)
  custom_fields:
    header:
      - font-size: 17px
      - font-weight: 600
      - text-align: center
      - padding-bottom: 6px
      - border-bottom: 1px solid rgba(255,255,255,0.2)
    standings:
      - font-size: 14px
      - line-height: 1.6
      - display: flex
      - flex-direction: column
      - gap: 6px
custom_fields:
  header: |
    [[[ 
      if (!entity?.attributes) return "Standings not available";
      return "🏁  2025 Team Standings - Round " + entity.attributes.round;
    ]]]
  standings: |
    [[[
      if (!entity?.attributes?.constructor_standings) return "No data";

      const teamColors = {
        "Red Bull": "#1E41FF",
        "Ferrari": "#DC0000",
        "Mercedes": "#00D2BE",
        "McLaren": "#FF8700",
        "Aston Martin": "#006F62",
        "Alpine F1 Team": "#0090FF",
        "RB F1 Team": "#6699FF",
        "Haas F1 Team": "#B6BABD",
        "Williams": "#005AFF",
        "Sauber": "#52E252"
      };

      return entity.attributes.constructor_standings.slice(0, 10).map((c, idx) => {
        const name = c.Constructor.name;
        const nationality = c.Constructor.nationality;
        const points = c.points;
        const wins = c.wins;
        const color = teamColors[name] || "#888";
        const position = c.position;

        const highlight = idx === 0 
          ? '<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;">
            <div style="width: 4px; height: 26px; background: ${color}; border-radius: 2px; margin-right: 10px;"></div>
            <div style="flex: 1">
              <div style="color: var(--primary-text-color); font-weight: 500;"><strong>${position}.</strong> ${highlight}</div>
              <div style="font-size: 12px; color: var(--secondary-text-color);">${nationality} • ${points} pt${points == 1 ? '' : 's'} • ${wins} win${wins == 1 ? '' : 's'}</div>
            </div>
          </div>`;
      }).join('');
    ]]]

8 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

15 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