What you need
- Home Assistant, with the button-card extension installed from HACS (Frontend category).
- Your car in Home Assistant, through any integration: the guide for your brand explains how.
- The car's battery level, in %, as an entity (for example
sensor.my_car_battery). The table below says which entity to use for each brand.
1Install the card
- Paste the cardDashboard → edit → add card → Manual, then paste the code from the next section.
- Set the batteryIn the
variablesblock at the top, replacesensor.ma_voiture_batteriewith your car's battery entity. Also changenom, and if you like the photo (image) and the battery capacity (capacite_kwh). - See what shows upThe card shows the data it recognises (range, charging, plug, lock, mileage, tyres, openings…) and leaves out the rest, without breaking anything.
- Fix if neededIf a value is missing or wrong, force the entity to use with the matching variable (“Variables” table).
The card code
Paste it as is, then edit the variables block at the top.
type: custom:button-card
show_name: false
show_icon: false
show_state: false
show_label: true
tap_action:
action: none
hold_action:
action: none
triggers_update: all
grid_options:
columns: full
variables:
nom: Ma voiture # nom affiché en haut de la carte
image: '' # photo de la voiture : '' pour aucune, ou /local/… (dossier config/www)
batterie: sensor.ma_voiture_batterie # LA SEULE ENTITÉ OBLIGATOIRE : le niveau de batterie (en %) de votre voiture
capacite_kwh: 0 # capacité de la batterie en kWh (facultatif : sert à estimer les kWh manquants ; 0 pour ne pas les afficher)
pression_min: 2.0 # plage de pression des pneus jugée normale (bar)
pression_max: 3.0
# Facultatif : la carte retrouve seule les autres données à partir de l'appareil de la voiture.
# Si l'une est fausse ou absente, indiquez l'entité à utiliser (ex. sensor.ma_voiture_autonomie).
autonomie: ''
odometre: ''
en_charge: ''
branche: ''
puissance: ''
limite: ''
temps_restant: ''
verrou: ''
position: ''
temp_habitacle: ''
pression_av_g: ''
pression_av_d: ''
pression_ar_g: ''
pression_ar_d: ''
extra_styles: >-
@keyframes cvmv { to { background-position: 26px 0; } }
@keyframes cvpulse { 50% { opacity: .4; } }
.cvhalo { animation: cvpulse 2.4s ease-in-out infinite; }
@keyframes cvpl { 50% { opacity: .7; } }
styles:
card:
- background: |
[[[
const s0 = parseFloat(states[variables.batterie]?.state);
const s = isNaN(s0) ? 76 : Math.max(0, Math.min(100, s0));
const S = [[0, [255, 93, 93]], [25, [255, 138, 61]], [45, [247, 209, 84]], [70, [61, 220, 151]], [100, [79, 163, 255]]];
let c = [79, 163, 255];
for (let i = 1; i < S.length; i++) { if (s <= S[i][0]) { const t = (s - S[i - 1][0]) / (S[i][0] - S[i - 1][0]); c = S[i - 1][1].map((x, j) => Math.round(x + (S[i][1][j] - x) * t)); break; } }
return 'radial-gradient(60% 190px at 50% 0, rgba(' + c.join(',') + ',0.14), transparent 75%), #1c1f24';
]]]
- border: 1px solid #2b3038
- border-radius: 18px
- padding: 16px
- box-shadow: none
- overflow: hidden
- pointer-events: auto
label:
- width: 100%
- text-align: left
- white-space: normal
- justify-self: stretch
- pointer-events: auto
label: |
[[[
const V = variables;
const S = e => (e ? states[e]?.state : undefined);
const N = e => { const x = parseFloat(S(e)); return (e && !isNaN(x)) ? x : null; };
const lang = String(hass.language || 'fr').slice(0, 2);
const PC = lang === 'en' ? '%' : ' %';
const fr = (x, d) => x === null ? '—' : x.toFixed(d).replace('.', lang === 'en' ? '.' : ',');
const TT = {
fr: { charging: 'En charge', driving: 'En route', parked: 'Garée', home: 'À la maison', away: 'Hors domicile', seen: 'vu', nodata: 'pas de donnée', now: "à l'instant", ago: (n, u) => 'il y a ' + n + ' ' + u, d: 'j', target: 'Cible', targetIn: (t, d) => t + PC + ' dans ' + d, maxAt: k => 'Max ' + k + ' km à 100 %', missing: k => 'manque ' + k + ' kWh', locked: 'Verrouillée', unlocked: 'Déverrouillée', plugged: 'Branchée', unplugged: 'Non branchée', homeChip: 'Maison', lvl: ['Critique', 'Bas', 'Moyen', 'Bon', 'Plein'], cabin: 'Habitacle', hv: 'Batterie', odo: 'Odomètre', outside: 'Extérieur', tw: 'Pneus et ouvertures', ok: 'Tout est OK', tyres: n => n + ' pneu' + (n > 1 ? 's' : '') + ' à vérifier', opens: n => n + ' ouverture' + (n > 1 ? 's' : '') + ' ouverte' + (n > 1 ? 's' : ''), hist: 'Historique de la batterie', noEnt: 'Entité introuvable : ', fix: 'Corrigez la variable « batterie » de la carte.' },
en: { charging: 'Charging', driving: 'Driving', parked: 'Parked', home: 'At home', away: 'Away from home', seen: 'seen', nodata: 'no data', now: 'just now', ago: (n, u) => n + ' ' + u + ' ago', d: 'd', target: 'Target', targetIn: (t, d) => t + PC + ' in ' + d, maxAt: k => 'Max ' + k + ' km at 100%', missing: k => k + ' kWh missing', locked: 'Locked', unlocked: 'Unlocked', plugged: 'Plugged in', unplugged: 'Unplugged', homeChip: 'Home', lvl: ['Critical', 'Low', 'Medium', 'Good', 'Full'], cabin: 'Cabin', hv: 'Battery', odo: 'Odometer', outside: 'Outside', tw: 'Tyres and openings', ok: 'All good', tyres: n => n + ' tyre' + (n > 1 ? 's' : '') + ' to check', opens: n => n + ' opening' + (n > 1 ? 's' : '') + ' open', hist: 'Battery history', noEnt: 'Entity not found: ', fix: 'Fix the “batterie” variable of the card.' },
es: { charging: 'Cargando', driving: 'En marcha', parked: 'Aparcado', home: 'En casa', away: 'Fuera de casa', seen: 'visto', nodata: 'sin datos', now: 'ahora mismo', ago: (n, u) => 'hace ' + n + ' ' + u, d: 'd', target: 'Objetivo', targetIn: (t, d) => t + PC + ' en ' + d, maxAt: k => 'Máx. ' + k + ' km al 100 %', missing: k => 'faltan ' + k + ' kWh', locked: 'Bloqueado', unlocked: 'Desbloqueado', plugged: 'Conectado', unplugged: 'Desconectado', homeChip: 'Casa', lvl: ['Crítico', 'Bajo', 'Medio', 'Bueno', 'Lleno'], cabin: 'Habitáculo', hv: 'Batería', odo: 'Cuentakm', outside: 'Exterior', tw: 'Neumáticos y aperturas', ok: 'Todo correcto', tyres: n => n + ' neumático' + (n > 1 ? 's' : '') + ' a revisar', opens: n => n + ' apertura' + (n > 1 ? 's' : '') + ' abierta' + (n > 1 ? 's' : ''), hist: 'Historial de la batería', noEnt: 'Entidad no encontrada: ', fix: 'Corrige la variable «batterie» de la tarjeta.' },
de: { charging: 'Lädt', driving: 'Unterwegs', parked: 'Geparkt', home: 'Zu Hause', away: 'Nicht zu Hause', seen: 'zuletzt gesehen', nodata: 'keine Daten', now: 'gerade eben', ago: (n, u) => 'vor ' + n + ' ' + u, d: 'T', target: 'Ziel', targetIn: (t, d) => t + PC + ' in ' + d, maxAt: k => 'Max. ' + k + ' km bei 100 %', missing: k => 'es fehlen ' + k + ' kWh', locked: 'Verriegelt', unlocked: 'Entriegelt', plugged: 'Angesteckt', unplugged: 'Nicht angesteckt', homeChip: 'Zuhause', lvl: ['Kritisch', 'Niedrig', 'Mittel', 'Gut', 'Voll'], cabin: 'Innenraum', hv: 'Akku', odo: 'Kilometer', outside: 'Außen', tw: 'Reifen und Öffnungen', ok: 'Alles in Ordnung', tyres: n => n + ' Reifen prüfen', opens: n => n + ' Öffnung' + (n > 1 ? 'en' : '') + ' offen', hist: 'Akkuverlauf', noEnt: 'Entität nicht gefunden: ', fix: 'Korrigieren Sie die Variable „batterie“ der Karte.' }
};
const T = TT[lang] || TT.en;
const C = { g: '#3ddc97', am: '#f5a524', rd: '#ff5d5d', bl: '#4fa3ff', yl: '#f7d154', vi: '#b18cff', or: '#ff8a3d', mu: '#8a919c' };
if (!V.batterie || !states[V.batterie]) {
return '<div style="color:#e8eaed;font-size:13px;line-height:1.4"><b style="color:' + C.am + '">' + T.noEnt + '<code>' + (V.batterie || '—') + '</code></b><br>' + T.fix + '</div>';
}
/* Retrouver les autres entités de la voiture : même appareil que l'entité « batterie » (ou, à défaut, même début de nom), puis classe d'appareil et mots du nom. */
const REG = hass.entities || {};
const dev = REG[V.batterie]?.device_id;
const base = (V.batterie.split('.')[1] || '').replace(/_(battery_level|battery|soc|state_of_charge|ev_battery_level|batterie|niveau_de_batterie|niveau_batterie|ladezustand|nivel_de_bateria|bateria)$/, '');
const cand = Object.keys(states).filter(k => dev ? REG[k]?.device_id === dev : k.indexOf('.' + base + '_') > 0);
const at = k => states[k]?.attributes || {};
const nm = k => (k.split('.')[1] + ' ' + (at(k).friendly_name || '')).toLowerCase();
const dc = k => at(k).device_class;
const un = k => String(at(k).unit_of_measurement || '');
const find = (dom, test) => cand.find(k => k.startsWith(dom + '.') && test(k)) || null;
const use = (v, ...fs) => { if (v) return v; for (const f of fs) { const r = f(); if (r) return r; } return null; };
const ON = /^(on|true|yes|charging|in_progress|charge_in_progress|chargingactive|started|laden|en_charge|cargando)$/i;
const PLUG = /^(on|true|yes|connected|plugged|plugged_in|plugged_in_charging|charging|angesteckt|branche|conectado)$/i;
const eSoc = V.batterie;
const eRng = use(V.autonomie, () => find('sensor', k => (dc(k) === 'distance' || /^(km|mi)$/.test(un(k))) && /range|autonom|reichweite/.test(nm(k)) && !/max|total|fuel|carburant|100|target|cible|limit/.test(nm(k))));
const eOdo = use(V.odometre, () => find('sensor', k => (dc(k) === 'distance' || /^(km|mi)$/.test(un(k))) && /odom|mileage|kilom|kilometer|cuentakm|kilometrage|kilométrage/.test(nm(k))));
const eChg = use(V.en_charge, () => find('binary_sensor', k => dc(k) === 'battery_charging'),
() => find('binary_sensor', k => /charging|en_charge|laden|cargando/.test(nm(k)) && !/plug|cable|prise|stecker|connect|enchufe/.test(nm(k))),
() => find('sensor', k => /charging_state|charge_state|charging_status|charge_status|etat_de_charge|ladestatus|ladevorgang|estado_de_carga/.test(nm(k)) && !/power|puiss|leistung|potencia|remain|restant|limit/.test(nm(k))));
const ePlg = use(V.branche, () => find('binary_sensor', k => dc(k) === 'plug'),
() => find('binary_sensor', k => /plug|cable|prise|stecker|enchufe/.test(nm(k)) && !/wifi|online|internet|remote/.test(nm(k))),
() => find('sensor', k => /plug_state|plugged|prise|stecker|enchufe/.test(nm(k))));
const ePow = use(V.puissance, () => find('sensor', k => (dc(k) === 'power' || /^k?w$/i.test(un(k))) && /charg|lad|carga|puiss|leist|potencia/.test(nm(k))));
const eTgt = use(V.limite, () => cand.find(k => /^(sensor|number)\./.test(k) && un(k) === '%' && k !== eSoc && /limit|target|cible|objectif|ziel|max_?soc|objetivo/.test(nm(k))) || null);
const eRem = use(V.temps_restant, () => find('sensor', k => /remain|restant|time_to_full|until_full|time_to_complete|verbleib|restante/.test(nm(k)) && (dc(k) === 'duration' || /^(min|h|s)$/.test(un(k)))));
const eLck = use(V.verrou, () => find('lock', k => true), () => find('binary_sensor', k => dc(k) === 'lock'));
const eTrk = use(V.position, () => find('device_tracker', k => true));
const eCab = use(V.temp_habitacle, () => find('sensor', k => dc(k) === 'temperature' && /interior|cabin|inside|habitacle|innen|interior|cabina/.test(nm(k))));
const eOut = find('sensor', k => dc(k) === 'temperature' && /outside|outdoor|exterior|ext[eé]rieur|außen|aussen/.test(nm(k)));
const eBat = find('sensor', k => dc(k) === 'temperature' && /battery|batterie|hv|akku|bater/.test(nm(k)) && !/interior|cabin|habitacle/.test(nm(k)));
const e12v = find('sensor', k => dc(k) === 'voltage' && /12|aux/.test(nm(k)));
const eSpd = find('sensor', k => dc(k) === 'speed');
const eSeen = find('sensor', k => dc(k) === 'timestamp' && /last.?(seen|update|updated|contact|sync|connection)|derni[eè]re|aktualis|actualiz/.test(nm(k)));
const soc = N(eSoc);
const socV = soc === null ? 0 : Math.max(0, Math.min(100, soc));
const rngRaw = N(eRng);
const rng = rngRaw !== null && /^mi$/.test(un(eRng)) ? rngRaw * 1.609 : rngRaw;
const pwRaw = N(ePow), pw = pwRaw !== null && /^w$/i.test(un(ePow)) ? pwRaw / 1000 : pwRaw;
const charging = (eChg ? ON.test(String(S(eChg))) : false) || (pw !== null && pw > 0.2);
const plugged = charging || (ePlg ? PLUG.test(String(S(ePlg))) : false);
const tgtRaw = N(eTgt), tgt = tgtRaw !== null && tgtRaw > 0 && tgtRaw <= 100 ? tgtRaw : 100;
const miss = (V.capacite_kwh > 0 && soc !== null) ? Math.max(0, (tgt - soc) / 100 * V.capacite_kwh) : null;
const remRaw = N(eRem), rem = remRaw === null ? null : Math.round(un(eRem) === 'h' ? remRaw * 60 : un(eRem) === 's' ? remRaw / 60 : remRaw);
const spd = N(eSpd) || 0;
const driving = !charging && spd > 3;
let locked = null;
if (eLck) { const l = String(S(eLck)).toLowerCase(); locked = eLck.startsWith('lock.') ? l === 'locked' : dc(eLck) === 'lock' ? l === 'off' : (l === 'locked' || l === 'on'); }
let home = null;
if (eTrk) {
const ts = String(S(eTrk)).toLowerCase(), ta = at(eTrk), hz = states['zone.home']?.attributes;
if (ts === 'home') home = true;
else if (ta.latitude !== undefined && hz) { const dLa = (ta.latitude - hz.latitude) * 111320, dLo = (ta.longitude - hz.longitude) * 111320 * Math.cos(hz.latitude * Math.PI / 180); home = Math.sqrt(dLa * dLa + dLo * dLo) < 200; }
else if (ts !== 'unknown' && ts !== 'unavailable') home = false;
}
const seenIso = (eSeen && S(eSeen)) || states[eSoc].last_reported || states[eSoc].last_updated;
const age = seenIso ? Math.round((Date.now() - new Date(seenIso).getTime()) / 60000) : null;
const ageTxt = age === null || isNaN(age) ? T.nodata : age < 1 ? T.now : age < 60 ? T.ago(age, 'min') : age < 1440 ? T.ago(Math.round(age / 60), 'h') : T.ago(Math.round(age / 1440), T.d);
const STOPS = [[0, [255, 93, 93]], [25, [255, 138, 61]], [45, [247, 209, 84]], [70, [61, 220, 151]], [100, [79, 163, 255]]];
const colAt = s => { for (let i = 1; i < STOPS.length; i++) { if (s <= STOPS[i][0]) { const a = STOPS[i - 1], b = STOPS[i], t = (s - a[0]) / (b[0] - a[0]); return '#' + a[1].map((v, j) => Math.round(v + (b[1][j] - v) * t).toString(16).padStart(2, '0')).join(''); } } return '#4fa3ff'; };
const sc = colAt(socV);
const lvl = T.lvl[socV < 20 ? 0 : socV < 40 ? 1 : socV < 60 ? 2 : socV <= 90 ? 3 : 4];
const mix = (c, a) => c + Math.round(a * 255).toString(16).padStart(2, '0');
const IC = (ic, n, c) => '<span style="width:' + n + 'px;height:' + n + 'px;flex:none;display:inline-flex;overflow:hidden;align-items:center;justify-content:center;line-height:0"><ha-icon icon="' + ic + '" style="--mdc-icon-size:' + n + 'px' + (c ? ';color:' + c : '') + '"></ha-icon></span>';
let st, stIc, stC;
if (charging) { st = T.charging + (pw ? ' ' + fr(pw, 1) + ' kW' : ''); stIc = 'mdi:lightning-bolt'; stC = C.g; }
else if (driving) { st = T.driving; stIc = 'mdi:steering'; stC = C.bl; }
else if (plugged) { st = T.plugged; stIc = 'mdi:power-plug'; stC = C.bl; }
else { st = T.parked; stIc = 'mdi:parking'; stC = C.vi; }
const where = home === null ? '' : home ? T.home : T.away;
const sub = where + (where ? ' · ' : '') + (driving ? Math.round(spd) + ' km/h' : T.seen + ' ' + ageTxt);
let line;
if (charging) {
line = rem ? T.targetIn(Math.round(tgt), rem >= 60 ? Math.floor(rem / 60) + ' h ' + String(rem % 60).padStart(2, '0') : rem + ' min') : T.target + ' ' + Math.round(tgt) + PC;
} else if (driving) {
line = (rng !== null && soc > 0) ? T.maxAt(fr(rng * 100 / soc, 0)) : '';
} else {
line = T.target + ' ' + Math.round(tgt) + PC + (miss !== null && miss > 0 ? ' · ' + T.missing(fr(miss, 1)) : '');
}
const chip = (ic, t, c) => '<span style="display:inline-flex;align-items:center;gap:5px;border-radius:20px;padding:4px 10px;font-size:12px;margin:0 4px 5px 0;background:' + mix(c, 0.14) + ';color:' + c + '">' + IC(ic, 15) + t + '</span>';
let chips = '';
if (locked !== null) chips += locked ? chip('mdi:lock', T.locked, C.g) : chip('mdi:lock-open-variant', T.unlocked, C.am);
if (charging) chips += chip('mdi:ev-plug-type2', pw ? fr(pw, 1) + ' kW' : T.charging, C.yl);
else if (eChg || ePlg) chips += plugged ? chip('mdi:power-plug', T.plugged, C.bl) : chip('mdi:power-plug-off', T.unplugged, C.mu);
if (home) chips += chip('mdi:home', T.homeChip, C.vi);
const tile = (ic, k, v, c, vc) => '<div style="background:#252a31;border-radius:12px;padding:8px 10px;min-width:0;text-align:center;border-top:2px solid ' + c + '"><div style="font-size:11px;color:' + C.mu + ';display:flex;align-items:center;justify-content:center;gap:5px;white-space:nowrap">' + IC(ic, 14, c) + '<span>' + k + '</span></div><div style="font-size:14px;font-weight:500;white-space:nowrap;margin-top:3px;text-align:center;color:' + (vc || '#e8eaed') + '">' + v + '</div></div>';
const tmp = e => { const x = N(e); return x === null ? null : fr(/f/i.test(un(e)) ? (x - 32) * 5 / 9 : x, 0) + ' °C'; };
const odoRaw = N(eOdo), odo = odoRaw !== null && /^mi$/.test(un(eOdo)) ? odoRaw * 1.609 : odoRaw;
const v12 = N(e12v);
let tiles = '';
if (tmp(eCab) !== null) tiles += tile('mdi:car-seat', T.cabin, tmp(eCab), C.or);
if (tmp(eOut) !== null) tiles += tile('mdi:thermometer', T.outside, tmp(eOut), C.bl);
if (tmp(eBat) !== null) tiles += tile('mdi:battery-high', T.hv, tmp(eBat), C.bl);
if (v12 !== null) tiles += tile('mdi:car-battery', '12 V', fr(v12, 1) + ' V', C.yl, v12 < 12.2 ? C.rd : null);
if (odo !== null) tiles += tile('mdi:counter', T.odo, Math.round(odo).toLocaleString(lang) + ' km', C.vi);
/* Pneus : par position (avant/arrière, gauche/droite) ; les pressions en kPa ou en psi sont converties en bar. */
const POS = {
fl: [V.pression_av_g, /front.?left|avant.?gauche|vorn.?links|vorne.?links|delantera.?izq|\bfl\b|\blf\b|\bav.?g\b/],
fr: [V.pression_av_d, /front.?right|avant.?droit|vorn.?rechts|vorne.?rechts|delantera.?der|\bfr\b|\brf\b|\bav.?d\b/],
rl: [V.pression_ar_g, /rear.?left|arri[eè]re.?gauche|hinten.?links|trasera.?izq|\brl\b|\blr\b|\bar.?g\b/],
rr: [V.pression_ar_d, /rear.?right|arri[eè]re.?droit|hinten.?rechts|trasera.?der|\brr\b|\bar.?d\b/]
};
const tyreAll = cand.filter(k => k.startsWith('sensor.') && dc(k) === 'pressure' && /tire|tyre|pneu|reifen|neum/.test(nm(k)));
const bar = e => { const x = N(e); if (x === null) return null; const u = un(e).toLowerCase(); return u === 'kpa' ? x / 100 : u === 'psi' ? x / 14.5038 : u === 'pa' ? x / 100000 : u === 'hpa' || u === 'mbar' ? x / 1000 : x; };
const used = new Set(); const tyre = {};
for (const p of ['fl', 'fr', 'rl', 'rr']) { const e = POS[p][0] || tyreAll.find(k => !used.has(k) && POS[p][1].test(nm(k))); if (e) { used.add(e); tyre[p] = e; } }
const rest = tyreAll.filter(k => !used.has(k));
let nTyre = 0;
const pill = e => { const p = bar(e), ok = p !== null && p >= V.pression_min && p <= V.pression_max; if (!ok) nTyre++; const c = ok ? C.g : C.am;
return '<div style="display:flex;align-items:center;gap:6px;background:#252a31;border-radius:10px;padding:6px 8px;font-size:13px"><span style="width:8px;height:8px;border-radius:50%;background:' + c + ';flex:none"></span><b style="font-weight:500">' + fr(p, 2) + '</b><span style="color:' + C.mu + ';font-size:11px;margin-left:auto">bar</span></div>'; };
const tyreList = ['fl', 'fr', 'rl', 'rr'].filter(p => tyre[p]).map(p => tyre[p]).concat(rest.slice(0, 4 - Object.keys(tyre).length));
const tyreHtml = tyreList.map(pill).join('');
const nOpen = cand.filter(k => (k.startsWith('binary_sensor.') && (dc(k) === 'door' || dc(k) === 'window' || dc(k) === 'opening') && S(k) === 'on') || (k.startsWith('cover.') && dc(k) === 'window' && S(k) === 'open')).length;
const hasOpen = cand.some(k => k.startsWith('binary_sensor.') && (dc(k) === 'door' || dc(k) === 'window'));
const showTW = tyreList.length > 0 || hasOpen;
const sumTxt = (nTyre === 0 && nOpen === 0) ? T.ok : [nTyre ? T.tyres(nTyre) : '', nOpen ? T.opens(nOpen) : ''].filter(x => x).join(' · ');
const sumC = (nTyre === 0 && nOpen === 0) ? C.g : C.am;
const SEC = 'background:#20242a;border:1px solid #2b3038;border-radius:14px;padding:10px 12px;margin-top:10px';
const H4 = (ic, t, r, rc) => '<div style="display:flex;justify-content:space-between;align-items:center;font-size:12px;color:' + C.mu + ';margin-bottom:8px"><span style="display:flex;align-items:center;gap:5px">' + IC(ic, 15) + t + '</span><span style="color:' + rc + '">' + r + '</span></div>';
const MORE = 'onclick="event.stopPropagation();this.dispatchEvent(new CustomEvent(\'hass-more-info\',{bubbles:true,composed:true,detail:{entityId:\'' + eSoc + '\'}}))" title="' + T.hist + '"';
let H = '<div style="color:#e8eaed;font-size:13px;line-height:1.35">';
H += '<div style="display:flex;justify-content:space-between;align-items:flex-start"><div><div style="font-size:18px;font-weight:500">' + V.nom + '</div><div style="font-size:12px;color:' + C.mu + '">' + sub + '</div></div>';
H += '<span style="font-size:12px;padding:3px 10px;border-radius:20px;background:' + mix(stC, 0.18) + ';color:' + stC + ';display:inline-flex;gap:4px;align-items:center;white-space:nowrap">' + IC(stIc, 14) + st + '</span></div>';
if (V.image) {
H += '<div style="position:relative;margin:4px 0 0">';
if (charging) H += '<div class="cvhalo" style="position:absolute;left:8%;right:8%;bottom:0;height:26%;border-radius:50%;background:radial-gradient(closest-side,' + mix(sc, 0.6) + ',transparent)"></div>';
H += '<img src="' + V.image + '" style="width:100%;max-width:460px;display:block;margin:0 auto;position:relative"></div>';
} else { H += '<div style="height:10px"></div>'; }
H += '<div ' + MORE + ' style="display:flex;align-items:baseline;gap:10px;white-space:nowrap;cursor:pointer"><span style="font-size:34px;font-weight:500;line-height:1;color:' + sc + '">' + (soc === null ? '—' : Math.round(soc) + PC) + '</span><span style="font-size:16px">' + (rng === null ? '' : fr(rng, 0) + ' km') + '</span><span style="font-size:12px;margin-left:auto;color:' + sc + '">' + lvl + '</span></div>';
H += '<div style="font-size:12px;color:' + C.mu + ';margin:5px 0 7px">' + line + '</div>';
const SPEC = 'linear-gradient(90deg,#ff5d5d 0%,#ff8a3d 25%,#f7d154 45%,#3ddc97 70%,#4fa3ff 100%)';
H += '<div ' + MORE + ' style="position:relative;height:12px;margin:2px 0 4px;cursor:pointer">';
H += '<div style="position:absolute;inset:0;border-radius:6px;overflow:hidden"><div style="position:absolute;inset:0;background:' + SPEC + '"></div>';
if (charging) H += '<div style="position:absolute;top:0;bottom:0;left:0;width:' + socV + '%;background:repeating-linear-gradient(110deg,rgba(255,255,255,.22) 0 8px,transparent 8px 20px);background-size:26px 100%;animation:cvmv .9s linear infinite"></div>';
H += '<div style="position:absolute;top:0;bottom:0;right:0;width:' + (100 - socV) + '%;background:#252a31"></div></div>';
H += '<div style="position:absolute;top:-1px;bottom:-1px;left:calc(' + Math.min(100, tgt) + '% - 1px);width:2px;background:#ffffff66;border-radius:1px"></div>';
H += '<div style="position:absolute;top:-3px;bottom:-3px;left:calc(' + socV + '% - 2px);width:4px;border-radius:2px;background:#fff;box-shadow:0 0 8px #fff' + (charging ? ';animation:cvpl 1.6s ease-in-out infinite' : '') + '"></div></div>';
H += '<div style="display:flex;justify-content:space-between;font-size:10px;color:#5d646e;margin-bottom:10px"><span>0</span><span>20</span><span>40</span><span>60</span><span>80</span><span>100</span></div>';
if (chips) H += '<div>' + chips + '</div>';
if (tiles) H += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(78px,1fr));gap:8px;margin-top:5px">' + tiles + '</div>';
if (showTW) H += '<div style="' + SEC + '">' + H4('mdi:tire', T.tw, sumTxt, sumC) + (tyreHtml ? '<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px">' + tyreHtml + '</div>' : '') + '</div>';
return H + '</div>';
]]]Which entity for your brand?
For each brand, here is the sensor to put in the batterie variable, from the guides on this site. Exact names depend on the language of your Home Assistant: look for the sensor in Settings → Devices & services → Entities.
| Brand | Sensor to give in “batterie” |
|---|---|
| Xpeng | The battery level sensor from XPENG Direct or from the WiCAN. For a Xpeng, the Xpeng card is more complete (battery diagnostics from the WiCAN). |
| Renault · Dacia | The Battery sensor of the Renault device (in %). |
| MG | The SoC sensor (battery level in %) of the car's MQTT device. |
| Škoda | The battery level sensor (in %) of the Škoda device. |
| Kia · Hyundai · Genesis | The EV Battery Level sensor (in %) of the Kia Uvo device. |
| Tesla | The Battery level sensor (in %) of the car's device. |
| Peugeot · Citroën · DS · Opel | The Battery sensor (in %) of the Stellantis Vehicles device. |
| Volkswagen · Cupra · SEAT · Audi | The battery level sensor (in %) of the VW Group Connect device. |
| BMW · Mini | The battery level sensor (in %) that BMW CarData creates for the car. |
| Ford | The battery level sensor (in %) of the Ford device. |
| Nissan | The Battery sensor (in %) of the NissanConnect device. |
| Volvo | The Battery sensor (in %) of the Volvo device. |
| Polestar | The battery level sensor (in %) of the Polestar device. |
| Mercedes-Benz | The battery level sensor (state of charge, in %) of the Mercedes device. |
| BYD | The battery level sensor (in %) the WiCAN publishes over MQTT. |
How the card finds your data
- It starts from the
batterieentity, then looks for the other entities of the same device in Home Assistant (or, if the device is unknown, whose name starts the same way). - It recognises them by their class (distance, power, pressure, plug, lock, temperature, voltage) and by words in their name, in French, English, Spanish and German.
- Pressures in kPa or psi, distances in miles and temperatures in °F are converted (bar, km, °C).
- What is shown depends on what is found: missing data simply shows nothing.
Variables
| Variable | Purpose |
|---|---|
| nom | Name shown at the top of the card. |
| image | Car photo: '' for none, or /local/… (Home Assistant's config/www folder). |
| batterie | The only required entity: the battery level in %. |
| capacite_kwh | Battery capacity in kWh, to estimate the kWh missing to the target. 0 to hide them. |
| pression_min, pression_max | Tyre pressure range considered normal, in bar. |
| autonomie | Force the range entity (km). |
| odometre | Force the mileage entity. |
| en_charge | Force the “charging” entity (on/off sensor or charging state). |
| branche | Force the “plugged in” entity. |
| puissance | Force the charging power entity (kW or W). |
| limite | Force the charge limit or target entity (%). |
| temps_restant | Force the remaining charging time entity (minutes, hours or seconds). |
| verrou | Force the lock entity: a lock. entity or a sensor of the “lock” class. |
| position | Force the location tracker (device_tracker.), to show “At home”. |
| temp_habitacle | Force the cabin temperature sensor. |
| pression_av_g, pression_av_d, pression_ar_g, pression_ar_d | Force each tyre pressure sensor (front left, front right, rear left, rear right). |
Good to know
The card's logic was tested with simulated states imitating several integration styles (English or French names, pressures in kPa or psi, known or unknown device), but not in a real Home Assistant with every brand. Integrations do not all expose the same entities: if some data is not recognised, force it with the matching variable, or write to me so I can improve the detection.
Going further
Does your car reach Home Assistant another way? Share your method: the next guides will follow your feedback.
What does a charge really cost you? Off-peak tariffs, solar, rapid chargers and XCombo codes: Watt Budget works it all out, free and without an account.
Open Watt Budget