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:
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user