Adapter Hyundai (Bluelink) oder KIA (UVO)
-
@ilovegym sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):
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)
Guude Bernd,
ich hab da nur
bluelink.0.blabla.vehicleStatusRaw.Green.ChargingInformation.ConnectorFastening.State
gefunden.Offenbar haben wir verschiedene Adapter-Versionen, aber von der Logik sieht das gut aus.
Irgendwie ist das mit dem
vehicelStatusRaw
ein lustiges Ratespiel. Wenn Hyundai/Kia irgendwann mal fertig ist, könnte ich ja @arteck bei der Doku behilflich sein (Zeit habe ich ja jetzt genug).
-
Ja haben wir, ich „sitz“ auf der 3.1.6 mit meinem Ioniq5N , die funktioniert, neuere haben ein Login Problem..
-
@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.
-
@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..
-
@arteck Der Workaround hat jetzt 5 Tage funktioniert und jetzt wieder der Fehler mit Login. Hat Kia wieder etwas verändert ?
-
@gargano sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):
Hat Kia wieder etwas verändert ?
woher soll ich das wissen ??
geht es den mit neuen Token ?
-
@arteck sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):
geht es den mit neuen Token
Weder mit dem bisherigen Token, noch mit einem neu generierten.
Ich wollte auch nicht nerven, sondern nur Bescheid geben. -
@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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')+'"></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();
-
Du machst bald die Adapterentwickler arbeitsfrei.
Ab dann biste aber Skriptentwickler for all
-
@gargano sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):
@arteck sagte in Adapter Hyundai (Bluelink) oder KIA (UVO):
geht es den mit neuen Token
Weder mit dem bisherigen Token, noch mit einem neu generierten.
Ich wollte auch nicht nerven, sondern nur Bescheid geben.Siehe oben, hatte ich auch.
Haste mal das Prozedere erneut durchgeführt und einen neuen refresh token geholt?