diff --git a/src/lib/components/blocks/StrommixBlock.svelte b/src/lib/components/blocks/StrommixBlock.svelte index 72ca55d..7e5bf82 100644 --- a/src/lib/components/blocks/StrommixBlock.svelte +++ b/src/lib/components/blocks/StrommixBlock.svelte @@ -186,18 +186,25 @@ {:else if !live}
Lade Live-Daten…
- {:else if renewablePct == null} -
Keine gültigen Live-Werte verfügbar.
{:else}
-
- {Math.round(renewablePct)}% -
-
Wind & Solar am Bedarf
-
→ {statusLabel}
- {#if argumentHtml} -
- {@html argumentHtml} + {#if renewablePct != null} +
+ {Math.round(renewablePct)}% +
+
Wind & Solar am Bedarf
+
→ {statusLabel}
+ {#if argumentHtml} +
+ {@html argumentHtml} +
+ {/if} + {:else} + +
+ Lastwert von SMARD gerade nicht verfügbar — Prozentanzeige ausgesetzt. + Erzeugung wird unten in der Detailansicht weiter angezeigt.
{/if}
@@ -270,9 +277,14 @@
Last
{fmtMw(live.loadMw)}

- Quelle: {live.dataSource === "smard" - ? "smard.de (Bundesnetzagentur)" - : "energy-charts.info (ENTSO-E + SMARD)"} + Quelle: + {#if live.dataSource === "smard"} + smard.de (Bundesnetzagentur) + {:else if live.dataSource === "smard+energy-charts"} + smard.de + energy-charts.info (Last-Wert ergänzt) + {:else} + energy-charts.info (ENTSO-E + SMARD) + {/if} — 15-Min-Auflösung, Stand: {fmtTime(live.measuredAtUnix)} Uhr. Konventionell = Braunkohle + Steinkohle + Erdgas + Kernkraft (Pumpspeicher, Sonstige Konventionelle und Wasserkraft sind separat ausgewiesen). diff --git a/src/lib/smard.ts b/src/lib/smard.ts index 62fe6c8..b648984 100644 --- a/src/lib/smard.ts +++ b/src/lib/smard.ts @@ -116,8 +116,12 @@ export type SmardSnapshot = { * 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; + /** Average MW for each filter at the aligned slot. `null` means the + * filter could not be resolved (404, all-null series, fetch error) + * — distinguished from a genuine zero so callers can choose a + * fallback source per-field instead of ignoring real zeroes + * (e.g. solar at night, nuclear in DE). */ + values: Record; }; /** @@ -174,9 +178,9 @@ export async function fetchSmardSnapshot( }) ); - const values = {} as Record; + const values = {} as Record; for (const key of Object.keys(SMARD_FILTERS) as SmardFilter[]) { - values[key] = 0; + values[key] = null; } for (const [key, mwh] of aligned) { if (mwh != null) { diff --git a/src/lib/strommix.ts b/src/lib/strommix.ts index 334f540..1f469c9 100644 --- a/src/lib/strommix.ts +++ b/src/lib/strommix.ts @@ -11,10 +11,13 @@ 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"; + /** Where production + load came from. `smard` = pure SMARD (canonical); + * `smard+energy-charts` = SMARD with Energy-Charts as per-field + * fallback for filters SMARD couldn't resolve at the snapshot's slot + * (typically the load filter when SMARD's pipeline lags more than + * the production filters); `energy-charts` = SMARD entirely + * unreachable, full fallback. */ + dataSource: "smard" | "smard+energy-charts" | "energy-charts"; // Production (MW) windOnshoreMw: number; windOffshoreMw: number; diff --git a/src/routes/api/strommix/+server.ts b/src/routes/api/strommix/+server.ts index 1add633..ae40176 100644 --- a/src/routes/api/strommix/+server.ts +++ b/src/routes/api/strommix/+server.ts @@ -24,7 +24,7 @@ import type { RequestHandler } from "./$types"; import { env } from "$env/dynamic/public"; import type { StrommixResponse } from "$lib/strommix"; -import { fetchSmardSnapshot, type SmardSnapshot } from "$lib/smard"; +import { fetchSmardSnapshot } from "$lib/smard"; const CMS_BASE = ( env.PUBLIC_CMS_URL || @@ -32,6 +32,20 @@ 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; @@ -113,26 +127,53 @@ async function fetchPriceAtNow( } export const GET: RequestHandler = async ({ fetch }) => { - const [smard, cross, priceDe, priceFr] = await Promise.all([ + const [smard, energyCharts, cross, priceDe, priceFr] = await Promise.all([ fetchSmardSnapshot(fetch), + // Energy-Charts via the CMS — used as per-field fallback when SMARD + // returns null for a specific filter (notably load 410 when SMARD's + // pipeline lags more than the production filters). + fetchOne("strommix_de", fetch), fetchOne("crossborder_de", fetch), fetchPriceAtNow("DE-LU", fetch), fetchPriceAtNow("FR", fetch), ]); - // 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, + const ec = energyCharts.data ?? {}; + + // Per-field fallback. SMARD value `null` = filter unavailable; we + // reach for the Energy-Charts equivalent. SMARD `0` is preserved + // (genuine night-time solar / decommissioned nuclear / etc). + const pick = (smardVal: number | null | undefined, ecVal: number | null | undefined): number => + typeof smardVal === "number" + ? smardVal + : typeof ecVal === "number" && Number.isFinite(ecVal) + ? ecVal + : 0; + + const v = { + lignite: pick(smard?.values.lignite, ec.lignite_mw), + hardCoal: pick(smard?.values.hardCoal, ec.hard_coal_mw), + gas: pick(smard?.values.gas, ec.gas_mw), + nuclear: pick(smard?.values.nuclear, ec.nuclear_mw), + windOnshore: pick(smard?.values.windOnshore, ec.wind_onshore_mw), + windOffshore: pick(smard?.values.windOffshore, ec.wind_offshore_mw), + solar: pick(smard?.values.solar, ec.solar_mw), + hydro: pick(smard?.values.hydro, ec.hydro_mw), + biomass: pick(smard?.values.biomass, ec.biomass_mw), + load: pick(smard?.values.load, ec.load_mw), }; + // Track whether any field needed the Energy-Charts fallback so the + // widget can attribute the source honestly. + const usedFallback = + !smard || + (Object.keys(v) as (keyof typeof v)[]).some( + (k) => smard.values[k as keyof typeof smard.values] == null && (v[k] ?? 0) > 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). + // hydro are NOT included. const conventionalMw = v.lignite + v.hardCoal + v.gas + v.nuclear; const weatherRenewableMw = v.windOnshore + v.windOffshore + v.solar; @@ -153,8 +194,8 @@ export const GET: RequestHandler = async ({ fetch }) => { const netFlowGw = Object.values(flowsByCountry).reduce((a, b) => a + b, 0); const body: StrommixResponse = { - measuredAtUnix: smard?.measuredAtUnix ?? null, - dataSource: smard ? "smard" : "energy-charts", + measuredAtUnix: smard?.measuredAtUnix ?? ec.measured_at_unix ?? null, + dataSource: !smard ? "energy-charts" : usedFallback ? "smard+energy-charts" : "smard", windOnshoreMw: v.windOnshore, windOffshoreMw: v.windOffshore, solarMw: v.solar, @@ -173,7 +214,7 @@ export const GET: RequestHandler = async ({ fetch }) => { priceDeEurMwh: priceDe.price, priceFrEurMwh: priceFr.price, priceMeasuredAtUnix: priceDe.slotUnix, - staleStrommix: smard == null, + staleStrommix: smard == null && !energyCharts.data, staleCrossborder: cross.stale, stalePrice: priceDe.stale || priceFr.stale, };