@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,34 @@
1
+ import { z } from "zod";
2
+ export declare const smardSchema: z.ZodObject<{
3
+ dataset: z.ZodEnum<{
4
+ generation: "generation";
5
+ consumption: "consumption";
6
+ market_price: "market_price";
7
+ }>;
8
+ }, z.core.$strip>;
9
+ interface HourlyPoint {
10
+ timestamp: string;
11
+ value: number;
12
+ }
13
+ interface GenerationSource {
14
+ fuel: string;
15
+ latest_mw: number;
16
+ hourly: HourlyPoint[];
17
+ }
18
+ interface SmardGenerationResult {
19
+ dataset: "generation";
20
+ source: "smard.de";
21
+ generation: GenerationSource[];
22
+ total_mw: number;
23
+ }
24
+ interface SmardTimeseriesResult {
25
+ dataset: string;
26
+ source: "smard.de";
27
+ resolution: "hourly";
28
+ data: HourlyPoint[];
29
+ latest: number;
30
+ unit: string;
31
+ }
32
+ type SmardResult = SmardGenerationResult | SmardTimeseriesResult;
33
+ export declare function getSmardData(params: z.infer<typeof smardSchema>): Promise<SmardResult>;
34
+ export {};
@@ -0,0 +1,155 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ const cache = new TtlCache();
4
+ const SMARD_BASE = "https://www.smard.de/app/chart_data";
5
+ export const smardSchema = z.object({
6
+ dataset: z
7
+ .enum(["generation", "consumption", "market_price"])
8
+ .describe('"generation" = hourly generation by source. "consumption" = total consumption. "market_price" = day-ahead + intraday prices.'),
9
+ });
10
+ // SMARD filter IDs
11
+ const FILTER_IDS = {
12
+ total_generation: 410,
13
+ total_consumption: 4169,
14
+ day_ahead_price: 4170,
15
+ };
16
+ const GENERATION_SOURCES = {
17
+ Biomass: 4068,
18
+ Hydro: 1226,
19
+ "Wind Offshore": 1225,
20
+ "Wind Onshore": 4067,
21
+ Solar: 4069,
22
+ Gas: 1228,
23
+ Coal: 1227,
24
+ Nuclear: 4071,
25
+ };
26
+ async function fetchSmardIndex(filterId) {
27
+ const cacheKey = `smard:index:${filterId}`;
28
+ const cached = cache.get(cacheKey);
29
+ if (cached)
30
+ return cached;
31
+ try {
32
+ const url = `${SMARD_BASE}/${filterId}/DE/index_hour.json`;
33
+ const response = await fetch(url, {
34
+ headers: { "User-Agent": "luminus-mcp/0.2" },
35
+ });
36
+ if (!response.ok)
37
+ return null;
38
+ const data = (await response.json());
39
+ if (!data.timestamps || data.timestamps.length === 0)
40
+ return null;
41
+ const latest = data.timestamps[data.timestamps.length - 1];
42
+ cache.set(cacheKey, latest, TTL.REALTIME);
43
+ return latest;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ async function fetchSmardSeries(filterId, timestamp) {
50
+ const cacheKey = `smard:data:${filterId}:${timestamp}`;
51
+ const cached = cache.get(cacheKey);
52
+ if (cached)
53
+ return cached;
54
+ try {
55
+ const url = `${SMARD_BASE}/${filterId}/DE/${filterId}_DE_hour_${timestamp}.json`;
56
+ const response = await fetch(url, {
57
+ headers: { "User-Agent": "luminus-mcp/0.2" },
58
+ });
59
+ if (!response.ok)
60
+ return null;
61
+ const data = (await response.json());
62
+ if (!data.series || data.series.length === 0)
63
+ return null;
64
+ cache.set(cacheKey, data.series, TTL.REALTIME);
65
+ return data.series;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ function extractLast24h(series) {
72
+ const now = Date.now();
73
+ const cutoff = now - 24 * 60 * 60 * 1000;
74
+ const points = [];
75
+ for (const [tsMs, value] of series) {
76
+ if (tsMs < cutoff)
77
+ continue;
78
+ if (value == null || !Number.isFinite(value))
79
+ continue;
80
+ points.push({
81
+ timestamp: new Date(tsMs).toISOString(),
82
+ value: Math.round(value * 100) / 100,
83
+ });
84
+ }
85
+ return points;
86
+ }
87
+ function latestValue(series) {
88
+ for (let i = series.length - 1; i >= 0; i--) {
89
+ const val = series[i][1];
90
+ if (val != null && Number.isFinite(val)) {
91
+ return Math.round(val * 100) / 100;
92
+ }
93
+ }
94
+ return 0;
95
+ }
96
+ async function fetchGeneration() {
97
+ // Fetch all generation sources in parallel
98
+ const entries = Object.entries(GENERATION_SOURCES);
99
+ const results = await Promise.all(entries.map(async ([fuel, filterId]) => {
100
+ const timestamp = await fetchSmardIndex(filterId);
101
+ if (timestamp == null)
102
+ return null;
103
+ const series = await fetchSmardSeries(filterId, timestamp);
104
+ if (!series)
105
+ return null;
106
+ const hourly = extractLast24h(series);
107
+ const latest = latestValue(series);
108
+ return { fuel, latest_mw: latest, hourly };
109
+ }));
110
+ const generation = results.filter((r) => r !== null && r.latest_mw > 0);
111
+ generation.sort((a, b) => b.latest_mw - a.latest_mw);
112
+ const totalMw = generation.reduce((sum, g) => sum + g.latest_mw, 0);
113
+ return {
114
+ dataset: "generation",
115
+ source: "smard.de",
116
+ generation,
117
+ total_mw: Math.round(totalMw),
118
+ };
119
+ }
120
+ async function fetchTimeseries(dataset) {
121
+ const filterId = dataset === "consumption"
122
+ ? FILTER_IDS.total_consumption
123
+ : FILTER_IDS.day_ahead_price;
124
+ const unit = dataset === "consumption" ? "MW" : "EUR/MWh";
125
+ const timestamp = await fetchSmardIndex(filterId);
126
+ if (timestamp == null) {
127
+ throw new Error(`Unable to fetch SMARD index for ${dataset}. Service may be temporarily unavailable.`);
128
+ }
129
+ const series = await fetchSmardSeries(filterId, timestamp);
130
+ if (!series) {
131
+ throw new Error(`Unable to fetch SMARD data for ${dataset}. Service may be temporarily unavailable.`);
132
+ }
133
+ const data = extractLast24h(series);
134
+ const latest = latestValue(series);
135
+ return {
136
+ dataset,
137
+ source: "smard.de",
138
+ resolution: "hourly",
139
+ data,
140
+ latest,
141
+ unit,
142
+ };
143
+ }
144
+ export async function getSmardData(params) {
145
+ switch (params.dataset) {
146
+ case "generation":
147
+ return fetchGeneration();
148
+ case "consumption":
149
+ return fetchTimeseries("consumption");
150
+ case "market_price":
151
+ return fetchTimeseries("market_price");
152
+ default:
153
+ throw new Error(`Unknown dataset "${params.dataset}". Use: generation, consumption, market_price.`);
154
+ }
155
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { type GisSourceMetadata } from "../lib/gis-sources.js";
3
+ export declare const solarSchema: z.ZodObject<{
4
+ lat: z.ZodNumber;
5
+ lon: z.ZodNumber;
6
+ year: z.ZodOptional<z.ZodNumber>;
7
+ }, z.core.$strip>;
8
+ interface MonthlyIrradiance {
9
+ month: number;
10
+ irradiance_kwh_m2: number;
11
+ energy_kwh: number;
12
+ }
13
+ interface SolarResult {
14
+ lat: number;
15
+ lon: number;
16
+ optimal_angle_deg: number;
17
+ annual_irradiance_kwh_m2: number;
18
+ annual_yield_kwh: number;
19
+ monthly: MonthlyIrradiance[];
20
+ source_metadata: GisSourceMetadata;
21
+ }
22
+ export declare function getSolarIrradiance(params: z.infer<typeof solarSchema>): Promise<SolarResult>;
23
+ export {};
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ import { GIS_SOURCES } from "../lib/gis-sources.js";
4
+ const BASE_URL = "https://re.jrc.ec.europa.eu/api/v5_3";
5
+ const cache = new TtlCache();
6
+ export const solarSchema = z.object({
7
+ lat: z
8
+ .number()
9
+ .describe("Latitude (-90 to 90). Example: 48.86 for Paris."),
10
+ lon: z
11
+ .number()
12
+ .describe("Longitude (-180 to 180). Example: 2.35 for Paris."),
13
+ year: z
14
+ .number()
15
+ .optional()
16
+ .describe("Specific year for hourly time series (2005-2023). Omit for long-term monthly averages."),
17
+ });
18
+ export async function getSolarIrradiance(params) {
19
+ const { lat, lon, year } = params;
20
+ if (lat < -90 || lat > 90)
21
+ throw new Error("Latitude must be between -90 and 90.");
22
+ if (lon < -180 || lon > 180)
23
+ throw new Error("Longitude must be between -180 and 180.");
24
+ const cacheKey = `pvgis:${lat}:${lon}:${year ?? "avg"}`;
25
+ const cached = cache.get(cacheKey);
26
+ if (cached)
27
+ return cached;
28
+ const url = new URL(`${BASE_URL}/PVcalc`);
29
+ url.searchParams.set("lat", String(lat));
30
+ url.searchParams.set("lon", String(lon));
31
+ url.searchParams.set("peakpower", "1");
32
+ url.searchParams.set("loss", "14");
33
+ url.searchParams.set("outputformat", "json");
34
+ if (year) {
35
+ url.searchParams.set("startyear", String(year));
36
+ url.searchParams.set("endyear", String(year));
37
+ }
38
+ const response = await fetch(url.toString());
39
+ if (!response.ok) {
40
+ const body = await response.text();
41
+ throw new Error(`PVGIS API returned ${response.status}: ${body.slice(0, 300)}`);
42
+ }
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ const json = await response.json();
45
+ const outputs = json.outputs;
46
+ if (!outputs)
47
+ throw new Error("No data returned from PVGIS for this location.");
48
+ const monthlyData = outputs.monthly?.fixed ?? [];
49
+ const totals = outputs.totals?.fixed ?? {};
50
+ const optimalAngle = json.inputs?.mounting_system?.fixed?.slope?.value ?? 0;
51
+ const monthly = monthlyData.map(
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ (m) => ({
54
+ month: m.month,
55
+ irradiance_kwh_m2: Number(m["H(i)_m"] ?? 0),
56
+ energy_kwh: Number(m.E_m ?? 0),
57
+ }));
58
+ const result = {
59
+ lat,
60
+ lon,
61
+ optimal_angle_deg: Number(optimalAngle),
62
+ annual_irradiance_kwh_m2: Number(totals["H(i)_y"] ?? 0),
63
+ annual_yield_kwh: Number(totals.E_y ?? 0),
64
+ monthly,
65
+ source_metadata: GIS_SOURCES["pvgis"],
66
+ };
67
+ cache.set(cacheKey, result, TTL.STATIC_DATA);
68
+ return result;
69
+ }
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ export declare const stormglassSchema: z.ZodObject<{
3
+ latitude: z.ZodNumber;
4
+ longitude: z.ZodNumber;
5
+ dataset: z.ZodOptional<z.ZodEnum<{
6
+ weather: "weather";
7
+ tide: "tide";
8
+ }>>;
9
+ }, z.core.$strip>;
10
+ interface WeatherHour {
11
+ timestamp: string;
12
+ wind_speed_ms: number;
13
+ wind_direction_deg: number;
14
+ wind_gust_ms: number;
15
+ wave_height_m: number;
16
+ wave_period_s: number;
17
+ wave_direction_deg: number;
18
+ swell_height_m: number;
19
+ swell_period_s: number;
20
+ sea_surface_temp_c: number;
21
+ air_temp_c: number;
22
+ pressure_hpa: number;
23
+ visibility_km: number;
24
+ cloud_cover_pct: number;
25
+ }
26
+ interface WeatherResult {
27
+ dataset: "weather";
28
+ source: string;
29
+ latitude: number;
30
+ longitude: number;
31
+ description: string;
32
+ hours: WeatherHour[];
33
+ summary: {
34
+ avg_wind_speed_ms: number;
35
+ max_wind_gust_ms: number;
36
+ avg_wave_height_m: number;
37
+ max_wave_height_m: number;
38
+ avg_sea_temp_c: number;
39
+ conditions: string;
40
+ };
41
+ }
42
+ interface TidePoint {
43
+ timestamp: string;
44
+ type: string;
45
+ height_m: number;
46
+ }
47
+ interface TideResult {
48
+ dataset: "tide";
49
+ source: string;
50
+ latitude: number;
51
+ longitude: number;
52
+ tides: TidePoint[];
53
+ }
54
+ type StormglassResult = WeatherResult | TideResult;
55
+ export declare function getStormglass(params: z.infer<typeof stormglassSchema>): Promise<StormglassResult>;
56
+ export {};
@@ -0,0 +1,172 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ import { resolveApiKey } from "../lib/auth.js";
4
+ const API_BASE = "https://api.stormglass.io/v2";
5
+ const cache = new TtlCache();
6
+ const FORECAST_WINDOW_MS = 48 * 60 * 60 * 1000;
7
+ export const stormglassSchema = z.object({
8
+ latitude: z.number().describe("Latitude (-90 to 90)."),
9
+ longitude: z.number().describe("Longitude (-180 to 180)."),
10
+ dataset: z
11
+ .enum(["weather", "tide"])
12
+ .optional()
13
+ .describe('"weather" = marine/offshore weather including wind at hub height, waves, swell (default). ' +
14
+ '"tide" = tidal predictions (sea level, high/low tide times).'),
15
+ });
16
+ async function getApiKey() {
17
+ try {
18
+ return await resolveApiKey("STORMGLASS_API_KEY");
19
+ }
20
+ catch {
21
+ throw new Error("STORMGLASS_API_KEY is required. Set it as an environment variable or in ~/.luminus/keys.json. " +
22
+ "Get a free key at https://stormglass.io/ (10 requests/day on free tier).");
23
+ }
24
+ }
25
+ function alignToBucket(date, bucketMs) {
26
+ return new Date(Math.floor(date.getTime() / bucketMs) * bucketMs);
27
+ }
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ function pickSource(obj) {
30
+ // Storm Glass returns data from multiple sources; pick the best available
31
+ if (typeof obj === "number")
32
+ return obj;
33
+ if (obj?.sg != null)
34
+ return Number(obj.sg);
35
+ if (obj?.noaa != null)
36
+ return Number(obj.noaa);
37
+ if (obj?.dwd != null)
38
+ return Number(obj.dwd);
39
+ if (obj?.icon != null)
40
+ return Number(obj.icon);
41
+ const vals = Object.values(obj ?? {}).filter((v) => typeof v === "number");
42
+ return vals.length > 0 ? vals[0] : 0;
43
+ }
44
+ function buildWeatherUrl(lat, lon) {
45
+ const params = [
46
+ "windSpeed",
47
+ "windDirection",
48
+ "gust",
49
+ "waveHeight",
50
+ "wavePeriod",
51
+ "waveDirection",
52
+ "swellHeight",
53
+ "swellPeriod",
54
+ "waterTemperature",
55
+ "airTemperature",
56
+ "pressure",
57
+ "visibility",
58
+ "cloudCover",
59
+ ].join(",");
60
+ const start = alignToBucket(new Date(), TTL.WEATHER);
61
+ const end = new Date(start.getTime() + FORECAST_WINDOW_MS);
62
+ return (`${API_BASE}/weather/point?lat=${lat}&lng=${lon}` +
63
+ `&params=${params}` +
64
+ `&start=${start.toISOString()}&end=${end.toISOString()}`);
65
+ }
66
+ async function fetchWeather(lat, lon) {
67
+ const url = buildWeatherUrl(lat, lon);
68
+ const cached = cache.get(url);
69
+ if (cached)
70
+ return cached;
71
+ const response = await fetch(url, {
72
+ headers: { Authorization: await getApiKey() },
73
+ });
74
+ if (!response.ok) {
75
+ const body = await response.text();
76
+ throw new Error(`Storm Glass API returned ${response.status}: ${body.slice(0, 300)}`);
77
+ }
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ const json = await response.json();
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ const rawHours = json?.hours ?? [];
82
+ const hours = rawHours.slice(0, 48).map((h) => ({
83
+ timestamp: h.time ?? "",
84
+ wind_speed_ms: Math.round(pickSource(h.windSpeed) * 10) / 10,
85
+ wind_direction_deg: Math.round(pickSource(h.windDirection)),
86
+ wind_gust_ms: Math.round(pickSource(h.gust) * 10) / 10,
87
+ wave_height_m: Math.round(pickSource(h.waveHeight) * 100) / 100,
88
+ wave_period_s: Math.round(pickSource(h.wavePeriod) * 10) / 10,
89
+ wave_direction_deg: Math.round(pickSource(h.waveDirection)),
90
+ swell_height_m: Math.round(pickSource(h.swellHeight) * 100) / 100,
91
+ swell_period_s: Math.round(pickSource(h.swellPeriod) * 10) / 10,
92
+ sea_surface_temp_c: Math.round(pickSource(h.waterTemperature) * 10) / 10,
93
+ air_temp_c: Math.round(pickSource(h.airTemperature) * 10) / 10,
94
+ pressure_hpa: Math.round(pickSource(h.pressure) * 10) / 10,
95
+ visibility_km: Math.round(pickSource(h.visibility) * 10) / 10,
96
+ cloud_cover_pct: Math.round(pickSource(h.cloudCover)),
97
+ }));
98
+ const windSpeeds = hours.map((h) => h.wind_speed_ms);
99
+ const gusts = hours.map((h) => h.wind_gust_ms);
100
+ const waves = hours.map((h) => h.wave_height_m);
101
+ const seaTemps = hours.filter((h) => h.sea_surface_temp_c !== 0).map((h) => h.sea_surface_temp_c);
102
+ const avg = (arr) => arr.length > 0 ? Math.round((arr.reduce((s, v) => s + v, 0) / arr.length) * 10) / 10 : 0;
103
+ const avgWind = avg(windSpeeds);
104
+ const maxWave = waves.length > 0 ? Math.round(Math.max(...waves) * 100) / 100 : 0;
105
+ let conditions = "Calm";
106
+ if (avgWind > 15)
107
+ conditions = "Storm conditions — offshore operations likely suspended";
108
+ else if (avgWind > 10)
109
+ conditions = "Strong winds — challenging for offshore maintenance";
110
+ else if (avgWind > 6)
111
+ conditions = "Moderate winds — good for wind generation";
112
+ else if (avgWind > 3)
113
+ conditions = "Light winds — reduced wind output";
114
+ const result = {
115
+ dataset: "weather",
116
+ source: "Storm Glass (marine/offshore weather)",
117
+ latitude: lat,
118
+ longitude: lon,
119
+ description: "48-hour marine weather forecast. Key for offshore wind assessment and maintenance windows.",
120
+ hours,
121
+ summary: {
122
+ avg_wind_speed_ms: avgWind,
123
+ max_wind_gust_ms: gusts.length > 0 ? Math.round(Math.max(...gusts) * 10) / 10 : 0,
124
+ avg_wave_height_m: avg(waves),
125
+ max_wave_height_m: maxWave,
126
+ avg_sea_temp_c: avg(seaTemps),
127
+ conditions,
128
+ },
129
+ };
130
+ cache.set(url, result, TTL.WEATHER);
131
+ return result;
132
+ }
133
+ async function fetchTide(lat, lon) {
134
+ const url = `${API_BASE}/tide/extremes/point?lat=${lat}&lng=${lon}`;
135
+ const cached = cache.get(url);
136
+ if (cached)
137
+ return cached;
138
+ const response = await fetch(url, {
139
+ headers: { Authorization: await getApiKey() },
140
+ });
141
+ if (!response.ok) {
142
+ const body = await response.text();
143
+ throw new Error(`Storm Glass Tide API returned ${response.status}: ${body.slice(0, 300)}`);
144
+ }
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ const json = await response.json();
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ const rawTides = json?.data ?? [];
149
+ const tides = rawTides.slice(0, 20).map((t) => ({
150
+ timestamp: t.time ?? "",
151
+ type: t.type ?? "unknown",
152
+ height_m: Math.round((Number(t.height ?? 0)) * 100) / 100,
153
+ }));
154
+ const result = {
155
+ dataset: "tide",
156
+ source: "Storm Glass (tidal predictions)",
157
+ latitude: lat,
158
+ longitude: lon,
159
+ tides,
160
+ };
161
+ cache.set(url, result, TTL.WEATHER);
162
+ return result;
163
+ }
164
+ export async function getStormglass(params) {
165
+ const dataset = params.dataset ?? "weather";
166
+ switch (dataset) {
167
+ case "weather":
168
+ return fetchWeather(params.latitude, params.longitude);
169
+ case "tide":
170
+ return fetchTide(params.latitude, params.longitude);
171
+ }
172
+ }
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ export declare const ternaSchema: z.ZodObject<{
3
+ dataset: z.ZodEnum<{
4
+ generation: "generation";
5
+ demand: "demand";
6
+ exchanges: "exchanges";
7
+ market_data: "market_data";
8
+ }>;
9
+ date: z.ZodOptional<z.ZodString>;
10
+ zone: z.ZodOptional<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ interface GenerationRecord {
13
+ timestamp: string;
14
+ thermal_mw: number;
15
+ hydro_mw: number;
16
+ wind_mw: number;
17
+ solar_mw: number;
18
+ geothermal_mw: number;
19
+ biomass_mw: number;
20
+ total_mw: number;
21
+ renewable_pct: number;
22
+ }
23
+ interface TernaGenerationResult {
24
+ dataset: "generation";
25
+ source: string;
26
+ date: string;
27
+ zone: string;
28
+ records: GenerationRecord[];
29
+ latest: GenerationRecord | null;
30
+ }
31
+ interface DemandRecord {
32
+ timestamp: string;
33
+ demand_mw: number;
34
+ forecast_mw: number;
35
+ }
36
+ interface TernaDemandResult {
37
+ dataset: "demand";
38
+ source: string;
39
+ date: string;
40
+ zone: string;
41
+ records: DemandRecord[];
42
+ }
43
+ interface ExchangeRecord {
44
+ border: string;
45
+ flow_mw: number;
46
+ direction: string;
47
+ }
48
+ interface TernaExchangesResult {
49
+ dataset: "exchanges";
50
+ source: string;
51
+ date: string;
52
+ exchanges: ExchangeRecord[];
53
+ net_import_mw: number;
54
+ }
55
+ interface MarketRecord {
56
+ zone: string;
57
+ timestamp: string;
58
+ price_eur_mwh: number;
59
+ }
60
+ interface TernaMarketResult {
61
+ dataset: "market_data";
62
+ source: string;
63
+ date: string;
64
+ zone: string;
65
+ records: MarketRecord[];
66
+ }
67
+ type TernaResult = TernaGenerationResult | TernaDemandResult | TernaExchangesResult | TernaMarketResult;
68
+ export declare function getTernaData(params: z.infer<typeof ternaSchema>): Promise<TernaResult>;
69
+ export {};