Watt Budget Ladekosten-Rechner

Eine Home-Assistant-Karte für Ihren Xpeng

Akkustand, Reichweite, Ladevorgang, Verriegelung, Reifen und Fenster und eine vom WiCAN gelesene Akku-Diagnose: Das ist die Karte, die ich nutze, zum Kopieren bereit. Sie enthält nichts, was nur zu meinem Haus gehört.

  • Kostenlos, einfach kopieren
  • Nur eine Erweiterung: button-card
  • Texte in 4 Sprachen, je nach Home Assistant
  • Autofoto inklusive
Xpeng G6 Performance Black Edition
Zu Hause · vor 3 Min. gesehen
Geparkt
Xpeng G6
41 %201 kmMittel
Ziel 100 % · es fehlen 47,2 kWh
020406080100
VerriegeltNicht eingestecktOnlineZuhause
Innenraum
21 °C
HV-Batt.
19–20 °C
12 V
12,9 V
Kilometer
6.732 km
Reifen und FensterAlles in Ordnung
2,44zu
2,39zu
2,42zu
2,42zu
Die Karte, wie der Code unten sie anzeigt, mit Beispielwerten.

Was Sie brauchen

  • Home Assistant mit der über HACS installierten Erweiterung button-card (Kategorie Frontend).
  • Die Daten Ihres Xpeng in Home Assistant: die App XPENG Direct, der WiCAN-Pro-Dongle oder beides. Die Xpeng-Anleitung erklärt, wie.
  • Die Karte nutzt die Daten von XPENG Direct, wenn sie jünger als 30 Minuten sind, sonst die des WiCAN. Eine fehlende Entität zeigt einfach „—“ und stört nichts.

1Die Karte installieren

  1. Hauptkarte einfügenDashboard → bearbeiten → Karte hinzufügen → Manuell, dann den Code aus dem nächsten Abschnitt einfügen.
  2. Variablen anpassenDer Block variables am Anfang des Codes enthält alles, was von Ihrer Installation abhängt (Tabelle unten). Am wichtigsten ist app: Bei mir heißen die Entitäten von XPENG Direct sensor.xpeng_vehicle_0000_soc. Übernehmen Sie den Anfang des Namens vor _soc.
  3. Diagnose hinzufügen (optional)Eine zweite Karte, die genauso eingefügt wird, zeigt die vom WiCAN gelesenen Temperaturen, Zellspannungen und die 12 V.

Die Hauptkarte

Ladezustand, Reichweite, Ladevorgang, Verriegelung, Kabel, Innenraumtemperatur, Reifen und Fenster.

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: Xpeng G6                              # nom affiché en haut de la carte
  image: https://wattbudget.fr/assets/ha/xpeng-g6.webp   # photo de la voiture ; '' pour aucune, ou /local/… (dossier config/www)
  capacite_kwh: 80.8                         # capacité de la batterie (80,8 kWh sur les G6 2026), pour estimer les kWh manquants
  pression_min: 2.3                          # plage de pression des pneus jugée normale (bar)
  pression_max: 2.8
  app: xpeng_vehicle_0000                    # début du nom de vos entités XPENG Direct (sensor.xpeng_vehicle_0000_soc…)
  wican_soc: sensor.xpeng_g6_soc             # entités du WiCAN (noms proposés dans le tutoriel Xpeng)
  wican_temp_min: sensor.xpeng_g6_temperature_batterie_min
  wican_temp_max: sensor.xpeng_g6_temperature_batterie_max
  wican_cell_min: sensor.xpeng_g6_tension_cellule_min
  wican_cell_max: sensor.xpeng_g6_tension_cellule_max
  wican_12v: sensor.xpeng_g6_tension_12v
  wican_courant: sensor.xpeng_g6_courant_batterie
  wican_odometre: sensor.xpeng_g6_odometre_fiable
extra_styles: >-
  @keyframes g6mv { to { background-position: 26px 0; } }
  @keyframes g6pulse { 50% { opacity: .4; } }
  .g6halo { animation: g6pulse 2.4s ease-in-out infinite; }
  @keyframes g6pl { 50% { opacity: .7; } }
