rui-weather-service-api
Version:
Weather API service with OpenWeatherMap integration - includes both standalone API and MCP server
264 lines • 11.6 kB
JavaScript
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