@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,114 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const API_BASE = "https://api.open-meteo.com/v1/forecast";
|
|
4
|
+
const HYDRO_API = "https://archive-api.open-meteo.com/v1/archive";
|
|
5
|
+
const cache = new TtlCache();
|
|
6
|
+
/**
|
|
7
|
+
* Key hydropower basins in Europe with representative coordinates.
|
|
8
|
+
* River discharge data from Open-Meteo (backed by ERA5-Land) serves as
|
|
9
|
+
* a proxy for hydropower inflow conditions.
|
|
10
|
+
*/
|
|
11
|
+
const HYDRO_BASINS = {
|
|
12
|
+
NO: { lat: 60.5, lon: 7.5, description: "Norway (major hydro reservoirs)" },
|
|
13
|
+
SE: { lat: 63.0, lon: 15.0, description: "Sweden (northern hydro)" },
|
|
14
|
+
CH: { lat: 46.8, lon: 8.2, description: "Switzerland (Alpine hydro)" },
|
|
15
|
+
AT: { lat: 47.2, lon: 13.3, description: "Austria (Alpine hydro)" },
|
|
16
|
+
FR: { lat: 45.0, lon: 6.0, description: "France (Alpine + Pyrenees hydro)" },
|
|
17
|
+
IT: { lat: 46.0, lon: 11.0, description: "Italy (Alpine hydro)" },
|
|
18
|
+
ES: { lat: 42.5, lon: -0.5, description: "Spain (Pyrenees + central hydro)" },
|
|
19
|
+
PT: { lat: 41.0, lon: -8.0, description: "Portugal (Douro basin)" },
|
|
20
|
+
FI: { lat: 64.0, lon: 27.0, description: "Finland (northern hydro)" },
|
|
21
|
+
RO: { lat: 45.5, lon: 24.5, description: "Romania (Carpathian hydro)" },
|
|
22
|
+
};
|
|
23
|
+
export const hydroInflowsSchema = z.object({
|
|
24
|
+
country: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe(`Country code for hydropower basin. Available: ${Object.keys(HYDRO_BASINS).join(", ")}. ` +
|
|
27
|
+
"Data uses precipitation, snowmelt, and temperature as proxies for hydro inflow conditions."),
|
|
28
|
+
period: z
|
|
29
|
+
.enum(["recent", "historical"])
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('"recent" = last 7 days of conditions (default). ' +
|
|
32
|
+
'"historical" = last 30 days for trend analysis.'),
|
|
33
|
+
});
|
|
34
|
+
export async function getHydroInflows(params) {
|
|
35
|
+
const code = params.country.toUpperCase();
|
|
36
|
+
const basin = HYDRO_BASINS[code];
|
|
37
|
+
if (!basin) {
|
|
38
|
+
throw new Error(`Unknown country "${params.country}". Available: ${Object.keys(HYDRO_BASINS).join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
const days = params.period === "historical" ? 30 : 7;
|
|
41
|
+
const endDate = new Date();
|
|
42
|
+
endDate.setDate(endDate.getDate() - 1); // yesterday (ERA5 lag)
|
|
43
|
+
const startDate = new Date(endDate);
|
|
44
|
+
startDate.setDate(startDate.getDate() - days);
|
|
45
|
+
const start = startDate.toISOString().slice(0, 10);
|
|
46
|
+
const end = endDate.toISOString().slice(0, 10);
|
|
47
|
+
const url = `${HYDRO_API}?latitude=${basin.lat}&longitude=${basin.lon}` +
|
|
48
|
+
`&start_date=${start}&end_date=${end}` +
|
|
49
|
+
`&daily=precipitation_sum,snowfall_sum,temperature_2m_max,rain_sum` +
|
|
50
|
+
`&timezone=UTC`;
|
|
51
|
+
const cached = cache.get(url);
|
|
52
|
+
if (cached)
|
|
53
|
+
return cached;
|
|
54
|
+
const response = await fetch(url);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const body = await response.text();
|
|
57
|
+
throw new Error(`Open-Meteo Archive returned ${response.status}: ${body.slice(0, 300)}`);
|
|
58
|
+
}
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
const json = await response.json();
|
|
61
|
+
const dailyData = json.daily ?? {};
|
|
62
|
+
const times = dailyData.time ?? [];
|
|
63
|
+
const daily = [];
|
|
64
|
+
let totalPrecip = 0;
|
|
65
|
+
let totalSnow = 0;
|
|
66
|
+
let tempSum = 0;
|
|
67
|
+
let proxySum = 0;
|
|
68
|
+
for (let i = 0; i < times.length; i++) {
|
|
69
|
+
const precip = Number(dailyData.precipitation_sum?.[i] ?? 0);
|
|
70
|
+
const snow = Number(dailyData.snowfall_sum?.[i] ?? 0);
|
|
71
|
+
const tempMax = Number(dailyData.temperature_2m_max?.[i] ?? 0);
|
|
72
|
+
const rain = Number(dailyData.rain_sum?.[i] ?? 0);
|
|
73
|
+
// Inflow proxy: rain + snowmelt contribution (positive temps melt accumulated snow)
|
|
74
|
+
const snowmeltContrib = tempMax > 2 && snow > 0 ? Math.min(snow * 5, tempMax * 2) : 0;
|
|
75
|
+
const inflowProxy = Math.round((rain + snowmeltContrib) * 10) / 10;
|
|
76
|
+
daily.push({
|
|
77
|
+
date: times[i],
|
|
78
|
+
precipitation_mm: Math.round(precip * 10) / 10,
|
|
79
|
+
snowfall_mm: Math.round(snow * 10) / 10,
|
|
80
|
+
temperature_max_c: Math.round(tempMax * 10) / 10,
|
|
81
|
+
rain_mm: Math.round(rain * 10) / 10,
|
|
82
|
+
inflow_proxy_index: inflowProxy,
|
|
83
|
+
});
|
|
84
|
+
totalPrecip += precip;
|
|
85
|
+
totalSnow += snow;
|
|
86
|
+
tempSum += tempMax;
|
|
87
|
+
proxySum += inflowProxy;
|
|
88
|
+
}
|
|
89
|
+
const n = daily.length || 1;
|
|
90
|
+
const avgProxy = Math.round((proxySum / n) * 10) / 10;
|
|
91
|
+
// Determine trend from first half vs second half
|
|
92
|
+
const mid = Math.floor(n / 2);
|
|
93
|
+
const firstHalf = daily.slice(0, mid).reduce((s, d) => s + d.inflow_proxy_index, 0) / (mid || 1);
|
|
94
|
+
const secondHalf = daily.slice(mid).reduce((s, d) => s + d.inflow_proxy_index, 0) / ((n - mid) || 1);
|
|
95
|
+
const trend = secondHalf > firstHalf * 1.2 ? "increasing" : secondHalf < firstHalf * 0.8 ? "decreasing" : "stable";
|
|
96
|
+
const result = {
|
|
97
|
+
source: "Open-Meteo Archive (ERA5-Land reanalysis) — hydro inflow proxy",
|
|
98
|
+
country: code,
|
|
99
|
+
basin_description: basin.description,
|
|
100
|
+
latitude: basin.lat,
|
|
101
|
+
longitude: basin.lon,
|
|
102
|
+
period_days: days,
|
|
103
|
+
daily,
|
|
104
|
+
summary: {
|
|
105
|
+
total_precipitation_mm: Math.round(totalPrecip * 10) / 10,
|
|
106
|
+
total_snowfall_mm: Math.round(totalSnow * 10) / 10,
|
|
107
|
+
avg_temperature_c: Math.round((tempSum / n) * 10) / 10,
|
|
108
|
+
avg_inflow_proxy: avgProxy,
|
|
109
|
+
trend,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
cache.set(url, result, TTL.WEATHER);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const hydroSchema: 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 ReservoirPoint {
|
|
8
|
+
week_start: string;
|
|
9
|
+
stored_energy_mwh: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function getHydroReservoir(params: z.infer<typeof hydroSchema>): Promise<{
|
|
12
|
+
zone: string;
|
|
13
|
+
start_date: string;
|
|
14
|
+
end_date: string;
|
|
15
|
+
reservoir: ReservoirPoint[];
|
|
16
|
+
latest_stored_mwh: number;
|
|
17
|
+
}>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
export const hydroSchema = z.object({
|
|
7
|
+
zone: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe(`Country/zone code. Available: ${AVAILABLE_ZONES}. ` +
|
|
10
|
+
"Best coverage: NO (Norway), SE (Sweden), AT (Austria), CH (Switzerland), ES (Spain), PT (Portugal)."),
|
|
11
|
+
start_date: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Start date in YYYY-MM-DD format. Defaults to 4 weeks ago."),
|
|
15
|
+
end_date: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("End date in YYYY-MM-DD format. Defaults to today."),
|
|
19
|
+
});
|
|
20
|
+
function parsePeriodStepDays(resolution) {
|
|
21
|
+
if (!resolution)
|
|
22
|
+
return 7;
|
|
23
|
+
const weekMatch = /^P(\d+)W$/i.exec(resolution);
|
|
24
|
+
if (weekMatch)
|
|
25
|
+
return Number(weekMatch[1]) * 7;
|
|
26
|
+
const dayMatch = /^P(\d+)D$/i.exec(resolution);
|
|
27
|
+
if (dayMatch)
|
|
28
|
+
return Number(dayMatch[1]);
|
|
29
|
+
return 7;
|
|
30
|
+
}
|
|
31
|
+
function addDays(isoStart, dayOffset) {
|
|
32
|
+
const date = new Date(isoStart);
|
|
33
|
+
date.setUTCDate(date.getUTCDate() + dayOffset);
|
|
34
|
+
return date.toISOString().slice(0, 10);
|
|
35
|
+
}
|
|
36
|
+
export async function getHydroReservoir(params) {
|
|
37
|
+
const eic = resolveZone(params.zone);
|
|
38
|
+
const endDate = params.end_date
|
|
39
|
+
? new Date(params.end_date + "T00:00:00Z")
|
|
40
|
+
: new Date();
|
|
41
|
+
const startDate = params.start_date
|
|
42
|
+
? new Date(params.start_date + "T00:00:00Z")
|
|
43
|
+
: new Date(endDate.getTime() - 28 * 24 * 60 * 60 * 1000);
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
const data = await queryEntsoe({
|
|
46
|
+
documentType: "A72",
|
|
47
|
+
processType: "A16",
|
|
48
|
+
in_Domain: eic,
|
|
49
|
+
periodStart: formatEntsoeDate(startDate),
|
|
50
|
+
periodEnd: formatEntsoeDate(endDate),
|
|
51
|
+
}, TTL.STORAGE);
|
|
52
|
+
const doc = data.GL_MarketDocument;
|
|
53
|
+
if (!doc)
|
|
54
|
+
throw new Error("No hydro reservoir data returned for this zone/date range.");
|
|
55
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
56
|
+
const reservoir = [];
|
|
57
|
+
for (const ts of timeSeries) {
|
|
58
|
+
const periods = ensureArray(ts.Period);
|
|
59
|
+
for (const period of periods) {
|
|
60
|
+
const start = period.timeInterval?.start ?? "";
|
|
61
|
+
const startIso = typeof start === "string" ? start : "";
|
|
62
|
+
const stepDays = parsePeriodStepDays(period.resolution);
|
|
63
|
+
const points = ensureArray(period.Point);
|
|
64
|
+
for (const point of points) {
|
|
65
|
+
const storedMwh = Number(point.quantity ?? 0);
|
|
66
|
+
const position = Math.max(1, Number(point.position ?? 1));
|
|
67
|
+
reservoir.push({
|
|
68
|
+
week_start: addDays(startIso, (position - 1) * stepDays),
|
|
69
|
+
stored_energy_mwh: storedMwh,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
reservoir.sort((a, b) => a.week_start.localeCompare(b.week_start));
|
|
75
|
+
const latestStored = reservoir.length > 0
|
|
76
|
+
? reservoir[reservoir.length - 1].stored_energy_mwh
|
|
77
|
+
: 0;
|
|
78
|
+
return {
|
|
79
|
+
zone: params.zone.toUpperCase(),
|
|
80
|
+
start_date: params.start_date ?? startDate.toISOString().slice(0, 10),
|
|
81
|
+
end_date: params.end_date ?? endDate.toISOString().slice(0, 10),
|
|
82
|
+
reservoir,
|
|
83
|
+
latest_stored_mwh: latestStored,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const imbalancePricesSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface ImbalancePricePoint {
|
|
7
|
+
period: number;
|
|
8
|
+
price_eur_mwh: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function getImbalancePrices(params: z.infer<typeof imbalancePricesSchema>): Promise<{
|
|
11
|
+
zone: string;
|
|
12
|
+
date: string;
|
|
13
|
+
currency: string;
|
|
14
|
+
prices: ImbalancePricePoint[];
|
|
15
|
+
stats: {
|
|
16
|
+
min: number;
|
|
17
|
+
max: number;
|
|
18
|
+
mean: 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 imbalancePricesSchema = z.object({
|
|
7
|
+
zone: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe(`Bidding 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 getImbalancePrices(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: "A86",
|
|
21
|
+
processType: "A16",
|
|
22
|
+
controlArea_Domain: eic,
|
|
23
|
+
periodStart,
|
|
24
|
+
periodEnd,
|
|
25
|
+
}, TTL.BALANCING);
|
|
26
|
+
const doc = data.Imbalance_MarketDocument ?? data.GL_MarketDocument ?? data.Publication_MarketDocument;
|
|
27
|
+
if (!doc)
|
|
28
|
+
throw new Error("No imbalance price data returned for this zone/date.");
|
|
29
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
30
|
+
const prices = [];
|
|
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 price = Number(point["imbalance_Price.amount"] ?? point["price.amount"] ?? point.quantity ?? 0);
|
|
38
|
+
prices.push({ period: position, price_eur_mwh: price });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
prices.sort((a, b) => a.period - b.period);
|
|
43
|
+
const values = prices.map((p) => p.price_eur_mwh);
|
|
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
|
+
return {
|
|
50
|
+
zone: params.zone.toUpperCase(),
|
|
51
|
+
date: params.date ?? new Date().toISOString().slice(0, 10),
|
|
52
|
+
currency: "EUR",
|
|
53
|
+
prices,
|
|
54
|
+
stats: { min, max, mean },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const intradayPricesSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface IntradayPricePoint {
|
|
7
|
+
hour: number;
|
|
8
|
+
price_eur_mwh: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function getIntradayPrices(params: z.infer<typeof intradayPricesSchema>): Promise<{
|
|
11
|
+
zone: string;
|
|
12
|
+
date: string;
|
|
13
|
+
currency: string;
|
|
14
|
+
prices: IntradayPricePoint[];
|
|
15
|
+
stats: {
|
|
16
|
+
min: number;
|
|
17
|
+
max: number;
|
|
18
|
+
mean: number;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryEntsoe, dayRange } 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 intradayPricesSchema = z.object({
|
|
7
|
+
zone: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe(`Bidding 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 getIntradayPrices(params) {
|
|
16
|
+
const eic = resolvePriceZone(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: "A67",
|
|
21
|
+
processType: "A01",
|
|
22
|
+
in_Domain: eic,
|
|
23
|
+
out_Domain: eic,
|
|
24
|
+
periodStart,
|
|
25
|
+
periodEnd,
|
|
26
|
+
}, TTL.INTRADAY);
|
|
27
|
+
const doc = data.Publication_MarketDocument;
|
|
28
|
+
if (!doc)
|
|
29
|
+
throw new Error("No intraday price data returned for this zone/date.");
|
|
30
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
31
|
+
const prices = [];
|
|
32
|
+
for (const ts of timeSeries) {
|
|
33
|
+
const periods = ensureArray(ts.Period);
|
|
34
|
+
for (const period of periods) {
|
|
35
|
+
const points = ensureArray(period.Point);
|
|
36
|
+
for (const point of points) {
|
|
37
|
+
const position = Number(point.position);
|
|
38
|
+
const price = Number(point["price.amount"]);
|
|
39
|
+
prices.push({ hour: position - 1, price_eur_mwh: price });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
prices.sort((a, b) => a.hour - b.hour);
|
|
44
|
+
const values = prices.map((p) => p.price_eur_mwh);
|
|
45
|
+
const min = values.length > 0 ? Math.min(...values) : 0;
|
|
46
|
+
const max = values.length > 0 ? Math.max(...values) : 0;
|
|
47
|
+
const mean = values.length > 0
|
|
48
|
+
? Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 100) / 100
|
|
49
|
+
: 0;
|
|
50
|
+
return {
|
|
51
|
+
zone: params.zone.toUpperCase(),
|
|
52
|
+
date: params.date ?? new Date().toISOString().slice(0, 10),
|
|
53
|
+
currency: "EUR",
|
|
54
|
+
prices,
|
|
55
|
+
stats: { min, max, mean },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const intradaySpreadSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface SpreadPoint {
|
|
7
|
+
hour: number;
|
|
8
|
+
day_ahead: number;
|
|
9
|
+
intraday: number;
|
|
10
|
+
spread: number;
|
|
11
|
+
}
|
|
12
|
+
type SpreadSignal = "intraday_premium" | "intraday_discount" | "neutral";
|
|
13
|
+
export declare function getIntradayDaSpread(params: z.infer<typeof intradaySpreadSchema>): Promise<{
|
|
14
|
+
zone: string;
|
|
15
|
+
date: string;
|
|
16
|
+
spreads: SpreadPoint[];
|
|
17
|
+
stats: {
|
|
18
|
+
mean_spread: number;
|
|
19
|
+
max_spread: number;
|
|
20
|
+
min_spread: number;
|
|
21
|
+
};
|
|
22
|
+
signal: SpreadSignal;
|
|
23
|
+
}>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getDayAheadPrices, pricesSchema } from "./prices.js";
|
|
3
|
+
import { getIntradayPrices } from "./intraday-prices.js";
|
|
4
|
+
import { AVAILABLE_ZONES } from "../lib/zone-codes.js";
|
|
5
|
+
export const intradaySpreadSchema = z.object({
|
|
6
|
+
zone: z
|
|
7
|
+
.string()
|
|
8
|
+
.describe(`Bidding zone code. Examples: DE, FR, GB. Available: ${AVAILABLE_ZONES}`),
|
|
9
|
+
date: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
13
|
+
});
|
|
14
|
+
export async function getIntradayDaSpread(params) {
|
|
15
|
+
const date = params.date ?? new Date().toISOString().slice(0, 10);
|
|
16
|
+
const [daResult, idResult] = await Promise.all([
|
|
17
|
+
getDayAheadPrices(pricesSchema.parse({ zone: params.zone, start_date: date })),
|
|
18
|
+
getIntradayPrices({ zone: params.zone, date }),
|
|
19
|
+
]);
|
|
20
|
+
// Build lookup maps keyed by hour
|
|
21
|
+
const daByHour = new Map(daResult.prices.map((p) => [p.hour, p.price_eur_mwh]));
|
|
22
|
+
const idByHour = new Map(idResult.prices.map((p) => [p.hour, p.price_eur_mwh]));
|
|
23
|
+
// Compute spreads for matching hours
|
|
24
|
+
const spreads = [];
|
|
25
|
+
for (const [hour, idPrice] of idByHour) {
|
|
26
|
+
const daPrice = daByHour.get(hour);
|
|
27
|
+
if (daPrice == null)
|
|
28
|
+
continue;
|
|
29
|
+
spreads.push({
|
|
30
|
+
hour,
|
|
31
|
+
day_ahead: daPrice,
|
|
32
|
+
intraday: idPrice,
|
|
33
|
+
spread: Math.round((idPrice - daPrice) * 100) / 100,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
spreads.sort((a, b) => a.hour - b.hour);
|
|
37
|
+
const spreadValues = spreads.map((s) => s.spread);
|
|
38
|
+
const meanSpread = spreadValues.length > 0
|
|
39
|
+
? Math.round((spreadValues.reduce((s, v) => s + v, 0) / spreadValues.length) * 100) / 100
|
|
40
|
+
: 0;
|
|
41
|
+
const maxSpread = spreadValues.length > 0 ? Math.max(...spreadValues) : 0;
|
|
42
|
+
const minSpread = spreadValues.length > 0 ? Math.min(...spreadValues) : 0;
|
|
43
|
+
let signal = "neutral";
|
|
44
|
+
if (meanSpread > 5)
|
|
45
|
+
signal = "intraday_premium";
|
|
46
|
+
else if (meanSpread < -5)
|
|
47
|
+
signal = "intraday_discount";
|
|
48
|
+
return {
|
|
49
|
+
zone: params.zone.toUpperCase(),
|
|
50
|
+
date,
|
|
51
|
+
spreads,
|
|
52
|
+
stats: { mean_spread: meanSpread, max_spread: maxSpread, min_spread: minSpread },
|
|
53
|
+
signal,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ConstraintFeature } from "../lib/natural-england.js";
|
|
3
|
+
import { type GisSourceMetadata } from "../lib/gis-sources.js";
|
|
4
|
+
export declare const landConstraintsSchema: z.ZodObject<{
|
|
5
|
+
lat: z.ZodNumber;
|
|
6
|
+
lon: z.ZodNumber;
|
|
7
|
+
radius_km: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
country: z.ZodString;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
interface LandConstraintsSummary {
|
|
11
|
+
has_hard_constraint: boolean;
|
|
12
|
+
constraint_count: number;
|
|
13
|
+
}
|
|
14
|
+
interface LandConstraintsResult {
|
|
15
|
+
lat: number;
|
|
16
|
+
lon: number;
|
|
17
|
+
radius_km: number;
|
|
18
|
+
country: string;
|
|
19
|
+
constraints: ConstraintFeature[];
|
|
20
|
+
summary: LandConstraintsSummary;
|
|
21
|
+
source_metadata: GisSourceMetadata;
|
|
22
|
+
warnings?: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function getLandConstraints(params: z.infer<typeof landConstraintsSchema>): Promise<LandConstraintsResult>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
import { GB_PROTECTED_AREA_LAYERS, queryLayer, } from "../lib/natural-england.js";
|
|
4
|
+
import { queryNatura2000Layer } from "../lib/eea-natura2000.js";
|
|
5
|
+
import { GIS_SOURCES } from "../lib/gis-sources.js";
|
|
6
|
+
const cache = new TtlCache();
|
|
7
|
+
export const landConstraintsSchema = z.object({
|
|
8
|
+
lat: z.number().describe("Latitude (-90 to 90). WGS84."),
|
|
9
|
+
lon: z.number().describe("Longitude (-180 to 180). WGS84."),
|
|
10
|
+
radius_km: z
|
|
11
|
+
.number()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Search radius in km (default 2, max 10)."),
|
|
14
|
+
country: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('ISO 3166-1 alpha-2 country code. Supports "GB" plus EU member states in this version.'),
|
|
17
|
+
});
|
|
18
|
+
/** Designation types that represent hard planning exclusions for PV/BESS siting. */
|
|
19
|
+
const HARD_CONSTRAINT_TYPES = new Set([
|
|
20
|
+
"sssi",
|
|
21
|
+
"sac",
|
|
22
|
+
"spa",
|
|
23
|
+
"ramsar",
|
|
24
|
+
"national_park",
|
|
25
|
+
"natura2000",
|
|
26
|
+
"natura2000_birds",
|
|
27
|
+
"natura2000_habitats",
|
|
28
|
+
]);
|
|
29
|
+
const EU_COUNTRY_CODES = new Set([
|
|
30
|
+
"AT",
|
|
31
|
+
"BE",
|
|
32
|
+
"BG",
|
|
33
|
+
"CY",
|
|
34
|
+
"CZ",
|
|
35
|
+
"DE",
|
|
36
|
+
"DK",
|
|
37
|
+
"EE",
|
|
38
|
+
"ES",
|
|
39
|
+
"FI",
|
|
40
|
+
"FR",
|
|
41
|
+
"GR",
|
|
42
|
+
"HR",
|
|
43
|
+
"HU",
|
|
44
|
+
"IE",
|
|
45
|
+
"IT",
|
|
46
|
+
"LT",
|
|
47
|
+
"LU",
|
|
48
|
+
"LV",
|
|
49
|
+
"MT",
|
|
50
|
+
"NL",
|
|
51
|
+
"PL",
|
|
52
|
+
"PT",
|
|
53
|
+
"RO",
|
|
54
|
+
"SE",
|
|
55
|
+
"SI",
|
|
56
|
+
"SK",
|
|
57
|
+
]);
|
|
58
|
+
function dedupeConstraints(constraints) {
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
return constraints.filter((constraint) => {
|
|
61
|
+
const key = `${constraint.type}:${constraint.name}`;
|
|
62
|
+
if (seen.has(key))
|
|
63
|
+
return false;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function summariseConstraints(constraints) {
|
|
69
|
+
return {
|
|
70
|
+
has_hard_constraint: constraints.some((constraint) => HARD_CONSTRAINT_TYPES.has(constraint.type)),
|
|
71
|
+
constraint_count: constraints.length,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function getLandConstraints(params) {
|
|
75
|
+
const { lat, lon } = params;
|
|
76
|
+
const country = params.country.toUpperCase();
|
|
77
|
+
const radiusKm = params.radius_km ?? 2;
|
|
78
|
+
if (lat < -90 || lat > 90) {
|
|
79
|
+
throw new Error("Latitude must be between -90 and 90.");
|
|
80
|
+
}
|
|
81
|
+
if (lon < -180 || lon > 180) {
|
|
82
|
+
throw new Error("Longitude must be between -180 and 180.");
|
|
83
|
+
}
|
|
84
|
+
if (radiusKm <= 0 || radiusKm > 10) {
|
|
85
|
+
throw new Error("radius_km must be between 0 and 10.");
|
|
86
|
+
}
|
|
87
|
+
if (country !== "GB" && !EU_COUNTRY_CODES.has(country)) {
|
|
88
|
+
throw new Error(`Country "${params.country}" is not supported. Use "GB" for Great Britain or an EU member-state country code in this version.`);
|
|
89
|
+
}
|
|
90
|
+
const cacheKey = `land-constraints:${lat}:${lon}:${radiusKm}:${country}`;
|
|
91
|
+
const cached = cache.get(cacheKey);
|
|
92
|
+
if (cached)
|
|
93
|
+
return cached;
|
|
94
|
+
if (country === "GB") {
|
|
95
|
+
const layerResults = await Promise.allSettled(GB_PROTECTED_AREA_LAYERS.map((layer) => queryLayer(layer, lat, lon, radiusKm)));
|
|
96
|
+
const constraints = [];
|
|
97
|
+
const warnings = [];
|
|
98
|
+
for (let i = 0; i < layerResults.length; i++) {
|
|
99
|
+
const result = layerResults[i];
|
|
100
|
+
if (result.status === "fulfilled") {
|
|
101
|
+
constraints.push(...result.value);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const layerType = GB_PROTECTED_AREA_LAYERS[i].constraintType;
|
|
105
|
+
const msg = result.reason instanceof Error
|
|
106
|
+
? result.reason.message
|
|
107
|
+
: String(result.reason);
|
|
108
|
+
warnings.push(`${layerType}: ${msg}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (warnings.length === GB_PROTECTED_AREA_LAYERS.length) {
|
|
112
|
+
throw new Error(`All Natural England API queries failed: ${warnings.join("; ")}`);
|
|
113
|
+
}
|
|
114
|
+
const deduped = dedupeConstraints(constraints);
|
|
115
|
+
const result = {
|
|
116
|
+
lat,
|
|
117
|
+
lon,
|
|
118
|
+
radius_km: radiusKm,
|
|
119
|
+
country: "GB",
|
|
120
|
+
constraints: deduped,
|
|
121
|
+
summary: summariseConstraints(deduped),
|
|
122
|
+
source_metadata: GIS_SOURCES["natural-england"],
|
|
123
|
+
};
|
|
124
|
+
if (warnings.length > 0) {
|
|
125
|
+
result.warnings = warnings;
|
|
126
|
+
}
|
|
127
|
+
cache.set(cacheKey, result, TTL.STATIC_DATA);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const constraints = dedupeConstraints(await queryNatura2000Layer(lat, lon, radiusKm));
|
|
132
|
+
const result = {
|
|
133
|
+
lat,
|
|
134
|
+
lon,
|
|
135
|
+
radius_km: radiusKm,
|
|
136
|
+
country,
|
|
137
|
+
constraints,
|
|
138
|
+
summary: summariseConstraints(constraints),
|
|
139
|
+
source_metadata: GIS_SOURCES["eea-natura2000"],
|
|
140
|
+
};
|
|
141
|
+
cache.set(cacheKey, result, TTL.STATIC_DATA);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
+
throw new Error(`EEA Natura 2000 query failed: ${message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CorineResult } from "../lib/corine.js";
|
|
3
|
+
import { type GisSourceMetadata } from "../lib/gis-sources.js";
|
|
4
|
+
export declare const landCoverSchema: z.ZodObject<{
|
|
5
|
+
lat: z.ZodNumber;
|
|
6
|
+
lon: z.ZodNumber;
|
|
7
|
+
country: z.ZodString;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
interface LandCoverResult {
|
|
10
|
+
lat: number;
|
|
11
|
+
lon: number;
|
|
12
|
+
country: string;
|
|
13
|
+
land_cover: CorineResult | null;
|
|
14
|
+
coverage_note: string | null;
|
|
15
|
+
source_metadata: GisSourceMetadata;
|
|
16
|
+
}
|
|
17
|
+
export declare function getLandCover(params: z.infer<typeof landCoverSchema>): Promise<LandCoverResult>;
|
|
18
|
+
export {};
|