styles:
  card:
    - background: |
        [[[
        const V = variables;
        const s0 = parseFloat((((Date.now() - new Date(states['sensor.' + V.app + '_last_seen']?.state || 0).getTime()) < 1800000) ? states['sensor.' + V.app + '_soc']?.state : null) ?? states[V.wican_soc]?.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 => states[e]?.state;
  const N = e => { const x = parseFloat(S(e)); return isNaN(x) ? null : x; };
  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', asleep: 'En veille', 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', online: 'En ligne', offline: 'Hors ligne', homeChip: 'Maison', lvl: ['Critique', 'Bas', 'Moyen', 'Bon', 'Plein'], cabin: 'Habitacle', hv: 'Batt. HV', odo: 'Odomètre', tw: 'Pneus et vitres', closed: 'fermée', open: p => 'ouv. ' + p + ' %', ok: 'Tout est OK', tyres: n => n + ' pneu' + (n > 1 ? 's' : '') + ' à vérifier', wins: n => n + ' vitre' + (n > 1 ? 's' : '') + ' ouverte' + (n > 1 ? 's' : ''), hist: 'Historique de la batterie' },
    en: { charging: 'Charging', driving: 'Driving', asleep: 'Asleep', 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', online: 'Online', offline: 'Offline', homeChip: 'Home', lvl: ['Critical', 'Low', 'Medium', 'Good', 'Full'], cabin: 'Cabin', hv: 'HV batt.', odo: 'Odometer', tw: 'Tyres and windows', closed: 'closed', open: p => 'open ' + p + '%', ok: 'All good', tyres: n => n + ' tyre' + (n > 1 ? 's' : '') + ' to check', wins: n => n + ' window' + (n > 1 ? 's' : '') + ' open', hist: 'Battery history' },
    es: { charging: 'Cargando', driving: 'En marcha', asleep: 'En reposo', 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', online: 'En línea', offline: 'Sin conexión', homeChip: 'Casa', lvl: ['Crítico', 'Bajo', 'Medio', 'Bueno', 'Lleno'], cabin: 'Habitáculo', hv: 'Bat. AV', odo: 'Cuentakm', tw: 'Neumáticos y ventanillas', closed: 'cerrada', open: p => 'abierta ' + p + ' %', ok: 'Todo correcto', tyres: n => n + ' neumático' + (n > 1 ? 's' : '') + ' a revisar', wins: n => n + ' ventanilla' + (n > 1 ? 's' : '') + ' abierta' + (n > 1 ? 's' : ''), hist: 'Historial de la batería' },
    de: { charging: 'Lädt', driving: 'Unterwegs', asleep: 'Standby', 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', online: 'Online', offline: 'Offline', homeChip: 'Zuhause', lvl: ['Kritisch', 'Niedrig', 'Mittel', 'Gut', 'Voll'], cabin: 'Innenraum', hv: 'HV-Akku', odo: 'Kilometer', tw: 'Reifen und Fenster', closed: 'geschlossen', open: p => 'offen ' + p + ' %', ok: 'Alles in Ordnung', tyres: n => n + ' Reifen prüfen', wins: n => n + ' Fenster offen', hist: 'Akkuverlauf' }
  };
  const T = TT[lang] || TT.en;

  const P = 'sensor.' + V.app + '_', B = 'binary_sensor.' + V.app + '_';
  const fresh = (Date.now() - new Date(S(P + 'last_seen') || 0).getTime()) < 1800000;
  const soc = (fresh ? N(P + 'soc') : null) ?? N(V.wican_soc);
  const socV = soc === null ? 0 : Math.max(0, Math.min(100, soc));
  const rng = fresh ? N(P + 'range') : ((N(P + 'range') !== null && N(P + 'soc') && soc !== null) ? N(P + 'range') * soc / N(P + 'soc') : null);
  const tgt = (fresh ? N(P + 'targetsoc') : null) ?? 100;
  const miss = fresh ? N(P + 'estimated_kwh_missing') : (soc !== null ? Math.max(0, (tgt - soc) / 100 * V.capacite_kwh) : null);
  const online = S(B + 'direct_online') === 'on';
  const cable = S(B + 'cable_connected') === 'on';
  const locked = S(B + 'driver_door_lock') === 'off';
  const charging = S(B + 'charging') === 'on';
  const pw = N(P + 'charge_power');
  const rem = N(P + 'charge_remaining');
  const spd = N(P + 'speed') || 0;
  const driving = !charging && spd > 3;

  const la = N(P + 'latitude'), lo = N(P + 'longitude');
  const hz = states['zone.home']?.attributes;
  let home = null;
  if (la !== null && lo !== null && hz) {
    const dLa = (la - hz.latitude) * 111320, dLo = (lo - hz.longitude) * 111320 * Math.cos(hz.latitude * Math.PI / 180);
    home = Math.sqrt(dLa * dLa + dLo * dLo) < 200;
  }
  const seen = S(P + 'last_seen');
  const age = seen ? Math.round((Date.now() - new Date(seen).getTime()) / 60000) : null;
  const ageTxt = age === null ? 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 C = { g: '#3ddc97', am: '#f5a524', rd: '#ff5d5d', bl: '#4fa3ff', yl: '#f7d154', vi: '#b18cff', or: '#ff8a3d', mu: '#8a919c' };
  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 (!online) { st = T.asleep; stIc = 'mdi:sleep'; stC = C.mu; }
  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' : (age === null ? T.nodata : 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 = T.maxAt(fr(N(P + 'max_range'), 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 = '';
  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 chips += cable ? chip('mdi:power-plug', T.plugged, C.bl) : chip('mdi:power-plug-off', T.unplugged, C.mu);
  chips += online ? chip('mdi:wifi', T.online, C.g) : chip('mdi:wifi-off', T.offline, 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 tMin = N(V.wican_temp_min), tMax = N(V.wican_temp_max);
  const tb = tMin === null ? '—' : (tMin === tMax ? fr(tMin, 0) : fr(tMin, 0) + '–' + fr(tMax, 0)) + ' °C';
  const v12 = N(V.wican_12v);
  const odo = N(V.wican_odometre) ?? N(P + 'odometer');
  const odoTxt = odo === null ? '—' : Math.round(odo).toLocaleString(lang) + ' km';

  const W = { fl: 'front_left', fr: 'front_right', rl: 'rear_left', rr: 'rear_right' };
  let nWin = 0, nTyre = 0;
  const pill = k => {
    const p = N(P + W[k] + '_tire_pressure'), w = N(P + W[k] + '_window_position');
    const pOk = p !== null && p >= V.pression_min && p <= V.pression_max, wOk = w === null || w >= 100;
    if (!pOk) nTyre++; if (!wOk) nWin++;
    const pc = pOk ? C.g : C.am, wc = wOk ? C.g : C.am;
    const wt = w === null ? '—' : wOk ? T.closed : T.open(Math.round(100 - w));
    return { pc: pc, wc: wc, h: '<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:' + pc + ';flex:none"></span><b style="font-weight:500">' + fr(p, 2) + '</b><span style="color:' + C.mu + ';font-size:11px;margin-left:auto;display:flex;align-items:center;gap:3px;white-space:nowrap">' + IC('mdi:car-door', 13, wc) + wt + '</span></div>' };
  };
  const q = { fl: pill('fl'), fr: pill('fr'), rl: pill('rl'), rr: pill('rr') };
  const sumTxt = (nTyre === 0 && nWin === 0) ? T.ok : [nTyre ? T.tyres(nTyre) : '', nWin ? T.wins(nWin) : ''].filter(x => x).join(' · ');
  const sumC = (nTyre === 0 && nWin === 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:\'' + P + 'soc\'}}))" 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="g6halo" 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:g6mv .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:g6pl 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>';
  H += '<div>' + chips + '</div>';

  H += '<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:5px">';
  H += tile('mdi:car-seat', T.cabin, fr(N(P + 'interior_temp'), 0) + ' °C', C.or);
  H += tile('mdi:battery-high', T.hv, tb, C.bl);
  H += tile('mdi:car-battery', '12 V', fr(v12, 1) + ' V', C.yl, v12 !== null && v12 < 12.2 ? C.rd : null);
  H += tile('mdi:counter', T.odo, odoTxt, C.vi);
  H += '</div>';

  H += '<div style="' + SEC + '">' + H4('mdi:tire', T.tw, sumTxt, sumC);
  H += '<div style="display:grid;grid-template-columns:minmax(0,1fr) 92px minmax(0,1fr);gap:8px;align-items:stretch">';
  H += '<div style="display:flex;flex-direction:column;justify-content:space-between;padding:16px 0 10px">' + q.fl.h + q.rl.h + '</div>';
  H += '<svg viewBox="0 0 92 150" width="92"><rect x="22" y="8" width="48" height="134" rx="22" fill="#2b3038" stroke="#3a414b"/><rect x="28" y="36" width="36" height="24" rx="6" fill="#14171b"/><rect x="28" y="98" width="36" height="20" rx="6" fill="#14171b"/>';
  H += '<rect x="22" y="62" width="4" height="16" rx="2" fill="' + q.fl.wc + '"/><rect x="66" y="62" width="4" height="16" rx="2" fill="' + q.fr.wc + '"/><rect x="22" y="82" width="4" height="14" rx="2" fill="' + q.rl.wc + '"/><rect x="66" y="82" width="4" height="14" rx="2" fill="' + q.rr.wc + '"/>';
  H += '<rect x="14" y="28" width="9" height="22" rx="3" fill="' + q.fl.pc + '"/><rect x="69" y="28" width="9" height="22" rx="3" fill="' + q.fr.pc + '"/><rect x="14" y="104" width="9" height="22" rx="3" fill="' + q.rl.pc + '"/><rect x="69" y="104" width="9" height="22" rx="3" fill="' + q.rr.pc + '"/>';
  H += '<text x="46" y="20" text-anchor="middle" fill="#8a919c" font-size="9">▲</text></svg>';
  H += '<div style="display:flex;flex-direction:column;justify-content:space-between;padding:16px 0 10px">' + q.fr.h + q.rr.h + '</div>';
  H += '</div></div>';

  return H + '</div>';
  ]]]

Akku-Diagnose (WiCAN)

Optional. Hochvoltstrom, Temperaturen, Zellspannungen, 12 V. Benötigt die WiCAN-Entitäten.

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:
  wican_soc: sensor.xpeng_g6_soc             # entités du WiCAN (noms proposés dans le tutoriel Xpeng)
  wican_temp_min: sensor.xpeng_g6_temperature_batterie_min
  wican_temp_max: sensor.xpeng_g6_temperature_batterie_max
  wican_cell_min: sensor.xpeng_g6_tension_cellule_min
  wican_cell_max: sensor.xpeng_g6_tension_cellule_max
  wican_12v: sensor.xpeng_g6_tension_12v
  wican_courant: sensor.xpeng_g6_courant_batterie
styles:
  card:
    - background: '#1c1f24'
    - border: 1px solid #2b3038
    - border-radius: 18px
    - padding: 16px
    - box-shadow: none
    - overflow: hidden
  label:
    - width: 100%
    - text-align: left
    - white-space: normal
    - justify-self: stretch
label: |
  [[[
  const V = variables;
  const S = e => states[e]?.state;
  const N = e => { const x = parseFloat(S(e)); return isNaN(x) ? null : x; };
  const lang = String(hass.language || 'fr').slice(0, 2);
  const fr = (x, d) => (x === null || isNaN(x)) ? '—' : x.toFixed(d).replace('.', lang === 'en' ? '.' : ',');
  const TT = {
    fr: { title: 'Diagnostic batterie', nodata: 'pas de donnée', upd: n => 'maj il y a ' + n + ' min', hvOn: 'HV active', hvOff: 'HV en veille', cur: 'Courant HV', tmin: 'Temp. min', tmax: 'Temp. max', cmin: 'Cellule min', cmax: 'Cellule max', spread: 'Écart cellules', note: "Lecture directe OBD par le WiCAN Pro (MQTT), complétée par XPENG Direct pour les portes, vitres, pneus et la position. L'état de santé (SOH) et la tension du pack ne sont pas exposés." },
    en: { title: 'Battery diagnostics', nodata: 'no data', upd: n => 'updated ' + n + ' min ago', hvOn: 'HV active', hvOff: 'HV asleep', cur: 'HV current', tmin: 'Min temp.', tmax: 'Max temp.', cmin: 'Min cell', cmax: 'Max cell', spread: 'Cell spread', note: 'Read straight from the OBD port by the WiCAN Pro (MQTT), with XPENG Direct adding doors, windows, tyres and position. State of health (SOH) and pack voltage are not exposed.' },
    es: { title: 'Diagnóstico de la batería', nodata: 'sin datos', upd: n => 'actualizado hace ' + n + ' min', hvOn: 'AV activa', hvOff: 'AV en reposo', cur: 'Corriente AV', tmin: 'Temp. mín.', tmax: 'Temp. máx.', cmin: 'Celda mín.', cmax: 'Celda máx.', spread: 'Dif. celdas', note: 'Lectura directa del OBD con el WiCAN Pro (MQTT), completada con XPENG Direct para puertas, ventanillas, neumáticos y posición. El estado de salud (SOH) y la tensión del pack no están disponibles.' },
    de: { title: 'Akku-Diagnose', nodata: 'keine Daten', upd: n => 'aktualisiert vor ' + n + ' Min.', hvOn: 'HV aktiv', hvOff: 'HV im Standby', cur: 'HV-Strom', tmin: 'Temp. min.', tmax: 'Temp. max.', cmin: 'Zelle min.', cmax: 'Zelle max.', spread: 'Zellspreizung', note: 'Direkt über den OBD-Anschluss vom WiCAN Pro gelesen (MQTT), ergänzt durch XPENG Direct für Türen, Fenster, Reifen und Position. Gesundheitszustand (SOH) und Packspannung sind nicht verfügbar.' }
  };
  const T = TT[lang] || TT.en;
  const C = { g: '#3ddc97', am: '#f5a524', rd: '#ff5d5d', bl: '#4fa3ff', yl: '#f7d154', vi: '#b18cff', or: '#ff8a3d', mu: '#8a919c' };
  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>';
  const tile = (ic, k, val, c, vc) => '<div style="background:#252a31;border-radius:12px;padding:8px 6px;min-width:0;text-align:center;border-top:2px solid ' + c + '"><div style="font-size:11px;color:#8a919c;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:15px;font-weight:500;white-space:nowrap;margin-top:3px;color:' + (vc || '#e8eaed') + '">' + val + '</div></div>';
  const grid = (n, a) => '<div style="display:grid;grid-template-columns:repeat(' + n + ',minmax(0,1fr));gap:8px;margin-top:10px">' + a.join('') + '</div>';
  const badge = (t, ic, c) => '<span style="margin-left:auto;font-size:12px;padding:3px 10px;border-radius:20px;background:' + mix(c, .18) + ';color:' + c + ';display:inline-flex;gap:4px;align-items:center;white-space:nowrap">' + IC(ic, 14) + '<span>' + t + '</span></span>';
  const hd = (ic, c, t, s, b) => '<div style="display:flex;align-items:center;gap:10px"><div style="width:34px;height:34px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:' + mix(c, .18) + ';flex:none">' + IC(ic, 19, c) + '</div><div style="min-width:0"><div style="font-size:15px;font-weight:500">' + t + '</div><div style="font-size:12px;color:#8a919c">' + s + '</div></div>' + (b || '') + '</div>';

  const tMin = N(V.wican_temp_min), tMax = N(V.wican_temp_max);
  const cMin = N(V.wican_cell_min), cMax = N(V.wican_cell_max);
  const v12 = N(V.wican_12v), cur = N(V.wican_courant);
  const lu = states[V.wican_soc]?.last_updated;
  const age = lu ? Math.round((Date.now() - new Date(lu).getTime()) / 60000) : null;
  const hv = v12 !== null && v12 > 13.3;

  let H = '<div style="color:#e8eaed;font-size:13px;line-height:1.35;width:100%">';
  H += hd('mdi:car-wrench', C.bl, T.title, 'WiCAN OBD · ' + (age === null ? T.nodata : T.upd(age)), badge(hv ? T.hvOn : T.hvOff, 'mdi:flash', hv ? C.g : C.mu));
  H += grid(3, [
    tile('mdi:current-dc', T.cur, fr(cur, 1) + ' A', C.bl),
    tile('mdi:thermometer-low', T.tmin, fr(tMin, 0) + ' °C', C.bl),
    tile('mdi:thermometer-high', T.tmax, fr(tMax, 0) + ' °C', C.or),
    tile('mdi:sine-wave', T.cmin, fr(cMin, 3) + ' V', C.g),
    tile('mdi:sine-wave', T.cmax, fr(cMax, 3) + ' V', C.g),
    tile('mdi:car-battery', '12 V', fr(v12, 1) + ' V', C.yl, v12 !== null && v12 < 12.2 ? C.rd : null)
  ]);
  H += grid(1, [tile('mdi:delta', T.spread, (cMin !== null && cMax !== null) ? fr((cMax - cMin) * 1000, 0) + ' mV' : '—', C.vi)]);
  H += '<div style="display:flex;gap:6px;margin-top:10px;font-size:11px;color:#6b727c;line-height:1.5">' + IC('mdi:information-outline', 14) + '<span>' + T.note + '</span></div>';
  return H + '</div>';
  ]]]

Die Variablen

VariableZweck
nomName oben auf der Karte.
imageAutofoto. Standardmäßig das auf dieser Seite gehostete. Um nicht von der Seite abzuhängen, speichern Sie es in config/www und tragen /local/xpeng-g6.webp ein; '' zeigt kein Bild.
capacite_kwhAkkukapazität, um die fehlenden kWh zu schätzen, wenn XPENG Direct nicht mehr antwortet (80,8 beim G6 2026).
pression_min, pression_maxAls normal geltender Reifendruckbereich in bar. Außerhalb wird der Reifen orange.
appAnfang der Namen Ihrer XPENG-Direct-Entitäten vor _soc (z. B. xpeng_vehicle_0000).
wican_socVom WiCAN gelesener Akkustand, genutzt, wenn XPENG Direct seit 30 Minuten nichts gesendet hat.
wican_temp_min, wican_temp_maxTemperaturen des Hochvoltakkus.
wican_cell_min, wican_cell_maxNiedrigste und höchste Zellspannung (Diagnose).
wican_12v, wican_courantSpannung der 12-V-Batterie und Hochvoltstrom (Diagnose).
wican_odometreGenauer Kilometerstand vom WiCAN; sonst der von XPENG Direct.
Xpeng G6

Das G6-Foto liegt auf dieser Seite: Bild herunterladen. Für ein anderes Auto oder eine andere Farbe ändern Sie einfach die Variable image.

Was die Karte nicht enthält

Auf meinem eigenen Dashboard zeigt die Karte zusätzlich den Solaranteil jeder Ladung und die Jahresbilanz: Diese Zahlen stammen von meiner Wallbox, meinen Batterien und meinen Tarifen und sind deshalb nicht in diesem Code.

Weiterführend

Kommt Ihr Auto auf anderem Weg in Home Assistant? Teilen Sie Ihre Methode: Die nächsten Anleitungen richten sich nach Ihren Rückmeldungen.

Was kostet eine Ladung wirklich? Nachttarife, Solarstrom, Schnelllader und XCombo-Codes: Watt Budget rechnet alles aus, kostenlos und ohne Konto.

Watt Budget öffnen