@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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +488 -0
  5. package/dist/lib/audit.d.ts +3 -0
  6. package/dist/lib/audit.js +66 -0
  7. package/dist/lib/auth.d.ts +26 -0
  8. package/dist/lib/auth.js +199 -0
  9. package/dist/lib/cache.d.ts +25 -0
  10. package/dist/lib/cache.js +38 -0
  11. package/dist/lib/cli.d.ts +1 -0
  12. package/dist/lib/cli.js +10 -0
  13. package/dist/lib/corine.d.ts +31 -0
  14. package/dist/lib/corine.js +137 -0
  15. package/dist/lib/eea-natura2000.d.ts +7 -0
  16. package/dist/lib/eea-natura2000.js +53 -0
  17. package/dist/lib/entsoe-client.d.ts +22 -0
  18. package/dist/lib/entsoe-client.js +69 -0
  19. package/dist/lib/gis-sources.d.ts +33 -0
  20. package/dist/lib/gis-sources.js +392 -0
  21. package/dist/lib/natural-england.d.ts +27 -0
  22. package/dist/lib/natural-england.js +105 -0
  23. package/dist/lib/neso-gsp.d.ts +18 -0
  24. package/dist/lib/neso-gsp.js +113 -0
  25. package/dist/lib/overpass.d.ts +13 -0
  26. package/dist/lib/overpass.js +193 -0
  27. package/dist/lib/profiles.d.ts +23 -0
  28. package/dist/lib/profiles.js +149 -0
  29. package/dist/lib/schema-guard.d.ts +22 -0
  30. package/dist/lib/schema-guard.js +38 -0
  31. package/dist/lib/tool-handler.d.ts +15 -0
  32. package/dist/lib/tool-handler.js +95 -0
  33. package/dist/lib/xml-parser.d.ts +4 -0
  34. package/dist/lib/xml-parser.js +34 -0
  35. package/dist/lib/zone-codes.d.ts +12 -0
  36. package/dist/lib/zone-codes.js +127 -0
  37. package/dist/tools/acer-remit.d.ts +60 -0
  38. package/dist/tools/acer-remit.js +154 -0
  39. package/dist/tools/agricultural-land.d.ts +31 -0
  40. package/dist/tools/agricultural-land.js +210 -0
  41. package/dist/tools/ancillary-prices.d.ts +27 -0
  42. package/dist/tools/ancillary-prices.js +70 -0
  43. package/dist/tools/auctions.d.ts +15 -0
  44. package/dist/tools/auctions.js +89 -0
  45. package/dist/tools/balancing-actions.d.ts +22 -0
  46. package/dist/tools/balancing-actions.js +151 -0
  47. package/dist/tools/balancing.d.ts +21 -0
  48. package/dist/tools/balancing.js +56 -0
  49. package/dist/tools/carbon.d.ts +21 -0
  50. package/dist/tools/carbon.js +68 -0
  51. package/dist/tools/commodity-prices.d.ts +26 -0
  52. package/dist/tools/commodity-prices.js +100 -0
  53. package/dist/tools/compare-sites.d.ts +41 -0
  54. package/dist/tools/compare-sites.js +237 -0
  55. package/dist/tools/demand-forecast.d.ts +21 -0
  56. package/dist/tools/demand-forecast.js +56 -0
  57. package/dist/tools/elexon-bmrs.d.ts +72 -0
  58. package/dist/tools/elexon-bmrs.js +117 -0
  59. package/dist/tools/energi-data.d.ts +72 -0
  60. package/dist/tools/energi-data.js +170 -0
  61. package/dist/tools/energy-charts.d.ts +103 -0
  62. package/dist/tools/energy-charts.js +411 -0
  63. package/dist/tools/entsog.d.ts +71 -0
  64. package/dist/tools/entsog.js +159 -0
  65. package/dist/tools/era5-weather.d.ts +39 -0
  66. package/dist/tools/era5-weather.js +117 -0
  67. package/dist/tools/eu-gas-price.d.ts +38 -0
  68. package/dist/tools/eu-gas-price.js +110 -0
  69. package/dist/tools/fingrid.d.ts +39 -0
  70. package/dist/tools/fingrid.js +158 -0
  71. package/dist/tools/flood-risk.d.ts +33 -0
  72. package/dist/tools/flood-risk.js +166 -0
  73. package/dist/tools/flows.d.ts +23 -0
  74. package/dist/tools/flows.js +61 -0
  75. package/dist/tools/frequency.d.ts +10 -0
  76. package/dist/tools/frequency.js +35 -0
  77. package/dist/tools/gas-storage.d.ts +18 -0
  78. package/dist/tools/gas-storage.js +72 -0
  79. package/dist/tools/generation.d.ts +17 -0
  80. package/dist/tools/generation.js +80 -0
  81. package/dist/tools/grid-connection-intelligence.d.ts +42 -0
  82. package/dist/tools/grid-connection-intelligence.js +122 -0
  83. package/dist/tools/grid-connection-queue.d.ts +64 -0
  84. package/dist/tools/grid-connection-queue.js +198 -0
  85. package/dist/tools/grid-proximity.d.ts +38 -0
  86. package/dist/tools/grid-proximity.js +123 -0
  87. package/dist/tools/hydro-inflows.d.ts +34 -0
  88. package/dist/tools/hydro-inflows.js +114 -0
  89. package/dist/tools/hydro.d.ts +18 -0
  90. package/dist/tools/hydro.js +85 -0
  91. package/dist/tools/imbalance-prices.d.ts +21 -0
  92. package/dist/tools/imbalance-prices.js +56 -0
  93. package/dist/tools/intraday-prices.d.ts +21 -0
  94. package/dist/tools/intraday-prices.js +57 -0
  95. package/dist/tools/intraday-spread.d.ts +24 -0
  96. package/dist/tools/intraday-spread.js +55 -0
  97. package/dist/tools/land-constraints.d.ts +25 -0
  98. package/dist/tools/land-constraints.js +148 -0
  99. package/dist/tools/land-cover.d.ts +18 -0
  100. package/dist/tools/land-cover.js +64 -0
  101. package/dist/tools/lng-terminals.d.ts +22 -0
  102. package/dist/tools/lng-terminals.js +75 -0
  103. package/dist/tools/net-positions.d.ts +19 -0
  104. package/dist/tools/net-positions.js +74 -0
  105. package/dist/tools/nordpool-prices.d.ts +29 -0
  106. package/dist/tools/nordpool-prices.js +80 -0
  107. package/dist/tools/outages.d.ts +28 -0
  108. package/dist/tools/outages.js +107 -0
  109. package/dist/tools/power-plants.d.ts +26 -0
  110. package/dist/tools/power-plants.js +224 -0
  111. package/dist/tools/price-spread-analysis.d.ts +27 -0
  112. package/dist/tools/price-spread-analysis.js +97 -0
  113. package/dist/tools/prices.d.ts +23 -0
  114. package/dist/tools/prices.js +79 -0
  115. package/dist/tools/realtime-generation.d.ts +19 -0
  116. package/dist/tools/realtime-generation.js +141 -0
  117. package/dist/tools/ree-esios.d.ts +78 -0
  118. package/dist/tools/ree-esios.js +216 -0
  119. package/dist/tools/regelleistung.d.ts +28 -0
  120. package/dist/tools/regelleistung.js +71 -0
  121. package/dist/tools/remit-messages.d.ts +23 -0
  122. package/dist/tools/remit-messages.js +110 -0
  123. package/dist/tools/renewable-forecast.d.ts +23 -0
  124. package/dist/tools/renewable-forecast.js +75 -0
  125. package/dist/tools/rte-france.d.ts +72 -0
  126. package/dist/tools/rte-france.js +147 -0
  127. package/dist/tools/screen-site.d.ts +50 -0
  128. package/dist/tools/screen-site.js +288 -0
  129. package/dist/tools/site-revenue.d.ts +50 -0
  130. package/dist/tools/site-revenue.js +147 -0
  131. package/dist/tools/smard-data.d.ts +34 -0
  132. package/dist/tools/smard-data.js +155 -0
  133. package/dist/tools/solar.d.ts +23 -0
  134. package/dist/tools/solar.js +69 -0
  135. package/dist/tools/stormglass.d.ts +56 -0
  136. package/dist/tools/stormglass.js +172 -0
  137. package/dist/tools/terna.d.ts +69 -0
  138. package/dist/tools/terna.js +159 -0
  139. package/dist/tools/terrain-analysis.d.ts +19 -0
  140. package/dist/tools/terrain-analysis.js +120 -0
  141. package/dist/tools/transfer-capacity.d.ts +22 -0
  142. package/dist/tools/transfer-capacity.js +61 -0
  143. package/dist/tools/transmission.d.ts +29 -0
  144. package/dist/tools/transmission.js +159 -0
  145. package/dist/tools/uk-carbon.d.ts +51 -0
  146. package/dist/tools/uk-carbon.js +109 -0
  147. package/dist/tools/uk-grid.d.ts +28 -0
  148. package/dist/tools/uk-grid.js +70 -0
  149. package/dist/tools/us-gas.d.ts +30 -0
  150. package/dist/tools/us-gas.js +100 -0
  151. package/dist/tools/verify-gis-sources.d.ts +25 -0
  152. package/dist/tools/verify-gis-sources.js +119 -0
  153. package/dist/tools/weather.d.ts +27 -0
  154. package/dist/tools/weather.js +120 -0
  155. package/package.json +62 -0
