Hey!
Those separate sensors are really powerful, they give you a lot of freedom to build your own dashboard cards. I built the Results card shown here and I’m working on the rest, all without any issues so far. You can click the chevron icon to expand the card and reveal the full race results. The dashboard cards update automatically, even if a race finishes after midnight. The current race is highlighted with a glow effect, and the cards automatically adapt their layout when a sprint race weekend is detected.
Here’s an overview of all the cards I’ve built so far:
- Next Grand Prix — shows the upcoming race with session schedule, countdown timer, circuit map and live weather for both current conditions and race time forecast
- Season Calendar — full 2026 race calendar with sprint indicators, next race highlight and expandable to show all 24 rounds
- Driver Standings — top 10 with team colors and points bar, expandable to all 22 drivers
- Constructor Standings — same style as driver standings, expandable to all 11 teams
- Last Race Results — full results with team colors, points, DNF status and expandable to all 22 drivers
Required custom cards
custom button card
TailwindCSS Template Card
Vertical Stack In Card
Calendar Card Pro
card-mod 4
1. Next Grand Prix
Shows the upcoming race with a session schedule, countdown timer, circuit map and live weather forecast for both current conditions and race time. Sessions that are live are highlighted in yellow. Past sessions are greyed out. The card automatically adapts when a sprint race weekend is detected, sprint sessions only appear when scheduled.
type: custom:vertical-stack-in-card
cards:
- type: custom:tailwindcss-template-card
content: |
{% set s = 'sensor.f1_next_race' %}
{% set race = state_attr(s, 'race_name') %}
{% set circuit = state_attr(s, 'circuit_name') %}
{% set locality = state_attr(s, 'circuit_locality') %}
{% set country = state_attr(s, 'circuit_country') %}
{% set flag = state_attr(s, 'country_flag_url') %}
{% set race_start = state_attr(s, 'race_start') %}
{% set now_ts = now().timestamp() %}
{% set race_ts = as_timestamp(race_start) %}
{% set diff = race_ts - now_ts %}
{% set days = (diff / 86400) | int %}
{% set hours = ((diff % 86400) / 3600) | int %}
{% set mins = ((diff % 3600) / 60) | int %}
{% if diff > 0 %}
{% set status = "⏳ Over " ~ days ~ "d " ~ hours ~ "u " ~ mins ~ "m" %}
{% set status_color = "#FFF200" %}
{% elif diff > -5400 %}
{% set status = "🏁 Race is bezig!" %}
{% set status_color = "#E10600" %}
{% else %}
{% set status = "✅ Race afgelopen" %}
{% set status_color = "#aaaaaa" %}
{% endif %}
<div style="padding: 2px;">
<!-- Header -->
<div style="text-align: center; border-bottom: 1px solid rgba(225,6,0,0.25); padding-bottom: 6px; margin-bottom: 12px;">
<div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">🏎️ Volgende Grand Prix</div>
<div style="display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 4px;">
<img src="{{ flag }}" style="height: 18px; border-radius: 2px;">
<span style="font-size: 14px; font-weight: bold; color: #ffffff;">{{ race }}</span>
</div>
<div style="font-size: 13px; color: #888888; margin-top: 2px;">{{ circuit }} · {{ locality }}, {{ country }}</div>
<div style="font-size: 13px; font-weight: bold; margin-top: 4px; color: {{ status_color }};">{{ status }}</div>
</div>
<!-- Sessieschema -->
<div style="display: flex; flex-direction: column; gap: 6px;">
{% set sessions = [
('⚙️ VT1', state_attr(s, 'first_practice_start')),
('⚙️ VT2', state_attr(s, 'second_practice_start')),
('⚙️ VT3', state_attr(s, 'third_practice_start')),
('⚡ Sprint Kwal.', state_attr(s, 'sprint_qualifying_start')),
('⚡ Sprint', state_attr(s, 'sprint_start')),
('⏱️ Kwalificatie', state_attr(s, 'qualifying_start')),
('🏁 Race', state_attr(s, 'race_start')),
] %}
{% for label, dt in sessions %}
{% if dt %}
{% set ts = as_timestamp(dt) %}
{% set is_live = now_ts >= ts and now_ts <= (ts + 4500) %}
{% set is_past = now_ts > (ts + 4500) %}
<div style="display: flex; justify-content: space-between; align-items: center;
padding: 6px 10px; border-radius: 4px; height: 50px;
{% if is_live %}background: rgba(255,242,0,0.08); border: 1px solid #FFF200;
{% elif is_past %}background: rgba(255,255,255,0.04); border: 1px solid transparent;
{% else %}background: rgba(255,255,255,0.08); border: 1px solid transparent;{% endif %}">
<span style="font-size: 15px; color: {% if is_live %}#FFF200{% elif is_past %}#555555{% else %}#cccccc{% endif %}; font-weight: {% if is_live %}bold{% else %}normal{% endif %};">
{{ label }}{% if is_live %} 🔴 LIVE{% endif %}
</span>
<span style="font-size: 13px; font-weight: bold; color: {% if is_live %}#FFF200{% elif is_past %}#555555{% else %}#888888{% endif %};">
{{ as_timestamp(dt) | timestamp_custom('%a %d %b · %H:%M') }}
</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
card_mod:
style: |
ha-card {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
- type: map
entities:
- entity: sensor.f1_circuit_locatie
hours_to_show: 0
default_zoom: 10
theme_mode: auto
aspect_ratio: "2"
card_mod:
style: |
ha-card {
height: 220px !important;
border-radius: 0 !important;
border: none !important;
border-top: 1px solid rgba(225,6,0,0.25) !important;
border-bottom: 1px solid rgba(225,6,0,0.25) !important;
box-shadow: none !important;
}
.card-header { display: none !important; }
- type: custom:tailwindcss-template-card
content: >
{% set s = 'sensor.f1_weather' %}
{% set c_temp = state_attr(s, 'current_temperature') %}
{% set c_hum = state_attr(s, 'current_humidity') %}
{% set c_wind = state_attr(s, 'current_wind_speed') %}
{% set c_gust = state_attr(s, 'current_wind_gusts') %}
{% set c_dir = state_attr(s, 'current_wind_direction') %}
{% set c_rain = state_attr(s, 'current_precipitation_probability') %}
{% set c_cloud = state_attr(s, 'current_cloud_cover') %}
{% set r_temp = state_attr(s, 'race_temperature') %}
{% set r_hum = state_attr(s, 'race_humidity') %}
{% set r_wind = state_attr(s, 'race_wind_speed') %}
{% set r_gust = state_attr(s, 'race_wind_gusts') %}
{% set r_dir = state_attr(s, 'race_wind_direction') %}
{% set r_rain = state_attr(s, 'race_precipitation_probability') %}
{% set r_cloud = state_attr(s, 'race_cloud_cover') %}
{% macro beaufort(mps) %}{% set mps = mps | float %}{% if mps <= 0.3 %}0{%
elif mps <= 1.5 %}1{% elif mps <= 3.3 %}2{% elif mps <= 5.4 %}3{% elif mps
<= 7.9 %}4{% elif mps <= 10.7 %}5{% elif mps <= 13.8 %}6{% elif mps <=
17.1 %}7{% elif mps <= 20.7 %}8{% else %}9+{% endif %}{% endmacro %}
<div style="padding: 12px;">
<!-- Header -->
<div style="text-align: center; border-bottom: 1px solid rgba(225,6,0,0.25); padding-bottom: 6px; margin-bottom: 12px;">
<div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">🌤️ Circuit Weer</div>
<div style="font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 4px;">
{{ state_attr('sensor.f1_next_race', 'circuit_locality') }},
{{ state_attr('sensor.f1_next_race', 'circuit_country') }}
</div>
</div>
<!-- Twee kolommen -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<!-- NU -->
<div style="border-radius: 4px; padding: 10px; background: rgba(255,255,255,0.04); border: 1px solid transparent;">
<div style="font-size: 13px; font-weight: bold; text-transform: uppercase; text-align: center; color: #888888; margin-bottom: 8px;">Nu</div>
<div style="text-align: center; margin-bottom: 8px;">
<span style="font-size: 28px; font-weight: bold; color: #ffffff;">{{ c_temp | round(1) }}</span>
<span style="font-size: 14px; color: #888888;">°C</span>
</div>
<div style="display: flex; flex-direction: column; gap: 4px;">
{% for icon, label, val in [
('💧', 'Vochtigheid', c_hum ~ '%'),
('🌧️', 'Regen', c_rain ~ '%'),
('☁️', 'Bewolking', c_cloud ~ '%'),
('💨', 'Wind', beaufort(c_wind) | trim ~ ' Bft ' ~ c_dir),
('💨', 'Windstoten', c_gust ~ ' m/s'),
] %}
<div style="display: flex; justify-content: space-between; font-size: 13px;">
<span style="color: #888888;">{{ icon }} {{ label }}</span>
<span style="color: #cccccc; font-weight: bold;">{{ val }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- RACE -->
<div style="border-radius: 4px; padding: 10px; background: rgba(225,6,0,0.06); border: 1px solid rgba(225,6,0,0.25);">
<div style="font-size: 13px; font-weight: bold; text-transform: uppercase; text-align: center; color: #E10600; margin-bottom: 8px;">Race</div>
<div style="text-align: center; margin-bottom: 8px;">
<span style="font-size: 28px; font-weight: bold; color: #ffffff;">{{ r_temp | round(1) }}</span>
<span style="font-size: 14px; color: #888888;">°C</span>
</div>
<div style="display: flex; flex-direction: column; gap: 4px;">
{% for icon, label, val, warn in [
('💧', 'Vochtigheid', r_hum ~ '%', false),
('🌧️', 'Regen', r_rain ~ '%', r_rain | int > 30),
('☁️', 'Bewolking', r_cloud ~ '%', false),
('💨', 'Wind', beaufort(r_wind) | trim ~ ' Bft ' ~ r_dir, false),
('💨', 'Windstoten', r_gust ~ ' m/s', false),
] %}
<div style="display: flex; justify-content: space-between; font-size: 13px;">
<span style="color: #888888;">{{ icon }} {{ label }}</span>
<span style="color: {% if warn %}#E10600{% else %}#cccccc{% endif %}; font-weight: bold;">{{ val }}</span>
</div>
{% endfor %}
</div>
</div>
</div>
<div style="text-align: center; font-size: 11px; color: #555555; margin-top: 12px;">Bron: open-meteo</div>
</div>
card_mod:
style: |
ha-card {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
card_mod:
style: |
ha-card {
background: var(--ha-card-background, var(--card-background-color, #1e1e1e)) !important;
border: 0px solid rgba(225,6,0,0.19) !important;
border-radius: 12px !important;
box-shadow: 0px 4px 24px rgba(225,6,0,0.12) !important;
overflow: hidden;
}
Required template sensor
add this to your configuration.yaml or a template package file
template:
- sensor:
- name: "F1 Circuit Locatie"
unique_id: 39899dcb-87b4-4aef-8e7f-83741475e9e5
state: "{{ state_attr('sensor.f1_next_race', 'circuit_name') }}"
attributes:
latitude: "{{ state_attr('sensor.f1_next_race', 'circuit_lat') }}"
longitude: "{{ state_attr('sensor.f1_next_race', 'circuit_long') }}"
country: "{{ state_attr('sensor.f1_next_race', 'circuit_country') }}"
icon: mdi:map-marker
2. Season Calendar
Full 2026 race calendar powered by calendar-card-pro. Shows all sessions per race weekend with colour coded event types (race, qualifying, sprint, practice). The current race is highlighted with a pulse indicator. Tap the card to expand it to full screen.
entities:
- entity: calendar.f1_season_calendar
allowlist: Race$
color: "#FFFFFF"
accent_color: "#E10600"
label: mdi:flag-checkered
show_location: true
- entity: calendar.f1_season_calendar
allowlist: Qualifying$
color: "#FFFFFF"
accent_color: "#FFF200"
label: mdi:timer-outline
show_location: false
- entity: calendar.f1_season_calendar
allowlist: Sprint$
color: "#FFFFFF"
accent_color: "#FF8C00"
label: mdi:lightning-bolt
show_location: false
- entity: calendar.f1_season_calendar
allowlist: Sprint Qualifying$
color: "#FFFFFF"
accent_color: "#FFA500"
label: mdi:timer-sand
show_location: false
- entity: calendar.f1_season_calendar
allowlist: Practice
color: "#AAAAAA"
accent_color: "#444444"
label: mdi:wrench-outline
show_location: false
start_date: today
days_to_show: 275
compact_events_to_show: 5
compact_events_complete_days: true
filter_duplicates: true
language: nl
title: 🏎️ Formula 1 — 2026 Season
title_font_size: 22px
title_color: "#E10600"
background_color: var(--ha-card-background, var(--card-background-color,
vertical_line_width: 4px
event_spacing: 6px
additional_card_spacing: 8px
max_height: 650px
day_separator_width: 1px
day_separator_color: "#333333"
month_separator_width: 1px
month_separator_color: "#E1060060"
today_indicator: pulse
today_indicator_color: "#E10600"
weekday_font_size: 11px
weekday_color: "#888888"
day_font_size: 28px
day_color: "#FFFFFF"
month_font_size: 10px
month_color: "#E10600"
weekend_weekday_color: "#AAAAAA"
weekend_day_color: "#FFFFFF"
today_weekday_color: "#E10600"
today_day_color: "#E10600"
today_month_color: "#E10600"
event_background_opacity: 12
show_countdown: true
show_progress_bar: true
progress_bar_color: "#E10600"
event_font_size: 13px
event_color: "#FFFFFF"
time_24h: true
time_font_size: 11px
time_color: "#888888"
remove_location_country: true
location_font_size: 11px
location_color: "#666666"
weather:
position: date
date:
show_conditions: true
show_high_temp: true
show_low_temp: false
icon_size: 14px
font_size: 12px
color: var(--primary-text-color)
event:
show_conditions: true
show_temp: true
icon_size: 14px
font_size: 12px
color: var(--primary-text-color)
entity: weather.buienradar
tap_action:
action: expand
refresh_interval: 60
type: custom:calendar-card-pro
card_mod:
style: |
ha-card {
background: var(--ha-card-background, var(--card-background-color, #1e1e1e)) !important;
border: 0px solid rgba(225,6,0,0.19) !important;
border-radius: 12px !important;
box-shadow: 0px 4px 24px rgba(225,6,0,0.12) !important;
overflow: hidden;
}
ha-card .header-container h1.card-header {
font-size: 15px !important;
font-weight: bold !important;
letter-spacing: 2px !important;
text-transform: uppercase !important;
text-align: center !important;
float: none !important;
border-bottom: 1px solid rgba(225,6,0,0.25) !important;
padding-bottom: 8px !important;
}
3. Driver Standings
Shows the current driver standings with team colours, driver name and points. Displays the top 10 by default. Tap the card to expand and show all 22 drivers.
Required boolean
input_boolean:
f1_drivers_toggle:
name: Show all drivers
initial: false
type: custom:button-card
entity: sensor.f1_driver_standings
show_name: false
show_state: false
show_icon: false
layout: custom
tap_action:
action: call-service
service: input_boolean.toggle
service_data:
entity_id: input_boolean.f1_drivers_toggle
styles:
grid:
- grid-template-areas: |
"header"
"standings"
- row-gap: 12px
card:
- padding: 12px
- border-radius: 12px
- background: var(--ha-card-background, var(--card-background-color,
- color: white
- font-family: sans-serif
- box-shadow: 0px 4px 24px rgba(225,6,0,0.12)
- border: 0px solid rgba(225,6,0,0.19)
custom_fields:
header:
- padding-bottom: 6px
- border-bottom: 1px solid rgba(225,6,0,0.25)
standings:
- display: flex
- flex-direction: column
- gap: 6px
custom_fields:
header: |
[[[
if (!entity?.attributes) return "Geen data";
const expanded = states['input_boolean.f1_drivers_toggle']?.state === 'on';
const icon = expanded ? "mdi:chevron-up" : "mdi:chevron-down";
return '<div style="width: 100%; text-align: center;">'
+ '<div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">👤 Rijdersklassement</div>'
+ '<div style="font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 4px;">' + entity.attributes.season + ' Season</div>'
+ '<div style="font-size: 13px; color: #888888; margin-top: 2px;">Na ronde ' + entity.attributes.round + '</div>'
+ '<div style="display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; cursor: pointer;">'
+ '<span style="font-size: 12px; color: #555555;">' + (expanded ? '▲ Toon minder' : '▼ Toon alle ' + entity.attributes.driver_standings.length + ' rijders') + '</span>'
+ '<ha-icon icon="' + icon + '" style="--mdc-icon-size: 16px; color: #555555;"></ha-icon>'
+ '</div></div>';
]]]
standings: |
[[[
if (!entity?.attributes?.driver_standings) return "Geen data";
const expanded = states['input_boolean.f1_drivers_toggle']?.state === 'on';
const standings = expanded
? entity.attributes.driver_standings
: entity.attributes.driver_standings.slice(0, 10);
const teamColors = {
"Mercedes": "#00D2BE",
"Ferrari": "#E8002D",
"McLaren": "#FF8000",
"Red Bull": "#3671C6",
"Haas F1 Team": "#B6BABD",
"RB F1 Team": "#6692FF",
"Audi": "#B5B5B5",
"Alpine F1 Team": "#FF87BC",
"Williams": "#64C4FF",
"Cadillac F1 Team": "#D9042B",
"Aston Martin": "#229971"
};
return standings.map(function(d, idx) {
const pos = d.positionText !== '-' ? parseInt(d.positionText) : (idx + 1);
const code = d.Driver.code;
const name = d.Driver.givenName + ' ' + d.Driver.familyName;
const team = d.Constructors[0]?.name || '';
const color = teamColors[team] || "#888";
const points = d.points;
const isTop3 = pos <= 3;
const medal = pos == 1 ? '🥇' : pos == 2 ? '🥈' : pos == 3 ? '🥉' : pos + '.';
const nameDisplay = pos == 1 ? '<b style="color: gold">' + name + '</b>' : name;
const bg = idx % 2 === 0 ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.08)';
return '<div style="display: flex; align-items: center; background: ' + bg + '; padding: 6px 10px; border-radius: 4px; height: 38px;">'
+ '<div style="min-width: 28px; font-size: ' + (isTop3 ? '18px' : '15px') + '; font-weight: bold; text-align: center; color: ' + (pos == 1 ? '#FFF200' : isTop3 ? '#ffffff' : '#555555') + ';">' + medal + '</div>'
+ '<div style="width: 4px; height: 26px; background: ' + color + '; border-radius: 2px; margin: 0 10px; flex-shrink: 0;"></div>'
+ '<div style="flex: 1;">'
+ '<div style="font-size: 15px; color: ' + (isTop3 ? '#ffffff' : '#cccccc') + '; font-weight: ' + (isTop3 ? 'bold' : 'normal') + ';">' + nameDisplay + ' <span style="font-size: 12px; color: #666666;">' + code + '</span></div>'
+ '<div style="font-size: 11px; color: #666666;">' + team + ' • ' + points + ' pt' + (points == 1 ? '' : 's') + '</div>'
+ '</div></div>';
}).join('');
]]]
4. Constructor Standings
Shows the current constructor standings with team colours and points. Displays the top 10 by default. Tap the card to expand and show all 11 teams.
Required boolean
input_boolean:
f1_race_toggle:
name: Show all teams
initial: false
type: custom:button-card
entity: sensor.f1_constructor_standings
show_name: false
show_state: false
show_icon: false
layout: custom
tap_action:
action: call-service
service: input_boolean.toggle
service_data:
entity_id: input_boolean.f1_race_toggle
styles:
grid:
- grid-template-areas: |
"header"
"standings"
- row-gap: 12px
card:
- padding: 12px
- border-radius: 12px
- background: var(--ha-card-background, var(--card-background-color,
- color: white
- font-family: sans-serif
- box-shadow: 0px 4px 24px rgba(225,6,0,0.12)
- border: 0px solid rgba(225,6,0,0.19)
custom_fields:
header:
- padding-bottom: 6px
- border-bottom: 1px solid rgba(225,6,0,0.25)
standings:
- display: flex
- flex-direction: column
- gap: 6px
custom_fields:
header: |
[[[
if (!entity?.attributes) return "Geen data";
const expanded = states['input_boolean.f1_race_toggle']?.state === 'on';
const icon = expanded ? "mdi:chevron-up" : "mdi:chevron-down";
return '<div style="width: 100%; text-align: center;">'
+ '<div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">🏭 Constructeursklassement</div>'
+ '<div style="font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 4px;">' + entity.attributes.season + ' Season</div>'
+ '<div style="font-size: 13px; color: #888888; margin-top: 2px;">Na ronde ' + entity.attributes.round + '</div>'
+ '<div style="display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; cursor: pointer;">'
+ '<span style="font-size: 12px; color: #555555;">' + (expanded ? '▲ Toon minder' : '▼ Toon alle ' + entity.attributes.constructor_standings.length + ' teams') + '</span>'
+ '<ha-icon icon="' + icon + '" style="--mdc-icon-size: 16px; color: #555555;"></ha-icon>'
+ '</div></div>';
]]]
standings: |
[[[
if (!entity?.attributes?.constructor_standings) return "Geen data";
const expanded = states['input_boolean.f1_race_toggle']?.state === 'on';
const standings = expanded
? entity.attributes.constructor_standings
: entity.attributes.constructor_standings.slice(0, 10);
const teamColors = {
"Mercedes": "#00D2BE",
"Ferrari": "#E8002D",
"McLaren": "#FF8000",
"Red Bull": "#3671C6",
"Haas F1 Team": "#B6BABD",
"RB F1 Team": "#6692FF",
"Audi": "#B5B5B5",
"Alpine F1 Team": "#FF87BC",
"Williams": "#64C4FF",
"Cadillac F1 Team": "#D9042B",
"Aston Martin": "#229971"
};
return standings.map(function(c, idx) {
const pos = c.positionText !== '-' ? parseInt(c.positionText) : (idx + 1);
const name = c.Constructor.name;
const nationality = c.Constructor.nationality;
const points = c.points;
const wins = c.wins;
const color = teamColors[name] || "#888";
const isTop3 = pos <= 3;
const medal = pos == 1 ? '🥇' : pos == 2 ? '🥈' : pos == 3 ? '🥉' : pos + '.';
const nameDisplay = pos == 1 ? '<b style="color: gold">' + name + '</b>' : name;
const bg = idx % 2 === 0 ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.08)';
return '<div style="display: flex; align-items: center; background: ' + bg + '; padding: 6px 10px; border-radius: 4px; height: 38px;">'
+ '<div style="min-width: 28px; font-size: ' + (isTop3 ? '18px' : '15px') + '; font-weight: bold; text-align: center; color: ' + (pos == 1 ? '#FFF200' : isTop3 ? '#ffffff' : '#555555') + ';">' + medal + '</div>'
+ '<div style="width: 4px; height: 26px; background: ' + color + '; border-radius: 2px; margin: 0 10px; flex-shrink: 0;"></div>'
+ '<div style="flex: 1;">'
+ '<div style="font-size: 15px; color: ' + (isTop3 ? '#ffffff' : '#cccccc') + '; font-weight: ' + (isTop3 ? 'bold' : 'normal') + ';">' + nameDisplay + '</div>'
+ '<div style="font-size: 11px; color: #666666;">' + nationality + ' • ' + points + ' pt' + (points == 1 ? '' : 's') + ' • ' + wins + ' win' + (wins == 1 ? '' : 's') + '</div>'
+ '</div></div>';
}).join('');
]]]
5. Last Race Results
Shows the full results of the most recent race with team colours, points scored, and DNF/DNS status in red. Displays the top 10 by default. Tap the card to expand and show all 22 drivers.
Required boolean
input_boolean:
f1_race_results:
name: Race results
initial: false
type: custom:button-card
entity: sensor.f1_last_race_results
show_name: false
show_state: false
show_icon: false
layout: custom
tap_action:
action: call-service
service: input_boolean.toggle
service_data:
entity_id: input_boolean.f1_race_results
styles:
grid:
- grid-template-areas: |
"header"
"results"
- row-gap: 12px
card:
- padding: 12px
- border-radius: 12px
- background: var(--ha-card-background, var(--card-background-color,
- color: white
- font-family: sans-serif
- box-shadow: 0px 4px 24px rgba(225,6,0,0.12)
- border: 0px solid rgba(225,6,0,0.19)
custom_fields:
header:
- padding-bottom: 6px
- border-bottom: 1px solid rgba(225,6,0,0.25)
results:
- display: flex
- flex-direction: column
- gap: 6px
custom_fields:
header: |
[[[
if (!entity?.attributes) return "Geen data";
const expanded = states['input_boolean.f1_race_results']?.state === 'on';
const icon = expanded ? "mdi:chevron-up" : "mdi:chevron-down";
const date = new Date(entity.attributes.race_start_utc).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' });
return '<div style="width: 100%; text-align: center;">'
+ '<div style="font-size: 15px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase; color: #E10600;">🏁 Laatste Race</div>'
+ '<div style="font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 4px;">' + entity.attributes.race_name + '</div>'
+ '<div style="font-size: 13px; color: #888888; margin-top: 2px;">' + entity.attributes.circuit_locality + ', ' + entity.attributes.circuit_country + ' · ' + date + '</div>'
+ '<div style="display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; cursor: pointer;">'
+ '<span style="font-size: 12px; color: #555555;">' + (expanded ? '▲ Toon minder' : '▼ Toon alle ' + entity.attributes.results.length + ' rijders') + '</span>'
+ '<ha-icon icon="' + icon + '" style="--mdc-icon-size: 16px; color: #555555;"></ha-icon>'
+ '</div></div>';
]]]
results: |
[[[
if (!entity?.attributes?.results) return "Geen data";
const expanded = states['input_boolean.f1_race_results']?.state === 'on';
const results = expanded
? entity.attributes.results
: entity.attributes.results.slice(0, 10);
const teamColors = {
"mercedes": "#00D2BE",
"ferrari": "#E8002D",
"mclaren": "#FF8000",
"red_bull": "#3671C6",
"haas": "#B6BABD",
"rb": "#6692FF",
"audi": "#B5B5B5",
"alpine": "#FF87BC",
"williams": "#64C4FF",
"cadillac": "#D9042B",
"aston_martin": "#229971"
};
return results.map(function(r, idx) {
const pos = parseInt(r.position);
const code = r.driver.code;
const name = r.driver.givenName + ' ' + r.driver.familyName;
const team = r.constructor.name;
const teamId = r.constructor.constructorId;
const color = teamColors[teamId] || "#888";
const points = parseInt(r.points);
const status = r.status;
const isPoints = points > 0;
const isDNF = status === 'Retired' || status === 'Did not start';
const statusColor = isDNF ? '#E10600' : '#aaaaaa';
const statusText = isPoints ? '+' + points + ' pt' + (points == 1 ? '' : 's') : isDNF ? status : status;
const isTop3 = pos <= 3;
const medal = pos == 1 ? '🥇' : pos == 2 ? '🥈' : pos == 3 ? '🥉' : pos + '.';
const nameDisplay = pos == 1 ? '<b style="color: gold">' + name + '</b>' : name;
const bg = idx % 2 === 0 ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.08)';
return '<div style="display: flex; align-items: center; background: ' + bg + '; padding: 6px 10px; border-radius: 4px; height: 38px;">'
+ '<div style="min-width: 28px; font-size: ' + (isTop3 ? '18px' : '15px') + '; font-weight: bold; text-align: center; color: ' + (pos == 1 ? '#FFF200' : isTop3 ? '#ffffff' : '#555555') + ';">' + medal + '</div>'
+ '<div style="width: 4px; height: 26px; background: ' + color + '; border-radius: 2px; margin: 0 10px; flex-shrink: 0;"></div>'
+ '<div style="flex: 1;">'
+ '<div style="font-size: 15px; color: ' + (isTop3 ? '#ffffff' : '#cccccc') + '; font-weight: ' + (isTop3 ? 'bold' : 'normal') + ';">' + nameDisplay + ' <span style="font-size: 12px; color: #666666;">' + code + '</span></div>'
+ '<div style="font-size: 11px; color: #666666;">' + team + '</div>'
+ '</div>'
+ '<div style="font-size: 13px; font-weight: bold; color: ' + statusColor + '; text-align: right; min-width: 60px;">' + statusText + '</div>'
+ '</div>';
}).join('');
]]]
Here is the link where everything is neatly in one post.
Share: Full F1 Dashboard — 10 cards with live timing, standings, weather, calendar and more · Nicxe/f1_sensor · Discussion #445