Adapter Hyundai (Bluelink) oder KIA (UVO)
-
@arteck
3.1.6 läuft. Danke.
-
Jetzt haben sie offenbar an der Abfrage der Geo-Daten geschraubt. Diese API ist aktuell wirklich ein Graus.
bluelink.0 2025-08-13 15:58:00.349 error Error on API-Request Status, ErrorCount:1 bluelink.0 2025-08-13 15:58:00.350 error Cannot read properties of undefined (reading 'GeoCoord')
-
@arteck
Danke, werde es morgen mal testen, Kaffeespende ist schon mal unterwegs -
@meister-mopper hast du die neue von GIT geladen ?
-
@arteck sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):
@meister-mopper hast du die neue von GIT geladen ?
Danke, mit v3.1.20 ist der Fehler weg.
-
aber bei der Version 3.1.20 geht der Login beim Hyundai noch nicht? Ich bin ja von 3.1.19 auf 3.1.17 zurück, da geht es alles
-
bei mir ist die letzte die 3.1.16, alles was danach kam, geht nicht..
Und nein, @arteck ist nicht Merlin
auch wenn er hier schon viel zaubert
Ueberlegt mal, wieviel verschiedene Fahrzeugtypen es bei Hyundai/Kia gibt, mit zig verschiedenen Software und Hardware variationen.. -
@rissn sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):
aber bei der Version 3.1.20 geht der Login beim Hyundai noch nicht? Ich bin ja von 3.1.19 auf 3.1.17 zurück, da geht es alles
dann lass laufen .. hab schon mal gesagt.. es ändert sich nix an dem adapter ausser für KIA Fahrzeuge die mal wieder was anderes senden..
wenn eine ältere Version geht (login funktioniert) dann nutze diese .. ich werde nicht eine alternative einbauen ala "welches login willst du haben neu oder alt" das ist zu viel Arbeit.. und so wie es aussieht passiert da gerade sehr viel in der API .. -
@arteck
Danke für deine ganzen Mühen, Version 3.1.20 läuft bei Kia wieder ohne Fehlermeldungen und tut was es soll.
Hoffen wir mal, das die an der Api/Servern nicht mehr zuviel rumschrauben. -
Hab mal meinen digitalen Codierknecht
solange gequaelt, bis das hier rauskam:
Ist n einfaches Dashboard, kann in jeder Vis (Vis, Vis-NG, MinuVis, Jarvis und was es noch so gibt) angezeigt werden, legt die Daten in einen State ab.
Hier das Script dazu, das Hintergrundbild kann natuerlich angepasst werden, muss nich meins von der Nordschleife sein
Eingebaut sind mir alle sinvoll vorgekommenen Werte des Adapters Bluelink, sowie von meiner Wallbox Go-E und ein paar selbst definierte (12V laedt), desweiteren eine Ladehistorie graphisch und tabellar..- responsive Design, kann man auch aufm Smartphone ansehen
/****************************************************** * IONIQ 5 N – Bluelink Dashboard (HTML) for ioBroker * Version: 1.6.6 (SVG-Chart responsive, volle Kartenbreite & ~Tabelle-Höhe) * (c) 2025 Bernd / ilovegym – privat ******************************************************/ /* =================== KONFIG =================== */ const BLUELINK_INST = 'bluelink.0'; let VEHICLE_ID = ''; // leer = Auto-Detect const OUT_HTML_DP = '0_userdata.0.vis.IoniqDashboardHTML'; // Eigene Zusatz-DPs const DP_12V_LADEN = '0_userdata.0.Zaehler.Hyundai_12V'; // true = 12V lädt const DP_KM_YESTER = '0_userdata.0.Zaehler.Hyundai_KM_'; // km gestern // Wallbox (go-e) const WB = { energy: 'go-e.0.loaded_energy_kwh', state: 'go-e.0.car', // 1 Standby, 2 Laden, 3 Warten auf Auto, 4 Fertig power: 'go-e.0.energy.power', temp1: 'go-e.0.temperatures.temperature1', temp2: 'go-e.0.temperatures.temperature2', allow: 'go-e.0.allow_charging', amp: 'go-e.0.ampere' }; const WB_STATE_TXT = {1:'Standby',2:'Laden',3:'Warte auf Auto',4:'Fertig'}; // Ladehistorie (Samples werden hier gespeichert) const HIST_SAMPLES_DP = '0_userdata.0.Ioniq.History.samples'; // string (JSON Array: {t,soc,wb}) const HIST_LAST_TS_DP = '0_userdata.0.Ioniq.History.lastSample'; // number (ms) const SAMPLE_INTERVAL_MS = 5 * 60 * 1000; // alle 5 Minuten const MAX_SAMPLES = 3000; // ~7 Tage bei 5-Minuten-Samples // Debug const DEBUG = false; /* =================== UTIL =================== */ function ensureState(id, def = '', common = {name:'IONIQ 5 N Dashboard HTML', type:'string', role:'html', read:true, write:false}) { try { if (!existsObject(id)) createState(id, def, true, common); } catch (e) { log('ensureState error ' + e, 'warn'); } } function ensureDataPoint(id, def, common){ try{ if(!existsObject(id)) createState(id, def, true, common); }catch(e){} } function JP(...parts){ return parts.filter(p => p !== '' && p != null).join('.'); } function es(id){ try { return !!(id && existsState(id)); } catch(_) { return false; } } function gs(id){ try { return es(id)? getState(id).val:undefined; } catch(_) { return undefined; } } function ss(id, val){ try { setState(id, val, true); } catch(_){} } function firstExisting(paths){ if(!Array.isArray(paths)) return {path:null,val:undefined}; for(const p of paths){ if(es(p)){ const v=gs(p); if(v!==undefined && v!==null) return {path:p,val:v}; } } return {path:null,val:undefined}; } function P(...parts){ return JP(BLUELINK_INST, VEHICLE_ID, ...parts); } /* =================== VIN-AUTODETECT =================== */ function detectVehicleId(){ try{ const rows = getObjectView('system','state',{ startkey: BLUELINK_INST+'.', endkey: BLUELINK_INST+'.\u9999' }).rows; const seen = {}; for(const r of rows){ const id = r.id || ''; const seg = id.split('.'); if(seg.length>=3){ const veh = seg[2]; if(veh && !['info','remote','vehicles'].includes(veh)) seen[veh]=true; } } const list = Object.keys(seen); return list.length ? list[0] : ''; }catch(e){ log('VIN-Detect: '+e,'warn'); return ''; } } /* =================== KANDIDATEN =================== */ function candidates(){ return { // Identität / Fahrt carName: [ P('general.carName'), P('general.modelName') ], vin: [ P('general.vin') ], odometer_km: [ P('odometer.value') ], speed: [ P('vehicleLocation.speed') ], // Standort (mit Fallbacks) lat: [ P('vehicleLocation.lat'), P('location.coord.lat') ], lon: [ P('vehicleLocation.lon'), P('location.coord.lon') ], position_text: [ P('vehicleLocation.position_text'), P('location.formattedAddress') ], position_url: [ P('vehicleLocation.position_url') ], // HV & 12V soc_pct: [ P('vehicleStatus.battery.soc') ], charge_active: [ P('vehicleStatus.battery.charge') ], minutes_to_charged: [ P('vehicleStatus.battery.minutes_to_charged') ], plugin_code: [ P('vehicleStatus.battery.plugin') ], soc12v: [ P('vehicleStatus.battery.soc-12V') ], state12v: [ P('vehicleStatus.battery.state-12V') ], soh: [ P('vehicleStatus.battery.soh') ], charge12v: [ DP_12V_LADEN ], // Klima & Komfort hvacOn: [ P('vehicleStatus.airCtrlOn') ], insideTemp: [ P('vehicleStatus.airTemp') ], airClean: [ P('vehicleStatusRaw.vehicleStatus.airCleaning.airPurifierStatus') ], defrost: [ P('vehicleStatusRaw.vehicleStatus.defrost') ], seatFL: [ P('vehicleStatusRaw.vehicleStatus.seatHeaterVentState.flSeatHeatState') ], seatFR: [ P('vehicleStatusRaw.vehicleStatus.seatHeaterVentState.frSeatHeatState') ], seatRL: [ P('vehicleStatusRaw.vehicleStatus.seatHeaterVentState.rlSeatHeatState') ], seatRR: [ P('vehicleStatusRaw.vehicleStatus.seatHeaterVentState.rrSeatHeatState') ], steerHeat: [ P('vehicleStatus.steerWheelHeat') ], // Öffnungen / Fenster doorFL: [ P('vehicleStatus.doorOpen.frontLeft') ], doorFR: [ P('vehicleStatus.doorOpen.frontRight') ], doorRL: [ P('vehicleStatus.doorOpen.backLeft') ], doorRR: [ P('vehicleStatus.doorOpen.backRight') ], trunk: [ P('vehicleStatus.trunkOpen') ], frunk: [ P('vehicleStatus.hoodOpen') ], winFL: [ P('vehicleStatusRaw.vehicleStatus.windowOpen.frontLeft') ], winFR: [ P('vehicleStatusRaw.vehicleStatus.windowOpen.frontRight') ], winRL: [ P('vehicleStatusRaw.vehicleStatus.windowOpen.backLeft') ], winRR: [ P('vehicleStatusRaw.vehicleStatus.windowOpen.backRight') ], // Reifen-Warnlampen tireFL: [ P('vehicleStatusRaw.vehicleStatus.tirePressureLamp.tirePressureLampFL') ], tireFR: [ P('vehicleStatusRaw.vehicleStatus.tirePressureLamp.tirePressureLampFR') ], tireRL: [ P('vehicleStatusRaw.vehicleStatus.tirePressureLamp.tirePressureLampRL') ], tireRR: [ P('vehicleStatusRaw.vehicleStatus.tirePressureLamp.tirePressureLampRR') ], // Flüssigkeiten & Warnungen dte: [ P('vehicleStatusRaw.vehicleStatus.dte.value') ], washer: [ P('vehicleStatus.washerFluidStatus') ], // false=OK, true=leer smartKeyBat:[ P('vehicleStatus.smartKeyBatteryWarning') ], // true=Warnung breakOil: [ P('vehicleStatus.breakOilStatus') ], // false=OK, true=Niedrig // Wallbox wb_energy: [ WB.energy ], wb_state: [ WB.state ], wb_power: [ WB.power ], wb_temp1: [ WB.temp1 ], wb_temp2: [ WB.temp2 ], wb_allow: [ WB.allow ], wb_amp: [ WB.amp ], // Zeit / extra lastUpdate: [ P('info.lastUpdate'), P('vehicleStatus.updatedAt') ], kmYesterday: [ DP_KM_YESTER ] }; } /* =================== CSS =================== */ function css(){ return ` <style> :root{ --bg:#0a0c10; --card:rgba(18,21,28,0.88); --muted:#8a93a6; --text:#e7ecf6; --ok:#33d17a; --warn:#ffbf3c; --err:#ff5c5c; --accent:#60a5fa; --chip:#1b2030; --chipText:#cfe0ff; } *{box-sizing:border-box} .wrap{ font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif; color:var(--text); padding:16px; min-height:100vh; background:url('http://10.1.1.2:8081/files/0_userdata.0/background/IMG_2721.jpeg') center/cover no-repeat fixed; } .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px;align-items:stretch} .card{background:var(--card);border-radius:12px;padding:12px;box-shadow:0 4px 14px rgba(0,0,0,.25);height:100%;display:flex;flex-direction:column} .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap} .title{display:flex;align-items:center;gap:10px;font-weight:700;font-size:18px;margin-bottom:10px} .kpi{font-size:26px;font-weight:800} .sub{color:var(--muted);font-size:13px} .badge{background:#1b2030;color:#cfe0ff;padding:6px 12px;border-radius:10px;font-size:14px;display:inline-block;max-width:100%;white-space:normal;word-break:break-word;text-align:center} .stat{display:flex;justify-content:space-between;margin:4px 0;font-size:13px} .meter{height:10px;background:#0f1220;border-radius:8px;overflow:hidden} .meter>span{display:block;height:100%} .kv{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin-top:8px} .tile{background:#0f1220;border-radius:8px;padding:6px;font-size:12px;display:flex;align-items:center;gap:6px} .ok{color:var(--ok)} .warn{color:var(--warn)} .err{color:var(--err)} .acc{color:var(--accent)} .footer{margin-top:8px;color:var(--muted);font-size:12px;text-align:right} table{width:100%;border-collapse:collapse;font-size:12px} th,td{padding:6px 8px;border-bottom:1px solid #222} th{text-align:left;color:#cfe0ff} /* Chart Box: gleiche Höhe wie Tabelle (~280px), passt sich Breite an */ .chartBox{width:100%;height:280px;display:block} .chartBox svg{width:100%;height:100%;display:block} </style>`; } /* =================== SVG CHART BUILDER =================== */ function buildHistorySVG(labels, soc, kwh){ // Basisgröße für viewBox (skaliert über CSS auf 100%x100% in .chartBox) const W=720, H=280; // größer als vorher const padL=44, padR=14, padT=14, padB=34; const x0=padL, y0=padT, x1=W-padR, y1=H-padB; const w=x1-x0, h=y1-y0; const n = Math.max(1, labels.length||1); const xAt = (i)=> x0 + (n<=1 ? 0 : (w * i/(n-1))); const ySoc = (v)=> y0 + (1 - (Math.max(0,Math.min(100, +v||0))/100)) * h; // Bars scale let maxK=0; for (let i=0;i<kwh.length;i++){ const v=+kwh[i]||0; if (v>maxK) maxK=v; } if (maxK<1) maxK=1; const barW = Math.max(8, Math.min(28, w / (n*1.8))); const bars = []; for (let i=0;i<n;i++){ const xx = xAt(i); const kh = ( ( (+kwh[i]||0)/maxK ) * h ); const x = xx - barW/2; const y = y1 - kh; const bw = barW; const bh = kh; bars.push(`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${bh.toFixed(1)}" rx="2" fill="#60a5fa" />`); } // SOC path let d=''; for (let i=0;i<n;i++){ const xx = xAt(i), yy = ySoc(soc[i]||0); d += (i===0? 'M':' L') + xx.toFixed(1) + ' ' + yy.toFixed(1); } const points = soc.map((v,i)=>{ const xx=xAt(i), yy=ySoc(v||0); return `<circle cx="${xx.toFixed(1)}" cy="${yy.toFixed(1)}" r="3" fill="#33d17a"/>`; }).join(''); // grid & labels const grid=[]; for (let gy=0; gy<=5; gy++){ const yy = y0 + h*(gy/5); grid.push(`<line x1="${x0}" y1="${yy.toFixed(1)}" x2="${x1}" y2="${yy.toFixed(1)}" stroke="rgba(255,255,255,0.15)" stroke-width="1"/>`); } const ylab=[]; for (let gy=0; gy<=5; gy++){ const val = 100 - gy*20; const yy = y0 + h*(gy/5); ylab.push(`<text x="${x0-8}" y="${yy+4}" fill="#cfe0ff" font-size="12" text-anchor="end">${val}</text>`); } const xlab = labels.map((t,i)=>{ const xx = xAt(i); return `<text x="${xx}" y="${y1+18}" fill="#cfe0ff" font-size="12" text-anchor="middle">${String(t)}</text>`; }).join(''); return ` <svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Ladehistorie"> <rect x="0" y="0" width="${W}" height="${H}" fill="transparent"/> <rect x="${x0}" y="${y0}" width="${w}" height="${h}" rx="6" fill="rgba(255,255,255,0.04)"/> ${grid.join('')} ${ylab.join('')} ${xlab} ${bars.join('')} <path d="${d}" fill="none" stroke="#33d17a" stroke-width="2.5"/> ${points} <!-- Legende --> <g> <rect x="${x0+6}" y="${y0+6}" width="12" height="3" fill="#33d17a"/><text x="${x0+24}" y="${y0+14}" font-size="13" fill="#cfe0ff">SOC %</text> <rect x="${x0+92}" y="${y0+6}" width="12" height="12" fill="#60a5fa"/><text x="${x0+110}" y="${y0+16}" font-size="13" fill="#cfe0ff">kWh/Tag</text> </g> </svg>`; } /* =================== RENDER =================== */ function renderHTML(d, hist, lastSamples){ if (!d || d.__noVin){ return `${css()}<div class="wrap"><div class="card"><div class="kpi">Bluelink</div><div class="sub">Bitte VIN (VEHICLE_ID) setzen oder Auto-Detect abwarten.</div></div></div>`; } const hvColor = p=>p<75?'#ff5c5c':(p<80?'#ffbf3c':'#33d17a'); const socBar = p => (p!=null ? ` <div class="meter" style="height:14px;border-radius:7px;overflow:hidden"> <span style="width:${Math.max(0,Math.min(100,p))}%;background:${hvColor(p)}"></span> </div> <div style="font-size:12px;text-align:right;margin-top:2px">${p}%</div>` : '–'); const seatTxt = v => (v===1?'<span class="heatAnim">🔥 Heizen</span>':(v===0?'<span class="coolAnim">❄️ Kühlen</span>':'Aus')); const flag = (v, ok='geschlossen', bad='offen') => v===true?`<b class="err">${bad}</b>`:(v===false?`<b class="ok">${ok}</b>`:'–'); const tireTxt = v => (v===true ? '<b class="warn">Warn</b>' : (v===false ? '<b class="ok">OK</b>' : '–')); const locStr = d.address || '–'; const coords = (d.lat!=null && d.lon!=null) ? `${(+d.lat).toFixed(5)}, ${(+d.lon).toFixed(5)}` : '–'; const wbStateTxt = (d.wb_stateNum!=null? (WB_STATE_TXT[d.wb_stateNum]||String(d.wb_stateNum)) : (d.wb_stateText||'–')); // Historie-Daten (letzte 7 Tage) const labels = hist.labels || ['Mo','Di','Mi','Do','Fr','Sa','So']; const socLine = hist.dailySoc || [0,0,0,0,0,0,0]; const kwhBars = hist.dailyKwh || [0,0,0,0,0,0,0]; // Tabelle der letzten 10 Einträge let tableRows = ''; if (lastSamples && lastSamples.length){ const last10 = lastSamples.slice(-10); tableRows = last10.map(s=>{ const dt = new Date(s.t).toLocaleString(); const soc = (typeof s.soc==='number') ? (Math.round(s.soc*10)/10)+' %' : '–'; const wb = (typeof s.wb ==='number') ? (Math.round(s.wb*100)/100).toFixed(2)+' kWh' : '–'; return `<tr><td>${dt}</td><td>${soc}</td><td>${wb}</td></tr>`; }).join(''); } const svgChart = buildHistorySVG(labels, socLine, kwhBars); return ` ${css()} <div class="wrap"> <div class="title"><span class="kpi">${d.carName||'IONIQ 5 N'}</span> <span class="badge">${d.vin||''}</span></div> <div class="grid"> <!-- HV Akku --> <div class="card"> <div class="row">🔋 <b>State of Charge</b></div> <div class="kpi" style="margin:6px 0">${d.soc!=null? d.soc+' %' : '–'}</div> ${socBar(d.soc)} <div class="stat"><span>Laden</span><span class="${d.charging?'chargeAnim':''}"><b>${d.charging===true?'aktiv':(d.charging===false?'nein':'–')}</b></span></div> <div class="stat"><span>Min. bis voll</span><span><b>${d.minToFull!=null? d.minToFull : '–'}</b></span></div> <div class="stat"><span>Reichweite</span><span><b>${d.dte!=null? d.dte+' km':'–'}</b></span></div> </div> <!-- 12V / SOH --> <div class="card"> <div class="row">🔋 <b>12V & SOH</b></div> <div class="stat"><span>12V SoC</span><span style="flex:1;margin-left:10px">${socBar(d.soc12v)}</span></div> <div class="stat"><span>12V Status</span><span><b>${d.state12v ?? '–'}</b></span></div> <div class="stat"><span>12V lädt</span><span><b>${d.charge12v===true?'Ja':(d.charge12v===false?'Nein':'–')}</b></span></div> <div class="stat"><span>SOH (HV)</span><span><b>${d.soh!=null? d.soh+' %':'–'}</b></span></div> </div> <!-- Fahrzeug --> <div class="card"> <div class="row">🚗 <b>Fahrzeug</b></div> <div class="stat"><span>Odometer</span><span><b>${d.odoKm!=null? d.odoKm+' km':'–'}</b></span></div> <div class="stat"><span>Gestern gefahren</span><span><b>${d.kmYesterday!=null? d.kmYesterday+' km':'–'}</b></span></div> </div> <!-- Standort --> <div class="card"> <div class="row">📍 <b>Standort</b></div> <div class="badge">${locStr}</div> <div class="sub" style="margin-top:8px">Koordinaten: <b>${coords}</b></div> </div> <!-- Türen --> <div class="card"> <div class="row">🚪 <b>Öffnungen</b></div> <div class="kv"> <div class="tile">VL: ${flag(d.doorFL)}</div> <div class="tile">VR: ${flag(d.doorFR)}</div> <div class="tile">HL: ${flag(d.doorRL)}</div> <div class="tile">HR: ${flag(d.doorRR)}</div> <div class="tile">Kofferraum: ${flag(d.trunk)}</div> <div class="tile">Frunk: ${flag(d.frunk)}</div> </div> <div class="sub" style="margin-top:6px">Gesamt: ${ [d.doorFL,d.doorFR,d.doorRL,d.doorRR,d.trunk,d.frunk].some(v=>v===true) ? '<b class="err">offen</b>' : '<b class="ok">alle zu</b>' }</div> </div> <!-- Fenster --> <div class="card"> <div class="row">🪟 <b>Fenster</b></div> <div class="kv"> <div class="tile">VL: ${flag(d.winFL,'zu','offen')}</div> <div class="tile">VR: ${flag(d.winFR,'zu','offen')}</div> <div class="tile">HL: ${flag(d.winRL,'zu','offen')}</div> <div class="tile">HR: ${flag(d.winRR,'zu','offen')}</div> </div> </div> <!-- Reifen --> <div class="card"> <div class="row">🛞 <b>Reifen</b></div> <div class="kv"> <div class="tile">VL: ${tireTxt(d.tireFL)}</div> <div class="tile">VR: ${tireTxt(d.tireFR)}</div> <div class="tile">HL: ${tireTxt(d.tireRL)}</div> <div class="tile">HR: ${tireTxt(d.tireRR)}</div> </div> </div> <!-- Klima & Komfort --> <div class="card"> <div class="row">❄️ <b>Klima & Komfort</b></div> <div class="stat"><span>Klima</span><span class="${d.hvacOn?'fanAnim':''}"><b>${d.hvacOn===true?'AN':(d.hvacOn===false?'AUS':'–')}</b></span></div> <div class="stat"><span>Innen</span><span><b>${d.tIn!=null? d.tIn+' °C':'–'}</b></span></div> <div class="stat"><span>Luftreiniger</span><span><b>${d.airClean===true?'AN':(d.airClean===false?'AUS':'–')}</b></span></div> <div class="stat"><span>Defrost</span><span><b>${d.defrost===true?'AN':(d.defrost===false?'AUS':'–')}</b></span></div> <div class="stat"><span>Lenkrad</span><span>${d.steerHeat===true?'<span class="heatAnim">🔥</span>':'AUS'}</span></div> <div class="sub" style="margin-top:6px">Sitze:</div> <div class="kv"> <div class="tile">VL: <b>${seatTxt(d.seatFL)}</b></div> <div class="tile">VR: <b>${seatTxt(d.seatFR)}</b></div> <div class="tile">HL: <b>${seatTxt(d.seatRL)}</b></div> <div class="tile">HR: <b>${seatTxt(d.seatRR)}</b></div> </div> </div> <!-- Fahrzeugstatus --> <div class="card"> <div class="row">ℹ️ <b>Fahrzeugstatus</b></div> <div class="kv"> <div class="tile">Wischerwasser: ${d.washer===true?'<b class="err">LEER</b>':(d.washer===false?'<b class="ok">OK</b>':'–')}</div> <div class="tile">Bremsöl: ${d.breakOil===true?'<b class="err">NIEDRIG</b>':(d.breakOil===false?'<b class="ok">OK</b>':'–')}</div> <div class="tile">Smartkey: ${d.smartKeyBat===true?'<b class="warn">WARNUNG</b>':(d.smartKeyBat===false?'<b class="ok">OK</b>':'–')}</div> </div> </div> <!-- Wallbox --> <div class="card"> <div class="row">🔌 <b>Wallbox</b></div> <div class="kv"> <div class="tile">Status: <b>${wbStateTxt}</b></div> <div class="tile">Leistung: <b>${d.wb_powerFmt || '–'}</b></div> <div class="tile">Energie: <b>${d.wb_energyFmt || '–'} kWh</b></div> <div class="tile">Ampere: <b>${d.wb_ampere!=null? d.wb_ampere+' A':'–'}</b></div> <div class="tile">Freigabe: <b>${d.wb_allow===true?'Ja':(d.wb_allow===false?'Nein':'–')}</b></div> <div class="tile">Temp1: <b>${d.wb_temp1!=null? d.wb_temp1+' °C':'–'}</b></div> <div class="tile">Temp2: <b>${d.wb_temp2!=null? d.wb_temp2+' °C':'–'}</b></div> </div> </div> <!-- Ladehistorie (7 Tage) – reines SVG, volle Breite/Höhe --> <div class="card"> <div class="row">📈 <b>Ladehistorie (7 Tage)</b></div> <div class="chartBox"> ${svgChart} </div> </div> <!-- Ladehistorie Tabelle (letzte 10 Samples) --> <div class="card"> <div class="row">⚡ <b>Ladehistorie – letzte 10 Werte</b></div> ${ (lastSamples && lastSamples.length) ? `<table><tr><th>Zeitpunkt</th><th>SOC</th><th>Wallbox</th></tr>${tableRows}</table>` : `<div class="sub">Keine Daten vorhanden.</div>` } </div> </div> <div class="footer">Zuletzt aktualisiert: ${d.lastUpdate || new Date().toLocaleString()}</div> </div>`; } /* =================== HISTORIE: DPs + Verarbeitung =================== */ function ensureHistoryDPs(){ ensureDataPoint(HIST_SAMPLES_DP, '[]', {name:'Ioniq History Samples', type:'string', role:'json'}); ensureDataPoint(HIST_LAST_TS_DP, 0, {name:'Ioniq History Last Sample', type:'number', role:'value.time'}); } function _normalizeSamples(arr){ const now = Date.now(); const twelveH = 12*3600*1000; return arr.map(s=>{ if(!s) return null; let t = Number(s.t); if (!isFinite(t)) return null; if (t < 1e12) t = t * 1000; // Sekunden -> ms if (t > now + twelveH) return null; // Zukunft verwerfen const out = { t }; if (typeof s.soc === 'number' && isFinite(s.soc)) out.soc = s.soc; if (typeof s.wb === 'number' && isFinite(s.wb )) out.wb = s.wb; return out; }).filter(Boolean); } function loadSamples(){ try{ const txt=gs(HIST_SAMPLES_DP); if(!txt) return []; const parsed = JSON.parse(txt); const arr = Array.isArray(parsed)? parsed : []; return _normalizeSamples(arr); }catch(_){ return []; } } function saveSamples(arr){ try{ if (arr.length>MAX_SAMPLES) arr = arr.slice(arr.length-MAX_SAMPLES); ss(HIST_SAMPLES_DP, JSON.stringify(arr)); }catch(_){} } function trySample(nowMs, data){ const lastTs = Number(gs(HIST_LAST_TS_DP) || 0); if (nowMs - lastTs < SAMPLE_INTERVAL_MS) return; const soc = data._hist_soc; const wb = data._hist_wb_energy; if (soc==null && wb==null) { ss(HIST_LAST_TS_DP, nowMs); if (DEBUG) log('[IONIQ5N] Sample SKIP: keine Werte (soc/wb beide leer)', 'info'); return; } let arr = loadSamples(); arr.push({ t: nowMs, soc: soc, wb: wb }); saveSamples(arr); ss(HIST_LAST_TS_DP, nowMs); if (DEBUG) { const socTxt = (soc==null)?'—':String(soc); const wbTxt = (wb==null)?'—':String(wb); log(`[IONIQ5N] Sample OK @ ${new Date(nowMs).toLocaleString()} | SOC=${socTxt} | WB=${wbTxt}`, 'info'); } } function computeDailyFromSamples(nowMs){ const arr = loadSamples(); // Labels & Tageskeys (letzte 7 Tage inkl. heute) const labels=[], dayKeys=[]; for(let i=6;i>=0;i--){ const d=new Date(nowMs - i*24*3600*1000); labels.push(d.toLocaleDateString(undefined,{weekday:'short'})); dayKeys.push(`${d.getFullYear()}-${('0'+(d.getMonth()+1)).slice(-2)}-${('0'+d.getDate()).slice(-2)}`); } if(!arr.length) return {labels, dailySoc:[0,0,0,0,0,0,0], dailyKwh:[0,0,0,0,0,0,0]}; const byDay={}; for(const s of arr){ const d=new Date(s.t); const key=`${d.getFullYear()}-${('0'+(d.getMonth()+1)).slice(-2)}-${('0'+d.getDate()).slice(-2)}`; if(!byDay[key]) byDay[key]={soc:[], wb:[]}; if(typeof s.soc==='number') byDay[key].soc.push(s.soc); if(typeof s.wb ==='number') byDay[key].wb.push(s.wb); } const dailySoc=[], dailyKwh=[]; for(const key of dayKeys){ const g=byDay[key]; if(!g){ dailySoc.push(0); dailyKwh.push(0); continue; } const avgSoc = g.soc.length ? Math.round(g.soc.reduce((a,b)=>a+b,0)/g.soc.length) : 0; let kwh=0; if(g.wb.length){ const mn=Math.min.apply(null,g.wb), mx=Math.max.apply(null,g.wb); kwh = mx-mn; if(!isFinite(kwh)||kwh<0) kwh=0; kwh = Math.round(kwh*100)/100; } dailySoc.push(avgSoc); dailyKwh.push(kwh); } return {labels, dailySoc, dailyKwh}; } /* =================== DATEN SAMMELN =================== */ function readAll(){ const cand = candidates(); const pick = (name, map=v=>v)=>{ const {val} = firstExisting(cand[name]||[]); if (val===undefined) return undefined; try{ if (typeof val === 'string' && !isNaN(val)) return map(Number(val)); return map(val); }catch(_){ return val; } }; const toBool = (v) => (typeof v==='boolean') ? v : (v!=null ? Number(v)>0 : undefined); // Fahrwerte const odoKm = (function(){ const v = pick('odometer_km', x=>x); if (v===undefined) return undefined; const num = typeof v==='string' ? parseFloat(v.replace(/[^\d.,]/g,'').replace(',','.')) : Number(v); return isNaN(num) ? undefined : Math.round(num); })(); // Wallbox Zahlen const wb_stateNum = pick('wb_state', Number); const wb_powerNum = pick('wb_power', Number); const wb_energyNum= pick('wb_energy', Number); const data = { // Kopf carName: pick('carName'), vin: pick('vin'), // HV / 12V soc: pick('soc_pct', v => Math.round(Number(v))), charging: pick('charge_active', toBool), minToFull: pick('minutes_to_charged', Number), soc12v: pick('soc12v', v => Math.round(Number(v))), state12v: pick('state12v'), soh: pick('soh', v => Math.round(Number(v))), charge12v: pick('charge12v', v => v === true || v === 'true' || Number(v) === 1), // Reichweite & Klima dte: pick('dte', v => Math.round(Number(v))), hvacOn: pick('hvacOn', toBool), tIn: pick('insideTemp', v => Math.round(Number(v)*10)/10), airClean: pick('airClean', toBool), defrost: pick('defrost', toBool), seatFL: pick('seatFL', Number), seatFR: pick('seatFR', Number), seatRL: pick('seatRL', Number), seatRR: pick('seatRR', Number), steerHeat: pick('steerHeat', toBool), // Öffnungen / Fenster doorFL: pick('doorFL', toBool), doorFR: pick('doorFR', toBool), doorRL: pick('doorRL', toBool), doorRR: pick('doorRR', toBool), trunk: pick('trunk', toBool), frunk: pick('frunk', toBool), winFL: pick('winFL', toBool), winFR: pick('winFR', toBool), winRL: pick('winRL', toBool), winRR: pick('winRR', toBool), // Reifenlampen tireFL: pick('tireFL', toBool), tireFR: pick('tireFR', toBool), tireRL: pick('tireRL', toBool), tireRR: pick('tireRR', toBool), // Flüssigkeiten & Warnungen washer: pick('washer', toBool), breakOil: pick('breakOil', toBool), smartKeyBat: pick('smartKeyBat', toBool), // Standort lat: pick('lat', Number), lon: pick('lon', Number), address: pick('position_text', String), positionUrl: pick('position_url', String), // Wallbox – formatierte Strings wb_stateNum, wb_stateText: (wb_stateNum!=null ? (WB_STATE_TXT[wb_stateNum] || String(wb_stateNum)) : undefined), wb_powerFmt: (wb_powerNum!=null ? (wb_powerNum >= 1000 ? (Math.round(wb_powerNum/100)/10)+' kW' : Math.round(wb_powerNum)+' W') : undefined), wb_energyFmt: (wb_energyNum!=null ? wb_energyNum.toFixed(2) : undefined), wb_ampere: pick('wb_amp', v => Math.round(Number(v))), wb_allow: pick('wb_allow', toBool), wb_temp1: pick('wb_temp1', v => Math.round(Number(v))), wb_temp2: pick('wb_temp2', v => Math.round(Number(v))), // Zeit / extra lastUpdate: (function(){ const raw = pick('lastUpdate'); if (!raw) return ''; try{ const d=new Date(raw); return isNaN(d)? String(raw) : d.toLocaleString(); }catch(_){ return String(raw); } })(), kmYesterday: pick('kmYesterday', v=> (v==null? undefined : Math.round(Number(v)))), // Rohwerte für Historie-Sampling _hist_soc: pick('soc_pct', Number), _hist_wb_energy: wb_energyNum, // Odometer odoKm }; return data; } /* =================== MAIN =================== */ ensureState(OUT_HTML_DP); ensureHistoryDPs(); let vinAnnounced = false; function update(){ try{ const nowMs = Date.now(); if (!VEHICLE_ID){ VEHICLE_ID = detectVehicleId(); if (VEHICLE_ID && !vinAnnounced){ log('[IONIQ5N] Auto-Detected VIN: '+VEHICLE_ID,'info'); vinAnnounced=true; } } if (!VEHICLE_ID){ ss(OUT_HTML_DP, renderHTML({__noVin:true}, {labels:[],dailySoc:[],dailyKwh:[]}, [])); return; } const data = readAll(); trySample(nowMs, data); const hist = computeDailyFromSamples(nowMs); const lastSamples = loadSamples(); const html = renderHTML(data, hist, lastSamples); ss(OUT_HTML_DP, html); }catch(e){ log('Update error: '+e, 'error'); } } // Initial + Intervall update(); schedule('*/30 * * * * *', update);