@iflow-mcp/kitfunso-luminus 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +488 -0
- package/dist/lib/audit.d.ts +3 -0
- package/dist/lib/audit.js +66 -0
- package/dist/lib/auth.d.ts +26 -0
- package/dist/lib/auth.js +199 -0
- package/dist/lib/cache.d.ts +25 -0
- package/dist/lib/cache.js +38 -0
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.js +10 -0
- package/dist/lib/corine.d.ts +31 -0
- package/dist/lib/corine.js +137 -0
- package/dist/lib/eea-natura2000.d.ts +7 -0
- package/dist/lib/eea-natura2000.js +53 -0
- package/dist/lib/entsoe-client.d.ts +22 -0
- package/dist/lib/entsoe-client.js +69 -0
- package/dist/lib/gis-sources.d.ts +33 -0
- package/dist/lib/gis-sources.js +392 -0
- package/dist/lib/natural-england.d.ts +27 -0
- package/dist/lib/natural-england.js +105 -0
- package/dist/lib/neso-gsp.d.ts +18 -0
- package/dist/lib/neso-gsp.js +113 -0
- package/dist/lib/overpass.d.ts +13 -0
- package/dist/lib/overpass.js +193 -0
- package/dist/lib/profiles.d.ts +23 -0
- package/dist/lib/profiles.js +149 -0
- package/dist/lib/schema-guard.d.ts +22 -0
- package/dist/lib/schema-guard.js +38 -0
- package/dist/lib/tool-handler.d.ts +15 -0
- package/dist/lib/tool-handler.js +95 -0
- package/dist/lib/xml-parser.d.ts +4 -0
- package/dist/lib/xml-parser.js +34 -0
- package/dist/lib/zone-codes.d.ts +12 -0
- package/dist/lib/zone-codes.js +127 -0
- package/dist/tools/acer-remit.d.ts +60 -0
- package/dist/tools/acer-remit.js +154 -0
- package/dist/tools/agricultural-land.d.ts +31 -0
- package/dist/tools/agricultural-land.js +210 -0
- package/dist/tools/ancillary-prices.d.ts +27 -0
- package/dist/tools/ancillary-prices.js +70 -0
- package/dist/tools/auctions.d.ts +15 -0
- package/dist/tools/auctions.js +89 -0
- package/dist/tools/balancing-actions.d.ts +22 -0
- package/dist/tools/balancing-actions.js +151 -0
- package/dist/tools/balancing.d.ts +21 -0
- package/dist/tools/balancing.js +56 -0
- package/dist/tools/carbon.d.ts +21 -0
- package/dist/tools/carbon.js +68 -0
- package/dist/tools/commodity-prices.d.ts +26 -0
- package/dist/tools/commodity-prices.js +100 -0
- package/dist/tools/compare-sites.d.ts +41 -0
- package/dist/tools/compare-sites.js +237 -0
- package/dist/tools/demand-forecast.d.ts +21 -0
- package/dist/tools/demand-forecast.js +56 -0
- package/dist/tools/elexon-bmrs.d.ts +72 -0
- package/dist/tools/elexon-bmrs.js +117 -0
- package/dist/tools/energi-data.d.ts +72 -0
- package/dist/tools/energi-data.js +170 -0
- package/dist/tools/energy-charts.d.ts +103 -0
- package/dist/tools/energy-charts.js +411 -0
- package/dist/tools/entsog.d.ts +71 -0
- package/dist/tools/entsog.js +159 -0
- package/dist/tools/era5-weather.d.ts +39 -0
- package/dist/tools/era5-weather.js +117 -0
- package/dist/tools/eu-gas-price.d.ts +38 -0
- package/dist/tools/eu-gas-price.js +110 -0
- package/dist/tools/fingrid.d.ts +39 -0
- package/dist/tools/fingrid.js +158 -0
- package/dist/tools/flood-risk.d.ts +33 -0
- package/dist/tools/flood-risk.js +166 -0
- package/dist/tools/flows.d.ts +23 -0
- package/dist/tools/flows.js +61 -0
- package/dist/tools/frequency.d.ts +10 -0
- package/dist/tools/frequency.js +35 -0
- package/dist/tools/gas-storage.d.ts +18 -0
- package/dist/tools/gas-storage.js +72 -0
- package/dist/tools/generation.d.ts +17 -0
- package/dist/tools/generation.js +80 -0
- package/dist/tools/grid-connection-intelligence.d.ts +42 -0
- package/dist/tools/grid-connection-intelligence.js +122 -0
- package/dist/tools/grid-connection-queue.d.ts +64 -0
- package/dist/tools/grid-connection-queue.js +198 -0
- package/dist/tools/grid-proximity.d.ts +38 -0
- package/dist/tools/grid-proximity.js +123 -0
- package/dist/tools/hydro-inflows.d.ts +34 -0
- package/dist/tools/hydro-inflows.js +114 -0
- package/dist/tools/hydro.d.ts +18 -0
- package/dist/tools/hydro.js +85 -0
- package/dist/tools/imbalance-prices.d.ts +21 -0
- package/dist/tools/imbalance-prices.js +56 -0
- package/dist/tools/intraday-prices.d.ts +21 -0
- package/dist/tools/intraday-prices.js +57 -0
- package/dist/tools/intraday-spread.d.ts +24 -0
- package/dist/tools/intraday-spread.js +55 -0
- package/dist/tools/land-constraints.d.ts +25 -0
- package/dist/tools/land-constraints.js +148 -0
- package/dist/tools/land-cover.d.ts +18 -0
- package/dist/tools/land-cover.js +64 -0
- package/dist/tools/lng-terminals.d.ts +22 -0
- package/dist/tools/lng-terminals.js +75 -0
- package/dist/tools/net-positions.d.ts +19 -0
- package/dist/tools/net-positions.js +74 -0
- package/dist/tools/nordpool-prices.d.ts +29 -0
- package/dist/tools/nordpool-prices.js +80 -0
- package/dist/tools/outages.d.ts +28 -0
- package/dist/tools/outages.js +107 -0
- package/dist/tools/power-plants.d.ts +26 -0
- package/dist/tools/power-plants.js +224 -0
- package/dist/tools/price-spread-analysis.d.ts +27 -0
- package/dist/tools/price-spread-analysis.js +97 -0
- package/dist/tools/prices.d.ts +23 -0
- package/dist/tools/prices.js +79 -0
- package/dist/tools/realtime-generation.d.ts +19 -0
- package/dist/tools/realtime-generation.js +141 -0
- package/dist/tools/ree-esios.d.ts +78 -0
- package/dist/tools/ree-esios.js +216 -0
- package/dist/tools/regelleistung.d.ts +28 -0
- package/dist/tools/regelleistung.js +71 -0
- package/dist/tools/remit-messages.d.ts +23 -0
- package/dist/tools/remit-messages.js +110 -0
- package/dist/tools/renewable-forecast.d.ts +23 -0
- package/dist/tools/renewable-forecast.js +75 -0
- package/dist/tools/rte-france.d.ts +72 -0
- package/dist/tools/rte-france.js +147 -0
- package/dist/tools/screen-site.d.ts +50 -0
- package/dist/tools/screen-site.js +288 -0
- package/dist/tools/site-revenue.d.ts +50 -0
- package/dist/tools/site-revenue.js +147 -0
- package/dist/tools/smard-data.d.ts +34 -0
- package/dist/tools/smard-data.js +155 -0
- package/dist/tools/solar.d.ts +23 -0
- package/dist/tools/solar.js +69 -0
- package/dist/tools/stormglass.d.ts +56 -0
- package/dist/tools/stormglass.js +172 -0
- package/dist/tools/terna.d.ts +69 -0
- package/dist/tools/terna.js +159 -0
- package/dist/tools/terrain-analysis.d.ts +19 -0
- package/dist/tools/terrain-analysis.js +120 -0
- package/dist/tools/transfer-capacity.d.ts +22 -0
- package/dist/tools/transfer-capacity.js +61 -0
- package/dist/tools/transmission.d.ts +29 -0
- package/dist/tools/transmission.js +159 -0
- package/dist/tools/uk-carbon.d.ts +51 -0
- package/dist/tools/uk-carbon.js +109 -0
- package/dist/tools/uk-grid.d.ts +28 -0
- package/dist/tools/uk-grid.js +70 -0
- package/dist/tools/us-gas.d.ts +30 -0
- package/dist/tools/us-gas.js +100 -0
- package/dist/tools/verify-gis-sources.d.ts +25 -0
- package/dist/tools/verify-gis-sources.js +119 -0
- package/dist/tools/weather.d.ts +27 -0
- package/dist/tools/weather.js +120 -0
- package/package.json +62 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const API_BASE = "https://api.energidataservice.dk/dataset";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
const DATASETS = [
|
|
6
|
+
"co2_emissions",
|
|
7
|
+
"electricity_production",
|
|
8
|
+
"electricity_prices",
|
|
9
|
+
"electricity_balance",
|
|
10
|
+
];
|
|
11
|
+
export const energiDataSchema = z.object({
|
|
12
|
+
dataset: z
|
|
13
|
+
.enum(DATASETS)
|
|
14
|
+
.describe('"co2_emissions" = real-time CO2 emissions intensity for DK1/DK2 (gCO2/kWh). ' +
|
|
15
|
+
'"electricity_production" = Danish electricity production by source (MW). ' +
|
|
16
|
+
'"electricity_prices" = day-ahead spot prices for DK1/DK2 (DKK & EUR/MWh). ' +
|
|
17
|
+
'"electricity_balance" = production, consumption, import/export balance.'),
|
|
18
|
+
zone: z
|
|
19
|
+
.enum(["DK1", "DK2", "DK"])
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("DK1 = West Denmark, DK2 = East Denmark, DK = both. Defaults to DK."),
|
|
22
|
+
date: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
26
|
+
});
|
|
27
|
+
const DATASET_NAMES = {
|
|
28
|
+
co2_emissions: "CO2Emis",
|
|
29
|
+
electricity_production: "ElectricityProdex5MinRealtime",
|
|
30
|
+
electricity_prices: "Elspotprices",
|
|
31
|
+
electricity_balance: "ElectricityBalanceNonv",
|
|
32
|
+
};
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
async function fetchEnergiData(datasetName, filter, limit = 48) {
|
|
35
|
+
const url = `${API_BASE}/${datasetName}?limit=${limit}&offset=0&sort=Minutes5UTC%20DESC` +
|
|
36
|
+
`&filter=${encodeURIComponent(filter)}`;
|
|
37
|
+
const cached = cache.get(url);
|
|
38
|
+
if (cached)
|
|
39
|
+
return cached.records;
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
headers: { Accept: "application/json" },
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
// Try alternative sort column
|
|
45
|
+
const altUrl = `${API_BASE}/${datasetName}?limit=${limit}&offset=0&sort=HourUTC%20DESC` +
|
|
46
|
+
`&filter=${encodeURIComponent(filter)}`;
|
|
47
|
+
const altResponse = await fetch(altUrl, {
|
|
48
|
+
headers: { Accept: "application/json" },
|
|
49
|
+
});
|
|
50
|
+
if (!altResponse.ok) {
|
|
51
|
+
const body = await altResponse.text();
|
|
52
|
+
throw new Error(`Energi Data Service returned ${altResponse.status}: ${body.slice(0, 300)}`);
|
|
53
|
+
}
|
|
54
|
+
const altJson = await altResponse.json();
|
|
55
|
+
cache.set(url, altJson, TTL.REALTIME);
|
|
56
|
+
return altJson.records ?? [];
|
|
57
|
+
}
|
|
58
|
+
const json = await response.json();
|
|
59
|
+
cache.set(url, json, TTL.REALTIME);
|
|
60
|
+
return json.records ?? [];
|
|
61
|
+
}
|
|
62
|
+
async function fetchCo2(zone, date) {
|
|
63
|
+
const filter = zone === "DK"
|
|
64
|
+
? `{"Minutes5UTC": {"$gte": "${date}T00:00"}}`
|
|
65
|
+
: `{"PriceArea": "${zone}", "Minutes5UTC": {"$gte": "${date}T00:00"}}`;
|
|
66
|
+
const records = await fetchEnergiData("CO2Emis", filter, 48);
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
const parsed = records.map((r) => ({
|
|
69
|
+
timestamp: r.Minutes5UTC ?? r.Minutes5DK ?? "",
|
|
70
|
+
zone: r.PriceArea ?? zone,
|
|
71
|
+
co2_gkwh: Math.round((Number(r.CO2Emission ?? 0)) * 10) / 10,
|
|
72
|
+
}));
|
|
73
|
+
return {
|
|
74
|
+
dataset: "co2_emissions",
|
|
75
|
+
source: "Energi Data Service (Energinet)",
|
|
76
|
+
zone,
|
|
77
|
+
records: parsed,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function fetchProduction(zone, date) {
|
|
81
|
+
const filter = zone === "DK"
|
|
82
|
+
? `{"Minutes5UTC": {"$gte": "${date}T00:00"}}`
|
|
83
|
+
: `{"PriceArea": "${zone}", "Minutes5UTC": {"$gte": "${date}T00:00"}}`;
|
|
84
|
+
const records = await fetchEnergiData("ElectricityProdex5MinRealtime", filter, 48);
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
const parsed = records.map((r) => {
|
|
87
|
+
const windOn = Number(r.OnshoreWindPower ?? r.OnshoreWindGe100MW_MWh ?? 0);
|
|
88
|
+
const windOff = Number(r.OffshoreWindPower ?? r.OffshoreWindGe100MW_MWh ?? 0);
|
|
89
|
+
const solar = Number(r.SolarPower ?? r.SolarGe100MW_MWh ?? 0);
|
|
90
|
+
const thermal = Number(r.ThermalPower ?? r.CentralPowerMWh ?? 0);
|
|
91
|
+
const hydro = Number(r.HydroPower ?? 0);
|
|
92
|
+
return {
|
|
93
|
+
timestamp: r.Minutes5UTC ?? r.Minutes5DK ?? "",
|
|
94
|
+
zone: r.PriceArea ?? zone,
|
|
95
|
+
wind_onshore_mw: Math.round(windOn),
|
|
96
|
+
wind_offshore_mw: Math.round(windOff),
|
|
97
|
+
solar_mw: Math.round(solar),
|
|
98
|
+
thermal_mw: Math.round(thermal),
|
|
99
|
+
hydro_mw: Math.round(hydro),
|
|
100
|
+
total_mw: Math.round(windOn + windOff + solar + thermal + hydro),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
dataset: "electricity_production",
|
|
105
|
+
source: "Energi Data Service (Energinet)",
|
|
106
|
+
zone,
|
|
107
|
+
records: parsed,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function fetchPrices(zone, date) {
|
|
111
|
+
const filter = zone === "DK"
|
|
112
|
+
? `{"HourUTC": {"$gte": "${date}T00:00"}}`
|
|
113
|
+
: `{"PriceArea": "${zone}", "HourUTC": {"$gte": "${date}T00:00"}}`;
|
|
114
|
+
const records = await fetchEnergiData("Elspotprices", filter, 48);
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
const parsed = records.map((r) => ({
|
|
117
|
+
timestamp: r.HourUTC ?? r.HourDK ?? "",
|
|
118
|
+
zone: r.PriceArea ?? zone,
|
|
119
|
+
price_eur_mwh: Math.round((Number(r.SpotPriceEUR ?? 0)) * 100) / 100,
|
|
120
|
+
price_dkk_mwh: Math.round((Number(r.SpotPriceDKK ?? 0)) * 100) / 100,
|
|
121
|
+
}));
|
|
122
|
+
return {
|
|
123
|
+
dataset: "electricity_prices",
|
|
124
|
+
source: "Energi Data Service (Energinet)",
|
|
125
|
+
zone,
|
|
126
|
+
records: parsed,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function fetchBalance(zone, date) {
|
|
130
|
+
const filter = zone === "DK"
|
|
131
|
+
? `{"HourUTC": {"$gte": "${date}T00:00"}}`
|
|
132
|
+
: `{"PriceArea": "${zone}", "HourUTC": {"$gte": "${date}T00:00"}}`;
|
|
133
|
+
const records = await fetchEnergiData("ElectricityBalanceNonv", filter, 48);
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
135
|
+
const parsed = records.map((r) => {
|
|
136
|
+
const prod = Number(r.ProductionGe100MW ?? r.GrossCon ?? 0);
|
|
137
|
+
const cons = Number(r.GrossCon ?? 0);
|
|
138
|
+
const imp = Number(r.ExchangeContinent ?? 0) > 0 ? Number(r.ExchangeContinent ?? 0) : 0;
|
|
139
|
+
const exp = Number(r.ExchangeContinent ?? 0) < 0 ? Math.abs(Number(r.ExchangeContinent ?? 0)) : 0;
|
|
140
|
+
return {
|
|
141
|
+
timestamp: r.HourUTC ?? r.HourDK ?? "",
|
|
142
|
+
zone: r.PriceArea ?? zone,
|
|
143
|
+
production_mwh: Math.round(prod),
|
|
144
|
+
consumption_mwh: Math.round(cons),
|
|
145
|
+
import_mwh: Math.round(imp),
|
|
146
|
+
export_mwh: Math.round(exp),
|
|
147
|
+
net_exchange_mwh: Math.round(Number(r.ExchangeContinent ?? 0)),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
dataset: "electricity_balance",
|
|
152
|
+
source: "Energi Data Service (Energinet)",
|
|
153
|
+
zone,
|
|
154
|
+
records: parsed,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export async function getEnergiData(params) {
|
|
158
|
+
const zone = params.zone ?? "DK";
|
|
159
|
+
const date = params.date ?? new Date().toISOString().slice(0, 10);
|
|
160
|
+
switch (params.dataset) {
|
|
161
|
+
case "co2_emissions":
|
|
162
|
+
return fetchCo2(zone, date);
|
|
163
|
+
case "electricity_production":
|
|
164
|
+
return fetchProduction(zone, date);
|
|
165
|
+
case "electricity_prices":
|
|
166
|
+
return fetchPrices(zone, date);
|
|
167
|
+
case "electricity_balance":
|
|
168
|
+
return fetchBalance(zone, date);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const energyChartsSchema: z.ZodObject<{
|
|
3
|
+
dataset: z.ZodEnum<{
|
|
4
|
+
generation: "generation";
|
|
5
|
+
prices: "prices";
|
|
6
|
+
flows: "flows";
|
|
7
|
+
demand: "demand";
|
|
8
|
+
signal: "signal";
|
|
9
|
+
installed_capacity: "installed_capacity";
|
|
10
|
+
}>;
|
|
11
|
+
zone: z.ZodString;
|
|
12
|
+
start_date: z.ZodOptional<z.ZodString>;
|
|
13
|
+
end_date: z.ZodOptional<z.ZodString>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
interface PricePoint15min {
|
|
16
|
+
timestamp: string;
|
|
17
|
+
price: number;
|
|
18
|
+
}
|
|
19
|
+
interface PricePointHourly {
|
|
20
|
+
hour: number;
|
|
21
|
+
price: number;
|
|
22
|
+
}
|
|
23
|
+
interface PricesResult {
|
|
24
|
+
zone: string;
|
|
25
|
+
start_date: string;
|
|
26
|
+
end_date: string;
|
|
27
|
+
source: string;
|
|
28
|
+
resolution: string;
|
|
29
|
+
prices_15min: PricePoint15min[];
|
|
30
|
+
prices_hourly: PricePointHourly[];
|
|
31
|
+
stats: {
|
|
32
|
+
min: number;
|
|
33
|
+
max: number;
|
|
34
|
+
mean: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
interface GenerationEntry {
|
|
38
|
+
fuel: string;
|
|
39
|
+
generation_mw: number;
|
|
40
|
+
}
|
|
41
|
+
interface GenerationResult {
|
|
42
|
+
zone: string;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
source: string;
|
|
45
|
+
generation: GenerationEntry[];
|
|
46
|
+
total_mw: number;
|
|
47
|
+
renewable_pct: number;
|
|
48
|
+
}
|
|
49
|
+
interface FlowEntry {
|
|
50
|
+
country: string;
|
|
51
|
+
flow_mw: number;
|
|
52
|
+
}
|
|
53
|
+
interface FlowsResult {
|
|
54
|
+
zone: string;
|
|
55
|
+
source: string;
|
|
56
|
+
flows: FlowEntry[];
|
|
57
|
+
net_position_mw: number;
|
|
58
|
+
}
|
|
59
|
+
interface DemandHourly {
|
|
60
|
+
hour: number;
|
|
61
|
+
demand_mw: number;
|
|
62
|
+
}
|
|
63
|
+
interface DemandResult {
|
|
64
|
+
zone: string;
|
|
65
|
+
source: string;
|
|
66
|
+
timestamp: string;
|
|
67
|
+
demand_mw: number;
|
|
68
|
+
residual_load_mw: number;
|
|
69
|
+
renewable_share_load_pct: number;
|
|
70
|
+
renewable_share_generation_pct: number;
|
|
71
|
+
hourly_demand: DemandHourly[];
|
|
72
|
+
}
|
|
73
|
+
type SignalColor = "green" | "yellow" | "red";
|
|
74
|
+
interface SignalHistoryEntry {
|
|
75
|
+
timestamp: string;
|
|
76
|
+
share_pct: number;
|
|
77
|
+
signal: SignalColor;
|
|
78
|
+
}
|
|
79
|
+
interface SignalResult {
|
|
80
|
+
zone: string;
|
|
81
|
+
source: string;
|
|
82
|
+
timestamp: string;
|
|
83
|
+
renewable_share_pct: number;
|
|
84
|
+
signal: SignalColor;
|
|
85
|
+
signal_description: string;
|
|
86
|
+
recent_history: SignalHistoryEntry[];
|
|
87
|
+
}
|
|
88
|
+
interface CapacityEntry {
|
|
89
|
+
fuel: string;
|
|
90
|
+
capacity_mw: number;
|
|
91
|
+
}
|
|
92
|
+
interface InstalledCapacityResult {
|
|
93
|
+
zone: string;
|
|
94
|
+
source: string;
|
|
95
|
+
year: string;
|
|
96
|
+
capacity: CapacityEntry[];
|
|
97
|
+
total_mw: number;
|
|
98
|
+
renewable_mw: number;
|
|
99
|
+
renewable_pct: number;
|
|
100
|
+
}
|
|
101
|
+
type EnergyChartsResult = PricesResult | GenerationResult | FlowsResult | DemandResult | SignalResult | InstalledCapacityResult;
|
|
102
|
+
export declare function getEnergyCharts(params: z.infer<typeof energyChartsSchema>): Promise<EnergyChartsResult>;
|
|
103
|
+
export {};
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const API_BASE = "https://api.energy-charts.info";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
export const energyChartsSchema = z.object({
|
|
6
|
+
dataset: z
|
|
7
|
+
.enum(["prices", "generation", "flows", "demand", "signal", "installed_capacity"])
|
|
8
|
+
.describe('"prices" = day-ahead electricity prices (15-min resolution). ' +
|
|
9
|
+
'"generation" = real-time generation by fuel type. ' +
|
|
10
|
+
'"flows" = cross-border physical flows. ' +
|
|
11
|
+
'"demand" = total power demand and residual load. ' +
|
|
12
|
+
'"signal" = real-time renewable share traffic-light signal. ' +
|
|
13
|
+
'"installed_capacity" = installed generation capacity by fuel type (yearly).'),
|
|
14
|
+
zone: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Country or bidding zone code. " +
|
|
17
|
+
"For prices: DE-LU, FR, ES, IT-North, NL, BE, AT, PL, NO2, SE4, DK1, DK2, CZ, HU, RO, BG, HR, SI, SK, GR, PT, FI, LT, LV, EE, IE-SEM. " +
|
|
18
|
+
"For generation/flows: de, fr, es, it, nl, be, at, pl, no, se, dk, cz, hu, ro, bg, hr, si, sk, gr, pt, fi, lt, lv, ee, ie."),
|
|
19
|
+
start_date: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Start date YYYY-MM-DD. Defaults to today."),
|
|
23
|
+
end_date: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("End date YYYY-MM-DD. Defaults to start + 1 day."),
|
|
27
|
+
});
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Shared fetch with caching
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
async function fetchEnergyCharts(url) {
|
|
32
|
+
const cached = cache.get(url);
|
|
33
|
+
if (cached)
|
|
34
|
+
return cached;
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
headers: { "User-Agent": "luminus-mcp/0.2" },
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const body = await response.text();
|
|
40
|
+
throw new Error(`energy-charts.info returned ${response.status}: ${body.slice(0, 300)}`);
|
|
41
|
+
}
|
|
42
|
+
const json = (await response.json());
|
|
43
|
+
cache.set(url, json, TTL.REALTIME);
|
|
44
|
+
return json;
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Date helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
function resolveDates(startDate, endDate) {
|
|
50
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
51
|
+
const start = startDate ?? today;
|
|
52
|
+
if (endDate) {
|
|
53
|
+
return { start, end: endDate };
|
|
54
|
+
}
|
|
55
|
+
const startDt = new Date(start + "T00:00:00Z");
|
|
56
|
+
const endDt = new Date(startDt.getTime() + 24 * 60 * 60 * 1000);
|
|
57
|
+
return { start, end: endDt.toISOString().slice(0, 10) };
|
|
58
|
+
}
|
|
59
|
+
async function fetchPrices(zone, startDate, endDate) {
|
|
60
|
+
const { start, end } = resolveDates(startDate, endDate);
|
|
61
|
+
const url = `${API_BASE}/price?bzn=${encodeURIComponent(zone)}&start=${start}&end=${end}`;
|
|
62
|
+
const data = await fetchEnergyCharts(url);
|
|
63
|
+
if (!data.unix_seconds || data.unix_seconds.length === 0) {
|
|
64
|
+
throw new Error(`No price data returned for zone "${zone}" (${start} to ${end}).`);
|
|
65
|
+
}
|
|
66
|
+
// Build 15-min price points, capped at first 96
|
|
67
|
+
const maxPoints = Math.min(data.unix_seconds.length, 96);
|
|
68
|
+
const prices15min = [];
|
|
69
|
+
for (let i = 0; i < maxPoints; i++) {
|
|
70
|
+
const ts = data.unix_seconds[i];
|
|
71
|
+
const price = data.price[i];
|
|
72
|
+
if (ts == null || price == null || !Number.isFinite(price))
|
|
73
|
+
continue;
|
|
74
|
+
prices15min.push({
|
|
75
|
+
timestamp: new Date(ts * 1000).toISOString(),
|
|
76
|
+
price: Math.round(price * 100) / 100,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Convert 15-min to hourly by averaging each 4 consecutive values
|
|
80
|
+
const pricesHourly = [];
|
|
81
|
+
for (let h = 0; h < Math.floor(prices15min.length / 4); h++) {
|
|
82
|
+
const chunk = prices15min.slice(h * 4, h * 4 + 4);
|
|
83
|
+
const avg = chunk.reduce((s, p) => s + p.price, 0) / chunk.length;
|
|
84
|
+
pricesHourly.push({
|
|
85
|
+
hour: h,
|
|
86
|
+
price: Math.round(avg * 100) / 100,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const values = prices15min.map((p) => p.price);
|
|
90
|
+
const min = values.length > 0 ? Math.round(Math.min(...values) * 100) / 100 : 0;
|
|
91
|
+
const max = values.length > 0 ? Math.round(Math.max(...values) * 100) / 100 : 0;
|
|
92
|
+
const mean = values.length > 0
|
|
93
|
+
? Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 100) / 100
|
|
94
|
+
: 0;
|
|
95
|
+
return {
|
|
96
|
+
zone,
|
|
97
|
+
start_date: start,
|
|
98
|
+
end_date: end,
|
|
99
|
+
source: "energy-charts.info",
|
|
100
|
+
resolution: "15min",
|
|
101
|
+
prices_15min: prices15min,
|
|
102
|
+
prices_hourly: pricesHourly,
|
|
103
|
+
stats: { min, max, mean },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Fuel category mapping: source name -> aggregated category */
|
|
107
|
+
const FUEL_CATEGORIES = {
|
|
108
|
+
"Wind onshore": "Wind",
|
|
109
|
+
"Wind offshore": "Wind",
|
|
110
|
+
Solar: "Solar",
|
|
111
|
+
"Fossil gas": "Gas",
|
|
112
|
+
"Fossil coal-derived gas": "Gas",
|
|
113
|
+
Nuclear: "Nuclear",
|
|
114
|
+
"Hydro Run-of-River": "Hydro",
|
|
115
|
+
"Hydro water reservoir": "Hydro",
|
|
116
|
+
"Hydro pumped storage": "Hydro",
|
|
117
|
+
"Fossil brown coal / lignite": "Coal",
|
|
118
|
+
"Fossil hard coal": "Coal",
|
|
119
|
+
Biomass: "Biomass",
|
|
120
|
+
};
|
|
121
|
+
/** Production types to exclude from aggregation (consumption, load, cross-border) */
|
|
122
|
+
const EXCLUDED_TYPES = new Set([
|
|
123
|
+
"Load",
|
|
124
|
+
"Residual load",
|
|
125
|
+
"Pumped storage consumption",
|
|
126
|
+
"Cross border electricity trading",
|
|
127
|
+
"Power consumption",
|
|
128
|
+
"Import Balance",
|
|
129
|
+
"Export Balance",
|
|
130
|
+
]);
|
|
131
|
+
const RENEWABLE_FUELS = new Set(["Wind", "Solar", "Hydro", "Biomass"]);
|
|
132
|
+
function getLatestNonNull(data) {
|
|
133
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
134
|
+
if (data[i] != null && Number.isFinite(data[i])) {
|
|
135
|
+
return data[i];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
async function fetchGeneration(zone, startDate, endDate) {
|
|
141
|
+
const { start, end } = resolveDates(startDate, endDate);
|
|
142
|
+
const url = `${API_BASE}/public_power?country=${encodeURIComponent(zone)}&start=${start}&end=${end}`;
|
|
143
|
+
const data = await fetchEnergyCharts(url);
|
|
144
|
+
if (!data.production_types || data.production_types.length === 0) {
|
|
145
|
+
throw new Error(`No generation data returned for zone "${zone}" (${start} to ${end}).`);
|
|
146
|
+
}
|
|
147
|
+
// Aggregate by fuel category
|
|
148
|
+
const categoryMw = new Map();
|
|
149
|
+
for (const pt of data.production_types) {
|
|
150
|
+
if (EXCLUDED_TYPES.has(pt.name))
|
|
151
|
+
continue;
|
|
152
|
+
const category = FUEL_CATEGORIES[pt.name] ?? "Other";
|
|
153
|
+
const mw = getLatestNonNull(pt.data);
|
|
154
|
+
if (mw == null || mw <= 0)
|
|
155
|
+
continue;
|
|
156
|
+
categoryMw.set(category, (categoryMw.get(category) ?? 0) + mw);
|
|
157
|
+
}
|
|
158
|
+
const generation = [];
|
|
159
|
+
for (const [fuel, mw] of categoryMw.entries()) {
|
|
160
|
+
generation.push({ fuel, generation_mw: Math.round(mw) });
|
|
161
|
+
}
|
|
162
|
+
generation.sort((a, b) => b.generation_mw - a.generation_mw);
|
|
163
|
+
const totalMw = generation.reduce((sum, g) => sum + g.generation_mw, 0);
|
|
164
|
+
const renewableMw = generation
|
|
165
|
+
.filter((g) => RENEWABLE_FUELS.has(g.fuel))
|
|
166
|
+
.reduce((sum, g) => sum + g.generation_mw, 0);
|
|
167
|
+
const renewablePct = totalMw > 0 ? Math.round((renewableMw / totalMw) * 1000) / 10 : 0;
|
|
168
|
+
// Determine timestamp from the latest non-null entry
|
|
169
|
+
let timestamp = new Date().toISOString();
|
|
170
|
+
if (data.unix_seconds && data.unix_seconds.length > 0) {
|
|
171
|
+
const lastTs = data.unix_seconds[data.unix_seconds.length - 1];
|
|
172
|
+
if (lastTs != null) {
|
|
173
|
+
timestamp = new Date(lastTs * 1000).toISOString();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
zone,
|
|
178
|
+
timestamp,
|
|
179
|
+
source: "energy-charts.info",
|
|
180
|
+
generation,
|
|
181
|
+
total_mw: totalMw,
|
|
182
|
+
renewable_pct: renewablePct,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function fetchFlows(zone) {
|
|
186
|
+
const url = `${API_BASE}/cbpf?country=${encodeURIComponent(zone)}`;
|
|
187
|
+
const data = await fetchEnergyCharts(url);
|
|
188
|
+
if (!data.countries || data.countries.length === 0) {
|
|
189
|
+
throw new Error(`No cross-border flow data returned for zone "${zone}".`);
|
|
190
|
+
}
|
|
191
|
+
const flows = [];
|
|
192
|
+
let netPosition = 0;
|
|
193
|
+
for (const country of data.countries) {
|
|
194
|
+
const mw = getLatestNonNull(country.data);
|
|
195
|
+
if (mw == null)
|
|
196
|
+
continue;
|
|
197
|
+
const rounded = Math.round(mw);
|
|
198
|
+
flows.push({ country: country.name, flow_mw: rounded });
|
|
199
|
+
netPosition += rounded;
|
|
200
|
+
}
|
|
201
|
+
flows.sort((a, b) => Math.abs(b.flow_mw) - Math.abs(a.flow_mw));
|
|
202
|
+
return {
|
|
203
|
+
zone,
|
|
204
|
+
source: "energy-charts.info",
|
|
205
|
+
flows,
|
|
206
|
+
net_position_mw: netPosition,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async function fetchDemand(zone, startDate, endDate) {
|
|
210
|
+
const { start, end } = resolveDates(startDate, endDate);
|
|
211
|
+
const url = `${API_BASE}/total_power?country=${encodeURIComponent(zone)}&start=${start}&end=${end}`;
|
|
212
|
+
const data = await fetchEnergyCharts(url);
|
|
213
|
+
if (!data.production_types || data.production_types.length === 0) {
|
|
214
|
+
throw new Error(`No demand data returned for zone "${zone}" (${start} to ${end}).`);
|
|
215
|
+
}
|
|
216
|
+
// Extract key metrics from production types
|
|
217
|
+
let demandMw = 0;
|
|
218
|
+
let residualLoadMw = 0;
|
|
219
|
+
let renewableShareLoadPct = 0;
|
|
220
|
+
let renewableShareGenPct = 0;
|
|
221
|
+
let loadData = [];
|
|
222
|
+
for (const pt of data.production_types) {
|
|
223
|
+
if (pt.name === "Load" || pt.name === "Load (incl. self-consumption)") {
|
|
224
|
+
const val = getLatestNonNull(pt.data);
|
|
225
|
+
if (val != null)
|
|
226
|
+
demandMw = Math.round(val);
|
|
227
|
+
loadData = pt.data;
|
|
228
|
+
}
|
|
229
|
+
else if (pt.name === "Residual load") {
|
|
230
|
+
const val = getLatestNonNull(pt.data);
|
|
231
|
+
if (val != null)
|
|
232
|
+
residualLoadMw = Math.round(val);
|
|
233
|
+
}
|
|
234
|
+
else if (pt.name === "Renewable share of load") {
|
|
235
|
+
const val = getLatestNonNull(pt.data);
|
|
236
|
+
if (val != null)
|
|
237
|
+
renewableShareLoadPct = Math.round(val * 10) / 10;
|
|
238
|
+
}
|
|
239
|
+
else if (pt.name === "Renewable share of generation") {
|
|
240
|
+
const val = getLatestNonNull(pt.data);
|
|
241
|
+
if (val != null)
|
|
242
|
+
renewableShareGenPct = Math.round(val * 10) / 10;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Build hourly demand profile from load data
|
|
246
|
+
const hourlyDemand = [];
|
|
247
|
+
if (data.unix_seconds && loadData.length > 0) {
|
|
248
|
+
const hourBuckets = new Map();
|
|
249
|
+
const maxPoints = Math.min(data.unix_seconds.length, loadData.length);
|
|
250
|
+
for (let i = 0; i < maxPoints; i++) {
|
|
251
|
+
const ts = data.unix_seconds[i];
|
|
252
|
+
const mw = loadData[i];
|
|
253
|
+
if (ts == null || mw == null || !Number.isFinite(mw))
|
|
254
|
+
continue;
|
|
255
|
+
const hour = new Date(ts * 1000).getUTCHours();
|
|
256
|
+
const bucket = hourBuckets.get(hour);
|
|
257
|
+
if (bucket) {
|
|
258
|
+
bucket.push(mw);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
hourBuckets.set(hour, [mw]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const [hour, values] of hourBuckets.entries()) {
|
|
265
|
+
const avg = values.reduce((s, v) => s + v, 0) / values.length;
|
|
266
|
+
hourlyDemand.push({ hour, demand_mw: Math.round(avg) });
|
|
267
|
+
}
|
|
268
|
+
hourlyDemand.sort((a, b) => a.hour - b.hour);
|
|
269
|
+
}
|
|
270
|
+
// Determine timestamp
|
|
271
|
+
let timestamp = new Date().toISOString();
|
|
272
|
+
if (data.unix_seconds && data.unix_seconds.length > 0) {
|
|
273
|
+
const lastTs = data.unix_seconds[data.unix_seconds.length - 1];
|
|
274
|
+
if (lastTs != null) {
|
|
275
|
+
timestamp = new Date(lastTs * 1000).toISOString();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
zone,
|
|
280
|
+
source: "energy-charts.info",
|
|
281
|
+
timestamp,
|
|
282
|
+
demand_mw: demandMw,
|
|
283
|
+
residual_load_mw: residualLoadMw,
|
|
284
|
+
renewable_share_load_pct: renewableShareLoadPct,
|
|
285
|
+
renewable_share_generation_pct: renewableShareGenPct,
|
|
286
|
+
hourly_demand: hourlyDemand,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function signalToColor(value) {
|
|
290
|
+
if (value === 0)
|
|
291
|
+
return "green";
|
|
292
|
+
if (value === 1)
|
|
293
|
+
return "yellow";
|
|
294
|
+
return "red";
|
|
295
|
+
}
|
|
296
|
+
function signalDescription(color, sharePct) {
|
|
297
|
+
const label = color === "green"
|
|
298
|
+
? "High renewable share"
|
|
299
|
+
: color === "yellow"
|
|
300
|
+
? "Medium renewable share"
|
|
301
|
+
: "Low renewable share";
|
|
302
|
+
return `${label} (${sharePct}%)`;
|
|
303
|
+
}
|
|
304
|
+
async function fetchSignal(zone) {
|
|
305
|
+
const url = `${API_BASE}/signal?country=${encodeURIComponent(zone)}`;
|
|
306
|
+
const data = await fetchEnergyCharts(url);
|
|
307
|
+
if (!data.unix_seconds || data.unix_seconds.length === 0) {
|
|
308
|
+
throw new Error(`No signal data returned for zone "${zone}".`);
|
|
309
|
+
}
|
|
310
|
+
// Find latest non-null values
|
|
311
|
+
let latestShare = 0;
|
|
312
|
+
let latestSignal = "red";
|
|
313
|
+
let latestTimestamp = new Date().toISOString();
|
|
314
|
+
for (let i = data.unix_seconds.length - 1; i >= 0; i--) {
|
|
315
|
+
const ts = data.unix_seconds[i];
|
|
316
|
+
const share = data.share[i];
|
|
317
|
+
const sig = data.signal[i];
|
|
318
|
+
if (ts == null || share == null || sig == null)
|
|
319
|
+
continue;
|
|
320
|
+
if (!Number.isFinite(share) || !Number.isFinite(sig))
|
|
321
|
+
continue;
|
|
322
|
+
latestShare = Math.round(share * 10) / 10;
|
|
323
|
+
latestSignal = signalToColor(sig);
|
|
324
|
+
latestTimestamp = new Date(ts * 1000).toISOString();
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
// Build recent history (last 12 non-null entries)
|
|
328
|
+
const recentHistory = [];
|
|
329
|
+
for (let i = data.unix_seconds.length - 1; i >= 0 && recentHistory.length < 12; i--) {
|
|
330
|
+
const ts = data.unix_seconds[i];
|
|
331
|
+
const share = data.share[i];
|
|
332
|
+
const sig = data.signal[i];
|
|
333
|
+
if (ts == null || share == null || sig == null)
|
|
334
|
+
continue;
|
|
335
|
+
if (!Number.isFinite(share) || !Number.isFinite(sig))
|
|
336
|
+
continue;
|
|
337
|
+
recentHistory.push({
|
|
338
|
+
timestamp: new Date(ts * 1000).toISOString(),
|
|
339
|
+
share_pct: Math.round(share * 10) / 10,
|
|
340
|
+
signal: signalToColor(sig),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
recentHistory.reverse();
|
|
344
|
+
return {
|
|
345
|
+
zone,
|
|
346
|
+
source: "energy-charts.info",
|
|
347
|
+
timestamp: latestTimestamp,
|
|
348
|
+
renewable_share_pct: latestShare,
|
|
349
|
+
signal: latestSignal,
|
|
350
|
+
signal_description: signalDescription(latestSignal, latestShare),
|
|
351
|
+
recent_history: recentHistory,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async function fetchInstalledCapacity(zone) {
|
|
355
|
+
const url = `${API_BASE}/installed_power?country=${encodeURIComponent(zone)}&time_step=yearly`;
|
|
356
|
+
const data = await fetchEnergyCharts(url);
|
|
357
|
+
if (!data.production_types || data.production_types.length === 0) {
|
|
358
|
+
throw new Error(`No installed capacity data returned for zone "${zone}".`);
|
|
359
|
+
}
|
|
360
|
+
// Use the latest year's data
|
|
361
|
+
const latestIndex = data.time && data.time.length > 0 ? data.time.length - 1 : 0;
|
|
362
|
+
const year = data.time && data.time.length > 0 ? data.time[latestIndex] : "unknown";
|
|
363
|
+
// Aggregate by fuel category (reuse FUEL_CATEGORIES mapping)
|
|
364
|
+
const categoryMw = new Map();
|
|
365
|
+
for (const pt of data.production_types) {
|
|
366
|
+
if (EXCLUDED_TYPES.has(pt.name))
|
|
367
|
+
continue;
|
|
368
|
+
const category = FUEL_CATEGORIES[pt.name] ?? "Other";
|
|
369
|
+
const mw = pt.data[latestIndex];
|
|
370
|
+
if (mw == null || !Number.isFinite(mw) || mw <= 0)
|
|
371
|
+
continue;
|
|
372
|
+
categoryMw.set(category, (categoryMw.get(category) ?? 0) + mw);
|
|
373
|
+
}
|
|
374
|
+
const capacity = [];
|
|
375
|
+
for (const [fuel, mw] of categoryMw.entries()) {
|
|
376
|
+
capacity.push({ fuel, capacity_mw: Math.round(mw) });
|
|
377
|
+
}
|
|
378
|
+
capacity.sort((a, b) => b.capacity_mw - a.capacity_mw);
|
|
379
|
+
const totalMw = capacity.reduce((sum, c) => sum + c.capacity_mw, 0);
|
|
380
|
+
const renewableMw = capacity
|
|
381
|
+
.filter((c) => RENEWABLE_FUELS.has(c.fuel))
|
|
382
|
+
.reduce((sum, c) => sum + c.capacity_mw, 0);
|
|
383
|
+
const renewablePct = totalMw > 0 ? Math.round((renewableMw / totalMw) * 1000) / 10 : 0;
|
|
384
|
+
return {
|
|
385
|
+
zone,
|
|
386
|
+
source: "energy-charts.info",
|
|
387
|
+
year,
|
|
388
|
+
capacity,
|
|
389
|
+
total_mw: totalMw,
|
|
390
|
+
renewable_mw: renewableMw,
|
|
391
|
+
renewable_pct: renewablePct,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
export async function getEnergyCharts(params) {
|
|
395
|
+
switch (params.dataset) {
|
|
396
|
+
case "prices":
|
|
397
|
+
return fetchPrices(params.zone, params.start_date, params.end_date);
|
|
398
|
+
case "generation":
|
|
399
|
+
return fetchGeneration(params.zone, params.start_date, params.end_date);
|
|
400
|
+
case "flows":
|
|
401
|
+
return fetchFlows(params.zone);
|
|
402
|
+
case "demand":
|
|
403
|
+
return fetchDemand(params.zone, params.start_date, params.end_date);
|
|
404
|
+
case "signal":
|
|
405
|
+
return fetchSignal(params.zone);
|
|
406
|
+
case "installed_capacity":
|
|
407
|
+
return fetchInstalledCapacity(params.zone);
|
|
408
|
+
default:
|
|
409
|
+
throw new Error(`Unknown dataset "${params.dataset}". Use: prices, generation, flows, demand, signal, installed_capacity.`);
|
|
410
|
+
}
|
|
411
|
+
}
|