diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index 8237164..ea8a354 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -133,6 +133,19 @@ font-size: 12px; color: var(--text-dim); } .info-chip span { color: var(--text); font-weight: 600; } + /* Energy Dashboard */ + .energy-wrap { max-width: 580px; margin: 0 auto; } + .energy-svg-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; overflow: hidden; } + .energy-kwh { display: grid; grid-template-columns: repeat(auto-fit, minmax(95px, 1fr)); gap: 10px; } + .kwh-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 8px; text-align: center; } + .kwh-card .kv { font-size: 18px; font-weight: 700; line-height: 1.2; font-variant-numeric: tabular-nums; } + .kwh-card .kl { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; margin-top: 4px; } + @keyframes flowFwd { to { stroke-dashoffset: -16; } } + @keyframes flowBwd { to { stroke-dashoffset: 16; } } + .flow-line { stroke-dasharray: 6 6; animation: flowFwd .7s linear infinite; } + .flow-line.reverse { animation: flowBwd .7s linear infinite; } + .flow-line.inactive { animation: none; opacity: .18; } + /* Toast */ .toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px; background: var(--surface2); border: 1px solid var(--border); @@ -162,13 +175,19 @@
-
Live-Daten
+
Energie
+
Live-Daten
Geräte
Einstellungen
+ +
+
Warte auf erste Messung…
+
+ -
+
Warte auf erste Messung...
@@ -330,11 +349,110 @@ function showToast(msg, type) { } function switchTab(name) { - ["live","inverters","settings"].forEach((t, i) => { + ["energy","live","inverters","settings"].forEach((t, i) => { document.querySelectorAll(".tab")[i].classList.toggle("active", t === name); document.querySelectorAll(".panel")[i].classList.toggle("active", t === name); }); - if (name === "live") startRefresh(); else stopRefresh(); + if (name === "energy" || name === "live") startRefresh(); else stopRefresh(); +} + +// ── Energy Dashboard ────────────────────────────────────────── + +function renderEnergy(inverters, aggregates) { + const el = document.getElementById("energy-content"); + if (!aggregates || !Object.keys(aggregates).length) { + el.innerHTML = '
Warte auf erste Messung…
'; + return; + } + + const pvW = aggregates.total_pv_power || 0; + const gridW = aggregates.grid_power || 0; // + = Bezug, - = Einspeisung + const batChW = aggregates.bat_charge_power || 0; + const batDchW = aggregates.bat_discharge_power|| 0; + const batSoc = aggregates.bat_soc; + + let evW = 0; + Object.entries(inverters).forEach(([id, inv]) => { + const cfg = invertersList.find(c => c.id === id); + if (cfg && cfg.inverter_model === 'KATHREIN_WALLBOX') + evW += (inv.values && inv.values.total_power) || 0; + }); + + const gridImport = Math.max(0, gridW); + const gridExport = Math.max(0, -gridW); + const houseW = Math.max(0, pvW + gridImport + batDchW - batChW - evW); + + function fW(w) { + if (Math.abs(w) < 1) return '0 W'; + if (Math.abs(w) >= 1000) return (w/1000).toFixed(2) + ' kW'; + return Math.round(w) + ' W'; + } + + function node(cx, cy, icon, label, value, col, extra) { + return ` + + ${icon} + ${fW(value)} + ${label} + ${extra||''} + `; + } + + const C = { acc:'var(--accent)', red:'var(--red)', grn:'var(--green)', pur:'var(--purple)', blu:'var(--blue)', dim:'var(--text-dim)', brd:'var(--border)' }; + + // Flow line helpers + function fline(x1,y1,x2,y2, col, cls) { + return ``; + } + + const hasBat = batSoc !== undefined && batSoc !== null; + + const svg = ` + + ${fline(168,78,237,126, pvW>10?C.acc:C.brd, pvW>10?'':'inactive')} + + + ${fline(372,78,303,126, + gridImport>10?C.red:gridExport>10?C.grn:C.brd, + gridExport>10?'reverse':gridImport>10?'':'inactive')} + + ${hasBat ? ` + ${fline(237,170,164,222, batChW>10||batDchW>10?C.pur:C.brd, batDchW>10?'reverse':batChW>10?'':'inactive')}` : ''} + + + ${fline(303,170,376,222, evW>10?C.blu:C.brd, evW>10?'':'inactive')} + + + ${node(135, 60, '☀️', 'Solar', pvW, C.acc)} + ${node(405, 60, '⚡', gridImport>10?'Netzbezug':gridExport>10?'Einspeisung':'Netz', + gridImport>10?gridImport:gridExport, gridImport>10?C.red:gridExport>10?C.grn:C.dim)} + ${node(270, 148, '🏠', 'Haus', houseW, 'var(--text)')} + ${hasBat ? node(135, 244, '🔋', `Batt. ${Math.round(batSoc)}%`, + batChW>batDchW?batChW:batDchW, C.pur) : ''} + ${node(405, 244, '🚗', 'Wallbox', evW, evW>10?C.blu:C.dim)} + `; + + function kwhCard(icon, label, val, col) { + if (val === undefined || val === null) return ''; + const d = val >= 100 ? val.toFixed(0) : val >= 10 ? val.toFixed(1) : val.toFixed(2); + return `
+
${d}
+
${icon} ${label}
+
`; + } + + const cards = [ + kwhCard('☀️','PV Heute kWh', aggregates.total_energy_today, C.acc), + kwhCard('📥','Bezug kWh', aggregates.grid_import_kwh, C.red), + kwhCard('📤','Einspeisung kWh', aggregates.grid_export_kwh, C.grn), + kwhCard('⚡','Bat. Laden kWh', aggregates.bat_charge_total, C.pur), + kwhCard('🔋','Bat. Entl. kWh', aggregates.bat_discharge_total, C.pur), + ].filter(Boolean).join(''); + + el.innerHTML = `
+
${svg}
+ ${cards ? `
${cards}
` : ''} +
`; } // ── Live Data ───────────────────────────────────────────────── @@ -348,6 +466,7 @@ async function refreshData() { document.getElementById("subtitle").textContent = keys.length ? `${keys.length} Gerät${keys.length !== 1 ? "e" : ""}` : "Keine Geräte"; renderLive(d.inverters || {}, d.aggregates || {}); + renderEnergy(d.inverters || {}, d.aggregates || {}); } catch(e) { document.getElementById("pill-mqtt").className = "pill err"; }