@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,216 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
import { resolveApiKey } from "../lib/auth.js";
|
|
4
|
+
const API_BASE = "https://api.esios.ree.es";
|
|
5
|
+
const REE_PUBLIC = "https://www.ree.es/en/datos/apidatos";
|
|
6
|
+
const cache = new TtlCache();
|
|
7
|
+
/** ESIOS indicator IDs for key datasets */
|
|
8
|
+
const INDICATORS = {
|
|
9
|
+
day_ahead_price: 600,
|
|
10
|
+
intraday_price: 612,
|
|
11
|
+
demand_forecast: 460,
|
|
12
|
+
demand_actual: 1293,
|
|
13
|
+
wind_forecast: 541,
|
|
14
|
+
wind_actual: 551,
|
|
15
|
+
solar_forecast: 542,
|
|
16
|
+
solar_actual: 552,
|
|
17
|
+
generation_mix: 1295,
|
|
18
|
+
interconnector_flows: 10210,
|
|
19
|
+
};
|
|
20
|
+
export const reeEsiosSchema = z.object({
|
|
21
|
+
dataset: z
|
|
22
|
+
.enum([
|
|
23
|
+
"day_ahead_price",
|
|
24
|
+
"demand",
|
|
25
|
+
"generation",
|
|
26
|
+
"wind_solar",
|
|
27
|
+
"interconnectors",
|
|
28
|
+
])
|
|
29
|
+
.describe('"day_ahead_price" = Spanish day-ahead electricity prices (EUR/MWh). ' +
|
|
30
|
+
'"demand" = forecast and actual demand (MW). ' +
|
|
31
|
+
'"generation" = generation mix by technology. ' +
|
|
32
|
+
'"wind_solar" = wind and solar forecast vs actual (MW). ' +
|
|
33
|
+
'"interconnectors" = cross-border flows with France, Portugal, Morocco.'),
|
|
34
|
+
date: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
38
|
+
});
|
|
39
|
+
async function getApiToken() {
|
|
40
|
+
try {
|
|
41
|
+
return await resolveApiKey("ESIOS_API_TOKEN");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error("ESIOS_API_TOKEN is required. Set it as an environment variable or in ~/.luminus/keys.json. " +
|
|
45
|
+
"Request a free token by emailing consultasios@ree.es or via " +
|
|
46
|
+
REE_PUBLIC);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
async function fetchEsios(indicatorId, startDate, endDate) {
|
|
51
|
+
const url = `${API_BASE}/indicators/${indicatorId}?` +
|
|
52
|
+
`start_date=${encodeURIComponent(startDate + "T00:00:00Z")}&` +
|
|
53
|
+
`end_date=${encodeURIComponent(endDate + "T23:59:59Z")}`;
|
|
54
|
+
const cached = cache.get(url);
|
|
55
|
+
if (cached)
|
|
56
|
+
return cached.values;
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
headers: {
|
|
59
|
+
Accept: "application/json; application/vnd.esios-api-v1+json",
|
|
60
|
+
Authorization: `Token token="${await getApiToken()}"`,
|
|
61
|
+
"User-Agent": "luminus-mcp/0.2",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const body = await response.text();
|
|
66
|
+
throw new Error(`ESIOS API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
67
|
+
}
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
const json = await response.json();
|
|
70
|
+
const values = json?.indicator?.values ?? [];
|
|
71
|
+
cache.set(url, { values }, TTL.PRICES);
|
|
72
|
+
return values;
|
|
73
|
+
}
|
|
74
|
+
async function fetchPrices(date) {
|
|
75
|
+
const values = await fetchEsios(INDICATORS.day_ahead_price, date, date);
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
const records = values.slice(0, 48).map((v) => ({
|
|
78
|
+
timestamp: v.datetime ?? v.date ?? "",
|
|
79
|
+
price_eur_mwh: Math.round((Number(v.value ?? 0)) * 100) / 100,
|
|
80
|
+
}));
|
|
81
|
+
const prices = records.map((r) => r.price_eur_mwh);
|
|
82
|
+
return {
|
|
83
|
+
dataset: "day_ahead_price",
|
|
84
|
+
source: "REE ESIOS (Red Eléctrica de España)",
|
|
85
|
+
date,
|
|
86
|
+
records,
|
|
87
|
+
stats: {
|
|
88
|
+
min: prices.length > 0 ? Math.round(Math.min(...prices) * 100) / 100 : 0,
|
|
89
|
+
max: prices.length > 0 ? Math.round(Math.max(...prices) * 100) / 100 : 0,
|
|
90
|
+
mean: prices.length > 0
|
|
91
|
+
? Math.round((prices.reduce((s, v) => s + v, 0) / prices.length) * 100) / 100
|
|
92
|
+
: 0,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function fetchDemand(date) {
|
|
97
|
+
const [forecastValues, actualValues] = await Promise.all([
|
|
98
|
+
fetchEsios(INDICATORS.demand_forecast, date, date),
|
|
99
|
+
fetchEsios(INDICATORS.demand_actual, date, date),
|
|
100
|
+
]);
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
const actualMap = new Map();
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
actualValues.forEach((v) => {
|
|
105
|
+
const ts = (v.datetime ?? "").slice(0, 16);
|
|
106
|
+
actualMap.set(ts, Number(v.value ?? 0));
|
|
107
|
+
});
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
const records = forecastValues.slice(0, 48).map((v) => {
|
|
110
|
+
const ts = (v.datetime ?? "").slice(0, 16);
|
|
111
|
+
const forecast = Number(v.value ?? 0);
|
|
112
|
+
const actual = actualMap.get(ts) ?? 0;
|
|
113
|
+
return {
|
|
114
|
+
timestamp: v.datetime ?? "",
|
|
115
|
+
forecast_mw: Math.round(forecast),
|
|
116
|
+
actual_mw: Math.round(actual),
|
|
117
|
+
error_mw: Math.round(actual - forecast),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
dataset: "demand",
|
|
122
|
+
source: "REE ESIOS",
|
|
123
|
+
date,
|
|
124
|
+
records,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function fetchGeneration(date) {
|
|
128
|
+
const values = await fetchEsios(INDICATORS.generation_mix, date, date);
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
130
|
+
const records = values.slice(0, 50).map((v) => ({
|
|
131
|
+
timestamp: v.datetime ?? "",
|
|
132
|
+
technology: v.geo_name ?? v.name ?? "Unknown",
|
|
133
|
+
generation_mw: Math.round(Number(v.value ?? 0)),
|
|
134
|
+
}));
|
|
135
|
+
return {
|
|
136
|
+
dataset: "generation",
|
|
137
|
+
source: "REE ESIOS",
|
|
138
|
+
date,
|
|
139
|
+
records,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function fetchWindSolar(date) {
|
|
143
|
+
const [windFc, windAct, solarFc, solarAct] = await Promise.all([
|
|
144
|
+
fetchEsios(INDICATORS.wind_forecast, date, date),
|
|
145
|
+
fetchEsios(INDICATORS.wind_actual, date, date),
|
|
146
|
+
fetchEsios(INDICATORS.solar_forecast, date, date),
|
|
147
|
+
fetchEsios(INDICATORS.solar_actual, date, date),
|
|
148
|
+
]);
|
|
149
|
+
// Build a time-aligned merge
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
const toMap = (arr) => {
|
|
152
|
+
const m = new Map();
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
154
|
+
arr.forEach((v) => {
|
|
155
|
+
const ts = (v.datetime ?? "").slice(0, 16);
|
|
156
|
+
m.set(ts, Number(v.value ?? 0));
|
|
157
|
+
});
|
|
158
|
+
return m;
|
|
159
|
+
};
|
|
160
|
+
const windActMap = toMap(windAct);
|
|
161
|
+
const solarFcMap = toMap(solarFc);
|
|
162
|
+
const solarActMap = toMap(solarAct);
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
|
+
const records = windFc.slice(0, 48).map((v) => {
|
|
165
|
+
const ts = (v.datetime ?? "").slice(0, 16);
|
|
166
|
+
return {
|
|
167
|
+
timestamp: v.datetime ?? "",
|
|
168
|
+
wind_forecast_mw: Math.round(Number(v.value ?? 0)),
|
|
169
|
+
wind_actual_mw: Math.round(windActMap.get(ts) ?? 0),
|
|
170
|
+
solar_forecast_mw: Math.round(solarFcMap.get(ts) ?? 0),
|
|
171
|
+
solar_actual_mw: Math.round(solarActMap.get(ts) ?? 0),
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
dataset: "wind_solar",
|
|
176
|
+
source: "REE ESIOS",
|
|
177
|
+
date,
|
|
178
|
+
records,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function fetchInterconnectors(date) {
|
|
182
|
+
const values = await fetchEsios(INDICATORS.interconnector_flows, date, date);
|
|
183
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
184
|
+
const records = values.slice(0, 30).map((v) => {
|
|
185
|
+
const flow = Number(v.value ?? 0);
|
|
186
|
+
return {
|
|
187
|
+
border: v.geo_name ?? v.name ?? "Unknown",
|
|
188
|
+
flow_mw: Math.round(flow),
|
|
189
|
+
direction: flow >= 0 ? "import" : "export",
|
|
190
|
+
timestamp: v.datetime ?? "",
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
const netImport = records.reduce((s, r) => s + r.flow_mw, 0);
|
|
194
|
+
return {
|
|
195
|
+
dataset: "interconnectors",
|
|
196
|
+
source: "REE ESIOS",
|
|
197
|
+
date,
|
|
198
|
+
records,
|
|
199
|
+
net_import_mw: netImport,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export async function getReeEsios(params) {
|
|
203
|
+
const date = params.date ?? new Date().toISOString().slice(0, 10);
|
|
204
|
+
switch (params.dataset) {
|
|
205
|
+
case "day_ahead_price":
|
|
206
|
+
return fetchPrices(date);
|
|
207
|
+
case "demand":
|
|
208
|
+
return fetchDemand(date);
|
|
209
|
+
case "generation":
|
|
210
|
+
return fetchGeneration(date);
|
|
211
|
+
case "wind_solar":
|
|
212
|
+
return fetchWindSolar(date);
|
|
213
|
+
case "interconnectors":
|
|
214
|
+
return fetchInterconnectors(date);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const regelleistungSchema: z.ZodObject<{
|
|
3
|
+
product: z.ZodEnum<{
|
|
4
|
+
FCR: "FCR";
|
|
5
|
+
aFRR: "aFRR";
|
|
6
|
+
mFRR: "mFRR";
|
|
7
|
+
}>;
|
|
8
|
+
date: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
interface TenderRecord {
|
|
11
|
+
product: string;
|
|
12
|
+
delivery_date: string;
|
|
13
|
+
direction: string;
|
|
14
|
+
procured_mw: number;
|
|
15
|
+
marginal_price_eur_mw: number;
|
|
16
|
+
average_price_eur_mw: number;
|
|
17
|
+
min_price_eur_mw: number;
|
|
18
|
+
max_price_eur_mw: number;
|
|
19
|
+
}
|
|
20
|
+
interface RegelleistungResult {
|
|
21
|
+
source: string;
|
|
22
|
+
product: string;
|
|
23
|
+
date: string;
|
|
24
|
+
description: string;
|
|
25
|
+
tenders: TenderRecord[];
|
|
26
|
+
}
|
|
27
|
+
export declare function getRegelleistung(params: z.infer<typeof regelleistungSchema>): Promise<RegelleistungResult>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const API_BASE = "https://www.regelleistung.net/apps/datacenter/tenders";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
const PRODUCTS = ["FCR", "aFRR", "mFRR"];
|
|
6
|
+
export const regelleistungSchema = z.object({
|
|
7
|
+
product: z
|
|
8
|
+
.enum(PRODUCTS)
|
|
9
|
+
.describe('"FCR" = Frequency Containment Reserve (primary, ±200mHz, all EU). ' +
|
|
10
|
+
'"aFRR" = automatic Frequency Restoration Reserve (secondary, 5min). ' +
|
|
11
|
+
'"mFRR" = manual Frequency Restoration Reserve (tertiary, 15min).'),
|
|
12
|
+
date: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
16
|
+
});
|
|
17
|
+
export async function getRegelleistung(params) {
|
|
18
|
+
const date = params.date ?? new Date().toISOString().slice(0, 10);
|
|
19
|
+
const product = params.product;
|
|
20
|
+
const url = `${API_BASE}?productTypes=${product}&deliveryDate=${date}&format=json`;
|
|
21
|
+
const cached = cache.get(url);
|
|
22
|
+
if (cached)
|
|
23
|
+
return cached;
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
headers: { Accept: "application/json", "User-Agent": "luminus-mcp/0.2" },
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
// Regelleistung may not have a clean JSON API; fall back to description
|
|
29
|
+
const descriptions = {
|
|
30
|
+
FCR: "FCR (Frequency Containment Reserve): ±200mHz deadband, symmetric product, " +
|
|
31
|
+
"auctioned daily via the FCR Cooperation (DE, FR, NL, BE, AT, CH, DK, SI). " +
|
|
32
|
+
"Primary revenue stream for European BESS. Check regelleistung.net/apps/datacenter for latest results.",
|
|
33
|
+
aFRR: "aFRR (automatic Frequency Restoration Reserve): Activated within 5 minutes, " +
|
|
34
|
+
"energy + capacity pricing. DE-specific but harmonising across EU. " +
|
|
35
|
+
"Check regelleistung.net/apps/datacenter for latest results.",
|
|
36
|
+
mFRR: "mFRR (manual Frequency Restoration Reserve): Activated within 15 minutes, " +
|
|
37
|
+
"manual dispatch. Often used for larger imbalances. " +
|
|
38
|
+
"Check regelleistung.net/apps/datacenter for latest results.",
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
source: "Regelleistung.net (Balancing Market Data Center)",
|
|
42
|
+
product,
|
|
43
|
+
date,
|
|
44
|
+
description: descriptions[product] ?? `${product} balancing reserve data. No tender results available from the API for this date.`,
|
|
45
|
+
tenders: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
const json = await response.json();
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
const items = Array.isArray(json) ? json : json?.data ?? json?.tenders ?? [];
|
|
52
|
+
const tenders = items.slice(0, 30).map((t) => ({
|
|
53
|
+
product: t.productType ?? product,
|
|
54
|
+
delivery_date: t.deliveryDate ?? date,
|
|
55
|
+
direction: t.direction ?? "symmetric",
|
|
56
|
+
procured_mw: Math.round(Number(t.procuredCapacity ?? t.demandMw ?? 0)),
|
|
57
|
+
marginal_price_eur_mw: Math.round((Number(t.marginalPrice ?? 0)) * 100) / 100,
|
|
58
|
+
average_price_eur_mw: Math.round((Number(t.averagePrice ?? t.weightedAveragePrice ?? 0)) * 100) / 100,
|
|
59
|
+
min_price_eur_mw: Math.round((Number(t.minPrice ?? 0)) * 100) / 100,
|
|
60
|
+
max_price_eur_mw: Math.round((Number(t.maxPrice ?? 0)) * 100) / 100,
|
|
61
|
+
}));
|
|
62
|
+
const result = {
|
|
63
|
+
source: "Regelleistung.net (Balancing Market Data Center)",
|
|
64
|
+
product,
|
|
65
|
+
date,
|
|
66
|
+
description: `${product} tender results for ${date}. ${tenders.length} records.`,
|
|
67
|
+
tenders,
|
|
68
|
+
};
|
|
69
|
+
cache.set(url, result, TTL.ANCILLARY);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const remitMessagesSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface RemitMessage {
|
|
7
|
+
id: string;
|
|
8
|
+
plant: string;
|
|
9
|
+
fuel: string | null;
|
|
10
|
+
type: "planned" | "unplanned" | string;
|
|
11
|
+
availableMW: number | null;
|
|
12
|
+
unavailableMW: number | null;
|
|
13
|
+
start: string;
|
|
14
|
+
end: string;
|
|
15
|
+
reason: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function getRemitMessages(params: z.infer<typeof remitMessagesSchema>): Promise<{
|
|
18
|
+
zone: string;
|
|
19
|
+
date: string;
|
|
20
|
+
messageCount: number;
|
|
21
|
+
messages: RemitMessage[];
|
|
22
|
+
}>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryEntsoe, formatEntsoeDate } from "../lib/entsoe-client.js";
|
|
3
|
+
import { resolveZone, AVAILABLE_ZONES } from "../lib/zone-codes.js";
|
|
4
|
+
import { ensureArray } from "../lib/xml-parser.js";
|
|
5
|
+
import { TTL } from "../lib/cache.js";
|
|
6
|
+
/** PSR type codes to human-readable fuel names */
|
|
7
|
+
const PSR_TYPES = {
|
|
8
|
+
B01: "Biomass",
|
|
9
|
+
B02: "Lignite",
|
|
10
|
+
B03: "Coal-derived gas",
|
|
11
|
+
B04: "Gas",
|
|
12
|
+
B05: "Hard coal",
|
|
13
|
+
B06: "Oil",
|
|
14
|
+
B09: "Geothermal",
|
|
15
|
+
B10: "Hydro Pumped Storage",
|
|
16
|
+
B11: "Hydro Run-of-river",
|
|
17
|
+
B12: "Hydro Reservoir",
|
|
18
|
+
B14: "Nuclear",
|
|
19
|
+
B16: "Solar",
|
|
20
|
+
B18: "Wind Offshore",
|
|
21
|
+
B19: "Wind Onshore",
|
|
22
|
+
};
|
|
23
|
+
export const remitMessagesSchema = z.object({
|
|
24
|
+
zone: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe(`Bidding zone code. Examples: DE, FR, GB. Available: ${AVAILABLE_ZONES}`),
|
|
27
|
+
date: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
31
|
+
});
|
|
32
|
+
export async function getRemitMessages(params) {
|
|
33
|
+
const eic = resolveZone(params.zone);
|
|
34
|
+
const date = params.date ?? new Date().toISOString().slice(0, 10);
|
|
35
|
+
const base = new Date(date + "T00:00:00Z");
|
|
36
|
+
const start = new Date(Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate()));
|
|
37
|
+
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
|
|
38
|
+
let data;
|
|
39
|
+
try {
|
|
40
|
+
data = await queryEntsoe({
|
|
41
|
+
documentType: "A80",
|
|
42
|
+
processType: "A26",
|
|
43
|
+
biddingZone_Domain: eic,
|
|
44
|
+
periodStart: formatEntsoeDate(start),
|
|
45
|
+
periodEnd: formatEntsoeDate(end),
|
|
46
|
+
}, TTL.OUTAGES);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// API error or no data = empty result, not a thrown error
|
|
50
|
+
return { zone: params.zone.toUpperCase(), date, messageCount: 0, messages: [] };
|
|
51
|
+
}
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
const doc = data.UnavailabilityMarketDocument ??
|
|
54
|
+
data.Unavailability_MarketDocument;
|
|
55
|
+
if (!doc) {
|
|
56
|
+
return { zone: params.zone.toUpperCase(), date, messageCount: 0, messages: [] };
|
|
57
|
+
}
|
|
58
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
59
|
+
const messages = [];
|
|
60
|
+
for (const ts of timeSeries) {
|
|
61
|
+
const resource = ts.production_RegisteredResource ?? ts.Asset_RegisteredResource ?? {};
|
|
62
|
+
const plant = resource.name ?? resource.mRID ?? "Unknown";
|
|
63
|
+
const psrCode = resource.pSRType?.psrType ?? null;
|
|
64
|
+
const fuel = psrCode ? (PSR_TYPES[psrCode] ?? psrCode) : null;
|
|
65
|
+
const nominalMw = resource.pSRType?.powerSystemResources?.nominalP
|
|
66
|
+
? Number(resource.pSRType.powerSystemResources.nominalP)
|
|
67
|
+
: null;
|
|
68
|
+
const businessType = ts.businessType ?? "";
|
|
69
|
+
const outageType = businessType === "A53" ? "planned"
|
|
70
|
+
: businessType === "A54" ? "unplanned"
|
|
71
|
+
: businessType;
|
|
72
|
+
const startDate = ts.start_DateAndOrTime?.date ?? ts.start_DateAndOrTime ?? "";
|
|
73
|
+
const endDate = ts.end_DateAndOrTime?.date ?? ts.end_DateAndOrTime ?? "";
|
|
74
|
+
const reason = ts.Reason?.text ?? null;
|
|
75
|
+
const id = ts.mRID ?? "";
|
|
76
|
+
const availPeriods = ensureArray(ts.Available_Period);
|
|
77
|
+
let availableMw = null;
|
|
78
|
+
for (const period of availPeriods) {
|
|
79
|
+
const points = ensureArray(period.Point);
|
|
80
|
+
if (points.length > 0) {
|
|
81
|
+
availableMw = Number(points[0].quantity ?? 0);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const unavailableMw = nominalMw != null && availableMw != null
|
|
86
|
+
? Math.round(nominalMw - availableMw)
|
|
87
|
+
: null;
|
|
88
|
+
messages.push({
|
|
89
|
+
id,
|
|
90
|
+
plant,
|
|
91
|
+
fuel,
|
|
92
|
+
type: outageType,
|
|
93
|
+
availableMW: availableMw != null ? Math.round(availableMw) : null,
|
|
94
|
+
unavailableMW: unavailableMw,
|
|
95
|
+
start: startDate,
|
|
96
|
+
end: endDate,
|
|
97
|
+
reason,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Sort by unavailableMW descending, nulls last
|
|
101
|
+
messages.sort((a, b) => (b.unavailableMW ?? 0) - (a.unavailableMW ?? 0));
|
|
102
|
+
// Limit to first 20
|
|
103
|
+
const limited = messages.slice(0, 20);
|
|
104
|
+
return {
|
|
105
|
+
zone: params.zone.toUpperCase(),
|
|
106
|
+
date,
|
|
107
|
+
messageCount: limited.length,
|
|
108
|
+
messages: limited,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const renewableForecastSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface ForecastPoint {
|
|
7
|
+
hour: number;
|
|
8
|
+
mw: number;
|
|
9
|
+
}
|
|
10
|
+
interface RenewableForecastEntry {
|
|
11
|
+
source: string;
|
|
12
|
+
psr_code: string;
|
|
13
|
+
forecast: ForecastPoint[];
|
|
14
|
+
peak_mw: number;
|
|
15
|
+
total_mwh: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function getRenewableForecast(params: z.infer<typeof renewableForecastSchema>): Promise<{
|
|
18
|
+
zone: string;
|
|
19
|
+
date: string;
|
|
20
|
+
forecasts: RenewableForecastEntry[];
|
|
21
|
+
total_peak_mw: number;
|
|
22
|
+
}>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryEntsoe, dayRange } from "../lib/entsoe-client.js";
|
|
3
|
+
import { resolveZone, AVAILABLE_ZONES } from "../lib/zone-codes.js";
|
|
4
|
+
import { ensureArray } from "../lib/xml-parser.js";
|
|
5
|
+
import { TTL } from "../lib/cache.js";
|
|
6
|
+
/** PSR type codes for renewable sources */
|
|
7
|
+
const RENEWABLE_PSR = {
|
|
8
|
+
B16: "Solar",
|
|
9
|
+
B18: "Wind Offshore",
|
|
10
|
+
B19: "Wind Onshore",
|
|
11
|
+
};
|
|
12
|
+
export const renewableForecastSchema = z.object({
|
|
13
|
+
zone: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe(`Country/zone code. Examples: DE, FR, ES. Available: ${AVAILABLE_ZONES}`),
|
|
16
|
+
date: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
20
|
+
});
|
|
21
|
+
export async function getRenewableForecast(params) {
|
|
22
|
+
const eic = resolveZone(params.zone);
|
|
23
|
+
const { periodStart, periodEnd } = dayRange(params.date);
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
const data = await queryEntsoe({
|
|
26
|
+
documentType: "A69",
|
|
27
|
+
processType: "A01",
|
|
28
|
+
in_Domain: eic,
|
|
29
|
+
periodStart,
|
|
30
|
+
periodEnd,
|
|
31
|
+
}, TTL.FORECAST);
|
|
32
|
+
const doc = data.GL_MarketDocument;
|
|
33
|
+
if (!doc)
|
|
34
|
+
throw new Error("No renewable forecast data returned for this zone/date.");
|
|
35
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
36
|
+
const forecastMap = new Map();
|
|
37
|
+
for (const ts of timeSeries) {
|
|
38
|
+
const psrCode = ts.MktPSRType?.psrType ?? "unknown";
|
|
39
|
+
const source = RENEWABLE_PSR[psrCode] ?? psrCode;
|
|
40
|
+
const periods = ensureArray(ts.Period);
|
|
41
|
+
for (const period of periods) {
|
|
42
|
+
const points = ensureArray(period.Point);
|
|
43
|
+
for (const point of points) {
|
|
44
|
+
const position = Number(point.position);
|
|
45
|
+
const mw = Number(point.quantity ?? 0);
|
|
46
|
+
if (!forecastMap.has(source))
|
|
47
|
+
forecastMap.set(source, []);
|
|
48
|
+
forecastMap.get(source).push({ hour: position - 1, mw });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const forecasts = [];
|
|
53
|
+
for (const [source, points] of forecastMap) {
|
|
54
|
+
points.sort((a, b) => a.hour - b.hour);
|
|
55
|
+
const values = points.map((p) => p.mw);
|
|
56
|
+
const peak_mw = values.length > 0 ? Math.max(...values) : 0;
|
|
57
|
+
const total_mwh = values.reduce((s, v) => s + v, 0);
|
|
58
|
+
const psrEntry = Object.entries(RENEWABLE_PSR).find(([, v]) => v === source);
|
|
59
|
+
forecasts.push({
|
|
60
|
+
source,
|
|
61
|
+
psr_code: psrEntry ? psrEntry[0] : "unknown",
|
|
62
|
+
forecast: points,
|
|
63
|
+
peak_mw,
|
|
64
|
+
total_mwh,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
forecasts.sort((a, b) => b.peak_mw - a.peak_mw);
|
|
68
|
+
const total_peak_mw = forecasts.reduce((s, f) => s + f.peak_mw, 0);
|
|
69
|
+
return {
|
|
70
|
+
zone: params.zone.toUpperCase(),
|
|
71
|
+
date: params.date ?? new Date().toISOString().slice(0, 10),
|
|
72
|
+
forecasts,
|
|
73
|
+
total_peak_mw,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const rteFranceSchema: z.ZodObject<{
|
|
3
|
+
dataset: z.ZodEnum<{
|
|
4
|
+
generation: "generation";
|
|
5
|
+
outages: "outages";
|
|
6
|
+
consumption: "consumption";
|
|
7
|
+
exchanges: "exchanges";
|
|
8
|
+
}>;
|
|
9
|
+
date: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
interface GenerationRecord {
|
|
12
|
+
timestamp: string;
|
|
13
|
+
nuclear_mw: number;
|
|
14
|
+
wind_mw: number;
|
|
15
|
+
solar_mw: number;
|
|
16
|
+
hydro_mw: number;
|
|
17
|
+
gas_mw: number;
|
|
18
|
+
coal_mw: number;
|
|
19
|
+
bioenergy_mw: number;
|
|
20
|
+
total_mw: number;
|
|
21
|
+
}
|
|
22
|
+
interface FranceGenerationResult {
|
|
23
|
+
dataset: "generation";
|
|
24
|
+
source: string;
|
|
25
|
+
date: string;
|
|
26
|
+
records: GenerationRecord[];
|
|
27
|
+
latest: GenerationRecord | null;
|
|
28
|
+
}
|
|
29
|
+
interface ConsumptionRecord {
|
|
30
|
+
timestamp: string;
|
|
31
|
+
consumption_mw: number;
|
|
32
|
+
forecast_mw: number;
|
|
33
|
+
}
|
|
34
|
+
interface FranceConsumptionResult {
|
|
35
|
+
dataset: "consumption";
|
|
36
|
+
source: string;
|
|
37
|
+
date: string;
|
|
38
|
+
records: ConsumptionRecord[];
|
|
39
|
+
}
|
|
40
|
+
interface ExchangeRecord {
|
|
41
|
+
timestamp: string;
|
|
42
|
+
gb_mw: number;
|
|
43
|
+
spain_mw: number;
|
|
44
|
+
italy_mw: number;
|
|
45
|
+
switzerland_mw: number;
|
|
46
|
+
germany_belgium_mw: number;
|
|
47
|
+
net_mw: number;
|
|
48
|
+
}
|
|
49
|
+
interface FranceExchangesResult {
|
|
50
|
+
dataset: "exchanges";
|
|
51
|
+
source: string;
|
|
52
|
+
date: string;
|
|
53
|
+
records: ExchangeRecord[];
|
|
54
|
+
}
|
|
55
|
+
interface OutageRecord {
|
|
56
|
+
unit_name: string;
|
|
57
|
+
fuel_type: string;
|
|
58
|
+
unavailable_mw: number;
|
|
59
|
+
available_mw: number;
|
|
60
|
+
start_date: string;
|
|
61
|
+
end_date: string;
|
|
62
|
+
reason: string;
|
|
63
|
+
}
|
|
64
|
+
interface FranceOutagesResult {
|
|
65
|
+
dataset: "outages";
|
|
66
|
+
source: string;
|
|
67
|
+
description: string;
|
|
68
|
+
records: OutageRecord[];
|
|
69
|
+
}
|
|
70
|
+
type RteFranceResult = FranceGenerationResult | FranceConsumptionResult | FranceExchangesResult | FranceOutagesResult;
|
|
71
|
+
export declare function getRteFrance(params: z.infer<typeof rteFranceSchema>): Promise<RteFranceResult>;
|
|
72
|
+
export {};
|