Lo que necesitas
- Home Assistant, con la extensión button-card instalada desde HACS (categoría Frontend).
- Tu coche en Home Assistant, con cualquier integración: el tutorial de tu marca explica cómo.
- El nivel de batería del coche, en %, como entidad (por ejemplo
sensor.mi_coche_bateria). La tabla de abajo dice qué entidad usar según la marca.
1Instalar la tarjeta
- Pega la tarjetaPanel → editar → añadir tarjeta → Manual y pega el código de la sección siguiente.
- Indica la bateríaEn el bloque
variables, arriba, sustituyesensor.ma_voiture_batteriepor la entidad de batería de tu coche. Cambia tambiénnomy, si quieres, la foto (image) y la capacidad de la batería (capacite_kwh). - Mira lo que apareceLa tarjeta muestra los datos que reconoce (autonomía, carga, enchufe, bloqueo, kilometraje, neumáticos, aperturas…) y omite los demás, sin romper nada.
- Corrige si hace faltaSi falta un valor o es erróneo, fuerza la entidad con la variable correspondiente (tabla «Variables»).
El código de la tarjeta
Pégalo tal cual y luego edita el bloque variables de arriba.
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>';
]]]¿Qué entidad para tu marca?
Para cada marca, este es el sensor que va en la variable batterie, según los tutoriales del sitio. Los nombres exactos dependen del idioma de tu Home Assistant: busca el sensor en Ajustes → Dispositivos y servicios → Entidades.
| Marca | Sensor que indicar en «batterie» |
|---|---|
| Xpeng | El sensor de nivel de batería de XPENG Direct o del WiCAN. Para un Xpeng, la tarjeta Xpeng es más completa (diagnóstico de la batería con el WiCAN). |
| Renault · Dacia | El sensor Batería del dispositivo Renault (en %). |
| MG | El sensor SoC (nivel de batería en %) del dispositivo MQTT del coche. |
| Škoda | El sensor de nivel de batería (en %) del dispositivo Škoda. |
| Kia · Hyundai · Genesis | El sensor EV Battery Level (en %) del dispositivo Kia Uvo. |
| Tesla | El sensor Nivel de batería (en %) del dispositivo del coche. |
| Peugeot · Citroën · DS · Opel | El sensor Battery (en %) del dispositivo Stellantis Vehicles. |
| Volkswagen · Cupra · SEAT · Audi | El sensor de nivel de batería (en %) del dispositivo VW Group Connect. |
| BMW · Mini | El sensor de nivel de batería (en %) que BMW CarData crea para el coche. |
| Ford | El sensor de nivel de batería (en %) del dispositivo Ford. |
| Nissan | El sensor Battery (en %) del dispositivo NissanConnect. |
| Volvo | El sensor Batería (en %) del dispositivo Volvo. |
| Polestar | El sensor de nivel de batería (en %) del dispositivo Polestar. |
| Mercedes-Benz | El sensor de nivel de batería (state of charge, en %) del dispositivo Mercedes. |
| BYD | El sensor de nivel de batería (en %) que publica el WiCAN por MQTT. |
Cómo encuentra la tarjeta tus datos
- Parte de la entidad
batteriey busca las demás entidades del mismo dispositivo de Home Assistant (o, si el dispositivo no se conoce, cuyo nombre empieza igual). - Las reconoce por su clase (distancia, potencia, presión, enchufe, cerradura, temperatura, tensión) y por palabras de su nombre, en francés, inglés, español y alemán.
- Las presiones en kPa o psi, las distancias en millas y las temperaturas en °F se convierten (bar, km, °C).
- Lo que se muestra depende de lo que se encuentra: un dato ausente simplemente no muestra nada.
Variables
| Variable | Función |
|---|---|
| nom | Nombre que se muestra arriba de la tarjeta. |
| image | Foto del coche: '' para ninguna, o /local/… (carpeta config/www de Home Assistant). |
| batterie | La única entidad obligatoria: el nivel de batería en %. |
| capacite_kwh | Capacidad de la batería en kWh, para estimar los kWh que faltan hasta el objetivo. 0 para no mostrarlos. |
| pression_min, pression_max | Rango de presión de neumáticos considerado normal, en bar. |
| autonomie | Forzar la entidad de autonomía (km). |
| odometre | Forzar la entidad del kilometraje. |
| en_charge | Forzar la entidad «cargando» (sensor on/off o estado de carga). |
| branche | Forzar la entidad «enchufado». |
| puissance | Forzar la entidad de potencia de carga (kW o W). |
| limite | Forzar la entidad de límite u objetivo de carga (%). |
| temps_restant | Forzar la entidad de tiempo de carga restante (minutos, horas o segundos). |
| verrou | Forzar la entidad de bloqueo: una entidad lock. o un sensor de clase «cerradura». |
| position | Forzar el seguimiento de posición (device_tracker.), para mostrar «En casa». |
| temp_habitacle | Forzar el sensor de temperatura del habitáculo. |
| pression_av_g, pression_av_d, pression_ar_g, pression_ar_d | Forzar cada sensor de presión de neumático (delantero izquierdo, delantero derecho, trasero izquierdo, trasero derecho). |
Conviene saber
La lógica de la tarjeta se probó con estados simulados que imitan varios estilos de integración (nombres en inglés o francés, presiones en kPa o psi, dispositivo conocido o no), pero no en un Home Assistant real con cada marca. Las integraciones no exponen todas las mismas entidades: si no se reconoce un dato, fuérzalo con la variable correspondiente o escríbeme para que mejore la detección.
Para ir más lejos
- La tarjeta especial Xpeng (diagnóstico de la batería con el WiCAN)
- Todos los tutoriales de Home Assistant
¿Tu coche llega a Home Assistant de otra forma? Comparte tu método: los próximos tutoriales seguirán vuestros comentarios.
¿Cuánto te cuesta de verdad una recarga? Tarifas valle, solar, cargadores rápidos y códigos XCombo: Watt Budget lo calcula todo, gratis y sin cuenta.
Abrir Watt Budget