import fetch from 'node-fetch';
import { WEATHER_CODES } from '../types/openmeteo.js';
export async function getHistoricalWeather(args: any) {
const {
latitude,
longitude,
start_date,
end_date,
daily = true,
hourly = false,
units = 'celsius'
} = args;
if (typeof latitude !== 'number' || typeof longitude !== 'number') {
throw new Error('Latitude and longitude must be numbers');
}
if (latitude < -90 || latitude > 90) {
throw new Error('Latitude must be between -90 and 90');
}
if (longitude < -180 || longitude > 180) {
throw new Error('Longitude must be between -180 and 180');
}
// Validate date format
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(start_date) || !dateRegex.test(end_date)) {
throw new Error('Dates must be in YYYY-MM-DD format');
}
const startDate = new Date(start_date);
const endDate = new Date(end_date);
const now = new Date();
if (startDate >= now) {
throw new Error('Start date must be in the past');
}
if (endDate >= now) {
throw new Error('End date must be in the past');
}
if (startDate > endDate) {
throw new Error('Start date must be before end date');
}
// Check if the date range is within the last 90 days for the free API
const ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
if (startDate < ninetyDaysAgo) {
console.warn('Warning: Historical data older than 90 days may require a commercial API key');
}
const temperatureUnit = units === 'fahrenheit' ? 'fahrenheit' : 'celsius';
const hourlyParams = hourly ? [
'temperature_2m',
'relative_humidity_2m',
'precipitation',
'rain',
'snowfall',
'weather_code',
'pressure_msl',
'surface_pressure',
'cloud_cover',
'wind_speed_10m',
'wind_direction_10m',
'wind_gusts_10m'
].join(',') : '';
const dailyParams = daily ? [
'weather_code',
'temperature_2m_max',
'temperature_2m_min',
'temperature_2m_mean',
'apparent_temperature_max',
'apparent_temperature_min',
'apparent_temperature_mean',
'sunrise',
'sunset',
'daylight_duration',
'sunshine_duration',
'precipitation_sum',
'rain_sum',
'snowfall_sum',
'precipitation_hours',
'wind_speed_10m_max',
'wind_gusts_10m_max',
'wind_direction_10m_dominant',
'shortwave_radiation_sum'
].join(',') : '';
let url = `https://archive-api.open-meteo.com/v1/archive?latitude=${latitude}&longitude=${longitude}&start_date=${start_date}&end_date=${end_date}&temperature_unit=${temperatureUnit}&wind_speed_unit=kmh&precipitation_unit=mm`;
if (hourlyParams) {
url += `&hourly=${hourlyParams}`;
}
if (dailyParams) {
url += `&daily=${dailyParams}`;
}
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Open Meteo Historical API error: ${response.status} ${response.statusText}`);
}
const data = await response.json() as any;
const tempUnit = units === 'fahrenheit' ? '°F' : '°C';
const result: any = {
location: {
latitude: data.latitude,
longitude: data.longitude,
elevation: data.elevation,
timezone: data.timezone,
utc_offset_seconds: data.utc_offset_seconds,
},
period: {
start_date,
end_date,
},
};
let historicalText = `# Historical Weather Data
**Location:** ${data.latitude.toFixed(4)}°N, ${data.longitude.toFixed(4)}°E
**Elevation:** ${data.elevation}m
**Timezone:** ${data.timezone}
**Period:** ${start_date} to ${end_date}
`;
if (daily && data.daily) {
result.daily_data = data.daily.time.map((time: string, index: number) => ({
date: time,
weather_description: WEATHER_CODES[data.daily.weather_code[index]] || 'Unknown',
weather_code: data.daily.weather_code[index],
temperature_max: `${data.daily.temperature_2m_max[index]}${tempUnit}`,
temperature_min: `${data.daily.temperature_2m_min[index]}${tempUnit}`,
temperature_mean: `${data.daily.temperature_2m_mean[index]}${tempUnit}`,
apparent_temperature_max: `${data.daily.apparent_temperature_max[index]}${tempUnit}`,
apparent_temperature_min: `${data.daily.apparent_temperature_min[index]}${tempUnit}`,
apparent_temperature_mean: `${data.daily.apparent_temperature_mean[index]}${tempUnit}`,
sunrise: data.daily.sunrise[index],
sunset: data.daily.sunset[index],
daylight_duration: `${(data.daily.daylight_duration[index] / 3600).toFixed(1)} hours`,
sunshine_duration: `${(data.daily.sunshine_duration[index] / 3600).toFixed(1)} hours`,
precipitation_sum: `${data.daily.precipitation_sum[index]} mm`,
rain_sum: `${data.daily.rain_sum[index]} mm`,
snowfall_sum: `${data.daily.snowfall_sum[index]} mm`,
precipitation_hours: `${data.daily.precipitation_hours[index]} hours`,
wind_speed_max: `${data.daily.wind_speed_10m_max[index]} km/h`,
wind_gusts_max: `${data.daily.wind_gusts_10m_max[index]} km/h`,
wind_direction_dominant: `${data.daily.wind_direction_10m_dominant[index]}°`,
solar_radiation: `${data.daily.shortwave_radiation_sum[index]} MJ/m²`,
}));
// Calculate statistics
const temperatures = result.daily_data.map((day: any) =>
parseFloat(day.temperature_mean.replace(tempUnit, ''))
);
const precipitations = result.daily_data.map((day: any) =>
parseFloat(day.precipitation_sum.replace(' mm', ''))
);
const avgTemp = (temperatures.reduce((a: number, b: number) => a + b, 0) / temperatures.length).toFixed(1);
const maxTemp = Math.max(...temperatures).toFixed(1);
const minTemp = Math.min(...temperatures).toFixed(1);
const totalPrecip = precipitations.reduce((a: number, b: number) => a + b, 0).toFixed(1);
historicalText += `## Summary Statistics
- **Average Temperature:** ${avgTemp}${tempUnit}
- **Highest Temperature:** ${maxTemp}${tempUnit}
- **Lowest Temperature:** ${minTemp}${tempUnit}
- **Total Precipitation:** ${totalPrecip} mm
- **Days with Precipitation:** ${precipitations.filter((p: number) => p > 0).length}
## Daily Historical Data
`;
result.daily_data.slice(0, 10).forEach((day: any) => {
historicalText += `### ${day.date}
- **Weather:** ${day.weather_description}
- **Temperature:** ${day.temperature_min} to ${day.temperature_max} (avg: ${day.temperature_mean})
- **Precipitation:** ${day.precipitation_sum}
- **Wind:** Max ${day.wind_speed_max}
- **Sunshine:** ${day.sunshine_duration}
`;
});
if (result.daily_data.length > 10) {
historicalText += `... and ${result.daily_data.length - 10} more days (see JSON data for complete information)\n\n`;
}
}
if (hourly && data.hourly) {
result.hourly_data = data.hourly.time.map((time: string, index: number) => ({
time: time,
temperature: `${data.hourly.temperature_2m[index]}${tempUnit}`,
humidity: `${data.hourly.relative_humidity_2m[index]}%`,
precipitation: `${data.hourly.precipitation[index]} mm`,
rain: `${data.hourly.rain[index]} mm`,
snowfall: `${data.hourly.snowfall[index]} mm`,
weather_description: WEATHER_CODES[data.hourly.weather_code[index]] || 'Unknown',
weather_code: data.hourly.weather_code[index],
cloud_cover: `${data.hourly.cloud_cover[index]}%`,
pressure_sea_level: `${data.hourly.pressure_msl[index]} hPa`,
surface_pressure: `${data.hourly.surface_pressure[index]} hPa`,
wind_speed: `${data.hourly.wind_speed_10m[index]} km/h`,
wind_direction: `${data.hourly.wind_direction_10m[index]}°`,
wind_gusts: `${data.hourly.wind_gusts_10m[index]} km/h`,
}));
historicalText += `## Hourly Data Sample (First 12 Hours)\n\n`;
result.hourly_data.slice(0, 12).forEach((hour: any) => {
historicalText += `**${hour.time}** - ${hour.temperature}, ${hour.weather_description}, ${hour.precipitation}\n`;
});
}
return {
content: [
{
type: 'text',
text: historicalText,
},
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
throw new Error(`Failed to fetch historical weather: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}