201 lines
4.8 KiB
HTML
201 lines
4.8 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>PV_Forecast</title>
|
||
<script src="https://code.highcharts.com/highcharts.js"></script>
|
||
|
||
<style>
|
||
body {
|
||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||
margin: 0;
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
/* Kopfzeile */
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 14px;
|
||
background: #1f2937;
|
||
border-bottom: 1px solid #374151;
|
||
}
|
||
|
||
.meta {
|
||
color: #e5e7eb;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* Chart */
|
||
#chart {
|
||
height: 520px;
|
||
width: 100%;
|
||
padding: 10px;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="header">
|
||
<div class="meta" id="meta">Lade…</div>
|
||
</div>
|
||
|
||
<div id="chart"></div>
|
||
|
||
<script>
|
||
const metaEl = document.getElementById("meta");
|
||
|
||
window.onerror = (msg, url, line, col) => {
|
||
metaEl.textContent = `JS-Fehler: ${msg} (${line}:${col})`;
|
||
};
|
||
|
||
const instanceId = Number("{{INSTANCE_ID}}");
|
||
if (!Number.isFinite(instanceId) || instanceId <= 0) {
|
||
metaEl.textContent = "Fehler: INSTANCE_ID nicht korrekt.";
|
||
throw new Error("Invalid INSTANCE_ID");
|
||
}
|
||
|
||
const endpoint =
|
||
`${location.protocol}//${location.host}` +
|
||
`/hook/solcastcompare?instance=${instanceId}&action=data`;
|
||
|
||
let chart;
|
||
|
||
async function loadData() {
|
||
metaEl.textContent = "⏳ Daten werden geladen…";
|
||
const r = await fetch(endpoint, { cache: "no-store" });
|
||
if (!r.ok) {
|
||
const text = await r.text().catch(() => "");
|
||
throw new Error(`HTTP ${r.status}${text ? " - " + text : ""}`);
|
||
}
|
||
return await r.json();
|
||
}
|
||
|
||
function render(data) {
|
||
if (typeof Highcharts === "undefined") {
|
||
metaEl.textContent = "Fehler: Highcharts nicht geladen.";
|
||
return;
|
||
}
|
||
|
||
const forecast = data?.series?.forecast ?? [];
|
||
const actual = data?.series?.actual ?? [];
|
||
|
||
const cachedAt = data?.meta?.forecast_cached_at
|
||
? new Date(data.meta.forecast_cached_at * 1000).toLocaleString()
|
||
: "unbekannt";
|
||
|
||
const nowMs = data?.meta?.now ?? Date.now();
|
||
|
||
metaEl.textContent =
|
||
`OK | Cache: ${cachedAt} | Forecast: ${forecast.length} | Ist: ${actual.length}`;
|
||
|
||
const options = {
|
||
title: {
|
||
text: "PV: Erwartung vs. Tatsächlich (heute)",
|
||
style: { fontWeight: "700" }
|
||
},
|
||
|
||
chart: {
|
||
animation: false,
|
||
spacingTop: 18
|
||
},
|
||
|
||
xAxis: {
|
||
type: "datetime",
|
||
plotLines: [{
|
||
color: "#ef4444",
|
||
width: 2,
|
||
value: nowMs,
|
||
dashStyle: "Dash",
|
||
label: {
|
||
text: "JETZT",
|
||
align: "right",
|
||
style: { color: "#ef4444", fontWeight: "700" }
|
||
}
|
||
}]
|
||
},
|
||
|
||
yAxis: {
|
||
title: {
|
||
text: "Leistung (kW)",
|
||
style: { fontWeight: "700" }
|
||
},
|
||
gridLineColor: "#e5e7eb"
|
||
},
|
||
|
||
legend: { enabled: true },
|
||
|
||
tooltip: {
|
||
shared: true,
|
||
formatter: function () {
|
||
let s = `<b>${Highcharts.dateFormat('%d.%m.%Y %H:%M', this.x)}</b><br/>`;
|
||
this.points.forEach(p => {
|
||
const val = (p.y === null || typeof p.y === "undefined")
|
||
? "–"
|
||
: `${p.y.toFixed(1)} kW`;
|
||
s += `<span style="color:${p.color}">●</span> ${p.series.name}: <b>${val}</b><br/>`;
|
||
});
|
||
return s;
|
||
}
|
||
},
|
||
|
||
plotOptions: {
|
||
series: {
|
||
animation: false,
|
||
turboThreshold: 0,
|
||
marker: { enabled: true }
|
||
}
|
||
},
|
||
|
||
series: [
|
||
{
|
||
name: "Erwartet (Solcast)",
|
||
data: forecast,
|
||
color: "#38bdf8",
|
||
lineWidth: 4,
|
||
marker: { radius: 4 }
|
||
},
|
||
{
|
||
name: "Tatsächlich (Archiv)",
|
||
data: actual,
|
||
color: "#7c3aed",
|
||
lineWidth: 4,
|
||
marker: { radius: 5, symbol: "diamond" }
|
||
}
|
||
],
|
||
|
||
credits: { enabled: false }
|
||
};
|
||
|
||
if (!chart) {
|
||
chart = Highcharts.chart("chart", options);
|
||
} else {
|
||
chart.update({
|
||
xAxis: { plotLines: options.xAxis.plotLines }
|
||
}, false);
|
||
|
||
chart.series[0].setData(forecast, false);
|
||
chart.series[1].setData(actual, false);
|
||
chart.redraw();
|
||
}
|
||
}
|
||
|
||
(async () => {
|
||
try {
|
||
const data = await loadData();
|
||
render(data);
|
||
} catch (e) {
|
||
metaEl.textContent = "Fehler beim Laden: " + (e?.message ?? e);
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|