UNPKG

rui-weather-service-api

Version:

Weather API service with OpenWeatherMap integration - includes both standalone API and MCP server

264 lines 11.6 kB
import axios from 'axios'; import https from 'https'; import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; // Load environment variables from .env file dotenv.config(); // Configure API key securely from environment variables const API_KEY = process.env.WEATHER_API_KEY || ''; const BASE_URL = 'https://api.openweathermap.org/data/2.5'; // Create axios instance with retry logic and certificate handling const axiosInstance = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false // Note: In production, proper certificate validation should be implemented }), timeout: 10000 // 10 second timeout }); // Implement retry logic with exponential backoff async function fetchWithRetry(url, params, maxRetries = 3) { let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`Attempt ${attempt}: Fetching weather data...`); const response = await axiosInstance.get(url, { params }); console.log('Weather data retrieved successfully'); return response.data; } catch (error) { lastError = error; console.error(`Attempt ${attempt} failed: ${error.message}`); if (attempt < maxRetries) { // Exponential backoff with jitter const delay = Math.min(1000 * Math.pow(2, attempt - 1) + Math.random() * 1000, 10000); console.log(`Retrying in ${Math.round(delay)}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw new Error(`Failed after ${maxRetries} attempts: ${lastError?.message}`); } export class WeatherService { constructor(options = {}) { this.apiKey = options.apiKey || process.env.WEATHER_API_KEY || ''; this.baseUrl = BASE_URL; this.units = options.units || 'metric'; this.language = options.language || 'en'; this.maxRetries = options.maxRetries || 3; if (!this.apiKey) { console.warn('No API key provided. Please set WEATHER_API_KEY environment variable or provide apiKey in options.'); } } // Function to get current weather for a city async getCurrentWeather(city) { try { // Validate inputs if (!city || city.trim() === '') { throw new Error('City name is required'); } if (!this.apiKey) { throw new Error('API key is not configured. Please set WEATHER_API_KEY environment variable or provide in constructor'); } console.log(`Fetching current weather for ${city}...`); const params = { q: city, appid: this.apiKey, units: this.units, lang: this.language }; const weatherData = await fetchWithRetry(`${this.baseUrl}/weather`, params, this.maxRetries); // Format the weather information const weatherInfo = { location: { name: weatherData.name, country: weatherData.sys.country, coordinates: { lat: weatherData.coord.lat, lon: weatherData.coord.lon } }, temperature: { current: weatherData.main.temp, feelsLike: weatherData.main.feels_like, min: weatherData.main.temp_min, max: weatherData.main.temp_max }, weather: { main: weatherData.weather[0].main, description: weatherData.weather[0].description, icon: weatherData.weather[0].icon }, wind: { speed: weatherData.wind.speed, direction: weatherData.wind.deg }, details: { humidity: weatherData.main.humidity, pressure: weatherData.main.pressure, visibility: weatherData.visibility / 1000, sunrise: new Date(weatherData.sys.sunrise * 1000), sunset: new Date(weatherData.sys.sunset * 1000) }, updatedAt: new Date(weatherData.dt * 1000), raw: weatherData // Include raw data for advanced use cases }; return weatherInfo; } catch (error) { const errorMessage = `Error fetching weather data: ${error.message}`; console.error(errorMessage); throw error; } } // Function to get weather forecast for a city async getForecast(city, days = 5) { try { // Validate inputs if (!city || city.trim() === '') { throw new Error('City name is required'); } if (!this.apiKey) { throw new Error('API key is not configured'); } console.log(`Fetching ${days}-day forecast for ${city}...`); const params = { q: city, appid: this.apiKey, units: this.units, lang: this.language, cnt: Math.min(days * 8, 40), // Each day has 8 forecasts (every 3 hours), max 5 days }; const forecastData = await fetchWithRetry(`${this.baseUrl}/forecast`, params, this.maxRetries); // Process forecast data by day const dailyForecasts = {}; forecastData.list.forEach((item) => { const date = new Date(item.dt * 1000).toLocaleDateString(); if (!dailyForecasts[date]) { dailyForecasts[date] = { date: new Date(item.dt * 1000), temperature: { min: item.main.temp, max: item.main.temp, avg: item.main.temp }, weather: { main: item.weather[0].main, description: item.weather[0].description, icon: item.weather[0].icon }, wind: { speed: item.wind.speed, direction: item.wind.deg }, details: { humidity: item.main.humidity, pressure: item.main.pressure }, hourlyForecasts: [] }; } else { // Update min/max temperatures dailyForecasts[date].temperature.min = Math.min(dailyForecasts[date].temperature.min, item.main.temp); dailyForecasts[date].temperature.max = Math.max(dailyForecasts[date].temperature.max, item.main.temp); } // Add hourly forecast dailyForecasts[date].hourlyForecasts.push({ time: new Date(item.dt * 1000), temperature: item.main.temp, feelsLike: item.main.feels_like, weather: { main: item.weather[0].main, description: item.weather[0].description, icon: item.weather[0].icon }, wind: { speed: item.wind.speed, direction: item.wind.deg }, details: { humidity: item.main.humidity, pressure: item.main.pressure } }); }); // Calculate average temperatures Object.keys(dailyForecasts).forEach(date => { const forecasts = dailyForecasts[date].hourlyForecasts; const sum = forecasts.reduce((acc, curr) => acc + curr.temperature, 0); dailyForecasts[date].temperature.avg = sum / forecasts.length; }); return { city: { name: forecastData.city.name, country: forecastData.city.country, coordinates: { lat: forecastData.city.coord.lat, lon: forecastData.city.coord.lon } }, forecasts: Object.values(dailyForecasts), raw: forecastData // Include raw data for advanced use cases }; } catch (error) { const errorMessage = `Error fetching forecast data: ${error.message}`; console.error(errorMessage); throw error; } } // Helper method to format weather information as human-readable string formatCurrentWeather(weatherData) { return ` Current Weather for ${weatherData.location.name}, ${weatherData.location.country}: Temperature: ${weatherData.temperature.current}°C ${this.units === 'metric' ? `(${(weatherData.temperature.current * 9 / 5 + 32).toFixed(1)}°F)` : `(${((weatherData.temperature.current - 32) * 5 / 9).toFixed(1)}°C)`} Feels like: ${weatherData.temperature.feelsLike}°C Conditions: ${weatherData.weather.description} Humidity: ${weatherData.details.humidity}% Wind: ${weatherData.wind.speed} ${this.units === 'metric' ? 'm/s' : 'mph'} Pressure: ${weatherData.details.pressure} hPa Visibility: ${weatherData.details.visibility} km Updated: ${weatherData.updatedAt.toLocaleString()} `; } // Helper method to format forecast as human-readable string formatForecast(forecastData) { let forecastInfo = `Weather Forecast for ${forecastData.city.name}, ${forecastData.city.country}:\n\n`; forecastData.forecasts.forEach((forecast) => { const date = forecast.date.toLocaleDateString(); forecastInfo += `${date}:\n`; forecastInfo += ` Temperature: ${forecast.temperature.avg.toFixed(2)}°C\n`; forecastInfo += ` Conditions: ${forecast.weather.description}\n`; forecastInfo += ` Humidity: ${forecast.details.humidity}%\n`; forecastInfo += ` Wind: ${forecast.wind.speed} ${this.units === 'metric' ? 'm/s' : 'mph'}\n\n`; }); return forecastInfo; } } // For backward compatibility and convenience export async function getCurrentWeather(city) { const weatherService = new WeatherService(); const weatherData = await weatherService.getCurrentWeather(city); return weatherService.formatCurrentWeather(weatherData); } // Export a default instance for convenience export const weatherService = new WeatherService(); // Main execution only when run directly - ES Module version // Check if this file is being run directly const isMainModule = typeof import.meta.url === 'string' && import.meta.url === `file://${fileURLToPath(import.meta.url)}`; if (isMainModule) { (async () => { try { const result = await getCurrentWeather('New York'); console.log(result); process.exit(0); } catch (error) { console.error('Application error:', error); process.exit(1); } })(); } //# sourceMappingURL=getWeather.js.map