Ce qu'il vous faut
- Home Assistant, avec l'extension button-card installée depuis HACS (catégorie Frontend).
- Votre voiture dans Home Assistant, par n'importe quelle intégration : le tutoriel de votre marque explique comment faire.
- Le niveau de batterie de la voiture, en %, comme entité (par exemple
sensor.ma_voiture_batterie). Le tableau plus bas dit quelle entité prendre selon la marque.
1Installer la carte
- Collez la carteTableau de bord → modifier → ajouter une carte → Manuel, puis collez le code de la section suivante.
- Indiquez la batterieDans le bloc
variables, en haut, remplacezsensor.ma_voiture_batteriepar l'entité de batterie de votre voiture. Changez aussinom, et si vous le souhaitez la photo (image) et la capacité de la batterie (capacite_kwh). - Regardez ce qui s'afficheLa carte affiche les données qu'elle reconnaît (autonomie, charge, prise, verrouillage, kilométrage, pneus, ouvertures…) et laisse de côté les autres, sans rien casser.
- Corrigez si besoinSi une valeur manque ou est fausse, forcez l'entité à utiliser avec la variable correspondante (tableau « Variables »).
Le code de la carte
Collez-le tel quel, puis modifiez le bloc variables en haut.
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>';
]]]Quelle entité pour votre marque ?
Pour chaque marque, voici le capteur à mettre dans la variable batterie, d'après les tutoriels du site. Les noms exacts dépendent de la langue de votre Home Assistant : cherchez le capteur dans Paramètres → Appareils et services → Entités.
| Marque | Capteur à indiquer dans « batterie » |
|---|---|
| Xpeng | Le capteur de niveau de batterie de XPENG Direct ou du WiCAN. Pour une Xpeng, la carte Xpeng est plus complète (diagnostic de la batterie par le WiCAN). |
| Renault · Dacia | Le capteur Batterie de l'appareil Renault (en %). |
| MG | Le capteur SoC (niveau de batterie en %) de l'appareil MQTT de la voiture. |
| Škoda | Le capteur niveau de batterie (en %) de l'appareil Škoda. |
| Kia · Hyundai · Genesis | Le capteur EV Battery Level (en %) de l'appareil Kia Uvo. |
| Tesla | Le capteur Niveau de batterie (en %) de l'appareil de la voiture. |
| Peugeot · Citroën · DS · Opel | Le capteur Battery (en %) de l'appareil Stellantis Vehicles. |
| Volkswagen · Cupra · SEAT · Audi | Le capteur niveau de batterie (en %) de l'appareil VW Group Connect. |
| BMW · Mini | Le capteur de niveau de batterie (en %) créé par BMW CarData pour la voiture. |
| Ford | Le capteur de niveau de batterie (en %) de l'appareil Ford. |
| Nissan | Le capteur Battery (en %) de l'appareil NissanConnect. |
| Volvo | Le capteur Batterie (en %) de l'appareil Volvo. |
| Polestar | Le capteur de niveau de batterie (en %) de l'appareil Polestar. |
| Mercedes-Benz | Le capteur niveau de batterie (state of charge, en %) de l'appareil Mercedes. |
| BYD | Le capteur de niveau de batterie (en %) publié par le WiCAN en MQTT. |
Comment la carte retrouve vos données
- Elle part de l'entité
batterie, puis cherche les autres entités du même appareil Home Assistant (ou, si l'appareil n'est pas connu, dont le nom commence de la même façon). - Elle les reconnaît à leur classe (distance, puissance, pression, prise, verrou, température, tension) et à des mots de leur nom, en français, anglais, espagnol et allemand.
- Les pressions en kPa ou en psi, les distances en miles et les températures en °F sont converties (bar, km, °C).
- Ce qui s'affiche dépend de ce qui est trouvé : une donnée absente n'affiche simplement rien.
Variables
| Variable | Rôle |
|---|---|
| nom | Nom affiché en haut de la carte. |
| image | Photo de la voiture : '' pour aucune, ou /local/… (dossier config/www de Home Assistant). |
| batterie | Seule entité obligatoire : le niveau de batterie en %. |
| capacite_kwh | Capacité de la batterie en kWh, pour estimer les kWh manquants jusqu'à la cible. 0 pour ne pas les afficher. |
| pression_min, pression_max | Plage de pression des pneus jugée normale, en bar. |
| autonomie | Forcer l'entité d'autonomie (km). |
| odometre | Forcer l'entité du kilométrage. |
| en_charge | Forcer l'entité « en charge » (capteur on/off ou état de charge). |
| branche | Forcer l'entité « prise branchée ». |
| puissance | Forcer l'entité de puissance de charge (kW ou W). |
| limite | Forcer l'entité de limite ou de cible de charge (%). |
| temps_restant | Forcer l'entité de temps de charge restant (minutes, heures ou secondes). |
| verrou | Forcer l'entité de verrouillage : une entité lock. ou un capteur de classe « verrou ». |
| position | Forcer le suivi de position (device_tracker.), pour afficher « À la maison ». |
| temp_habitacle | Forcer le capteur de température de l'habitacle. |
| pression_av_g, pression_av_d, pression_ar_g, pression_ar_d | Forcer chaque capteur de pression de pneu (avant gauche, avant droit, arrière gauche, arrière droit). |
Ce qu'il faut savoir
La logique de la carte a été testée avec des états simulés imitant plusieurs styles d'intégrations (noms anglais ou français, pressions en kPa ou en psi, appareil connu ou non), mais pas dans un vrai Home Assistant avec chaque marque. Les intégrations n'ont pas toutes les mêmes entités : si une donnée n'est pas reconnue, forcez-la avec la variable correspondante, ou écrivez-moi pour que j'améliore la détection.
Pour aller plus loin
Votre voiture remonte dans Home Assistant d'une autre façon ? Partagez votre méthode : les prochains tutoriels suivront les retours.
Combien vous coûte vraiment une recharge ? Heures creuses, Tempo, solaire, bornes rapides et codes XCombo : Watt Budget calcule tout, gratuitement et sans compte.
Ouvrir Watt Budget