I’ve introduced improved documentation for the F1 Sensor, see here.
If you want your project showcased among the examples or have suggestions for what else should be included in the documentation, just let me know!
I’ve introduced improved documentation for the F1 Sensor, see here.
If you want your project showcased among the examples or have suggestions for what else should be included in the documentation, just let me know!
You have to install the F1 font. Info on how to on previous posts. Make sure to rename f1regular in my code with your font name.
Also, the headshots are taken from sensor.f1_driver_list. Look and see if you have that sensor enabled. Also the drivers colors are off (fallback color), which tells me there is something wrong with that sensor, because also the drivers color is there.
Hint: if you are using Chrome as browser, CTRL+F5 does not do a proper hard refresh, to see if the font applied. Instead, open Developer tools (CTRL+SHIFT+I), right click on refresh icon and select Empty Cache and Hard Reload. ![]()
had a silly thought today at work
1: your sensor may have a 40 second delay ? BUT you tell it 30 seconds away (this give seconds to play with)
2: by automation you delay your “true” lights by the 10 seconds you fail to give it in the first place (returning everything back in syn)
3: now have a ten second warning of future events to use how you like, if this is a count down lights to the start of the race, or what ever you may wish to dream up
food for thought
Hey guys, sorry, I’m not very experienced/tech-savvy (or I’m new to this), it’s my first time trying to add a font.
I followed [this] exactly, but I think I messed something up. These are the parts I used:
And this is what the file looks like inside Home Assistant (i change the name of the file font in f1regular.woff:
/* F1 Bold */
@font-face {
font-family: 'F1Bold';
src: url('/local/fonts/formula1-bold.woff') format('woff');
font-weight: normal;
font-style: normal;
}
/* F1 Regular */
@font-face {
font-family: 'F1Regular';
src: url('/local/fonts/f1regular.woff') format('woff');
font-weight: normal;
font-style: normal;
}
/* F1 Wide */
@font-face {
font-family: 'F1Wide';
src: url('/local/fonts/formula1-wide.woff') format('woff');
font-weight: normal;
font-style: normal;
}
and in my home assistant i save this file f1regular.woff
Maybe that’s where the problem is? Did I interpret something wrong? Because I wanted them (the fonts/characters) to show up in those amazing cards made by boredmthfkr, but that’s not happening.
For convenience, I’m reposting the code, but it’s the same code copied from a few posts above:
type: custom:button-card
entity: sensor.f1_driver_standings
show_name: false
show_state: false
show_icon: false
layout: custom
styles:
grid:
- grid-template-areas: |
"header"
"results"
- row-gap: 12px
card:
- padding: 12px
- border-radius: 8px
- background: linear-gradient(135deg, rgba(30, 30, 30, 0.8), rgba(10, 10, 10, 0.8))
- color: white
- box-shadow: 0px 4px 10px rgba(0,0,0,0.3)
custom_fields:
header:
- font-size: 20px
- font-weight: 600
- text-align: center
- padding-bottom: 6px
- border-bottom: 1px solid rgba(255,255,255,0.2)
- cursor: default
- font-family: f1regular
results:
- font-size: 14px
- line-height: 1.6
- display: flex
- flex-direction: column
- gap: 6px
- font-family: f1regular
- overflow-y: auto
- max-height: 600px
custom_fields:
header: |
[[[
// SIMPLE, NON-INTERACTIVE HEADER
return `
<div style="width: 100%; text-align: center;">
<div style="display: inline-flex; align-items: center; gap: 8px;">
<span style="font-weight: 600; font-family: 'f1regular';">🏁 DRIVERS STANDINGS</span>
</div>
</div>
`;
]]]
results: |
[[[
const results = entity.attributes?.driver_standings || [];
const displayResults = results.slice(0, 25);
const getDriverData = () => {
const driverList = states['sensor.f1_driver_list']?.attributes?.drivers || [];
const dataMap = {};
driverList.forEach(d => {
if (d.tla) {
dataMap[d.tla] = {
color: d.team_color,
headshot: d.headshot_small
};
}
});
return dataMap;
};
const teamLogos = {
"Red Bull": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/redbullracing/2025redbullracinglogo.webp",
"Ferrari": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/ferrari/2025ferrarilogo.webp",
"Mercedes": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/mercedes/2025mercedeslogowhite.webp",
"McLaren": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/mclaren/2025mclarenlogo.webp",
"Aston Martin": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/astonmartin/2025astonmartinlogowhite.webp",
"Alpine F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/alpine/2025alpinelogo.webp",
"RB F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/racingbulls/2025racingbullslogowhite.webp",
"Haas F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/haas/2025haaslogo.webp",
"Williams": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/williams/2025williamslogo.webp",
"Sauber": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/kicksauber/2025kicksauberlogo.webp",
};
const driverData = getDriverData();
const FALLBACK_COLOR = '#888';
const FALLBACK_ICON = 'mdi:racing-helmet';
const FONT_STYLE = "font-family: 'f1regular';";
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.Constructors?.[0]?.name || '';
const points = r.points || '0';
const driverSpecificData = driverData[code] || {};
const color = driverSpecificData.color || FALLBACK_COLOR;
const teamLogoUrl = teamLogos[team] || '';
const headshotUrl = driverSpecificData.headshot;
const bgColor = color.length === 7 ? color + '1A' : color;
const posTextColor = color === FALLBACK_COLOR ? 'white' : color;
const IMG_SIZE = '45px';
const ICON_SIZE = '45px';
let headshotTag;
if (headshotUrl) {
headshotTag = `<img src="${headshotUrl}" style="width: ${IMG_SIZE}; height: ${IMG_SIZE}; border-radius: 50%; object-fit: cover;">`;
} else {
// FIX: Wrapped ha-icon in a flex container to ensure perfect vertical and horizontal centering.
headshotTag = `
<div style="
display: flex;
align-items: center;
justify-content: center;
width: ${IMG_SIZE};
height: ${IMG_SIZE};
border-radius: 50%;
background: rgba(255, 255, 255, 0);
line-height: 1;
">
<ha-icon icon="${FALLBACK_ICON}" style="--mdc-icon-size: ${ICON_SIZE}; color: ${posTextColor};"></ha-icon>
</div>
`;
}
const logoTag = teamLogoUrl
? `<img src="${teamLogoUrl}" style="width: ${IMG_SIZE}; height: auto; max-height: ${ICON_SIZE};">`
: '';
return `
<div style="
display: grid;
grid-template-columns: 40px 1fr auto;
column-gap: 2px;
align-items: center;
background: ${bgColor};
padding: 1px 6px;
border-radius: 6px;
${FONT_STYLE}
">
<div style="
font-size: 30px;
font-weight: 900;
color: ${posTextColor};
text-align: center;
${FONT_STYLE}
">
${pos}
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<div style="
display: flex;
align-items: center;
height: 30px;
gap: 2px;
padding-right: 2px;
">
${headshotTag}
${logoTag}
</div>
<div style="display: flex; flex-direction: column; line-height: 1.2;">
<div style="color: var(--primary-text-color); font-weight: 600; font-size: 13px;">${code} - ${name}</div>
<div style="font-size: 10px; color: rgba(255, 255, 255, 0.8);">${team}</div>
</div>
</div>
<div style="
font-size: 18px;
font-weight: 900;
color: var(--primary-text-color);
text-align: right;
${FONT_STYLE}
">
${points} <span style="font-size: 10px; font-weight: 600;">PTS</span>
</div>
</div>
`;
}).join('');
]]]
I decided to change the font file’s name to match the existing code, but I’m not certain if I executed these steps properly.
But the font/characters didn’t show up on the card, not on my PC, tablet, or smartphone, even after clearing the cache:
this is from pc:
Can anyone tell me where I went wrong?
Thanks
Have you completed steps 4, 5 & 6 from the instructions you quoted (as you don’t mention that in your post)?
Also the font name vs the code in the card may be case sensitive (can’t remember 100%) so you might look at that too as they are different in your code snippets…
Yes, I attempted to add it twice because I was unsure whether to define it as a JavaScript or Stylesheet resource. I have since removed the duplicate entry and only kept the Stylesheet type, and it appears to be working on my phone now. ![]()
Does anyone know how I can turn the race control messages into notifications on my phone? f1_sensor_race_control_event
Funny, I came here looking for a way to use this integration to show the race control and radio messages on a tablet like an iPad during the races live. Kinda like they show in the corners on the TV screen for some of the messages. But I’d love to see all of them if possible.
I figure someone has already got that working. That’s probably even mentioned in this long thread somewhere. But I can’t look at it right now while I’m watching this fun qualy…
This integration won’t do radio messages, just race control messages
There is an example in the docs,
I missed that, but I want it to notify my phone only when it’s live session. I came up with this with AI, but not sure how good it is.
id: '1692023060001'
alias: F1 Race Control Mobile Notifications # <--- The updated name
description: Fires notification when session is one of multiple states and TV is on.
trigger:
- platform: event
event_type: f1_sensor_race_control_event
condition:
# CONDITION 1: F1 Session Status Check (Checks against the list of states)
- condition: template
value_template: >
{{ states('sensor.f1_session_status') in ['live', 'suspended', 'finished', 'break', 'finalised'] }}
# CONDITION 2: TV is On
- condition: state
entity_id: media_player.living_room_tv_2
state: 'on'
action:
- service: notify.mobile_app_thomas_phone
data_template:
title: '🏁 F1 Race Control Update ({{ trigger.event.data.message.Category | default("Event") }})'
message: '{{ trigger.event.data.message.Message }}'
data:
priority: 'high'
ttl: 0
icon: '{{ "mdi:alert-octagon" if "Flag" in trigger.event.data.message.Category else "mdi:information" }}'
# Templates the channel to group messages vs. flags
channel: >
{% if 'Flag' in trigger.event.data.message.Category %}
f1_flags
{% else %}
f1_messages
{% endif %}
mode: single
type: custom:button-card
entity: sensor.f1_track_status
show_state: true
show_name: false
styles:
card:
- height: 120px
- border-radius: 10px
state:
- font-size: 72px
- font-weight: bold
- text-align: center
state:
- value: CLEAR
styles:
card:
- background-color: "#4CAF50"
state:
- color: white
- value: RED
styles:
card:
- background-color: "#f44336"
state:
- color: white
- value: YELLOW
styles:
card:
- background-color: "#FFC107"
state:
- color: black
- value: VSC
styles:
card:
- background-color: "#FFC107"
state:
- color: black
a bit late here tho!..
some guidance Stimo?
{{ states('sensor.f1_track_status') | lower }}
Radio messages is not provided in text in the API, there is a limited selection of communications are included in form of URL to a MP3 file. I have added this to the integration yet
Update on my cards. I made them responsive to phone/tablet use. On my phone the info displayed was incomplete or very crowded. You can adjust it to your liking by modifying the sizes in the //Responsive sizing section.
Also, added driver’s racing number before the team name (way too much info on this card to place it elsewhere, on my opinion)
Drivers standings
type: custom:button-card
entity: sensor.f1_driver_standings
show_name: false
show_state: false
show_icon: false
layout: custom
styles:
grid:
- grid-template-areas: |
"header"
"results"
- row-gap: 12px
card:
- padding: 12px
- border-radius: 8px
- background: linear-gradient(135deg, rgba(30, 30, 30, 0.8), rgba(10, 10, 10, 0.8))
- color: white
- box-shadow: 0px 4px 10px rgba(0,0,0,0.3)
custom_fields:
header:
- font-size: 20px
- font-weight: 600
- text-align: center
- padding-bottom: 6px
- border-bottom: 1px solid rgba(255,255,255,0.2)
- cursor: default
- font-family: f1regular
results:
- font-size: 14px
- line-height: 1.6
- display: flex
- flex-direction: column
- gap: 6px
- font-family: f1regular
- overflow-y: auto
- max-height: 600px
custom_fields:
header: |
[[[
return `
<div style="width: 100%; text-align: center;">
<div style="display: inline-flex; align-items: center; gap: 8px;">
<span style="font-weight: 600; font-family: 'f1regular';">🏁 DRIVERS STANDINGS</span>
</div>
</div>
`;
]]]
results: |
[[[
const results = entity.attributes?.driver_standings || [];
const displayResults = results.slice(0, 25);
const getDriverData = () => {
const driverList = states['sensor.f1_driver_list']?.attributes?.drivers || [];
const dataMap = {};
driverList.forEach(d => {
if (d.tla) {
dataMap[d.tla] = {
color: d.team_color,
headshot: d.headshot_small,
racing_number: d.racing_number
};
}
});
return dataMap;
};
const teamLogos = {
"Red Bull": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/redbullracing/2025redbullracinglogo.webp",
"Ferrari": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/ferrari/2025ferrarilogo.webp",
"Mercedes": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/mercedes/2025mercedeslogowhite.webp",
"McLaren": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/mclaren/2025mclarenlogo.webp",
"Aston Martin": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/astonmartin/2025astonmartinlogowhite.webp",
"Alpine F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/alpine/2025alpinelogo.webp",
"RB F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/racingbulls/2025racingbullslogowhite.webp",
"Haas F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/haas/2025haaslogo.webp",
"Williams": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/williams/2025williamslogo.webp",
"Sauber": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/kicksauber/2025kicksauberlogo.webp",
};
const driverData = getDriverData();
const FALLBACK_COLOR = '#888';
const FALLBACK_ICON = 'mdi:racing-helmet';
const FONT_STYLE = "font-family: 'f1regular';";
// Detect mobile screen
const isMobile = window.innerWidth <= 768;
// Responsive sizing
const IMG_SIZE = isMobile ? '35px' : '45px';
const ICON_SIZE = isMobile ? '35px' : '45px';
const POS_FONT = isMobile ? '24px' : '30px';
const NAME_FONT = isMobile ? '11px' : '13px';
const TEAM_FONT = isMobile ? '9px' : '10px';
const PTS_FONT = isMobile ? '14px' : '18px';
const PTS_LABEL_FONT = isMobile ? '8px' : '10px';
const GAP = isMobile ? '4px' : '8px';
const PADDING = isMobile ? '1px 4px' : '1px 6px';
const POS_WIDTH = isMobile ? '30px' : '40px';
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.Constructors?.[0]?.name || '';
const points = r.points || '0';
const driverSpecificData = driverData[code] || {};
const color = driverSpecificData.color || FALLBACK_COLOR;
const teamLogoUrl = teamLogos[team] || '';
const headshotUrl = driverSpecificData.headshot;
const racingNumber = driverSpecificData.racing_number || '';
const bgColor = color.length === 7 ? color + '1A' : color;
const posTextColor = color === FALLBACK_COLOR ? 'white' : color;
let headshotTag;
if (headshotUrl) {
headshotTag = `<img src="${headshotUrl}" style="width: ${IMG_SIZE}; height: ${IMG_SIZE}; border-radius: 50%; object-fit: cover;">`;
} else {
headshotTag = `
<div style="
display: flex;
align-items: center;
justify-content: center;
width: ${IMG_SIZE};
height: ${IMG_SIZE};
border-radius: 50%;
background: rgba(255, 255, 255, 0);
line-height: 1;
">
<ha-icon icon="${FALLBACK_ICON}" style="--mdc-icon-size: ${ICON_SIZE}; color: ${posTextColor};"></ha-icon>
</div>
`;
}
const logoTag = teamLogoUrl
? `<img src="${teamLogoUrl}" style="width: ${IMG_SIZE}; height: auto; max-height: ${ICON_SIZE};">`
: '';
const secondLine = racingNumber ? `#${racingNumber} - ${team}` : team;
return `
<div style="
display: grid;
grid-template-columns: ${POS_WIDTH} 1fr auto;
column-gap: 2px;
align-items: center;
background: ${bgColor};
padding: ${PADDING};
border-radius: 6px;
${FONT_STYLE}
">
<div style="
font-size: ${POS_FONT};
font-weight: 900;
color: ${posTextColor};
text-align: center;
${FONT_STYLE}
">
${pos}
</div>
<div style="display: flex; align-items: center; gap: ${GAP};">
<div style="
display: flex;
align-items: center;
height: 30px;
gap: 2px;
padding-right: 2px;
">
${headshotTag}
${logoTag}
</div>
<div style="display: flex; flex-direction: column; line-height: 1.2;">
<div style="color: var(--primary-text-color); font-weight: 600; font-size: ${NAME_FONT};">${code} - ${name}</div>
<div style="font-size: ${TEAM_FONT}; color: rgba(255, 255, 255, 0.8);">${secondLine}</div>
</div>
</div>
<div style="
font-size: ${PTS_FONT};
font-weight: 900;
color: var(--primary-text-color);
text-align: right;
${FONT_STYLE}
">
${points} <span style="font-size: ${PTS_LABEL_FONT}; font-weight: 600;">PTS</span>
</div>
</div>
`;
}).join('');
]]]
Constructors standings
type: custom:button-card
entity: sensor.f1_constructor_standings
show_name: false
show_state: false
show_icon: false
layout: custom
styles:
grid:
- grid-template-areas: |
"header"
"results"
- row-gap: 12px
card:
- padding: 12px
- border-radius: 8px
- background: linear-gradient(135deg, rgba(30, 30, 30, 0.8), rgba(10, 10, 10, 0.8))
- color: white
- box-shadow: 0px 4px 10px rgba(0,0,0,0.3)
custom_fields:
header:
- font-size: 20px
- font-weight: 600
- text-align: center
- padding-bottom: 6px
- border-bottom: 1px solid rgba(255,255,255,0.2)
- cursor: default
- font-family: f1regular
results:
- font-size: 14px
- line-height: 1.6
- display: flex
- flex-direction: column
- gap: 6px
- font-family: f1regular
- overflow-y: auto
- max-height: 650px
custom_fields:
header: |
[[[
return `
<div style="width: 100%; text-align: center;">
<div style="display: inline-flex; align-items: center; gap: 8px;">
<span style="font-weight: 600; font-family: 'f1regular';">🔧 CONSTRUCTORS STANDINGS</span>
</div>
</div>
`;
]]]
results: |
[[[
const results = entity.attributes?.constructor_standings || [];
const displayResults = results.slice(0, 25);
const FALLBACK_COLOR = '#888';
const FONT_STYLE = "font-family: 'f1regular';";
const constructorColors = {
"McLaren": "#FF8700",
"Mercedes": "#00D2BE",
"Red Bull": "#1E41FF",
"Ferrari": "#DC0000",
"Williams": "#005AFF",
"RB F1 Team": "#6699FF",
"Aston Martin": "#006F62",
"Haas F1 Team": "#B6BABD",
"Sauber": "#52E252",
"Alpine F1 Team": "#0090FF"
};
const carImages = {
"McLaren": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/mclaren/2025mclarencarright.webp",
"Mercedes": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/mercedes/2025mercedescarright.webp",
"Red Bull": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/redbullracing/2025redbullracingcarright.webp",
"Ferrari": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/ferrari/2025ferraricarright.webp",
"Williams": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/williams/2025williamscarright.webp",
"RB F1 Team": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/racingbulls/2025racingbullscarright.webp",
"Aston Martin": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/astonmartin/2025astonmartincarright.webp",
"Haas F1 Team": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/haas/2025haascarright.webp",
"Sauber": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/kicksauber/2025kicksaubercarright.webp",
"Alpine F1 Team": "https://media.formula1.com/image/upload/c_lfill,w_3392/q_auto/v1740000000/common/f1/2025/alpine/2025alpinecarright.webp"
};
const teamLogos = {
"Red Bull": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/redbullracing/2025redbullracinglogo.webp",
"Ferrari": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/ferrari/2025ferrarilogo.webp",
"Mercedes": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/mercedes/2025mercedeslogowhite.webp",
"McLaren": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/mclaren/2025mclarenlogo.webp",
"Aston Martin": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/astonmartin/2025astonmartinlogowhite.webp",
"Alpine F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/alpine/2025alpinelogo.webp",
"RB F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/racingbulls/2025racingbullslogowhite.webp",
"Haas F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/haas/2025haaslogo.webp",
"Williams": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/williams/2025williamslogo.webp",
"Sauber": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/kicksauber/2025kicksauberlogo.webp",
};
// Detect mobile screen
const isMobile = window.innerWidth <= 768;
// Responsive sizing
const LOGO_SIZE = isMobile ? '35px' : '45px';
const CAR_IMG_WIDTH = isMobile ? '80px' : '130px';
const POS_FONT = isMobile ? '24px' : '30px';
const NAME_FONT = isMobile ? '11px' : '13px';
const WINS_FONT = isMobile ? '9px' : '10px';
const PTS_FONT = isMobile ? '16px' : '20px';
const PTS_LABEL_FONT = isMobile ? '8px' : '10px';
const GAP = isMobile ? '4px' : '8px';
const PADDING = isMobile ? '1px 4px' : '1px 6px';
const POS_WIDTH = isMobile ? '30px' : '40px';
return displayResults.map((r) => {
const pos = r.position;
const name = r.Constructor?.name || 'Unknown';
const points = r.points || '0';
const color = constructorColors[name] || FALLBACK_COLOR;
const carImageUrl = carImages[name] || '';
const teamLogoUrl = teamLogos[name] || '';
const bgColor = color.length === 7 ? color + '1A' : color;
const posTextColor = color === FALLBACK_COLOR ? 'white' : color;
const carImageTag = carImageUrl
? `<img src="${carImageUrl}" style="width: ${CAR_IMG_WIDTH}; height: auto; max-height: ${LOGO_SIZE}; object-fit: cover;">`
: '';
const logoTag = teamLogoUrl
? `<img src="${teamLogoUrl}" style="width: ${LOGO_SIZE}; height: auto; max-height: ${LOGO_SIZE};">`
: '';
return `
<div style="
display: grid;
grid-template-columns: ${POS_WIDTH} 1fr auto;
column-gap: 2px;
align-items: center;
background: ${bgColor};
padding: ${PADDING};
border-radius: 6px;
${FONT_STYLE}
">
<div style="
font-size: ${POS_FONT};
font-weight: 900;
color: ${posTextColor};
text-align: center;
${FONT_STYLE}
">
${pos}
</div>
<div style="display: flex; align-items: center; gap: ${GAP};">
<div style="
display: flex;
align-items: center;
height: 30px;
gap: 2px;
padding-right: 2px;
">
${logoTag}
</div>
<div style="
display: flex;
flex-direction: column;
line-height: 1.2;
align-items: center;
flex-grow: 1;
">
<div style="color: var(--primary-text-color); font-weight: 600; font-size: ${NAME_FONT};">${name}</div>
<div style="font-size: ${WINS_FONT}; color: rgba(255, 255, 255, 0.8);">Wins: ${r.wins}</div>
</div>
</div>
<div style="
display: flex;
align-items: center;
justify-content: flex-end;
gap: ${GAP};
font-size: ${PTS_FONT};
font-weight: 900;
color: var(--primary-text-color);
${FONT_STYLE}
">
${carImageTag}
<div>
${points} <span style="font-size: ${PTS_LABEL_FONT}; font-weight: 600;">PTS</span>
</div>
</div>
</div>
`;
}).join('');
]]]
Or… I got another crazy idea… Drivers standings with driver’s racing number as a badge on the driver’s headshot…
type: custom:button-card
entity: sensor.f1_driver_standings
show_name: false
show_state: false
show_icon: false
layout: custom
styles:
grid:
- grid-template-areas: |
"header"
"results"
- row-gap: 12px
card:
- padding: 12px
- border-radius: 8px
- background: linear-gradient(135deg, rgba(30, 30, 30, 0.8), rgba(10, 10, 10, 0.8))
- color: white
- box-shadow: 0px 4px 10px rgba(0,0,0,0.3)
custom_fields:
header:
- font-size: 20px
- font-weight: 600
- text-align: center
- padding-bottom: 6px
- border-bottom: 1px solid rgba(255,255,255,0.2)
- cursor: default
- font-family: f1regular
results:
- font-size: 14px
- line-height: 1.6
- display: flex
- flex-direction: column
- gap: 6px
- font-family: f1regular
- overflow-y: auto
- max-height: 600px
custom_fields:
header: |
[[[
return `
<div style="width: 100%; text-align: center;">
<div style="display: inline-flex; align-items: center; gap: 8px;">
<span style="font-weight: 600; font-family: 'f1regular';">🏁 DRIVERS STANDINGS</span>
</div>
</div>
`;
]]]
results: |
[[[
const results = entity.attributes?.driver_standings || [];
const displayResults = results.slice(0, 25);
const getDriverData = () => {
const driverList = states['sensor.f1_driver_list']?.attributes?.drivers || [];
const dataMap = {};
driverList.forEach(d => {
if (d.tla) {
dataMap[d.tla] = {
color: d.team_color,
headshot: d.headshot_small,
racing_number: d.racing_number
};
}
});
return dataMap;
};
const teamLogos = {
"Red Bull": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/redbullracing/2025redbullracinglogo.webp",
"Ferrari": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/ferrari/2025ferrarilogo.webp",
"Mercedes": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/mercedes/2025mercedeslogowhite.webp",
"McLaren": "https://media.formula1.com/image/upload/c_lfill,w_48/q_auto/v1740000000/common/f1/2025/mclaren/2025mclarenlogo.webp",
"Aston Martin": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/astonmartin/2025astonmartinlogowhite.webp",
"Alpine F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/alpine/2025alpinelogo.webp",
"RB F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/racingbulls/2025racingbullslogowhite.webp",
"Haas F1 Team": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/haas/2025haaslogo.webp",
"Williams": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/williams/2025williamslogo.webp",
"Sauber": "https://media.formula1.com/image/upload/c_fit,h_64/q_auto/v1740000000/common/f1/2025/kicksauber/2025kicksauberlogo.webp",
};
const driverData = getDriverData();
const FALLBACK_COLOR = '#888';
const FALLBACK_ICON = 'mdi:racing-helmet';
const FONT_STYLE = "font-family: 'f1regular';";
// Detect mobile screen
const isMobile = window.innerWidth <= 768;
// Responsive sizing
const IMG_SIZE = isMobile ? '35px' : '45px';
const ICON_SIZE = isMobile ? '35px' : '45px';
const POS_FONT = isMobile ? '24px' : '30px';
const NAME_FONT = isMobile ? '11px' : '13px';
const TEAM_FONT = isMobile ? '9px' : '10px';
const PTS_FONT = isMobile ? '14px' : '18px';
const PTS_LABEL_FONT = isMobile ? '8px' : '10px';
const GAP = isMobile ? '4px' : '8px';
const PADDING = isMobile ? '1px 4px' : '1px 6px';
const POS_WIDTH = isMobile ? '30px' : '40px';
const BADGE_FONT = isMobile ? '12px' : '15px';
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.Constructors?.[0]?.name || '';
const points = r.points || '0';
const driverSpecificData = driverData[code] || {};
const color = driverSpecificData.color || FALLBACK_COLOR;
const teamLogoUrl = teamLogos[team] || '';
const headshotUrl = driverSpecificData.headshot;
const racingNumber = driverSpecificData.racing_number || '';
const bgColor = color.length === 7 ? color + '1A' : color;
const posTextColor = color === FALLBACK_COLOR ? 'white' : color;
let headshotTag;
if (headshotUrl) {
headshotTag = `
<div style="position: relative; width: ${IMG_SIZE}; height: ${IMG_SIZE};">
<img src="${headshotUrl}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
${racingNumber ? `
<div style="
position: absolute;
bottom: -2px;
right: -2px;
color: ${color};
font-size: ${BADGE_FONT};
font-weight: 900;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
0 0 3px rgba(0,0,0,0.8);
">${racingNumber}</div>
` : ''}
</div>
`;
} else {
headshotTag = `
<div style="
display: flex;
align-items: center;
justify-content: center;
width: ${IMG_SIZE};
height: ${IMG_SIZE};
border-radius: 50%;
background: rgba(255, 255, 255, 0);
line-height: 1;
">
<ha-icon icon="${FALLBACK_ICON}" style="--mdc-icon-size: ${ICON_SIZE}; color: ${posTextColor};"></ha-icon>
</div>
`;
}
const logoTag = teamLogoUrl
? `<img src="${teamLogoUrl}" style="width: ${IMG_SIZE}; height: auto; max-height: ${ICON_SIZE};">`
: '';
return `
<div style="
display: grid;
grid-template-columns: ${POS_WIDTH} 1fr auto;
column-gap: 2px;
align-items: center;
background: ${bgColor};
padding: ${PADDING};
border-radius: 6px;
${FONT_STYLE}
">
<div style="
font-size: ${POS_FONT};
font-weight: 900;
color: ${posTextColor};
text-align: center;
${FONT_STYLE}
">
${pos}
</div>
<div style="display: flex; align-items: center; gap: ${GAP};">
<div style="
display: flex;
align-items: center;
height: 30px;
gap: 2px;
padding-right: 2px;
">
${headshotTag}
${logoTag}
</div>
<div style="display: flex; flex-direction: column; line-height: 1.2;">
<div style="color: var(--primary-text-color); font-weight: 600; font-size: ${NAME_FONT};">${code} - ${name}</div>
<div style="font-size: ${TEAM_FONT}; color: rgba(255, 255, 255, 0.8);">${team}</div>
</div>
</div>
<div style="
font-size: ${PTS_FONT};
font-weight: 900;
color: var(--primary-text-color);
text-align: right;
${FONT_STYLE}
">
${points} <span style="font-size: ${PTS_LABEL_FONT}; font-weight: 600;">PTS</span>
</div>
</div>
`;
}).join('');
]]]
I really love your card, amazing job!
This is a great example for the documentation of what you can do with the integration.
If you are up for it, let me know in a PM and we can take it from there.
Nice,
Can wait to get realtime track positions and gaps.
This is a card i use on my dashboard, the rest of the F1 dash uses the F1 card.
entity: sensor.f1_driver_standings
content: >
{% set standings = state_attr('sensor.f1_driver_standings',
'driver_standings') %} {% set p1 = standings[0] %} {% set p2 = standings[1] %}
{% set p3 = standings[2] %} {% set p1_points = p1.points | int %} {% set
p2_points = p2.points | int %} {% set p3_points = p3.points | int %}
{% set current_round = state_attr('sensor.f1_driver_standings', 'round') | int
%} {% set all_races = state_attr('sensor.f1_current_season', 'races') %}
{# Bereken nog te behalen punten #} {% set remaining_points =
namespace(total=0) %} {% for race in all_races %}
{% if race.round | int > current_round %}
{% set remaining_points.total = remaining_points.total + 25 %}
{% if race.Sprint is defined %}
{% set remaining_points.total = remaining_points.total + 8 %}
{% endif %}
{% endif %}
{% endfor %}
{% set gap_p2 = p1_points - p2_points %} {% set gap_p3 = p1_points - p3_points
%}
{# Vaste minimum breedte voor leesbaarheid (4 karakters + spacing) #} {% set
min_width = 60 %} {% set total_width = 1000 %}
{# P1 berekening - basis voor alle anderen #} {% set p1_total = p1_points +
remaining_points.total %} {% set available_p1 = total_width - (2 * min_width)
%} {% set p1_progress_pct = (p1_points / p1_total * 100) if p1_total > 0 else
100 %} {% set p1_remaining_pct = (remaining_points.total / p1_total * 100) if
p1_total > 0 else 0 %}
{% set p1_progress_width = min_width + ((p1_progress_pct / 100) *
available_p1) | round(0) %} {% set p1_remaining_width = min_width +
((p1_remaining_pct / 100) * available_p1) | round(0) %}
{# P2 berekening - relatief aan P1 #} {% set p2_remaining_width =
p1_remaining_width %} {% set available_p2 = total_width - p2_remaining_width
%} {% set to_distribute_p2 = available_p2 - (2 * min_width) %} {% set p2_ratio
= (p2_points / p1_points) if p1_points > 0 else 1 %} {% set p2_progress_extra
= (p2_ratio * to_distribute_p2) | round(0) %} {% set p2_gap_extra =
to_distribute_p2 - p2_progress_extra %} {% set p2_progress_width = min_width +
p2_progress_extra %} {% set p2_gap_width = min_width + p2_gap_extra %}
{# P3 berekening - relatief aan P1 #} {% set p3_remaining_width =
p1_remaining_width %} {% set available_p3 = total_width - p3_remaining_width
%} {% set to_distribute_p3 = available_p3 - (2 * min_width) %} {% set p3_ratio
= (p3_points / p1_points) if p1_points > 0 else 1 %} {% set p3_progress_extra
= (p3_ratio * to_distribute_p3) | round(0) %} {% set p3_gap_extra =
to_distribute_p3 - p3_progress_extra %} {% set p3_progress_width = min_width +
p3_progress_extra %} {% set p3_gap_width = min_width + p3_gap_extra %}
<div class="bg-white rounded-lg shadow-md overflow-hidden p-4">
<!-- P1 - Gold -->
<div class="mb-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-8 h-8 rounded-full text-white font-bold shadow-md text-sm flex-shrink-0" style="background: linear-gradient(135deg, #E6BE8A 0%, #D4A574 100%);">
1
</div>
<img src="{{ state_attr('image.f1_ch1', 'entity_picture') }}"
class="w-10 h-10 rounded-full object-cover ring-2 flex-shrink-0" style="border-color: #E6BE8A;">
<div class="flex-shrink-0" style="min-width: 150px;">
<div class="font-bold text-gray-900 text-sm">{{ p1.Driver.givenName }} {{ p1.Driver.familyName }}</div>
<div class="text-xs text-gray-500">{{ p1.Constructors[0].name }}</div>
</div>
<div class="flex items-center h-8">
<div class="h-full flex items-center px-3 {% if remaining_points.total == 0 %}rounded-full{% else %}rounded-l-full{% endif %}" style="background-color: #E6BE8A; width: {{ p1_progress_width }}px;">
<span class="text-gray-900 font-bold text-sm">{{ p1_points }}</span>
</div>
{% if remaining_points.total > 0 %}
<div class="bg-gray-300 h-full flex items-center justify-center rounded-r-full" style="width: {{ p1_remaining_width }}px;">
<span class="text-gray-600 font-medium text-sm">+{{ remaining_points.total }}</span>
</div>
{% endif %}
</div>
</div>
</div>
<!-- P2 - Silver -->
<div class="mb-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-8 h-8 rounded-full text-white font-bold shadow-md text-sm flex-shrink-0" style="background: linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%);">
2
</div>
<img src="{{ state_attr('image.f1_ch2', 'entity_picture') }}"
class="w-10 h-10 rounded-full object-cover ring-2 flex-shrink-0" style="border-color: #C0C0C0;">
<div class="flex-shrink-0" style="min-width: 150px;">
<div class="font-bold text-gray-900 text-sm">{{ p2.Driver.givenName }} {{ p2.Driver.familyName }}</div>
<div class="text-xs text-gray-500">{{ p2.Constructors[0].name }}</div>
</div>
<div class="flex items-center h-8">
<div class="h-full flex items-center px-3 rounded-l-full" style="background-color: #C0C0C0; width: {{ p2_progress_width }}px;">
<span class="text-gray-900 font-bold text-sm">{{ p2_points }}</span>
</div>
{% if gap_p2 > 0 %}
<div class="h-full flex items-center justify-center {% if remaining_points.total == 0 %}rounded-r-full{% endif %}" style="background-color: #E8E8E8; width: {{ p2_gap_width }}px;">
<span class="text-gray-600 font-bold text-base">-{{ gap_p2 }}</span>
</div>
{% endif %}
{% if remaining_points.total > 0 %}
<div class="bg-gray-300 h-full flex items-center justify-center rounded-r-full" style="width: {{ p2_remaining_width }}px;">
<span class="text-gray-600 font-medium text-sm">+{{ remaining_points.total }}</span>
</div>
{% endif %}
</div>
</div>
</div>
<!-- P3 - Bronze -->
<div>
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-8 h-8 rounded-full text-white font-bold shadow-md text-sm flex-shrink-0" style="background: linear-gradient(135deg, #CD7F32 0%, #B8692A 100%);">
3
</div>
<img src="{{ state_attr('image.f1_ch3', 'entity_picture') }}"
class="w-10 h-10 rounded-full object-cover ring-2 flex-shrink-0" style="border-color: #CD7F32;">
<div class="flex-shrink-0" style="min-width: 150px;">
<div class="font-bold text-gray-900 text-sm">{{ p3.Driver.givenName }} {{ p3.Driver.familyName }}</div>
<div class="text-xs text-gray-500">{{ p3.Constructors[0].name }}</div>
</div>
<div class="flex items-center h-8">
<div class="h-full flex items-center px-3 rounded-l-full" style="background-color: #CD7F32; width: {{ p3_progress_width }}px;">
<span class="text-white font-bold text-sm">{{ p3_points }}</span>
</div>
{% if gap_p3 > 0 %}
<div class="h-full flex items-center justify-center {% if remaining_points.total == 0 %}rounded-r-full{% endif %}" style="background-color: #E8B896; width: {{ p3_gap_width }}px;">
<span class="text-gray-800 font-bold text-base">-{{ gap_p3 }}</span>
</div>
{% endif %}
{% if remaining_points.total > 0 %}
<div class="bg-gray-300 h-full flex items-center justify-center rounded-r-full" style="width: {{ p3_remaining_width }}px;">
<span class="text-gray-600 font-medium text-sm">+{{ remaining_points.total }}</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
ignore_line_breaks: true
always_update: false
parse_jinja: true
code_editor: Ace
entities: []
bindings: []
actions: []
debounceChangePeriod: 100
plugins:
daisyui:
enabled: true
url: https://cdn.jsdelivr.net/npm/daisyui@latest/dist/full.css
theme: dark - dark
overrideCardBackground: false
tailwindElements:
enabled: false
type: custom:tailwindcss-template-card
grid_options:
columns: full
It’s made with claude so not perfect but works good enough.
Also having a ticker tape at the top of my dash during every session showing remaining laps and track state/flags.
type: custom:html-template-card
ignore_line_breaks: true
title: ""
content: |
{% set session_status = states('sensor.f1_session_status') %}
{% set track_status = states('sensor.f1_track_status') %}
{% set current_lap = states('sensor.f1_race_lap_count') %}
{% set total_laps = states('sensor.f1_race_total_laps') %}
{% set current_session_name = states('sensor.f1_current_session') %}
{# ---- Tekst + achtergrondkleur bepalen ---- #}
{% set status_text = '' %}
{% set bg = '#4CAF50' %} {# standaard groen #}
{% set fg = 'white' %}
{% if track_status == 'YELLOW' %}
{% set status_text = 'YELLOW' %}
{% set bg = '#FCD34D' %}
{% set fg = 'black' %}
{% elif track_status == 'VSC' %}
{% set status_text = 'VIRTUAL SAFETY CAR' %}
{% set bg = '#FCD34D' %}
{% set fg = 'black' %}
{% elif track_status == 'SC' %}
{% set status_text = 'SAFETY CAR' %}
{% set bg = '#FCD34D' %}
{% set fg = 'black' %}
{% elif track_status == 'VSC_ENDING' %}
{% set status_text = 'SAFETY CAR ENDING' %}
{% set bg = '#FCD34D' %}
{% set fg = 'black' %}
{% elif track_status == 'RED' %}
{% set status_text = 'RED' %}
{% set bg = '#F87171' %}
{% set fg = 'white' %}
{% endif %}
<div class="f1-wrap" style="background-color: {{ bg }}; color: {{ fg }};">
{# ---- Left: Session Status (live, ended, etc.) ---- #}
<div class="f1-left">
{{ session_status }}
</div>
{# ---- Center: track warning OR session name if green ---- #}
<div class="f1-center">
{% if status_text %}
{{ status_text }}
{% elif bg == '#4CAF50' and current_session_name %}
{{ current_session_name }}
{% else %}
{% endif %}
</div>
{# ---- Right: lap counter (only for race or sprint) ---- #}
<div class="f1-right">
{% if current_lap not in ['unknown','unavailable','None'] and current_session_name in ['Race','Sprint'] %}
{% if total_laps not in ['unknown','unavailable','None','',None] %}
{{ current_lap }} / {{ total_laps }}
{% else %}
{{ current_lap }} / ?
{% endif %}
{% endif %}
</div>
</div>
<style>
.f1-wrap {
position: fixed;
top: 0;
left: 0;
width: 100%;
max-width: 1440px;
display: grid;
grid-template-columns: 1fr 2fr 1fr;
align-items: center;
padding: 1rem 2rem;
box-sizing: border-box;
font-size: 4rem;
font-weight: 700;
text-align: center;
z-index: 9999;
min-height: 7rem;
}
.f1-left { text-align: left; }
.f1-center { text-align: center; }
.f1-right { text-align: right; }
</style>
grid_options:
columns: full
rows: 2
When there is no session it’s hidden,