@@ -0,0 +1,224 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ const CONVENTIONAL_URL = "https://data.open-power-system-data.org/conventional_power_plants/latest/conventional_power_plants_EU.csv";
4
+ const RENEWABLE_URL = "https://data.open-power-system-data.org/renewable_power_plants/latest/renewable_power_plants_EU_DE.csv";
5
+ // NESO publishes GB generation projects via two registers:
6
+ // - TEC Register: transmission-connected (large) plants
7
+ // - Embedded Register: distribution-connected (smaller/medium) plants
8
+ const NESO_TEC_URL = "https://api.neso.energy/api/3/action/datastore_search?resource_id=17becbab-e3e8-473f-b303-3806f43a6a10&limit=5000";
9
+ const NESO_EMBEDDED_URL = "https://api.neso.energy/api/3/action/package_show?id=embedded-register";
10
+ const cache = new TtlCache();
11
+ export const powerPlantsSchema = z.object({
12
+ country: z
13
+ .string()
14
+ .optional()
15
+ .describe("ISO country code to filter (e.g. DE, FR, GB). Returns all if omitted."),
16
+ fuel_type: z
17
+ .string()
18
+ .optional()
19
+ .describe("Fuel/energy type filter (e.g. Natural gas, Hard coal, Solar, Wind). Case-insensitive partial match."),
20
+ min_capacity_mw: z
21
+ .number()
22
+ .optional()
23
+ .describe("Minimum capacity in MW to include. Defaults to 0."),
24
+ });
25
+ function parseCsvRows(csv) {
26
+ const lines = csv.split("\n");
27
+ if (lines.length < 2)
28
+ return [];
29
+ const headers = parseCsvLine(lines[0]);
30
+ const rows = [];
31
+ for (let i = 1; i < lines.length; i++) {
32
+ const line = lines[i].trim();
33
+ if (!line)
34
+ continue;
35
+ const values = parseCsvLine(line);
36
+ const row = {};
37
+ for (let j = 0; j < headers.length; j++) {
38
+ row[headers[j]] = values[j] ?? "";
39
+ }
40
+ rows.push(row);
41
+ }
42
+ return rows;
43
+ }
44
+ function parseCsvLine(line) {
45
+ const result = [];
46
+ let current = "";
47
+ let inQuotes = false;
48
+ for (let i = 0; i < line.length; i++) {
49
+ const char = line[i];
50
+ if (char === '"') {
51
+ if (inQuotes && line[i + 1] === '"') {
52
+ current += '"';
53
+ i++;
54
+ }
55
+ else {
56
+ inQuotes = !inQuotes;
57
+ }
58
+ }
59
+ else if (char === "," && !inQuotes) {
60
+ result.push(current.trim());
61
+ current = "";
62
+ }
63
+ else {
64
+ current += char;
65
+ }
66
+ }
67
+ result.push(current.trim());
68
+ return result;
69
+ }
70
+ async function fetchAndParsePlants(url, type) {
71
+ const cacheKey = `opsd:${type}`;
72
+ const cached = cache.get(cacheKey);
73
+ if (cached)
74
+ return cached;
75
+ const response = await fetch(url);
76
+ if (!response.ok) {
77
+ throw new Error(`Open Power System Data returned ${response.status} for ${type} plants.`);
78
+ }
79
+ const csv = await response.text();
80
+ const rows = parseCsvRows(csv);
81
+ const plants = [];
82
+ for (const row of rows) {
83
+ const name = row["name"] ?? row["project_name"] ?? "";
84
+ const country = row["country"] ?? "";
85
+ const capacityStr = row["capacity_net_bnetza"] ?? row["capacity_gross_uba"] ??
86
+ row["electrical_capacity"] ?? row["capacity"] ?? "";
87
+ const capacity = parseFloat(capacityStr);
88
+ if (isNaN(capacity) || capacity <= 0)
89
+ continue;
90
+ const fuel = row["energy_source"] ?? row["fuel"] ?? row["technology"] ?? "";
91
+ const lat = parseFloat(row["lat"] ?? "");
92
+ const lon = parseFloat(row["lon"] ?? "");
93
+ const yearStr = row["commissioned"] ?? row["commissioning_date"] ?? "";
94
+ const year = parseInt(yearStr.slice(0, 4), 10);
95
+ plants.push({
96
+ name: name || "Unknown",
97
+ country: country.toUpperCase(),
98
+ capacity_mw: Math.round(capacity * 10) / 10,
99
+ fuel: fuel || "Unknown",
100
+ lat: isNaN(lat) ? null : lat,
101
+ lon: isNaN(lon) ? null : lon,
102
+ commissioned_year: isNaN(year) ? null : year,
103
+ });
104
+ }
105
+ cache.set(cacheKey, plants, TTL.STATIC_DATA);
106
+ return plants;
107
+ }
108
+ function dedupePlants(plants) {
109
+ const seen = new Set();
110
+ return plants.filter((plant) => {
111
+ const key = `${plant.country}:${plant.name}:${plant.capacity_mw}`;
112
+ if (seen.has(key))
113
+ return false;
114
+ seen.add(key);
115
+ return true;
116
+ });
117
+ }
118
+ async function fetchGbPlantsFromNeso() {
119
+ const cacheKey = "neso:gb-plants";
120
+ const cached = cache.get(cacheKey);
121
+ if (cached)
122
+ return cached;
123
+ const plants = [];
124
+ // Fetch TEC register (transmission-connected)
125
+ try {
126
+ const tecResponse = await fetch(NESO_TEC_URL);
127
+ if (tecResponse.ok) {
128
+ const json = await tecResponse.json();
129
+ const records = json.result?.records ?? [];
130
+ for (const r of records) {
131
+ const mw = parseFloat(r["MW Connected"] ?? r["Cumulative Total Capacity (MW)"] ?? "0");
132
+ if (isNaN(mw) || mw <= 0)
133
+ continue;
134
+ plants.push({
135
+ name: r["Project Name"] ?? "Unknown",
136
+ country: "GB",
137
+ capacity_mw: Math.round(mw * 10) / 10,
138
+ fuel: r["Plant Type"] ?? "Unknown",
139
+ lat: null,
140
+ lon: null,
141
+ commissioned_year: null,
142
+ });
143
+ }
144
+ }
145
+ }
146
+ catch {
147
+ // TEC fetch failed, continue with embedded
148
+ }
149
+ // Fetch Embedded register (distribution-connected)
150
+ try {
151
+ const pkgResponse = await fetch(NESO_EMBEDDED_URL);
152
+ if (pkgResponse.ok) {
153
+ const pkgJson = await pkgResponse.json();
154
+ const resources = pkgJson.result?.resources ?? [];
155
+ const csvResource = resources.find((r) => r.format === "CSV");
156
+ if (csvResource?.url) {
157
+ const csvResponse = await fetch(csvResource.url);
158
+ if (csvResponse.ok) {
159
+ const csv = await csvResponse.text();
160
+ const rows = parseCsvRows(csv);
161
+ for (const row of rows) {
162
+ const mw = parseFloat(row["MW Connected"] ?? row["Cumulative Total Capacity (MW)"] ?? "0");
163
+ if (isNaN(mw) || mw <= 0)
164
+ continue;
165
+ plants.push({
166
+ name: row["Project Name"] ?? "Unknown",
167
+ country: "GB",
168
+ capacity_mw: Math.round(mw * 10) / 10,
169
+ fuel: row["Plant Type"] ?? "Unknown",
170
+ lat: null,
171
+ lon: null,
172
+ commissioned_year: null,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ catch {
180
+ // Embedded fetch failed
181
+ }
182
+ const deduped = dedupePlants(plants);
183
+ cache.set(cacheKey, deduped, TTL.STATIC_DATA);
184
+ return deduped;
185
+ }
186
+ export async function getPowerPlants(params) {
187
+ const minCap = params.min_capacity_mw ?? 0;
188
+ // Fetch both datasets concurrently
189
+ const [conventional, renewable] = await Promise.all([
190
+ fetchAndParsePlants(CONVENTIONAL_URL, "conventional").catch(() => []),
191
+ fetchAndParsePlants(RENEWABLE_URL, "renewable").catch(() => []),
192
+ ]);
193
+ let plants = [...conventional, ...renewable];
194
+ const shouldIncludeGb = !params.country || params.country.toUpperCase() === "GB";
195
+ if (shouldIncludeGb) {
196
+ const gbPlants = await fetchGbPlantsFromNeso();
197
+ plants = dedupePlants([...plants, ...gbPlants]);
198
+ }
199
+ // Apply filters
200
+ if (params.country) {
201
+ const countryUpper = params.country.toUpperCase();
202
+ plants = plants.filter((p) => p.country === countryUpper);
203
+ }
204
+ if (params.fuel_type) {
205
+ const fuelLower = params.fuel_type.toLowerCase();
206
+ plants = plants.filter((p) => p.fuel.toLowerCase().includes(fuelLower));
207
+ }
208
+ plants = plants.filter((p) => p.capacity_mw >= minCap);
209
+ // Sort by capacity descending
210
+ plants.sort((a, b) => b.capacity_mw - a.capacity_mw);
211
+ // Limit to top 200 to avoid huge responses
212
+ const limited = plants.slice(0, 200);
213
+ const total_capacity_mw = Math.round(plants.reduce((s, p) => s + p.capacity_mw, 0) * 10) / 10;
214
+ return {
215
+ filters: {
216
+ country: params.country?.toUpperCase(),
217
+ fuel_type: params.fuel_type,
218
+ min_capacity_mw: minCap,
219
+ },
220
+ total_count: plants.length,
221
+ plants: limited,
222
+ total_capacity_mw,
223
+ };
224
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ export declare const priceSpreadAnalysisSchema: z.ZodObject<{
3
+ zone: z.ZodString;
4
+ date: z.ZodOptional<z.ZodString>;
5
+ efficiency: z.ZodOptional<z.ZodNumber>;
6
+ cycles: z.ZodOptional<z.ZodNumber>;
7
+ }, z.core.$strip>;
8
+ interface ScheduleEntry {
9
+ hour: number;
10
+ price: number;
11
+ action: "charge" | "discharge" | "hold";
12
+ }
13
+ type ArbSignal = "strong_arb" | "moderate_arb" | "weak_arb" | "no_arb";
14
+ export declare function getPriceSpreadAnalysis(params: z.infer<typeof priceSpreadAnalysisSchema>): Promise<{
15
+ zone: string;
16
+ date: string;
17
+ efficiency: number;
18
+ targetCycles: number;
19
+ grossSpread: number;
20
+ netSpread: number;
21
+ revenuePerMwDay: number;
22
+ signal: ArbSignal;
23
+ peakPrice: number;
24
+ offPeakPrice: number;
25
+ schedule: ScheduleEntry[];
26
+ }>;
27
+ export {};
@@ -0,0 +1,97 @@
1
+ import { z } from "zod";
2
+ import { getDayAheadPrices } from "./prices.js";
3
+ import { AVAILABLE_ZONES } from "../lib/zone-codes.js";
4
+ export const priceSpreadAnalysisSchema = z.object({
5
+ zone: z
6
+ .string()
7
+ .describe(`Bidding zone code. Examples: DE, FR, GB. Available: ${AVAILABLE_ZONES}`),
8
+ date: z
9
+ .string()
10
+ .optional()
11
+ .describe("Date in YYYY-MM-DD format. Defaults to today."),
12
+ efficiency: z
13
+ .number()
14
+ .optional()
15
+ .describe("Round-trip efficiency of the BESS (0-1). Defaults to 0.88."),
16
+ cycles: z
17
+ .number()
18
+ .optional()
19
+ .describe("Target charge/discharge cycles per day. Defaults to 2."),
20
+ });
21
+ export async function getPriceSpreadAnalysis(params) {
22
+ const efficiency = params.efficiency ?? 0.88;
23
+ const cycles = params.cycles ?? 2;
24
+ const date = params.date ?? new Date().toISOString().slice(0, 10);
25
+ const priceData = await getDayAheadPrices({
26
+ zone: params.zone,
27
+ start_date: date,
28
+ });
29
+ const prices = priceData.prices;
30
+ if (prices.length === 0) {
31
+ return {
32
+ zone: params.zone.toUpperCase(),
33
+ date,
34
+ efficiency,
35
+ targetCycles: cycles,
36
+ grossSpread: 0,
37
+ netSpread: 0,
38
+ revenuePerMwDay: 0,
39
+ signal: "no_arb",
40
+ peakPrice: 0,
41
+ offPeakPrice: 0,
42
+ schedule: [],
43
+ };
44
+ }
45
+ // Sort by price to find cheapest/most expensive hours
46
+ const sorted = [...prices].sort((a, b) => a.price_eur_mwh - b.price_eur_mwh);
47
+ const chargeCount = Math.min(cycles, sorted.length);
48
+ const dischargeCount = Math.min(cycles, sorted.length);
49
+ const chargeHours = new Set(sorted.slice(0, chargeCount).map((p) => p.hour));
50
+ const dischargeHours = new Set(sorted.slice(-dischargeCount).map((p) => p.hour));
51
+ // Resolve conflicts: if same hour appears in both, remove from charge
52
+ for (const h of chargeHours) {
53
+ if (dischargeHours.has(h)) {
54
+ chargeHours.delete(h);
55
+ }
56
+ }
57
+ const chargeAvg = sorted.slice(0, chargeCount).reduce((s, p) => s + p.price_eur_mwh, 0) / chargeCount;
58
+ const dischargeAvg = sorted.slice(-dischargeCount).reduce((s, p) => s + p.price_eur_mwh, 0) / dischargeCount;
59
+ const grossSpread = Math.round((dischargeAvg - chargeAvg) * 100) / 100;
60
+ // Net spread: discharge_price * efficiency - charge_price (efficiency loss on discharge side)
61
+ const netSpread = Math.round((dischargeAvg * efficiency - chargeAvg) * 100) / 100;
62
+ const revenuePerMwDay = Math.round(netSpread * cycles * 100) / 100;
63
+ let signal;
64
+ if (netSpread > 30)
65
+ signal = "strong_arb";
66
+ else if (netSpread > 15)
67
+ signal = "moderate_arb";
68
+ else if (netSpread > 5)
69
+ signal = "weak_arb";
70
+ else
71
+ signal = "no_arb";
72
+ const schedule = prices.map((p) => ({
73
+ hour: p.hour,
74
+ price: Math.round(p.price_eur_mwh * 100) / 100,
75
+ action: chargeHours.has(p.hour)
76
+ ? "charge"
77
+ : dischargeHours.has(p.hour)
78
+ ? "discharge"
79
+ : "hold",
80
+ }));
81
+ schedule.sort((a, b) => a.hour - b.hour);
82
+ const peakPrice = Math.round(Math.max(...prices.map((p) => p.price_eur_mwh)) * 100) / 100;
83
+ const offPeakPrice = Math.round(Math.min(...prices.map((p) => p.price_eur_mwh)) * 100) / 100;
84
+ return {
85
+ zone: params.zone.toUpperCase(),
86
+ date,
87
+ efficiency,
88
+ targetCycles: cycles,
89
+ grossSpread,
90
+ netSpread,
91
+ revenuePerMwDay,
92
+ signal,
93
+ peakPrice,
94
+ offPeakPrice,
95
+ schedule,
96
+ };
97
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ export declare const pricesSchema: z.ZodObject<{
3
+ zone: z.ZodString;
4
+ start_date: z.ZodOptional<z.ZodString>;
5
+ end_date: z.ZodOptional<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ interface PricePoint {
8
+ hour: number;
9
+ price_eur_mwh: number;
10
+ }
11
+ export declare function getDayAheadPrices(params: z.infer<typeof pricesSchema>): Promise<{
12
+ zone: string;
13
+ start_date: string;
14
+ end_date: string;
15
+ currency: string;
16
+ prices: PricePoint[];
17
+ stats: {
18
+ min: number;
19
+ max: number;
20
+ mean: number;
21
+ };
22
+ }>;
23
+ export {};
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { queryEntsoe, dayRange, formatEntsoeDate } from "../lib/entsoe-client.js";
3
+ import { resolvePriceZone, AVAILABLE_ZONES } from "../lib/zone-codes.js";
4
+ import { ensureArray } from "../lib/xml-parser.js";
5
+ import { TTL } from "../lib/cache.js";
6
+ export const pricesSchema = z.object({
7
+ zone: z
8
+ .string()
9
+ .describe(`Bidding zone code. Examples: DE, FR, GB. Available: ${AVAILABLE_ZONES}`),
10
+ start_date: z
11
+ .string()
12
+ .optional()
13
+ .describe("Start date YYYY-MM-DD. Defaults to today."),
14
+ end_date: z
15
+ .string()
16
+ .optional()
17
+ .describe("End date YYYY-MM-DD. Defaults to start_date + 1 day."),
18
+ });
19
+ export async function getDayAheadPrices(params) {
20
+ const eic = resolvePriceZone(params.zone);
21
+ let periodStart;
22
+ let periodEnd;
23
+ if (params.start_date) {
24
+ const startDt = new Date(params.start_date + "T00:00:00Z");
25
+ periodStart = formatEntsoeDate(startDt);
26
+ if (params.end_date) {
27
+ const endDt = new Date(params.end_date + "T00:00:00Z");
28
+ periodEnd = formatEntsoeDate(endDt);
29
+ }
30
+ else {
31
+ periodEnd = formatEntsoeDate(new Date(startDt.getTime() + 24 * 60 * 60 * 1000));
32
+ }
33
+ }
34
+ else {
35
+ const range = dayRange();
36
+ periodStart = range.periodStart;
37
+ periodEnd = range.periodEnd;
38
+ }
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ const data = await queryEntsoe({
41
+ documentType: "A44",
42
+ in_Domain: eic,
43
+ out_Domain: eic,
44
+ periodStart,
45
+ periodEnd,
46
+ }, TTL.PRICES);
47
+ const doc = data.Publication_MarketDocument;
48
+ if (!doc)
49
+ throw new Error("No price data returned for this zone/date range.");
50
+ const timeSeries = ensureArray(doc.TimeSeries);
51
+ const prices = [];
52
+ for (const ts of timeSeries) {
53
+ const currency = ts["currency_Unit.name"] ?? "EUR";
54
+ const periods = ensureArray(ts.Period);
55
+ for (const period of periods) {
56
+ const points = ensureArray(period.Point);
57
+ for (const point of points) {
58
+ const position = Number(point.position);
59
+ const price = Number(point["price.amount"]);
60
+ prices.push({ hour: position - 1, price_eur_mwh: price });
61
+ }
62
+ }
63
+ }
64
+ prices.sort((a, b) => a.hour - b.hour);
65
+ const values = prices.map((p) => p.price_eur_mwh);
66
+ const min = Math.min(...values);
67
+ const max = Math.max(...values);
68
+ const mean = values.length > 0
69
+ ? Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 100) / 100
70
+ : 0;
71
+ return {
72
+ zone: params.zone.toUpperCase(),
73
+ start_date: params.start_date ?? new Date().toISOString().slice(0, 10),
74
+ end_date: params.end_date ?? params.start_date ?? new Date().toISOString().slice(0, 10),
75
+ currency: "EUR",
76
+ prices,
77
+ stats: { min, max, mean },
78
+ };
79
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ export declare const realtimeGenerationSchema: z.ZodObject<{
3
+ zone: z.ZodString;
4
+ date: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$strip>;
6
+ interface GenerationEntry {
7
+ fuel: string;
8
+ fuel_code: string;
9
+ generation_mw: number;
10
+ }
11
+ interface RealtimeGenerationResult {
12
+ zone: string;
13
+ date: string;
14
+ generation: GenerationEntry[];
15
+ total_mw: number;
16
+ timestamp: string;
17
+ }
18
+ export declare function getRealtimeGeneration(params: z.infer<typeof realtimeGenerationSchema>): Promise<RealtimeGenerationResult>;
19
+ export {};
@@ -0,0 +1,141 @@
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 { TtlCache, TTL } from "../lib/cache.js";
6
+ const ELEXON_API = "https://data.elexon.co.uk/bmrs/api/v1";
7
+ const cache = new TtlCache();
8
+ export const realtimeGenerationSchema = z.object({
9
+ zone: z
10
+ .string()
11
+ .describe(`Country/zone code. Examples: DE, FR, GB. Available: ${AVAILABLE_ZONES}`),
12
+ date: z
13
+ .string()
14
+ .optional()
15
+ .describe("Date in YYYY-MM-DD format. Defaults to today."),
16
+ });
17
+ /** ENTSO-E PSR type codes to human-readable fuel names */
18
+ const PSR_TYPES = {
19
+ B01: "Biomass",
20
+ B02: "Lignite",
21
+ B04: "Gas",
22
+ B05: "Coal",
23
+ B06: "Oil",
24
+ B09: "Geothermal",
25
+ B10: "Hydro Pumped Storage",
26
+ B11: "Hydro Run-of-river",
27
+ B12: "Hydro Reservoir",
28
+ B14: "Nuclear",
29
+ B15: "Other",
30
+ B16: "Solar",
31
+ B17: "Waste",
32
+ B18: "Wind Offshore",
33
+ B19: "Wind Onshore",
34
+ };
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ async function fetchElexon(path) {
37
+ const url = `${ELEXON_API}${path}`;
38
+ const cached = cache.get(url);
39
+ if (cached)
40
+ return cached;
41
+ const response = await fetch(url);
42
+ if (!response.ok) {
43
+ const body = await response.text();
44
+ throw new Error(`Elexon BMRS API returned ${response.status}: ${body.slice(0, 300)}`);
45
+ }
46
+ const json = await response.json();
47
+ cache.set(url, json, TTL.REALTIME);
48
+ return json;
49
+ }
50
+ async function getGbGeneration(date) {
51
+ const data = await fetchElexon(`/datasets/FUELHH?format=json`);
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ const rows = data?.data ?? [];
54
+ if (rows.length === 0) {
55
+ throw new Error("No GB generation data available from Elexon BMRS.");
56
+ }
57
+ // Group by fuelType, take latest settlement period
58
+ const latestPeriod = Math.max(...rows.map((r) => Number(r.settlementPeriod ?? 0)));
59
+ const latestRows = rows.filter((r) => Number(r.settlementPeriod) === latestPeriod);
60
+ const generation = [];
61
+ for (const row of latestRows) {
62
+ const fuelType = String(row.fuelType ?? "Unknown");
63
+ const mw = Number(row.generation ?? 0);
64
+ if (mw > 0) {
65
+ generation.push({
66
+ fuel: fuelType,
67
+ fuel_code: fuelType,
68
+ generation_mw: Math.round(mw),
69
+ });
70
+ }
71
+ }
72
+ generation.sort((a, b) => b.generation_mw - a.generation_mw);
73
+ const total_mw = generation.reduce((sum, g) => sum + g.generation_mw, 0);
74
+ const timestamp = latestRows[0]?.startTime ?? new Date().toISOString();
75
+ return {
76
+ zone: "GB",
77
+ date,
78
+ generation,
79
+ total_mw,
80
+ timestamp,
81
+ };
82
+ }
83
+ async function getEntsoeGeneration(zone, date) {
84
+ const eic = resolveZone(zone);
85
+ const { periodStart, periodEnd } = dayRange(date);
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ const data = await queryEntsoe({
88
+ documentType: "A75",
89
+ processType: "A16",
90
+ in_Domain: eic,
91
+ periodStart,
92
+ periodEnd,
93
+ }, TTL.REALTIME);
94
+ const doc = data.GL_MarketDocument;
95
+ if (!doc)
96
+ throw new Error("No realtime generation data returned for this zone/date.");
97
+ const timeSeries = ensureArray(doc.TimeSeries);
98
+ const generation = [];
99
+ let latestTimestamp = "";
100
+ for (const ts of timeSeries) {
101
+ const psrCode = ts.MktPSRType?.psrType ?? "unknown";
102
+ const fuel = PSR_TYPES[psrCode] ?? psrCode;
103
+ const periods = ensureArray(ts.Period);
104
+ const lastPeriod = periods[periods.length - 1];
105
+ if (!lastPeriod)
106
+ continue;
107
+ // Track the period start for timestamp
108
+ const periodStartStr = lastPeriod.timeInterval?.start ?? "";
109
+ if (periodStartStr > latestTimestamp) {
110
+ latestTimestamp = periodStartStr;
111
+ }
112
+ const points = ensureArray(lastPeriod.Point);
113
+ if (points.length === 0)
114
+ continue;
115
+ const latestPoint = points[points.length - 1];
116
+ const mw = Number(latestPoint.quantity ?? 0);
117
+ if (mw > 0) {
118
+ generation.push({
119
+ fuel,
120
+ fuel_code: psrCode,
121
+ generation_mw: Math.round(mw),
122
+ });
123
+ }
124
+ }
125
+ generation.sort((a, b) => b.generation_mw - a.generation_mw);
126
+ const total_mw = generation.reduce((sum, g) => sum + g.generation_mw, 0);
127
+ return {
128
+ zone: zone.toUpperCase(),
129
+ date: date ?? new Date().toISOString().slice(0, 10),
130
+ generation,
131
+ total_mw,
132
+ timestamp: latestTimestamp || new Date().toISOString(),
133
+ };
134
+ }
135
+ export async function getRealtimeGeneration(params) {
136
+ const date = params.date ?? new Date().toISOString().slice(0, 10);
137
+ if (params.zone.toUpperCase() === "GB") {
138
+ return getGbGeneration(date);
139
+ }
140
+ return getEntsoeGeneration(params.zone, params.date);
141
+ }
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ export declare const reeEsiosSchema: z.ZodObject<{
3
+ dataset: z.ZodEnum<{
4
+ generation: "generation";
5
+ demand: "demand";
6
+ day_ahead_price: "day_ahead_price";
7
+ wind_solar: "wind_solar";
8
+ interconnectors: "interconnectors";
9
+ }>;
10
+ date: z.ZodOptional<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ interface PriceRecord {
13
+ timestamp: string;
14
+ price_eur_mwh: number;
15
+ }
16
+ interface SpainPricesResult {
17
+ dataset: "day_ahead_price";
18
+ source: string;
19
+ date: string;
20
+ records: PriceRecord[];
21
+ stats: {
22
+ min: number;
23
+ max: number;
24
+ mean: number;
25
+ };
26
+ }
27
+ interface DemandRecord {
28
+ timestamp: string;
29
+ forecast_mw: number;
30
+ actual_mw: number;
31
+ error_mw: number;
32
+ }
33
+ interface SpainDemandResult {
34
+ dataset: "demand";
35
+ source: string;
36
+ date: string;
37
+ records: DemandRecord[];
38
+ }
39
+ interface GenMixRecord {
40
+ timestamp: string;
41
+ technology: string;
42
+ generation_mw: number;
43
+ }
44
+ interface SpainGenerationResult {
45
+ dataset: "generation";
46
+ source: string;
47
+ date: string;
48
+ records: GenMixRecord[];
49
+ }
50
+ interface WindSolarRecord {
51
+ timestamp: string;
52
+ wind_forecast_mw: number;
53
+ wind_actual_mw: number;
54
+ solar_forecast_mw: number;
55
+ solar_actual_mw: number;
56
+ }
57
+ interface SpainWindSolarResult {
58
+ dataset: "wind_solar";
59
+ source: string;
60
+ date: string;
61
+ records: WindSolarRecord[];
62
+ }
63
+ interface InterconnectorRecord {
64
+ border: string;
65
+ flow_mw: number;
66
+ direction: string;
67
+ timestamp: string;
68
+ }
69
+ interface SpainInterconnectorsResult {
70
+ dataset: "interconnectors";
71
+ source: string;
72
+ date: string;
73
+ records: InterconnectorRecord[];
74
+ net_import_mw: number;
75
+ }
76
+ type ReeEsiosResult = SpainPricesResult | SpainDemandResult | SpainGenerationResult | SpainWindSolarResult | SpainInterconnectorsResult;
77
+ export declare function getReeEsios(params: z.infer<typeof reeEsiosSchema>): Promise<ReeEsiosResult>;
78
+ export {};