@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,237 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { screenSite, EU_COUNTRY_CODES } from "./screen-site.js";
|
|
3
|
+
// --- Schema ---
|
|
4
|
+
const siteInputSchema = z.object({
|
|
5
|
+
lat: z.number().describe("Latitude (-90 to 90). WGS84."),
|
|
6
|
+
lon: z.number().describe("Longitude (-180 to 180). WGS84."),
|
|
7
|
+
label: z.string().optional().describe("Optional human-readable label for this site."),
|
|
8
|
+
});
|
|
9
|
+
export const compareSitesSchema = z.object({
|
|
10
|
+
sites: z
|
|
11
|
+
.array(siteInputSchema)
|
|
12
|
+
.describe("Array of candidate sites to compare (2-10)."),
|
|
13
|
+
country: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('ISO 3166-1 alpha-2 country code. "GB" and EU member states are supported.'),
|
|
16
|
+
radius_km: z
|
|
17
|
+
.number()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Search radius in km for grid and constraints (default 2, max 10)."),
|
|
20
|
+
});
|
|
21
|
+
// --- Scoring heuristics ---
|
|
22
|
+
/**
|
|
23
|
+
* Explicit, conservative ranking heuristics.
|
|
24
|
+
*
|
|
25
|
+
* 1. Verdict tier: pass (3) > warn (2) > fail (1). Hard constraints disqualify.
|
|
26
|
+
* 2. Solar resource: higher annual irradiance is better (normalised 0-1 within the set).
|
|
27
|
+
* 3. Grid proximity: closer to nearest substation or HV line is better (normalised 0-1, inverted).
|
|
28
|
+
* 4. Terrain flatness: lower slope is better (normalised 0-1, inverted).
|
|
29
|
+
*
|
|
30
|
+
* Weights are explicit constants. The composite score is a weighted sum.
|
|
31
|
+
*/
|
|
32
|
+
const VERDICT_SCORES = { pass: 3, warn: 2, fail: 1 };
|
|
33
|
+
const WEIGHT_VERDICT = 40;
|
|
34
|
+
const WEIGHT_SOLAR = 30;
|
|
35
|
+
const WEIGHT_GRID = 20;
|
|
36
|
+
const WEIGHT_TERRAIN = 10;
|
|
37
|
+
const HEURISTICS_USED = [
|
|
38
|
+
`Verdict tier (weight ${WEIGHT_VERDICT}%): pass=3, warn=2, fail=1. Hard constraints and high flood-planning risk (for example Flood Zone 3) produce fail.`,
|
|
39
|
+
`Solar resource (weight ${WEIGHT_SOLAR}%): annual irradiance (kWh/m2), higher is better. Normalised across candidates.`,
|
|
40
|
+
`Grid proximity (weight ${WEIGHT_GRID}%): min(nearest_substation_km, nearest_line_km), closer is better. Normalised across candidates.`,
|
|
41
|
+
`Terrain flatness (weight ${WEIGHT_TERRAIN}%): slope in degrees, flatter is better. Normalised across candidates.`,
|
|
42
|
+
"Missing data for a dimension scores 0 for that dimension (conservative penalty).",
|
|
43
|
+
"Ties broken by input order (stable sort).",
|
|
44
|
+
];
|
|
45
|
+
const DISCLAIMER = "This is an automated comparison using public data. " +
|
|
46
|
+
"Rankings reflect heuristic scoring, not commercial viability. " +
|
|
47
|
+
"Professional due diligence is required before any development decision.";
|
|
48
|
+
// --- Helpers ---
|
|
49
|
+
/** Normalise a value into 0-1 given min/max of the set. Higher raw = higher normalised. */
|
|
50
|
+
function normaliseHigherBetter(value, min, max) {
|
|
51
|
+
if (value === null)
|
|
52
|
+
return 0;
|
|
53
|
+
if (max === min)
|
|
54
|
+
return 1;
|
|
55
|
+
return (value - min) / (max - min);
|
|
56
|
+
}
|
|
57
|
+
/** Normalise a value into 0-1 given min/max of the set. Lower raw = higher normalised. */
|
|
58
|
+
function normaliseLowerBetter(value, min, max) {
|
|
59
|
+
if (value === null)
|
|
60
|
+
return 0;
|
|
61
|
+
if (max === min)
|
|
62
|
+
return 1;
|
|
63
|
+
return 1 - (value - min) / (max - min);
|
|
64
|
+
}
|
|
65
|
+
function extractSolar(screen) {
|
|
66
|
+
return screen.solar?.annual_irradiance_kwh_m2 ?? null;
|
|
67
|
+
}
|
|
68
|
+
function extractSlope(screen) {
|
|
69
|
+
return screen.terrain?.slope_deg ?? null;
|
|
70
|
+
}
|
|
71
|
+
function extractNearestGrid(screen) {
|
|
72
|
+
const sub = screen.grid?.summary?.nearest_substation_km ?? null;
|
|
73
|
+
const line = screen.grid?.summary?.nearest_line_km ?? null;
|
|
74
|
+
if (sub === null && line === null)
|
|
75
|
+
return null;
|
|
76
|
+
if (sub === null)
|
|
77
|
+
return line;
|
|
78
|
+
if (line === null)
|
|
79
|
+
return sub;
|
|
80
|
+
return Math.min(sub, line);
|
|
81
|
+
}
|
|
82
|
+
function extractDataGaps(screen) {
|
|
83
|
+
const gaps = [];
|
|
84
|
+
if (screen.terrain === null)
|
|
85
|
+
gaps.push("terrain");
|
|
86
|
+
if (screen.grid === null)
|
|
87
|
+
gaps.push("grid");
|
|
88
|
+
if (screen.solar === null)
|
|
89
|
+
gaps.push("solar");
|
|
90
|
+
if (screen.constraints === null)
|
|
91
|
+
gaps.push("constraints");
|
|
92
|
+
if (screen.agricultural_land === null)
|
|
93
|
+
gaps.push("agricultural_land");
|
|
94
|
+
if (screen.flood_risk === null)
|
|
95
|
+
gaps.push("flood_risk");
|
|
96
|
+
return gaps;
|
|
97
|
+
}
|
|
98
|
+
function buildReasoning(ranked) {
|
|
99
|
+
const parts = [];
|
|
100
|
+
if (ranked.verdict === "fail") {
|
|
101
|
+
const categories = ranked.flags.map((f) => f.category).join(", ");
|
|
102
|
+
parts.push(`Failed screening due to: ${categories || "unknown reason"}.`);
|
|
103
|
+
}
|
|
104
|
+
else if (ranked.verdict === "warn") {
|
|
105
|
+
const categories = ranked.flags.map((f) => f.category).join(", ");
|
|
106
|
+
parts.push(`Warnings present: ${categories || "see screen_site flags for details"}.`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
parts.push("No blocking constraints or warnings.");
|
|
110
|
+
}
|
|
111
|
+
if (ranked.solar_kwh_m2 !== null) {
|
|
112
|
+
parts.push(`Solar: ${ranked.solar_kwh_m2} kWh/m2/yr.`);
|
|
113
|
+
}
|
|
114
|
+
if (ranked.nearest_grid_km !== null) {
|
|
115
|
+
parts.push(`Nearest grid: ${ranked.nearest_grid_km.toFixed(1)} km.`);
|
|
116
|
+
}
|
|
117
|
+
if (ranked.slope_deg !== null) {
|
|
118
|
+
parts.push(`Slope: ${ranked.slope_deg.toFixed(1)} deg.`);
|
|
119
|
+
}
|
|
120
|
+
if (ranked.data_gaps.length > 0) {
|
|
121
|
+
parts.push(`Missing data: ${ranked.data_gaps.join(", ")}.`);
|
|
122
|
+
}
|
|
123
|
+
return parts.join(" ");
|
|
124
|
+
}
|
|
125
|
+
// --- Main tool function ---
|
|
126
|
+
export async function compareSites(params) {
|
|
127
|
+
const country = params.country.toUpperCase();
|
|
128
|
+
const radiusKm = params.radius_km;
|
|
129
|
+
if (country !== "GB" && !EU_COUNTRY_CODES.has(country)) {
|
|
130
|
+
throw new Error(`Country "${params.country}" is not supported. "GB" and EU member states are available.`);
|
|
131
|
+
}
|
|
132
|
+
if (params.sites.length < 2) {
|
|
133
|
+
throw new Error("At least 2 sites are required for comparison.");
|
|
134
|
+
}
|
|
135
|
+
if (params.sites.length > 10) {
|
|
136
|
+
throw new Error("Compare at most 10 sites per call.");
|
|
137
|
+
}
|
|
138
|
+
// Screen all sites in parallel
|
|
139
|
+
const screenResults = await Promise.allSettled(params.sites.map((site) => screenSite({
|
|
140
|
+
lat: site.lat,
|
|
141
|
+
lon: site.lon,
|
|
142
|
+
country,
|
|
143
|
+
...(radiusKm !== undefined ? { radius_km: radiusKm } : {}),
|
|
144
|
+
})));
|
|
145
|
+
// Separate successes from failures
|
|
146
|
+
const screened = [];
|
|
147
|
+
const failedSites = [];
|
|
148
|
+
for (let i = 0; i < screenResults.length; i++) {
|
|
149
|
+
const site = params.sites[i];
|
|
150
|
+
const label = site.label ?? `Site ${i + 1}`;
|
|
151
|
+
const outcome = screenResults[i];
|
|
152
|
+
if (outcome.status === "fulfilled") {
|
|
153
|
+
screened.push({ index: i, label, result: outcome.value });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
failedSites.push({
|
|
157
|
+
lat: site.lat,
|
|
158
|
+
lon: site.lon,
|
|
159
|
+
label,
|
|
160
|
+
error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (screened.length === 0) {
|
|
165
|
+
throw new Error("All sites failed screening. Cannot produce a comparison.");
|
|
166
|
+
}
|
|
167
|
+
// Extract raw values for normalisation
|
|
168
|
+
const solarValues = screened.map((s) => extractSolar(s.result));
|
|
169
|
+
const gridValues = screened.map((s) => extractNearestGrid(s.result));
|
|
170
|
+
const slopeValues = screened.map((s) => extractSlope(s.result));
|
|
171
|
+
const validSolars = solarValues.filter((v) => v !== null);
|
|
172
|
+
const validGrids = gridValues.filter((v) => v !== null);
|
|
173
|
+
const validSlopes = slopeValues.filter((v) => v !== null);
|
|
174
|
+
const solarMin = validSolars.length > 0 ? Math.min(...validSolars) : 0;
|
|
175
|
+
const solarMax = validSolars.length > 0 ? Math.max(...validSolars) : 0;
|
|
176
|
+
const gridMin = validGrids.length > 0 ? Math.min(...validGrids) : 0;
|
|
177
|
+
const gridMax = validGrids.length > 0 ? Math.max(...validGrids) : 0;
|
|
178
|
+
const slopeMin = validSlopes.length > 0 ? Math.min(...validSlopes) : 0;
|
|
179
|
+
const slopeMax = validSlopes.length > 0 ? Math.max(...validSlopes) : 0;
|
|
180
|
+
// Score each site
|
|
181
|
+
const scored = screened.map((s, idx) => {
|
|
182
|
+
const verdict = s.result.verdict.overall;
|
|
183
|
+
const solar = solarValues[idx];
|
|
184
|
+
const grid = gridValues[idx];
|
|
185
|
+
const slope = slopeValues[idx];
|
|
186
|
+
const constraintCount = s.result.constraints?.summary?.constraint_count ?? 0;
|
|
187
|
+
const dataGaps = extractDataGaps(s.result);
|
|
188
|
+
const verdictNorm = VERDICT_SCORES[verdict] / 3; // 0.33 - 1.0
|
|
189
|
+
const solarNorm = normaliseHigherBetter(solar, solarMin, solarMax);
|
|
190
|
+
const gridNorm = normaliseLowerBetter(grid, gridMin, gridMax);
|
|
191
|
+
const slopeNorm = normaliseLowerBetter(slope, slopeMin, slopeMax);
|
|
192
|
+
const score = WEIGHT_VERDICT * verdictNorm +
|
|
193
|
+
WEIGHT_SOLAR * solarNorm +
|
|
194
|
+
WEIGHT_GRID * gridNorm +
|
|
195
|
+
WEIGHT_TERRAIN * slopeNorm;
|
|
196
|
+
return {
|
|
197
|
+
index: s.index,
|
|
198
|
+
label: s.label,
|
|
199
|
+
lat: s.result.lat,
|
|
200
|
+
lon: s.result.lon,
|
|
201
|
+
verdict,
|
|
202
|
+
flag_count: s.result.verdict.flags?.length ?? 0,
|
|
203
|
+
flags: s.result.verdict.flags ?? [],
|
|
204
|
+
solar_kwh_m2: solar,
|
|
205
|
+
slope_deg: slope,
|
|
206
|
+
nearest_grid_km: grid,
|
|
207
|
+
constraint_count: constraintCount,
|
|
208
|
+
score: Math.round(score * 100) / 100,
|
|
209
|
+
data_gaps: dataGaps,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
// Sort descending by score, stable (input order breaks ties)
|
|
213
|
+
scored.sort((a, b) => b.score - a.score);
|
|
214
|
+
// Build ranked output with reasoning
|
|
215
|
+
const rankings = scored.map((s, idx) => ({
|
|
216
|
+
rank: idx + 1,
|
|
217
|
+
label: s.label,
|
|
218
|
+
lat: s.lat,
|
|
219
|
+
lon: s.lon,
|
|
220
|
+
verdict: s.verdict,
|
|
221
|
+
flag_count: s.flag_count,
|
|
222
|
+
solar_kwh_m2: s.solar_kwh_m2,
|
|
223
|
+
slope_deg: s.slope_deg,
|
|
224
|
+
nearest_grid_km: s.nearest_grid_km,
|
|
225
|
+
constraint_count: s.constraint_count,
|
|
226
|
+
score: s.score,
|
|
227
|
+
reasoning: buildReasoning(s),
|
|
228
|
+
data_gaps: s.data_gaps,
|
|
229
|
+
}));
|
|
230
|
+
return {
|
|
231
|
+
site_count: params.sites.length,
|
|
232
|
+
rankings,
|
|
233
|
+
failed_sites: failedSites,
|
|
234
|
+
heuristics_used: HEURISTICS_USED,
|
|
235
|
+
disclaimer: DISCLAIMER,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const demandForecastSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface DemandPoint {
|
|
7
|
+
hour: number;
|
|
8
|
+
mw: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function getDemandForecast(params: z.infer<typeof demandForecastSchema>): Promise<{
|
|
11
|
+
zone: string;
|
|
12
|
+
date: string;
|
|
13
|
+
forecast: DemandPoint[];
|
|
14
|
+
stats: {
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
mean: number;
|
|
18
|
+
total_mwh: number;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
export const demandForecastSchema = z.object({
|
|
7
|
+
zone: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe(`Country/zone code. Examples: DE, FR, GB. Available: ${AVAILABLE_ZONES}`),
|
|
10
|
+
date: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
14
|
+
});
|
|
15
|
+
export async function getDemandForecast(params) {
|
|
16
|
+
const eic = resolveZone(params.zone);
|
|
17
|
+
const { periodStart, periodEnd } = dayRange(params.date);
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
const data = await queryEntsoe({
|
|
20
|
+
documentType: "A65",
|
|
21
|
+
processType: "A01",
|
|
22
|
+
outBiddingZone_Domain: eic,
|
|
23
|
+
periodStart,
|
|
24
|
+
periodEnd,
|
|
25
|
+
}, TTL.FORECAST);
|
|
26
|
+
const doc = data.GL_MarketDocument;
|
|
27
|
+
if (!doc)
|
|
28
|
+
throw new Error("No demand forecast data returned for this zone/date.");
|
|
29
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
30
|
+
const forecast = [];
|
|
31
|
+
for (const ts of timeSeries) {
|
|
32
|
+
const periods = ensureArray(ts.Period);
|
|
33
|
+
for (const period of periods) {
|
|
34
|
+
const points = ensureArray(period.Point);
|
|
35
|
+
for (const point of points) {
|
|
36
|
+
const position = Number(point.position);
|
|
37
|
+
const mw = Number(point.quantity ?? 0);
|
|
38
|
+
forecast.push({ hour: position - 1, mw });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
forecast.sort((a, b) => a.hour - b.hour);
|
|
43
|
+
const values = forecast.map((f) => f.mw);
|
|
44
|
+
const min = values.length > 0 ? Math.min(...values) : 0;
|
|
45
|
+
const max = values.length > 0 ? Math.max(...values) : 0;
|
|
46
|
+
const mean = values.length > 0
|
|
47
|
+
? Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 100) / 100
|
|
48
|
+
: 0;
|
|
49
|
+
const total_mwh = values.reduce((s, v) => s + v, 0);
|
|
50
|
+
return {
|
|
51
|
+
zone: params.zone.toUpperCase(),
|
|
52
|
+
date: params.date ?? new Date().toISOString().slice(0, 10),
|
|
53
|
+
forecast,
|
|
54
|
+
stats: { min, max, mean, total_mwh },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const elexonBmrsSchema: z.ZodObject<{
|
|
3
|
+
dataset: z.ZodEnum<{
|
|
4
|
+
imbalance_prices: "imbalance_prices";
|
|
5
|
+
generation_by_fuel: "generation_by_fuel";
|
|
6
|
+
balancing_bids: "balancing_bids";
|
|
7
|
+
system_warnings: "system_warnings";
|
|
8
|
+
interconnector_flows: "interconnector_flows";
|
|
9
|
+
}>;
|
|
10
|
+
date: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
interface ImbalancePriceRecord {
|
|
13
|
+
settlement_date: string;
|
|
14
|
+
settlement_period: number;
|
|
15
|
+
imbalance_price_gbp: number;
|
|
16
|
+
system_sell_price_gbp: number;
|
|
17
|
+
system_buy_price_gbp: number;
|
|
18
|
+
}
|
|
19
|
+
interface ImbalanceResult {
|
|
20
|
+
dataset: "imbalance_prices";
|
|
21
|
+
source: string;
|
|
22
|
+
records: ImbalancePriceRecord[];
|
|
23
|
+
}
|
|
24
|
+
interface FuelGenRecord {
|
|
25
|
+
fuel_type: string;
|
|
26
|
+
generation_mw: number;
|
|
27
|
+
settlement_period: number;
|
|
28
|
+
}
|
|
29
|
+
interface GenerationByFuelResult {
|
|
30
|
+
dataset: "generation_by_fuel";
|
|
31
|
+
source: string;
|
|
32
|
+
date: string;
|
|
33
|
+
records: FuelGenRecord[];
|
|
34
|
+
}
|
|
35
|
+
interface BalancingBidRecord {
|
|
36
|
+
bmu_id: string;
|
|
37
|
+
bid_offer: string;
|
|
38
|
+
level_mw: number;
|
|
39
|
+
price_gbp: number;
|
|
40
|
+
settlement_period: number;
|
|
41
|
+
}
|
|
42
|
+
interface BalancingBidsResult {
|
|
43
|
+
dataset: "balancing_bids";
|
|
44
|
+
source: string;
|
|
45
|
+
date: string;
|
|
46
|
+
records: BalancingBidRecord[];
|
|
47
|
+
}
|
|
48
|
+
interface SystemWarning {
|
|
49
|
+
warning_type: string;
|
|
50
|
+
message: string;
|
|
51
|
+
published: string;
|
|
52
|
+
}
|
|
53
|
+
interface SystemWarningsResult {
|
|
54
|
+
dataset: "system_warnings";
|
|
55
|
+
source: string;
|
|
56
|
+
warnings: SystemWarning[];
|
|
57
|
+
}
|
|
58
|
+
interface InterconnectorRecord {
|
|
59
|
+
interconnector: string;
|
|
60
|
+
flow_mw: number;
|
|
61
|
+
direction: string;
|
|
62
|
+
settlement_period: number;
|
|
63
|
+
}
|
|
64
|
+
interface InterconnectorResult {
|
|
65
|
+
dataset: "interconnector_flows";
|
|
66
|
+
source: string;
|
|
67
|
+
date: string;
|
|
68
|
+
records: InterconnectorRecord[];
|
|
69
|
+
}
|
|
70
|
+
type ElexonResult = ImbalanceResult | GenerationByFuelResult | BalancingBidsResult | SystemWarningsResult | InterconnectorResult;
|
|
71
|
+
export declare function getElexonBmrs(params: z.infer<typeof elexonBmrsSchema>): Promise<ElexonResult>;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const API_BASE = "https://data.elexon.co.uk/bmrs/api/v1";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
export const elexonBmrsSchema = z.object({
|
|
6
|
+
dataset: z
|
|
7
|
+
.enum([
|
|
8
|
+
"imbalance_prices",
|
|
9
|
+
"generation_by_fuel",
|
|
10
|
+
"balancing_bids",
|
|
11
|
+
"system_warnings",
|
|
12
|
+
"interconnector_flows",
|
|
13
|
+
])
|
|
14
|
+
.describe('"imbalance_prices" = GB settlement-period imbalance/cashout prices. ' +
|
|
15
|
+
'"generation_by_fuel" = half-hourly generation by fuel type (MW). ' +
|
|
16
|
+
'"balancing_bids" = balancing mechanism bids and offers. ' +
|
|
17
|
+
'"system_warnings" = system operator warnings and notifications. ' +
|
|
18
|
+
'"interconnector_flows" = cross-border interconnector flows (MW).'),
|
|
19
|
+
date: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
23
|
+
});
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
async function fetchBmrs(path) {
|
|
26
|
+
const url = `${API_BASE}${path}`;
|
|
27
|
+
const cached = cache.get(url);
|
|
28
|
+
if (cached)
|
|
29
|
+
return cached;
|
|
30
|
+
const response = await fetch(url);
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const body = await response.text();
|
|
33
|
+
throw new Error(`Elexon BMRS returned ${response.status}: ${body.slice(0, 300)}`);
|
|
34
|
+
}
|
|
35
|
+
const json = await response.json();
|
|
36
|
+
cache.set(url, json, TTL.BALANCING);
|
|
37
|
+
return json;
|
|
38
|
+
}
|
|
39
|
+
async function fetchImbalancePrices(date) {
|
|
40
|
+
const data = await fetchBmrs(`/balancing/settlement/system-prices?settlementDate=${date}&format=json`);
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
const rows = data?.data ?? [];
|
|
43
|
+
const recent = rows.slice(-24);
|
|
44
|
+
const records = recent.map((r) => ({
|
|
45
|
+
settlement_date: r.settlementDate ?? date,
|
|
46
|
+
settlement_period: Number(r.settlementPeriod ?? 0),
|
|
47
|
+
imbalance_price_gbp: Math.round((Number(r.systemSellPrice ?? 0)) * 100) / 100,
|
|
48
|
+
system_sell_price_gbp: Math.round((Number(r.systemSellPrice ?? 0)) * 100) / 100,
|
|
49
|
+
system_buy_price_gbp: Math.round((Number(r.systemBuyPrice ?? 0)) * 100) / 100,
|
|
50
|
+
}));
|
|
51
|
+
return { dataset: "imbalance_prices", source: "Elexon BMRS", records };
|
|
52
|
+
}
|
|
53
|
+
async function fetchGenerationByFuel(date) {
|
|
54
|
+
const data = await fetchBmrs(`/generation/outturn/summary?settlementDate=${date}&format=json`);
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
const rows = data?.data ?? [];
|
|
57
|
+
const recent = rows.slice(-20);
|
|
58
|
+
const records = recent.map((r) => ({
|
|
59
|
+
fuel_type: r.fuelType ?? "Unknown",
|
|
60
|
+
generation_mw: Math.round(Number(r.currentMw ?? r.generation ?? 0)),
|
|
61
|
+
settlement_period: Number(r.settlementPeriod ?? 0),
|
|
62
|
+
}));
|
|
63
|
+
return { dataset: "generation_by_fuel", source: "Elexon BMRS", date, records };
|
|
64
|
+
}
|
|
65
|
+
async function fetchBalancingBids(date) {
|
|
66
|
+
const data = await fetchBmrs(`/balancing/physical?settlementDate=${date}&format=json`);
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
const rows = data?.data ?? [];
|
|
69
|
+
const recent = rows.slice(-30);
|
|
70
|
+
const records = recent.map((r) => ({
|
|
71
|
+
bmu_id: r.bmUnit ?? r.nationalGridBmUnit ?? "Unknown",
|
|
72
|
+
bid_offer: r.bidOfferPairId ?? "unknown",
|
|
73
|
+
level_mw: Math.round(Number(r.levelMw ?? r.pnLevelMw ?? 0)),
|
|
74
|
+
price_gbp: Math.round((Number(r.price ?? 0)) * 100) / 100,
|
|
75
|
+
settlement_period: Number(r.settlementPeriod ?? 0),
|
|
76
|
+
}));
|
|
77
|
+
return { dataset: "balancing_bids", source: "Elexon BMRS", date, records };
|
|
78
|
+
}
|
|
79
|
+
async function fetchSystemWarnings() {
|
|
80
|
+
const data = await fetchBmrs(`/system/warnings?format=json`);
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
const rows = data?.data ?? [];
|
|
83
|
+
const warnings = rows.slice(0, 20).map((r) => ({
|
|
84
|
+
warning_type: r.warningType ?? "unknown",
|
|
85
|
+
message: r.description ?? r.message ?? "",
|
|
86
|
+
published: r.publishTime ?? r.createdDateTime ?? "",
|
|
87
|
+
}));
|
|
88
|
+
return { dataset: "system_warnings", source: "Elexon BMRS", warnings };
|
|
89
|
+
}
|
|
90
|
+
async function fetchInterconnectors(date) {
|
|
91
|
+
const data = await fetchBmrs(`/generation/outturn/interconnectors?settlementDate=${date}&format=json`);
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
const rows = data?.data ?? [];
|
|
94
|
+
const recent = rows.slice(-20);
|
|
95
|
+
const records = recent.map((r) => ({
|
|
96
|
+
interconnector: r.interconnectorName ?? r.interconnectorId ?? "Unknown",
|
|
97
|
+
flow_mw: Math.round(Number(r.generation ?? r.estimate ?? 0)),
|
|
98
|
+
direction: Number(r.generation ?? 0) >= 0 ? "import" : "export",
|
|
99
|
+
settlement_period: Number(r.settlementPeriod ?? 0),
|
|
100
|
+
}));
|
|
101
|
+
return { dataset: "interconnector_flows", source: "Elexon BMRS", date, records };
|
|
102
|
+
}
|
|
103
|
+
export async function getElexonBmrs(params) {
|
|
104
|
+
const date = params.date ?? new Date().toISOString().slice(0, 10);
|
|
105
|
+
switch (params.dataset) {
|
|
106
|
+
case "imbalance_prices":
|
|
107
|
+
return fetchImbalancePrices(date);
|
|
108
|
+
case "generation_by_fuel":
|
|
109
|
+
return fetchGenerationByFuel(date);
|
|
110
|
+
case "balancing_bids":
|
|
111
|
+
return fetchBalancingBids(date);
|
|
112
|
+
case "system_warnings":
|
|
113
|
+
return fetchSystemWarnings();
|
|
114
|
+
case "interconnector_flows":
|
|
115
|
+
return fetchInterconnectors(date);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const energiDataSchema: z.ZodObject<{
|
|
3
|
+
dataset: z.ZodEnum<{
|
|
4
|
+
co2_emissions: "co2_emissions";
|
|
5
|
+
electricity_production: "electricity_production";
|
|
6
|
+
electricity_prices: "electricity_prices";
|
|
7
|
+
electricity_balance: "electricity_balance";
|
|
8
|
+
}>;
|
|
9
|
+
zone: z.ZodOptional<z.ZodEnum<{
|
|
10
|
+
DK1: "DK1";
|
|
11
|
+
DK2: "DK2";
|
|
12
|
+
DK: "DK";
|
|
13
|
+
}>>;
|
|
14
|
+
date: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
interface Co2Record {
|
|
17
|
+
timestamp: string;
|
|
18
|
+
zone: string;
|
|
19
|
+
co2_gkwh: number;
|
|
20
|
+
}
|
|
21
|
+
interface Co2Result {
|
|
22
|
+
dataset: "co2_emissions";
|
|
23
|
+
source: string;
|
|
24
|
+
zone: string;
|
|
25
|
+
records: Co2Record[];
|
|
26
|
+
}
|
|
27
|
+
interface ProductionRecord {
|
|
28
|
+
timestamp: string;
|
|
29
|
+
zone: string;
|
|
30
|
+
wind_onshore_mw: number;
|
|
31
|
+
wind_offshore_mw: number;
|
|
32
|
+
solar_mw: number;
|
|
33
|
+
thermal_mw: number;
|
|
34
|
+
hydro_mw: number;
|
|
35
|
+
total_mw: number;
|
|
36
|
+
}
|
|
37
|
+
interface ProductionResult {
|
|
38
|
+
dataset: "electricity_production";
|
|
39
|
+
source: string;
|
|
40
|
+
zone: string;
|
|
41
|
+
records: ProductionRecord[];
|
|
42
|
+
}
|
|
43
|
+
interface PriceRecord {
|
|
44
|
+
timestamp: string;
|
|
45
|
+
zone: string;
|
|
46
|
+
price_eur_mwh: number;
|
|
47
|
+
price_dkk_mwh: number;
|
|
48
|
+
}
|
|
49
|
+
interface PricesResult {
|
|
50
|
+
dataset: "electricity_prices";
|
|
51
|
+
source: string;
|
|
52
|
+
zone: string;
|
|
53
|
+
records: PriceRecord[];
|
|
54
|
+
}
|
|
55
|
+
interface BalanceRecord {
|
|
56
|
+
timestamp: string;
|
|
57
|
+
zone: string;
|
|
58
|
+
production_mwh: number;
|
|
59
|
+
consumption_mwh: number;
|
|
60
|
+
import_mwh: number;
|
|
61
|
+
export_mwh: number;
|
|
62
|
+
net_exchange_mwh: number;
|
|
63
|
+
}
|
|
64
|
+
interface BalanceResult {
|
|
65
|
+
dataset: "electricity_balance";
|
|
66
|
+
source: string;
|
|
67
|
+
zone: string;
|
|
68
|
+
records: BalanceRecord[];
|
|
69
|
+
}
|
|
70
|
+
type EnergiDataResult = Co2Result | ProductionResult | PricesResult | BalanceResult;
|
|
71
|
+
export declare function getEnergiData(params: z.infer<typeof energiDataSchema>): Promise<EnergiDataResult>;
|
|
72
|
+
export {};
|