@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,109 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const CARBON_API = "https://api.carbonintensity.org.uk";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
export const ukCarbonSchema = z.object({
|
|
6
|
+
action: z
|
|
7
|
+
.enum(["current", "regional", "date"])
|
|
8
|
+
.describe('"current" = national carbon intensity + generation mix. ' +
|
|
9
|
+
'"regional" = carbon intensity by GB region. ' +
|
|
10
|
+
'"date" = historical carbon intensity for a specific date (YYYY-MM-DD).'),
|
|
11
|
+
date: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Date in YYYY-MM-DD format. Required for action "date".'),
|
|
15
|
+
});
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
async function fetchCarbon(path) {
|
|
18
|
+
const url = `${CARBON_API}${path}`;
|
|
19
|
+
const cached = cache.get(url);
|
|
20
|
+
if (cached)
|
|
21
|
+
return cached;
|
|
22
|
+
const response = await fetch(url);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const body = await response.text();
|
|
25
|
+
throw new Error(`Carbon Intensity API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
26
|
+
}
|
|
27
|
+
const json = await response.json();
|
|
28
|
+
cache.set(url, json, TTL.REALTIME);
|
|
29
|
+
return json;
|
|
30
|
+
}
|
|
31
|
+
async function getCurrent() {
|
|
32
|
+
const [intensityData, genData] = await Promise.all([
|
|
33
|
+
fetchCarbon("/intensity"),
|
|
34
|
+
fetchCarbon("/generation"),
|
|
35
|
+
]);
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
const intensity = intensityData?.data?.[0] ?? {};
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const genMix = genData?.data?.generationmix ?? [];
|
|
40
|
+
return {
|
|
41
|
+
action: "current",
|
|
42
|
+
timestamp: intensity.from ?? new Date().toISOString(),
|
|
43
|
+
intensity_gco2_kwh: Number(intensity.intensity?.actual ?? intensity.intensity?.forecast ?? 0),
|
|
44
|
+
index: intensity.intensity?.index ?? "unknown",
|
|
45
|
+
generation_mix: genMix.map((g) => ({
|
|
46
|
+
fuel: g.fuel ?? "",
|
|
47
|
+
perc: Number(g.perc ?? 0),
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function getRegional() {
|
|
52
|
+
const data = await fetchCarbon("/regional");
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
const regions = data?.data?.[0]?.regions ?? [];
|
|
55
|
+
return {
|
|
56
|
+
action: "regional",
|
|
57
|
+
timestamp: data?.data?.[0]?.from ?? new Date().toISOString(),
|
|
58
|
+
regions: regions.map((r) => ({
|
|
59
|
+
region: r.shortname ?? r.dnoregion ?? "unknown",
|
|
60
|
+
intensity_gco2_kwh: Number(r.intensity?.forecast ?? 0),
|
|
61
|
+
index: r.intensity?.index ?? "unknown",
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
generation_mix: (r.generationmix ?? []).map((g) => ({
|
|
64
|
+
fuel: g.fuel ?? "",
|
|
65
|
+
perc: Number(g.perc ?? 0),
|
|
66
|
+
})),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function getByDate(date) {
|
|
71
|
+
const data = await fetchCarbon(`/intensity/date/${date}`);
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
const periods = data?.data ?? [];
|
|
74
|
+
const entries = periods.map((p) => ({
|
|
75
|
+
from: p.from ?? "",
|
|
76
|
+
to: p.to ?? "",
|
|
77
|
+
intensity_forecast: Number(p.intensity?.forecast ?? 0),
|
|
78
|
+
intensity_actual: p.intensity?.actual != null ? Number(p.intensity.actual) : null,
|
|
79
|
+
index: p.intensity?.index ?? "unknown",
|
|
80
|
+
}));
|
|
81
|
+
const forecasts = entries.map((e) => e.intensity_forecast).filter((v) => v > 0);
|
|
82
|
+
const mean = forecasts.length > 0
|
|
83
|
+
? Math.round((forecasts.reduce((s, v) => s + v, 0) / forecasts.length) * 10) / 10
|
|
84
|
+
: 0;
|
|
85
|
+
return {
|
|
86
|
+
action: "date",
|
|
87
|
+
date,
|
|
88
|
+
periods: entries,
|
|
89
|
+
stats: {
|
|
90
|
+
mean_forecast: mean,
|
|
91
|
+
min_forecast: forecasts.length > 0 ? Math.min(...forecasts) : 0,
|
|
92
|
+
max_forecast: forecasts.length > 0 ? Math.max(...forecasts) : 0,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function getUkCarbonIntensity(params) {
|
|
97
|
+
switch (params.action) {
|
|
98
|
+
case "current":
|
|
99
|
+
return getCurrent();
|
|
100
|
+
case "regional":
|
|
101
|
+
return getRegional();
|
|
102
|
+
case "date": {
|
|
103
|
+
if (!params.date) {
|
|
104
|
+
throw new Error('Date parameter is required for action "date".');
|
|
105
|
+
}
|
|
106
|
+
return getByDate(params.date);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ukGridSchema: z.ZodObject<{
|
|
3
|
+
action: z.ZodEnum<{
|
|
4
|
+
frequency: "frequency";
|
|
5
|
+
demand: "demand";
|
|
6
|
+
}>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
interface DemandRecord {
|
|
9
|
+
timestamp: string;
|
|
10
|
+
settlement_period: number;
|
|
11
|
+
demand_mw: number;
|
|
12
|
+
transmission_demand_mw: number;
|
|
13
|
+
}
|
|
14
|
+
interface DemandResult {
|
|
15
|
+
action: "demand";
|
|
16
|
+
description: string;
|
|
17
|
+
records: DemandRecord[];
|
|
18
|
+
}
|
|
19
|
+
interface FrequencyResult {
|
|
20
|
+
action: "frequency";
|
|
21
|
+
description: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
frequency_hz: number;
|
|
24
|
+
deviation_hz: number;
|
|
25
|
+
}
|
|
26
|
+
type UkGridResult = DemandResult | FrequencyResult;
|
|
27
|
+
export declare function getUkGridDemand(params: z.infer<typeof ukGridSchema>): Promise<UkGridResult>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const ELEXON_API = "https://data.elexon.co.uk/bmrs/api/v1";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
export const ukGridSchema = z.object({
|
|
6
|
+
action: z
|
|
7
|
+
.enum(["demand", "frequency"])
|
|
8
|
+
.describe('"demand" = current GB demand actuals (MW) per settlement period. ' +
|
|
9
|
+
'"frequency" = real-time grid frequency (~50 Hz; deviations indicate stress).'),
|
|
10
|
+
});
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
async function fetchElexon(path) {
|
|
13
|
+
const url = `${ELEXON_API}${path}`;
|
|
14
|
+
const cached = cache.get(url);
|
|
15
|
+
if (cached)
|
|
16
|
+
return cached;
|
|
17
|
+
const response = await fetch(url);
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
const body = await response.text();
|
|
20
|
+
throw new Error(`Elexon BMRS API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
21
|
+
}
|
|
22
|
+
const json = await response.json();
|
|
23
|
+
cache.set(url, json, TTL.REALTIME);
|
|
24
|
+
return json;
|
|
25
|
+
}
|
|
26
|
+
async function getDemand() {
|
|
27
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
28
|
+
const data = await fetchElexon(`/demand/outturn?settlementDateFrom=${today}&settlementDateTo=${today}&format=json`);
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
const rows = data?.data ?? [];
|
|
31
|
+
// Take last 24 periods (12 hours) for a useful window
|
|
32
|
+
const recent = rows.slice(-24);
|
|
33
|
+
const records = recent.map((r) => ({
|
|
34
|
+
timestamp: r.startTime ?? "",
|
|
35
|
+
settlement_period: Number(r.settlementPeriod ?? 0),
|
|
36
|
+
demand_mw: Number(r.initialDemandOutturn ?? 0),
|
|
37
|
+
transmission_demand_mw: Number(r.initialTransmissionSystemDemandOutturn ?? 0),
|
|
38
|
+
}));
|
|
39
|
+
return {
|
|
40
|
+
action: "demand",
|
|
41
|
+
description: "GB electricity demand outturn (MW), recent settlement periods",
|
|
42
|
+
records,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function getFrequency() {
|
|
46
|
+
const data = await fetchElexon("/datasets/FREQ?format=json");
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
const rows = data?.data ?? [];
|
|
49
|
+
if (rows.length === 0) {
|
|
50
|
+
throw new Error("No frequency data available from Elexon BMRS.");
|
|
51
|
+
}
|
|
52
|
+
// First record is the most recent
|
|
53
|
+
const row = rows[0];
|
|
54
|
+
const freq = Number(row.frequency ?? 50.0);
|
|
55
|
+
return {
|
|
56
|
+
action: "frequency",
|
|
57
|
+
description: "GB grid frequency (Hz). Nominal 50 Hz; deviations indicate grid stress.",
|
|
58
|
+
timestamp: row.measurementTime ?? new Date().toISOString(),
|
|
59
|
+
frequency_hz: Math.round(freq * 1000) / 1000,
|
|
60
|
+
deviation_hz: Math.round((freq - 50.0) * 1000) / 1000,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function getUkGridDemand(params) {
|
|
64
|
+
switch (params.action) {
|
|
65
|
+
case "demand":
|
|
66
|
+
return getDemand();
|
|
67
|
+
case "frequency":
|
|
68
|
+
return getFrequency();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const usGasSchema: z.ZodObject<{
|
|
3
|
+
dataset: z.ZodEnum<{
|
|
4
|
+
storage: "storage";
|
|
5
|
+
henry_hub: "henry_hub";
|
|
6
|
+
}>;
|
|
7
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
interface StorageRecord {
|
|
10
|
+
period: string;
|
|
11
|
+
value_bcf: number;
|
|
12
|
+
region: string;
|
|
13
|
+
}
|
|
14
|
+
interface HenryHubRecord {
|
|
15
|
+
period: string;
|
|
16
|
+
price_usd_mmbtu: number;
|
|
17
|
+
}
|
|
18
|
+
interface UsGasStorageResult {
|
|
19
|
+
dataset: "storage";
|
|
20
|
+
description: string;
|
|
21
|
+
records: StorageRecord[];
|
|
22
|
+
}
|
|
23
|
+
interface UsGasHenryHubResult {
|
|
24
|
+
dataset: "henry_hub";
|
|
25
|
+
description: string;
|
|
26
|
+
records: HenryHubRecord[];
|
|
27
|
+
}
|
|
28
|
+
type UsGasResult = UsGasStorageResult | UsGasHenryHubResult;
|
|
29
|
+
export declare function getUsGasData(params: z.infer<typeof usGasSchema>): Promise<UsGasResult>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
import { resolveApiKey } from "../lib/auth.js";
|
|
4
|
+
const BASE_URL = "https://api.eia.gov/v2";
|
|
5
|
+
const cache = new TtlCache();
|
|
6
|
+
export const usGasSchema = z.object({
|
|
7
|
+
dataset: z
|
|
8
|
+
.enum(["storage", "henry_hub"])
|
|
9
|
+
.describe('Dataset to query. "storage" = weekly US gas storage levels (Lower 48). ' +
|
|
10
|
+
'"henry_hub" = Henry Hub natural gas spot price.'),
|
|
11
|
+
limit: z
|
|
12
|
+
.number()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Number of records to return. Defaults to 10."),
|
|
15
|
+
});
|
|
16
|
+
async function getApiKey() {
|
|
17
|
+
try {
|
|
18
|
+
return await resolveApiKey("EIA_API_KEY");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error("EIA_API_KEY is required. Set it as an environment variable or in ~/.luminus/keys.json. " +
|
|
22
|
+
"Get one at https://www.eia.gov/opendata/register.php");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function fetchEia(path, params, ttlMs
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
) {
|
|
28
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
29
|
+
url.searchParams.set("api_key", await getApiKey());
|
|
30
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31
|
+
url.searchParams.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
const cacheKey = url.toString().replace(/api_key=[^&]+/, "api_key=***");
|
|
34
|
+
const cached = cache.get(cacheKey);
|
|
35
|
+
if (cached)
|
|
36
|
+
return cached;
|
|
37
|
+
const response = await fetch(url.toString());
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const body = await response.text();
|
|
40
|
+
throw new Error(`EIA API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
41
|
+
}
|
|
42
|
+
const json = await response.json();
|
|
43
|
+
cache.set(cacheKey, json, ttlMs);
|
|
44
|
+
return json;
|
|
45
|
+
}
|
|
46
|
+
async function getWeeklyStorage(limit) {
|
|
47
|
+
// EIA v2 requires /data/ suffix for data queries
|
|
48
|
+
// Filter: R48 = Lower 48 total, SWO = Working Gas
|
|
49
|
+
const data = await fetchEia("/natural-gas/stor/wkly/data/", {
|
|
50
|
+
"data[]": "value",
|
|
51
|
+
"facets[duoarea][]": "R48",
|
|
52
|
+
"facets[process][]": "SWO",
|
|
53
|
+
frequency: "weekly",
|
|
54
|
+
"sort[0][column]": "period",
|
|
55
|
+
"sort[0][direction]": "desc",
|
|
56
|
+
length: String(limit),
|
|
57
|
+
}, TTL.EIA);
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
const rows = data?.response?.data ?? [];
|
|
60
|
+
const records = rows.map((r) => ({
|
|
61
|
+
period: r.period ?? "",
|
|
62
|
+
value_bcf: Number(r.value ?? 0),
|
|
63
|
+
region: r["series-description"] ?? "Lower 48",
|
|
64
|
+
}));
|
|
65
|
+
return {
|
|
66
|
+
dataset: "storage",
|
|
67
|
+
description: "US weekly natural gas working storage, Lower 48 (Bcf)",
|
|
68
|
+
records,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function getHenryHub(limit) {
|
|
72
|
+
const data = await fetchEia("/natural-gas/pri/fut/data/", {
|
|
73
|
+
"data[]": "value",
|
|
74
|
+
"facets[series][]": "RNGWHHD",
|
|
75
|
+
frequency: "daily",
|
|
76
|
+
"sort[0][column]": "period",
|
|
77
|
+
"sort[0][direction]": "desc",
|
|
78
|
+
length: String(limit),
|
|
79
|
+
}, TTL.EIA);
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
const rows = data?.response?.data ?? [];
|
|
82
|
+
const records = rows.map((r) => ({
|
|
83
|
+
period: r.period ?? "",
|
|
84
|
+
price_usd_mmbtu: Number(r.value ?? 0),
|
|
85
|
+
}));
|
|
86
|
+
return {
|
|
87
|
+
dataset: "henry_hub",
|
|
88
|
+
description: "Henry Hub natural gas spot price (USD/MMBtu)",
|
|
89
|
+
records,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export async function getUsGasData(params) {
|
|
93
|
+
const limit = params.limit ?? 10;
|
|
94
|
+
switch (params.dataset) {
|
|
95
|
+
case "storage":
|
|
96
|
+
return getWeeklyStorage(limit);
|
|
97
|
+
case "henry_hub":
|
|
98
|
+
return getHenryHub(limit);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type GisSourceMetadata } from "../lib/gis-sources.js";
|
|
3
|
+
export declare const verifyGisSourcesSchema: z.ZodObject<{
|
|
4
|
+
source_id: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
interface SourceCheckResult {
|
|
7
|
+
source_id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
status: "ok" | "degraded" | "unreachable";
|
|
10
|
+
response_time_ms: number | null;
|
|
11
|
+
error: string | null;
|
|
12
|
+
metadata: GisSourceMetadata;
|
|
13
|
+
}
|
|
14
|
+
interface VerifyGisSourcesResult {
|
|
15
|
+
checked_at: string;
|
|
16
|
+
sources: SourceCheckResult[];
|
|
17
|
+
summary: {
|
|
18
|
+
total: number;
|
|
19
|
+
ok: number;
|
|
20
|
+
degraded: number;
|
|
21
|
+
unreachable: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function verifyGisSources(params: z.infer<typeof verifyGisSourcesSchema>): Promise<VerifyGisSourcesResult>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { GIS_SOURCES, GIS_HEALTH_CHECKS, } from "../lib/gis-sources.js";
|
|
3
|
+
export const verifyGisSourcesSchema = z.object({
|
|
4
|
+
source_id: z
|
|
5
|
+
.string()
|
|
6
|
+
.optional()
|
|
7
|
+
.describe('Check a single source by ID (e.g. "natural-england"). Omit to check all GIS sources.'),
|
|
8
|
+
});
|
|
9
|
+
async function checkSource(sourceId) {
|
|
10
|
+
const metadata = GIS_SOURCES[sourceId];
|
|
11
|
+
if (!metadata) {
|
|
12
|
+
return {
|
|
13
|
+
source_id: sourceId,
|
|
14
|
+
name: "Unknown",
|
|
15
|
+
status: "unreachable",
|
|
16
|
+
response_time_ms: null,
|
|
17
|
+
error: `No metadata defined for source "${sourceId}"`,
|
|
18
|
+
metadata: {},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const healthCheck = GIS_HEALTH_CHECKS.find((h) => h.source_id === sourceId);
|
|
22
|
+
if (!healthCheck) {
|
|
23
|
+
return {
|
|
24
|
+
source_id: sourceId,
|
|
25
|
+
name: metadata.name,
|
|
26
|
+
status: "degraded",
|
|
27
|
+
response_time_ms: null,
|
|
28
|
+
error: "No health check configured for this source",
|
|
29
|
+
metadata,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const start = Date.now();
|
|
33
|
+
try {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeout = setTimeout(() => controller.abort(), healthCheck.timeout_ms);
|
|
36
|
+
const fetchOptions = {
|
|
37
|
+
method: healthCheck.method,
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
};
|
|
40
|
+
if (healthCheck.method === "POST" && healthCheck.body) {
|
|
41
|
+
fetchOptions.headers = {
|
|
42
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
43
|
+
};
|
|
44
|
+
fetchOptions.body = healthCheck.body;
|
|
45
|
+
}
|
|
46
|
+
const response = await fetch(healthCheck.url, fetchOptions);
|
|
47
|
+
clearTimeout(timeout);
|
|
48
|
+
const elapsed = Date.now() - start;
|
|
49
|
+
const body = await response.text();
|
|
50
|
+
const validationError = healthCheck.validate(response.status, body);
|
|
51
|
+
if (validationError) {
|
|
52
|
+
return {
|
|
53
|
+
source_id: sourceId,
|
|
54
|
+
name: metadata.name,
|
|
55
|
+
status: "degraded",
|
|
56
|
+
response_time_ms: elapsed,
|
|
57
|
+
error: validationError,
|
|
58
|
+
metadata,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
source_id: sourceId,
|
|
63
|
+
name: metadata.name,
|
|
64
|
+
status: "ok",
|
|
65
|
+
response_time_ms: elapsed,
|
|
66
|
+
error: null,
|
|
67
|
+
metadata,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const elapsed = Date.now() - start;
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
const isTimeout = message.includes("abort");
|
|
74
|
+
return {
|
|
75
|
+
source_id: sourceId,
|
|
76
|
+
name: metadata.name,
|
|
77
|
+
status: "unreachable",
|
|
78
|
+
response_time_ms: isTimeout ? null : elapsed,
|
|
79
|
+
error: isTimeout
|
|
80
|
+
? `Timed out after ${healthCheck.timeout_ms}ms`
|
|
81
|
+
: message,
|
|
82
|
+
metadata,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function verifyGisSources(params) {
|
|
87
|
+
const sourceIds = params.source_id
|
|
88
|
+
? [params.source_id]
|
|
89
|
+
: Object.keys(GIS_SOURCES);
|
|
90
|
+
if (params.source_id && !GIS_SOURCES[params.source_id]) {
|
|
91
|
+
throw new Error(`Unknown source ID "${params.source_id}". Valid IDs: ${Object.keys(GIS_SOURCES).join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
const results = await Promise.allSettled(sourceIds.map((id) => checkSource(id)));
|
|
94
|
+
const sources = results.map((r, i) => {
|
|
95
|
+
if (r.status === "fulfilled")
|
|
96
|
+
return r.value;
|
|
97
|
+
return {
|
|
98
|
+
source_id: sourceIds[i],
|
|
99
|
+
name: GIS_SOURCES[sourceIds[i]]?.name ?? "Unknown",
|
|
100
|
+
status: "unreachable",
|
|
101
|
+
response_time_ms: null,
|
|
102
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason),
|
|
103
|
+
metadata: GIS_SOURCES[sourceIds[i]] ?? {},
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
const ok = sources.filter((s) => s.status === "ok").length;
|
|
107
|
+
const degraded = sources.filter((s) => s.status === "degraded").length;
|
|
108
|
+
const unreachable = sources.filter((s) => s.status === "unreachable").length;
|
|
109
|
+
return {
|
|
110
|
+
checked_at: new Date().toISOString(),
|
|
111
|
+
sources,
|
|
112
|
+
summary: {
|
|
113
|
+
total: sources.length,
|
|
114
|
+
ok,
|
|
115
|
+
degraded,
|
|
116
|
+
unreachable,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const weatherSchema: z.ZodObject<{
|
|
3
|
+
country: z.ZodOptional<z.ZodString>;
|
|
4
|
+
latitude: z.ZodOptional<z.ZodNumber>;
|
|
5
|
+
longitude: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
interface HourlyPoint {
|
|
8
|
+
time: string;
|
|
9
|
+
temperature_c: number;
|
|
10
|
+
wind_speed_kmh: number;
|
|
11
|
+
solar_radiation_wm2: number;
|
|
12
|
+
}
|
|
13
|
+
interface WeatherResult {
|
|
14
|
+
location: string;
|
|
15
|
+
latitude: number;
|
|
16
|
+
longitude: number;
|
|
17
|
+
hourly: HourlyPoint[];
|
|
18
|
+
stats: {
|
|
19
|
+
temp_min_c: number;
|
|
20
|
+
temp_max_c: number;
|
|
21
|
+
temp_mean_c: number;
|
|
22
|
+
wind_mean_kmh: number;
|
|
23
|
+
solar_mean_wm2: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function getWeatherForecast(params: z.infer<typeof weatherSchema>): Promise<WeatherResult>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
const BASE_URL = "https://api.open-meteo.com/v1/forecast";
|
|
4
|
+
const cache = new TtlCache();
|
|
5
|
+
/** Capital city coordinates for default weather lookups */
|
|
6
|
+
const CAPITAL_COORDS = {
|
|
7
|
+
AT: { lat: 48.21, lon: 16.37, city: "Vienna" },
|
|
8
|
+
BE: { lat: 50.85, lon: 4.35, city: "Brussels" },
|
|
9
|
+
BG: { lat: 42.70, lon: 23.32, city: "Sofia" },
|
|
10
|
+
CH: { lat: 46.95, lon: 7.45, city: "Bern" },
|
|
11
|
+
CZ: { lat: 50.08, lon: 14.42, city: "Prague" },
|
|
12
|
+
DE: { lat: 52.52, lon: 13.41, city: "Berlin" },
|
|
13
|
+
DK: { lat: 55.68, lon: 12.57, city: "Copenhagen" },
|
|
14
|
+
EE: { lat: 59.44, lon: 24.75, city: "Tallinn" },
|
|
15
|
+
ES: { lat: 40.42, lon: -3.70, city: "Madrid" },
|
|
16
|
+
FI: { lat: 60.17, lon: 24.94, city: "Helsinki" },
|
|
17
|
+
FR: { lat: 48.86, lon: 2.35, city: "Paris" },
|
|
18
|
+
GB: { lat: 51.51, lon: -0.13, city: "London" },
|
|
19
|
+
GR: { lat: 37.98, lon: 23.73, city: "Athens" },
|
|
20
|
+
HR: { lat: 45.81, lon: 15.98, city: "Zagreb" },
|
|
21
|
+
HU: { lat: 47.50, lon: 19.04, city: "Budapest" },
|
|
22
|
+
IE: { lat: 53.35, lon: -6.26, city: "Dublin" },
|
|
23
|
+
IT: { lat: 41.90, lon: 12.50, city: "Rome" },
|
|
24
|
+
LT: { lat: 54.69, lon: 25.28, city: "Vilnius" },
|
|
25
|
+
LV: { lat: 56.95, lon: 24.11, city: "Riga" },
|
|
26
|
+
NL: { lat: 52.37, lon: 4.90, city: "Amsterdam" },
|
|
27
|
+
NO: { lat: 59.91, lon: 10.75, city: "Oslo" },
|
|
28
|
+
PL: { lat: 52.23, lon: 21.01, city: "Warsaw" },
|
|
29
|
+
PT: { lat: 38.72, lon: -9.14, city: "Lisbon" },
|
|
30
|
+
RO: { lat: 44.43, lon: 26.10, city: "Bucharest" },
|
|
31
|
+
SE: { lat: 59.33, lon: 18.07, city: "Stockholm" },
|
|
32
|
+
SI: { lat: 46.05, lon: 14.51, city: "Ljubljana" },
|
|
33
|
+
SK: { lat: 48.15, lon: 17.11, city: "Bratislava" },
|
|
34
|
+
};
|
|
35
|
+
export const weatherSchema = z.object({
|
|
36
|
+
country: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe(`Country code for capital city lookup. Available: ${Object.keys(CAPITAL_COORDS).join(", ")}. ` +
|
|
40
|
+
"Ignored if latitude/longitude are provided."),
|
|
41
|
+
latitude: z
|
|
42
|
+
.number()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Latitude (-90 to 90). Overrides country-based lookup."),
|
|
45
|
+
longitude: z
|
|
46
|
+
.number()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Longitude (-180 to 180). Overrides country-based lookup."),
|
|
49
|
+
});
|
|
50
|
+
function resolveCoords(params) {
|
|
51
|
+
if (params.latitude != null && params.longitude != null) {
|
|
52
|
+
return {
|
|
53
|
+
lat: params.latitude,
|
|
54
|
+
lon: params.longitude,
|
|
55
|
+
location: `${params.latitude},${params.longitude}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!params.country) {
|
|
59
|
+
throw new Error("Provide either a country code or latitude/longitude coordinates.");
|
|
60
|
+
}
|
|
61
|
+
const upper = params.country.toUpperCase();
|
|
62
|
+
const coords = CAPITAL_COORDS[upper];
|
|
63
|
+
if (!coords) {
|
|
64
|
+
throw new Error(`Unknown country "${params.country}". Available: ${Object.keys(CAPITAL_COORDS).join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
return { lat: coords.lat, lon: coords.lon, location: `${coords.city}, ${upper}` };
|
|
67
|
+
}
|
|
68
|
+
export async function getWeatherForecast(params) {
|
|
69
|
+
const { lat, lon, location } = resolveCoords(params);
|
|
70
|
+
const cacheKey = `weather:${lat}:${lon}`;
|
|
71
|
+
const cached = cache.get(cacheKey);
|
|
72
|
+
if (cached)
|
|
73
|
+
return cached;
|
|
74
|
+
const url = new URL(BASE_URL);
|
|
75
|
+
url.searchParams.set("latitude", String(lat));
|
|
76
|
+
url.searchParams.set("longitude", String(lon));
|
|
77
|
+
url.searchParams.set("hourly", "temperature_2m,wind_speed_10m,shortwave_radiation");
|
|
78
|
+
const response = await fetch(url.toString());
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const body = await response.text();
|
|
81
|
+
throw new Error(`Open-Meteo API returned ${response.status}: ${body.slice(0, 300)}`);
|
|
82
|
+
}
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
const json = await response.json();
|
|
85
|
+
const h = json.hourly;
|
|
86
|
+
if (!h || !h.time) {
|
|
87
|
+
throw new Error("No hourly forecast data returned.");
|
|
88
|
+
}
|
|
89
|
+
const times = h.time;
|
|
90
|
+
const temps = h.temperature_2m;
|
|
91
|
+
const winds = h.wind_speed_10m;
|
|
92
|
+
const solar = h.shortwave_radiation;
|
|
93
|
+
const hourly = times.map((t, i) => ({
|
|
94
|
+
time: t,
|
|
95
|
+
temperature_c: temps[i] ?? 0,
|
|
96
|
+
wind_speed_kmh: winds[i] ?? 0,
|
|
97
|
+
solar_radiation_wm2: solar[i] ?? 0,
|
|
98
|
+
}));
|
|
99
|
+
const tempValues = temps.filter((v) => v != null);
|
|
100
|
+
const windValues = winds.filter((v) => v != null);
|
|
101
|
+
const solarValues = solar.filter((v) => v != null);
|
|
102
|
+
const mean = (arr) => arr.length > 0
|
|
103
|
+
? Math.round((arr.reduce((s, v) => s + v, 0) / arr.length) * 100) / 100
|
|
104
|
+
: 0;
|
|
105
|
+
const result = {
|
|
106
|
+
location,
|
|
107
|
+
latitude: lat,
|
|
108
|
+
longitude: lon,
|
|
109
|
+
hourly,
|
|
110
|
+
stats: {
|
|
111
|
+
temp_min_c: tempValues.length > 0 ? Math.min(...tempValues) : 0,
|
|
112
|
+
temp_max_c: tempValues.length > 0 ? Math.max(...tempValues) : 0,
|
|
113
|
+
temp_mean_c: mean(tempValues),
|
|
114
|
+
wind_mean_kmh: mean(windValues),
|
|
115
|
+
solar_mean_wm2: mean(solarValues),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
cache.set(cacheKey, result, TTL.WEATHER);
|
|
119
|
+
return result;
|
|
120
|
+
}
|