Feature: Börsenstrompreis-Chart (EPEX SPOT via aWATTar) im Energie-Dashboard (v1.8.0)

- /api/spot-price: Proxy zu api.awattar.de/v1/marketdata, 15-min Cache
- EUR/MWh → ct/kWh Konvertierung
- SVG-Balkenchart 24h-Prognose, gleiche Breite wie Flussdiagramm (viewBox 520)
- Farbe nach Preistertil: grün/gelb/rot; aktuelle Stunde hervorgehoben
- Aktuellen Preis als ct/kWh-Label in der Kopfzeile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-04-29 08:15:52 +02:00
parent f96d798274
commit 526e802c74
3 changed files with 80 additions and 4 deletions
+56 -3
View File
@@ -372,7 +372,57 @@ function switchTab(name) {
// ── Energy Dashboard ──────────────────────────────────────────
function renderEnergy(inverters, aggregates, period) {
function renderSpotChart(spotData) {
if (!spotData || !spotData.length) return '';
const now = Math.floor(Date.now() / 1000);
const startH = Math.floor(now / 3600) * 3600;
const hours = spotData.filter(d => d.ts >= startH && d.ts < startH + 86400).sort((a,b) => a.ts - b.ts);
if (hours.length < 2) return '';
const prices = hours.map(h => h.price);
const minP = Math.min(...prices), maxP = Math.max(...prices);
const rangeP = maxP - minP || 1;
const q1 = minP + rangeP / 3, q2 = minP + 2 * rangeP / 3;
const priceCol = p => p <= q1 ? '#3fb950' : p <= q2 ? '#f0c040' : '#f85149';
const W=520, H=108, PL=34, PR=8, PT=14, PB=22;
const cW=W-PL-PR, cH=H-PT-PB, bW=cW/hours.length;
const bars = hours.map((h,i) => {
const isNow = now >= h.ts && now < h.ts+3600;
const barH = Math.max(2, ((h.price-minP)/rangeP)*cH);
const x=(PL+i*bW), y=PT+cH-barH;
const col = priceCol(h.price);
const hh = new Date(h.ts*1000).getHours();
const tl = hh%6===0 ? `<text x="${(x+bW/2).toFixed(1)}" y="${H-3}" text-anchor="middle" font-size="8" fill="#8b949e">${String(hh).padStart(2,'0')}h</text>` : '';
const pl = isNow ? `<text x="${(x+bW/2).toFixed(1)}" y="${(y-3).toFixed(1)}" text-anchor="middle" font-size="8.5" font-weight="700" fill="${col}">${h.price.toFixed(1)}</text>` : '';
return `<rect x="${(x+1).toFixed(1)}" y="${y.toFixed(1)}" width="${(bW-2).toFixed(1)}" height="${barH.toFixed(1)}" fill="${col}" opacity="${isNow?1:.6}" rx="1.5"/>
${isNow?`<rect x="${x.toFixed(1)}" y="${PT}" width="${bW.toFixed(1)}" height="${cH}" fill="none" stroke="rgba(255,255,255,.25)" stroke-width="1.5" rx="1.5"/>`:''}
${tl}${pl}`;
}).join('');
const yTicks = [[minP,PT+cH],[maxP,PT]].map(([p,y]) =>
`<line x1="${PL}" y1="${y.toFixed(1)}" x2="${W-PR}" y2="${y.toFixed(1)}" stroke="#30363d" stroke-width="0.5"/>
<text x="${PL-3}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="8" fill="#8b949e">${p.toFixed(0)}</text>`
).join('');
const cur = hours.find(h => now>=h.ts && now<h.ts+3600);
const curLabel = cur
? `<span style="font-size:16px;font-weight:700;color:${priceCol(cur.price)}">${cur.price.toFixed(2)} ct/kWh</span><span style="font-size:10px;color:#8b949e;margin-left:6px">Jetzt</span>`
: '';
return `<div style="border-top:1px solid #30363d;padding:14px 14px 4px">
<div style="display:flex;align-items:baseline;gap:6px;margin-bottom:8px">
<span style="font-size:10px;font-weight:700;letter-spacing:.08em;color:#8b949e;text-transform:uppercase">EPEX SPOT · 24h</span>
${curLabel}
</div>
<svg viewBox="0 0 ${W} ${H}" width="100%" style="display:block" xmlns="http://www.w3.org/2000/svg">
${yTicks}${bars}
</svg>
</div>`;
}
function renderEnergy(inverters, aggregates, period, spotData) {
const el = document.getElementById("energy-content");
if (!aggregates || !Object.keys(aggregates).length) {
el.innerHTML = '<div class="no-data">Warte auf erste Messung…</div>';
@@ -536,8 +586,10 @@ function renderEnergy(inverters, aggregates, period) {
const cards = sectionCards(mon.label || 'Diesen Monat', mon, C.imp) +
sectionCards(yr.label || 'Dieses Jahr', yr, C.imp);
const spotHtml = renderSpotChart(spotData);
el.innerHTML = `<div class="energy-wrap">
<div class="energy-svg-wrap">${svg}</div>
<div class="energy-svg-wrap">${svg}${spotHtml}</div>
${cards ? `<div style="margin-top:16px">${cards}</div>` : ''}
</div>`;
}
@@ -554,7 +606,8 @@ async function refreshData() {
keys.length ? `${keys.length} Gerät${keys.length !== 1 ? "e" : ""}` : "Keine Geräte";
renderLive(d.inverters || {}, d.aggregates || {});
const period = await fetchJSON(api("api/period-energy")).catch(() => ({}));
renderEnergy(d.inverters || {}, d.aggregates || {}, period);
const spot = await fetchJSON(api("api/spot-price")).catch(() => ({}));
renderEnergy(d.inverters || {}, d.aggregates || {}, period, (spot.data || []));
} catch(e) {
document.getElementById("pill-mqtt").className = "pill err";
}