Navigation

    Logo
    • Register
    • Login
    • Search
    • Recent
    • Tags
    • Unread
    • Categories
    • Unreplied
    • Popular
    • GitHub
    • Docu
    • Hilfe
    1. Home
    2. ilovegym

    NEWS

    • Monatsrückblick – September 2025

    • Neues Video "KI im Smart Home" - ioBroker plus n8n

    • Neues Video über Aliase, virtuelle Geräte und Kategorien

    • Profile
    • Following 4
    • Followers 0
    • Topics 2
    • Posts 131
    • Best 40
    • Groups 2

    ilovegym

    @ilovegym

    55
    Reputation
    24
    Profile views
    131
    Posts
    0
    Followers
    4
    Following
    Joined Last Online

    ilovegym Follow
    Pro Starter

    Best posts made by ilovegym

    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @arteck

      Hab mal meinen digitalen Codierknecht 🤖 solange gequaelt, bis das hier rauskam:
      Screenshot 2025-08-16 at 15.03.35.png

      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);
      

      posted in Tester
      ilovegym
      ilovegym
    • RE: Betatest NSPanel-lovelace-ui v0.4.x

      @arteck

      denke da gibts keine Probleme, habe meine 13 Stueck seit ca 8 Monaten 24/7 laufen, allerdings dunkelt der Screensaver ja ab, und zwischen 22 und 7 Uhr schalte ich die Displays Helligkeit 0 - gehen also komplett aus. (Spart auch etwas Strom..)

      Die Idee mit dem Screensaver waere natuerlich immer eine gute Option, man weiss ja nie... 🙂

      posted in Tester
      ilovegym
      ilovegym
    • Usertreffen: Rhein-Main Event: 27.9.25 in Mainz!

      Willkommen beim Stammtisch im Raum Rhein-Main-Hessen 🙂


      Meetings:
      Online: jeden 1. Montag im Monat ab 20:30 - https://discord.gg/yC65zjr5uq

      Vor Ort: am 27.9.2025 ab 16.00 Uhr im Zeitlos in Mainz


      Wer Bock hat kann auch gerne zwischendurch in den Discord-Channel schauen 🙂 Einer ist meist online, und hilft bei Fragen gerne!

      posted in Usergroups
      ilovegym
      ilovegym
    • RE: Smartlock empfehlungen Pro/Contra

      @michael-schmitt

      ich hab 9 Nukis, davon 8 aktuell verbaut, seit dem ersten Nuki bin ich dabei, das neueste ist ein Ultra, das beste, meiner Meinung nach, auch wenn man den Zylinder erstmal zusammenbauen muss, und der nicht ueberall passen koennte. Dafuer macht es einen sehr robusten Eindruck.

      Ansteuern tu ich alle Nuki's mit jeweils einem WT32-ETH0 (esp32 mit lan und geflashter nukihub-software), das laeuft 100%ig und nur per bluetooth ans Nuki gekoppelt, was auch Akku spart.
      Das Ultra hab ich seit Feb. und das laeuft noch mit der ersten Akkuladung, ist an der Haustuer und verriegelt nach 2min sofort wieder.(unsere Katzen machen sonst die Tueren auf, deshalb auch (in zweiter Linie) die Nukis.. 🙂 )

      posted in Hardware
      ilovegym
      ilovegym
    • RE: PV - Begriffklärung

      @metaxa

      wenn du netzbezug hast, hast du keine Einspeisung..

      Hausverbrauch = PV + Netzbezug

      oder

      Hausverbrauch = PV - Einspeisung

      Die modernen Zaehler sind saldierend, es geht immer um den Gesamtwert, was eine einzelne Phase macht, ist wurscht.

      posted in Off Topic
      ilovegym
      ilovegym
    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @blockmove @Winni @RISSN @arteck @Meister-Mopper @MichaelWit @nik82

      Bei HA gabs fuer den Login wohl n Update, kann man sich da was abschauen, @arteck ?

      Screenshot 2025-08-02 at 10.17.08.png

      posted in Tester
      ilovegym
      ilovegym
    • RE: Usertreffen: Rhein-Main Event: 27.9.25 in Mainz!

      @linedancer

      wir vergessen dich nicht.. morgen haben wir wieder Online-Meeting 20.30 im Discord..
      Mal gespannt, was bei

      https://nuudel.digitalcourage.de/MxhkwS3od87PCKu6

      rauskommt.. ich hoffe, du bist dabei, und hoffentlich ein paar mehr 🙂

      Waere schoen, wenn vielleicht @Ahnungsbefreit @amg_666 @chris299 @Homoran auch online oder Vor-Ort dabei waeren..

      posted in Usergroups
      ilovegym
      ilovegym
    • RE: Smartlock empfehlungen Pro/Contra

      @arteck

      hab das mit etwas Rabatt bekommen 🙂

      Und ja, dafuer kann man 2/3x lecker essen gehen, hast schon recht.. 😄
      Ich hab hier halt die Nuki und klar.. wenn ich nochmal komplett neu anfangen wuerde (was ich nicht mache in diesem Leben), dann sowieso alles anders.. erstens mit dem Bagger durch und zweitens alles verkabelt, egal was es kostet..

      Die paar Jahre, die hier noch geplant sind, wirds / muss der Kram halten und dann gehts ins betreute Wohnen, da lass ich dann ne smarte Weiblichkeit kommen, die das Licht aus/an macht 🙂

      posted in Hardware
      ilovegym
      ilovegym
    • RE: Kaufberatung USV

      @stefan341

      Die erste Frage ist immer: Wie hoch ist denn der Verbrauch der beiden Verbraucher? 🙂
      Wenn du das weisst, dann kannst du dir ja ausrechnen, welche Kapazitaet die USV haben muss.

      Also erstmal messen, dann wissen, dann schlau machen und kaufen 🙂

      posted in Off Topic
      ilovegym
      ilovegym
    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @arteck Geniess deinen Urlaub !!

      posted in Tester
      ilovegym
      ilovegym

    Latest posts made by ilovegym

    • RE: Betatest Bright Sky v0.3.x

      @ticaki

      Funktioniert prima, gerade gesehen, dass man Radar extra aktivieren muss in der Config , getan, nach ein paar Minuten waren Daten da.

      Wie stellt man die am schönsten dar? Schon ne Idee ?

      posted in Tester
      ilovegym
      ilovegym
    • RE: Test Adapter Unifi Network

      @meister-mopper

      Danke, nee bin mit meinem Script sehr zufrieden aber werde mir drn hier mal auf dem Testsystem anschauen:-)

      posted in Tester
      ilovegym
      ilovegym
    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @arteck @Meister-Mopper und alle anderen.. 🙂

      Habe hier das oben von mir gerade erfundene Script, das den naechsten HPC und Ladeleistung etc raussucht, ausrechnet und in einem Dashboard darstellt, mal gemacht. Laeuft hier unter javascript 8.9.2 ohne irgendwelche Plugins ohne warnings/errors, auch die States werden quiet angelegt ( outHPCRoot und outEnergyRoot und das Dashboard kann am Anfang vom Script konfiguriert werden. Ebenso die States vom bluelink-Adapter, koennen auch aehnlich sein, je nach Fahrzeugtyp.)
      Zur Abfrage von OpenchargeMap sollte ein kostenloser Api-Key eingetragen sein, link im Script.

      Das Dashboard stelle ich mit MinuVis (Widget HTML auf den State) dar. Geht auch garantiert in allen anderen Vis-Varianten.

      Hier das Script:

      /**************************************************************
      * IONIQ 5 / Bluelink – HPC Nearby + Energy Integration + Dashboard
      * ioBroker JavaScript-Adapter >= 8.9.2 (Node 18+)
      * Version: 1.3.0 (selftest map centering, energy tiles, vis path)
      * (c) by ilovegym66
      **************************************************************/
      'use strict';
      
      const https = require('https');
      
      /*** ===== KONFIG ===== ***/
      const CFG = {
       bluelink: {
         power:    'bluelink.0.KMHKRxxxx.vehicleStatusRaw.ccs2Status.state.Vehicle.Green.Electric.SmartGrid.RealTimePower',
         charging: 'bluelink.0.KMHKRxxxx.vehicleStatus.battery.charge',
         soc:      'bluelink.0.KMHKRxxxx.vehicleStatus.battery.soc',
         lat:      'bluelink.0.KMHKRxxxx.vehicleLocation.lat',
         lon:      'bluelink.0.KMHKRxxxx.vehicleLocation.lon',
         latAlt:   'bluelink.0.KMHKRxxxx.vehicleLocation.latitude',
         lonAlt:   'bluelink.0.KMHKRxxxx.vehicleLocation.longitude'
       },
      
       hpc: {
         thresholdKW: 0,        // Suche auch ohne hohe Ladeleistung erlauben
         hpcMinKW:    149,      // Client-Filter Mindestleistung (kaskadiert runter falls 0 Treffer)
         radiusKm:    20,
         maxResults:  200,
         coordMinMoveM: 30,
         minCheckIntervalSec: 30,
         preferredOperators: ['ionity','enbw','aral pulse','fastned','shell recharge','allego','mer','ewe go','totalenergies','eviny','maingau','entega','pfalzwerke','tesla'],
         ocmEndpoint: 'https://api.openchargemap.io/v3/poi/',
         ocmApiKey:   'DEIN-OCM-KEY-HIER', // <— eintragen oder per State lesen (siehe unten)
         requireChargingForSearch: false,
      
         // Serverseitige Filter (erste Stufe „strict“)
         apiFilterFastDC: true,     // Level 3 + DC
         apiMinPowerKW:   120,
      
         // Kaskade bei 0 Treffern
         cascadeIfZero: true,
         cascadeMinKWClient: 120
       },
      
       energy: {
         powerIsWattAuto:    true,
         sessionPowerMinKW:  0.5,
         integTickSec:       10,
         sessionIdleEndSec:  180,
         usableCapacityKWh:  84
       },
      
       // Dashboard-Ausgabe (zurück in vis-Pfad)
       dash: {
         htmlState: '0_userdata.0.vis.Dashboards.HPC.HTML', // <- hier landet das fertige HTML
         allowScroll: false,    // MinuVis: kein Scroll/Wheel
         heightPx: 420          // Kartenhöhe
       },
      
       // Ausgabe-Roots (States)
       outHpcRoot:    '0_userdata.0.Cars.HPCNearby',
       outEnergyRoot: '0_userdata.0.Cars.Energy',
      
       debug: true
      };
      
      // OPTIONAL: OCM-Key aus State laden (wenn gewünscht)
      // try { const s=getState('0_userdata.0.secrets.ocmApiKey'); if (s && s.val) CFG.hpc.ocmApiKey = String(s.val); } catch(e){}
      
      /*** ===== Utils ===== ***/
      const idJ = (...a)=>a.join('.');
      const logI = m => log(`[Ioniq-HPC+Energy] ${m}`, 'info');
      const logD = m => CFG.debug && log(`[Ioniq-HPC+Energy] ${m}`, 'debug');
      const nowSec = ()=>Math.floor(Date.now()/1000);
      
      function exObj(id){ try{ return existsObject(id); }catch(e){ return false; } }
      function exState(id){ try{ return existsState(id); }catch(e){ return false; } }
      function g(id){
       try{
         if (!exState(id)) return undefined;
         const s = getState(id);
         return s ? s.val : undefined;
       }catch(e){ return undefined; }
      }
      async function es(id, common, init){ try{ if (!exObj(id)) await createStateAsync(id, common, init ?? null); }catch(e){} }
      async function ss(id, val){ try{ await setStateAsync(id, {val, ack:true}); }catch(e){} }
      function toNum(x, d=0){ const n = Number(x); return Number.isFinite(n) ? n : d; }
      function toBool(x){ return x===true || x===1 || x==='1' || String(x).toLowerCase()==='true'; }
      
      /*** ===== Haversine (m) ===== ***/
      function haversineMeters(lat1, lon1, lat2, lon2){
       const R=6371000, rad=d=>d*Math.PI/180;
       const dLat=rad(lat2-lat1), dLon=rad(lon2-lon1);
       const a=Math.sin(dLat/2)**2+Math.cos(rad(lat1))*Math.cos(rad(lat2))*Math.sin(dLon/2)**2;
       return 2*R*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
      }
      
      /*** ====== OUTPUT STATES: HPC ====== ***/
      const HPC = {
       ROOT:     CFG.outHpcRoot,
       HAS:      idJ(CFG.outHpcRoot,'hasHPCNearby'),
       COUNT:    idJ(CFG.outHpcRoot,'count'),
       NAME:     idJ(CFG.outHpcRoot,'nearest.name'),
       DISTM:    idJ(CFG.outHpcRoot,'nearest.distance_m'),
       KW:       idJ(CFG.outHpcRoot,'nearest.maxPower_kW'),
       OP:       idJ(CFG.outHpcRoot,'nearest.operator'),
       LASTJSON: idJ(CFG.outHpcRoot,'lastResultJson'),
       LASTCHK:  idJ(CFG.outHpcRoot,'lastCheck'),
       LASTWHY:  idJ(CFG.outHpcRoot,'lastReason'),
       LASTERR:  idJ(CFG.outHpcRoot,'lastError'),
       MISSING:  idJ(CFG.outHpcRoot,'debug.missingStates'),
      
       // Debug
       DBG_URL:   idJ(CFG.outHpcRoot,'debug.lastQueryUrl'),
       DBG_URL2:  idJ(CFG.outHpcRoot,'debug.lastQueryUrl_swapped'),
       DBG_URL3:  idJ(CFG.outHpcRoot,'debug.lastQueryUrl_wide'),
       DBG_RAW:   idJ(CFG.outHpcRoot,'debug.rawCount'),
       DBG_FIL:   idJ(CFG.outHpcRoot,'debug.filteredCount'),
       DBG_SAMPLE:idJ(CFG.outHpcRoot,'debug.sampleJson'),
       DBG_COORD_LAT: idJ(CFG.outHpcRoot,'debug.lastLat'),
       DBG_COORD_LON: idJ(CFG.outHpcRoot,'debug.lastLon'),
       DBG_HTTP:      idJ(CFG.outHpcRoot,'debug.lastHttpStatus'),
       DBG_ERRSHORT:  idJ(CFG.outHpcRoot,'debug.lastErrorShort'),
      
       // Commands
       CMD_TEST:      idJ(CFG.outHpcRoot,'cmd.TestSearch'),
       TEST_RADIUS:   idJ(CFG.outHpcRoot,'cmd.TestRadiusKm'),
       CMD_TEST_FFM:  idJ(CFG.outHpcRoot,'cmd.SelfTest_Frankfurt'),
       CMD_TEST_CGN:  idJ(CFG.outHpcRoot,'cmd.SelfTest_Koeln'),
       CMD_TEST_MUC:  idJ(CFG.outHpcRoot,'cmd.SelfTest_Muenchen')
      };
      
      async function ensureHpcStates(){
       await es(HPC.HAS,{type:'boolean',role:'indicator'},false);
       await es(HPC.COUNT,{type:'number',role:'value'},0);
       await es(HPC.NAME,{type:'string',role:'text'},'');
       await es(HPC.DISTM,{type:'number',role:'value'},0);
       await es(HPC.KW,{type:'number',role:'value'},0);
       await es(HPC.OP,{type:'string',role:'text'},'');
       await es(HPC.LASTJSON,{type:'string',role:'json'},'[]');
       await es(HPC.LASTCHK,{type:'string',role:'text'},'');
       await es(HPC.LASTWHY,{type:'string',role:'text'},'');
       await es(HPC.LASTERR,{type:'string',role:'text'},'');
       await es(HPC.MISSING,{type:'string',role:'text'},'');
      
       await es(HPC.DBG_URL,{type:'string',role:'text'},'');
       await es(HPC.DBG_URL2,{type:'string',role:'text'},'');
       await es(HPC.DBG_URL3,{type:'string',role:'text'},'');
       await es(HPC.DBG_RAW,{type:'number',role:'value'},0);
       await es(HPC.DBG_FIL,{type:'number',role:'value'},0);
       await es(HPC.DBG_SAMPLE,{type:'string',role:'json'},'');
       await es(HPC.DBG_COORD_LAT,{type:'number',role:'value.gps'},0);
       await es(HPC.DBG_COORD_LON,{type:'number',role:'value.gps'},0);
       await es(HPC.DBG_HTTP,{type:'string',role:'text'},'');
       await es(HPC.DBG_ERRSHORT,{type:'string',role:'text'},'');
      
       await es(HPC.CMD_TEST,{type:'boolean',role:'button'},false);
       await es(HPC.TEST_RADIUS,{type:'number',role:'value'},NaN);
       await es(HPC.CMD_TEST_FFM,{type:'boolean',role:'button'},false);
       await es(HPC.CMD_TEST_CGN,{type:'boolean',role:'button'},false);
       await es(HPC.CMD_TEST_MUC,{type:'boolean',role:'button'},false);
      }
      
      /*** ====== OUTPUT STATES: ENERGY ====== ***/
      const EN = {
       ROOT:          CFG.outEnergyRoot,
       ACTIVE:        idJ(CFG.outEnergyRoot,'session.active'),
       START_TS:      idJ(CFG.outEnergyRoot,'session.startTs'),
       END_TS:        idJ(CFG.outEnergyRoot,'session.endTs'),
       START_SOC:     idJ(CFG.outEnergyRoot,'session.startSoC'),
       END_SOC:       idJ(CFG.outEnergyRoot,'session.endSoC'),
       ENERGY_KWH:    idJ(CFG.outEnergyRoot,'session.energy_kWh'),
       ENERGY_SOC_KWH:idJ(CFG.outEnergyRoot,'session.energySoc_kWh'),
       LAST_ENERGY:   idJ(CFG.outEnergyRoot,'lastSession.energy_kWh'),
       LAST_START:    idJ(CFG.outEnergyRoot,'lastSession.startTs'),
       LAST_END:      idJ(CFG.outEnergyRoot,'lastSession.endTs'),
       TODAY_KWH:     idJ(CFG.outEnergyRoot,'today.energy_kWh'),
       TOTAL_KWH:     idJ(CFG.outEnergyRoot,'total.energy_kWh'),
       LAST_REASON:   idJ(CFG.outEnergyRoot,'debug.lastReason'),
       LAST_ERR:      idJ(CFG.outEnergyRoot,'debug.lastError')
      };
      async function ensureEnergyStates(){
       await es(EN.ACTIVE,        {type:'boolean', role:'indicator'}, false);
       await es(EN.START_TS,      {type:'string',  role:'text'}, '');
       await es(EN.END_TS,        {type:'string',  role:'text'}, '');
       await es(EN.START_SOC,     {type:'number',  role:'value'}, null);
       await es(EN.END_SOC,       {type:'number',  role:'value'}, null);
       await es(EN.ENERGY_KWH,    {type:'number',  role:'value.energy'}, 0);
       await es(EN.ENERGY_SOC_KWH,{type:'number',  role:'value.energy'}, 0);
       await es(EN.LAST_ENERGY,   {type:'number',  role:'value.energy'}, 0);
       await es(EN.LAST_START,    {type:'string',  role:'text'}, '');
       await es(EN.LAST_END,      {type:'string',  role:'text'}, '');
       await es(EN.TODAY_KWH,     {type:'number',  role:'value.energy'}, 0);
       await es(EN.TOTAL_KWH,     {type:'number',  role:'value.energy'}, 0);
       await es(EN.LAST_REASON,   {type:'string',  role:'text'}, '');
       await es(EN.LAST_ERR,      {type:'string',  role:'text'}, '');
      }
      
      /*** ===== OCM Helper ===== ***/
      function isPreferredOperator(op){
       if (!op) return false;
       const t = String(op).toLowerCase();
       return CFG.hpc.preferredOperators.some(x => t.includes(String(x).toLowerCase()));
      }
      function powerFromConn(c){
       if (!c) return 0;
       let pk = toNum(c.PowerKW, NaN);
       if (!Number.isFinite(pk)) pk = toNum(c.RatedPowerKW, NaN);
       if (!Number.isFinite(pk)) pk = toNum(c.Power, NaN);
       if (Number.isFinite(pk) && pk > 0) return pk;
      
       let v = toNum(c.Voltage, NaN); if (!Number.isFinite(v)) v = toNum(c.Volts, NaN);
       let a = toNum(c.Amps, NaN);    if (!Number.isFinite(a)) a = toNum(c.Current, NaN);
       if (Number.isFinite(v) && Number.isFinite(a) && v>0 && a>0) return (v*a)/1000;
      
       const levelId  = toNum(c.LevelID, NaN) || toNum(c?.Level?.ID, NaN) || 0;
       const currentId= toNum(c.CurrentTypeID, NaN) || toNum(c?.CurrentType?.ID, NaN) || 0;
       const lvlTitle = String(c?.Level?.Title||'').toLowerCase();
       const curTitle = String(c?.CurrentType?.Title||'').toLowerCase();
       const lvlFast  = levelId >= 3 || lvlTitle.includes('3');
       const isDC     = currentId === 30 || curTitle.includes('dc');
       if (lvlFast && isDC) return 150; // Fallback
       return 0;
      }
      function extractMaxPowerKW(poi){
       const arr = Array.isArray(poi?.Connections) ? poi.Connections : [];
       let max = 0;
       for (const c of arr){ const p = powerFromConn(c); if (p > max) max = p; }
       return max;
      }
      
      /*** HTTP ***/
      function buildOcmUrlStr(lat, lon, radiusKm, maxResults, opts){
       opts = opts || {}; // { fastDC?:boolean, minPowerKW?:number }
       function add(q, k, v){ if (v===undefined||v===null||v==='') return q; q.push(encodeURIComponent(k)+'='+encodeURIComponent(String(v))); return q; }
       const base = String(CFG.hpc.ocmEndpoint||'https://api.openchargemap.io/v3/poi/').replace(/\?+.*/, '');
       const params = [];
       add(params,'output','json');
       add(params,'latitude', lat);
       add(params,'longitude', lon);
       add(params,'distance', radiusKm);
       add(params,'distanceunit','KM');
       add(params,'maxresults', maxResults || CFG.hpc.maxResults);
       add(params,'compact','false'); add(params,'verbose','true');
       if (opts.fastDC){ add(params,'levelid',3); add(params,'currenttypeid',30); if (opts.minPowerKW!=null) add(params,'minpowerkw', opts.minPowerKW); }
       if (CFG.hpc.ocmApiKey && CFG.hpc.ocmApiKey.trim()){ add(params,'key', CFG.hpc.ocmApiKey.trim()); }
       return base + '?' + params.join('&');
      }
      function httpGetJson(urlStr){
       return new Promise((resolve, reject) => {
         const headers = { 'User-Agent':'ioBroker-HPC-Nearby/1.3 (+contact:local)' };
         // keinen X-API-Key Header verwenden – Key steckt in URL
         const req = https.get(urlStr, { headers, timeout: 12000 }, (res) => {
           let data=''; res.on('data', c=>data+=c);
           res.on('end', ()=>{ if (res.statusCode>=200 && res.statusCode<300) { try{ resolve({json:JSON.parse(data), status:res.statusCode}); } catch(e){ reject(new Error('OCM JSON parse error: '+e.message)); } } else reject(new Error('OCM HTTP '+res.statusCode)); });
         });
         req.on('timeout', ()=>req.destroy(new Error('timeout')));
         req.on('error', reject);
       });
      }
      async function fetchOCM(lat, lon, radiusKm, maxResults, mode){
       let url;
       if (mode==='strict') url = buildOcmUrlStr(lat,lon,radiusKm,maxResults,{fastDC:true,minPowerKW:CFG.hpc.apiMinPowerKW});
       else if (mode==='dcOnly') url = buildOcmUrlStr(lat,lon,radiusKm,maxResults,{fastDC:true});
       else url = buildOcmUrlStr(lat,lon,radiusKm,maxResults,{});
       const r = await httpGetJson(url);
       return { mode, url, status:r.status, json:r.json };
      }
      
      /*** ===== HPC intern ===== ***/
      let lastCheckSec = 0, lastLat = null, lastLon = null;
      function readLatLon(){
       let lat = g(CFG.bluelink.lat), lon = g(CFG.bluelink.lon);
       if ((lat===undefined||lat===null) && exState(CFG.bluelink.latAlt)) lat = g(CFG.bluelink.latAlt);
       if ((lon===undefined||lon===null) && exState(CFG.bluelink.lonAlt)) lon = g(CFG.bluelink.lonAlt);
       return {lat: toNum(lat, 0), lon: toNum(lon, 0)};
      }
      function listMissing(ids){ return ids.filter(id => !exState(id)); }
      
      async function hpcNo(reason){
       await ss(HPC.HAS,false); await ss(HPC.COUNT,0);
       await ss(HPC.NAME,''); await ss(HPC.DISTM,0); await ss(HPC.KW,0); await ss(HPC.OP,'');
       await ss(HPC.LASTJSON,'[]'); await ss(HPC.LASTCHK,new Date().toISOString());
       await ss(HPC.LASTERR,''); await ss(HPC.LASTWHY,reason||'');
       await ss(HPC.DBG_RAW,0); await ss(HPC.DBG_FIL,0);
       await ss(HPC.DBG_URL,''); await ss(HPC.DBG_URL2,''); await ss(HPC.DBG_URL3,''); await ss(HPC.DBG_HTTP,''); await ss(HPC.DBG_ERRSHORT,'');
      }
      
      function enrichAndFilter(json, lat, lon){
       const enriched=[];
       for (const poi of Array.isArray(json)?json:[]){
         const pMax = extractMaxPowerKW(poi);
         const conns = Array.isArray(poi?.Connections)?poi.Connections:[];
         const isDCfast = conns.some(c=>{
           const lvl = toNum(c?.LevelID, NaN) || toNum(c?.Level?.ID, NaN) || 0;
           const cur = toNum(c?.CurrentTypeID, NaN) || toNum(c?.CurrentType?.ID, NaN) || 0;
           const lvlTitle = String(c?.Level?.Title||'').toLowerCase();
           const curTitle = String(c?.CurrentType?.Title||'').toLowerCase();
           const lvlFast = lvl >= 3 || lvlTitle.includes('3'); const isDC = cur === 30 || curTitle.includes('dc');
           return lvlFast && isDC;
         });
         if (!(pMax >= CFG.hpc.hpcMinKW || (pMax === 0 && isDCfast))) continue;
      
         const op   = poi?.OperatorInfo?.Title || '';
         const name = poi?.AddressInfo?.Title || '';
         const plat = toNum(poi?.AddressInfo?.Latitude, NaN);
         const plon = toNum(poi?.AddressInfo?.Longitude, NaN);
         const distM = (Number.isFinite(plat)&&Number.isFinite(plon))?Math.round(haversineMeters(lat,lon,plat,plon)):null;
         enriched.push({
           id: poi?.ID, operator: op, preferred: isPreferredOperator(op), name,
           maxPower_kW: Math.round(pMax), distance_m: distM,
           lat: plat, lon: plon
         });
       }
       enriched.sort((a,b)=>{
         if (a.preferred!==b.preferred) return a.preferred?-1:1;
         const da=a.distance_m??9e9, db=b.distance_m??9e9;
         if (da!==db) return da-db;
         return (b.maxPower_kW||0)-(a.maxPower_kW||0);
       });
       return enriched;
      }
      
      async function maybeCheckHPC(reason, opts){
       opts = opts||{};
       await ss(HPC.LASTWHY, reason||'');
       await ss(HPC.DBG_ERRSHORT, '');
       try{
         const reqIds=[CFG.bluelink.power, CFG.bluelink.charging];
         const missing=listMissing(reqIds); if(missing.length){ await ss(HPC.MISSING,missing.join(', ')); return; } else await ss(HPC.MISSING,'');
      
         let powerKW = toNum(g(CFG.bluelink.power), 0);
         if (CFG.energy.powerIsWattAuto && Math.abs(powerKW)>1000) powerKW/=1000; powerKW=Math.abs(powerKW);
         const charging = toBool(g(CFG.bluelink.charging));
         const {lat,lon}=readLatLon();
         await ss(HPC.DBG_COORD_LAT, lat); await ss(HPC.DBG_COORD_LON, lon);
      
         if (!opts.force){
           if (CFG.hpc.requireChargingForSearch && !charging) return hpcNo('charging=false');
           if (!Number.isFinite(powerKW) || powerKW <= CFG.hpc.thresholdKW) return hpcNo(`power ${powerKW.toFixed(1)} <= ${CFG.hpc.thresholdKW}`);
         }
         if (!Number.isFinite(lat)||!Number.isFinite(lon)||lat===0||lon===0) return hpcNo('invalid coords');
      
         const tNow=nowSec(), since=tNow-lastCheckSec;
         if (!opts.force && lastLat!=null && lastLon!=null){
           const dist=haversineMeters(lastLat,lastLon,lat,lon);
           if (dist<CFG.hpc.coordMinMoveM && since<CFG.hpc.minCheckIntervalSec){ logD(`HPC skip: moved ${Math.round(dist)}m, since ${since}s`); return; }
         }
      
         // Stufe 1: strict
         let q = await fetchOCM(lat,lon,(opts.radiusKmOverride&&isFinite(opts.radiusKmOverride))?Number(opts.radiusKmOverride):CFG.hpc.radiusKm, CFG.hpc.maxResults, 'strict');
         lastCheckSec=tNow; lastLat=lat; lastLon=lon;
         await ss(HPC.DBG_URL, q.url); await ss(HPC.DBG_HTTP, `strict:${q.status}`);
         await ss(HPC.DBG_RAW, Array.isArray(q.json)?q.json.length:0);
      
         let enriched = enrichAndFilter(q.json, lat, lon);
         await ss(HPC.DBG_FIL, enriched.length);
      
         // Stufe 2: dcOnly
         if (CFG.hpc.cascadeIfZero && enriched.length===0){
           q = await fetchOCM(lat,lon,CFG.hpc.radiusKm,CFG.hpc.maxResults,'dcOnly');
           await ss(HPC.DBG_URL2, q.url); await ss(HPC.DBG_HTTP, `dcOnly:${q.status}`);
           await ss(HPC.DBG_RAW, Array.isArray(q.json)?q.json.length:0);
           enriched = enrichAndFilter(q.json, lat, lon);
           await ss(HPC.DBG_FIL, enriched.length);
         }
         // Stufe 3: all (client ≥ 120 kW)
         if (CFG.hpc.cascadeIfZero && enriched.length===0){
           const old=CFG.hpc.hpcMinKW; CFG.hpc.hpcMinKW=CFG.hpc.cascadeMinKWClient||120;
           q = await fetchOCM(lat,lon,CFG.hpc.radiusKm,Math.max(CFG.hpc.maxResults,120),'all');
           await ss(HPC.DBG_URL3, q.url); await ss(HPC.DBG_HTTP, `all:${q.status}`);
           await ss(HPC.DBG_RAW, Array.isArray(q.json)?q.json.length:0);
           enriched = enrichAndFilter(q.json, lat, lon);
           await ss(HPC.DBG_FIL, enriched.length);
           CFG.hpc.hpcMinKW=old;
         }
      
         await ss(HPC.COUNT, enriched.length);
         await ss(HPC.HAS, enriched.length>0);
         await ss(HPC.LASTJSON, JSON.stringify(enriched));
         await ss(HPC.LASTCHK, new Date().toISOString());
         await ss(HPC.LASTERR, '');
      
         if (enriched.length){
           const n=enriched[0];
           await ss(HPC.NAME, n.name||'');
           await ss(HPC.DISTM, n.distance_m||0);
           await ss(HPC.KW, n.maxPower_kW||0);
           await ss(HPC.OP, n.operator||'');
           logI(`HPC nearby: ${n.name} (${n.operator||'–'}), ${n.maxPower_kW} kW, ~${n.distance_m} m`);
         } else {
           await ss(HPC.NAME,''); await ss(HPC.DISTM,0); await ss(HPC.KW,0); await ss(HPC.OP,'');
           logD('HPC: none in radius');
         }
      
         // Dashboard neu rendern
         renderDashboard();
      
       } catch(err){
         await ss(HPC.LASTERR, String(err?.message||err));
         await ss(HPC.DBG_ERRSHORT, String(err?.message||err));
         await ss(HPC.LASTCHK, new Date().toISOString());
         renderDashboard();
       }
      }
      
      /*** ===== ENERGY intern ===== ***/
      let lastTickTs = Date.now(); let idleSince = null;
      function readPowerKW(){ let p=toNum(g(CFG.bluelink.power),0); if (CFG.energy.powerIsWattAuto && Math.abs(p)>1000) p/=1000; return Math.abs(p); }
      function readSocPct(){ const s=g(CFG.bluelink.soc); return s===undefined?null:toNum(s,null); }
      function readChargingActive(){ return toBool(g(CFG.bluelink.charging)); }
      
      async function sessionStart(){
       await ss(EN.ACTIVE,true); await ss(EN.START_TS,new Date().toISOString());
       await ss(EN.END_TS,''); await ss(EN.START_SOC, readSocPct());
       await ss(EN.END_SOC,null); await ss(EN.ENERGY_KWH,0); await ss(EN.ENERGY_SOC_KWH,0);
       renderDashboard();
      }
      async function sessionFinish(reason){
       const nowIso=new Date().toISOString(); const energy=toNum(g(EN.ENERGY_KWH),0);
       await ss(EN.LAST_ENERGY,energy); await ss(EN.LAST_START, g(EN.START_TS)||''); await ss(EN.LAST_END,nowIso);
       await ss(EN.END_TS,nowIso); await ss(EN.ACTIVE,false); await ss(EN.LAST_REASON,`finish:${reason||''}`);
      
       const today=new Date().toISOString().slice(0,10); const keyToday=idJ(EN.ROOT,'daily',today);
       if (!exObj(keyToday)) await es(keyToday,{type:'number',role:'value.energy'},0);
       const curDay=toNum(g(keyToday),0)+energy;
       await ss(keyToday,curDay); await ss(EN.TODAY_KWH,curDay);
       await ss(EN.TOTAL_KWH, toNum(g(EN.TOTAL_KWH),0)+energy);
       renderDashboard();
      }
      async function updateSocEstimate(){
       if (CFG.energy.usableCapacityKWh<=0) return;
       const s0=g(EN.START_SOC), s1=readSocPct(); if (s0==null || s1==null) return;
       const est=((toNum(s1,0)-toNum(s0,0))/100)*CFG.energy.usableCapacityKWh;
       await ss(EN.END_SOC,s1); await ss(EN.ENERGY_SOC_KWH, Math.max(0,est));
      }
      async function integrationTick(){
       try{
         const need=[CFG.bluelink.power, CFG.bluelink.charging]; if (listMissing(need).length) return;
         const active=toBool(g(EN.ACTIVE)), charging=readChargingActive(), powerKW=readPowerKW();
         const now=Date.now(); const dt_h=Math.max(0,(now-lastTickTs)/3600000); lastTickTs=now;
      
         if (!active && charging && powerKW>=CFG.energy.sessionPowerMinKW){ idleSince=null; await sessionStart(); }
         if (active){
           const eAdd=powerKW*dt_h; const eNow=Math.max(0, toNum(g(EN.ENERGY_KWH),0)+eAdd);
           await ss(EN.ENERGY_KWH, eNow); await updateSocEstimate();
           if (powerKW < CFG.energy.sessionPowerMinKW || !charging){
             if (idleSince===null) idleSince=now; const idleSec=(now-idleSince)/1000;
             if (idleSec>=CFG.energy.sessionIdleEndSec){ await sessionFinish(!charging?'charging=false':'power<min'); idleSince=null; }
           } else idleSince=null;
         }
         const todayKey=idJ(EN.ROOT,'daily', new Date().toISOString().slice(0,10));
         if (exObj(todayKey)) await ss(EN.TODAY_KWH, toNum(g(todayKey),0));
       } catch(err){ await ss(EN.LAST_ERR, String(err?.message||err)); }
       renderDashboard();
      }
      
      /*** ===== Dashboard ===== ***/
      async function ensureDashState(){ await es(CFG.dash.htmlState, {type:'string', role:'html'}, ''); }
      
      // Mini Map-Engine (OSM Tiles im Iframe; kein Scroll/Wheel)
      function buildMapIframeHTML(points, centerHint){
       // points: [{lat,lon,label,type:'car'|'hpc'}]
       // centerHint: {lat,lon} optional
       const W=100, H=CFG.dash.heightPx; // Breite 100% via CSS
       const payload = { points: points||[], centerHint: centerHint||null, lockScroll: !CFG.dash.allowScroll };
       const css =
         'html,body{margin:0;height:100%;background:#0b1020}#root{position:relative;width:100%;height:100%}'+
         '.tile{position:absolute}canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}'+
         '.mk{position:absolute;transform:translate(-50%,-100%);padding:3px 6px;border-radius:8px;border:1px solid rgba(148,163,184,.5);background:rgba(13,23,45,.9);color:#e5e7eb;font:12px system-ui,sans-serif;white-space:nowrap}'+
         '.dot{position:absolute;width:10px;height:10px;border-radius:50%;box-shadow:0 0 0 2px rgba(96,165,250,.35)}'+
         '.dot.car{background:#34d399} .dot.hpc{background:#60a5fa} .attr{position:absolute;right:6px;bottom:4px;font:11px system-ui;color:#93a3b8;background:rgba(0,0,0,.35);padding:2px 6px;border-radius:6px}';
       const js =
         '(function(){var DATA=window.__PAYLOAD__||{points:[],centerHint:null,lockScroll:true};'+
         'var root=document.getElementById("root");var W=root.clientWidth,H=root.clientHeight;'+
         'function rad(d){return d*Math.PI/180;} function lat2y(lat){var s=Math.sin(rad(lat));return 0.5-Math.log((1+s)/(1-s))/(4*Math.PI);} function lon2x(lon){return lon/360+0.5;}'+
         'function project(lat,lon,z){var s=256*Math.pow(2,z);return {x:lon2x(lon)*s,y:lat2y(lat)*s};} function deproject(x,y,z){var s=256*Math.pow(2,z);var lon=(x/s-0.5)*360;var n=Math.PI-2*Math.PI*(y/s-0.5);var lat=180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n)));return {lat:lat,lon:lon};}'+
         'function fitBounds(pts){ if(!pts||!pts.length){ return {c:(DATA.centerHint||{lat:51,lon:10}), z:8}; } var minLat=90,maxLat=-90,minLon=180,maxLon=-180; pts.forEach(function(p){if(p.lat<minLat)minLat=p.lat;if(p.lat>maxLat)maxLat=p.lat;if(p.lon<minLon)minLon=p.lon;if(p.lon>maxLon)maxLon=p.lon;}); var c={lat:(minLat+maxLat)/2,lon:(minLon+maxLon)/2}; var z=6; for(var zz=6;zz<=19;zz++){var a=project(maxLat,minLon,zz), b=project(minLat,maxLon,zz); var w=Math.abs(b.x-a.x), h=Math.abs(b.y-a.y); if(w<=W*0.85 && h<=H*0.85) z=zz; else break;} return {c:c,z:z}; }'+
         'var pts=DATA.points||[]; var f=fitBounds(pts.length?pts:(DATA.centerHint?[DATA.centerHint]:[])); var center=f.c, zoom=f.z;'+
         'var world=project(center.lat,center.lon,zoom); var originX=world.x-W/2, originY=world.y-H/2;'+
         'var tiles=document.createElement("div"); tiles.style.position="absolute"; root.appendChild(tiles); var ctx=document.createElement("canvas"); ctx.width=W; ctx.height=H; root.appendChild(ctx); var g=ctx.getContext("2d");'+
         'function wrapX(x,n){return ((x%n)+n)%n;} function toXY(lat,lon){var p=project(lat,lon,zoom); return {x:p.x-originX,y:p.y-originY};}'+
         'function draw(){ tiles.innerHTML=""; var n=Math.pow(2,zoom); var x0=Math.floor(originX/256), y0=Math.floor(originY/256); var x1=Math.floor((originX+W)/256), y1=Math.floor((originY+H)/256); for(var ty=y0;ty<=y1;ty++){ if(ty<0||ty>=n) continue; for(var tx=x0;tx<=x1;tx++){ var wx=wrapX(tx,n); var img=new Image(); img.className="tile"; img.src="https://tile.openstreetmap.org/"+zoom+"/"+wx+"/"+ty+".png"; img.width=256; img.height=256; img.style.left=(tx*256-originX)+"px"; img.style.top=(ty*256-originY)+"px"; tiles.appendChild(img);} } g.clearRect(0,0,W,H);'+
         '  (DATA.points||[]).forEach(function(p){ var q=toXY(p.lat,p.lon); var d=document.createElement("div"); d.className="dot "+(p.type||"hpc"); d.style.left=q.x+"px"; d.style.top=q.y+"px"; root.appendChild(d); var m=document.createElement("div"); m.className="mk"; m.style.left=q.x+"px"; m.style.top=(q.y-12)+"px"; m.textContent=p.label||""; root.appendChild(m); });'+
         '  var attr=document.createElement("div"); attr.className="attr"; attr.textContent="© OpenStreetMap-Mitwirkende"; root.appendChild(attr);'+
         '} draw();'+
         (CFG.dash.allowScroll ? '' : ' root.addEventListener("wheel", function(ev){ev.preventDefault();}, {passive:false}); ') +
         '})();';
       const srcdoc = '<!doctype html><html><head><meta charset="utf-8"><style>'+css+'</style></head><body><div id="root" style="width:100%;height:100%"></div><script>window.__PAYLOAD__='+JSON.stringify(payload)+'<\/script><script>'+js+'<\/script></body></html>';
       return '<iframe style="width:100%;height:'+H+'px;border:0;border-radius:12px;overflow:hidden;background:#0b1020" srcdoc="'+
              String(srcdoc).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')+'"></iframe>';
      }
      
      function fmtEnergy(n){ return (n==null||isNaN(n)) ? '0.0' : Number(n).toFixed(2); }
      function fmtDist(m){ if (m==null||isNaN(m)) return '-'; return (m>=1000)?(m/1000).toFixed(2)+' km': Math.round(m)+' m'; }
      
      // Render: liest aktuelle States + erzeugt HTML in CFG.dash.htmlState
      async function renderDashboard(centerOverride){
       try{
         await ensureDashState();
      
         const car = readLatLon();
         const nearestName = String(g(HPC.NAME)||'–');
         const nearestOp   = String(g(HPC.OP)||'');
         const nearestKW   = toNum(g(HPC.KW),0);
         const nearestDist = toNum(g(HPC.DISTM),0);
         const count       = toNum(g(HPC.COUNT),0);
      
         const sessActive  = toBool(g(EN.ACTIVE));
         const eSess       = fmtEnergy(toNum(g(EN.ENERGY_KWH),0));
         const eSoc        = fmtEnergy(toNum(g(EN.ENERGY_SOC_KWH),0));
         const eToday      = fmtEnergy(toNum(g(EN.TODAY_KWH),0));
         const eTotal      = fmtEnergy(toNum(g(EN.TOTAL_KWH),0));
      
         // Punkte für Map: Auto + bis zu 12 HPC
         let list=[]; try{ list = JSON.parse(String(g(HPC.LASTJSON)||'[]')); }catch(e){ list=[]; }
         const top = (Array.isArray(list)?list:[]).slice(0,12);
         const points = [];
         if (Number.isFinite(car.lat) && Number.isFinite(car.lon) && car.lat && car.lon){
           points.push({lat:car.lat, lon:car.lon, label:'Car', type:'car'});
         }
         for (const x of top){
           if (!Number.isFinite(x.lat)||!Number.isFinite(x.lon)) continue;
           const lbl = (x.name||'HPC') + ' · ' + (x.maxPower_kW||'?') + ' kW';
           points.push({lat:x.lat, lon:x.lon, label:lbl, type:'hpc'});
         }
      
         // SelfTest-Fix: falls centerOverride gesetzt -> nehmen. Sonst fitBounds macht’s automatisch auf Car+HPC
         const map = buildMapIframeHTML(points, centerOverride || null);
      
         const css =
           '.wrap{color:#e5e7eb;font:14px system-ui,-apple-system,Segoe UI,Roboto;line-height:1.4;background:#0b1020;padding:10px;border-radius:14px;border:1px solid #334155}'+
           '.row{display:flex;flex-wrap:wrap;gap:10px;margin:0 0 10px}'+
           '.chip{background:#111827;border:1px solid #374151;border-radius:999px;padding:6px 10px;font-weight:700}'+
           '.muted{opacity:.8;font-weight:600} .title{font-weight:800;font-size:14px}'+
           '.list{margin-top:10px;border-top:1px solid #334155;padding-top:6px} .item{padding:6px 0;border-bottom:1px dashed #334155}'+
           '.item:last-child{border-bottom:0} .badge{font-weight:700} .ok{color:#34d399} .warn{color:#f59e0b}';
      
         const listHtml = top.map(o=>{
           const pref = o.preferred ? ' • ★' : '';
           return `<div class="item">• <span class="title">${o.name||'-'}</span> <span class="muted">(${o.operator||'–'}${pref})</span><br/>
             <span class="muted">${o.maxPower_kW||'?'} kW · ${fmtDist(o.distance_m)}</span></div>`;
         }).join('');
      
         const headChips =
           `<div class="row">
             <span class="chip">Nearest: <span class="badge">${nearestName}</span> <span class="muted">(${nearestOp||'–'})</span></span>
             <span class="chip">Power: ${nearestKW||0} kW</span>
             <span class="chip">Distance: ${fmtDist(nearestDist)}</span>
             <span class="chip">Spots: ${count}</span>
             <span class="chip">Session: ${eSess} kWh</span>
             <span class="chip">Today: ${eToday} kWh</span>
             <span class="chip">Total: ${eTotal} kWh</span>
             <span class="chip">Car Pos: ${Number(car.lat||0).toFixed(5)}, ${Number(car.lon||0).toFixed(5)}</span>
             <span class="chip">Active: <span class="${sessActive?'ok':'warn'}">${sessActive?'yes':'no'}</span></span>
           </div>`;
      
         const html = `<div class="wrap">${headChips}${map}<div class="list">${listHtml||'<div class="muted">Keine Stationen.</div>'}</div></div>`;
         await ss(CFG.dash.htmlState, `<style>${css}</style>` + html);
       } catch(e){ /* noop */ }
      }
      
      /*** ===== Subscriptions / Scheduler ===== ***/
      function attachTriggers(){
       // HPC triggers
       on({id: CFG.bluelink.power,    change:'ne'}, ()=> maybeCheckHPC('power_changed'));
       on({id: CFG.bluelink.charging, change:'ne'}, ()=> maybeCheckHPC('charging_changed'));
       on({id: CFG.bluelink.lat,      change:'ne'}, ()=> maybeCheckHPC('lat_changed'));
       on({id: CFG.bluelink.lon,      change:'ne'}, ()=> maybeCheckHPC('lon_changed'));
       on({id: CFG.bluelink.latAlt,   change:'ne'}, ()=> maybeCheckHPC('lat_changed_alt'));
       on({id: CFG.bluelink.lonAlt,   change:'ne'}, ()=> maybeCheckHPC('lon_changed_alt'));
      
       // Energy ticker
       schedule(`*/${CFG.energy.integTickSec} * * * * *`, integrationTick);
      
       // Periodische HPC-Abfrage
       schedule('*/5 * * * *', ()=> maybeCheckHPC('scheduled'));
      
       // Test-Button
       on({id: HPC.CMD_TEST, change:'ne'}, async (s)=>{
         if (s && s.state && s.state.val===true){
           await ss(HPC.CMD_TEST,false);
           const rOverride = toNum(g(HPC.TEST_RADIUS), NaN);
           await maybeCheckHPC('manual_test', { force:true, radiusKmOverride: isFinite(rOverride)?rOverride:undefined });
         }
       });
      
       // SelfTests – **fixes map centering**: wir übergeben centerOverride
       function selfTest(lat, lon, label){
         return async (s)=>{
           if (s && s.state && s.state.val===true){
             await ss(s.id,false);
             // Debug-Anzeige der Test-Geo
             await ss(HPC.DBG_COORD_LAT, lat); await ss(HPC.DBG_COORD_LON, lon);
             // Direkte OCM-Abfrage (wie maybeCheckHPC, aber ohne Rate-Limit & mit Center-Override)
             try{
               let q = await fetchOCM(lat,lon,Math.max(20,CFG.hpc.radiusKm),Math.max(80,CFG.hpc.maxResults),'strict');
               await ss(HPC.DBG_URL, q.url); await ss(HPC.DBG_HTTP, `self:${q.status}`); await ss(HPC.DBG_RAW, Array.isArray(q.json)?q.json.length:0);
               let enriched = enrichAndFilter(q.json, lat, lon);
               if (enriched.length===0){
                 q = await fetchOCM(lat,lon,CFG.hpc.radiusKm,CFG.hpc.maxResults,'dcOnly');
                 enriched = enrichAndFilter(q.json, lat, lon);
               }
               if (enriched.length===0){
                 const old=CFG.hpc.hpcMinKW; CFG.hpc.hpcMinKW=CFG.hpc.cascadeMinKWClient||120;
                 q = await fetchOCM(lat,lon,CFG.hpc.radiusKm,Math.max(CFG.hpc.maxResults,120),'all');
                 enriched = enrichAndFilter(q.json, lat, lon);
                 CFG.hpc.hpcMinKW=old;
               }
               await ss(HPC.COUNT, enriched.length);
               await ss(HPC.HAS, enriched.length>0);
               await ss(HPC.LASTJSON, JSON.stringify(enriched));
               if (enriched.length){
                 const n=enriched[0];
                 await ss(HPC.NAME, n.name||''); await ss(HPC.DISTM, n.distance_m||0);
                 await ss(HPC.KW, n.maxPower_kW||0); await ss(HPC.OP, n.operator||'');
               } else {
                 await ss(HPC.NAME,''); await ss(HPC.DISTM,0); await ss(HPC.KW,0); await ss(HPC.OP,'');
               }
               await ss(HPC.LASTCHK, new Date().toISOString()); await ss(HPC.LASTERR,''); await ss(HPC.LASTWHY, 'selftest_'+label);
               // **WICHTIG**: Karte explizit um die Test-Geo zentrieren
               renderDashboard({lat:lat, lon:lon});
             } catch(err){
               await ss(HPC.LASTERR, String(err?.message||err));
               await ss(HPC.DBG_ERRSHORT, String(err?.message||err));
               renderDashboard({lat:lat, lon:lon});
             }
           }
         };
       }
       on({id:HPC.CMD_TEST_FFM,change:'ne'}, selfTest(50.1109, 8.6821, 'FFM'));
       on({id:HPC.CMD_TEST_CGN,change:'ne'}, selfTest(50.9375, 6.9603, 'CGN'));
       on({id:HPC.CMD_TEST_MUC,change:'ne'}, selfTest(48.1372,11.5756, 'MUC'));
      }
      
      /*** ===== Bootstrap ===== ***/
      async function init(){
       await ensureHpcStates();
       await ensureEnergyStates();
       await ensureDashState();
       // kleiner Delay, dann initial laden + rendern
       setTimeout(()=>{ maybeCheckHPC('startup'); integrationTick(); }, 1200);
       attachTriggers();
       logI('Init: ready');
      }
      init();
      
      

      posted in Tester
      ilovegym
      ilovegym
    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @meister-mopper sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):

      @ilovegym sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):

      Ioniq5N

      Das Kfz ist ja Teufelszeug, damit könnte ich meine Lady nie fahren lassen, die würde allen BMW und Audi Losern den Beschleunigungskrieg erklären.

      … und das macht sowas von Spaß… 😆😂🙈😇😇😇 wäre bei meiner aber auch, nur wenn se nebendran sitzt, darf ich die 200 nicht überschreiten.. 🙈😅

      posted in Tester
      ilovegym
      ilovegym
    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @meister-mopper

      Ja haben wir, ich „sitz“ auf der 3.1.6 mit meinem Ioniq5N , die funktioniert, neuere haben ein Login Problem..

      posted in Tester
      ilovegym
      ilovegym
    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @schmuh

      ich hatte in den letzten 4 Wochen bestimmt 20x DC geladen von unterwegs aus, bin viel gefahren, hatte daher aber leider keine Zeit, darauf zu achten, dass er auch bluelink abfragt, teilweise sogar bei einer ionity nichmal LTE gehabt.. zum Glueck ging die Ladekarte..

      Waere cool, wenn man seine Ladevorgaenge von ionity/enbw in den iobroker bringen koennte.. aber die haben keine Api (nur fuer die Flottenkarten) .. da musste erstmal mit n token rausbekommen..

      Alternativ koennte man sich n script schreiben, das den charging-state abfragt, dazu den standort analysiert, schaut, ob es ionity/enbw in der naehe gibt, und dann die geladene kw entsprechend in einen state schreibt.. hmm mal wieder was fuer n Onkel GPT 🙂

      @schmuh super(dank history), ja, schau, ist halt immer knapp, der braucht ja nur 15min von 15-80% bei >200kw
      Screenshot 2025-09-29 at 11.38.05.png

      posted in Tester
      ilovegym
      ilovegym
    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @meister-mopper @schmuh

      schaut mal unter bluelink.0.KMHKRxxxxxxxxxx.vehicleStatusRaw.ccs2Status.state.Vehicle.Green.ChargingInformation.ConnectorFastening.State

      kann auch sein, dass das einfach nur der Stecker connected ist.. war gestern nur AC laden an der Wallbox (da war er auf 1, sonst 0, DC muesste dann 2 sein)

      posted in Tester
      ilovegym
      ilovegym
    • RE: Betatest NSPanel-lovelace-ui v0.4.x

      @ticaki sagte in Betatest NSPanel-lovelace-ui v0.4.x:

      Nur damit ich nicht der einzige bin der es weiß - steht vielleicht ja in der wiki - aber trotzdem:

      Wenn der nspanel-adapter auf einem anderen Rechner/neuen iobroker zum Einsatz kommt, müssen die verbundenen NSPanel mit reset 4 auf der Konsole dazu animiert werden zu vergessen mit wem sie verbunden waren, sonst weigern sie sich. 🙂

      Danke hier auch nochmal 🙂 - Fuer alle anderen, der Adapter laeuft auch unter Debian Trixie mit Node 20.22.0, im neuen Admin V7.7.3 wird er "von Git installiert" und "nicht gewartet" angezeigt:
      Screenshot 2025-09-27 at 10.27.45.png

      Wichtig noch fuers Wiki: Mann muss bei Umzug auf den neuen Rechner die Panels aus der Konfig loeschen, da sich das TLS Zertifikat auf dem Host ja geaendert hat, und alle Panels wieder neu in die Konfig eingebunden werden muessen.
      Adapter selbst muss nicht geloescht werden, die scripts bleiben, werden aber bei Neueinrichtung eines Panels mit dem gleichen Namen ueberschrieben - so waren meine Scripts erstmal weg 😞 war zum Glueck nich viel drin, kann sie ja auch wieder restoren.

      Nervig ist, dass der Adapter bei vielen Dingen ein Konfig-Speichern und damit neustart macht, was bei allen schon eingetragenen Panels ein Reboot ausloest.

      Gut ist, dass der Berrytreiber und die Firmware automatisch aktualisiert werden, bei 12 Panels braucht man da viel Geduld, da alle nach und nach eingerichtet werden muessen, ich kann sie nicht auf einmal in der Konfig eintragen und dann auf Save gehen, da nahm er nur das letzte, und die ersten hatten zwar die Konfig, aber der Adapter dann nach Neustart nicht mehr..
      Vielleicht kann man diesen Vorgang optimieren?

      Ansonsten laeuft das stable, leider muss auch unbedingt der Pirate-Weather Adapter installiert sein, sonst hagelt es in V0.4.0 Warnings weil er die Templates hat und versucht da was zu finden... Auch wenn in der Konfig auf Brightsky gestellt ist..

      posted in Tester
      ilovegym
      ilovegym
    • RE: Adapter Hyundai (Bluelink) oder KIA (UVO)

      @arteck @Meister-Mopper

      so ist es in der My-Hyundai-App auch, denke, die beiden Apps sind ziemlich baugleich...

      Thema Adapter:
      Bei mir laeuft immer noch die 3.1.6 (auf en gestellt) erfolgreich.

      posted in Tester
      ilovegym
      ilovegym
    • RE: Usertreffen: Rhein-Main Event: 27.9.25 in Mainz!

      Ja, am 27.9.2025 ab 16.00 Uhr im Zeitlos zeitlosproviant.de in Mainz ist ein Tisch reserviert! 🙂

      posted in Usergroups
      ilovegym
      ilovegym
    Community
    Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen
    The ioBroker Community 2014-2023
    logo