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;
|
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 PublicPowerSeries = { name: string; data: (number | null)[] };
|
||||||
type PublicPowerResp = {
|
type PublicPowerResp = {
|
||||||
unix_seconds?: number[];
|
unix_seconds?: number[];
|
||||||
@@ -67,25 +60,33 @@ function lastNonNull(arr: (number | null)[]): number | null {
|
|||||||
return 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(
|
async function fetchEnergyChartsPublicPower(
|
||||||
fetcher: typeof fetch
|
fetcher: typeof fetch,
|
||||||
): Promise<{ data: StrommixRaw | null; stale: boolean }> {
|
fromSec: number
|
||||||
|
): Promise<{ data: StrommixRaw | null; rawSeries: PublicPowerResp | null; stale: boolean }> {
|
||||||
try {
|
try {
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
const fmt = (sec: number) =>
|
const fmt = (sec: number) =>
|
||||||
new Date(sec * 1000).toISOString().slice(0, 16) + "Z";
|
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 end = fmt(nowSec);
|
||||||
const url =
|
const url =
|
||||||
`https://api.energy-charts.info/public_power?country=de` +
|
`https://api.energy-charts.info/public_power?country=de` +
|
||||||
`&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`;
|
`&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`;
|
||||||
const res = await fetcher(url, { signal: AbortSignal.timeout(8_000) });
|
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 d = (await res.json()) as PublicPowerResp;
|
||||||
const find = (n: string) =>
|
const find = (n: string) =>
|
||||||
d.production_types?.find((p) => p.name === n)?.data ?? [];
|
d.production_types?.find((p) => p.name === n)?.data ?? [];
|
||||||
const ts = d.unix_seconds ?? [];
|
const ts = d.unix_seconds ?? [];
|
||||||
return {
|
return {
|
||||||
|
rawSeries: d,
|
||||||
data: {
|
data: {
|
||||||
wind_onshore_mw: lastNonNull(find("Wind onshore")),
|
wind_onshore_mw: lastNonNull(find("Wind onshore")),
|
||||||
wind_offshore_mw: lastNonNull(find("Wind offshore")),
|
wind_offshore_mw: lastNonNull(find("Wind offshore")),
|
||||||
@@ -102,10 +103,36 @@ async function fetchEnergyChartsPublicPower(
|
|||||||
stale: false,
|
stale: false,
|
||||||
};
|
};
|
||||||
} catch {
|
} 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 = {
|
type CrossborderRaw = {
|
||||||
flow_at_gw?: number | null;
|
flow_at_gw?: number | null;
|
||||||
flow_be_gw?: number | null;
|
flow_be_gw?: number | null;
|
||||||
@@ -501,10 +528,10 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
|||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchSmardSnapshot(fetch),
|
fetchSmardSnapshot(fetch),
|
||||||
// Energy-Charts /public_power direct (with explicit time range —
|
// Energy-Charts /public_power direct (with explicit time range —
|
||||||
// upstream now returns 404 without start/end). Used as per-field
|
// upstream returns 404 without start/end). Window starts at midnight so
|
||||||
// fallback when SMARD's load filter (id 410) lags 12+ hours behind
|
// the full-day series doubles as the sparkline fallback when the CMS
|
||||||
// the production filters and the SMARD snapshot gets discarded.
|
// history backend is empty. Used as per-field fallback when SMARD lags.
|
||||||
fetchEnergyChartsPublicPower(fetch),
|
fetchEnergyChartsPublicPower(fetch, dayStartSec),
|
||||||
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
||||||
fetchPriceAtNow("DE-LU", fetch),
|
fetchPriceAtNow("DE-LU", fetch),
|
||||||
fetchPriceAtNow("FR", fetch),
|
fetchPriceAtNow("FR", fetch),
|
||||||
@@ -599,9 +626,14 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
|||||||
// Historical aggregates derived from CMS `_history`. `null` when the
|
// Historical aggregates derived from CMS `_history`. `null` when the
|
||||||
// backing collection is unavailable (e.g. CMS not yet upgraded with
|
// backing collection is unavailable (e.g. CMS not yet upgraded with
|
||||||
// history-mode schemas) — widget renders without the affected panels.
|
// 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 =
|
const todayHourly =
|
||||||
histStrommix && histStrommix.items.length > 0
|
histStrommix && histStrommix.items.length > 0
|
||||||
? buildTodayHourly(histStrommix.items)
|
? buildTodayHourly(histStrommix.items)
|
||||||
|
: energyCharts.rawSeries
|
||||||
|
? buildTodayHourlyFromEC(energyCharts.rawSeries, dayStartSec)
|
||||||
: null;
|
: null;
|
||||||
const negPriceHoursYtd = histPriceDe ? countNegPriceSlots(histPriceDe.items) : null;
|
const negPriceHoursYtd = histPriceDe ? countNegPriceSlots(histPriceDe.items) : null;
|
||||||
const crossborderYtd = histCrossborder
|
const crossborderYtd = histCrossborder
|
||||||
|
|||||||
Reference in New Issue
Block a user