@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,127 @@
|
|
|
1
|
+
/** ENTSO-E EIC area codes for European bidding zones */
|
|
2
|
+
export const ZONE_CODES = {
|
|
3
|
+
GB: "10YGB----------A",
|
|
4
|
+
DE: "10Y1001A1001A83F",
|
|
5
|
+
DE_LU: "10Y1001A1001A82H", // DE-LU combined bidding zone (used for prices)
|
|
6
|
+
FR: "10YFR-RTE------C",
|
|
7
|
+
NL: "10YNL----------L",
|
|
8
|
+
BE: "10YBE----------2",
|
|
9
|
+
ES: "10YES-REE------0",
|
|
10
|
+
IT: "10YIT-GRTN-----B",
|
|
11
|
+
PT: "10YPT-REN------W",
|
|
12
|
+
NO1: "10YNO-1--------2",
|
|
13
|
+
NO2: "10YNO-2--------T",
|
|
14
|
+
SE1: "10Y1001A1001A44P",
|
|
15
|
+
SE2: "10Y1001A1001A45N",
|
|
16
|
+
SE3: "10Y1001A1001A46L",
|
|
17
|
+
SE4: "10Y1001A1001A47J",
|
|
18
|
+
DK1: "10YDK-1--------W",
|
|
19
|
+
DK2: "10YDK-2--------M",
|
|
20
|
+
PL: "10YPL-AREA-----S",
|
|
21
|
+
AT: "10YAT-APG------L",
|
|
22
|
+
CH: "10YCH-SWISSGRIDZ",
|
|
23
|
+
IE: "10YIE-1001A00010",
|
|
24
|
+
CZ: "10YCZ-CEPS-----N",
|
|
25
|
+
FI: "10YFI-1--------U",
|
|
26
|
+
GR: "10YGR-HTSO-----Y",
|
|
27
|
+
HU: "10YHU-MAVIR----U",
|
|
28
|
+
RO: "10YRO-TEL------P",
|
|
29
|
+
BG: "10YCA-BULGARIA-R",
|
|
30
|
+
HR: "10YHR-HEP------M",
|
|
31
|
+
SK: "10YSK-SEPS-----K",
|
|
32
|
+
SI: "10YSI-ELES-----O",
|
|
33
|
+
LT: "10YLT-1001A0008Q",
|
|
34
|
+
LV: "10YLV-1001A00074",
|
|
35
|
+
EE: "10Y1001A1001A39I",
|
|
36
|
+
};
|
|
37
|
+
/** Resolve a zone string to an EIC code. Accepts both "DE" and raw EIC codes. */
|
|
38
|
+
export function resolveZone(zone) {
|
|
39
|
+
const upper = zone.toUpperCase();
|
|
40
|
+
if (ZONE_CODES[upper])
|
|
41
|
+
return ZONE_CODES[upper];
|
|
42
|
+
// If it already looks like an EIC code, pass through
|
|
43
|
+
if (upper.startsWith("10Y"))
|
|
44
|
+
return zone;
|
|
45
|
+
throw new Error(`Unknown zone "${zone}". Use ISO country code (DE, FR, GB...) or raw EIC code.`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Some zones use different EIC codes for price queries (bidding zones differ
|
|
49
|
+
* from control areas). E.g. DE uses the DE-LU combined bidding zone for prices.
|
|
50
|
+
*/
|
|
51
|
+
const PRICE_ZONE_OVERRIDES = {
|
|
52
|
+
DE: "10Y1001A1001A82H", // DE-LU combined bidding zone
|
|
53
|
+
};
|
|
54
|
+
/** Resolve zone for price queries (uses bidding zone overrides) */
|
|
55
|
+
export function resolvePriceZone(zone) {
|
|
56
|
+
const upper = zone.toUpperCase();
|
|
57
|
+
if (PRICE_ZONE_OVERRIDES[upper])
|
|
58
|
+
return PRICE_ZONE_OVERRIDES[upper];
|
|
59
|
+
return resolveZone(zone);
|
|
60
|
+
}
|
|
61
|
+
/** All available zone keys for tool descriptions */
|
|
62
|
+
export const AVAILABLE_ZONES = Object.keys(ZONE_CODES).join(", ");
|
|
63
|
+
/** Neighbouring zones for net position calculation (cross-border flow pairs) */
|
|
64
|
+
export const ZONE_NEIGHBOURS = {
|
|
65
|
+
DE: ["FR", "NL", "BE", "PL", "CZ", "AT", "CH", "DK1", "DK2", "SE4"],
|
|
66
|
+
FR: ["DE", "BE", "ES", "IT", "CH", "GB"],
|
|
67
|
+
NL: ["DE", "BE", "GB", "NO2"],
|
|
68
|
+
BE: ["FR", "NL", "DE", "GB"],
|
|
69
|
+
GB: ["FR", "NL", "BE", "IE", "NO2"],
|
|
70
|
+
ES: ["FR", "PT"],
|
|
71
|
+
PT: ["ES"],
|
|
72
|
+
IT: ["FR", "AT", "SI", "CH", "GR"],
|
|
73
|
+
AT: ["DE", "CZ", "HU", "SI", "IT", "CH"],
|
|
74
|
+
CH: ["DE", "FR", "IT", "AT"],
|
|
75
|
+
PL: ["DE", "CZ", "SK", "SE4", "LT"],
|
|
76
|
+
CZ: ["DE", "PL", "SK", "AT"],
|
|
77
|
+
DK1: ["DE", "NO2", "SE3", "DK2"],
|
|
78
|
+
DK2: ["DE", "SE4", "DK1"],
|
|
79
|
+
NO1: ["NO2", "SE3"],
|
|
80
|
+
NO2: ["NO1", "NL", "GB", "DK1"],
|
|
81
|
+
SE1: ["SE2", "FI"],
|
|
82
|
+
SE2: ["SE1", "SE3"],
|
|
83
|
+
SE3: ["SE2", "NO1", "DK1", "SE4", "FI"],
|
|
84
|
+
SE4: ["SE3", "DE", "PL", "DK2", "LT"],
|
|
85
|
+
FI: ["SE1", "SE3", "EE"],
|
|
86
|
+
EE: ["FI", "LV"],
|
|
87
|
+
LV: ["EE", "LT"],
|
|
88
|
+
LT: ["LV", "PL", "SE4"],
|
|
89
|
+
HU: ["AT", "SK", "RO", "HR", "SI"],
|
|
90
|
+
RO: ["HU", "BG"],
|
|
91
|
+
BG: ["RO", "GR"],
|
|
92
|
+
HR: ["HU", "SI"],
|
|
93
|
+
SK: ["CZ", "PL", "HU", "AT"],
|
|
94
|
+
SI: ["AT", "IT", "HU", "HR"],
|
|
95
|
+
GR: ["IT", "BG"],
|
|
96
|
+
IE: ["GB"],
|
|
97
|
+
};
|
|
98
|
+
/** Approximate bounding boxes [lat_min, lon_min, lat_max, lon_max] for Overpass API queries */
|
|
99
|
+
export const COUNTRY_BBOXES = {
|
|
100
|
+
DE: [47.27, 5.87, 55.06, 15.04],
|
|
101
|
+
FR: [41.36, -5.14, 51.09, 9.56],
|
|
102
|
+
GB: [49.96, -6.37, 58.64, 1.76],
|
|
103
|
+
NL: [50.75, 3.36, 53.47, 7.21],
|
|
104
|
+
BE: [49.50, 2.55, 51.50, 6.40],
|
|
105
|
+
ES: [36.00, -9.30, 43.79, 3.33],
|
|
106
|
+
PT: [36.96, -9.50, 42.15, -6.19],
|
|
107
|
+
IT: [36.65, 6.63, 47.09, 18.52],
|
|
108
|
+
AT: [46.37, 9.53, 49.02, 17.16],
|
|
109
|
+
CH: [45.82, 5.96, 47.81, 10.49],
|
|
110
|
+
PL: [49.00, 14.12, 54.84, 24.15],
|
|
111
|
+
CZ: [48.55, 12.09, 51.06, 18.86],
|
|
112
|
+
DK: [54.56, 8.07, 57.75, 15.20],
|
|
113
|
+
NO: [57.96, 4.64, 71.19, 31.07],
|
|
114
|
+
SE: [55.34, 11.11, 69.06, 24.17],
|
|
115
|
+
FI: [59.81, 20.65, 70.09, 31.59],
|
|
116
|
+
GR: [34.80, 19.37, 41.75, 29.65],
|
|
117
|
+
HU: [45.74, 16.11, 48.58, 22.90],
|
|
118
|
+
RO: [43.62, 20.26, 48.27, 29.69],
|
|
119
|
+
BG: [41.24, 22.36, 44.21, 28.61],
|
|
120
|
+
HR: [42.39, 13.49, 46.55, 19.43],
|
|
121
|
+
SK: [47.73, 16.83, 49.60, 22.57],
|
|
122
|
+
SI: [45.42, 13.38, 46.88, 16.60],
|
|
123
|
+
IE: [51.42, -10.48, 55.38, -6.00],
|
|
124
|
+
EE: [57.52, 21.77, 59.68, 28.21],
|
|
125
|
+
LV: [55.67, 20.97, 58.09, 28.24],
|
|
126
|
+
LT: [53.90, 20.94, 56.45, 26.84],
|
|
127
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const acerRemitSchema: z.ZodObject<{
|
|
3
|
+
message_type: z.ZodEnum<{
|
|
4
|
+
urgent_market_messages: "urgent_market_messages";
|
|
5
|
+
outage_events: "outage_events";
|
|
6
|
+
}>;
|
|
7
|
+
country: z.ZodOptional<z.ZodString>;
|
|
8
|
+
fuel_type: z.ZodOptional<z.ZodString>;
|
|
9
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
interface UmmRecord {
|
|
12
|
+
message_id: string;
|
|
13
|
+
event_type: string;
|
|
14
|
+
market_participant: string;
|
|
15
|
+
asset_name: string;
|
|
16
|
+
fuel_type: string;
|
|
17
|
+
country: string;
|
|
18
|
+
unavailable_mw: number;
|
|
19
|
+
available_mw: number;
|
|
20
|
+
installed_mw: number;
|
|
21
|
+
event_start: string;
|
|
22
|
+
event_end: string;
|
|
23
|
+
publication_date: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}
|
|
26
|
+
interface UmmResult {
|
|
27
|
+
message_type: "urgent_market_messages";
|
|
28
|
+
source: string;
|
|
29
|
+
description: string;
|
|
30
|
+
count: number;
|
|
31
|
+
records: UmmRecord[];
|
|
32
|
+
}
|
|
33
|
+
interface OutageEvent {
|
|
34
|
+
asset_name: string;
|
|
35
|
+
fuel_type: string;
|
|
36
|
+
country: string;
|
|
37
|
+
unavailable_mw: number;
|
|
38
|
+
installed_mw: number;
|
|
39
|
+
event_start: string;
|
|
40
|
+
expected_return: string;
|
|
41
|
+
status: string;
|
|
42
|
+
}
|
|
43
|
+
interface OutageResult {
|
|
44
|
+
message_type: "outage_events";
|
|
45
|
+
source: string;
|
|
46
|
+
description: string;
|
|
47
|
+
count: number;
|
|
48
|
+
events: OutageEvent[];
|
|
49
|
+
}
|
|
50
|
+
type AcerRemitResult = UmmResult | OutageResult;
|
|
51
|
+
/**
|
|
52
|
+
* ACER REMIT data is available from multiple Inside Information Platforms (IIPs).
|
|
53
|
+
* We use a combination of ENTSO-E REMIT (already available via get_remit_messages)
|
|
54
|
+
* and supplement with aggregated ACER data where available.
|
|
55
|
+
*
|
|
56
|
+
* Since the centralized ACER REMIT Data Reference Centre launched in May 2025,
|
|
57
|
+
* this tool provides a unified view across multiple IIPs.
|
|
58
|
+
*/
|
|
59
|
+
export declare function getAcerRemit(params: z.infer<typeof acerRemitSchema>): Promise<AcerRemitResult>;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const REMIT_API = "https://remit.emtf.energinet.dk/api";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
export const acerRemitSchema = z.object({
|
|
6
|
+
message_type: z
|
|
7
|
+
.enum(["urgent_market_messages", "outage_events"])
|
|
8
|
+
.describe('"urgent_market_messages" = REMIT Article 4 urgent market messages (UMMs) — ' +
|
|
9
|
+
"forced outages, capacity reductions, and other inside information. " +
|
|
10
|
+
'"outage_events" = generation and transmission outage events from REMIT disclosures.'),
|
|
11
|
+
country: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("ISO-2 country code to filter (e.g. FR, DE, GB, SE, NO). Optional."),
|
|
15
|
+
fuel_type: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Filter by fuel type (e.g. Nuclear, Wind, Gas). Optional."),
|
|
19
|
+
limit: z
|
|
20
|
+
.number()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Max records to return (default 30, max 100)."),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* ACER REMIT data is available from multiple Inside Information Platforms (IIPs).
|
|
26
|
+
* We use a combination of ENTSO-E REMIT (already available via get_remit_messages)
|
|
27
|
+
* and supplement with aggregated ACER data where available.
|
|
28
|
+
*
|
|
29
|
+
* Since the centralized ACER REMIT Data Reference Centre launched in May 2025,
|
|
30
|
+
* this tool provides a unified view across multiple IIPs.
|
|
31
|
+
*/
|
|
32
|
+
export async function getAcerRemit(params) {
|
|
33
|
+
const limit = Math.min(params.limit ?? 30, 100);
|
|
34
|
+
const country = params.country?.toUpperCase();
|
|
35
|
+
const fuelType = params.fuel_type;
|
|
36
|
+
// Build query params
|
|
37
|
+
const queryParams = new URLSearchParams({
|
|
38
|
+
limit: String(limit),
|
|
39
|
+
...(country ? { country } : {}),
|
|
40
|
+
...(fuelType ? { fuelType } : {}),
|
|
41
|
+
});
|
|
42
|
+
if (params.message_type === "urgent_market_messages") {
|
|
43
|
+
return fetchUmms(queryParams, country, fuelType, limit);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return fetchOutageEvents(queryParams, country, fuelType, limit);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function fetchUmms(queryParams, country, fuelType, limit) {
|
|
50
|
+
const url = `${REMIT_API}/umm?${queryParams}`;
|
|
51
|
+
const cached = cache.get(url);
|
|
52
|
+
if (cached)
|
|
53
|
+
return cached;
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
headers: { Accept: "application/json", "User-Agent": "luminus-mcp/0.2" },
|
|
57
|
+
});
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
const json = await response.json();
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
const items = Array.isArray(json) ? json : json?.data ?? json?.records ?? [];
|
|
63
|
+
const records = items.slice(0, limit).map((u) => ({
|
|
64
|
+
message_id: u.messageId ?? u.id ?? "",
|
|
65
|
+
event_type: u.eventType ?? u.type ?? "Unknown",
|
|
66
|
+
market_participant: u.marketParticipant ?? u.participant ?? "",
|
|
67
|
+
asset_name: u.assetName ?? u.unitName ?? "Unknown",
|
|
68
|
+
fuel_type: u.fuelType ?? u.productionType ?? "Unknown",
|
|
69
|
+
country: u.biddingZone ?? u.country ?? "",
|
|
70
|
+
unavailable_mw: Math.round(Number(u.unavailableCapacity ?? u.unavailableMw ?? 0)),
|
|
71
|
+
available_mw: Math.round(Number(u.availableCapacity ?? u.availableMw ?? 0)),
|
|
72
|
+
installed_mw: Math.round(Number(u.installedCapacity ?? u.nominalPower ?? 0)),
|
|
73
|
+
event_start: u.eventStart ?? u.startDate ?? "",
|
|
74
|
+
event_end: u.eventEnd ?? u.endDate ?? "",
|
|
75
|
+
publication_date: u.publicationDate ?? u.publishedAt ?? "",
|
|
76
|
+
reason: u.reason ?? u.remarks ?? "",
|
|
77
|
+
}));
|
|
78
|
+
const result = {
|
|
79
|
+
message_type: "urgent_market_messages",
|
|
80
|
+
source: "ACER REMIT Inside Information Platforms",
|
|
81
|
+
description: `REMIT urgent market messages${country ? ` for ${country}` : ""}` +
|
|
82
|
+
`${fuelType ? ` (${fuelType})` : ""}. ` +
|
|
83
|
+
"Forced outages and capacity reductions that constitute inside information.",
|
|
84
|
+
count: records.length,
|
|
85
|
+
records,
|
|
86
|
+
};
|
|
87
|
+
cache.set(url, result, TTL.OUTAGES);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Fall through to fallback
|
|
93
|
+
}
|
|
94
|
+
// Fallback: provide guidance on accessing REMIT data
|
|
95
|
+
return {
|
|
96
|
+
message_type: "urgent_market_messages",
|
|
97
|
+
source: "ACER REMIT (reference)",
|
|
98
|
+
description: "ACER REMIT UMM data is available from approved Inside Information Platforms (IIPs). " +
|
|
99
|
+
"Key platforms: EEX Transparency (eex-transparency.com), Nordpool UMM (umm.nordpoolgroup.com), " +
|
|
100
|
+
"REMIT portal (remit.acer.europa.eu). For ENTSO-E REMIT messages, use get_remit_messages tool. " +
|
|
101
|
+
`${country ? `Filtered for: ${country}. ` : ""}` +
|
|
102
|
+
`${fuelType ? `Fuel: ${fuelType}. ` : ""}`,
|
|
103
|
+
count: 0,
|
|
104
|
+
records: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function fetchOutageEvents(queryParams, country, fuelType, limit) {
|
|
108
|
+
const url = `${REMIT_API}/outages?${queryParams}`;
|
|
109
|
+
const cached = cache.get(url);
|
|
110
|
+
if (cached)
|
|
111
|
+
return cached;
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(url, {
|
|
114
|
+
headers: { Accept: "application/json", "User-Agent": "luminus-mcp/0.2" },
|
|
115
|
+
});
|
|
116
|
+
if (response.ok) {
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
118
|
+
const json = await response.json();
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
+
const items = Array.isArray(json) ? json : json?.data ?? [];
|
|
121
|
+
const events = items.slice(0, limit).map((o) => ({
|
|
122
|
+
asset_name: o.assetName ?? o.unitName ?? "Unknown",
|
|
123
|
+
fuel_type: o.fuelType ?? o.productionType ?? "Unknown",
|
|
124
|
+
country: o.biddingZone ?? o.country ?? "",
|
|
125
|
+
unavailable_mw: Math.round(Number(o.unavailableCapacity ?? 0)),
|
|
126
|
+
installed_mw: Math.round(Number(o.installedCapacity ?? 0)),
|
|
127
|
+
event_start: o.eventStart ?? o.startDate ?? "",
|
|
128
|
+
expected_return: o.eventEnd ?? o.endDate ?? "",
|
|
129
|
+
status: o.status ?? "active",
|
|
130
|
+
}));
|
|
131
|
+
const result = {
|
|
132
|
+
message_type: "outage_events",
|
|
133
|
+
source: "ACER REMIT Inside Information Platforms",
|
|
134
|
+
description: `REMIT outage events${country ? ` for ${country}` : ""}`,
|
|
135
|
+
count: events.length,
|
|
136
|
+
events,
|
|
137
|
+
};
|
|
138
|
+
cache.set(url, result, TTL.OUTAGES);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Fall through
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
message_type: "outage_events",
|
|
147
|
+
source: "ACER REMIT (reference)",
|
|
148
|
+
description: "REMIT outage events aggregated from IIPs. For real-time outage data, " +
|
|
149
|
+
"use get_outages (ENTSO-E) or get_remit_messages (ENTSO-E REMIT UMMs). " +
|
|
150
|
+
"ACER's centralized REMIT Data Reference Centre provides harmonized data at remit.acer.europa.eu.",
|
|
151
|
+
count: 0,
|
|
152
|
+
events: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type GisSourceMetadata } from "../lib/gis-sources.js";
|
|
3
|
+
export declare const agriculturalLandSchema: z.ZodObject<{
|
|
4
|
+
lat: z.ZodNumber;
|
|
5
|
+
lon: z.ZodNumber;
|
|
6
|
+
country: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
type BmvStatus = "yes" | "no" | "uncertain" | "unknown";
|
|
9
|
+
type ClassificationBasis = "post_1988" | "provisional" | "none";
|
|
10
|
+
interface AlcClassification {
|
|
11
|
+
source: "post_1988" | "provisional";
|
|
12
|
+
grade: string | null;
|
|
13
|
+
area_ha: number | null;
|
|
14
|
+
survey_ref: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface AgriculturalLandResult {
|
|
17
|
+
lat: number;
|
|
18
|
+
lon: number;
|
|
19
|
+
country: string;
|
|
20
|
+
post_1988: AlcClassification | null;
|
|
21
|
+
provisional: AlcClassification | null;
|
|
22
|
+
effective_grade: string | null;
|
|
23
|
+
bmv_status: BmvStatus;
|
|
24
|
+
classification_basis: ClassificationBasis;
|
|
25
|
+
explanation: string;
|
|
26
|
+
source_metadata: GisSourceMetadata;
|
|
27
|
+
warnings?: string[];
|
|
28
|
+
disclaimer: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function getAgriculturalLand(params: z.infer<typeof agriculturalLandSchema>): Promise<AgriculturalLandResult>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
import { GIS_SOURCES } from "../lib/gis-sources.js";
|
|
4
|
+
import { guardArcGisFields } from "../lib/schema-guard.js";
|
|
5
|
+
const cache = new TtlCache();
|
|
6
|
+
const NE_ARCGIS_BASE = "https://services.arcgis.com/JJzESW51TqeY9uat/arcgis/rest/services";
|
|
7
|
+
const POST_1988_SERVICE = "Agricultural_Land_Classification_Post_1988";
|
|
8
|
+
const PROVISIONAL_SERVICE = "Provisional Agricultural Land Classification (ALC) (England)";
|
|
9
|
+
export const agriculturalLandSchema = z.object({
|
|
10
|
+
lat: z.number().describe("Latitude (-90 to 90). WGS84."),
|
|
11
|
+
lon: z.number().describe("Longitude (-180 to 180). WGS84."),
|
|
12
|
+
country: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe('ISO 3166-1 alpha-2 country code. Only "GB" is supported in this version.'),
|
|
15
|
+
});
|
|
16
|
+
const DISCLAIMER = "This is an automated agricultural-land screening result using public data. " +
|
|
17
|
+
"It is not a planning determination or a substitute for a formal Agricultural Land Classification survey.";
|
|
18
|
+
function buildPointQueryUrl(serviceName, lon, lat, outFields) {
|
|
19
|
+
const url = new URL(`${NE_ARCGIS_BASE}/${serviceName}/FeatureServer/0/query`);
|
|
20
|
+
const p = url.searchParams;
|
|
21
|
+
p.set("where", "1=1");
|
|
22
|
+
p.set("geometry", `${lon},${lat}`);
|
|
23
|
+
p.set("geometryType", "esriGeometryPoint");
|
|
24
|
+
p.set("inSR", "4326");
|
|
25
|
+
p.set("spatialRel", "esriSpatialRelIntersects");
|
|
26
|
+
p.set("outFields", outFields.join(","));
|
|
27
|
+
p.set("returnGeometry", "false");
|
|
28
|
+
p.set("resultRecordCount", "5");
|
|
29
|
+
p.set("f", "json");
|
|
30
|
+
return url.toString();
|
|
31
|
+
}
|
|
32
|
+
async function queryPointLayer(serviceName, lon, lat, outFields) {
|
|
33
|
+
const response = await fetch(buildPointQueryUrl(serviceName, lon, lat, outFields));
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const body = await response.text();
|
|
36
|
+
throw new Error(`Natural England ALC API returned ${response.status} for ${serviceName}: ${body.slice(0, 300)}`);
|
|
37
|
+
}
|
|
38
|
+
const json = await response.json();
|
|
39
|
+
if (json.error) {
|
|
40
|
+
throw new Error(`Natural England ALC API error for ${serviceName}: ${json.error.message ?? JSON.stringify(json.error)}`);
|
|
41
|
+
}
|
|
42
|
+
const features = Array.isArray(json.features) ? json.features : [];
|
|
43
|
+
guardArcGisFields(features, outFields, `Natural England ALC (${serviceName})`);
|
|
44
|
+
return features;
|
|
45
|
+
}
|
|
46
|
+
function normaliseGrade(value) {
|
|
47
|
+
if (typeof value !== "string")
|
|
48
|
+
return null;
|
|
49
|
+
const trimmed = value.trim().replace(/\s+/g, " ");
|
|
50
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
51
|
+
}
|
|
52
|
+
function toRoundedNumber(value) {
|
|
53
|
+
return typeof value === "number" ? Math.round(value * 100) / 100 : null;
|
|
54
|
+
}
|
|
55
|
+
function pickFeature(features) {
|
|
56
|
+
return (features.find((feature) => normaliseGrade(feature.attributes?.ALC_GRADE) !== null) ??
|
|
57
|
+
features[0] ??
|
|
58
|
+
null);
|
|
59
|
+
}
|
|
60
|
+
function mapPost1988(feature) {
|
|
61
|
+
if (!feature)
|
|
62
|
+
return null;
|
|
63
|
+
const attrs = feature.attributes ?? {};
|
|
64
|
+
return {
|
|
65
|
+
source: "post_1988",
|
|
66
|
+
grade: normaliseGrade(attrs.ALC_GRADE),
|
|
67
|
+
area_ha: toRoundedNumber(attrs.HECTARES),
|
|
68
|
+
survey_ref: typeof attrs.RPT === "string" ? attrs.RPT.trim() || null : null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function mapProvisional(feature) {
|
|
72
|
+
if (!feature)
|
|
73
|
+
return null;
|
|
74
|
+
const attrs = feature.attributes ?? {};
|
|
75
|
+
return {
|
|
76
|
+
source: "provisional",
|
|
77
|
+
grade: normaliseGrade(attrs.ALC_GRADE),
|
|
78
|
+
area_ha: toRoundedNumber(attrs.AREA),
|
|
79
|
+
survey_ref: typeof attrs.GEOGEXT === "string" ? attrs.GEOGEXT.trim() || null : null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function detailedBmvStatus(grade) {
|
|
83
|
+
if (!grade)
|
|
84
|
+
return "unknown";
|
|
85
|
+
const value = grade.toLowerCase();
|
|
86
|
+
if (["grade 1", "grade 2", "grade 3a"].includes(value))
|
|
87
|
+
return "yes";
|
|
88
|
+
if (["grade 3", "grade 3b"].includes(value))
|
|
89
|
+
return value === "grade 3" ? "uncertain" : "no";
|
|
90
|
+
if ([
|
|
91
|
+
"grade 4",
|
|
92
|
+
"grade 5",
|
|
93
|
+
"non agricultural",
|
|
94
|
+
"urban",
|
|
95
|
+
"other land",
|
|
96
|
+
"water",
|
|
97
|
+
].includes(value)) {
|
|
98
|
+
return "no";
|
|
99
|
+
}
|
|
100
|
+
return "unknown";
|
|
101
|
+
}
|
|
102
|
+
function provisionalBmvStatus(grade) {
|
|
103
|
+
if (!grade)
|
|
104
|
+
return "unknown";
|
|
105
|
+
const value = grade.toLowerCase();
|
|
106
|
+
if (["grade 1", "grade 2"].includes(value))
|
|
107
|
+
return "yes";
|
|
108
|
+
if (value === "grade 3")
|
|
109
|
+
return "uncertain";
|
|
110
|
+
if ([
|
|
111
|
+
"grade 4",
|
|
112
|
+
"grade 5",
|
|
113
|
+
"non agricultural",
|
|
114
|
+
"urban",
|
|
115
|
+
"other land",
|
|
116
|
+
"water",
|
|
117
|
+
].includes(value)) {
|
|
118
|
+
return "no";
|
|
119
|
+
}
|
|
120
|
+
return "unknown";
|
|
121
|
+
}
|
|
122
|
+
function buildExplanation(basis, effectiveGrade, bmvStatus) {
|
|
123
|
+
if (basis === "post_1988") {
|
|
124
|
+
if (bmvStatus === "yes") {
|
|
125
|
+
return `Detailed post-1988 ALC survey classifies this site as ${effectiveGrade}, which is Best and Most Versatile agricultural land.`;
|
|
126
|
+
}
|
|
127
|
+
if (bmvStatus === "no") {
|
|
128
|
+
return `Detailed post-1988 ALC survey classifies this site as ${effectiveGrade}, which is not Best and Most Versatile agricultural land.`;
|
|
129
|
+
}
|
|
130
|
+
return `Detailed post-1988 ALC survey returned ${effectiveGrade ?? "an unknown grade"}. Treat BMV status as uncertain until checked manually.`;
|
|
131
|
+
}
|
|
132
|
+
if (basis === "provisional") {
|
|
133
|
+
if (bmvStatus === "yes") {
|
|
134
|
+
return `Provisional ALC classifies this site as ${effectiveGrade}, which strongly suggests Best and Most Versatile agricultural land.`;
|
|
135
|
+
}
|
|
136
|
+
if (bmvStatus === "no") {
|
|
137
|
+
return `Provisional ALC classifies this site as ${effectiveGrade}, which is not Best and Most Versatile agricultural land.`;
|
|
138
|
+
}
|
|
139
|
+
return `Provisional ALC classifies this site as ${effectiveGrade ?? "an unknown grade"}. Grade 3 cannot distinguish 3a from 3b, so BMV status is uncertain.`;
|
|
140
|
+
}
|
|
141
|
+
return "No Natural England ALC polygon matched this point. Coverage is England-only and incomplete, so BMV status is unknown rather than clear.";
|
|
142
|
+
}
|
|
143
|
+
export async function getAgriculturalLand(params) {
|
|
144
|
+
const { lat, lon } = params;
|
|
145
|
+
const country = params.country.toUpperCase();
|
|
146
|
+
if (country !== "GB") {
|
|
147
|
+
throw new Error(`Country "${params.country}" is not supported. Only "GB" (Great Britain) is available in this version. England coverage is implemented first via Natural England ALC data.`);
|
|
148
|
+
}
|
|
149
|
+
if (lat < -90 || lat > 90) {
|
|
150
|
+
throw new Error("Latitude must be between -90 and 90.");
|
|
151
|
+
}
|
|
152
|
+
if (lon < -180 || lon > 180) {
|
|
153
|
+
throw new Error("Longitude must be between -180 and 180.");
|
|
154
|
+
}
|
|
155
|
+
const cacheKey = `agricultural-land:${lat}:${lon}:${country}`;
|
|
156
|
+
const cached = cache.get(cacheKey);
|
|
157
|
+
if (cached)
|
|
158
|
+
return cached;
|
|
159
|
+
const [post1988Result, provisionalResult] = await Promise.allSettled([
|
|
160
|
+
queryPointLayer(POST_1988_SERVICE, lon, lat, ["ALC_GRADE", "HECTARES", "RPT"]),
|
|
161
|
+
queryPointLayer(PROVISIONAL_SERVICE, lon, lat, ["ALC_GRADE", "AREA", "GEOGEXT"]),
|
|
162
|
+
]);
|
|
163
|
+
const warnings = [];
|
|
164
|
+
const post1988 = post1988Result.status === "fulfilled"
|
|
165
|
+
? mapPost1988(pickFeature(post1988Result.value))
|
|
166
|
+
: null;
|
|
167
|
+
if (post1988Result.status === "rejected") {
|
|
168
|
+
warnings.push(`post_1988: ${post1988Result.reason instanceof Error ? post1988Result.reason.message : String(post1988Result.reason)}`);
|
|
169
|
+
}
|
|
170
|
+
const provisional = provisionalResult.status === "fulfilled"
|
|
171
|
+
? mapProvisional(pickFeature(provisionalResult.value))
|
|
172
|
+
: null;
|
|
173
|
+
if (provisionalResult.status === "rejected") {
|
|
174
|
+
warnings.push(`provisional: ${provisionalResult.reason instanceof Error ? provisionalResult.reason.message : String(provisionalResult.reason)}`);
|
|
175
|
+
}
|
|
176
|
+
if (post1988Result.status === "rejected" && provisionalResult.status === "rejected") {
|
|
177
|
+
throw new Error(`All Natural England ALC queries failed: ${warnings.join("; ")}`);
|
|
178
|
+
}
|
|
179
|
+
let classificationBasis = "none";
|
|
180
|
+
let effectiveGrade = null;
|
|
181
|
+
let bmvStatus = "unknown";
|
|
182
|
+
if (post1988 !== null) {
|
|
183
|
+
classificationBasis = "post_1988";
|
|
184
|
+
effectiveGrade = post1988.grade;
|
|
185
|
+
bmvStatus = detailedBmvStatus(post1988.grade);
|
|
186
|
+
}
|
|
187
|
+
else if (provisional !== null) {
|
|
188
|
+
classificationBasis = "provisional";
|
|
189
|
+
effectiveGrade = provisional.grade;
|
|
190
|
+
bmvStatus = provisionalBmvStatus(provisional.grade);
|
|
191
|
+
}
|
|
192
|
+
const result = {
|
|
193
|
+
lat,
|
|
194
|
+
lon,
|
|
195
|
+
country: "GB",
|
|
196
|
+
post_1988: post1988,
|
|
197
|
+
provisional,
|
|
198
|
+
effective_grade: effectiveGrade,
|
|
199
|
+
bmv_status: bmvStatus,
|
|
200
|
+
classification_basis: classificationBasis,
|
|
201
|
+
explanation: buildExplanation(classificationBasis, effectiveGrade, bmvStatus),
|
|
202
|
+
source_metadata: GIS_SOURCES["natural-england-alc"],
|
|
203
|
+
disclaimer: DISCLAIMER,
|
|
204
|
+
};
|
|
205
|
+
if (warnings.length > 0) {
|
|
206
|
+
result.warnings = warnings;
|
|
207
|
+
}
|
|
208
|
+
cache.set(cacheKey, result, TTL.STATIC_DATA);
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ancillaryPricesSchema: z.ZodObject<{
|
|
3
|
+
zone: z.ZodString;
|
|
4
|
+
service: z.ZodOptional<z.ZodEnum<{
|
|
5
|
+
fcr: "fcr";
|
|
6
|
+
afrr: "afrr";
|
|
7
|
+
mfrr: "mfrr";
|
|
8
|
+
}>>;
|
|
9
|
+
date: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
interface AncillaryPricePoint {
|
|
12
|
+
period: number;
|
|
13
|
+
price_eur_mw: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function getAncillaryPrices(params: z.infer<typeof ancillaryPricesSchema>): Promise<{
|
|
16
|
+
zone: string;
|
|
17
|
+
service: string;
|
|
18
|
+
date: string;
|
|
19
|
+
currency: string;
|
|
20
|
+
prices: AncillaryPricePoint[];
|
|
21
|
+
stats: {
|
|
22
|
+
min: number;
|
|
23
|
+
max: number;
|
|
24
|
+
mean: number;
|
|
25
|
+
};
|
|
26
|
+
}>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
const SERVICE_PROCESS_TYPES = {
|
|
7
|
+
fcr: "A51",
|
|
8
|
+
afrr: "A52",
|
|
9
|
+
mfrr: "A56",
|
|
10
|
+
};
|
|
11
|
+
export const ancillaryPricesSchema = z.object({
|
|
12
|
+
zone: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe(`Bidding zone code. Examples: DE, FR, GB. Available: ${AVAILABLE_ZONES}`),
|
|
15
|
+
service: z
|
|
16
|
+
.enum(["fcr", "afrr", "mfrr"])
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Reserve type: 'fcr' (default), 'afrr', or 'mfrr'."),
|
|
19
|
+
date: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Date in YYYY-MM-DD format. Defaults to today."),
|
|
23
|
+
});
|
|
24
|
+
export async function getAncillaryPrices(params) {
|
|
25
|
+
const eic = resolveZone(params.zone);
|
|
26
|
+
const service = params.service ?? "fcr";
|
|
27
|
+
const processType = SERVICE_PROCESS_TYPES[service];
|
|
28
|
+
const { periodStart, periodEnd } = dayRange(params.date);
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
const data = await queryEntsoe({
|
|
31
|
+
documentType: "A84",
|
|
32
|
+
processType,
|
|
33
|
+
controlArea_Domain: eic,
|
|
34
|
+
periodStart,
|
|
35
|
+
periodEnd,
|
|
36
|
+
}, TTL.ANCILLARY);
|
|
37
|
+
const doc = data.Publication_MarketDocument ??
|
|
38
|
+
data.Balancing_MarketDocument ??
|
|
39
|
+
data.GL_MarketDocument;
|
|
40
|
+
if (!doc)
|
|
41
|
+
throw new Error("No ancillary price data returned for this zone/date.");
|
|
42
|
+
const timeSeries = ensureArray(doc.TimeSeries);
|
|
43
|
+
const prices = [];
|
|
44
|
+
for (const ts of timeSeries) {
|
|
45
|
+
const periods = ensureArray(ts.Period);
|
|
46
|
+
for (const period of periods) {
|
|
47
|
+
const points = ensureArray(period.Point);
|
|
48
|
+
for (const point of points) {
|
|
49
|
+
const position = Number(point.position);
|
|
50
|
+
const price = Number(point["procurement_Price.amount"] ?? point["price.amount"] ?? point.quantity ?? 0);
|
|
51
|
+
prices.push({ period: position, price_eur_mw: Math.round(price * 100) / 100 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
prices.sort((a, b) => a.period - b.period);
|
|
56
|
+
const values = prices.map((p) => p.price_eur_mw);
|
|
57
|
+
const min = values.length > 0 ? Math.min(...values) : 0;
|
|
58
|
+
const max = values.length > 0 ? Math.max(...values) : 0;
|
|
59
|
+
const mean = values.length > 0
|
|
60
|
+
? Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 100) / 100
|
|
61
|
+
: 0;
|
|
62
|
+
return {
|
|
63
|
+
zone: params.zone.toUpperCase(),
|
|
64
|
+
service,
|
|
65
|
+
date: params.date ?? new Date().toISOString().slice(0, 10),
|
|
66
|
+
currency: "EUR",
|
|
67
|
+
prices,
|
|
68
|
+
stats: { min, max, mean },
|
|
69
|
+
};
|
|
70
|
+
}
|