@dangahagan/weather-mcp 0.2.0 → 1.0.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 (88) hide show
  1. package/README.md +372 -53
  2. package/dist/config/cache.d.ts +2 -0
  3. package/dist/config/cache.d.ts.map +1 -1
  4. package/dist/config/cache.js +48 -2
  5. package/dist/config/cache.js.map +1 -1
  6. package/dist/config/displayThresholds.d.ts +83 -0
  7. package/dist/config/displayThresholds.d.ts.map +1 -0
  8. package/dist/config/displayThresholds.js +83 -0
  9. package/dist/config/displayThresholds.js.map +1 -0
  10. package/dist/errors/ApiError.d.ts +62 -0
  11. package/dist/errors/ApiError.d.ts.map +1 -0
  12. package/dist/errors/ApiError.js +147 -0
  13. package/dist/errors/ApiError.js.map +1 -0
  14. package/dist/handlers/airQualityHandler.d.ts +11 -0
  15. package/dist/handlers/airQualityHandler.d.ts.map +1 -0
  16. package/dist/handlers/airQualityHandler.js +154 -0
  17. package/dist/handlers/airQualityHandler.js.map +1 -0
  18. package/dist/handlers/alertsHandler.d.ts +11 -0
  19. package/dist/handlers/alertsHandler.d.ts.map +1 -0
  20. package/dist/handlers/alertsHandler.js +82 -0
  21. package/dist/handlers/alertsHandler.js.map +1 -0
  22. package/dist/handlers/currentConditionsHandler.d.ts +11 -0
  23. package/dist/handlers/currentConditionsHandler.d.ts.map +1 -0
  24. package/dist/handlers/currentConditionsHandler.js +231 -0
  25. package/dist/handlers/currentConditionsHandler.js.map +1 -0
  26. package/dist/handlers/forecastHandler.d.ts +13 -0
  27. package/dist/handlers/forecastHandler.d.ts.map +1 -0
  28. package/dist/handlers/forecastHandler.js +337 -0
  29. package/dist/handlers/forecastHandler.js.map +1 -0
  30. package/dist/handlers/historicalWeatherHandler.d.ts +12 -0
  31. package/dist/handlers/historicalWeatherHandler.d.ts.map +1 -0
  32. package/dist/handlers/historicalWeatherHandler.js +188 -0
  33. package/dist/handlers/historicalWeatherHandler.js.map +1 -0
  34. package/dist/handlers/locationHandler.d.ts +11 -0
  35. package/dist/handlers/locationHandler.d.ts.map +1 -0
  36. package/dist/handlers/locationHandler.js +113 -0
  37. package/dist/handlers/locationHandler.js.map +1 -0
  38. package/dist/handlers/marineConditionsHandler.d.ts +11 -0
  39. package/dist/handlers/marineConditionsHandler.d.ts.map +1 -0
  40. package/dist/handlers/marineConditionsHandler.js +154 -0
  41. package/dist/handlers/marineConditionsHandler.js.map +1 -0
  42. package/dist/handlers/statusHandler.d.ts +12 -0
  43. package/dist/handlers/statusHandler.d.ts.map +1 -0
  44. package/dist/handlers/statusHandler.js +106 -0
  45. package/dist/handlers/statusHandler.js.map +1 -0
  46. package/dist/index.js +219 -377
  47. package/dist/index.js.map +1 -1
  48. package/dist/services/noaa.d.ts +24 -1
  49. package/dist/services/noaa.d.ts.map +1 -1
  50. package/dist/services/noaa.js +116 -43
  51. package/dist/services/noaa.js.map +1 -1
  52. package/dist/services/openmeteo.d.ts +110 -3
  53. package/dist/services/openmeteo.d.ts.map +1 -1
  54. package/dist/services/openmeteo.js +568 -112
  55. package/dist/services/openmeteo.js.map +1 -1
  56. package/dist/types/noaa.d.ts +158 -12
  57. package/dist/types/noaa.d.ts.map +1 -1
  58. package/dist/types/openmeteo.d.ts +448 -0
  59. package/dist/types/openmeteo.d.ts.map +1 -1
  60. package/dist/utils/airQuality.d.ts +54 -0
  61. package/dist/utils/airQuality.d.ts.map +1 -0
  62. package/dist/utils/airQuality.js +251 -0
  63. package/dist/utils/airQuality.js.map +1 -0
  64. package/dist/utils/cache.d.ts +11 -4
  65. package/dist/utils/cache.d.ts.map +1 -1
  66. package/dist/utils/cache.js +33 -4
  67. package/dist/utils/cache.js.map +1 -1
  68. package/dist/utils/fireWeather.d.ts +62 -0
  69. package/dist/utils/fireWeather.d.ts.map +1 -0
  70. package/dist/utils/fireWeather.js +168 -0
  71. package/dist/utils/fireWeather.js.map +1 -0
  72. package/dist/utils/logger.d.ts +59 -0
  73. package/dist/utils/logger.d.ts.map +1 -0
  74. package/dist/utils/logger.js +128 -0
  75. package/dist/utils/logger.js.map +1 -0
  76. package/dist/utils/marine.d.ts +38 -0
  77. package/dist/utils/marine.d.ts.map +1 -0
  78. package/dist/utils/marine.js +162 -0
  79. package/dist/utils/marine.js.map +1 -0
  80. package/dist/utils/temperatureConversion.d.ts +12 -0
  81. package/dist/utils/temperatureConversion.d.ts.map +1 -0
  82. package/dist/utils/temperatureConversion.js +17 -0
  83. package/dist/utils/temperatureConversion.js.map +1 -0
  84. package/dist/utils/validation.d.ts +89 -0
  85. package/dist/utils/validation.d.ts.map +1 -0
  86. package/dist/utils/validation.js +177 -0
  87. package/dist/utils/validation.js.map +1 -0
  88. package/package.json +10 -3
