@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,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type GisSourceMetadata } from "../lib/gis-sources.js";
|
|
3
|
+
export declare const gridConnectionIntelligenceSchema: z.ZodObject<{
|
|
4
|
+
lat: z.ZodNumber;
|
|
5
|
+
lon: z.ZodNumber;
|
|
6
|
+
radius_km: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
country: z.ZodString;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
interface NearestGspResult {
|
|
10
|
+
gsp_id: string;
|
|
11
|
+
gsp_name: string;
|
|
12
|
+
distance_km: number;
|
|
13
|
+
region_id: string;
|
|
14
|
+
region_name: string;
|
|
15
|
+
}
|
|
16
|
+
interface ConnectionQueueResult {
|
|
17
|
+
projects: Array<Record<string, unknown>>;
|
|
18
|
+
total_mw_queued: number;
|
|
19
|
+
search_term: string;
|
|
20
|
+
}
|
|
21
|
+
interface NearbySubstation {
|
|
22
|
+
name: string | null;
|
|
23
|
+
voltage_kv: number | null;
|
|
24
|
+
distance_km: number;
|
|
25
|
+
}
|
|
26
|
+
interface GridConnectionIntelligenceResult {
|
|
27
|
+
lat: number;
|
|
28
|
+
lon: number;
|
|
29
|
+
country: string;
|
|
30
|
+
nearest_gsp: NearestGspResult | null;
|
|
31
|
+
connection_queue: ConnectionQueueResult | null;
|
|
32
|
+
nearby_substations: NearbySubstation[];
|
|
33
|
+
confidence_notes: string[];
|
|
34
|
+
source_metadata: {
|
|
35
|
+
gsp_lookup: GisSourceMetadata;
|
|
36
|
+
tec_register: GisSourceMetadata;
|
|
37
|
+
grid_proximity: GisSourceMetadata;
|
|
38
|
+
};
|
|
39
|
+
disclaimer: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function getGridConnectionIntelligence(params: z.infer<typeof gridConnectionIntelligenceSchema>): Promise<GridConnectionIntelligenceResult>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { lookupGspRegion } from "../lib/neso-gsp.js";
|
|
3
|
+
import { getGridConnectionQueue } from "./grid-connection-queue.js";
|
|
4
|
+
import { getGridProximity } from "./grid-proximity.js";
|
|
5
|
+
import { GIS_SOURCES } from "../lib/gis-sources.js";
|
|
6
|
+
export const gridConnectionIntelligenceSchema = z.object({
|
|
7
|
+
lat: z.number().describe("Latitude (-90 to 90). WGS84."),
|
|
8
|
+
lon: z.number().describe("Longitude (-180 to 180). WGS84."),
|
|
9
|
+
radius_km: z
|
|
10
|
+
.number()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("GSP search radius in km (default 25, max 50)."),
|
|
13
|
+
country: z.string().describe('Only "GB" is supported.'),
|
|
14
|
+
});
|
|
15
|
+
const DISCLAIMER = "This combines a nearest-GSP spatial approximation with the NESO TEC register and OSM substation data. " +
|
|
16
|
+
"It is not a connection offer, capacity guarantee, or DNO headroom assessment. " +
|
|
17
|
+
"Always verify with the relevant network operator before making connection decisions.";
|
|
18
|
+
function buildConfidenceNotes(gspResult) {
|
|
19
|
+
const notes = [
|
|
20
|
+
"GSP lookup uses nearest-GSP approximation, not polygon containment",
|
|
21
|
+
"TEC register connection sites are matched by GSP name substring, not spatial coordinates",
|
|
22
|
+
"Connection queue data shows contracted positions, not guaranteed available capacity",
|
|
23
|
+
];
|
|
24
|
+
if (!gspResult) {
|
|
25
|
+
notes.push("No GSP found within search radius");
|
|
26
|
+
}
|
|
27
|
+
return notes;
|
|
28
|
+
}
|
|
29
|
+
function deriveSearchTerm(regionName) {
|
|
30
|
+
// Region names are already human-readable (e.g. "Berkswell", "Bramley")
|
|
31
|
+
return regionName.trim();
|
|
32
|
+
}
|
|
33
|
+
export async function getGridConnectionIntelligence(params) {
|
|
34
|
+
const { lat, lon, country } = params;
|
|
35
|
+
const radiusKm = params.radius_km ?? 25;
|
|
36
|
+
if (country.toUpperCase() !== "GB") {
|
|
37
|
+
throw new Error('Only country "GB" is supported for grid connection intelligence.');
|
|
38
|
+
}
|
|
39
|
+
if (lat < -90 || lat > 90)
|
|
40
|
+
throw new Error("Latitude must be between -90 and 90.");
|
|
41
|
+
if (lon < -180 || lon > 180)
|
|
42
|
+
throw new Error("Longitude must be between -180 and 180.");
|
|
43
|
+
if (radiusKm <= 0 || radiusKm > 50)
|
|
44
|
+
throw new Error("radius_km must be between 0 and 50.");
|
|
45
|
+
// Step 1: Find nearest GSP
|
|
46
|
+
const gspResult = await lookupGspRegion(lat, lon, radiusKm);
|
|
47
|
+
// Step 2: In parallel, query TEC register (if GSP found) and nearby substations
|
|
48
|
+
// Use region_name (e.g. "Berkswell") not gsp_name (e.g. "BESW_1") — TEC register
|
|
49
|
+
// Connection Site values are human-readable names like "Berkswell GSP"
|
|
50
|
+
const tecPromise = gspResult
|
|
51
|
+
? queryTecRegister(gspResult.region_name)
|
|
52
|
+
: Promise.resolve(null);
|
|
53
|
+
const proximityPromise = queryGridProximity(lat, lon, radiusKm);
|
|
54
|
+
const [connectionQueue, nearbySubstations] = await Promise.all([
|
|
55
|
+
tecPromise,
|
|
56
|
+
proximityPromise,
|
|
57
|
+
]);
|
|
58
|
+
return {
|
|
59
|
+
lat,
|
|
60
|
+
lon,
|
|
61
|
+
country: "GB",
|
|
62
|
+
nearest_gsp: gspResult
|
|
63
|
+
? {
|
|
64
|
+
gsp_id: gspResult.gsp_id,
|
|
65
|
+
gsp_name: gspResult.gsp_name,
|
|
66
|
+
distance_km: gspResult.distance_km,
|
|
67
|
+
region_id: gspResult.region_id,
|
|
68
|
+
region_name: gspResult.region_name,
|
|
69
|
+
}
|
|
70
|
+
: null,
|
|
71
|
+
connection_queue: connectionQueue,
|
|
72
|
+
nearby_substations: nearbySubstations,
|
|
73
|
+
confidence_notes: buildConfidenceNotes(gspResult),
|
|
74
|
+
source_metadata: {
|
|
75
|
+
gsp_lookup: GIS_SOURCES["neso-gsp-lookup"],
|
|
76
|
+
tec_register: GIS_SOURCES["neso-tec-register"],
|
|
77
|
+
grid_proximity: GIS_SOURCES["overpass-osm"],
|
|
78
|
+
},
|
|
79
|
+
disclaimer: DISCLAIMER,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function queryTecRegister(regionName) {
|
|
83
|
+
const searchTerm = deriveSearchTerm(regionName);
|
|
84
|
+
if (searchTerm.length === 0)
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
const result = await getGridConnectionQueue({
|
|
88
|
+
connection_site_query: searchTerm,
|
|
89
|
+
limit: 50,
|
|
90
|
+
});
|
|
91
|
+
const totalMwQueued = result.summary.total_net_change_mw;
|
|
92
|
+
return {
|
|
93
|
+
projects: result.projects,
|
|
94
|
+
total_mw_queued: totalMwQueued,
|
|
95
|
+
search_term: searchTerm,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// TEC register failure should not block the overall result
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function queryGridProximity(lat, lon, radiusKm) {
|
|
104
|
+
try {
|
|
105
|
+
// grid-proximity has a max radius of 25km
|
|
106
|
+
const clampedRadius = Math.min(radiusKm, 25);
|
|
107
|
+
const result = await getGridProximity({
|
|
108
|
+
lat,
|
|
109
|
+
lon,
|
|
110
|
+
radius_km: clampedRadius,
|
|
111
|
+
});
|
|
112
|
+
return result.substations.map((sub) => ({
|
|
113
|
+
name: sub.name,
|
|
114
|
+
voltage_kv: sub.voltage_kv,
|
|
115
|
+
distance_km: sub.distance_km,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Proximity failure should not block the overall result
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type GisSourceMetadata } from "../lib/gis-sources.js";
|
|
3
|
+
export declare const gridConnectionQueueSchema: z.ZodObject<{
|
|
4
|
+
connection_site_query: z.ZodOptional<z.ZodString>;
|
|
5
|
+
project_name_query: z.ZodOptional<z.ZodString>;
|
|
6
|
+
host_to: z.ZodOptional<z.ZodString>;
|
|
7
|
+
plant_type: z.ZodOptional<z.ZodString>;
|
|
8
|
+
project_status: z.ZodOptional<z.ZodString>;
|
|
9
|
+
agreement_type: z.ZodOptional<z.ZodString>;
|
|
10
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
interface ProjectMatch {
|
|
13
|
+
project_name: string;
|
|
14
|
+
customer_name: string | null;
|
|
15
|
+
connection_site: string;
|
|
16
|
+
stage: number | null;
|
|
17
|
+
mw_connected: number;
|
|
18
|
+
mw_increase_decrease: number;
|
|
19
|
+
cumulative_total_capacity_mw: number;
|
|
20
|
+
mw_effective_from: string | null;
|
|
21
|
+
project_status: string | null;
|
|
22
|
+
agreement_type: string | null;
|
|
23
|
+
host_to: string | null;
|
|
24
|
+
plant_type: string | null;
|
|
25
|
+
project_id: string | null;
|
|
26
|
+
project_number: string | null;
|
|
27
|
+
gate: number | null;
|
|
28
|
+
}
|
|
29
|
+
interface ConnectionSiteSummary {
|
|
30
|
+
connection_site: string;
|
|
31
|
+
project_count: number;
|
|
32
|
+
total_net_change_mw: number;
|
|
33
|
+
total_connected_mw: number;
|
|
34
|
+
total_cumulative_capacity_mw: number;
|
|
35
|
+
plant_types: string[];
|
|
36
|
+
project_statuses: string[];
|
|
37
|
+
earliest_effective_from: string | null;
|
|
38
|
+
}
|
|
39
|
+
interface GridConnectionQueueResult {
|
|
40
|
+
filters: {
|
|
41
|
+
connection_site_query: string | null;
|
|
42
|
+
project_name_query: string | null;
|
|
43
|
+
host_to: string | null;
|
|
44
|
+
plant_type: string | null;
|
|
45
|
+
project_status: string | null;
|
|
46
|
+
agreement_type: string | null;
|
|
47
|
+
};
|
|
48
|
+
summary: {
|
|
49
|
+
matched_projects: number;
|
|
50
|
+
returned_projects: number;
|
|
51
|
+
total_connected_mw: number;
|
|
52
|
+
total_net_change_mw: number;
|
|
53
|
+
total_cumulative_capacity_mw: number;
|
|
54
|
+
earliest_effective_from: string | null;
|
|
55
|
+
latest_effective_from: string | null;
|
|
56
|
+
};
|
|
57
|
+
connection_sites: ConnectionSiteSummary[];
|
|
58
|
+
projects: ProjectMatch[];
|
|
59
|
+
source_metadata: GisSourceMetadata;
|
|
60
|
+
disclaimer: string;
|
|
61
|
+
}
|
|
62
|
+
export declare function resetGridConnectionQueueCacheForTests(): void;
|
|
63
|
+
export declare function getGridConnectionQueue(params: z.infer<typeof gridConnectionQueueSchema>): Promise<GridConnectionQueueResult>;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
import { GIS_SOURCES } from "../lib/gis-sources.js";
|
|
4
|
+
import { guardJsonFields } from "../lib/schema-guard.js";
|
|
5
|
+
const cache = new TtlCache();
|
|
6
|
+
const NESO_TEC_RESOURCE_ID = "17becbab-e3e8-473f-b303-3806f43a6a10";
|
|
7
|
+
const NESO_TEC_DATASTORE_URL = `https://api.neso.energy/api/3/action/datastore_search?resource_id=${NESO_TEC_RESOURCE_ID}&limit=5000`;
|
|
8
|
+
export const gridConnectionQueueSchema = z.object({
|
|
9
|
+
connection_site_query: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Case-insensitive substring match on NESO Connection Site, for example \"Berkswell\"."),
|
|
13
|
+
project_name_query: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Case-insensitive substring match on Project Name."),
|
|
17
|
+
host_to: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Filter by host transmission owner, for example "NGET", "SPT", or "SHET".'),
|
|
21
|
+
plant_type: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Case-insensitive exact match on Plant Type, for example "Solar" or "Energy Storage System".'),
|
|
25
|
+
project_status: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Case-insensitive exact match on Project Status, for example "Scoping" or "Awaiting Consents".'),
|
|
29
|
+
agreement_type: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Case-insensitive exact match on Agreement Type, for example "Embedded" or "Directly Connected".'),
|
|
33
|
+
limit: z
|
|
34
|
+
.number()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Maximum number of matched projects to return (default 20, max 50)."),
|
|
37
|
+
});
|
|
38
|
+
const DISCLAIMER = "This uses the public NESO Transmission Entry Capacity register as a GB transmission-level connection signal. " +
|
|
39
|
+
"It is not a DNO headroom map, queue-position guarantee, or connection offer. " +
|
|
40
|
+
"Connection site names reflect NESO register naming and may not match local substation labels exactly.";
|
|
41
|
+
export function resetGridConnectionQueueCacheForTests() {
|
|
42
|
+
cache.clear();
|
|
43
|
+
}
|
|
44
|
+
function normaliseText(value) {
|
|
45
|
+
return (value ?? "").trim().toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
function round1(value) {
|
|
48
|
+
return Math.round(value * 10) / 10;
|
|
49
|
+
}
|
|
50
|
+
function toProject(record) {
|
|
51
|
+
return {
|
|
52
|
+
project_name: record["Project Name"] ?? "Unknown project",
|
|
53
|
+
customer_name: record["Customer Name"] ?? null,
|
|
54
|
+
connection_site: record["Connection Site"] ?? "Unknown connection site",
|
|
55
|
+
stage: typeof record.Stage === "number" ? record.Stage : null,
|
|
56
|
+
mw_connected: typeof record["MW Connected"] === "number" ? record["MW Connected"] : 0,
|
|
57
|
+
mw_increase_decrease: typeof record["MW Increase / Decrease"] === "number" ? record["MW Increase / Decrease"] : 0,
|
|
58
|
+
cumulative_total_capacity_mw: typeof record["Cumulative Total Capacity (MW)"] === "number"
|
|
59
|
+
? record["Cumulative Total Capacity (MW)"]
|
|
60
|
+
: 0,
|
|
61
|
+
mw_effective_from: record["MW Effective From"] ?? null,
|
|
62
|
+
project_status: record["Project Status"] ?? null,
|
|
63
|
+
agreement_type: record["Agreement Type"] ?? null,
|
|
64
|
+
host_to: record["HOST TO"] ?? null,
|
|
65
|
+
plant_type: record["Plant Type"] ?? null,
|
|
66
|
+
project_id: record["Project ID"] ?? null,
|
|
67
|
+
project_number: record["Project Number"] ?? null,
|
|
68
|
+
gate: typeof record.Gate === "number" ? record.Gate : null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function fetchNesoTecRegister() {
|
|
72
|
+
const cached = cache.get("neso-tec-register:all");
|
|
73
|
+
if (cached)
|
|
74
|
+
return cached;
|
|
75
|
+
const response = await fetch(NESO_TEC_DATASTORE_URL);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`NESO API returned ${response.status}`);
|
|
78
|
+
}
|
|
79
|
+
const json = await response.json();
|
|
80
|
+
if (!json.success) {
|
|
81
|
+
throw new Error(json.error?.message ?? "NESO datastore request failed");
|
|
82
|
+
}
|
|
83
|
+
const records = Array.isArray(json.result?.records) ? json.result.records : [];
|
|
84
|
+
if (records.length > 0) {
|
|
85
|
+
guardJsonFields(records[0], [
|
|
86
|
+
"Project Name",
|
|
87
|
+
"Connection Site",
|
|
88
|
+
"MW Connected",
|
|
89
|
+
"MW Increase / Decrease",
|
|
90
|
+
"Cumulative Total Capacity (MW)",
|
|
91
|
+
"Project Status",
|
|
92
|
+
"HOST TO",
|
|
93
|
+
"Plant Type",
|
|
94
|
+
], "NESO TEC Register");
|
|
95
|
+
}
|
|
96
|
+
const projects = records.map(toProject);
|
|
97
|
+
cache.set("neso-tec-register:all", projects, TTL.STATIC_DATA);
|
|
98
|
+
return projects;
|
|
99
|
+
}
|
|
100
|
+
function matchesExactFilter(value, filter) {
|
|
101
|
+
if (!filter)
|
|
102
|
+
return true;
|
|
103
|
+
return normaliseText(value ?? undefined) === normaliseText(filter);
|
|
104
|
+
}
|
|
105
|
+
function matchesSubstring(value, query) {
|
|
106
|
+
if (!query)
|
|
107
|
+
return true;
|
|
108
|
+
return normaliseText(value ?? undefined).includes(normaliseText(query));
|
|
109
|
+
}
|
|
110
|
+
function summariseConnectionSites(projects) {
|
|
111
|
+
const grouped = new Map();
|
|
112
|
+
for (const project of projects) {
|
|
113
|
+
const existing = grouped.get(project.connection_site) ?? [];
|
|
114
|
+
existing.push(project);
|
|
115
|
+
grouped.set(project.connection_site, existing);
|
|
116
|
+
}
|
|
117
|
+
return [...grouped.entries()]
|
|
118
|
+
.map(([connectionSite, siteProjects]) => {
|
|
119
|
+
const dates = siteProjects
|
|
120
|
+
.map((p) => p.mw_effective_from)
|
|
121
|
+
.filter((d) => typeof d === "string" && d.length > 0)
|
|
122
|
+
.sort();
|
|
123
|
+
return {
|
|
124
|
+
connection_site: connectionSite,
|
|
125
|
+
project_count: siteProjects.length,
|
|
126
|
+
total_net_change_mw: round1(siteProjects.reduce((sum, p) => sum + p.mw_increase_decrease, 0)),
|
|
127
|
+
total_connected_mw: round1(siteProjects.reduce((sum, p) => sum + p.mw_connected, 0)),
|
|
128
|
+
total_cumulative_capacity_mw: round1(siteProjects.reduce((sum, p) => sum + p.cumulative_total_capacity_mw, 0)),
|
|
129
|
+
plant_types: [...new Set(siteProjects.map((p) => p.plant_type).filter((v) => Boolean(v)))].sort(),
|
|
130
|
+
project_statuses: [...new Set(siteProjects.map((p) => p.project_status).filter((v) => Boolean(v)))].sort(),
|
|
131
|
+
earliest_effective_from: dates[0] ?? null,
|
|
132
|
+
};
|
|
133
|
+
})
|
|
134
|
+
.sort((a, b) => b.total_net_change_mw - a.total_net_change_mw);
|
|
135
|
+
}
|
|
136
|
+
export async function getGridConnectionQueue(params) {
|
|
137
|
+
const limit = params.limit ?? 20;
|
|
138
|
+
if (limit <= 0 || limit > 50) {
|
|
139
|
+
throw new Error("limit must be between 1 and 50.");
|
|
140
|
+
}
|
|
141
|
+
if (!params.connection_site_query &&
|
|
142
|
+
!params.project_name_query &&
|
|
143
|
+
!params.host_to &&
|
|
144
|
+
!params.plant_type &&
|
|
145
|
+
!params.project_status &&
|
|
146
|
+
!params.agreement_type) {
|
|
147
|
+
throw new Error("At least one filter is required: connection_site_query, project_name_query, host_to, plant_type, project_status, or agreement_type.");
|
|
148
|
+
}
|
|
149
|
+
let projects;
|
|
150
|
+
try {
|
|
151
|
+
projects = await fetchNesoTecRegister();
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
throw new Error(`NESO TEC register query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
+
}
|
|
156
|
+
const matched = projects.filter((project) => {
|
|
157
|
+
return (matchesSubstring(project.connection_site, params.connection_site_query) &&
|
|
158
|
+
matchesSubstring(project.project_name, params.project_name_query) &&
|
|
159
|
+
matchesExactFilter(project.host_to, params.host_to) &&
|
|
160
|
+
matchesExactFilter(project.plant_type, params.plant_type) &&
|
|
161
|
+
matchesExactFilter(project.project_status, params.project_status) &&
|
|
162
|
+
matchesExactFilter(project.agreement_type, params.agreement_type));
|
|
163
|
+
});
|
|
164
|
+
matched.sort((a, b) => {
|
|
165
|
+
const dateA = a.mw_effective_from ?? "9999-12-31";
|
|
166
|
+
const dateB = b.mw_effective_from ?? "9999-12-31";
|
|
167
|
+
if (dateA !== dateB)
|
|
168
|
+
return dateA.localeCompare(dateB);
|
|
169
|
+
return b.mw_increase_decrease - a.mw_increase_decrease;
|
|
170
|
+
});
|
|
171
|
+
const dates = matched
|
|
172
|
+
.map((p) => p.mw_effective_from)
|
|
173
|
+
.filter((d) => typeof d === "string" && d.length > 0)
|
|
174
|
+
.sort();
|
|
175
|
+
return {
|
|
176
|
+
filters: {
|
|
177
|
+
connection_site_query: params.connection_site_query ?? null,
|
|
178
|
+
project_name_query: params.project_name_query ?? null,
|
|
179
|
+
host_to: params.host_to ?? null,
|
|
180
|
+
plant_type: params.plant_type ?? null,
|
|
181
|
+
project_status: params.project_status ?? null,
|
|
182
|
+
agreement_type: params.agreement_type ?? null,
|
|
183
|
+
},
|
|
184
|
+
summary: {
|
|
185
|
+
matched_projects: matched.length,
|
|
186
|
+
returned_projects: Math.min(matched.length, limit),
|
|
187
|
+
total_connected_mw: round1(matched.reduce((sum, p) => sum + p.mw_connected, 0)),
|
|
188
|
+
total_net_change_mw: round1(matched.reduce((sum, p) => sum + p.mw_increase_decrease, 0)),
|
|
189
|
+
total_cumulative_capacity_mw: round1(matched.reduce((sum, p) => sum + p.cumulative_total_capacity_mw, 0)),
|
|
190
|
+
earliest_effective_from: dates[0] ?? null,
|
|
191
|
+
latest_effective_from: dates[dates.length - 1] ?? null,
|
|
192
|
+
},
|
|
193
|
+
connection_sites: summariseConnectionSites(matched),
|
|
194
|
+
projects: matched.slice(0, limit),
|
|
195
|
+
source_metadata: GIS_SOURCES["neso-tec-register"],
|
|
196
|
+
disclaimer: DISCLAIMER,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type GisSourceMetadata } from "../lib/gis-sources.js";
|
|
3
|
+
export declare const gridProximitySchema: z.ZodObject<{
|
|
4
|
+
lat: z.ZodNumber;
|
|
5
|
+
lon: z.ZodNumber;
|
|
6
|
+
radius_km: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
voltage_min_kv: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
interface Substation {
|
|
10
|
+
name: string | null;
|
|
11
|
+
voltage_kv: number | null;
|
|
12
|
+
operator: string | null;
|
|
13
|
+
distance_km: number;
|
|
14
|
+
lat: number;
|
|
15
|
+
lon: number;
|
|
16
|
+
}
|
|
17
|
+
interface Line {
|
|
18
|
+
voltage_kv: number;
|
|
19
|
+
operator: string | null;
|
|
20
|
+
distance_km: number;
|
|
21
|
+
cables: number | null;
|
|
22
|
+
}
|
|
23
|
+
interface GridProximitySummary {
|
|
24
|
+
nearest_substation_km: number | null;
|
|
25
|
+
nearest_line_km: number | null;
|
|
26
|
+
max_nearby_voltage_kv: number | null;
|
|
27
|
+
}
|
|
28
|
+
interface GridProximityResult {
|
|
29
|
+
lat: number;
|
|
30
|
+
lon: number;
|
|
31
|
+
radius_km: number;
|
|
32
|
+
substations: Substation[];
|
|
33
|
+
lines: Line[];
|
|
34
|
+
summary: GridProximitySummary;
|
|
35
|
+
source_metadata: GisSourceMetadata;
|
|
36
|
+
}
|
|
37
|
+
export declare function getGridProximity(params: z.infer<typeof gridProximitySchema>): Promise<GridProximityResult>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TtlCache, TTL } from "../lib/cache.js";
|
|
3
|
+
import { fetchOverpassJson } from "../lib/overpass.js";
|
|
4
|
+
import { GIS_SOURCES } from "../lib/gis-sources.js";
|
|
5
|
+
const cache = new TtlCache();
|
|
6
|
+
export const gridProximitySchema = z.object({
|
|
7
|
+
lat: z
|
|
8
|
+
.number()
|
|
9
|
+
.describe("Latitude (-90 to 90). WGS84."),
|
|
10
|
+
lon: z
|
|
11
|
+
.number()
|
|
12
|
+
.describe("Longitude (-180 to 180). WGS84."),
|
|
13
|
+
radius_km: z
|
|
14
|
+
.number()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Search radius in km (default 5, max 25)."),
|
|
17
|
+
voltage_min_kv: z
|
|
18
|
+
.number()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Minimum voltage in kV to include (default 33)."),
|
|
21
|
+
});
|
|
22
|
+
function haversineKm(lat1, lon1, lat2, lon2) {
|
|
23
|
+
const R = 6371;
|
|
24
|
+
const dLat = (lat2 - lat1) * (Math.PI / 180);
|
|
25
|
+
const dLon = (lon2 - lon1) * (Math.PI / 180);
|
|
26
|
+
const a = Math.sin(dLat / 2) ** 2 +
|
|
27
|
+
Math.cos(lat1 * (Math.PI / 180)) *
|
|
28
|
+
Math.cos(lat2 * (Math.PI / 180)) *
|
|
29
|
+
Math.sin(dLon / 2) ** 2;
|
|
30
|
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
31
|
+
}
|
|
32
|
+
function parseVoltageKv(voltageStr) {
|
|
33
|
+
if (!voltageStr)
|
|
34
|
+
return null;
|
|
35
|
+
const v = Number(voltageStr.split(";")[0]);
|
|
36
|
+
if (Number.isNaN(v) || v <= 0)
|
|
37
|
+
return null;
|
|
38
|
+
return Math.round(v / 1000);
|
|
39
|
+
}
|
|
40
|
+
function minDistanceToWay(lat, lon, geometry) {
|
|
41
|
+
if (geometry.length === 0)
|
|
42
|
+
return Infinity;
|
|
43
|
+
let minDist = Infinity;
|
|
44
|
+
for (const pt of geometry) {
|
|
45
|
+
const d = haversineKm(lat, lon, pt.lat, pt.lon);
|
|
46
|
+
if (d < minDist)
|
|
47
|
+
minDist = d;
|
|
48
|
+
}
|
|
49
|
+
return minDist;
|
|
50
|
+
}
|
|
51
|
+
export async function getGridProximity(params) {
|
|
52
|
+
const { lat, lon } = params;
|
|
53
|
+
const radiusKm = params.radius_km ?? 5;
|
|
54
|
+
const voltageMinKv = params.voltage_min_kv ?? 33;
|
|
55
|
+
if (lat < -90 || lat > 90)
|
|
56
|
+
throw new Error("Latitude must be between -90 and 90.");
|
|
57
|
+
if (lon < -180 || lon > 180)
|
|
58
|
+
throw new Error("Longitude must be between -180 and 180.");
|
|
59
|
+
if (radiusKm <= 0 || radiusKm > 25)
|
|
60
|
+
throw new Error("radius_km must be between 0 and 25.");
|
|
61
|
+
const cacheKey = `grid-prox:${lat}:${lon}:${radiusKm}:${voltageMinKv}`;
|
|
62
|
+
const cached = cache.get(cacheKey);
|
|
63
|
+
if (cached)
|
|
64
|
+
return cached;
|
|
65
|
+
const radiusM = radiusKm * 1000;
|
|
66
|
+
const query = `[out:json][timeout:30];(
|
|
67
|
+
node["power"="substation"](around:${radiusM},${lat},${lon});
|
|
68
|
+
way["power"="line"](around:${radiusM},${lat},${lon});
|
|
69
|
+
);out geom;`;
|
|
70
|
+
const json = await fetchOverpassJson(query);
|
|
71
|
+
const elements = Array.isArray(json.elements) ? json.elements : [];
|
|
72
|
+
const substations = [];
|
|
73
|
+
const lines = [];
|
|
74
|
+
for (const el of elements) {
|
|
75
|
+
if (el.tags?.power === "substation" && el.type === "node") {
|
|
76
|
+
const vKv = parseVoltageKv(el.tags?.voltage);
|
|
77
|
+
substations.push({
|
|
78
|
+
name: el.tags?.name ?? null,
|
|
79
|
+
voltage_kv: vKv,
|
|
80
|
+
operator: el.tags?.operator ?? null,
|
|
81
|
+
distance_km: Math.round(haversineKm(lat, lon, el.lat, el.lon) * 100) / 100,
|
|
82
|
+
lat: el.lat,
|
|
83
|
+
lon: el.lon,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (el.tags?.power === "line" && el.type === "way") {
|
|
87
|
+
const vKv = parseVoltageKv(el.tags?.voltage);
|
|
88
|
+
if (vKv !== null && vKv < voltageMinKv)
|
|
89
|
+
continue;
|
|
90
|
+
const geometry = Array.isArray(el.geometry)
|
|
91
|
+
? el.geometry.map((g) => ({ lat: g.lat, lon: g.lon }))
|
|
92
|
+
: [];
|
|
93
|
+
lines.push({
|
|
94
|
+
voltage_kv: vKv ?? 0,
|
|
95
|
+
operator: el.tags?.operator ?? null,
|
|
96
|
+
distance_km: Math.round(minDistanceToWay(lat, lon, geometry) * 100) / 100,
|
|
97
|
+
cables: el.tags?.cables ? Number(el.tags.cables) : null,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
substations.sort((a, b) => a.distance_km - b.distance_km);
|
|
102
|
+
lines.sort((a, b) => a.distance_km - b.distance_km);
|
|
103
|
+
const allVoltages = [
|
|
104
|
+
...substations.map((s) => s.voltage_kv).filter((v) => v !== null),
|
|
105
|
+
...lines.map((l) => l.voltage_kv),
|
|
106
|
+
];
|
|
107
|
+
const summary = {
|
|
108
|
+
nearest_substation_km: substations.length > 0 ? substations[0].distance_km : null,
|
|
109
|
+
nearest_line_km: lines.length > 0 ? lines[0].distance_km : null,
|
|
110
|
+
max_nearby_voltage_kv: allVoltages.length > 0 ? Math.max(...allVoltages) : null,
|
|
111
|
+
};
|
|
112
|
+
const result = {
|
|
113
|
+
lat,
|
|
114
|
+
lon,
|
|
115
|
+
radius_km: radiusKm,
|
|
116
|
+
substations,
|
|
117
|
+
lines,
|
|
118
|
+
summary,
|
|
119
|
+
source_metadata: GIS_SOURCES["overpass-osm"],
|
|
120
|
+
};
|
|
121
|
+
cache.set(cacheKey, result, TTL.STATIC_DATA);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const hydroInflowsSchema: z.ZodObject<{
|
|
3
|
+
country: z.ZodString;
|
|
4
|
+
period: z.ZodOptional<z.ZodEnum<{
|
|
5
|
+
recent: "recent";
|
|
6
|
+
historical: "historical";
|
|
7
|
+
}>>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
interface DailyInflow {
|
|
10
|
+
date: string;
|
|
11
|
+
precipitation_mm: number;
|
|
12
|
+
snowfall_mm: number;
|
|
13
|
+
temperature_max_c: number;
|
|
14
|
+
rain_mm: number;
|
|
15
|
+
inflow_proxy_index: number;
|
|
16
|
+
}
|
|
17
|
+
interface HydroInflowResult {
|
|
18
|
+
source: string;
|
|
19
|
+
country: string;
|
|
20
|
+
basin_description: string;
|
|
21
|
+
latitude: number;
|
|
22
|
+
longitude: number;
|
|
23
|
+
period_days: number;
|
|
24
|
+
daily: DailyInflow[];
|
|
25
|
+
summary: {
|
|
26
|
+
total_precipitation_mm: number;
|
|
27
|
+
total_snowfall_mm: number;
|
|
28
|
+
avg_temperature_c: number;
|
|
29
|
+
avg_inflow_proxy: number;
|
|
30
|
+
trend: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare function getHydroInflows(params: z.infer<typeof hydroInflowsSchema>): Promise<HydroInflowResult>;
|
|
34
|
+
export {};
|