/* ============================================================================ GC · LADDHISTORIK — hans sjudagarsblock, uppmätt data ---------------------------------------------------------------------------- GravioApp.jsx BilView avslutas med "Laddhistorik · 7 dagar". Hans rader kom ur rnd(); våra kommer ur GET /api/ev/history?days=7 (live 16/8), som mäter ur ev_actual. Skillnader mot hans block, alla för att kontraktet säger något annat: 1. INGEN BIL, BARA BOX. car_id finns inte i svaret — vi visar laddboxens namn. Hans "C40"/"G9P" är bilnamn. 2. TRE KÄLLOR, INTE TVÅ. Svaret skiljer solar_kwh, battery_kwh och grid_kwh. Hans block har bara sol och nät; en laddning ur hembatteriet fanns inte i hans värld. 3. KOSTNADEN ÄR BACKENDENS. cost_kr = nätdelen × köppris, solar_value_kr = soldelen × säljpris (alltså vad solen hade gett vid försäljning — hans "alt."). Vi räknar inte om dem. 4. within_plan och unknown_quarters syns. En laddning utanför styrningen är inte samma sak som en planerad, och kvartar utan mätvärde ska inte tyst räknas som noll. ========================================================================== */ const GH_SRC_COL = { sol:'#facc15', 'nät':'#60a5fa', batteri:'#fb923c', blandat:'#a3e635' }; function ghDayLabel(iso, today){ if (!iso) return '—'; const d = new Date(iso + 'T12:00:00'); if (isNaN(d.getTime())) return iso; const t = today ? new Date(today + 'T12:00:00') : null; if (t){ const diff = Math.round((t - d) / 86400000); if (diff === 0) return 'I dag'; if (diff === 1) return 'I går'; } return d.toLocaleDateString('sv-SE', { weekday:'short', day:'numeric', month:'short' }); } function GcLaddhistorik({ settings }){ const [state, setState] = React.useState({ loading:true, data:null, err:null }); React.useEffect(() => { if (!window.hems || !window.hems.getEvHistory){ setState({ loading:false, data:null, err:'saknas' }); return; } /* Samma mount-race som Prognoser hade: kortet monteras före bootHems satt nyckeln, och ett kast med noKeyYet är övergående. gcFetchReady väntar. */ return window.gcFetchReady(() => window.hems.getEvHistory(7), { // null = endpointen svarade inte (t.ex. inte deployad på den rev // sessionen träffar). Blocket ritas inte alls då — inget felmeddelande // om något användaren inte kan påverka. onOk: (d) => setState({ loading:false, data:d, err: d ? null : 'saknas' }), onFail: (e) => setState({ loading:false, data:null, err: String(e && e.message || e) }), }); }, []); const boxName = React.useCallback((id) => { const list = (window.evModel && window.evModel.normalize) ? window.evModel.normalize(settings) : []; // Kanoniskt id är ev_1, ev_2 … alltså POSITIONEN. Matcha på id först, annars // på ordningen — samma regel som ev_power_list och ev_per_car. const byId = list.findIndex(c => String(c.id) === String(id)); const m = /^ev_(\d+)$/.exec(String(id || '')); const k = byId >= 0 ? byId : (m ? Number(m[1]) - 1 : -1); const c = k >= 0 ? list[k] : null; return c ? ((window.evModel && window.evModel.displayName) ? window.evModel.displayName(c, k) : (c.name || 'Laddbox ' + (k+1))) : String(id || 'Laddbox'); }, [settings]); if (state.loading) { return (
Hämtar laddhistorik…
); } if (state.err || !state.data) return null; const sessions = Array.isArray(state.data.sessions) ? state.data.sessions : []; const tot = state.data.totals || {}; const today = window.hemsToday ? window.hemsToday() : null; const kr = (v) => v == null ? '—' : (Math.abs(v) < 10 ? window.gcNum(v, 2) : window.gcNum(v, 0)) + ' kr'; /* Summa och uppdelning måste stänga vid VISAD precision. Med regeln "under 10 en decimal, däröver heltal" fick totalen och delarna olika precision — 52,4 skrevs "52" medan totalen 52,7 skrevs "53", och foten påstod en uppdelning som inte gick ihop. Samma fälla som Statistik, Avancerat och Last redan städat bort. Regeln nu: EN precision för hela gruppen, satt av det minsta talet som ska synas. Delarna rundas, de som blir 0 faller bort, och differensen mot den avrundade totalen läggs på den största delen. Då summerar det som står på skärmen alltid till det som står i rubriken — och en batteridel under upplösningen försvinner inte ur summan, den hamnar hos den största källan i stället för att tyst ätas. */ const parts = [ { label:'Sol', v: tot.solar_kwh }, { label:'batteri', v: tot.battery_kwh }, { label:'nät', v: tot.grid_kwh }, ].filter(p => typeof p.v === 'number' && p.v > 0); const totKwh = typeof tot.kwh === 'number' ? tot.kwh : parts.reduce((a,p) => a + p.v, 0); const dec = parts.concat([{v:totKwh}]).some(p => p.v > 0 && p.v < 10) ? 1 : 0; const kwh = (v) => window.gcNum(v, dec) + ' kWh'; const rTot = Number(totKwh.toFixed(dec)); const shown = parts.map(p => ({ ...p, r: Number(p.v.toFixed(dec)) })).filter(p => p.r > 0); if (shown.length){ const diff = Number((rTot - shown.reduce((a,p) => a + p.r, 0)).toFixed(dec)); if (diff) shown.reduce((a,b) => b.r > a.r ? b : a).r += diff; } return (
Laddhistorik · {state.data.days || 7} dagar {tot.kwh != null ? `${kwh(rTot)} · ${kr(tot.cost_kr)}` : '—'}
{sessions.length === 0 ? (

Ingen laddning uppmätt de senaste dygnen.

) : (
{sessions.map((r, i) => { const col = GH_SRC_COL[r.source] || '#6B7A94'; return (
{ghDayLabel(r.date, today)} {boxName(r.charger_id)} {r.source || '—'} {r.within_plan === false && ( utanför plan )} {window.gcNum(r.kwh, 1)} kWh {kr(r.cost_kr)} {r.solar_value_kr > 0.005 && ( sol {kr(r.solar_value_kr)} )}
); })}
)} {shown.length > 0 && (
{shown.map((p, k) => ( {k > 0 ? ' · ' : ''}{p.label} {kwh(p.r)} ))} {tot.solar_value_kr > 0 && ( solen värd {kr(tot.solar_value_kr)} )}
)}

Uppmätt laddning, inte plan. Kostnaden är nätdelen gånger köppriset; solvärdet är vad soldelen hade gett vid försäljning. Kvartar utan mätvärde räknas inte med. {sessions.some(r => r.unknown_quarters > 0) && ' Några kvartar saknar mätvärde i perioden.'}

); } Object.assign(window, { GcLaddhistorik, ghDayLabel });