@@ -1,28 +1,81 @@
1
1
  /**
2
- * Service for interacting with the Open-Meteo Historical Weather API
3
- * Documentation: https://open-meteo.com/en/docs/historical-weather-api
2
+ * Service for interacting with the Open-Meteo APIs
3
+ * Documentation:
4
+ * - Historical Weather: https://open-meteo.com/en/docs/historical-weather-api
5
+ * - Forecast: https://open-meteo.com/en/docs
6
+ * - Geocoding: https://open-meteo.com/en/docs/geocoding-api
7
+ * - Air Quality: https://open-meteo.com/en/docs/air-quality-api
8
+ * - Marine: https://open-meteo.com/en/docs/marine-weather-api
4
9
  */
5
10
  import axios from 'axios';
6
11
  import { Cache } from '../utils/cache.js';
7
12
  import { CacheConfig, getHistoricalDataTTL } from '../config/cache.js';
13
+ import { validateLatitude, validateLongitude } from '../utils/validation.js';
14
+ import { logger } from '../utils/logger.js';
15
+ import { RateLimitError, ServiceUnavailableError, InvalidLocationError, DataNotFoundError, ApiError } from '../errors/ApiError.js';
8
16
  export class OpenMeteoService {
9
17
  client;
18
+ geocodingClient;
19
+ forecastClient;
20
+ airQualityClient;
21
+ marineClient;
10
22
  maxRetries;
11
23
  cache;
12
24
  constructor(config = {}) {
13
- const { baseURL = 'https://archive-api.open-meteo.com/v1', timeout = 30000, maxRetries = 3 } = config;
25
+ const { baseURL = 'https://archive-api.open-meteo.com/v1', geocodingURL = 'https://geocoding-api.open-meteo.com/v1', forecastURL = 'https://api.open-meteo.com/v1', airQualityURL = 'https://air-quality-api.open-meteo.com/v1', marineURL = 'https://marine-api.open-meteo.com/v1', timeout = CacheConfig.apiTimeoutMs, maxRetries = 3 } = config;
14
26
  this.maxRetries = maxRetries;
15
27
  this.cache = new Cache(CacheConfig.maxSize);
28
+ // Historical weather client
16
29
  this.client = axios.create({
17
30
  baseURL,
18
31
  timeout,
19
32
  headers: {
20
33
  'Accept': 'application/json',
21
- 'User-Agent': 'weather-mcp/0.1.0'
34
+ 'User-Agent': 'weather-mcp/0.6.0'
35
+ }
36
+ });
37
+ // Geocoding client
38
+ this.geocodingClient = axios.create({
39
+ baseURL: geocodingURL,
40
+ timeout,
41
+ headers: {
42
+ 'Accept': 'application/json',
43
+ 'User-Agent': 'weather-mcp/0.6.0'
44
+ }
45
+ });
46
+ // Forecast client
47
+ this.forecastClient = axios.create({
48
+ baseURL: forecastURL,
49
+ timeout,
50
+ headers: {
51
+ 'Accept': 'application/json',
52
+ 'User-Agent': 'weather-mcp/0.6.0'
53
+ }
54
+ });
55
+ // Air quality client
56
+ this.airQualityClient = axios.create({
57
+ baseURL: airQualityURL,
58
+ timeout,
59
+ headers: {
60
+ 'Accept': 'application/json',
61
+ 'User-Agent': 'weather-mcp/0.6.0'
62
+ }
63
+ });
64
+ // Marine client
65
+ this.marineClient = axios.create({
66
+ baseURL: marineURL,
67
+ timeout,
68
+ headers: {
69
+ 'Accept': 'application/json',
70
+ 'User-Agent': 'weather-mcp/0.6.0'
22
71
  }
23
72
  });
24
73
  // Add response interceptor for error handling
25
74
  this.client.interceptors.response.use(response => response, error => this.handleError(error));
75
+ this.geocodingClient.interceptors.response.use(response => response, error => this.handleError(error));
76
+ this.forecastClient.interceptors.response.use(response => response, error => this.handleError(error));
77
+ this.airQualityClient.interceptors.response.use(response => response, error => this.handleError(error));
78
+ this.marineClient.interceptors.response.use(response => response, error => this.handleError(error));
26
79
  }
27
80
  /**
28
81
  * Handle API errors with helpful status information
@@ -34,58 +87,46 @@ export class OpenMeteoService {
34
87
  // Bad request
35
88
  if (status === 400) {
36
89
  const reason = data.reason || 'Invalid request parameters';
37
- throw new Error(`Open-Meteo API error: ${reason}\n\n` +
38
- `Please verify:\n` +
90
+ logger.warn('Invalid request parameters', {
91
+ service: 'OpenMeteo',
92
+ reason,
93
+ securityEvent: true
94
+ });
95
+ throw new InvalidLocationError('OpenMeteo', `${reason}\n\nPlease verify:\n` +
39
96
  `- Coordinates are valid (latitude: -90 to 90, longitude: -180 to 180)\n` +
40
97
  `- Date range is valid (1940 to 5 days ago)\n` +
41
- `- Parameters are correctly formatted\n\n` +
42
- `API documentation: https://open-meteo.com/en/docs/historical-weather-api`);
98
+ `- Parameters are correctly formatted`);
43
99
  }
44
100
  // Rate limit error
45
101
  if (status === 429) {
46
- throw new Error(`Open-Meteo API rate limit exceeded (10,000 requests/day for non-commercial use).\n\n` +
47
- `Please retry later or consider:\n` +
48
- `- Reducing request frequency\n` +
49
- `- Using daily instead of hourly data for longer periods\n` +
50
- `- Upgrading to a commercial plan for higher limits\n\n` +
51
- `More info: https://open-meteo.com/en/pricing`);
102
+ logger.warn('Rate limit exceeded', {
103
+ service: 'OpenMeteo',
104
+ securityEvent: true
105
+ });
106
+ throw new RateLimitError('OpenMeteo');
52
107
  }
53
108
  // Server errors
54
109
  if (status >= 500) {
55
- throw new Error(`Open-Meteo API server error: Service temporarily unavailable\n\n` +
56
- `The Open-Meteo API may be experiencing an outage.\n\n` +
57
- `Check service status:\n` +
58
- `- Production status: https://open-meteo.com/en/docs/model-updates\n` +
59
- `- GitHub issues: https://github.com/open-meteo/open-meteo/issues`);
110
+ throw new ServiceUnavailableError('OpenMeteo', error);
60
111
  }
61
112
  // Other errors
62
- throw new Error(`Open-Meteo API error (${status}): ${data.reason || 'Request failed'}\n\n` +
63
- `For assistance, visit:\n` +
64
- `- API documentation: https://open-meteo.com/en/docs\n` +
65
- `- GitHub issues: https://github.com/open-meteo/open-meteo/issues`);
113
+ throw new ApiError(`Open-Meteo API error (${status})`, status, 'OpenMeteo', data.reason || 'Request failed', [
114
+ 'https://open-meteo.com/en/docs',
115
+ 'https://github.com/open-meteo/open-meteo/issues'
116
+ ]);
66
117
  }
67
118
  // Network errors
68
119
  if (error.code === 'ECONNABORTED') {
69
- throw new Error(`Request to Open-Meteo API timed out. Please try again.\n\n` +
70
- `If timeouts persist, check:\n` +
71
- `- Your internet connection\n` +
72
- `- Service status: https://open-meteo.com/en/docs/model-updates`);
120
+ throw new ServiceUnavailableError('OpenMeteo', error);
73
121
  }
74
122
  if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
75
- throw new Error(`Unable to connect to Open-Meteo API.\n\n` +
76
- `Possible causes:\n` +
77
- `- Internet connection issues\n` +
78
- `- Open-Meteo API service outage\n` +
79
- `- DNS resolution problems\n\n` +
80
- `Check:\n` +
81
- `- Your internet connection\n` +
82
- `- Service status: https://open-meteo.com/en/docs/model-updates`);
123
+ throw new ServiceUnavailableError('OpenMeteo', error);
83
124
  }
84
125
  // Generic error
85
- throw new Error(`Open-Meteo API request failed: ${error.message}\n\n` +
86
- `For assistance, visit:\n` +
87
- `- GitHub issues: https://github.com/open-meteo/open-meteo/issues\n` +
88
- `- Documentation: https://open-meteo.com/en/docs`);
126
+ throw new ApiError(`Open-Meteo API request failed: ${error.message}`, 500, 'OpenMeteo', `Request failed: ${error.message}`, [
127
+ 'https://github.com/open-meteo/open-meteo/issues',
128
+ 'https://open-meteo.com/en/docs'
129
+ ], true);
89
130
  }
90
131
  /**
91
132
  * Make request with retry logic
@@ -102,7 +143,9 @@ export class OpenMeteoService {
102
143
  error.message.includes('server error') ||
103
144
  error.message.includes('timed out');
104
145
  if (shouldRetry) {
105
- const delay = Math.pow(2, retries) * 1000; // Exponential backoff
146
+ // Exponential backoff with jitter to prevent thundering herd
147
+ const baseDelay = Math.pow(2, retries) * 1000;
148
+ const delay = baseDelay * (0.5 + Math.random() * 0.5);
106
149
  await new Promise(resolve => setTimeout(resolve, delay));
107
150
  return this.makeRequest(url, params, retries + 1);
108
151
  }
@@ -203,13 +246,11 @@ export class OpenMeteoService {
203
246
  * @returns Historical weather data
204
247
  */
205
248
  async getHistoricalWeather(latitude, longitude, startDate, endDate, useHourly = true) {
206
- // Validate coordinates
207
- if (latitude < -90 || latitude > 90) {
208
- throw new Error(`Invalid latitude: ${latitude}. Must be between -90 and 90.`);
209
- }
210
- if (longitude < -180 || longitude > 180) {
211
- throw new Error(`Invalid longitude: ${longitude}. Must be between -180 and 180.`);
212
- }
249
+ // Validate coordinates (checks for NaN, Infinity, and range)
250
+ validateLatitude(latitude);
251
+ validateLongitude(longitude);
252
+ // Build parameters once
253
+ const params = this.buildHistoricalParams(latitude, longitude, startDate, endDate, useHourly);
213
254
  // Check cache first (if enabled)
214
255
  if (CacheConfig.enabled) {
215
256
  const cacheKey = Cache.generateKey('openmeteo-historical', latitude, longitude, startDate, endDate, useHourly);
@@ -217,74 +258,23 @@ export class OpenMeteoService {
217
258
  if (cached) {
218
259
  return cached;
219
260
  }
220
- // Build request parameters
221
- const params = {
222
- latitude,
223
- longitude,
224
- start_date: startDate,
225
- end_date: endDate,
226
- temperature_unit: 'fahrenheit',
227
- wind_speed_unit: 'mph',
228
- precipitation_unit: 'inch',
229
- timezone: 'auto'
230
- };
231
- // Request appropriate data granularity
232
- if (useHourly) {
233
- // Hourly data for detailed observations
234
- params.hourly = [
235
- 'temperature_2m',
236
- 'relative_humidity_2m',
237
- 'dewpoint_2m',
238
- 'apparent_temperature',
239
- 'precipitation',
240
- 'rain',
241
- 'snowfall',
242
- 'weather_code',
243
- 'pressure_msl',
244
- 'cloud_cover',
245
- 'wind_speed_10m',
246
- 'wind_direction_10m',
247
- 'wind_gusts_10m'
248
- ].join(',');
249
- }
250
- else {
251
- // Daily summaries for longer time periods
252
- params.daily = [
253
- 'temperature_2m_max',
254
- 'temperature_2m_min',
255
- 'temperature_2m_mean',
256
- 'apparent_temperature_max',
257
- 'apparent_temperature_min',
258
- 'precipitation_sum',
259
- 'rain_sum',
260
- 'snowfall_sum',
261
- 'precipitation_hours',
262
- 'weather_code',
263
- 'wind_speed_10m_max',
264
- 'wind_gusts_10m_max',
265
- 'wind_direction_10m_dominant'
266
- ].join(',');
267
- }
268
261
  const response = await this.makeRequest('/archive', params);
269
- // Validate response has data
270
- if (useHourly && (!response.hourly || !response.hourly.time || response.hourly.time.length === 0)) {
271
- throw new Error(`No historical weather data available for the specified date range (${startDate} to ${endDate}).\n\n` +
272
- 'This may occur because:\n' +
273
- '- The dates are too recent (data has a 5-day delay for most models)\n' +
274
- '- The dates are before 1940 (earliest available data)\n\n' +
275
- 'Please try adjusting your date range.');
276
- }
277
- if (!useHourly && (!response.daily || !response.daily.time || response.daily.time.length === 0)) {
278
- throw new Error(`No historical weather data available for the specified date range (${startDate} to ${endDate}).\n\n` +
279
- 'Please try adjusting your date range.');
280
- }
262
+ this.validateResponse(response, startDate, endDate, useHourly);
281
263
  // Use smart TTL based on date range
282
264
  const ttl = getHistoricalDataTTL(startDate);
283
265
  this.cache.set(cacheKey, response, ttl);
284
266
  return response;
285
267
  }
286
- // No caching - original logic
287
- // Build request parameters
268
+ // No caching
269
+ const response = await this.makeRequest('/archive', params);
270
+ this.validateResponse(response, startDate, endDate, useHourly);
271
+ return response;
272
+ }
273
+ /**
274
+ * Build request parameters for historical weather data
275
+ * @private
276
+ */
277
+ buildHistoricalParams(latitude, longitude, startDate, endDate, useHourly) {
288
278
  const params = {
289
279
  latitude,
290
280
  longitude,
@@ -332,8 +322,13 @@ export class OpenMeteoService {
332
322
  'wind_direction_10m_dominant'
333
323
  ].join(',');
334
324
  }
335
- const response = await this.makeRequest('/archive', params);
336
- // Validate response has data
325
+ return params;
326
+ }
327
+ /**
328
+ * Validate that the response contains the expected data
329
+ * @private
330
+ */
331
+ validateResponse(response, startDate, endDate, useHourly) {
337
332
  if (useHourly && (!response.hourly || !response.hourly.time || response.hourly.time.length === 0)) {
338
333
  throw new Error(`No historical weather data available for the specified date range (${startDate} to ${endDate}).\n\n` +
339
334
  'This may occur because:\n' +
@@ -345,7 +340,6 @@ export class OpenMeteoService {
345
340
  throw new Error(`No historical weather data available for the specified date range (${startDate} to ${endDate}).\n\n` +
346
341
  'Please try adjusting your date range.');
347
342
  }
348
- return response;
349
343
  }
350
344
  /**
351
345
  * Get weather description from WMO weather code
@@ -384,5 +378,467 @@ export class OpenMeteoService {
384
378
  };
385
379
  return weatherCodes[code] || `Unknown (code: ${code})`;
386
380
  }
381
+ /**
382
+ * Search for locations by name using the Open-Meteo Geocoding API
383
+ *
384
+ * @param query - Location name to search for (e.g., "Paris", "New York, NY", "Tokyo")
385
+ * @param limit - Maximum number of results to return (default: 5, max: 100)
386
+ * @param language - Language for results (default: 'en')
387
+ * @returns Geocoding results with coordinates and metadata
388
+ */
389
+ async searchLocation(query, limit = 5, language = 'en') {
390
+ if (!query || query.trim().length === 0) {
391
+ throw new InvalidLocationError('OpenMeteo', 'Search query cannot be empty');
392
+ }
393
+ if (query.trim().length === 1) {
394
+ throw new InvalidLocationError('OpenMeteo', 'Search query must be at least 2 characters long');
395
+ }
396
+ // Validate limit
397
+ if (limit < 1 || limit > 100) {
398
+ throw new InvalidLocationError('OpenMeteo', 'Limit must be between 1 and 100');
399
+ }
400
+ // Check cache first (locations don't move, so cache indefinitely)
401
+ if (CacheConfig.enabled) {
402
+ const cacheKey = Cache.generateKey('openmeteo-geocoding', query, limit, language);
403
+ const cached = this.cache.get(cacheKey);
404
+ if (cached) {
405
+ return cached;
406
+ }
407
+ const params = {
408
+ name: query.trim(),
409
+ count: limit,
410
+ language,
411
+ format: 'json'
412
+ };
413
+ const response = await this.geocodingClient.get('/search', { params });
414
+ // Cache indefinitely (locations don't change)
415
+ // Using 30 days as TTL to keep cache from growing unbounded
416
+ this.cache.set(cacheKey, response.data, 30 * 24 * 60 * 60 * 1000);
417
+ return response.data;
418
+ }
419
+ // No caching
420
+ const params = {
421
+ name: query.trim(),
422
+ count: limit,
423
+ language,
424
+ format: 'json'
425
+ };
426
+ const response = await this.geocodingClient.get('/search', { params });
427
+ return response.data;
428
+ }
429
+ /**
430
+ * Get weather forecast from Open-Meteo Forecast API
431
+ *
432
+ * @param latitude - Latitude coordinate (-90 to 90)
433
+ * @param longitude - Longitude coordinate (-180 to 180)
434
+ * @param days - Number of forecast days (1-16, default: 7)
435
+ * @param hourly - Whether to include hourly data (default: false)
436
+ * @returns Weather forecast data
437
+ */
438
+ async getForecast(latitude, longitude, days = 7, hourly = false) {
439
+ // Validate coordinates
440
+ validateLatitude(latitude);
441
+ validateLongitude(longitude);
442
+ // Validate days
443
+ if (days < 1 || days > 16) {
444
+ throw new InvalidLocationError('OpenMeteo', 'Forecast days must be between 1 and 16');
445
+ }
446
+ // Build parameters
447
+ const params = this.buildForecastParams(latitude, longitude, days, hourly);
448
+ // Check cache first
449
+ if (CacheConfig.enabled) {
450
+ const cacheKey = Cache.generateKey('openmeteo-forecast', latitude, longitude, days, hourly);
451
+ const cached = this.cache.get(cacheKey);
452
+ if (cached) {
453
+ return cached;
454
+ }
455
+ const response = await this.makeRequestToForecast('/forecast', params);
456
+ this.validateForecastResponse(response, hourly);
457
+ // Cache for 2 hours (forecasts update regularly)
458
+ this.cache.set(cacheKey, response, 2 * 60 * 60 * 1000);
459
+ return response;
460
+ }
461
+ // No caching
462
+ const response = await this.makeRequestToForecast('/forecast', params);
463
+ this.validateForecastResponse(response, hourly);
464
+ return response;
465
+ }
466
+ /**
467
+ * Build request parameters for forecast data
468
+ * @private
469
+ */
470
+ buildForecastParams(latitude, longitude, days, hourly) {
471
+ const params = {
472
+ latitude,
473
+ longitude,
474
+ forecast_days: days,
475
+ temperature_unit: 'fahrenheit',
476
+ wind_speed_unit: 'mph',
477
+ precipitation_unit: 'inch',
478
+ timezone: 'auto'
479
+ };
480
+ // Always include daily data with sunrise/sunset
481
+ params.daily = [
482
+ 'weather_code',
483
+ 'temperature_2m_max',
484
+ 'temperature_2m_min',
485
+ 'apparent_temperature_max',
486
+ 'apparent_temperature_min',
487
+ 'sunrise',
488
+ 'sunset',
489
+ 'daylight_duration',
490
+ 'sunshine_duration',
491
+ 'uv_index_max',
492
+ 'precipitation_sum',
493
+ 'rain_sum',
494
+ 'showers_sum',
495
+ 'snowfall_sum',
496
+ 'precipitation_hours',
497
+ 'precipitation_probability_max',
498
+ 'wind_speed_10m_max',
499
+ 'wind_gusts_10m_max',
500
+ 'wind_direction_10m_dominant'
501
+ ].join(',');
502
+ // Optionally include hourly data
503
+ if (hourly) {
504
+ params.hourly = [
505
+ 'temperature_2m',
506
+ 'relative_humidity_2m',
507
+ 'dewpoint_2m',
508
+ 'apparent_temperature',
509
+ 'precipitation_probability',
510
+ 'precipitation',
511
+ 'rain',
512
+ 'showers',
513
+ 'snowfall',
514
+ 'snow_depth',
515
+ 'weather_code',
516
+ 'pressure_msl',
517
+ 'cloud_cover',
518
+ 'visibility',
519
+ 'wind_speed_10m',
520
+ 'wind_direction_10m',
521
+ 'wind_gusts_10m',
522
+ 'uv_index',
523
+ 'is_day'
524
+ ].join(',');
525
+ }
526
+ return params;
527
+ }
528
+ /**
529
+ * Make request to forecast API with retry logic
530
+ * @private
531
+ */
532
+ async makeRequestToForecast(url, params, retries = 0) {
533
+ try {
534
+ const response = await this.forecastClient.get(url, { params });
535
+ return response.data;
536
+ }
537
+ catch (error) {
538
+ // Retry on rate limit or server errors
539
+ if (retries < this.maxRetries) {
540
+ const shouldRetry = error.message.includes('rate limit') ||
541
+ error.message.includes('server error') ||
542
+ error.message.includes('timed out');
543
+ if (shouldRetry) {
544
+ const baseDelay = Math.pow(2, retries) * 1000;
545
+ const delay = baseDelay * (0.5 + Math.random() * 0.5);
546
+ await new Promise(resolve => setTimeout(resolve, delay));
547
+ return this.makeRequestToForecast(url, params, retries + 1);
548
+ }
549
+ }
550
+ throw error;
551
+ }
552
+ }
553
+ /**
554
+ * Validate that the forecast response contains the expected data
555
+ * @private
556
+ */
557
+ validateForecastResponse(response, hourly) {
558
+ if (hourly && (!response.hourly || !response.hourly.time || response.hourly.time.length === 0)) {
559
+ throw new DataNotFoundError('OpenMeteo', 'No hourly forecast data available for the specified location');
560
+ }
561
+ if (!response.daily || !response.daily.time || response.daily.time.length === 0) {
562
+ throw new DataNotFoundError('OpenMeteo', 'No daily forecast data available for the specified location');
563
+ }
564
+ }
565
+ /**
566
+ * Get air quality data from Open-Meteo Air Quality API
567
+ *
568
+ * @param latitude - Latitude coordinate (-90 to 90)
569
+ * @param longitude - Longitude coordinate (-180 to 180)
570
+ * @param forecast - Whether to include hourly forecast (default: false, returns current only)
571
+ * @param forecastDays - Number of forecast days (1-7, default: 5)
572
+ * @returns Air quality data including AQI, pollutants, and UV index
573
+ */
574
+ async getAirQuality(latitude, longitude, forecast = false, forecastDays = 5) {
575
+ // Validate coordinates
576
+ validateLatitude(latitude);
577
+ validateLongitude(longitude);
578
+ // Validate forecast days
579
+ if (forecastDays < 1 || forecastDays > 7) {
580
+ throw new InvalidLocationError('OpenMeteo', 'Air quality forecast days must be between 1 and 7');
581
+ }
582
+ // Build parameters
583
+ const params = this.buildAirQualityParams(latitude, longitude, forecast, forecastDays);
584
+ // Check cache first
585
+ if (CacheConfig.enabled) {
586
+ const cacheKey = Cache.generateKey('openmeteo-airquality', latitude, longitude, forecast, forecastDays);
587
+ const cached = this.cache.get(cacheKey);
588
+ if (cached) {
589
+ return cached;
590
+ }
591
+ const response = await this.makeRequestToAirQuality('/air-quality', params);
592
+ this.validateAirQualityResponse(response, forecast);
593
+ // Cache for 1 hour (air quality updates hourly)
594
+ this.cache.set(cacheKey, response, 60 * 60 * 1000);
595
+ return response;
596
+ }
597
+ // No caching
598
+ const response = await this.makeRequestToAirQuality('/air-quality', params);
599
+ this.validateAirQualityResponse(response, forecast);
600
+ return response;
601
+ }
602
+ /**
603
+ * Build request parameters for air quality data
604
+ * @private
605
+ */
606
+ buildAirQualityParams(latitude, longitude, forecast, forecastDays) {
607
+ const params = {
608
+ latitude,
609
+ longitude,
610
+ timezone: 'auto'
611
+ };
612
+ // Always include current data
613
+ params.current = [
614
+ 'pm10',
615
+ 'pm2_5',
616
+ 'carbon_monoxide',
617
+ 'nitrogen_dioxide',
618
+ 'sulphur_dioxide',
619
+ 'ozone',
620
+ 'aerosol_optical_depth',
621
+ 'dust',
622
+ 'uv_index',
623
+ 'uv_index_clear_sky',
624
+ 'ammonia',
625
+ 'european_aqi',
626
+ 'european_aqi_pm2_5',
627
+ 'european_aqi_pm10',
628
+ 'european_aqi_nitrogen_dioxide',
629
+ 'european_aqi_ozone',
630
+ 'european_aqi_sulphur_dioxide',
631
+ 'us_aqi',
632
+ 'us_aqi_pm2_5',
633
+ 'us_aqi_pm10',
634
+ 'us_aqi_nitrogen_dioxide',
635
+ 'us_aqi_ozone',
636
+ 'us_aqi_sulphur_dioxide',
637
+ 'us_aqi_carbon_monoxide'
638
+ ].join(',');
639
+ // Optionally include hourly forecast data
640
+ if (forecast) {
641
+ params.forecast_days = forecastDays;
642
+ params.hourly = [
643
+ 'pm10',
644
+ 'pm2_5',
645
+ 'carbon_monoxide',
646
+ 'nitrogen_dioxide',
647
+ 'sulphur_dioxide',
648
+ 'ozone',
649
+ 'aerosol_optical_depth',
650
+ 'dust',
651
+ 'uv_index',
652
+ 'uv_index_clear_sky',
653
+ 'ammonia',
654
+ 'european_aqi',
655
+ 'european_aqi_pm2_5',
656
+ 'european_aqi_pm10',
657
+ 'european_aqi_nitrogen_dioxide',
658
+ 'european_aqi_ozone',
659
+ 'european_aqi_sulphur_dioxide',
660
+ 'us_aqi',
661
+ 'us_aqi_pm2_5',
662
+ 'us_aqi_pm10',
663
+ 'us_aqi_nitrogen_dioxide',
664
+ 'us_aqi_ozone',
665
+ 'us_aqi_sulphur_dioxide',
666
+ 'us_aqi_carbon_monoxide'
667
+ ].join(',');
668
+ }
669
+ return params;
670
+ }
671
+ /**
672
+ * Make request to air quality API with retry logic
673
+ * @private
674
+ */
675
+ async makeRequestToAirQuality(url, params, retries = 0) {
676
+ try {
677
+ const response = await this.airQualityClient.get(url, { params });
678
+ return response.data;
679
+ }
680
+ catch (error) {
681
+ // Retry on rate limit or server errors
682
+ if (retries < this.maxRetries) {
683
+ const shouldRetry = error.message.includes('rate limit') ||
684
+ error.message.includes('server error') ||
685
+ error.message.includes('timed out');
686
+ if (shouldRetry) {
687
+ const baseDelay = Math.pow(2, retries) * 1000;
688
+ const delay = baseDelay * (0.5 + Math.random() * 0.5);
689
+ await new Promise(resolve => setTimeout(resolve, delay));
690
+ return this.makeRequestToAirQuality(url, params, retries + 1);
691
+ }
692
+ }
693
+ throw error;
694
+ }
695
+ }
696
+ /**
697
+ * Validate that the air quality response contains the expected data
698
+ * @private
699
+ */
700
+ validateAirQualityResponse(response, forecast) {
701
+ if (!response.current || !response.current.time) {
702
+ throw new DataNotFoundError('OpenMeteo', 'No current air quality data available for the specified location');
703
+ }
704
+ if (forecast && (!response.hourly || !response.hourly.time || response.hourly.time.length === 0)) {
705
+ throw new DataNotFoundError('OpenMeteo', 'No hourly air quality forecast data available for the specified location');
706
+ }
707
+ }
708
+ /**
709
+ * Get marine conditions data from Open-Meteo Marine API
710
+ *
711
+ * @param latitude - Latitude coordinate (-90 to 90)
712
+ * @param longitude - Longitude coordinate (-180 to 180)
713
+ * @param forecast - Whether to include hourly forecast (default: false, returns current only)
714
+ * @param forecastDays - Number of forecast days (1-7, default: 5)
715
+ * @returns Marine conditions including waves, swell, and currents
716
+ */
717
+ async getMarine(latitude, longitude, forecast = false, forecastDays = 5) {
718
+ // Validate coordinates
719
+ validateLatitude(latitude);
720
+ validateLongitude(longitude);
721
+ // Validate forecast days
722
+ if (forecastDays < 1 || forecastDays > 7) {
723
+ throw new InvalidLocationError('OpenMeteo', 'Marine forecast days must be between 1 and 7');
724
+ }
725
+ // Build parameters
726
+ const params = this.buildMarineParams(latitude, longitude, forecast, forecastDays);
727
+ // Check cache first
728
+ if (CacheConfig.enabled) {
729
+ const cacheKey = Cache.generateKey('openmeteo-marine', latitude, longitude, forecast, forecastDays);
730
+ const cached = this.cache.get(cacheKey);
731
+ if (cached) {
732
+ return cached;
733
+ }
734
+ const response = await this.makeRequestToMarine('/marine', params);
735
+ this.validateMarineResponse(response, forecast);
736
+ // Cache for 1 hour (marine conditions update hourly)
737
+ this.cache.set(cacheKey, response, 60 * 60 * 1000);
738
+ return response;
739
+ }
740
+ // No caching
741
+ const response = await this.makeRequestToMarine('/marine', params);
742
+ this.validateMarineResponse(response, forecast);
743
+ return response;
744
+ }
745
+ /**
746
+ * Build request parameters for marine data
747
+ * @private
748
+ */
749
+ buildMarineParams(latitude, longitude, forecast, forecastDays) {
750
+ const params = {
751
+ latitude,
752
+ longitude,
753
+ timezone: 'auto'
754
+ };
755
+ // Always include current data
756
+ params.current = [
757
+ 'wave_height',
758
+ 'wave_direction',
759
+ 'wave_period',
760
+ 'wind_wave_height',
761
+ 'wind_wave_direction',
762
+ 'wind_wave_period',
763
+ 'wind_wave_peak_period',
764
+ 'swell_wave_height',
765
+ 'swell_wave_direction',
766
+ 'swell_wave_period',
767
+ 'swell_wave_peak_period',
768
+ 'ocean_current_velocity',
769
+ 'ocean_current_direction'
770
+ ].join(',');
771
+ // Optionally include hourly forecast data
772
+ if (forecast) {
773
+ params.forecast_days = forecastDays;
774
+ params.hourly = [
775
+ 'wave_height',
776
+ 'wave_direction',
777
+ 'wave_period',
778
+ 'wind_wave_height',
779
+ 'wind_wave_direction',
780
+ 'wind_wave_period',
781
+ 'wind_wave_peak_period',
782
+ 'swell_wave_height',
783
+ 'swell_wave_direction',
784
+ 'swell_wave_period',
785
+ 'swell_wave_peak_period',
786
+ 'ocean_current_velocity',
787
+ 'ocean_current_direction'
788
+ ].join(',');
789
+ // Also include daily aggregates
790
+ params.daily = [
791
+ 'wave_height_max',
792
+ 'wave_direction_dominant',
793
+ 'wave_period_max',
794
+ 'wind_wave_height_max',
795
+ 'wind_wave_direction_dominant',
796
+ 'wind_wave_period_max',
797
+ 'wind_wave_peak_period_max',
798
+ 'swell_wave_height_max',
799
+ 'swell_wave_direction_dominant',
800
+ 'swell_wave_period_max',
801
+ 'swell_wave_peak_period_max'
802
+ ].join(',');
803
+ }
804
+ return params;
805
+ }
806
+ /**
807
+ * Make request to marine API with retry logic
808
+ * @private
809
+ */
810
+ async makeRequestToMarine(url, params, retries = 0) {
811
+ try {
812
+ const response = await this.marineClient.get(url, { params });
813
+ return response.data;
814
+ }
815
+ catch (error) {
816
+ // Retry on rate limit or server errors
817
+ if (retries < this.maxRetries) {
818
+ const shouldRetry = error.message.includes('rate limit') ||
819
+ error.message.includes('server error') ||
820
+ error.message.includes('timed out');
821
+ if (shouldRetry) {
822
+ const baseDelay = Math.pow(2, retries) * 1000;
823
+ const delay = baseDelay * (0.5 + Math.random() * 0.5);
824
+ await new Promise(resolve => setTimeout(resolve, delay));
825
+ return this.makeRequestToMarine(url, params, retries + 1);
826
+ }
827
+ }
828
+ throw error;
829
+ }
830
+ }
831
+ /**
832
+ * Validate that the marine response contains the expected data
833
+ * @private
834
+ */
835
+ validateMarineResponse(response, forecast) {
836
+ if (!response.current || !response.current.time) {
837
+ throw new DataNotFoundError('OpenMeteo', 'No current marine conditions data available for the specified location');
838
+ }
839
+ if (forecast && (!response.hourly || !response.hourly.time || response.hourly.time.length === 0)) {
840
+ throw new DataNotFoundError('OpenMeteo', 'No hourly marine forecast data available for the specified location');
841
+ }
842
+ }
387
843
  }
388
844
  //# sourceMappingURL=openmeteo.js.map