Formula 1 Racing sensor

Driver headshots are not hardcoded locally. They come from the official F1 live DriverList feed, not from the Jolpica/Ergast standings API. When no live DriverList is available, the integration keeps the last known driver list or falls back to standings data, and that fallback does not include headshots. That means images can be missing or outdated until the live feed updates them. Custom driver images are not supported at the moment.

1 Like

[Update] F1 Live Timing API changes — what’s affected, what’s fixed, and what’s coming

Hi everyone,

I wanted to share a detailed update on what has been happening with the integration over the past few weeks, following reports of missing tyre data and Championship Prediction sensors showing unknown during the 2026 season opener.

What changed in the F1 Live Timing API

During last season, F1’s Live Timing service no longer freely delivers all data streams via its SignalR connection. A number of streams now require a valid F1 TV subscription (an authenticated JWT token) to receive live data. This went largely unnoticed at first because Replay Mode continued to work perfectly, replay uses static archive files that are stored on F1’s servers without any authentication requirement. So everything looked fine in replay, but the live connection was silently missing data for certain sensors.

What is affected

The core of the integration, timing, session status, track status, race control messages, lap counts, weather, driver lists, sector times, and tyre data, all come from streams that remain fully open without authentication. These continue to work live, no changes needed.

The streams that are now auth-gated are:

  • Championship Prediction (driver and team standings predictions during a session)
  • Pit Stop Series (live pit stop data)
  • Driver Race Info (position changes, gap calculations used by some sensors)
  • Car telemetry (CarData.z / Position.z) — compressed high-frequency telemetry

Replay-only sensors

Rather than silently show stale or unknown values, sensors that depend on auth-gated streams will be designated as Replay Only. This means they will update correctly when you play back a finished session, but will not receive live updates during a race. The Lovelace card will been updated to show a clear amber indicator when a sensor is in this state, so you’ll know the data shown is from the archive rather than live.

What will be fixed

Two significant fixes have landed in BETA based on this analysis:

Tyre data source migration. The integration previously used TyreStintSeries as its primary source for tyre compound data. That stream is unreliable live. It has been replaced with TimingAppData, which is the same approach used by others and is confirmed open and stable. Tyre data (compound, new/used, stint laps) should be reliable during live sessions.

SignalR protocol upgrade. The integration has been migrated from the legacy ASP.NET SignalR protocol to ASP.NET Core SignalR. The legacy protocol required re-subscribing every 5 minutes due to Azure connection limits, which caused 589 reconnects over a two-day period in testing. The Core protocol has built-in ping/pong keepalives and is significantly more stable. This will be verified against packet captures from the Japanese GP.

Why authentication is not being implemented right now

Several users have asked about adding F1 TV login support to restore the auth-gated streams. I want to be transparent here: I do not personally have an F1 TV Pro subscription, which means I cannot test an authenticated integration end-to-end against real live data. Shipping untested auth code risks breaking things for everyone, including the streams that currently work fine.

My priority right now is making sure the open streams, which cover the majority of what most users care about, are as stable and accurate as possible. Once the current fixes are validated across a few race weekends, authentication can be revisited. Any contributions or testing help from users who do have F1 TV Pro would be very welcome.

What’s still being monitored

A capture service is running on my homelab server recording live SignalR data across multiple parallel streams. This gives direct visibility into exactly what data F1’s servers are delivering. I’ll continue using this to validate sensor behaviour across upcoming race weekends.

Thanks for your patience and for all the bug reports, the detailed logs from issues like #427, #428, and #425 were essential to tracking this down. Feel free to reply here or open an issue on GitHub if you notice anything unexpected.

Thank you all for contributing to this little hobby project of mine.

9 Likes

Keep rocking Stimo!!!
And thank you for this great addon cq cards!
I think all of us will agree on that!

1 Like

hi hi all, busy trying to unravel all the capabilities of the F1 Sensor integration and then think the various cards.

some immediate “desires” for this weekend as there is data coming that I can test against.
dashboard:
practice and race times, as in when ?
practice and race times per driver per session

I love this dashboard by Nicxe… would love if someone has build THIS and willing to share the dashboard yaml.

then came across the FOLLOWING dashboard.
I got the points shown next to the names, I even got the cars shown, but the back ground, the helmets, the proper text / font, does not seem to want o work.

asking for some assistance.

G

They’re the same cards that go with the sensor. They’re both by @Stimo

would really love if someone with a couple of dashboards might be willing to paste their complete dashboard yaml per dashboard.
will long term customise, create own, jsut want to see all the lights on, data flowing etc as a taster this weekend.
G

you just need to install the live data cards … sensors needed to use are all layed out on which ones to use when you pick the card

Live Data Cards

guessing allot of them are dependant on activity on track, will see later.
G

No need to guess, everything is documented here. You just need to take the time to read it :wink:

1 Like

You can test the live data cards out by playing a replay session.

1 Like

do really love asking for assistance/example yaml, known to be working, that basically cost the responder the simple task of a copy paste, but instead,

I get a RTFM…
G

explains why previously I had no head shots and suddenly this morning they were there, thought I did something wrong, now understand why. :wink:
G

hi hi,
Trying to add some of your cards.
I can’t seem to add the 2026 calendar ?
G

There is no YAML, it’s custom cards installed using the UI

then it gets even more interesting, will see tomorrow while qualifying is on.
see there is a qualifying card,
is there a card for each of the practice sessions, other words it should show/data for practice 1 and practice 2.
See everyone have a card showing China… which card shows the name/location of the race for the wknd.
G

Dear George,
As it is is covered in the Readme (RTFM) there is a card that covers info during the (sprint) qualification and one specific for the (sprint) race. You can easily setup all the cards on a page and see what happens while (re)playing a race of qualification. For testing of the cards I always load a session and see how the data fills the cards, that is so cool. Have fun with the F1 sensor and the cards. Of course you have made integrations (HACS) for both !!

1 Like

i get where your coming from with wanting a working cut n paste altho these cards are a little different in the fact they are perdefined working card just like what your asking for

… I currently have the calendar added to a dashboard and the below is what I see.

  - type: custom:atomic-calendar-revive
    name: Calendar
    enableModeChange: true
    entities:
      - entity: calendar.f1_season_calendar
        icon: mdi:calendar-month

showing me:

is there a larger calendar showing all the race names/countries/dates as a table.

figured out, rough, but it’s there

type: vertical-stack
cards:
    
  - type: custom:f1-live-session-card
    show_flag: true
    show_lap_progress: true
    show_track_status: true
    show_weather: true
    show_time_remaining: true
    show_time_elapsed: true
    session_entity: sensor.f1_current_session
    session_status_entity: sensor.f1_session_status
    formation_start_entity: binary_sensor.f1_formation_start
    lap_count_entity: sensor.f1_race_lap_count
    track_status_entity: sensor.f1_track_status
    weather_entity: sensor.f1_track_weather
    next_race_entity: ""
    session_time_remaining_entity: sensor.f1_session_time_remaining
    session_time_elapsed_entity: sensor.f1_session_time_elapsed
    overtake_mode_entity: binary_sensor.f1_overtake_mode
    straight_mode_entity: sensor.f1_straight_mode

  - type: custom:atomic-calendar-revive
    name: Calendar
    enableModeChange: true
    entities:
      - entity: calendar.f1_season_calendar
        icon: mdi:calendar-month
  
  - type: custom:button-card
    entity: sensor.f1_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('');
        ]]]

in your post below you show the calendars…
I tried to implement, but some how it’s coming up in a non english languages.
below is the yaml. any idea where the problem is coming from?

- type: custom:button-card
        - 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('');
        ]]]