@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,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const entsogSchema: z.ZodObject<{
|
|
3
|
+
dataset: z.ZodEnum<{
|
|
4
|
+
capacities: "capacities";
|
|
5
|
+
physical_flows: "physical_flows";
|
|
6
|
+
nominations: "nominations";
|
|
7
|
+
interruptions: "interruptions";
|
|
8
|
+
}>;
|
|
9
|
+
country: z.ZodOptional<z.ZodString>;
|
|
10
|
+
date: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
interface FlowRecord {
|
|
13
|
+
point_label: string;
|
|
14
|
+
operator: string;
|
|
15
|
+
direction: string;
|
|
16
|
+
flow_gwh: number;
|
|
17
|
+
date: string;
|
|
18
|
+
}
|
|
19
|
+
interface FlowsResult {
|
|
20
|
+
dataset: "physical_flows";
|
|
21
|
+
source: string;
|
|
22
|
+
date: string;
|
|
23
|
+
country: string | null;
|
|
24
|
+
records: FlowRecord[];
|
|
25
|
+
}
|
|
26
|
+
interface NominationRecord {
|
|
27
|
+
point_label: string;
|
|
28
|
+
operator: string;
|
|
29
|
+
direction: string;
|
|
30
|
+
nominated_gwh: number;
|
|
31
|
+
date: string;
|
|
32
|
+
}
|
|
33
|
+
interface NominationsResult {
|
|
34
|
+
dataset: "nominations";
|
|
35
|
+
source: string;
|
|
36
|
+
date: string;
|
|
37
|
+
country: string | null;
|
|
38
|
+
records: NominationRecord[];
|
|
39
|
+
}
|
|
40
|
+
interface InterruptionRecord {
|
|
41
|
+
point_label: string;
|
|
42
|
+
operator: string;
|
|
43
|
+
type: string;
|
|
44
|
+
start: string;
|
|
45
|
+
end: string;
|
|
46
|
+
affected_capacity_gwh: number;
|
|
47
|
+
}
|
|
48
|
+
interface InterruptionsResult {
|
|
49
|
+
dataset: "interruptions";
|
|
50
|
+
source: string;
|
|
51
|
+
country: string | null;
|
|
52
|
+
records: InterruptionRecord[];
|
|
53
|
+
}
|
|
54
|
+
interface CapacityRecord {
|
|
55
|
+
point_label: string;
|
|
56
|
+
operator: string;
|
|
57
|
+
direction: string;
|
|
58
|
+
technical_gwh: number;
|
|
59
|
+
booked_gwh: number;
|
|
60
|
+
available_gwh: number;
|
|
61
|
+
}
|
|
62
|
+
interface CapacitiesResult {
|
|
63
|
+
dataset: "capacities";
|
|
64
|
+
source: string;
|
|
65
|
+
date: string;
|
|
66
|
+
country: string | null;
|
|
67
|
+
records: CapacityRecord[];
|
|
68
|
+
}
|
|
69
|
+
type EntsogResult = FlowsResult | NominationsResult | InterruptionsResult | CapacitiesResult;
|
|
70
|
+
export declare function getEntsogData(params: z.infer<typeof entsogSchema>): Promise<EntsogResult>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const API_BASE = "https://transparency.entsog.eu/api/v1";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
export const entsogSchema = z.object({
|
|
6
|
+
dataset: z
|
|
7
|
+
.enum(["physical_flows", "nominations", "interruptions", "capacities"])
|
|
8
|
+
.describe('"physical_flows" = actual gas pipeline flows (GWh/d) between points. ' +
|
|
9
|
+
'"nominations" = day-ahead nominated gas volumes. ' +
|
|
10
|
+
'"interruptions" = pipeline capacity interruptions and maintenance. ' +
|
|
11
|
+
'"capacities" = technical/booked/available pipeline capacities.'),
|
|
12
|
+
country: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("ISO-2 country code to filter by operator country (e.g. DE, NL, AT, FR, IT, PL). Optional."),
|
|
16
|
+
date: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Date in YYYY-MM-DD format. Defaults to yesterday."),
|
|
20
|
+
});
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
async function fetchEntsog(endpoint, params) {
|
|
23
|
+
const query = new URLSearchParams({ ...params, limit: "100" });
|
|
24
|
+
const url = `${API_BASE}/${endpoint}?${query}`;
|
|
25
|
+
const cached = cache.get(url);
|
|
26
|
+
if (cached)
|
|
27
|
+
return cached;
|
|
28
|
+
const response = await fetch(url, {
|
|
29
|
+
headers: { Accept: "application/json" },
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const body = await response.text();
|
|
33
|
+
throw new Error(`ENTSOG API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
34
|
+
}
|
|
35
|
+
const json = await response.json();
|
|
36
|
+
cache.set(url, json, TTL.FLOWS);
|
|
37
|
+
return json;
|
|
38
|
+
}
|
|
39
|
+
function yesterday() {
|
|
40
|
+
const d = new Date();
|
|
41
|
+
d.setDate(d.getDate() - 1);
|
|
42
|
+
return d.toISOString().slice(0, 10);
|
|
43
|
+
}
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
function parseFlows(data, date, country) {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
const points = Array.isArray(data?.physicalflows) ? data.physicalflows : [];
|
|
48
|
+
const records = points.slice(0, 50).map((p) => ({
|
|
49
|
+
point_label: p.pointLabel ?? p.pointKey ?? "Unknown",
|
|
50
|
+
operator: p.operatorLabel ?? p.operatorKey ?? "Unknown",
|
|
51
|
+
direction: p.directionKey ?? "unknown",
|
|
52
|
+
flow_gwh: Math.round((Number(p.value ?? 0)) * 100) / 100,
|
|
53
|
+
date: p.periodFrom ?? date,
|
|
54
|
+
}));
|
|
55
|
+
return {
|
|
56
|
+
dataset: "physical_flows",
|
|
57
|
+
source: "ENTSOG Transparency Platform",
|
|
58
|
+
date,
|
|
59
|
+
country,
|
|
60
|
+
records,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
function parseNominations(data, date, country) {
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
const points = Array.isArray(data?.nominations) ? data.nominations : [];
|
|
67
|
+
const records = points.slice(0, 50).map((p) => ({
|
|
68
|
+
point_label: p.pointLabel ?? p.pointKey ?? "Unknown",
|
|
69
|
+
operator: p.operatorLabel ?? p.operatorKey ?? "Unknown",
|
|
70
|
+
direction: p.directionKey ?? "unknown",
|
|
71
|
+
nominated_gwh: Math.round((Number(p.value ?? 0)) * 100) / 100,
|
|
72
|
+
date: p.periodFrom ?? date,
|
|
73
|
+
}));
|
|
74
|
+
return {
|
|
75
|
+
dataset: "nominations",
|
|
76
|
+
source: "ENTSOG Transparency Platform",
|
|
77
|
+
date,
|
|
78
|
+
country,
|
|
79
|
+
records,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
function parseInterruptions(data, country) {
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
const items = Array.isArray(data?.interruptions) ? data.interruptions : [];
|
|
86
|
+
const records = items.slice(0, 50).map((i) => ({
|
|
87
|
+
point_label: i.pointLabel ?? i.pointKey ?? "Unknown",
|
|
88
|
+
operator: i.operatorLabel ?? i.operatorKey ?? "Unknown",
|
|
89
|
+
type: i.type ?? "unknown",
|
|
90
|
+
start: i.periodFrom ?? "",
|
|
91
|
+
end: i.periodTo ?? "",
|
|
92
|
+
affected_capacity_gwh: Math.round((Number(i.value ?? 0)) * 100) / 100,
|
|
93
|
+
}));
|
|
94
|
+
return {
|
|
95
|
+
dataset: "interruptions",
|
|
96
|
+
source: "ENTSOG Transparency Platform",
|
|
97
|
+
country,
|
|
98
|
+
records,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
function parseCapacities(data, date, country) {
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
const items = Array.isArray(data?.operationaldatas) ? data.operationaldatas : [];
|
|
105
|
+
const records = items.slice(0, 50).map((c) => ({
|
|
106
|
+
point_label: c.pointLabel ?? c.pointKey ?? "Unknown",
|
|
107
|
+
operator: c.operatorLabel ?? c.operatorKey ?? "Unknown",
|
|
108
|
+
direction: c.directionKey ?? "unknown",
|
|
109
|
+
technical_gwh: Math.round((Number(c.technicalCapacity ?? 0)) * 100) / 100,
|
|
110
|
+
booked_gwh: Math.round((Number(c.bookedCapacity ?? 0)) * 100) / 100,
|
|
111
|
+
available_gwh: Math.round((Number(c.availableCapacity ?? 0)) * 100) / 100,
|
|
112
|
+
}));
|
|
113
|
+
return {
|
|
114
|
+
dataset: "capacities",
|
|
115
|
+
source: "ENTSOG Transparency Platform",
|
|
116
|
+
date,
|
|
117
|
+
country,
|
|
118
|
+
records,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export async function getEntsogData(params) {
|
|
122
|
+
const date = params.date ?? yesterday();
|
|
123
|
+
const country = params.country?.toUpperCase() ?? null;
|
|
124
|
+
const baseParams = {
|
|
125
|
+
from: date,
|
|
126
|
+
to: date,
|
|
127
|
+
periodType: "day",
|
|
128
|
+
indicator: "Physical Flow",
|
|
129
|
+
};
|
|
130
|
+
if (country) {
|
|
131
|
+
baseParams.operatorCountry = country;
|
|
132
|
+
}
|
|
133
|
+
switch (params.dataset) {
|
|
134
|
+
case "physical_flows": {
|
|
135
|
+
baseParams.indicator = "Physical Flow";
|
|
136
|
+
const data = await fetchEntsog("operationaldatas", baseParams);
|
|
137
|
+
return parseFlows(data, date, country);
|
|
138
|
+
}
|
|
139
|
+
case "nominations": {
|
|
140
|
+
baseParams.indicator = "Nomination";
|
|
141
|
+
const data = await fetchEntsog("nominationdatas", baseParams);
|
|
142
|
+
return parseNominations(data, date, country);
|
|
143
|
+
}
|
|
144
|
+
case "interruptions": {
|
|
145
|
+
const data = await fetchEntsog("interruptions", {
|
|
146
|
+
from: date,
|
|
147
|
+
to: date,
|
|
148
|
+
...(country ? { operatorCountry: country } : {}),
|
|
149
|
+
limit: "50",
|
|
150
|
+
});
|
|
151
|
+
return parseInterruptions(data, country);
|
|
152
|
+
}
|
|
153
|
+
case "capacities": {
|
|
154
|
+
baseParams.indicator = "Physical Flow";
|
|
155
|
+
const data = await fetchEntsog("operationaldatas", baseParams);
|
|
156
|
+
return parseCapacities(data, date, country);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const era5WeatherSchema: z.ZodObject<{
|
|
3
|
+
latitude: z.ZodNumber;
|
|
4
|
+
longitude: z.ZodNumber;
|
|
5
|
+
start_date: z.ZodString;
|
|
6
|
+
end_date: z.ZodString;
|
|
7
|
+
variables: z.ZodOptional<z.ZodEnum<{
|
|
8
|
+
all: "all";
|
|
9
|
+
wind: "wind";
|
|
10
|
+
solar: "solar";
|
|
11
|
+
temperature: "temperature";
|
|
12
|
+
}>>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
interface HourlyEntry {
|
|
15
|
+
timestamp: string;
|
|
16
|
+
[key: string]: string | number | null;
|
|
17
|
+
}
|
|
18
|
+
interface Era5Result {
|
|
19
|
+
source: string;
|
|
20
|
+
latitude: number;
|
|
21
|
+
longitude: number;
|
|
22
|
+
start_date: string;
|
|
23
|
+
end_date: string;
|
|
24
|
+
variables: string[];
|
|
25
|
+
hourly_count: number;
|
|
26
|
+
hourly: HourlyEntry[];
|
|
27
|
+
daily_summary: DailySummary[];
|
|
28
|
+
}
|
|
29
|
+
interface DailySummary {
|
|
30
|
+
date: string;
|
|
31
|
+
temp_mean_c: number | null;
|
|
32
|
+
temp_min_c: number | null;
|
|
33
|
+
temp_max_c: number | null;
|
|
34
|
+
wind_10m_mean_ms: number | null;
|
|
35
|
+
wind_100m_mean_ms: number | null;
|
|
36
|
+
ghi_wh_m2: number | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function getEra5Weather(params: z.infer<typeof era5WeatherSchema>): Promise<Era5Result>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const API_BASE = "https://archive-api.open-meteo.com/v1/archive";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
export const era5WeatherSchema = z.object({
|
|
6
|
+
latitude: z.number().describe("Latitude (-90 to 90)."),
|
|
7
|
+
longitude: z.number().describe("Longitude (-180 to 180)."),
|
|
8
|
+
start_date: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("Start date YYYY-MM-DD. ERA5 data available from 1940."),
|
|
11
|
+
end_date: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("End date YYYY-MM-DD. Usually available up to 5 days ago."),
|
|
14
|
+
variables: z
|
|
15
|
+
.enum(["wind", "solar", "temperature", "all"])
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('"wind" = wind speed at 10m and 100m hub height. ' +
|
|
18
|
+
'"solar" = GHI, DNI, diffuse radiation. ' +
|
|
19
|
+
'"temperature" = 2m temperature, dewpoint, soil temperature. ' +
|
|
20
|
+
'"all" = all variables (default).'),
|
|
21
|
+
});
|
|
22
|
+
function getHourlyParams(variables) {
|
|
23
|
+
const wind = "windspeed_10m,windspeed_100m,winddirection_10m,winddirection_100m";
|
|
24
|
+
const solar = "shortwave_radiation,direct_radiation,diffuse_radiation,direct_normal_irradiance";
|
|
25
|
+
const temp = "temperature_2m,dewpoint_2m,soil_temperature_0cm";
|
|
26
|
+
switch (variables) {
|
|
27
|
+
case "wind":
|
|
28
|
+
return wind;
|
|
29
|
+
case "solar":
|
|
30
|
+
return solar;
|
|
31
|
+
case "temperature":
|
|
32
|
+
return temp;
|
|
33
|
+
default:
|
|
34
|
+
return `${temp},${wind},${solar}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function getEra5Weather(params) {
|
|
38
|
+
const vars = params.variables ?? "all";
|
|
39
|
+
const hourlyParams = getHourlyParams(vars);
|
|
40
|
+
const url = `${API_BASE}?latitude=${params.latitude}&longitude=${params.longitude}` +
|
|
41
|
+
`&start_date=${params.start_date}&end_date=${params.end_date}` +
|
|
42
|
+
`&hourly=${hourlyParams}&timezone=UTC`;
|
|
43
|
+
const cached = cache.get(url);
|
|
44
|
+
if (cached)
|
|
45
|
+
return cached;
|
|
46
|
+
const response = await fetch(url);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const body = await response.text();
|
|
49
|
+
throw new Error(`Open-Meteo Archive API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
const json = await response.json();
|
|
53
|
+
const hourlyData = json.hourly ?? {};
|
|
54
|
+
const times = hourlyData.time ?? [];
|
|
55
|
+
// Build hourly entries (cap at 168 = 7 days of hourly)
|
|
56
|
+
const maxEntries = Math.min(times.length, 168);
|
|
57
|
+
const hourly = [];
|
|
58
|
+
for (let i = 0; i < maxEntries; i++) {
|
|
59
|
+
const entry = { timestamp: times[i] };
|
|
60
|
+
for (const [key, values] of Object.entries(hourlyData)) {
|
|
61
|
+
if (key === "time")
|
|
62
|
+
continue;
|
|
63
|
+
const arr = values;
|
|
64
|
+
entry[key] = arr[i] ?? null;
|
|
65
|
+
}
|
|
66
|
+
hourly.push(entry);
|
|
67
|
+
}
|
|
68
|
+
// Build daily summaries
|
|
69
|
+
const dailyMap = new Map();
|
|
70
|
+
for (let i = 0; i < times.length; i++) {
|
|
71
|
+
const date = times[i].slice(0, 10);
|
|
72
|
+
if (!dailyMap.has(date)) {
|
|
73
|
+
dailyMap.set(date, { temps: [], wind10: [], wind100: [], ghi: [] });
|
|
74
|
+
}
|
|
75
|
+
const bucket = dailyMap.get(date);
|
|
76
|
+
const temp = hourlyData.temperature_2m?.[i];
|
|
77
|
+
if (temp != null)
|
|
78
|
+
bucket.temps.push(temp);
|
|
79
|
+
const w10 = hourlyData.windspeed_10m?.[i];
|
|
80
|
+
if (w10 != null)
|
|
81
|
+
bucket.wind10.push(w10);
|
|
82
|
+
const w100 = hourlyData.windspeed_100m?.[i];
|
|
83
|
+
if (w100 != null)
|
|
84
|
+
bucket.wind100.push(w100);
|
|
85
|
+
const ghi = hourlyData.shortwave_radiation?.[i];
|
|
86
|
+
if (ghi != null)
|
|
87
|
+
bucket.ghi.push(ghi);
|
|
88
|
+
}
|
|
89
|
+
const dailySummary = [];
|
|
90
|
+
for (const [date, bucket] of dailyMap.entries()) {
|
|
91
|
+
const avg = (arr) => arr.length > 0 ? Math.round((arr.reduce((s, v) => s + v, 0) / arr.length) * 10) / 10 : null;
|
|
92
|
+
const sum = (arr) => arr.length > 0 ? Math.round(arr.reduce((s, v) => s + v, 0)) : null;
|
|
93
|
+
dailySummary.push({
|
|
94
|
+
date,
|
|
95
|
+
temp_mean_c: avg(bucket.temps),
|
|
96
|
+
temp_min_c: bucket.temps.length > 0 ? Math.round(Math.min(...bucket.temps) * 10) / 10 : null,
|
|
97
|
+
temp_max_c: bucket.temps.length > 0 ? Math.round(Math.max(...bucket.temps) * 10) / 10 : null,
|
|
98
|
+
wind_10m_mean_ms: avg(bucket.wind10),
|
|
99
|
+
wind_100m_mean_ms: avg(bucket.wind100),
|
|
100
|
+
ghi_wh_m2: sum(bucket.ghi),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const variableNames = Object.keys(hourlyData).filter((k) => k !== "time");
|
|
104
|
+
const result = {
|
|
105
|
+
source: "Open-Meteo Archive (ERA5 reanalysis, Copernicus/ECMWF)",
|
|
106
|
+
latitude: json.latitude ?? params.latitude,
|
|
107
|
+
longitude: json.longitude ?? params.longitude,
|
|
108
|
+
start_date: params.start_date,
|
|
109
|
+
end_date: params.end_date,
|
|
110
|
+
variables: variableNames,
|
|
111
|
+
hourly_count: times.length,
|
|
112
|
+
hourly,
|
|
113
|
+
daily_summary: dailySummary,
|
|
114
|
+
};
|
|
115
|
+
cache.set(url, result, TTL.WEATHER);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Fetches European natural gas prices from multiple free sources.
|
|
4
|
+
* Primary: Yahoo Finance TTF futures (no API key needed).
|
|
5
|
+
* Fallback: Derives from GIE AGSI+ storage economics if available.
|
|
6
|
+
*/
|
|
7
|
+
export declare const euGasPriceSchema: z.ZodObject<{
|
|
8
|
+
hub: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
ttf: "ttf";
|
|
10
|
+
nbp: "nbp";
|
|
11
|
+
}>>;
|
|
12
|
+
period: z.ZodOptional<z.ZodEnum<{
|
|
13
|
+
spot: "spot";
|
|
14
|
+
week: "week";
|
|
15
|
+
}>>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
interface GasPricePoint {
|
|
18
|
+
date: string;
|
|
19
|
+
price: number;
|
|
20
|
+
currency: string;
|
|
21
|
+
unit: string;
|
|
22
|
+
}
|
|
23
|
+
interface EuGasPriceResult {
|
|
24
|
+
hub: string;
|
|
25
|
+
hub_name: string;
|
|
26
|
+
currency: string;
|
|
27
|
+
unit: string;
|
|
28
|
+
latest: GasPricePoint;
|
|
29
|
+
prices: GasPricePoint[];
|
|
30
|
+
price_eur_mwh: number;
|
|
31
|
+
stats: {
|
|
32
|
+
min: number;
|
|
33
|
+
max: number;
|
|
34
|
+
mean: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export declare function getEuGasPrice(params: z.infer<typeof euGasPriceSchema>): Promise<EuGasPriceResult>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const cache = new TtlCache();
|
|
4
|
+
/**
|
|
5
|
+
* Fetches European natural gas prices from multiple free sources.
|
|
6
|
+
* Primary: Yahoo Finance TTF futures (no API key needed).
|
|
7
|
+
* Fallback: Derives from GIE AGSI+ storage economics if available.
|
|
8
|
+
*/
|
|
9
|
+
export const euGasPriceSchema = z.object({
|
|
10
|
+
hub: z
|
|
11
|
+
.enum(["ttf", "nbp"])
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('Gas trading hub. "ttf" = Dutch TTF (European benchmark, EUR/MWh). ' +
|
|
14
|
+
'"nbp" = UK NBP (GBp/therm). Defaults to ttf.'),
|
|
15
|
+
period: z
|
|
16
|
+
.enum(["spot", "week"])
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('"spot" = latest available price. "week" = last 7 daily closes. Defaults to spot.'),
|
|
19
|
+
});
|
|
20
|
+
// Yahoo Finance tickers for European gas hubs
|
|
21
|
+
const TICKERS = {
|
|
22
|
+
ttf: {
|
|
23
|
+
ticker: "TTF%3DF",
|
|
24
|
+
name: "Dutch TTF Natural Gas Futures",
|
|
25
|
+
currency: "EUR",
|
|
26
|
+
unit: "EUR/MWh",
|
|
27
|
+
toEurMwh: 1, // TTF is already quoted in EUR/MWh
|
|
28
|
+
},
|
|
29
|
+
nbp: {
|
|
30
|
+
ticker: "NG%3DF",
|
|
31
|
+
name: "UK NBP Natural Gas (Henry Hub proxy)",
|
|
32
|
+
currency: "USD",
|
|
33
|
+
unit: "USD/MMBtu",
|
|
34
|
+
toEurMwh: 3.41, // Approximate USD/MMBtu to EUR/MWh conversion
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
async function fetchYahooQuote(ticker, range) {
|
|
38
|
+
const cacheKey = `yahoo:${ticker}:${range}`;
|
|
39
|
+
const cached = cache.get(cacheKey);
|
|
40
|
+
if (cached)
|
|
41
|
+
return cached;
|
|
42
|
+
try {
|
|
43
|
+
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${ticker}?range=${range}&interval=1d`;
|
|
44
|
+
const response = await fetch(url, {
|
|
45
|
+
headers: {
|
|
46
|
+
"User-Agent": "Mozilla/5.0",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok)
|
|
50
|
+
return null;
|
|
51
|
+
const json = await response.json();
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
const result = json?.chart?.result?.[0];
|
|
54
|
+
if (!result)
|
|
55
|
+
return null;
|
|
56
|
+
const timestamps = result.timestamp ?? [];
|
|
57
|
+
const closes = result.indicators?.quote?.[0]?.close ?? [];
|
|
58
|
+
const dates = timestamps.map((ts) => new Date(ts * 1000).toISOString().slice(0, 10));
|
|
59
|
+
// Filter out null/NaN closes
|
|
60
|
+
const valid = { dates: [], closes: [] };
|
|
61
|
+
for (let i = 0; i < dates.length; i++) {
|
|
62
|
+
if (closes[i] != null && Number.isFinite(closes[i])) {
|
|
63
|
+
valid.dates.push(dates[i]);
|
|
64
|
+
valid.closes.push(closes[i]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (valid.dates.length > 0) {
|
|
68
|
+
cache.set(cacheKey, valid, TTL.STORAGE);
|
|
69
|
+
}
|
|
70
|
+
return valid.dates.length > 0 ? valid : null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function getEuGasPrice(params) {
|
|
77
|
+
const hub = params.hub ?? "ttf";
|
|
78
|
+
const period = params.period ?? "spot";
|
|
79
|
+
const config = TICKERS[hub];
|
|
80
|
+
if (!config) {
|
|
81
|
+
throw new Error(`Unknown gas hub "${hub}". Available: ttf, nbp`);
|
|
82
|
+
}
|
|
83
|
+
const range = period === "week" ? "7d" : "5d";
|
|
84
|
+
const data = await fetchYahooQuote(config.ticker, range);
|
|
85
|
+
if (!data || data.closes.length === 0) {
|
|
86
|
+
throw new Error(`Unable to fetch ${config.name} price. Yahoo Finance may be temporarily unavailable.`);
|
|
87
|
+
}
|
|
88
|
+
const prices = data.dates.map((date, i) => ({
|
|
89
|
+
date,
|
|
90
|
+
price: Math.round(data.closes[i] * 100) / 100,
|
|
91
|
+
currency: config.currency,
|
|
92
|
+
unit: config.unit,
|
|
93
|
+
}));
|
|
94
|
+
const latestPrice = data.closes[data.closes.length - 1];
|
|
95
|
+
const priceEurMwh = Math.round(latestPrice * config.toEurMwh * 100) / 100;
|
|
96
|
+
const values = data.closes;
|
|
97
|
+
const min = Math.round(Math.min(...values) * 100) / 100;
|
|
98
|
+
const max = Math.round(Math.max(...values) * 100) / 100;
|
|
99
|
+
const mean = Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 100) / 100;
|
|
100
|
+
return {
|
|
101
|
+
hub,
|
|
102
|
+
hub_name: config.name,
|
|
103
|
+
currency: config.currency,
|
|
104
|
+
unit: config.unit,
|
|
105
|
+
latest: prices[prices.length - 1],
|
|
106
|
+
prices: period === "week" ? prices : [prices[prices.length - 1]],
|
|
107
|
+
price_eur_mwh: priceEurMwh,
|
|
108
|
+
stats: { min, max, mean },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const fingridSchema: z.ZodObject<{
|
|
3
|
+
dataset: z.ZodEnum<{
|
|
4
|
+
frequency: "frequency";
|
|
5
|
+
consumption: "consumption";
|
|
6
|
+
production: "production";
|
|
7
|
+
wind_production: "wind_production";
|
|
8
|
+
solar_production: "solar_production";
|
|
9
|
+
nuclear_production: "nuclear_production";
|
|
10
|
+
hydro_production: "hydro_production";
|
|
11
|
+
imports: "imports";
|
|
12
|
+
exports: "exports";
|
|
13
|
+
reserve_prices: "reserve_prices";
|
|
14
|
+
}>;
|
|
15
|
+
start_date: z.ZodOptional<z.ZodString>;
|
|
16
|
+
end_date: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
interface DataPoint {
|
|
19
|
+
timestamp: string;
|
|
20
|
+
value: number;
|
|
21
|
+
}
|
|
22
|
+
interface FingridResult {
|
|
23
|
+
source: string;
|
|
24
|
+
dataset: string;
|
|
25
|
+
dataset_id: number;
|
|
26
|
+
unit: string;
|
|
27
|
+
start_date: string;
|
|
28
|
+
end_date: string;
|
|
29
|
+
count: number;
|
|
30
|
+
latest: DataPoint | null;
|
|
31
|
+
data: DataPoint[];
|
|
32
|
+
stats: {
|
|
33
|
+
min: number;
|
|
34
|
+
max: number;
|
|
35
|
+
mean: number;
|
|
36
|
+
} | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function getFingridData(params: z.infer<typeof fingridSchema>): Promise<FingridResult>;
|
|
39
|
+
export {};
|