Watt Budget Calculadora de recarga

Una tarjeta de Home Assistant para cualquier coche eléctrico

Batería, autonomía, carga en curso, bloqueo, neumáticos, kilometraje: una sola tarjeta para Renault, Tesla, Kia, BMW, Peugeot, Volkswagen y las demás. Indicas una sola entidad, la batería, y la tarjeta encuentra el resto a partir del dispositivo de tu coche.

  • Gratuita, para copiar y pegar
  • Una sola entidad que indicar
  • Una sola extensión: button-card
  • Textos en 4 idiomas, según Home Assistant
Mi coche
En casa · visto hace 3 min
Enchufado
47%176 km●
050 %100 %
Puertas
Cerradas
Odómetro
33.950 km
Objetivo
100 %
La tarjeta con valores de ejemplo: solo se muestran las filas cuyos datos encuentra la tarjeta.

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

  1. Pega la tarjetaPanel → editar → añadir tarjeta → Manual y pega el código de la sección siguiente.
  2. Indica la bateríaEn el bloque variables, arriba, sustituye sensor.ma_voiture_batterie por la entidad de batería de tu coche. Cambia también nom y, si quieres, la foto (image) y la capacidad de la batería (capacite_kwh).
  3. 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.
  4. 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.

MarcaSensor que indicar en «batterie»
XpengEl 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 · DaciaEl sensor Batería del dispositivo Renault (en %).
MGEl sensor SoC (nivel de batería en %) del dispositivo MQTT del coche.
ŠkodaEl sensor de nivel de batería (en %) del dispositivo Škoda.
Kia · Hyundai · GenesisEl sensor EV Battery Level (en %) del dispositivo Kia Uvo.
TeslaEl sensor Nivel de batería (en %) del dispositivo del coche.
Peugeot · Citroën · DS · OpelEl sensor Battery (en %) del dispositivo Stellantis Vehicles.
Volkswagen · Cupra · SEAT · AudiEl sensor de nivel de batería (en %) del dispositivo VW Group Connect.
BMW · MiniEl sensor de nivel de batería (en %) que BMW CarData crea para el coche.
FordEl sensor de nivel de batería (en %) del dispositivo Ford.
NissanEl sensor Battery (en %) del dispositivo NissanConnect.
VolvoEl sensor Batería (en %) del dispositivo Volvo.
PolestarEl sensor de nivel de batería (en %) del dispositivo Polestar.
Mercedes-BenzEl sensor de nivel de batería (state of charge, en %) del dispositivo Mercedes.
BYDEl sensor de nivel de batería (en %) que publica el WiCAN por MQTT.

Cómo encuentra la tarjeta tus datos

  • Parte de la entidad batterie y 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

VariableFunción
nomNombre que se muestra arriba de la tarjeta.
imageFoto del coche: '' para ninguna, o /local/… (carpeta config/www de Home Assistant).
batterieLa única entidad obligatoria: el nivel de batería en %.
capacite_kwhCapacidad de la batería en kWh, para estimar los kWh que faltan hasta el objetivo. 0 para no mostrarlos.
pression_min, pression_maxRango de presión de neumáticos considerado normal, en bar.
autonomieForzar la entidad de autonomía (km).
odometreForzar la entidad del kilometraje.
en_chargeForzar la entidad «cargando» (sensor on/off o estado de carga).
brancheForzar la entidad «enchufado».
puissanceForzar la entidad de potencia de carga (kW o W).
limiteForzar la entidad de límite u objetivo de carga (%).
temps_restantForzar la entidad de tiempo de carga restante (minutos, horas o segundos).
verrouForzar la entidad de bloqueo: una entidad lock. o un sensor de clase «cerradura».
positionForzar el seguimiento de posición (device_tracker.), para mostrar «En casa».
temp_habitacleForzar el sensor de temperatura del habitáculo.
pression_av_g, pression_av_d, pression_ar_g, pression_ar_dForzar cada sensor de presión de neumático (delantero izquierdo, delantero derecho, trasero izquierdo, trasero derecho).

Conviene saber

Probada con estados simulados, no con cada marca

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

¿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