@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.
@@ -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