|
|
|
@@ -554,6 +554,51 @@ function renderSpotChart(spotData) {
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderWeatherChart(weather) {
|
|
|
|
|
if (!weather || !weather.configured || weather.error) return '';
|
|
|
|
|
const hourly = weather.hourly || [];
|
|
|
|
|
if (hourly.length < 2) return '';
|
|
|
|
|
|
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
|
const maxGhi = Math.max(...hourly.map(h => h.ghi), 100);
|
|
|
|
|
const sunCol = g => g >= weather.sun_threshold ? '#f0c040' : g > 0 ? '#5b6472' : '#30363d';
|
|
|
|
|
|
|
|
|
|
const W=520, H=108, PL=34, PR=8, PT=14, PB=22;
|
|
|
|
|
const cW=W-PL-PR, cH=H-PT-PB, bW=cW/hourly.length;
|
|
|
|
|
|
|
|
|
|
const bars = hourly.map((h,i) => {
|
|
|
|
|
const isNow = now >= h.ts && now < h.ts+3600;
|
|
|
|
|
const barH = Math.max(2, (h.ghi/maxGhi)*cH);
|
|
|
|
|
const x=(PL+i*bW), y=PT+cH-barH;
|
|
|
|
|
const col = sunCol(h.ghi);
|
|
|
|
|
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>` : '';
|
|
|
|
|
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:.7}" 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}`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
const yTicks = [[0,PT+cH],[maxGhi,PT]].map(([g,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">${g.toFixed(0)}</text>`
|
|
|
|
|
).join('');
|
|
|
|
|
|
|
|
|
|
const cur = hourly.find(h => now>=h.ts && now<h.ts+3600);
|
|
|
|
|
const curLabel = cur
|
|
|
|
|
? `<span style="font-size:16px;font-weight:700;color:${sunCol(cur.ghi)}">${cur.ghi.toFixed(0)} W/m²</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">Solarprognose · 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 fEur(v) {
|
|
|
|
|
if (v == null) return null;
|
|
|
|
|
return v >= 100 ? v.toFixed(0) + ' €' : v.toFixed(2) + ' €';
|
|
|
|
@@ -563,7 +608,7 @@ function fKwh(v) {
|
|
|
|
|
return (v >= 100 ? v.toFixed(0) : v.toFixed(1)) + ' kWh';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderEnergy(inverters, aggregates, period, spotData) {
|
|
|
|
|
function renderEnergy(inverters, aggregates, period, spotData, weatherData) {
|
|
|
|
|
const el = document.getElementById("energy-content");
|
|
|
|
|
if (!aggregates || !Object.keys(aggregates).length) {
|
|
|
|
|
el.innerHTML = '<div class="no-data">Warte auf erste Messung…</div>';
|
|
|
|
@@ -729,6 +774,7 @@ function renderEnergy(inverters, aggregates, period, spotData) {
|
|
|
|
|
sectionCards(yr.label || 'Dieses Jahr', yr);
|
|
|
|
|
|
|
|
|
|
const spotHtml = (period.spot_chart !== false) ? renderSpotChart(spotData) : '';
|
|
|
|
|
const weatherHtml = renderWeatherChart(weatherData);
|
|
|
|
|
|
|
|
|
|
const bt = period.billing_tracker;
|
|
|
|
|
const trackerHtml = bt ? (() => {
|
|
|
|
@@ -764,7 +810,7 @@ function renderEnergy(inverters, aggregates, period, spotData) {
|
|
|
|
|
})() : '';
|
|
|
|
|
|
|
|
|
|
el.innerHTML = `<div class="energy-wrap">
|
|
|
|
|
<div class="energy-svg-wrap">${svg}${spotHtml}</div>
|
|
|
|
|
<div class="energy-svg-wrap">${svg}${spotHtml}${weatherHtml}</div>
|
|
|
|
|
${cards ? `<div style="margin-top:16px">${cards}</div>` : ''}
|
|
|
|
|
${trackerHtml}
|
|
|
|
|
</div>`;
|
|
|
|
@@ -974,7 +1020,8 @@ async function refreshData() {
|
|
|
|
|
renderLive(d.inverters || {}, d.aggregates || {}, surplusData);
|
|
|
|
|
const period = await fetchJSON(api("api/period-energy")).catch(() => ({}));
|
|
|
|
|
const spot = await fetchJSON(api("api/spot-price")).catch(() => ({}));
|
|
|
|
|
renderEnergy(d.inverters || {}, d.aggregates || {}, period, (spot.data || []));
|
|
|
|
|
const weather = await fetchJSON(api("api/weather-forecast")).catch(() => ({}));
|
|
|
|
|
renderEnergy(d.inverters || {}, d.aggregates || {}, period, (spot.data || []), weather);
|
|
|
|
|
} catch(e) {
|
|
|
|
|
document.getElementById("pill-mqtt").className = "pill err";
|
|
|
|
|
}
|
|
|
|
|