@dangahagan/weather-mcp 0.1.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 +414 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +515 -0
- package/dist/index.js.map +1 -0
- package/dist/services/noaa.d.ts +74 -0
- package/dist/services/noaa.d.ts.map +1 -0
- package/dist/services/noaa.js +296 -0
- package/dist/services/noaa.js.map +1 -0
- package/dist/services/openmeteo.d.ts +51 -0
- package/dist/services/openmeteo.d.ts.map +1 -0
- package/dist/services/openmeteo.js +298 -0
- package/dist/services/openmeteo.js.map +1 -0
- package/dist/types/noaa.d.ts +201 -0
- package/dist/types/noaa.d.ts.map +1 -0
- package/dist/types/noaa.js +5 -0
- package/dist/types/noaa.js.map +1 -0
- package/dist/types/openmeteo.d.ts +117 -0
- package/dist/types/openmeteo.d.ts.map +1 -0
- package/dist/types/openmeteo.js +6 -0
- package/dist/types/openmeteo.js.map +1 -0
- package/dist/utils/units.d.ts +69 -0
- package/dist/utils/units.d.ts.map +1 -0
- package/dist/utils/units.js +158 -0
- package/dist/utils/units.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for interacting with the NOAA Weather API
|
|
3
|
+
*/
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
export class NOAAService {
|
|
6
|
+
client;
|
|
7
|
+
maxRetries;
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
const { userAgent = '(weather-mcp, contact@example.com)', baseURL = 'https://api.weather.gov', timeout = 30000, maxRetries = 3 } = config;
|
|
10
|
+
this.maxRetries = maxRetries;
|
|
11
|
+
this.client = axios.create({
|
|
12
|
+
baseURL,
|
|
13
|
+
timeout,
|
|
14
|
+
headers: {
|
|
15
|
+
'User-Agent': userAgent,
|
|
16
|
+
'Accept': 'application/geo+json'
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
// Add response interceptor for error handling
|
|
20
|
+
this.client.interceptors.response.use(response => response, error => this.handleError(error));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Handle API errors with retry logic and helpful status information
|
|
24
|
+
*/
|
|
25
|
+
async handleError(error) {
|
|
26
|
+
if (error.response) {
|
|
27
|
+
const status = error.response.status;
|
|
28
|
+
const data = error.response.data;
|
|
29
|
+
// Rate limit error - suggest retry
|
|
30
|
+
if (status === 429) {
|
|
31
|
+
throw new Error(`NOAA API rate limit exceeded. Please retry in a few seconds.\n\n` +
|
|
32
|
+
`Details: ${data.detail || 'Too many requests'}\n\n` +
|
|
33
|
+
`For more information about rate limits, visit:\n` +
|
|
34
|
+
`https://weather-gov.github.io/api/`);
|
|
35
|
+
}
|
|
36
|
+
// Other client errors
|
|
37
|
+
if (status >= 400 && status < 500) {
|
|
38
|
+
let errorMsg = `NOAA API error: ${data.detail || data.title || 'Invalid request'}\n\n`;
|
|
39
|
+
// Add contextual help based on error type
|
|
40
|
+
if (status === 404) {
|
|
41
|
+
errorMsg += `This location may be outside NOAA's coverage area (US only).\n\n`;
|
|
42
|
+
}
|
|
43
|
+
errorMsg += `If this persists, check:\n` +
|
|
44
|
+
`- Planned outages: https://weather-gov.github.io/api/planned-outages\n` +
|
|
45
|
+
`- Service notices: https://www.weather.gov/notification\n` +
|
|
46
|
+
`- Report issues: https://weather-gov.github.io/api/reporting-issues`;
|
|
47
|
+
throw new Error(errorMsg);
|
|
48
|
+
}
|
|
49
|
+
// Server errors
|
|
50
|
+
if (status >= 500) {
|
|
51
|
+
throw new Error(`NOAA API server error: ${data.detail || 'Service temporarily unavailable'}\n\n` +
|
|
52
|
+
`The NOAA Weather API may be experiencing an outage.\n\n` +
|
|
53
|
+
`Check service status:\n` +
|
|
54
|
+
`- Planned outages: https://weather-gov.github.io/api/planned-outages\n` +
|
|
55
|
+
`- Service notices: https://www.weather.gov/notification\n` +
|
|
56
|
+
`- Report issues: nco.ops@noaa.gov or (301) 683-1518`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Network errors
|
|
60
|
+
if (error.code === 'ECONNABORTED') {
|
|
61
|
+
throw new Error(`Request to NOAA API timed out. Please try again.\n\n` +
|
|
62
|
+
`If timeouts persist, the service may be experiencing issues:\n` +
|
|
63
|
+
`https://weather-gov.github.io/api/planned-outages`);
|
|
64
|
+
}
|
|
65
|
+
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
66
|
+
throw new Error(`Unable to connect to NOAA API.\n\n` +
|
|
67
|
+
`Possible causes:\n` +
|
|
68
|
+
`- Internet connection issues\n` +
|
|
69
|
+
`- NOAA API service outage\n` +
|
|
70
|
+
`- DNS resolution problems\n\n` +
|
|
71
|
+
`Check:\n` +
|
|
72
|
+
`- Your internet connection\n` +
|
|
73
|
+
`- Service status: https://weather-gov.github.io/api/planned-outages`);
|
|
74
|
+
}
|
|
75
|
+
// Generic error
|
|
76
|
+
throw new Error(`NOAA API request failed: ${error.message}\n\n` +
|
|
77
|
+
`For assistance, visit:\n` +
|
|
78
|
+
`https://weather-gov.github.io/api/reporting-issues`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Make request with retry logic
|
|
82
|
+
*/
|
|
83
|
+
async makeRequest(url, retries = 0) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await this.client.get(url);
|
|
86
|
+
return response.data;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// Retry on rate limit or server errors
|
|
90
|
+
if (retries < this.maxRetries) {
|
|
91
|
+
const shouldRetry = error.message.includes('rate limit') ||
|
|
92
|
+
error.message.includes('server error') ||
|
|
93
|
+
error.message.includes('timed out');
|
|
94
|
+
if (shouldRetry) {
|
|
95
|
+
const delay = Math.pow(2, retries) * 1000; // Exponential backoff
|
|
96
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
97
|
+
return this.makeRequest(url, retries + 1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if the NOAA API is operational
|
|
105
|
+
* Performs a lightweight health check by requesting a well-known endpoint
|
|
106
|
+
* @returns Object with status information
|
|
107
|
+
*/
|
|
108
|
+
async checkServiceStatus() {
|
|
109
|
+
try {
|
|
110
|
+
// Use a simple, well-known location (US mainland center) for health check
|
|
111
|
+
const response = await this.client.get('/points/39.8283,-98.5795', {
|
|
112
|
+
timeout: 10000 // Shorter timeout for health check
|
|
113
|
+
});
|
|
114
|
+
if (response.status === 200) {
|
|
115
|
+
return {
|
|
116
|
+
operational: true,
|
|
117
|
+
message: 'NOAA Weather API is operational',
|
|
118
|
+
statusPage: 'https://weather-gov.github.io/api/planned-outages',
|
|
119
|
+
timestamp: new Date().toISOString()
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
operational: false,
|
|
124
|
+
message: `NOAA API returned unexpected status: ${response.status}`,
|
|
125
|
+
statusPage: 'https://weather-gov.github.io/api/planned-outages',
|
|
126
|
+
timestamp: new Date().toISOString()
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const axiosError = error;
|
|
131
|
+
let message = 'NOAA Weather API may be experiencing issues';
|
|
132
|
+
let operational = false;
|
|
133
|
+
if (axiosError.response) {
|
|
134
|
+
const status = axiosError.response.status;
|
|
135
|
+
if (status === 429) {
|
|
136
|
+
operational = true; // API is up, just rate limited
|
|
137
|
+
message = 'NOAA API is operational but rate limited';
|
|
138
|
+
}
|
|
139
|
+
else if (status >= 500) {
|
|
140
|
+
message = 'NOAA API is experiencing server errors (possible outage)';
|
|
141
|
+
}
|
|
142
|
+
else if (status === 404) {
|
|
143
|
+
operational = true; // 404 on this endpoint might just mean API change
|
|
144
|
+
message = 'NOAA API is responding (health check endpoint may have changed)';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (axiosError.code === 'ECONNABORTED') {
|
|
148
|
+
message = 'NOAA API is not responding (timeout)';
|
|
149
|
+
}
|
|
150
|
+
else if (axiosError.code === 'ENOTFOUND' || axiosError.code === 'ECONNREFUSED') {
|
|
151
|
+
message = 'Cannot connect to NOAA API (DNS or connection failure)';
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
operational,
|
|
155
|
+
message,
|
|
156
|
+
statusPage: 'https://weather-gov.github.io/api/planned-outages',
|
|
157
|
+
timestamp: new Date().toISOString()
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Convert lat/lon coordinates to NWS grid information
|
|
163
|
+
* This is the first step for getting forecast or observation data
|
|
164
|
+
*/
|
|
165
|
+
async getPointData(latitude, longitude) {
|
|
166
|
+
// Validate coordinates
|
|
167
|
+
if (latitude < -90 || latitude > 90) {
|
|
168
|
+
throw new Error(`Invalid latitude: ${latitude}. Must be between -90 and 90.`);
|
|
169
|
+
}
|
|
170
|
+
if (longitude < -180 || longitude > 180) {
|
|
171
|
+
throw new Error(`Invalid longitude: ${longitude}. Must be between -180 and 180.`);
|
|
172
|
+
}
|
|
173
|
+
const url = `/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
|
|
174
|
+
return this.makeRequest(url);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get forecast for a location using grid coordinates
|
|
178
|
+
*/
|
|
179
|
+
async getForecast(office, gridX, gridY) {
|
|
180
|
+
const url = `/gridpoints/${office}/${gridX},${gridY}/forecast`;
|
|
181
|
+
return this.makeRequest(url);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get hourly forecast for a location using grid coordinates
|
|
185
|
+
*/
|
|
186
|
+
async getHourlyForecast(office, gridX, gridY) {
|
|
187
|
+
const url = `/gridpoints/${office}/${gridX},${gridY}/forecast/hourly`;
|
|
188
|
+
return this.makeRequest(url);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get forecast for a location using lat/lon (convenience method)
|
|
192
|
+
* This combines getPointData and getForecast
|
|
193
|
+
*/
|
|
194
|
+
async getForecastByCoordinates(latitude, longitude) {
|
|
195
|
+
const pointData = await this.getPointData(latitude, longitude);
|
|
196
|
+
const { gridId, gridX, gridY } = pointData.properties;
|
|
197
|
+
return this.getForecast(gridId, gridX, gridY);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get nearest observation stations for a location
|
|
201
|
+
*/
|
|
202
|
+
async getStations(latitude, longitude) {
|
|
203
|
+
const url = `/points/${latitude.toFixed(4)},${longitude.toFixed(4)}/stations`;
|
|
204
|
+
return this.makeRequest(url);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get the latest observation from a station
|
|
208
|
+
*/
|
|
209
|
+
async getLatestObservation(stationId) {
|
|
210
|
+
const url = `/stations/${stationId}/observations/latest`;
|
|
211
|
+
return this.makeRequest(url);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get observations from a station within a time range
|
|
215
|
+
*/
|
|
216
|
+
async getObservations(stationId, startTime, endTime, limit) {
|
|
217
|
+
// Validate date range if both dates are provided
|
|
218
|
+
if (startTime && endTime) {
|
|
219
|
+
if (startTime > endTime) {
|
|
220
|
+
throw new Error(`Invalid date range: start date (${startTime.toISOString()}) must be before end date (${endTime.toISOString()})`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Validate dates are not in the future
|
|
224
|
+
const now = new Date();
|
|
225
|
+
if (startTime && startTime > now) {
|
|
226
|
+
throw new Error(`Start date (${startTime.toISOString()}) cannot be in the future`);
|
|
227
|
+
}
|
|
228
|
+
if (endTime && endTime > now) {
|
|
229
|
+
throw new Error(`End date (${endTime.toISOString()}) cannot be in the future`);
|
|
230
|
+
}
|
|
231
|
+
let url = `/stations/${stationId}/observations`;
|
|
232
|
+
const params = new URLSearchParams();
|
|
233
|
+
if (startTime) {
|
|
234
|
+
params.append('start', startTime.toISOString());
|
|
235
|
+
}
|
|
236
|
+
if (endTime) {
|
|
237
|
+
params.append('end', endTime.toISOString());
|
|
238
|
+
}
|
|
239
|
+
if (limit) {
|
|
240
|
+
// Ensure limit is between 1 and 500
|
|
241
|
+
const validLimit = Math.max(1, Math.min(limit, 500));
|
|
242
|
+
params.append('limit', validLimit.toString());
|
|
243
|
+
}
|
|
244
|
+
if (params.toString()) {
|
|
245
|
+
url += `?${params.toString()}`;
|
|
246
|
+
}
|
|
247
|
+
return this.makeRequest(url);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get current conditions for a location (convenience method)
|
|
251
|
+
* This combines getStations and getLatestObservation
|
|
252
|
+
*/
|
|
253
|
+
async getCurrentConditions(latitude, longitude) {
|
|
254
|
+
const stations = await this.getStations(latitude, longitude);
|
|
255
|
+
if (!stations.features || stations.features.length === 0) {
|
|
256
|
+
throw new Error('No weather stations found near the specified location.');
|
|
257
|
+
}
|
|
258
|
+
// Try the first station, fallback to others if it fails
|
|
259
|
+
for (const station of stations.features) {
|
|
260
|
+
try {
|
|
261
|
+
const stationId = station.properties.stationIdentifier;
|
|
262
|
+
return await this.getLatestObservation(stationId);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
// Try next station
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw new Error('Unable to retrieve current conditions from nearby stations.');
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get historical observations for a location (convenience method)
|
|
273
|
+
*/
|
|
274
|
+
async getHistoricalObservations(latitude, longitude, startTime, endTime, limit) {
|
|
275
|
+
// Validate date range
|
|
276
|
+
if (startTime > endTime) {
|
|
277
|
+
throw new Error(`Invalid date range: start date (${startTime.toISOString()}) must be before end date (${endTime.toISOString()})`);
|
|
278
|
+
}
|
|
279
|
+
// Validate dates are not in the future
|
|
280
|
+
const now = new Date();
|
|
281
|
+
if (startTime > now) {
|
|
282
|
+
throw new Error(`Start date (${startTime.toISOString()}) cannot be in the future`);
|
|
283
|
+
}
|
|
284
|
+
if (endTime > now) {
|
|
285
|
+
throw new Error(`End date (${endTime.toISOString()}) cannot be in the future`);
|
|
286
|
+
}
|
|
287
|
+
const stations = await this.getStations(latitude, longitude);
|
|
288
|
+
if (!stations.features || stations.features.length === 0) {
|
|
289
|
+
throw new Error('No weather stations found near the specified location.');
|
|
290
|
+
}
|
|
291
|
+
// Get observations from the nearest station
|
|
292
|
+
const stationId = stations.features[0].properties.stationIdentifier;
|
|
293
|
+
return this.getObservations(stationId, startTime, endTime, limit);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=noaa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noaa.js","sourceRoot":"","sources":["../../src/services/noaa.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for interacting with the Open-Meteo Historical Weather API
|
|
3
|
+
* Documentation: https://open-meteo.com/en/docs/historical-weather-api
|
|
4
|
+
*/
|
|
5
|
+
import type { OpenMeteoHistoricalResponse } from '../types/openmeteo.js';
|
|
6
|
+
export interface OpenMeteoServiceConfig {
|
|
7
|
+
baseURL?: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class OpenMeteoService {
|
|
12
|
+
private client;
|
|
13
|
+
private maxRetries;
|
|
14
|
+
constructor(config?: OpenMeteoServiceConfig);
|
|
15
|
+
/**
|
|
16
|
+
* Handle API errors with helpful status information
|
|
17
|
+
*/
|
|
18
|
+
private handleError;
|
|
19
|
+
/**
|
|
20
|
+
* Make request with retry logic
|
|
21
|
+
*/
|
|
22
|
+
private makeRequest;
|
|
23
|
+
/**
|
|
24
|
+
* Check if the Open-Meteo API is operational
|
|
25
|
+
* Performs a lightweight health check by requesting a simple query
|
|
26
|
+
* @returns Object with status information
|
|
27
|
+
*/
|
|
28
|
+
checkServiceStatus(): Promise<{
|
|
29
|
+
operational: boolean;
|
|
30
|
+
message: string;
|
|
31
|
+
statusPage: string;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Get historical weather data for a location
|
|
36
|
+
*
|
|
37
|
+
* @param latitude - Latitude coordinate (-90 to 90)
|
|
38
|
+
* @param longitude - Longitude coordinate (-180 to 180)
|
|
39
|
+
* @param startDate - Start date in ISO format (YYYY-MM-DD)
|
|
40
|
+
* @param endDate - End date in ISO format (YYYY-MM-DD)
|
|
41
|
+
* @param useHourly - Whether to request hourly data (default: true)
|
|
42
|
+
* @returns Historical weather data
|
|
43
|
+
*/
|
|
44
|
+
getHistoricalWeather(latitude: number, longitude: number, startDate: string, endDate: string, useHourly?: boolean): Promise<OpenMeteoHistoricalResponse>;
|
|
45
|
+
/**
|
|
46
|
+
* Get weather description from WMO weather code
|
|
47
|
+
* WMO Weather interpretation codes (WW): https://open-meteo.com/en/docs
|
|
48
|
+
*/
|
|
49
|
+
getWeatherDescription(code: number): string;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=openmeteo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openmeteo.d.ts","sourceRoot":"","sources":["../../src/services/openmeteo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,2BAA2B,EAE5B,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,UAAU,CAAS;gBAEf,MAAM,GAAE,sBAA2B;IAyB/C;;OAEG;YACW,WAAW;IAkFzB;;OAEG;YACW,WAAW;IA0BzB;;;;OAIG;IACG,kBAAkB,IAAI,OAAO,CAAC;QAClC,WAAW,EAAE,OAAO,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IAkEF;;;;;;;;;OASG;IACG,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,OAAc,GACxB,OAAO,CAAC,2BAA2B,CAAC;IAoFvC;;;OAGG;IACH,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAkC5C"}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for interacting with the Open-Meteo Historical Weather API
|
|
3
|
+
* Documentation: https://open-meteo.com/en/docs/historical-weather-api
|
|
4
|
+
*/
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
export class OpenMeteoService {
|
|
7
|
+
client;
|
|
8
|
+
maxRetries;
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
const { baseURL = 'https://archive-api.open-meteo.com/v1', timeout = 30000, maxRetries = 3 } = config;
|
|
11
|
+
this.maxRetries = maxRetries;
|
|
12
|
+
this.client = axios.create({
|
|
13
|
+
baseURL,
|
|
14
|
+
timeout,
|
|
15
|
+
headers: {
|
|
16
|
+
'Accept': 'application/json',
|
|
17
|
+
'User-Agent': 'weather-mcp/0.1.0'
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
// Add response interceptor for error handling
|
|
21
|
+
this.client.interceptors.response.use(response => response, error => this.handleError(error));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Handle API errors with helpful status information
|
|
25
|
+
*/
|
|
26
|
+
async handleError(error) {
|
|
27
|
+
if (error.response) {
|
|
28
|
+
const status = error.response.status;
|
|
29
|
+
const data = error.response.data;
|
|
30
|
+
// Bad request
|
|
31
|
+
if (status === 400) {
|
|
32
|
+
const reason = data.reason || 'Invalid request parameters';
|
|
33
|
+
throw new Error(`Open-Meteo API error: ${reason}\n\n` +
|
|
34
|
+
`Please verify:\n` +
|
|
35
|
+
`- Coordinates are valid (latitude: -90 to 90, longitude: -180 to 180)\n` +
|
|
36
|
+
`- Date range is valid (1940 to 5 days ago)\n` +
|
|
37
|
+
`- Parameters are correctly formatted\n\n` +
|
|
38
|
+
`API documentation: https://open-meteo.com/en/docs/historical-weather-api`);
|
|
39
|
+
}
|
|
40
|
+
// Rate limit error
|
|
41
|
+
if (status === 429) {
|
|
42
|
+
throw new Error(`Open-Meteo API rate limit exceeded (10,000 requests/day for non-commercial use).\n\n` +
|
|
43
|
+
`Please retry later or consider:\n` +
|
|
44
|
+
`- Reducing request frequency\n` +
|
|
45
|
+
`- Using daily instead of hourly data for longer periods\n` +
|
|
46
|
+
`- Upgrading to a commercial plan for higher limits\n\n` +
|
|
47
|
+
`More info: https://open-meteo.com/en/pricing`);
|
|
48
|
+
}
|
|
49
|
+
// Server errors
|
|
50
|
+
if (status >= 500) {
|
|
51
|
+
throw new Error(`Open-Meteo API server error: Service temporarily unavailable\n\n` +
|
|
52
|
+
`The Open-Meteo API may be experiencing an outage.\n\n` +
|
|
53
|
+
`Check service status:\n` +
|
|
54
|
+
`- Production status: https://open-meteo.com/en/docs/model-updates\n` +
|
|
55
|
+
`- GitHub issues: https://github.com/open-meteo/open-meteo/issues`);
|
|
56
|
+
}
|
|
57
|
+
// Other errors
|
|
58
|
+
throw new Error(`Open-Meteo API error (${status}): ${data.reason || 'Request failed'}\n\n` +
|
|
59
|
+
`For assistance, visit:\n` +
|
|
60
|
+
`- API documentation: https://open-meteo.com/en/docs\n` +
|
|
61
|
+
`- GitHub issues: https://github.com/open-meteo/open-meteo/issues`);
|
|
62
|
+
}
|
|
63
|
+
// Network errors
|
|
64
|
+
if (error.code === 'ECONNABORTED') {
|
|
65
|
+
throw new Error(`Request to Open-Meteo API timed out. Please try again.\n\n` +
|
|
66
|
+
`If timeouts persist, check:\n` +
|
|
67
|
+
`- Your internet connection\n` +
|
|
68
|
+
`- Service status: https://open-meteo.com/en/docs/model-updates`);
|
|
69
|
+
}
|
|
70
|
+
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
71
|
+
throw new Error(`Unable to connect to Open-Meteo API.\n\n` +
|
|
72
|
+
`Possible causes:\n` +
|
|
73
|
+
`- Internet connection issues\n` +
|
|
74
|
+
`- Open-Meteo API service outage\n` +
|
|
75
|
+
`- DNS resolution problems\n\n` +
|
|
76
|
+
`Check:\n` +
|
|
77
|
+
`- Your internet connection\n` +
|
|
78
|
+
`- Service status: https://open-meteo.com/en/docs/model-updates`);
|
|
79
|
+
}
|
|
80
|
+
// Generic error
|
|
81
|
+
throw new Error(`Open-Meteo API request failed: ${error.message}\n\n` +
|
|
82
|
+
`For assistance, visit:\n` +
|
|
83
|
+
`- GitHub issues: https://github.com/open-meteo/open-meteo/issues\n` +
|
|
84
|
+
`- Documentation: https://open-meteo.com/en/docs`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Make request with retry logic
|
|
88
|
+
*/
|
|
89
|
+
async makeRequest(url, params, retries = 0) {
|
|
90
|
+
try {
|
|
91
|
+
const response = await this.client.get(url, { params });
|
|
92
|
+
return response.data;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Retry on rate limit or server errors
|
|
96
|
+
if (retries < this.maxRetries) {
|
|
97
|
+
const shouldRetry = error.message.includes('rate limit') ||
|
|
98
|
+
error.message.includes('server error') ||
|
|
99
|
+
error.message.includes('timed out');
|
|
100
|
+
if (shouldRetry) {
|
|
101
|
+
const delay = Math.pow(2, retries) * 1000; // Exponential backoff
|
|
102
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
103
|
+
return this.makeRequest(url, params, retries + 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if the Open-Meteo API is operational
|
|
111
|
+
* Performs a lightweight health check by requesting a simple query
|
|
112
|
+
* @returns Object with status information
|
|
113
|
+
*/
|
|
114
|
+
async checkServiceStatus() {
|
|
115
|
+
try {
|
|
116
|
+
// Use a simple request for a recent date at a known location (London, UK)
|
|
117
|
+
// Using a 1-day range from 30 days ago to avoid the 5-day delay issue
|
|
118
|
+
const testDate = new Date();
|
|
119
|
+
testDate.setDate(testDate.getDate() - 30);
|
|
120
|
+
const dateStr = testDate.toISOString().split('T')[0];
|
|
121
|
+
const response = await this.client.get('/archive', {
|
|
122
|
+
params: {
|
|
123
|
+
latitude: 51.5074,
|
|
124
|
+
longitude: -0.1278,
|
|
125
|
+
start_date: dateStr,
|
|
126
|
+
end_date: dateStr,
|
|
127
|
+
daily: 'temperature_2m_max',
|
|
128
|
+
timezone: 'UTC'
|
|
129
|
+
},
|
|
130
|
+
timeout: 10000 // Shorter timeout for health check
|
|
131
|
+
});
|
|
132
|
+
if (response.status === 200 && response.data) {
|
|
133
|
+
return {
|
|
134
|
+
operational: true,
|
|
135
|
+
message: 'Open-Meteo API is operational',
|
|
136
|
+
statusPage: 'https://open-meteo.com/en/docs/model-updates',
|
|
137
|
+
timestamp: new Date().toISOString()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
operational: false,
|
|
142
|
+
message: `Open-Meteo API returned unexpected status: ${response.status}`,
|
|
143
|
+
statusPage: 'https://open-meteo.com/en/docs/model-updates',
|
|
144
|
+
timestamp: new Date().toISOString()
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const axiosError = error;
|
|
149
|
+
let message = 'Open-Meteo API may be experiencing issues';
|
|
150
|
+
let operational = false;
|
|
151
|
+
if (axiosError.response) {
|
|
152
|
+
const status = axiosError.response.status;
|
|
153
|
+
if (status === 429) {
|
|
154
|
+
operational = true; // API is up, just rate limited
|
|
155
|
+
message = 'Open-Meteo API is operational but rate limited';
|
|
156
|
+
}
|
|
157
|
+
else if (status >= 500) {
|
|
158
|
+
message = 'Open-Meteo API is experiencing server errors (possible outage)';
|
|
159
|
+
}
|
|
160
|
+
else if (status === 400) {
|
|
161
|
+
operational = true; // Bad request might indicate API is up but our test is wrong
|
|
162
|
+
message = 'Open-Meteo API is responding (health check may need adjustment)';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (axiosError.code === 'ECONNABORTED') {
|
|
166
|
+
message = 'Open-Meteo API is not responding (timeout)';
|
|
167
|
+
}
|
|
168
|
+
else if (axiosError.code === 'ENOTFOUND' || axiosError.code === 'ECONNREFUSED') {
|
|
169
|
+
message = 'Cannot connect to Open-Meteo API (DNS or connection failure)';
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
operational,
|
|
173
|
+
message,
|
|
174
|
+
statusPage: 'https://open-meteo.com/en/docs/model-updates',
|
|
175
|
+
timestamp: new Date().toISOString()
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get historical weather data for a location
|
|
181
|
+
*
|
|
182
|
+
* @param latitude - Latitude coordinate (-90 to 90)
|
|
183
|
+
* @param longitude - Longitude coordinate (-180 to 180)
|
|
184
|
+
* @param startDate - Start date in ISO format (YYYY-MM-DD)
|
|
185
|
+
* @param endDate - End date in ISO format (YYYY-MM-DD)
|
|
186
|
+
* @param useHourly - Whether to request hourly data (default: true)
|
|
187
|
+
* @returns Historical weather data
|
|
188
|
+
*/
|
|
189
|
+
async getHistoricalWeather(latitude, longitude, startDate, endDate, useHourly = true) {
|
|
190
|
+
// Validate coordinates
|
|
191
|
+
if (latitude < -90 || latitude > 90) {
|
|
192
|
+
throw new Error(`Invalid latitude: ${latitude}. Must be between -90 and 90.`);
|
|
193
|
+
}
|
|
194
|
+
if (longitude < -180 || longitude > 180) {
|
|
195
|
+
throw new Error(`Invalid longitude: ${longitude}. Must be between -180 and 180.`);
|
|
196
|
+
}
|
|
197
|
+
// Build request parameters
|
|
198
|
+
const params = {
|
|
199
|
+
latitude,
|
|
200
|
+
longitude,
|
|
201
|
+
start_date: startDate,
|
|
202
|
+
end_date: endDate,
|
|
203
|
+
temperature_unit: 'fahrenheit',
|
|
204
|
+
wind_speed_unit: 'mph',
|
|
205
|
+
precipitation_unit: 'inch',
|
|
206
|
+
timezone: 'auto'
|
|
207
|
+
};
|
|
208
|
+
// Request appropriate data granularity
|
|
209
|
+
if (useHourly) {
|
|
210
|
+
// Hourly data for detailed observations
|
|
211
|
+
params.hourly = [
|
|
212
|
+
'temperature_2m',
|
|
213
|
+
'relative_humidity_2m',
|
|
214
|
+
'dewpoint_2m',
|
|
215
|
+
'apparent_temperature',
|
|
216
|
+
'precipitation',
|
|
217
|
+
'rain',
|
|
218
|
+
'snowfall',
|
|
219
|
+
'weather_code',
|
|
220
|
+
'pressure_msl',
|
|
221
|
+
'cloud_cover',
|
|
222
|
+
'wind_speed_10m',
|
|
223
|
+
'wind_direction_10m',
|
|
224
|
+
'wind_gusts_10m'
|
|
225
|
+
].join(',');
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Daily summaries for longer time periods
|
|
229
|
+
params.daily = [
|
|
230
|
+
'temperature_2m_max',
|
|
231
|
+
'temperature_2m_min',
|
|
232
|
+
'temperature_2m_mean',
|
|
233
|
+
'apparent_temperature_max',
|
|
234
|
+
'apparent_temperature_min',
|
|
235
|
+
'precipitation_sum',
|
|
236
|
+
'rain_sum',
|
|
237
|
+
'snowfall_sum',
|
|
238
|
+
'precipitation_hours',
|
|
239
|
+
'weather_code',
|
|
240
|
+
'wind_speed_10m_max',
|
|
241
|
+
'wind_gusts_10m_max',
|
|
242
|
+
'wind_direction_10m_dominant'
|
|
243
|
+
].join(',');
|
|
244
|
+
}
|
|
245
|
+
const response = await this.makeRequest('/archive', params);
|
|
246
|
+
// Validate response has data
|
|
247
|
+
if (useHourly && (!response.hourly || !response.hourly.time || response.hourly.time.length === 0)) {
|
|
248
|
+
throw new Error(`No historical weather data available for the specified date range (${startDate} to ${endDate}).\n\n` +
|
|
249
|
+
'This may occur because:\n' +
|
|
250
|
+
'- The dates are too recent (data has a 5-day delay for most models)\n' +
|
|
251
|
+
'- The dates are before 1940 (earliest available data)\n\n' +
|
|
252
|
+
'Please try adjusting your date range.');
|
|
253
|
+
}
|
|
254
|
+
if (!useHourly && (!response.daily || !response.daily.time || response.daily.time.length === 0)) {
|
|
255
|
+
throw new Error(`No historical weather data available for the specified date range (${startDate} to ${endDate}).\n\n` +
|
|
256
|
+
'Please try adjusting your date range.');
|
|
257
|
+
}
|
|
258
|
+
return response;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get weather description from WMO weather code
|
|
262
|
+
* WMO Weather interpretation codes (WW): https://open-meteo.com/en/docs
|
|
263
|
+
*/
|
|
264
|
+
getWeatherDescription(code) {
|
|
265
|
+
const weatherCodes = {
|
|
266
|
+
0: 'Clear sky',
|
|
267
|
+
1: 'Mainly clear',
|
|
268
|
+
2: 'Partly cloudy',
|
|
269
|
+
3: 'Overcast',
|
|
270
|
+
45: 'Foggy',
|
|
271
|
+
48: 'Depositing rime fog',
|
|
272
|
+
51: 'Light drizzle',
|
|
273
|
+
53: 'Moderate drizzle',
|
|
274
|
+
55: 'Dense drizzle',
|
|
275
|
+
56: 'Light freezing drizzle',
|
|
276
|
+
57: 'Dense freezing drizzle',
|
|
277
|
+
61: 'Slight rain',
|
|
278
|
+
63: 'Moderate rain',
|
|
279
|
+
65: 'Heavy rain',
|
|
280
|
+
66: 'Light freezing rain',
|
|
281
|
+
67: 'Heavy freezing rain',
|
|
282
|
+
71: 'Slight snow',
|
|
283
|
+
73: 'Moderate snow',
|
|
284
|
+
75: 'Heavy snow',
|
|
285
|
+
77: 'Snow grains',
|
|
286
|
+
80: 'Slight rain showers',
|
|
287
|
+
81: 'Moderate rain showers',
|
|
288
|
+
82: 'Violent rain showers',
|
|
289
|
+
85: 'Slight snow showers',
|
|
290
|
+
86: 'Heavy snow showers',
|
|
291
|
+
95: 'Thunderstorm',
|
|
292
|
+
96: 'Thunderstorm with slight hail',
|
|
293
|
+
99: 'Thunderstorm with heavy hail'
|
|
294
|
+
};
|
|
295
|
+
return weatherCodes[code] || `Unknown (code: ${code})`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=openmeteo.js.map
|