@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,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
import { queryCorineAtPoint, CORINE_COVERED_COUNTRIES } from "../lib/corine.js";
|
|
4
|
+
import { GIS_SOURCES } from "../lib/gis-sources.js";
|
|
5
|
+
const cache = new TtlCache();
|
|
6
|
+
export const landCoverSchema = z.object({
|
|
7
|
+
lat: z.number().describe("Latitude (-90 to 90). WGS84."),
|
|
8
|
+
lon: z.number().describe("Longitude (-180 to 180). WGS84."),
|
|
9
|
+
country: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("ISO 3166-1 alpha-2 country code. Required to select the correct data source. " +
|
|
12
|
+
"CORINE 2018 covers EU27 + EEA/EFTA countries. Great Britain (GB) is not covered " +
|
|
13
|
+
"(UK withdrew from CORINE after 2012); use get_agricultural_land for GB land-use context instead."),
|
|
14
|
+
});
|
|
15
|
+
export async function getLandCover(params) {
|
|
16
|
+
const { lat, lon } = params;
|
|
17
|
+
const country = params.country.toUpperCase();
|
|
18
|
+
if (lat < -90 || lat > 90) {
|
|
19
|
+
throw new Error("Latitude must be between -90 and 90.");
|
|
20
|
+
}
|
|
21
|
+
if (lon < -180 || lon > 180) {
|
|
22
|
+
throw new Error("Longitude must be between -180 and 180.");
|
|
23
|
+
}
|
|
24
|
+
if (country === "GB") {
|
|
25
|
+
return {
|
|
26
|
+
lat,
|
|
27
|
+
lon,
|
|
28
|
+
country: "GB",
|
|
29
|
+
land_cover: null,
|
|
30
|
+
coverage_note: "Great Britain is not covered by CORINE Land Cover 2018. The UK withdrew from the " +
|
|
31
|
+
"programme after CLC 2012. Use get_agricultural_land for agricultural land classification " +
|
|
32
|
+
"in England, or get_land_constraints for protected-area context.",
|
|
33
|
+
source_metadata: GIS_SOURCES["corine-land-cover"],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!CORINE_COVERED_COUNTRIES.has(country)) {
|
|
37
|
+
throw new Error(`Country "${params.country}" is not supported by CORINE Land Cover 2018. ` +
|
|
38
|
+
"Supported: EU27 member states plus EEA/EFTA countries (IS, LI, NO, TR, AL, BA, ME, MK, RS, XK).");
|
|
39
|
+
}
|
|
40
|
+
const cacheKey = `land-cover:${lat}:${lon}:${country}`;
|
|
41
|
+
const cached = cache.get(cacheKey);
|
|
42
|
+
if (cached)
|
|
43
|
+
return cached;
|
|
44
|
+
try {
|
|
45
|
+
const landCover = await queryCorineAtPoint(lat, lon);
|
|
46
|
+
const result = {
|
|
47
|
+
lat,
|
|
48
|
+
lon,
|
|
49
|
+
country,
|
|
50
|
+
land_cover: landCover,
|
|
51
|
+
coverage_note: landCover === null
|
|
52
|
+
? "No CORINE polygon found at this point. The location may be offshore, at a coverage boundary, " +
|
|
53
|
+
"or below the 25 ha minimum mapping unit."
|
|
54
|
+
: null,
|
|
55
|
+
source_metadata: GIS_SOURCES["corine-land-cover"],
|
|
56
|
+
};
|
|
57
|
+
cache.set(cacheKey, result, TTL.STATIC_DATA);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
throw new Error(`CORINE Land Cover query failed: ${message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const lngTerminalsSchema: z.ZodObject<{
|
|
3
|
+
country: z.ZodOptional<z.ZodString>;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface TerminalEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
lng_inventory_mcm: number;
|
|
9
|
+
send_out_gwh: number;
|
|
10
|
+
capacity_mcm: number;
|
|
11
|
+
dtrs: number | null;
|
|
12
|
+
status: string;
|
|
13
|
+
}
|
|
14
|
+
interface LngData {
|
|
15
|
+
country: string;
|
|
16
|
+
date: string;
|
|
17
|
+
terminals: TerminalEntry[];
|
|
18
|
+
total_inventory_mcm: number;
|
|
19
|
+
total_send_out_gwh: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function getLngTerminals(params: z.infer<typeof lngTerminalsSchema>): Promise<LngData>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
import { resolveApiKey } from "../lib/auth.js";
|
|
4
|
+
const BASE_URL = "https://alsi.gie.eu/api";
|
|
5
|
+
const cache = new TtlCache();
|
|
6
|
+
const ALSI_COUNTRIES = [
|
|
7
|
+
"EU", "BE", "ES", "FR", "GB", "GR", "HR", "IT", "LT", "MT", "NL", "PL", "PT",
|
|
8
|
+
];
|
|
9
|
+
export const lngTerminalsSchema = z.object({
|
|
10
|
+
country: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(`Country code for LNG terminal data. Available: ${ALSI_COUNTRIES.join(", ")}. Defaults to EU aggregate.`),
|
|
14
|
+
date: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
18
|
+
});
|
|
19
|
+
async function getApiKey() {
|
|
20
|
+
try {
|
|
21
|
+
return await resolveApiKey("GIE_API_KEY");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("GIE_API_KEY is required. Set it as an environment variable or in ~/.luminus/keys.json. " +
|
|
25
|
+
"Get one at https://alsi.gie.eu/ (same key as AGSI+).");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function validateCountry(code) {
|
|
29
|
+
const upper = code.toUpperCase();
|
|
30
|
+
if (!ALSI_COUNTRIES.includes(upper)) {
|
|
31
|
+
throw new Error(`Unknown country "${code}". Available: ${ALSI_COUNTRIES.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
return upper;
|
|
34
|
+
}
|
|
35
|
+
export async function getLngTerminals(params) {
|
|
36
|
+
const country = validateCountry(params.country ?? "EU");
|
|
37
|
+
const date = params.date ?? new Date().toISOString().slice(0, 10);
|
|
38
|
+
const cacheKey = `alsi:${country}:${date}`;
|
|
39
|
+
const cached = cache.get(cacheKey);
|
|
40
|
+
if (cached)
|
|
41
|
+
return cached;
|
|
42
|
+
const url = `${BASE_URL}/data/${country}?date=${date}`;
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
headers: { "x-key": await getApiKey() },
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const body = await response.text();
|
|
48
|
+
throw new Error(`ALSI API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
49
|
+
}
|
|
50
|
+
const json = await response.json();
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
const entries = Array.isArray(json.data) ? json.data : [];
|
|
53
|
+
if (entries.length === 0) {
|
|
54
|
+
throw new Error(`No LNG terminal data for ${country} on ${date}.`);
|
|
55
|
+
}
|
|
56
|
+
const terminals = entries.map((e) => ({
|
|
57
|
+
name: e.name ?? e.facility ?? "Unknown",
|
|
58
|
+
lng_inventory_mcm: Number(e.lngInventory ?? 0),
|
|
59
|
+
send_out_gwh: Number(e.sendOut ?? 0),
|
|
60
|
+
capacity_mcm: Number(e.dtsp ?? 0),
|
|
61
|
+
dtrs: e.dtrs != null ? Number(e.dtrs) : null,
|
|
62
|
+
status: e.status ?? "unknown",
|
|
63
|
+
}));
|
|
64
|
+
const totalInventory = terminals.reduce((s, t) => s + t.lng_inventory_mcm, 0);
|
|
65
|
+
const totalSendOut = terminals.reduce((s, t) => s + t.send_out_gwh, 0);
|
|
66
|
+
const result = {
|
|
67
|
+
country,
|
|
68
|
+
date: entries[0]?.gasDayStart ?? date,
|
|
69
|
+
terminals,
|
|
70
|
+
total_inventory_mcm: totalInventory,
|
|
71
|
+
total_send_out_gwh: totalSendOut,
|
|
72
|
+
};
|
|
73
|
+
cache.set(cacheKey, result, TTL.STORAGE);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const netPositionsSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface BorderFlow {
|
|
7
|
+
neighbour: string;
|
|
8
|
+
import_mw: number;
|
|
9
|
+
export_mw: number;
|
|
10
|
+
net_mw: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function getNetPositions(params: z.infer<typeof netPositionsSchema>): Promise<{
|
|
13
|
+
zone: string;
|
|
14
|
+
date: string;
|
|
15
|
+
net_position_mw: number;
|
|
16
|
+
status: string;
|
|
17
|
+
borders: BorderFlow[];
|
|
18
|
+
}>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryEntsoe, dayRange } from "../lib/entsoe-client.js";
|
|
3
|
+
import { resolveZone, AVAILABLE_ZONES, ZONE_NEIGHBOURS } from "../lib/zone-codes.js";
|
|
4
|
+
import { ensureArray } from "../lib/xml-parser.js";
|
|
5
|
+
import { TTL } from "../lib/cache.js";
|
|
6
|
+
export const netPositionsSchema = z.object({
|
|
7
|
+
zone: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe(`Country/zone code. Available: ${AVAILABLE_ZONES}. ` +
|
|
10
|
+
"Net position = total imports minus total exports across all borders."),
|
|
11
|
+
date: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
15
|
+
});
|
|
16
|
+
async function queryFlow(fromEic, toEic, periodStart, periodEnd) {
|
|
17
|
+
try {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
const data = await queryEntsoe({ documentType: "A11", in_Domain: toEic, out_Domain: fromEic, periodStart, periodEnd }, TTL.OUTAGES);
|
|
20
|
+
const doc = data.GL_MarketDocument;
|
|
21
|
+
if (!doc)
|
|
22
|
+
return 0;
|
|
23
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
24
|
+
let total = 0;
|
|
25
|
+
let count = 0;
|
|
26
|
+
for (const ts of timeSeries) {
|
|
27
|
+
const periods = ensureArray(ts.Period);
|
|
28
|
+
for (const period of periods) {
|
|
29
|
+
const points = ensureArray(period.Point);
|
|
30
|
+
for (const point of points) {
|
|
31
|
+
total += Number(point.quantity ?? 0);
|
|
32
|
+
count++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return count > 0 ? Math.round(total / count) : 0;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function getNetPositions(params) {
|
|
43
|
+
const zoneUpper = params.zone.toUpperCase();
|
|
44
|
+
const neighbours = ZONE_NEIGHBOURS[zoneUpper];
|
|
45
|
+
if (!neighbours) {
|
|
46
|
+
throw new Error(`No neighbour mapping for "${zoneUpper}". Available: ${Object.keys(ZONE_NEIGHBOURS).join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
const zoneEic = resolveZone(zoneUpper);
|
|
49
|
+
const { periodStart, periodEnd } = dayRange(params.date);
|
|
50
|
+
// Query import and export flows for each neighbour in parallel
|
|
51
|
+
const borderPromises = neighbours.map(async (neighbour) => {
|
|
52
|
+
const neighbourEic = resolveZone(neighbour);
|
|
53
|
+
const [importMw, exportMw] = await Promise.all([
|
|
54
|
+
queryFlow(neighbourEic, zoneEic, periodStart, periodEnd),
|
|
55
|
+
queryFlow(zoneEic, neighbourEic, periodStart, periodEnd),
|
|
56
|
+
]);
|
|
57
|
+
return {
|
|
58
|
+
neighbour,
|
|
59
|
+
import_mw: importMw,
|
|
60
|
+
export_mw: exportMw,
|
|
61
|
+
net_mw: importMw - exportMw,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
const borders = await Promise.all(borderPromises);
|
|
65
|
+
const netPosition = borders.reduce((sum, b) => sum + b.net_mw, 0);
|
|
66
|
+
const status = netPosition > 0 ? "net importer" : netPosition < 0 ? "net exporter" : "balanced";
|
|
67
|
+
return {
|
|
68
|
+
zone: zoneUpper,
|
|
69
|
+
date: params.date ?? new Date().toISOString().slice(0, 10),
|
|
70
|
+
net_position_mw: netPosition,
|
|
71
|
+
status,
|
|
72
|
+
borders: borders.filter((b) => b.import_mw !== 0 || b.export_mw !== 0),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const nordpoolSchema: z.ZodObject<{
|
|
3
|
+
areas: z.ZodString;
|
|
4
|
+
date: z.ZodOptional<z.ZodString>;
|
|
5
|
+
currency: z.ZodOptional<z.ZodEnum<{
|
|
6
|
+
EUR: "EUR";
|
|
7
|
+
NOK: "NOK";
|
|
8
|
+
SEK: "SEK";
|
|
9
|
+
DKK: "DKK";
|
|
10
|
+
}>>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
interface AreaPriceData {
|
|
13
|
+
prices: {
|
|
14
|
+
timestamp: string;
|
|
15
|
+
price: number;
|
|
16
|
+
}[];
|
|
17
|
+
stats: {
|
|
18
|
+
min: number;
|
|
19
|
+
max: number;
|
|
20
|
+
mean: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface NordpoolResult {
|
|
24
|
+
date: string;
|
|
25
|
+
currency: string;
|
|
26
|
+
areas: Record<string, AreaPriceData>;
|
|
27
|
+
}
|
|
28
|
+
export declare function getNordpoolPrices(params: z.infer<typeof nordpoolSchema>): Promise<NordpoolResult>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const cache = new TtlCache();
|
|
4
|
+
const VALID_AREAS = new Set([
|
|
5
|
+
"SE1", "SE2", "SE3", "SE4",
|
|
6
|
+
"NO1", "NO2", "NO3", "NO4", "NO5",
|
|
7
|
+
"DK1", "DK2",
|
|
8
|
+
"FI",
|
|
9
|
+
]);
|
|
10
|
+
export const nordpoolSchema = z.object({
|
|
11
|
+
areas: z.string().describe('Comma-separated delivery areas. Available: SE1,SE2,SE3,SE4,NO1,NO2,NO3,NO4,NO5,DK1,DK2,FI. Example: "SE3,NO1,DK1"'),
|
|
12
|
+
date: z.string().optional().describe("Date YYYY-MM-DD. Defaults to today."),
|
|
13
|
+
currency: z
|
|
14
|
+
.enum(["EUR", "NOK", "SEK", "DKK"])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Price currency. Defaults to EUR."),
|
|
17
|
+
});
|
|
18
|
+
function todayDate() {
|
|
19
|
+
return new Date().toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
export async function getNordpoolPrices(params) {
|
|
22
|
+
const date = params.date ?? todayDate();
|
|
23
|
+
const currency = params.currency ?? "EUR";
|
|
24
|
+
const requestedAreas = params.areas
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((a) => a.trim().toUpperCase())
|
|
27
|
+
.filter((a) => a.length > 0);
|
|
28
|
+
const invalidAreas = requestedAreas.filter((a) => !VALID_AREAS.has(a));
|
|
29
|
+
if (invalidAreas.length > 0) {
|
|
30
|
+
throw new Error(`Invalid delivery areas: ${invalidAreas.join(", ")}. Available: ${[...VALID_AREAS].join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
if (requestedAreas.length === 0) {
|
|
33
|
+
throw new Error("No delivery areas specified.");
|
|
34
|
+
}
|
|
35
|
+
const areasParam = requestedAreas.join(",");
|
|
36
|
+
const cacheKey = `nordpool:${date}:${areasParam}:${currency}`;
|
|
37
|
+
const cached = cache.get(cacheKey);
|
|
38
|
+
if (cached)
|
|
39
|
+
return cached;
|
|
40
|
+
const url = `https://dataportal-api.nordpoolgroup.com/api/DayAheadPrices` +
|
|
41
|
+
`?date=${date}&market=DayAhead&deliveryArea=${areasParam}¤cy=${currency}`;
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
headers: { "User-Agent": "luminus-mcp/0.2" },
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const body = await response.text();
|
|
47
|
+
throw new Error(`Nordpool API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
48
|
+
}
|
|
49
|
+
const data = (await response.json());
|
|
50
|
+
if (!data.multiAreaEntries || data.multiAreaEntries.length === 0) {
|
|
51
|
+
throw new Error(`No day-ahead prices returned for areas ${areasParam} on ${date}. Data may not be available yet.`);
|
|
52
|
+
}
|
|
53
|
+
const areas = {};
|
|
54
|
+
for (const area of requestedAreas) {
|
|
55
|
+
const prices = [];
|
|
56
|
+
for (const entry of data.multiAreaEntries) {
|
|
57
|
+
const value = entry.entryPerArea[area];
|
|
58
|
+
if (value == null || !Number.isFinite(value))
|
|
59
|
+
continue;
|
|
60
|
+
prices.push({
|
|
61
|
+
timestamp: entry.deliveryStart,
|
|
62
|
+
price: Math.round(value * 100) / 100,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (prices.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
const values = prices.map((p) => p.price);
|
|
68
|
+
const min = Math.round(Math.min(...values) * 100) / 100;
|
|
69
|
+
const max = Math.round(Math.max(...values) * 100) / 100;
|
|
70
|
+
const mean = Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 100) /
|
|
71
|
+
100;
|
|
72
|
+
areas[area] = { prices, stats: { min, max, mean } };
|
|
73
|
+
}
|
|
74
|
+
if (Object.keys(areas).length === 0) {
|
|
75
|
+
throw new Error(`No price data found for the requested areas on ${date}.`);
|
|
76
|
+
}
|
|
77
|
+
const result = { date, currency, areas };
|
|
78
|
+
cache.set(cacheKey, result, TTL.PRICES);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const outagesSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
type: z.ZodEnum<{
|
|
5
|
+
generation: "generation";
|
|
6
|
+
transmission: "transmission";
|
|
7
|
+
}>;
|
|
8
|
+
date: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
interface OutageEntry {
|
|
11
|
+
unit_name: string;
|
|
12
|
+
fuel_type: string | null;
|
|
13
|
+
available_mw: number | null;
|
|
14
|
+
unavailable_mw: number | null;
|
|
15
|
+
nominal_mw: number | null;
|
|
16
|
+
start_date: string;
|
|
17
|
+
end_date: string;
|
|
18
|
+
reason: string | null;
|
|
19
|
+
outage_type: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function getOutages(params: z.infer<typeof outagesSchema>): Promise<{
|
|
22
|
+
zone: string;
|
|
23
|
+
type: string;
|
|
24
|
+
outages: OutageEntry[];
|
|
25
|
+
total_unavailable_mw: number;
|
|
26
|
+
count: number;
|
|
27
|
+
}>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryEntsoe, formatEntsoeDate } from "../lib/entsoe-client.js";
|
|
3
|
+
import { resolveZone, AVAILABLE_ZONES } from "../lib/zone-codes.js";
|
|
4
|
+
import { ensureArray } from "../lib/xml-parser.js";
|
|
5
|
+
import { TTL } from "../lib/cache.js";
|
|
6
|
+
/** PSR type codes to human-readable fuel names */
|
|
7
|
+
const PSR_TYPES = {
|
|
8
|
+
B01: "Biomass",
|
|
9
|
+
B02: "Lignite",
|
|
10
|
+
B03: "Coal-derived gas",
|
|
11
|
+
B04: "Gas",
|
|
12
|
+
B05: "Hard coal",
|
|
13
|
+
B06: "Oil",
|
|
14
|
+
B07: "Oil shale",
|
|
15
|
+
B08: "Peat",
|
|
16
|
+
B09: "Geothermal",
|
|
17
|
+
B10: "Hydro Pumped Storage",
|
|
18
|
+
B11: "Hydro Run-of-river",
|
|
19
|
+
B12: "Hydro Reservoir",
|
|
20
|
+
B14: "Nuclear",
|
|
21
|
+
B16: "Solar",
|
|
22
|
+
B18: "Wind Offshore",
|
|
23
|
+
B19: "Wind Onshore",
|
|
24
|
+
};
|
|
25
|
+
export const outagesSchema = z.object({
|
|
26
|
+
zone: z
|
|
27
|
+
.string()
|
|
28
|
+
.describe(`Country/zone code. Available: ${AVAILABLE_ZONES}`),
|
|
29
|
+
type: z
|
|
30
|
+
.enum(["generation", "transmission"])
|
|
31
|
+
.describe("Type of outage: 'generation' for power plant outages, 'transmission' for grid outages."),
|
|
32
|
+
date: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Start date in YYYY-MM-DD format. Returns outages active within 7 days from this date. Defaults to today."),
|
|
36
|
+
});
|
|
37
|
+
export async function getOutages(params) {
|
|
38
|
+
const eic = resolveZone(params.zone);
|
|
39
|
+
const documentType = params.type === "generation" ? "A80" : "A78";
|
|
40
|
+
const base = params.date ? new Date(params.date + "T00:00:00Z") : new Date();
|
|
41
|
+
const start = new Date(Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate()));
|
|
42
|
+
const end = new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
43
|
+
const queryParams = {
|
|
44
|
+
documentType,
|
|
45
|
+
periodStart: formatEntsoeDate(start),
|
|
46
|
+
periodEnd: formatEntsoeDate(end),
|
|
47
|
+
};
|
|
48
|
+
if (params.type === "generation") {
|
|
49
|
+
queryParams.biddingZone_Domain = eic;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
queryParams.in_Domain = eic;
|
|
53
|
+
queryParams.out_Domain = eic;
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
const data = await queryEntsoe(queryParams, TTL.OUTAGES);
|
|
57
|
+
const doc = data.Unavailability_MarketDocument;
|
|
58
|
+
if (!doc)
|
|
59
|
+
throw new Error("No outage data returned for this zone/date.");
|
|
60
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
61
|
+
const outages = [];
|
|
62
|
+
for (const ts of timeSeries) {
|
|
63
|
+
const resource = ts.production_RegisteredResource ?? ts.Asset_RegisteredResource ?? {};
|
|
64
|
+
const unitName = resource.name ?? resource.mRID ?? "Unknown";
|
|
65
|
+
const psrCode = resource.pSRType?.psrType ?? null;
|
|
66
|
+
const fuelType = psrCode ? (PSR_TYPES[psrCode] ?? psrCode) : null;
|
|
67
|
+
const nominalMw = resource.pSRType?.powerSystemResources?.nominalP
|
|
68
|
+
? Number(resource.pSRType.powerSystemResources.nominalP)
|
|
69
|
+
: null;
|
|
70
|
+
const businessType = ts.businessType ?? "";
|
|
71
|
+
const outageType = businessType === "A53" ? "planned" : businessType === "A54" ? "forced" : businessType;
|
|
72
|
+
const startDate = ts.start_DateAndOrTime?.date ?? ts.start_DateAndOrTime ?? "";
|
|
73
|
+
const endDate = ts.end_DateAndOrTime?.date ?? ts.end_DateAndOrTime ?? "";
|
|
74
|
+
const reason = ts.Reason?.text ?? null;
|
|
75
|
+
const availPeriods = ensureArray(ts.Available_Period);
|
|
76
|
+
let availableMw = null;
|
|
77
|
+
for (const period of availPeriods) {
|
|
78
|
+
const points = ensureArray(period.Point);
|
|
79
|
+
if (points.length > 0) {
|
|
80
|
+
availableMw = Number(points[0].quantity ?? 0);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const unavailableMw = nominalMw != null && availableMw != null
|
|
85
|
+
? nominalMw - availableMw
|
|
86
|
+
: null;
|
|
87
|
+
outages.push({
|
|
88
|
+
unit_name: unitName,
|
|
89
|
+
fuel_type: fuelType,
|
|
90
|
+
available_mw: availableMw,
|
|
91
|
+
unavailable_mw: unavailableMw,
|
|
92
|
+
nominal_mw: nominalMw,
|
|
93
|
+
start_date: startDate,
|
|
94
|
+
end_date: endDate,
|
|
95
|
+
reason,
|
|
96
|
+
outage_type: outageType,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const totalUnavailable = outages.reduce((sum, o) => sum + (o.unavailable_mw ?? 0), 0);
|
|
100
|
+
return {
|
|
101
|
+
zone: params.zone.toUpperCase(),
|
|
102
|
+
type: params.type,
|
|
103
|
+
outages,
|
|
104
|
+
total_unavailable_mw: totalUnavailable,
|
|
105
|
+
count: outages.length,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const powerPlantsSchema: z.ZodObject<{
|
|
3
|
+
country: z.ZodOptional<z.ZodString>;
|
|
4
|
+
fuel_type: z.ZodOptional<z.ZodString>;
|
|
5
|
+
min_capacity_mw: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
interface PowerPlant {
|
|
8
|
+
name: string;
|
|
9
|
+
country: string;
|
|
10
|
+
capacity_mw: number;
|
|
11
|
+
fuel: string;
|
|
12
|
+
lat: number | null;
|
|
13
|
+
lon: number | null;
|
|
14
|
+
commissioned_year: number | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function getPowerPlants(params: z.infer<typeof powerPlantsSchema>): Promise<{
|
|
17
|
+
filters: {
|
|
18
|
+
country?: string;
|
|
19
|
+
fuel_type?: string;
|
|
20
|
+
min_capacity_mw: number;
|
|
21
|
+
};
|
|
22
|
+
total_count: number;
|
|
23
|
+
plants: PowerPlant[];
|
|
24
|
+
total_capacity_mw: number;
|
|
25
|
+
}>;
|
|
26
|
+
export {};
|