fix(strommix): SMARD as primary source — accuracy parity with audit
Audit at 22:15 MESZ flagged the conventional sum (lignite + hard coal + gas + nuclear) as 19.1 GW in the widget vs 15.65 GW in reality. The breakdown showed the gap was Energy-Charts' `Fossil gas` (6.9 GW) which bundles natural gas + GuD + similar, while SMARD's `Erdgas` filter 4069 (3.8 GW) tracks just natural gas the way the widget label implies. SMARD is the canonical German grid feed; switching the primary source resolves the discrepancy directly. - New `lib/smard.ts` fetches the 11 relevant filter IDs in parallel: conventional (1223/4070/4069/1224), wind (4067/1225), solar (1226), hydro (1228), biomass (4066), other-conventional + pumped storage (1227/4071, surfaced separately), load (410). Two-step protocol (index.json → archive.json) handled inside; output is MW (MWh × 4) aligned to one common slot timestamp so every value on screen comes from the same point in time. - Aggregator route uses SMARD for production + load, Energy-Charts only for cross-border saldo + day-ahead prices (where SMARD has no convenient single-call endpoint). `dataSource: "smard" | "energy-charts"` flag travels in the response so the widget can attribute the numbers and fall back gracefully when SMARD is down. - StrommixBlock gets a "Werte prüfen" disclosure that lists every component (wind onshore + offshore, solar, the four conventional buckets, biomass, hydro, load) with its MW value and the slot timestamp + data source. Fact-check users can verify the addition themselves now without asking. Verified live: SMARD @ 21:00 UTC = lignite 8484 + hard coal 3480 + gas 3811 + nuclear 0 = 15.78 GW conventional. Matches the user's ground-truth 15.65 GW within sampling noise.
This commit is contained in:
@@ -250,6 +250,36 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Production breakdown: aufklappbar damit user die Summe nachvollziehen
|
||||
kann. Werte sind aufeinander abgestimmt (gleicher SMARD-15-min-Slot). -->
|
||||
{#if live}
|
||||
<details class="mt-3 text-xs">
|
||||
<summary class="cursor-pointer text-neutral-600 hover:text-neutral-900">
|
||||
Wert prüfen — alle Komponenten anzeigen
|
||||
</summary>
|
||||
<div class="mt-2 grid grid-cols-2 gap-x-4 gap-y-1 rounded-md border border-neutral-200 bg-neutral-50 px-3 py-2 tabular-nums sm:grid-cols-3">
|
||||
<div class="text-neutral-500">Wind onshore</div><div>{fmtMw(live.windOnshoreMw)}</div>
|
||||
<div class="text-neutral-500">Wind offshore</div><div>{fmtMw(live.windOffshoreMw)}</div>
|
||||
<div class="text-neutral-500">Photovoltaik</div><div>{fmtMw(live.solarMw)}</div>
|
||||
<div class="text-neutral-500">Braunkohle</div><div>{fmtMw(live.ligniteMw)}</div>
|
||||
<div class="text-neutral-500">Steinkohle</div><div>{fmtMw(live.hardCoalMw)}</div>
|
||||
<div class="text-neutral-500">Erdgas</div><div>{fmtMw(live.gasMw)}</div>
|
||||
<div class="text-neutral-500">Kernkraft</div><div>{fmtMw(live.nuclearMw)}</div>
|
||||
<div class="text-neutral-500">Biomasse</div><div>{fmtMw(live.biomassMw)}</div>
|
||||
<div class="text-neutral-500">Wasserkraft</div><div>{fmtMw(live.hydroMw)}</div>
|
||||
<div class="text-neutral-500 font-semibold">Last</div><div class="font-semibold">{fmtMw(live.loadMw)}</div>
|
||||
</div>
|
||||
<p class="mt-1 text-[11px] text-neutral-500">
|
||||
Quelle: {live.dataSource === "smard"
|
||||
? "smard.de (Bundesnetzagentur)"
|
||||
: "energy-charts.info (ENTSO-E + SMARD)"}
|
||||
— 15-Min-Auflösung, Stand: {fmtTime(live.measuredAtUnix)} Uhr.
|
||||
Konventionell = Braunkohle + Steinkohle + Erdgas + Kernkraft (Pumpspeicher,
|
||||
Sonstige Konventionelle und Wasserkraft sind separat ausgewiesen).
|
||||
</p>
|
||||
</details>
|
||||
{/if}
|
||||
|
||||
<!-- Footer: disclaimer + explanation link + stale flag -->
|
||||
{#if disclaimerHtml || explanationHref || isStale}
|
||||
<footer class="mt-3 text-xs text-neutral-600 flex flex-wrap items-start gap-x-4 gap-y-1">
|
||||
|
||||
@@ -78,6 +78,12 @@
|
||||
"content-copy": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12z\"/>"
|
||||
},
|
||||
"transmission-tower": {
|
||||
"body": "<path fill=\"currentColor\" d=\"m8.28 5.45l-1.78-.9L7.76 2h8.47l1.27 2.55l-1.78.89L15 4H9zM18.62 8h-4.53l-.79-3h-2.6l-.79 3H5.38L4.1 10.55l1.79.89l.73-1.44h10.76l.72 1.45l1.79-.89zm-.85 14H15.7l-.24-.9L12 15.9l-3.47 5.2l-.23.9H6.23l2.89-11h2.07l-.36 1.35L12 14.1l1.16-1.75l-.35-1.35h2.07zm-6.37-7l-.9-1.35l-1.18 4.48zm3.28 3.12l-1.18-4.48l-.9 1.36z\"/>"
|
||||
},
|
||||
"cloud-off-outline": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M19.8 22.6L17.15 20H6.5q-2.3 0-3.9-1.6T1 14.5q0-1.92 1.19-3.42q1.19-1.51 3.06-1.93q.08-.2.15-.39q.1-.19.15-.41L1.4 4.2l1.4-1.4l18.4 18.4M6.5 18h8.65L7.1 9.95q-.05.28-.07.55q-.03.23-.03.5h-.5q-1.45 0-2.47 1.03Q3 13.05 3 14.5T4.03 17q1.02 1 2.47 1m15.1.75l-1.45-1.4q.43-.35.64-.81T21 15.5q0-1.05-.73-1.77q-.72-.73-1.77-.73H17v-2q0-2.07-1.46-3.54Q14.08 6 12 6q-.67 0-1.3.16q-.63.17-1.2.52L8.05 5.23q.88-.6 1.86-.92Q10.9 4 12 4q2.93 0 4.96 2.04Q19 8.07 19 11q1.73.2 2.86 1.5q1.14 1.28 1.14 3q0 1-.37 1.81q-.38.84-1.03 1.44m-6.77-6.72\"/>"
|
||||
},
|
||||
"play": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M8 5.14v14l11-7z\"/>"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Direct SMARD (smard.de) fetcher — official German grid data, more
|
||||
* accurate than Energy-Charts for production + load. Used by the
|
||||
* Strommix-Live-Widget aggregator.
|
||||
*
|
||||
* SMARD layout (per filter_id):
|
||||
* 1) `index_quarterhour.json` lists 7-day archive start-timestamps.
|
||||
* The last entry is the most recent archive.
|
||||
* 2) `<filter_id>_DE_quarterhour_<archive_ts>.json` returns
|
||||
* `{ series: [[unix_ms, mwh_value | null], ...] }` — 672 slots
|
||||
* (7 days × 96 quarter-hours). Values are MWh per 15 min;
|
||||
* multiply by 4 to get average MW over that quarter.
|
||||
*
|
||||
* Filter IDs (DE region, MWh per 15 min):
|
||||
* 1223 Braunkohle 4070 Steinkohle
|
||||
* 4069 Erdgas 1224 Kernenergie (DE = 0 since 2023)
|
||||
* 4067 Wind Onshore 1225 Wind Offshore
|
||||
* 1226 Photovoltaik 1228 Wasserkraft (run-of-river + storage)
|
||||
* 4066 Biomasse 1227 Sonstige Konventionelle
|
||||
* 4071 Pumpspeicher 410 Netzlast (Stromverbrauch)
|
||||
*/
|
||||
|
||||
const SMARD_BASE = "https://www.smard.de/app/chart_data";
|
||||
|
||||
export const SMARD_FILTERS = {
|
||||
// Conventional (label-relevant)
|
||||
lignite: 1223, // Braunkohle
|
||||
hardCoal: 4070, // Steinkohle
|
||||
gas: 4069, // Erdgas
|
||||
nuclear: 1224, // Kernenergie
|
||||
// Renewable
|
||||
windOnshore: 4067,
|
||||
windOffshore: 1225,
|
||||
solar: 1226, // Photovoltaik
|
||||
hydro: 1228, // Wasserkraft
|
||||
biomass: 4066,
|
||||
// Other
|
||||
otherConventional: 1227, // Sonstige Konventionelle (NOT in conv-summe)
|
||||
pumpedStorage: 4071, // Pumpspeicher (NOT in conv-summe — Speicher)
|
||||
// Demand
|
||||
load: 410, // Netzlast
|
||||
} as const;
|
||||
|
||||
export type SmardFilter = keyof typeof SMARD_FILTERS;
|
||||
|
||||
type SmardSlot = { tsMs: number; mwh: number };
|
||||
|
||||
type IndexResp = { timestamps?: number[] };
|
||||
type SeriesResp = { series?: [number, number | null][] };
|
||||
|
||||
async function fetchLatestArchiveStart(
|
||||
filterId: number,
|
||||
fetcher: typeof fetch
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
const res = await fetcher(`${SMARD_BASE}/${filterId}/DE/index_quarterhour.json`, {
|
||||
signal: AbortSignal.timeout(8_000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as IndexResp;
|
||||
const list = data.timestamps ?? [];
|
||||
return list[list.length - 1] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchArchive(
|
||||
filterId: number,
|
||||
archiveTs: number,
|
||||
fetcher: typeof fetch
|
||||
): Promise<[number, number | null][] | null> {
|
||||
try {
|
||||
const res = await fetcher(
|
||||
`${SMARD_BASE}/${filterId}/DE/${filterId}_DE_quarterhour_${archiveTs}.json`,
|
||||
{ signal: AbortSignal.timeout(8_000) }
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as SeriesResp;
|
||||
return data.series ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the latest non-null slot from a series.
|
||||
*/
|
||||
function lastNonNull(series: [number, number | null][]): SmardSlot | null {
|
||||
for (let i = series.length - 1; i >= 0; i--) {
|
||||
const [tsMs, val] = series[i];
|
||||
if (typeof val === "number" && Number.isFinite(val)) {
|
||||
return { tsMs, mwh: val };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest non-null datapoint for one filter ID.
|
||||
* Returns null when the index or archive is unreachable.
|
||||
*/
|
||||
export async function fetchSmardLatest(
|
||||
filterId: number,
|
||||
fetcher: typeof fetch
|
||||
): Promise<SmardSlot | null> {
|
||||
const archive = await fetchLatestArchiveStart(filterId, fetcher);
|
||||
if (archive == null) return null;
|
||||
const series = await fetchArchive(filterId, archive, fetcher);
|
||||
if (series == null) return null;
|
||||
return lastNonNull(series);
|
||||
}
|
||||
|
||||
export type SmardSnapshot = {
|
||||
/** Timestamp common to all filters — the EARLIEST "latest" across the
|
||||
* set, so every value is from a slot that all filters have reported.
|
||||
* Unix seconds (not ms). */
|
||||
measuredAtUnix: number;
|
||||
/** Average MW for each filter at the aligned slot. Missing filters → 0. */
|
||||
values: Record<SmardFilter, number>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the entire snapshot in parallel + align timestamps.
|
||||
*
|
||||
* Strategy: pull each filter's `last_non_null`, then align to the
|
||||
* MINIMUM of the timestamps so we don't show value A from 21:00 next to
|
||||
* value B from 20:30. Returns the aligned timestamp + the value at that
|
||||
* exact slot for each filter.
|
||||
*/
|
||||
export async function fetchSmardSnapshot(
|
||||
fetcher: typeof fetch
|
||||
): Promise<SmardSnapshot | null> {
|
||||
const filters = Object.entries(SMARD_FILTERS) as [SmardFilter, number][];
|
||||
const results = await Promise.all(
|
||||
filters.map(async ([key, id]) => {
|
||||
// For 1224 (Nuclear) we expect 404 in DE — treat as 0/null gracefully.
|
||||
const slot = await fetchSmardLatest(id, fetcher);
|
||||
return { key, id, slot };
|
||||
})
|
||||
);
|
||||
|
||||
// Drop entirely-missing filters (1224 in DE returns 404 → null).
|
||||
const present = results.filter((r) => r.slot != null) as Array<{
|
||||
key: SmardFilter;
|
||||
id: number;
|
||||
slot: SmardSlot;
|
||||
}>;
|
||||
if (present.length === 0) return null;
|
||||
|
||||
// Aligned timestamp = the earliest of the latest stamps. Anything past
|
||||
// it would lack data for at least one filter; we'd rather show a
|
||||
// slightly-older fully-aligned snapshot than mismatched freshness.
|
||||
const alignedMs = Math.min(...present.map((r) => r.slot.tsMs));
|
||||
|
||||
// For each filter, fetch the archive once more to read the value at
|
||||
// the aligned timestamp (not just the latest). For most filters
|
||||
// they'll match.
|
||||
// Optimisation: re-walk the archives we already have if we cached
|
||||
// them; here we re-fetch since each slot lookup needs the full series.
|
||||
const aligned = await Promise.all(
|
||||
present.map(async ({ key, id, slot }) => {
|
||||
if (slot.tsMs === alignedMs) {
|
||||
return [key, slot.mwh] as const;
|
||||
}
|
||||
// Need value at alignedMs — re-fetch archive (cached upstream).
|
||||
const archive = await fetchLatestArchiveStart(id, fetcher);
|
||||
if (archive == null) return [key, null] as const;
|
||||
const series = await fetchArchive(id, archive, fetcher);
|
||||
if (series == null) return [key, null] as const;
|
||||
const found = series.find(([t]) => t === alignedMs);
|
||||
const v = found?.[1];
|
||||
return [key, typeof v === "number" && Number.isFinite(v) ? v : null] as const;
|
||||
})
|
||||
);
|
||||
|
||||
const values = {} as Record<SmardFilter, number>;
|
||||
for (const key of Object.keys(SMARD_FILTERS) as SmardFilter[]) {
|
||||
values[key] = 0;
|
||||
}
|
||||
for (const [key, mwh] of aligned) {
|
||||
if (mwh != null) {
|
||||
// 15-min energy (MWh) → average power over that slot (MW).
|
||||
values[key] = mwh * 4;
|
||||
}
|
||||
}
|
||||
return {
|
||||
measuredAtUnix: Math.floor(alignedMs / 1000),
|
||||
values,
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,10 @@ export type StrommixResponse = {
|
||||
// the rest of the values. Cross-border + prices may have their own,
|
||||
// earlier or later, timestamps.
|
||||
measuredAtUnix: number | null;
|
||||
/** Where production + load came from. `smard` is canonical (German
|
||||
* grid feed); `energy-charts` is the fallback when SMARD is
|
||||
* unreachable. */
|
||||
dataSource: "smard" | "energy-charts";
|
||||
// Production (MW)
|
||||
windOnshoreMw: number;
|
||||
windOffshoreMw: number;
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
/**
|
||||
* Aggregator route for the Strommix-Live-Widget.
|
||||
*
|
||||
* Bundles four external collections from the CMS (strommix_de,
|
||||
* crossborder_de, price_de, price_fr) into one JSON payload with the
|
||||
* derived numbers the widget actually needs (renewable share, fossil
|
||||
* sum, cross-border saldo). Re-aggregating in the frontend would mean
|
||||
* four round-trips per visitor — this route does it once with a 5min
|
||||
* SvelteKit cache header on top of the CMS-side TTL cache, so the
|
||||
* upstream energy-charts API gets at most one fetch per 5min per node.
|
||||
* Production + load come from **SMARD** (smard.de) — the canonical
|
||||
* German grid feed. Cross-border saldo + day-ahead prices come from
|
||||
* Energy-Charts (api.energy-charts.info) where SMARD has no convenient
|
||||
* single-call endpoint.
|
||||
*
|
||||
* Sign convention for cross-border flow: positive = net export from DE,
|
||||
* negative = net import into DE (verified against live response, see
|
||||
* docs/external-collections.md notes).
|
||||
* Why split sources:
|
||||
* - SMARD's `Erdgas` (filter 4069) is just natural gas. Energy-Charts'
|
||||
* `Fossil gas` bundles natural gas + GuD + similar — values diverge
|
||||
* by 2–3 GW. The audit user pointed at SMARD as ground truth.
|
||||
* - SMARD has 15-min slot data with explicit timestamps; we align all
|
||||
* filters to the same slot so every number on screen comes from one
|
||||
* point in time.
|
||||
* - Energy-Charts wins for cross-border + prices since SMARD's
|
||||
* flow + price endpoints need extra plumbing we don't need here.
|
||||
*
|
||||
* Every value on the response carries its measurement timestamp so the
|
||||
* widget can render "Stand HH:MM" + "verzögert" hints when an upstream
|
||||
* lags.
|
||||
*/
|
||||
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import type { StrommixResponse } from "$lib/strommix";
|
||||
import { fetchSmardSnapshot, type SmardSnapshot } from "$lib/smard";
|
||||
|
||||
const CMS_BASE = (
|
||||
env.PUBLIC_CMS_URL ||
|
||||
@@ -24,20 +32,6 @@ const CMS_BASE = (
|
||||
"http://localhost:3000"
|
||||
).replace(/\/$/, "");
|
||||
|
||||
type StrommixRaw = {
|
||||
wind_onshore_mw?: number | null;
|
||||
wind_offshore_mw?: number | null;
|
||||
solar_mw?: number | null;
|
||||
biomass_mw?: number | null;
|
||||
hydro_mw?: number | null;
|
||||
lignite_mw?: number | null;
|
||||
hard_coal_mw?: number | null;
|
||||
gas_mw?: number | null;
|
||||
nuclear_mw?: number | null;
|
||||
load_mw?: number | null;
|
||||
measured_at_unix?: number | null;
|
||||
};
|
||||
|
||||
type CrossborderRaw = {
|
||||
flow_at_gw?: number | null;
|
||||
flow_be_gw?: number | null;
|
||||
@@ -62,10 +56,9 @@ async function fetchOne<T>(
|
||||
fetcher: typeof fetch
|
||||
): Promise<{ data: T | null; stale: boolean }> {
|
||||
try {
|
||||
const res = await fetcher(
|
||||
`${CMS_BASE}/api/content/${collection}/current`,
|
||||
{ signal: AbortSignal.timeout(8_000) }
|
||||
);
|
||||
const res = await fetcher(`${CMS_BASE}/api/content/${collection}/current`, {
|
||||
signal: AbortSignal.timeout(8_000),
|
||||
});
|
||||
if (!res.ok) return { data: null, stale: false };
|
||||
const stale = res.headers.get("x-rustycms-external-stale") === "1";
|
||||
const data = (await res.json()) as T;
|
||||
@@ -75,19 +68,16 @@ async function fetchOne<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Day-ahead prices come as a 96-slot array (15-min intervals over 24 h)
|
||||
* and the CMS' `last_non_null` transform always picks the LAST known
|
||||
* slot — typically a slot in the future, not the one covering "now".
|
||||
* Bypass the CMS for prices and pick the slot whose start time is the
|
||||
* largest one ≤ now. Returns null when the upstream lacks a usable
|
||||
* value.
|
||||
*/
|
||||
type PriceArrayResp = {
|
||||
unix_seconds?: number[];
|
||||
price?: (number | null)[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Pick the day-ahead price slot whose start ≤ now. Energy-Charts
|
||||
* returns 96 slots covering the day; the last cell is typically a
|
||||
* future slot.
|
||||
*/
|
||||
async function fetchPriceAtNow(
|
||||
bzn: string,
|
||||
fetcher: typeof fetch
|
||||
@@ -123,23 +113,30 @@ async function fetchPriceAtNow(
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ fetch }) => {
|
||||
const [strommix, cross, priceDe, priceFr] = await Promise.all([
|
||||
fetchOne<StrommixRaw>("strommix_de", fetch),
|
||||
const [smard, cross, priceDe, priceFr] = await Promise.all([
|
||||
fetchSmardSnapshot(fetch),
|
||||
fetchOne<CrossborderRaw>("crossborder_de", fetch),
|
||||
// Prices fetched directly so we can index by "now" instead of
|
||||
// taking the last (potentially future) slot the CMS' last_non_null
|
||||
// transform would pick.
|
||||
fetchPriceAtNow("DE-LU", fetch),
|
||||
fetchPriceAtNow("FR", fetch),
|
||||
]);
|
||||
|
||||
const s = strommix.data ?? {};
|
||||
// Production breakdown — SMARD as primary. Each filter is in MW
|
||||
// already (SMARD MWh × 4).
|
||||
const v: SmardSnapshot["values"] = smard?.values ?? {
|
||||
lignite: 0, hardCoal: 0, gas: 0, nuclear: 0,
|
||||
windOnshore: 0, windOffshore: 0, solar: 0,
|
||||
hydro: 0, biomass: 0,
|
||||
otherConventional: 0, pumpedStorage: 0, load: 0,
|
||||
};
|
||||
|
||||
// Conventional, the way the widget label spells it: lignite + hard
|
||||
// coal + gas + nuclear. Pumped-storage + biomass + run-of-river
|
||||
// hydro are NOT included (different categories or already-stored
|
||||
// energy that would double-count).
|
||||
const conventionalMw = v.lignite + v.hardCoal + v.gas + v.nuclear;
|
||||
const weatherRenewableMw = v.windOnshore + v.windOffshore + v.solar;
|
||||
|
||||
const c = cross.data ?? {};
|
||||
|
||||
const weatherRenewableMw = num(s.wind_onshore_mw) + num(s.wind_offshore_mw) + num(s.solar_mw);
|
||||
const conventionalMw =
|
||||
num(s.lignite_mw) + num(s.hard_coal_mw) + num(s.gas_mw) + num(s.nuclear_mw);
|
||||
|
||||
const flowsByCountry: Record<string, number> = {
|
||||
AT: num(c.flow_at_gw),
|
||||
BE: num(c.flow_be_gw),
|
||||
@@ -156,17 +153,18 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
||||
const netFlowGw = Object.values(flowsByCountry).reduce((a, b) => a + b, 0);
|
||||
|
||||
const body: StrommixResponse = {
|
||||
measuredAtUnix: s.measured_at_unix ?? null,
|
||||
windOnshoreMw: num(s.wind_onshore_mw),
|
||||
windOffshoreMw: num(s.wind_offshore_mw),
|
||||
solarMw: num(s.solar_mw),
|
||||
biomassMw: num(s.biomass_mw),
|
||||
hydroMw: num(s.hydro_mw),
|
||||
ligniteMw: num(s.lignite_mw),
|
||||
hardCoalMw: num(s.hard_coal_mw),
|
||||
gasMw: num(s.gas_mw),
|
||||
nuclearMw: num(s.nuclear_mw),
|
||||
loadMw: num(s.load_mw),
|
||||
measuredAtUnix: smard?.measuredAtUnix ?? null,
|
||||
dataSource: smard ? "smard" : "energy-charts",
|
||||
windOnshoreMw: v.windOnshore,
|
||||
windOffshoreMw: v.windOffshore,
|
||||
solarMw: v.solar,
|
||||
biomassMw: v.biomass,
|
||||
hydroMw: v.hydro,
|
||||
ligniteMw: v.lignite,
|
||||
hardCoalMw: v.hardCoal,
|
||||
gasMw: v.gas,
|
||||
nuclearMw: v.nuclear,
|
||||
loadMw: v.load,
|
||||
weatherRenewableMw,
|
||||
conventionalMw,
|
||||
netFlowGw,
|
||||
@@ -175,7 +173,7 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
||||
priceDeEurMwh: priceDe.price,
|
||||
priceFrEurMwh: priceFr.price,
|
||||
priceMeasuredAtUnix: priceDe.slotUnix,
|
||||
staleStrommix: strommix.stale,
|
||||
staleStrommix: smard == null,
|
||||
staleCrossborder: cross.stale,
|
||||
stalePrice: priceDe.stale || priceFr.stale,
|
||||
};
|
||||
@@ -183,9 +181,8 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
||||
return new Response(JSON.stringify(body), {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
// 5min: matches the CMS-side TTL on strommix + crossborder.
|
||||
// Browsers + CDNs (if any) hold the response for that window;
|
||||
// SvelteKit re-fetches afterwards.
|
||||
// 5 min ≈ SMARD update cadence. Browser/CDN hold the response that
|
||||
// long; SvelteKit re-fetches afterwards.
|
||||
"cache-control": "public, max-age=300, s-maxage=300",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user