@iflow-mcp/kitfunso-luminus 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +488 -0
  5. package/dist/lib/audit.d.ts +3 -0
  6. package/dist/lib/audit.js +66 -0
  7. package/dist/lib/auth.d.ts +26 -0
  8. package/dist/lib/auth.js +199 -0
  9. package/dist/lib/cache.d.ts +25 -0
  10. package/dist/lib/cache.js +38 -0
  11. package/dist/lib/cli.d.ts +1 -0
  12. package/dist/lib/cli.js +10 -0
  13. package/dist/lib/corine.d.ts +31 -0
  14. package/dist/lib/corine.js +137 -0
  15. package/dist/lib/eea-natura2000.d.ts +7 -0
  16. package/dist/lib/eea-natura2000.js +53 -0
  17. package/dist/lib/entsoe-client.d.ts +22 -0
  18. package/dist/lib/entsoe-client.js +69 -0
  19. package/dist/lib/gis-sources.d.ts +33 -0
  20. package/dist/lib/gis-sources.js +392 -0
  21. package/dist/lib/natural-england.d.ts +27 -0
  22. package/dist/lib/natural-england.js +105 -0
  23. package/dist/lib/neso-gsp.d.ts +18 -0
  24. package/dist/lib/neso-gsp.js +113 -0
  25. package/dist/lib/overpass.d.ts +13 -0
  26. package/dist/lib/overpass.js +193 -0
  27. package/dist/lib/profiles.d.ts +23 -0
  28. package/dist/lib/profiles.js +149 -0
  29. package/dist/lib/schema-guard.d.ts +22 -0
  30. package/dist/lib/schema-guard.js +38 -0
  31. package/dist/lib/tool-handler.d.ts +15 -0
  32. package/dist/lib/tool-handler.js +95 -0
  33. package/dist/lib/xml-parser.d.ts +4 -0
  34. package/dist/lib/xml-parser.js +34 -0
  35. package/dist/lib/zone-codes.d.ts +12 -0
  36. package/dist/lib/zone-codes.js +127 -0
  37. package/dist/tools/acer-remit.d.ts +60 -0
  38. package/dist/tools/acer-remit.js +154 -0
  39. package/dist/tools/agricultural-land.d.ts +31 -0
  40. package/dist/tools/agricultural-land.js +210 -0
  41. package/dist/tools/ancillary-prices.d.ts +27 -0
  42. package/dist/tools/ancillary-prices.js +70 -0
  43. package/dist/tools/auctions.d.ts +15 -0
  44. package/dist/tools/auctions.js +89 -0
  45. package/dist/tools/balancing-actions.d.ts +22 -0
  46. package/dist/tools/balancing-actions.js +151 -0
  47. package/dist/tools/balancing.d.ts +21 -0
  48. package/dist/tools/balancing.js +56 -0
  49. package/dist/tools/carbon.d.ts +21 -0
  50. package/dist/tools/carbon.js +68 -0
  51. package/dist/tools/commodity-prices.d.ts +26 -0
  52. package/dist/tools/commodity-prices.js +100 -0
  53. package/dist/tools/compare-sites.d.ts +41 -0
  54. package/dist/tools/compare-sites.js +237 -0
  55. package/dist/tools/demand-forecast.d.ts +21 -0
  56. package/dist/tools/demand-forecast.js +56 -0
  57. package/dist/tools/elexon-bmrs.d.ts +72 -0
  58. package/dist/tools/elexon-bmrs.js +117 -0
  59. package/dist/tools/energi-data.d.ts +72 -0
  60. package/dist/tools/energi-data.js +170 -0
  61. package/dist/tools/energy-charts.d.ts +103 -0
  62. package/dist/tools/energy-charts.js +411 -0
  63. package/dist/tools/entsog.d.ts +71 -0
  64. package/dist/tools/entsog.js +159 -0
  65. package/dist/tools/era5-weather.d.ts +39 -0
  66. package/dist/tools/era5-weather.js +117 -0
  67. package/dist/tools/eu-gas-price.d.ts +38 -0
  68. package/dist/tools/eu-gas-price.js +110 -0
  69. package/dist/tools/fingrid.d.ts +39 -0
  70. package/dist/tools/fingrid.js +158 -0
  71. package/dist/tools/flood-risk.d.ts +33 -0
  72. package/dist/tools/flood-risk.js +166 -0
  73. package/dist/tools/flows.d.ts +23 -0
  74. package/dist/tools/flows.js +61 -0
  75. package/dist/tools/frequency.d.ts +10 -0
  76. package/dist/tools/frequency.js +35 -0
  77. package/dist/tools/gas-storage.d.ts +18 -0
  78. package/dist/tools/gas-storage.js +72 -0
  79. package/dist/tools/generation.d.ts +17 -0
  80. package/dist/tools/generation.js +80 -0
  81. package/dist/tools/grid-connection-intelligence.d.ts +42 -0
  82. package/dist/tools/grid-connection-intelligence.js +122 -0
  83. package/dist/tools/grid-connection-queue.d.ts +64 -0
  84. package/dist/tools/grid-connection-queue.js +198 -0
  85. package/dist/tools/grid-proximity.d.ts +38 -0
  86. package/dist/tools/grid-proximity.js +123 -0
  87. package/dist/tools/hydro-inflows.d.ts +34 -0
  88. package/dist/tools/hydro-inflows.js +114 -0
  89. package/dist/tools/hydro.d.ts +18 -0
  90. package/dist/tools/hydro.js +85 -0
  91. package/dist/tools/imbalance-prices.d.ts +21 -0
  92. package/dist/tools/imbalance-prices.js +56 -0
  93. package/dist/tools/intraday-prices.d.ts +21 -0
  94. package/dist/tools/intraday-prices.js +57 -0
  95. package/dist/tools/intraday-spread.d.ts +24 -0
  96. package/dist/tools/intraday-spread.js +55 -0
  97. package/dist/tools/land-constraints.d.ts +25 -0
  98. package/dist/tools/land-constraints.js +148 -0
  99. package/dist/tools/land-cover.d.ts +18 -0
  100. package/dist/tools/land-cover.js +64 -0
  101. package/dist/tools/lng-terminals.d.ts +22 -0
  102. package/dist/tools/lng-terminals.js +75 -0
  103. package/dist/tools/net-positions.d.ts +19 -0
  104. package/dist/tools/net-positions.js +74 -0
  105. package/dist/tools/nordpool-prices.d.ts +29 -0
  106. package/dist/tools/nordpool-prices.js +80 -0
  107. package/dist/tools/outages.d.ts +28 -0
  108. package/dist/tools/outages.js +107 -0
  109. package/dist/tools/power-plants.d.ts +26 -0
  110. package/dist/tools/power-plants.js +224 -0
  111. package/dist/tools/price-spread-analysis.d.ts +27 -0
  112. package/dist/tools/price-spread-analysis.js +97 -0
  113. package/dist/tools/prices.d.ts +23 -0
  114. package/dist/tools/prices.js +79 -0
  115. package/dist/tools/realtime-generation.d.ts +19 -0
  116. package/dist/tools/realtime-generation.js +141 -0
  117. package/dist/tools/ree-esios.d.ts +78 -0
  118. package/dist/tools/ree-esios.js +216 -0
  119. package/dist/tools/regelleistung.d.ts +28 -0
  120. package/dist/tools/regelleistung.js +71 -0
  121. package/dist/tools/remit-messages.d.ts +23 -0
  122. package/dist/tools/remit-messages.js +110 -0
  123. package/dist/tools/renewable-forecast.d.ts +23 -0
  124. package/dist/tools/renewable-forecast.js +75 -0
  125. package/dist/tools/rte-france.d.ts +72 -0
  126. package/dist/tools/rte-france.js +147 -0
  127. package/dist/tools/screen-site.d.ts +50 -0
  128. package/dist/tools/screen-site.js +288 -0
  129. package/dist/tools/site-revenue.d.ts +50 -0
  130. package/dist/tools/site-revenue.js +147 -0
  131. package/dist/tools/smard-data.d.ts +34 -0
  132. package/dist/tools/smard-data.js +155 -0
  133. package/dist/tools/solar.d.ts +23 -0
  134. package/dist/tools/solar.js +69 -0
  135. package/dist/tools/stormglass.d.ts +56 -0
  136. package/dist/tools/stormglass.js +172 -0
  137. package/dist/tools/terna.d.ts +69 -0
  138. package/dist/tools/terna.js +159 -0
  139. package/dist/tools/terrain-analysis.d.ts +19 -0
  140. package/dist/tools/terrain-analysis.js +120 -0
  141. package/dist/tools/transfer-capacity.d.ts +22 -0
  142. package/dist/tools/transfer-capacity.js +61 -0
  143. package/dist/tools/transmission.d.ts +29 -0
  144. package/dist/tools/transmission.js +159 -0
  145. package/dist/tools/uk-carbon.d.ts +51 -0
  146. package/dist/tools/uk-carbon.js +109 -0
  147. package/dist/tools/uk-grid.d.ts +28 -0
  148. package/dist/tools/uk-grid.js +70 -0
  149. package/dist/tools/us-gas.d.ts +30 -0
  150. package/dist/tools/us-gas.js +100 -0
  151. package/dist/tools/verify-gis-sources.d.ts +25 -0
  152. package/dist/tools/verify-gis-sources.js +119 -0
  153. package/dist/tools/weather.d.ts +27 -0
  154. package/dist/tools/weather.js +120 -0
  155. package/package.json +62 -0
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ export declare const auctionSchema: z.ZodObject<{
3
+ date: z.ZodString;
4
+ corridor: z.ZodString;
5
+ }, z.core.$strip>;
6
+ interface AuctionResult {
7
+ corridor: string;
8
+ date: string;
9
+ allocated_capacity_mw: number | null;
10
+ auction_price_eur_mw: number | null;
11
+ offered_capacity_mw: number | null;
12
+ requested_capacity_mw: number | null;
13
+ }
14
+ export declare function getAuctionResults(params: z.infer<typeof auctionSchema>): Promise<AuctionResult>;
15
+ export {};
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ const BASE_URL = "https://publicationtool.jao.eu/core/api/data";
4
+ const cache = new TtlCache();
5
+ const MAX_RETRIES = 3;
6
+ const RETRY_DELAY_MS = 1000;
7
+ const CORRIDORS = [
8
+ "DE-FR", "FR-DE", "DE-NL", "NL-DE", "DE-BE", "BE-DE",
9
+ "DE-AT", "AT-DE", "DE-PL", "PL-DE", "DE-CZ", "CZ-DE",
10
+ "DE-DK1", "DK1-DE", "FR-BE", "BE-FR", "FR-ES", "ES-FR",
11
+ "NL-BE", "BE-NL", "AT-CZ", "CZ-AT", "AT-HU", "HU-AT",
12
+ "AT-SI", "SI-AT", "PL-CZ", "CZ-PL", "PL-SK", "SK-PL",
13
+ "HU-SK", "SK-HU", "HU-RO", "RO-HU", "HR-SI", "SI-HR",
14
+ "HR-HU", "HU-HR",
15
+ ];
16
+ export const auctionSchema = z.object({
17
+ date: z
18
+ .string()
19
+ .describe("Auction date in YYYY-MM-DD format."),
20
+ corridor: z
21
+ .string()
22
+ .describe(`Border corridor (e.g. DE-FR, FR-DE, NL-BE). Available: ${CORRIDORS.slice(0, 10).join(", ")}...`),
23
+ });
24
+ async function fetchWithRetry(url, retries = MAX_RETRIES) {
25
+ for (let attempt = 1; attempt <= retries; attempt++) {
26
+ try {
27
+ const response = await fetch(url, {
28
+ headers: { Accept: "application/json" },
29
+ });
30
+ if (response.ok)
31
+ return response;
32
+ if (attempt < retries && response.status >= 500) {
33
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS * attempt));
34
+ continue;
35
+ }
36
+ const body = await response.text();
37
+ throw new Error(`JAO API returned ${response.status}: ${body.slice(0, 300)}`);
38
+ }
39
+ catch (err) {
40
+ if (attempt >= retries)
41
+ throw err;
42
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS * attempt));
43
+ }
44
+ }
45
+ throw new Error("JAO API: max retries exceeded");
46
+ }
47
+ export async function getAuctionResults(params) {
48
+ const corridor = params.corridor.toUpperCase();
49
+ const cacheKey = `jao:${corridor}:${params.date}`;
50
+ const cached = cache.get(cacheKey);
51
+ if (cached)
52
+ return cached;
53
+ // JAO API requires FromUtc and ToUtc as ISO datetime range
54
+ const fromUtc = `${params.date}T00:00:00.000Z`;
55
+ const toUtc = `${params.date}T23:59:59.999Z`;
56
+ const url = `${BASE_URL}/finalComputation?FromUtc=${fromUtc}&ToUtc=${toUtc}&corridor=${corridor}`;
57
+ const response = await fetchWithRetry(url);
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ const json = await response.json();
60
+ // JAO finalComputation endpoint now returns CNEC flow-based data (not auction allocations).
61
+ // Extract cross-border capacity results for the requested corridor.
62
+ const entries = Array.isArray(json) ? json : json.data ? (Array.isArray(json.data) ? json.data : [json.data]) : [json];
63
+ if (entries.length === 0) {
64
+ throw new Error(`No auction data for corridor ${corridor} on ${params.date}.`);
65
+ }
66
+ // Filter entries relevant to this corridor direction
67
+ const [hubFrom, hubTo] = corridor.split("-");
68
+ const relevant = entries.filter((e) => e.hubFrom === hubFrom && e.hubTo === hubTo);
69
+ // Aggregate: sum RAM (Remaining Available Margin) across CNECs as a capacity proxy
70
+ let totalRamMw = 0;
71
+ let count = 0;
72
+ for (const e of relevant) {
73
+ const ram = Number(e.ram ?? 0);
74
+ if (ram > 0) {
75
+ totalRamMw += ram;
76
+ count++;
77
+ }
78
+ }
79
+ const result = {
80
+ corridor,
81
+ date: params.date,
82
+ allocated_capacity_mw: count > 0 ? Math.round(totalRamMw / count) : null,
83
+ auction_price_eur_mw: null, // No longer in this endpoint
84
+ offered_capacity_mw: count > 0 ? Math.round(totalRamMw / count) : null,
85
+ requested_capacity_mw: null,
86
+ };
87
+ cache.set(cacheKey, result, TTL.AUCTION);
88
+ return result;
89
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ export declare const balancingActionsSchema: z.ZodObject<{
3
+ zone: z.ZodString;
4
+ date: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$strip>;
6
+ interface BalancingAction {
7
+ direction: "up" | "down";
8
+ volume_mw: number;
9
+ period: number;
10
+ }
11
+ interface BalancingActionsResult {
12
+ zone: string;
13
+ date: string;
14
+ actions: BalancingAction[];
15
+ summary: {
16
+ total_up_mw: number;
17
+ total_down_mw: number;
18
+ net_mw: number;
19
+ };
20
+ }
21
+ export declare function getBalancingActions(params: z.infer<typeof balancingActionsSchema>): Promise<BalancingActionsResult>;
22
+ export {};
@@ -0,0 +1,151 @@
1
+ import { z } from "zod";
2
+ import { queryEntsoe, dayRange } from "../lib/entsoe-client.js";
3
+ import { resolveZone, AVAILABLE_ZONES } from "../lib/zone-codes.js";
4
+ import { ensureArray } from "../lib/xml-parser.js";
5
+ import { TtlCache, TTL } from "../lib/cache.js";
6
+ const ELEXON_API = "https://data.elexon.co.uk/bmrs/api/v1";
7
+ const cache = new TtlCache();
8
+ export const balancingActionsSchema = z.object({
9
+ zone: z
10
+ .string()
11
+ .describe(`Bidding zone code. Examples: DE, FR, GB. Available: ${AVAILABLE_ZONES}`),
12
+ date: z
13
+ .string()
14
+ .optional()
15
+ .describe("Date in YYYY-MM-DD format. Defaults to today."),
16
+ });
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ async function fetchElexon(path) {
19
+ const url = `${ELEXON_API}${path}`;
20
+ const cached = cache.get(url);
21
+ if (cached)
22
+ return cached;
23
+ const response = await fetch(url);
24
+ if (!response.ok) {
25
+ const body = await response.text();
26
+ throw new Error(`Elexon BMRS API returned ${response.status}: ${body.slice(0, 300)}`);
27
+ }
28
+ const json = await response.json();
29
+ cache.set(url, json, TTL.BALANCING);
30
+ return json;
31
+ }
32
+ async function getGbBalancingActions(date) {
33
+ const data = await fetchElexon(`/datasets/BOD?settlementDate=${date}&format=json`);
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ const rows = data?.data ?? [];
36
+ if (rows.length === 0) {
37
+ throw new Error("No GB balancing action data available from Elexon BMRS.");
38
+ }
39
+ // Aggregate volumes per settlement period and direction
40
+ const periodMap = new Map();
41
+ for (const row of rows) {
42
+ const sp = Number(row.settlementPeriod ?? 0);
43
+ const level = Number(row.bidOfferPairNumber ?? 0);
44
+ const volume = Number(row.bidOfferLevelTo ?? 0) - Number(row.bidOfferLevelFrom ?? 0);
45
+ if (!periodMap.has(sp)) {
46
+ periodMap.set(sp, { up: 0, down: 0 });
47
+ }
48
+ const entry = periodMap.get(sp);
49
+ if (level > 0 && volume > 0) {
50
+ entry.up += volume;
51
+ }
52
+ else if (level < 0 && volume < 0) {
53
+ entry.down += Math.abs(volume);
54
+ }
55
+ }
56
+ const actions = [];
57
+ for (const [period, volumes] of periodMap) {
58
+ if (volumes.up > 0) {
59
+ actions.push({
60
+ direction: "up",
61
+ volume_mw: Math.round(volumes.up),
62
+ period,
63
+ });
64
+ }
65
+ if (volumes.down > 0) {
66
+ actions.push({
67
+ direction: "down",
68
+ volume_mw: Math.round(volumes.down),
69
+ period,
70
+ });
71
+ }
72
+ }
73
+ actions.sort((a, b) => a.period - b.period);
74
+ const totalUp = actions
75
+ .filter((a) => a.direction === "up")
76
+ .reduce((sum, a) => sum + a.volume_mw, 0);
77
+ const totalDown = actions
78
+ .filter((a) => a.direction === "down")
79
+ .reduce((sum, a) => sum + a.volume_mw, 0);
80
+ return {
81
+ zone: "GB",
82
+ date,
83
+ actions,
84
+ summary: {
85
+ total_up_mw: totalUp,
86
+ total_down_mw: totalDown,
87
+ net_mw: totalUp - totalDown,
88
+ },
89
+ };
90
+ }
91
+ async function getEntsoeBalancingActions(zone, date) {
92
+ const eic = resolveZone(zone);
93
+ const { periodStart, periodEnd } = dayRange(date);
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ const data = await queryEntsoe({
96
+ documentType: "A88",
97
+ processType: "A16",
98
+ controlArea_Domain: eic,
99
+ periodStart,
100
+ periodEnd,
101
+ }, TTL.BALANCING);
102
+ const doc = data.GL_MarketDocument ?? data.Imbalance_MarketDocument ?? data.Publication_MarketDocument;
103
+ if (!doc)
104
+ throw new Error("No balancing action data returned for this zone/date.");
105
+ const timeSeries = ensureArray(doc.TimeSeries);
106
+ const actions = [];
107
+ for (const ts of timeSeries) {
108
+ // flowDirection.direction: A01 = up (increase generation), A02 = down (decrease generation)
109
+ const flowDir = ts["flowDirection.direction"] ?? ts.flowDirection ?? "";
110
+ const direction = flowDir === "A02" ? "down" : "up";
111
+ const periods = ensureArray(ts.Period);
112
+ for (const period of periods) {
113
+ const points = ensureArray(period.Point);
114
+ for (const point of points) {
115
+ const position = Number(point.position);
116
+ const volume = Number(point.quantity ?? 0);
117
+ if (volume > 0) {
118
+ actions.push({
119
+ direction,
120
+ volume_mw: Math.round(volume),
121
+ period: position,
122
+ });
123
+ }
124
+ }
125
+ }
126
+ }
127
+ actions.sort((a, b) => a.period - b.period);
128
+ const totalUp = actions
129
+ .filter((a) => a.direction === "up")
130
+ .reduce((sum, a) => sum + a.volume_mw, 0);
131
+ const totalDown = actions
132
+ .filter((a) => a.direction === "down")
133
+ .reduce((sum, a) => sum + a.volume_mw, 0);
134
+ return {
135
+ zone: zone.toUpperCase(),
136
+ date: date ?? new Date().toISOString().slice(0, 10),
137
+ actions,
138
+ summary: {
139
+ total_up_mw: totalUp,
140
+ total_down_mw: totalDown,
141
+ net_mw: totalUp - totalDown,
142
+ },
143
+ };
144
+ }
145
+ export async function getBalancingActions(params) {
146
+ const date = params.date ?? new Date().toISOString().slice(0, 10);
147
+ if (params.zone.toUpperCase() === "GB") {
148
+ return getGbBalancingActions(date);
149
+ }
150
+ return getEntsoeBalancingActions(params.zone, params.date);
151
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ export declare const balancingSchema: 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 getBalancingPrices(params: z.infer<typeof balancingSchema>): 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 balancingSchema = 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 getBalancingPrices(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: "A85",
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 balancing 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 carbonSchema: z.ZodObject<{
3
+ zone: z.ZodString;
4
+ date: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$strip>;
6
+ interface CarbonBreakdown {
7
+ fuel_type: string;
8
+ mw: number;
9
+ emission_factor_gco2_kwh: number;
10
+ contribution_gco2_kwh: number;
11
+ }
12
+ export declare function getCarbonIntensity(params: z.infer<typeof carbonSchema>): Promise<{
13
+ zone: string;
14
+ date: string;
15
+ carbon_intensity_gco2_kwh: number;
16
+ total_mw: number;
17
+ breakdown: CarbonBreakdown[];
18
+ renewable_pct: number;
19
+ fossil_pct: number;
20
+ }>;
21
+ export {};
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { getGenerationMix, generationSchema } from "./generation.js";
3
+ export const carbonSchema = z.object({
4
+ zone: generationSchema.shape.zone,
5
+ date: generationSchema.shape.date,
6
+ });
7
+ /** Emission factors in gCO2/kWh by PSR code */
8
+ const EMISSION_FACTORS = {
9
+ B01: 50, // Biomass
10
+ B02: 900, // Lignite
11
+ B03: 400, // Coal-derived gas
12
+ B04: 400, // Gas
13
+ B05: 900, // Hard coal
14
+ B06: 650, // Oil
15
+ B07: 650, // Oil shale
16
+ B08: 900, // Peat
17
+ B09: 0, // Geothermal
18
+ B10: 0, // Hydro Pumped Storage (assumed zero at point of generation)
19
+ B11: 0, // Hydro Run-of-river
20
+ B12: 0, // Hydro Reservoir
21
+ B13: 0, // Marine
22
+ B14: 0, // Nuclear
23
+ B15: 0, // Other renewable
24
+ B16: 0, // Solar
25
+ B17: 100, // Waste
26
+ B18: 0, // Wind Offshore
27
+ B19: 0, // Wind Onshore
28
+ B20: 300, // Other (conservative estimate)
29
+ };
30
+ export async function getCarbonIntensity(params) {
31
+ const genData = await getGenerationMix(params);
32
+ const breakdown = [];
33
+ let totalEmissions = 0;
34
+ for (const gen of genData.generation) {
35
+ const factor = EMISSION_FACTORS[gen.psr_code] ?? 300;
36
+ const contribution = (gen.mw / genData.total_mw) * factor;
37
+ totalEmissions += contribution;
38
+ breakdown.push({
39
+ fuel_type: gen.fuel_type,
40
+ mw: gen.mw,
41
+ emission_factor_gco2_kwh: factor,
42
+ contribution_gco2_kwh: Math.round(contribution * 100) / 100,
43
+ });
44
+ }
45
+ const renewableCodes = new Set([
46
+ "B01", "B09", "B10", "B11", "B12", "B13", "B15", "B16", "B18", "B19",
47
+ ]);
48
+ const fossilCodes = new Set(["B02", "B03", "B04", "B05", "B06", "B07", "B08"]);
49
+ const renewableMw = genData.generation
50
+ .filter((g) => renewableCodes.has(g.psr_code))
51
+ .reduce((s, g) => s + g.mw, 0);
52
+ const fossilMw = genData.generation
53
+ .filter((g) => fossilCodes.has(g.psr_code))
54
+ .reduce((s, g) => s + g.mw, 0);
55
+ return {
56
+ zone: genData.zone,
57
+ date: genData.date,
58
+ carbon_intensity_gco2_kwh: Math.round(totalEmissions * 100) / 100,
59
+ total_mw: genData.total_mw,
60
+ breakdown,
61
+ renewable_pct: genData.total_mw > 0
62
+ ? Math.round((renewableMw / genData.total_mw) * 10000) / 100
63
+ : 0,
64
+ fossil_pct: genData.total_mw > 0
65
+ ? Math.round((fossilMw / genData.total_mw) * 10000) / 100
66
+ : 0,
67
+ };
68
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ export declare const commodityPricesSchema: z.ZodObject<{
3
+ commodity: z.ZodEnum<{
4
+ ttf: "ttf";
5
+ carbon: "carbon";
6
+ brent: "brent";
7
+ all: "all";
8
+ }>;
9
+ }, z.core.$strip>;
10
+ interface CommodityEntry {
11
+ name: string;
12
+ ticker: string;
13
+ price: number;
14
+ currency: string;
15
+ unit: string;
16
+ change_5d_pct: number;
17
+ prices_5d: {
18
+ date: string;
19
+ price: number;
20
+ }[];
21
+ }
22
+ interface CommodityPricesResult {
23
+ commodities: CommodityEntry[];
24
+ }
25
+ export declare function getCommodityPrices(params: z.infer<typeof commodityPricesSchema>): Promise<CommodityPricesResult>;
26
+ export {};
@@ -0,0 +1,100 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ const cache = new TtlCache();
4
+ export const commodityPricesSchema = z.object({
5
+ commodity: z.enum(["carbon", "brent", "ttf", "all"]).describe('"carbon" = EU ETS carbon allowance (EUR/tCO2). "brent" = Brent crude (USD/bbl). "ttf" = Dutch TTF gas (EUR/MWh). "all" = all three.'),
6
+ });
7
+ const COMMODITIES = {
8
+ carbon: {
9
+ ticker: "CO2.L",
10
+ name: "EU Carbon (EUA)",
11
+ currency: "EUR",
12
+ unit: "EUR/tCO2",
13
+ },
14
+ brent: {
15
+ ticker: "BZ%3DF",
16
+ name: "Brent Crude",
17
+ currency: "USD",
18
+ unit: "USD/bbl",
19
+ },
20
+ ttf: {
21
+ ticker: "TTF%3DF",
22
+ name: "TTF Natural Gas",
23
+ currency: "EUR",
24
+ unit: "EUR/MWh",
25
+ },
26
+ };
27
+ async function fetchYahoo5d(ticker) {
28
+ const cacheKey = `commodity:${ticker}:5d`;
29
+ const cached = cache.get(cacheKey);
30
+ if (cached)
31
+ return cached;
32
+ try {
33
+ const url = `https://query1.finance.yahoo.com/v8/finance/chart/${ticker}?range=5d&interval=1d`;
34
+ const response = await fetch(url, {
35
+ headers: { "User-Agent": "Mozilla/5.0" },
36
+ });
37
+ if (!response.ok)
38
+ return null;
39
+ const json = await response.json();
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ const result = json?.chart?.result?.[0];
42
+ if (!result)
43
+ return null;
44
+ const timestamps = result.timestamp ?? [];
45
+ const closes = result.indicators?.quote?.[0]?.close ?? [];
46
+ const valid = { dates: [], closes: [] };
47
+ for (let i = 0; i < timestamps.length; i++) {
48
+ if (closes[i] != null && Number.isFinite(closes[i])) {
49
+ valid.dates.push(new Date(timestamps[i] * 1000).toISOString().slice(0, 10));
50
+ valid.closes.push(closes[i]);
51
+ }
52
+ }
53
+ if (valid.dates.length > 0) {
54
+ cache.set(cacheKey, valid, TTL.STORAGE);
55
+ }
56
+ return valid.dates.length > 0 ? valid : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function buildEntry(key, data) {
63
+ const config = COMMODITIES[key];
64
+ const latestPrice = data.closes[data.closes.length - 1];
65
+ const firstPrice = data.closes[0];
66
+ const change5dPct = firstPrice > 0
67
+ ? Math.round(((latestPrice - firstPrice) / firstPrice) * 10000) / 100
68
+ : 0;
69
+ const prices5d = data.dates.map((date, i) => ({
70
+ date,
71
+ price: Math.round(data.closes[i] * 100) / 100,
72
+ }));
73
+ return {
74
+ name: config.name,
75
+ ticker: config.ticker,
76
+ price: Math.round(latestPrice * 100) / 100,
77
+ currency: config.currency,
78
+ unit: config.unit,
79
+ change_5d_pct: change5dPct,
80
+ prices_5d: prices5d,
81
+ };
82
+ }
83
+ export async function getCommodityPrices(params) {
84
+ const keys = params.commodity === "all"
85
+ ? Object.keys(COMMODITIES)
86
+ : [params.commodity];
87
+ const results = await Promise.all(keys.map(async (key) => {
88
+ const config = COMMODITIES[key];
89
+ const data = await fetchYahoo5d(config.ticker);
90
+ if (!data || data.closes.length === 0) {
91
+ return null;
92
+ }
93
+ return buildEntry(key, data);
94
+ }));
95
+ const commodities = results.filter((r) => r !== null);
96
+ if (commodities.length === 0) {
97
+ throw new Error("Unable to fetch commodity prices. Yahoo Finance may be temporarily unavailable.");
98
+ }
99
+ return { commodities };
100
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ export declare const compareSitesSchema: z.ZodObject<{
3
+ sites: z.ZodArray<z.ZodObject<{
4
+ lat: z.ZodNumber;
5
+ lon: z.ZodNumber;
6
+ label: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>>;
8
+ country: z.ZodString;
9
+ radius_km: z.ZodOptional<z.ZodNumber>;
10
+ }, z.core.$strip>;
11
+ type Verdict = "pass" | "warn" | "fail";
12
+ interface RankedSite {
13
+ rank: number;
14
+ label: string;
15
+ lat: number;
16
+ lon: number;
17
+ verdict: Verdict;
18
+ flag_count: number;
19
+ solar_kwh_m2: number | null;
20
+ slope_deg: number | null;
21
+ nearest_grid_km: number | null;
22
+ constraint_count: number;
23
+ score: number;
24
+ reasoning: string;
25
+ data_gaps: string[];
26
+ }
27
+ interface FailedSite {
28
+ lat: number;
29
+ lon: number;
30
+ label: string;
31
+ error: string;
32
+ }
33
+ interface CompareSitesResult {
34
+ site_count: number;
35
+ rankings: RankedSite[];
36
+ failed_sites: FailedSite[];
37
+ heuristics_used: string[];
38
+ disclaimer: string;
39
+ }
40
+ export declare function compareSites(params: z.infer<typeof compareSitesSchema>): Promise<CompareSitesResult>;
41
+ export {};