fix(strommix): show sparkline without history backend
The strommix_history_de collection source URL 404s (Energy-Charts needs start/end params), so the CMS history backend stays empty and todayHourly was always null — making the Wind & Solar sparkline never appear. Extend fetchEnergyChartsPublicPower to start at midnight (was: -2h) and return the raw series alongside the snapshot values. Build todayHourly from it when the history backend has no items. No extra API call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,13 +46,6 @@ type StrommixRaw = {
|
||||
measured_at_unix?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch Energy-Charts /public_power directly with an explicit time
|
||||
* window. The CMS-cached `strommix_de` schema cannot do this because
|
||||
* external-collections v1 has no runtime-vars; without start/end the
|
||||
* upstream returns 404 ("no content available"). 2-hour window covers
|
||||
* latest 8 quarter-hour slots; we pick the latest non-null per series.
|
||||
*/
|
||||
type PublicPowerSeries = { name: string; data: (number | null)[] };
|
||||
type PublicPowerResp = {
|
||||
unix_seconds?: number[];
|
||||
@@ -67,25 +60,33 @@ function lastNonNull(arr: (number | null)[]): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Energy-Charts /public_power with an explicit time window.
|
||||
* `fromSec` is the window start (pass `dayStartSec` to cover all of today).
|
||||
* Returns both the current-snapshot values (for live numbers) and the raw
|
||||
* series (for the today-hourly sparkline when the history backend is empty).
|
||||
*/
|
||||
async function fetchEnergyChartsPublicPower(
|
||||
fetcher: typeof fetch
|
||||
): Promise<{ data: StrommixRaw | null; stale: boolean }> {
|
||||
fetcher: typeof fetch,
|
||||
fromSec: number
|
||||
): Promise<{ data: StrommixRaw | null; rawSeries: PublicPowerResp | null; stale: boolean }> {
|
||||
try {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const fmt = (sec: number) =>
|
||||
new Date(sec * 1000).toISOString().slice(0, 16) + "Z";
|
||||
const start = fmt(nowSec - 2 * 60 * 60);
|
||||
const start = fmt(fromSec);
|
||||
const end = fmt(nowSec);
|
||||
const url =
|
||||
`https://api.energy-charts.info/public_power?country=de` +
|
||||
`&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`;
|
||||
const res = await fetcher(url, { signal: AbortSignal.timeout(8_000) });
|
||||
if (!res.ok) return { data: null, stale: false };
|
||||
if (!res.ok) return { data: null, rawSeries: null, stale: false };
|
||||
const d = (await res.json()) as PublicPowerResp;
|
||||
const find = (n: string) =>
|
||||
d.production_types?.find((p) => p.name === n)?.data ?? [];
|
||||
const ts = d.unix_seconds ?? [];
|
||||
return {
|
||||
rawSeries: d,
|
||||
data: {
|
||||
wind_onshore_mw: lastNonNull(find("Wind onshore")),
|
||||
wind_offshore_mw: lastNonNull(find("Wind offshore")),
|
||||
@@ -102,10 +103,36 @@ async function fetchEnergyChartsPublicPower(
|
||||
stale: false,
|
||||
};
|
||||
} catch {
|
||||
return { data: null, stale: true };
|
||||
return { data: null, rawSeries: null, stale: true };
|
||||
}
|
||||
}
|
||||
|
||||
/** Build today's sparkline series directly from an Energy-Charts response.
|
||||
* Used as fallback when the CMS history backend has no data yet. */
|
||||
function buildTodayHourlyFromEC(
|
||||
d: PublicPowerResp,
|
||||
dayStartSec: number
|
||||
): ResidualLoadPoint[] {
|
||||
const ts = d.unix_seconds ?? [];
|
||||
const find = (n: string) =>
|
||||
d.production_types?.find((p) => p.name === n)?.data ?? [];
|
||||
const load = find("Load");
|
||||
const wOn = find("Wind onshore");
|
||||
const wOff = find("Wind offshore");
|
||||
const sol = find("Solar");
|
||||
const result: ResidualLoadPoint[] = [];
|
||||
for (let i = 0; i < ts.length; i++) {
|
||||
if (ts[i] < dayStartSec) continue;
|
||||
if (load[i] == null) continue;
|
||||
result.push({
|
||||
unix: ts[i],
|
||||
loadMw: num(load[i]),
|
||||
weatherRenewableMw: num(wOn[i]) + num(wOff[i]) + num(sol[i]),
|
||||
});
|
||||
}
|
||||
return result.sort((a, b) => a.unix - b.unix);
|
||||
}
|
||||
|
||||
type CrossborderRaw = {
|
||||
flow_at_gw?: number | null;
|
||||
flow_be_gw?: number | null;
|
||||
@@ -501,10 +528,10 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
||||
] = await Promise.all([
|
||||
fetchSmardSnapshot(fetch),
|
||||
// Energy-Charts /public_power direct (with explicit time range —
|
||||
// upstream now returns 404 without start/end). Used as per-field
|
||||
// fallback when SMARD's load filter (id 410) lags 12+ hours behind
|
||||
// the production filters and the SMARD snapshot gets discarded.
|
||||
fetchEnergyChartsPublicPower(fetch),
|
||||
// upstream returns 404 without start/end). Window starts at midnight so
|
||||
// the full-day series doubles as the sparkline fallback when the CMS
|
||||
// history backend is empty. Used as per-field fallback when SMARD lags.
|
||||
fetchEnergyChartsPublicPower(fetch, dayStartSec),
|
||||
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
||||
fetchPriceAtNow("DE-LU", fetch),
|
||||
fetchPriceAtNow("FR", fetch),
|
||||
@@ -599,10 +626,15 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
||||
// Historical aggregates derived from CMS `_history`. `null` when the
|
||||
// backing collection is unavailable (e.g. CMS not yet upgraded with
|
||||
// history-mode schemas) — widget renders without the affected panels.
|
||||
// Build today's sparkline series. Prefer the CMS history backend (full
|
||||
// deduped timeline); fall back to the Energy-Charts direct response when
|
||||
// history is empty (e.g. backfill not yet run on a fresh install).
|
||||
const todayHourly =
|
||||
histStrommix && histStrommix.items.length > 0
|
||||
? buildTodayHourly(histStrommix.items)
|
||||
: null;
|
||||
: energyCharts.rawSeries
|
||||
? buildTodayHourlyFromEC(energyCharts.rawSeries, dayStartSec)
|
||||
: null;
|
||||
const negPriceHoursYtd = histPriceDe ? countNegPriceSlots(histPriceDe.items) : null;
|
||||
const crossborderYtd = histCrossborder
|
||||
? aggregateCrossborderRange(histCrossborder.items, yearStartSec)
|
||||
|
||||
Reference in New Issue
Block a user