import fetch from 'node-fetch';
// Air Quality Index interpretations
const AQI_LEVELS = {
european: {
'good': { min: 0, max: 20, color: 'green', description: 'Good air quality' },
'fair': { min: 21, max: 40, color: 'yellow', description: 'Fair air quality' },
'moderate': { min: 41, max: 60, color: 'orange', description: 'Moderate air quality' },
'poor': { min: 61, max: 80, color: 'red', description: 'Poor air quality' },
'very_poor': { min: 81, max: 100, color: 'purple', description: 'Very poor air quality' },
'extremely_poor': { min: 101, max: 999, color: 'maroon', description: 'Extremely poor air quality' },
},
us: {
'good': { min: 0, max: 50, color: 'green', description: 'Good air quality' },
'moderate': { min: 51, max: 100, color: 'yellow', description: 'Moderate air quality' },
'unhealthy_sensitive': { min: 101, max: 150, color: 'orange', description: 'Unhealthy for sensitive groups' },
'unhealthy': { min: 151, max: 200, color: 'red', description: 'Unhealthy air quality' },
'very_unhealthy': { min: 201, max: 300, color: 'purple', description: 'Very unhealthy air quality' },
'hazardous': { min: 301, max: 999, color: 'maroon', description: 'Hazardous air quality' },
}
};
function getAQILevel(aqi: number, system: 'european' | 'us') {
const levels = AQI_LEVELS[system];
for (const [level, range] of Object.entries(levels)) {
if (aqi >= range.min && aqi <= range.max) {
return { level, ...range };
}
}
return { level: 'unknown', min: 0, max: 0, color: 'gray', description: 'Unknown air quality level' };
}
export async function getAirQuality(args: any) {
const { latitude, longitude, days = 3, current = true } = 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');
}
if (days < 1 || days > 5) {
throw new Error('Days must be between 1 and 5');
}
const hourlyParams = [
'pm10',
'pm2_5',
'carbon_monoxide',
'nitrogen_dioxide',
'sulphur_dioxide',
'ozone',
'aerosol_optical_depth',
'dust',
'uv_index',
'uv_index_clear_sky',
'ammonia',
'alder_pollen',
'birch_pollen',
'grass_pollen',
'mugwort_pollen',
'olive_pollen',
'ragweed_pollen',
'european_aqi',
'us_aqi',
'european_aqi_pm2_5',
'european_aqi_pm10',
'european_aqi_nitrogen_dioxide',
'european_aqi_ozone',
'european_aqi_sulphur_dioxide'
].join(',');
const currentParams = current ? [
'pm10',
'pm2_5',
'carbon_monoxide',
'nitrogen_dioxide',
'sulphur_dioxide',
'ozone',
'aerosol_optical_depth',
'dust',
'uv_index',
'european_aqi',
'us_aqi'
].join(',') : '';
let url = `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${latitude}&longitude=${longitude}&forecast_days=${days}&hourly=${hourlyParams}`;
if (currentParams) {
url += `¤t=${currentParams}`;
}
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Open Meteo Air Quality API error: ${response.status} ${response.statusText}`);
}
const data = await response.json() as any;
const result: any = {
location: {
latitude: data.latitude,
longitude: data.longitude,
timezone: data.timezone,
utc_offset_seconds: data.utc_offset_seconds,
},
};
let airQualityText = `# Air Quality Report
**Location:** ${data.latitude.toFixed(4)}°N, ${data.longitude.toFixed(4)}°E
**Timezone:** ${data.timezone}
**Forecast Days:** ${days}
`;
if (current && data.current) {
const curr = data.current;
const europeanLevel = getAQILevel(curr.european_aqi, 'european');
const usLevel = getAQILevel(curr.us_aqi, 'us');
result.current_air_quality = {
time: curr.time,
pm10: `${curr.pm10} μg/m³`,
pm2_5: `${curr.pm2_5} μg/m³`,
carbon_monoxide: `${curr.carbon_monoxide} μg/m³`,
nitrogen_dioxide: `${curr.nitrogen_dioxide} μg/m³`,
sulphur_dioxide: `${curr.sulphur_dioxide} μg/m³`,
ozone: `${curr.ozone} μg/m³`,
aerosol_optical_depth: curr.aerosol_optical_depth,
dust: `${curr.dust} μg/m³`,
uv_index: curr.uv_index,
european_aqi: {
value: curr.european_aqi,
level: europeanLevel.level,
description: europeanLevel.description,
},
us_aqi: {
value: curr.us_aqi,
level: usLevel.level,
description: usLevel.description,
},
};
airQualityText += `## Current Air Quality (${curr.time})
### Air Quality Indices
- **European AQI:** ${curr.european_aqi} (${europeanLevel.description})
- **US AQI:** ${curr.us_aqi} (${usLevel.description})
### Pollutant Concentrations
- **PM10:** ${curr.pm10} μg/m³ (Particulate matter ≤10μm)
- **PM2.5:** ${curr.pm2_5} μg/m³ (Particulate matter ≤2.5μm)
- **Ozone (O₃):** ${curr.ozone} μg/m³
- **Nitrogen Dioxide (NO₂):** ${curr.nitrogen_dioxide} μg/m³
- **Sulphur Dioxide (SO₂):** ${curr.sulphur_dioxide} μg/m³
- **Carbon Monoxide (CO):** ${curr.carbon_monoxide} μg/m³
### Additional Metrics
- **Dust:** ${curr.dust} μg/m³
- **UV Index:** ${curr.uv_index}
- **Aerosol Optical Depth:** ${curr.aerosol_optical_depth}
`;
}
if (data.hourly) {
// Process hourly data - show daily averages and extremes
const hourlyData = data.hourly;
const hoursPerDay = 24;
const totalHours = hourlyData.time.length;
const dayCount = Math.ceil(totalHours / hoursPerDay);
result.daily_summary = [];
for (let day = 0; day < Math.min(dayCount, days); day++) {
const startHour = day * hoursPerDay;
const endHour = Math.min(startHour + hoursPerDay, totalHours);
const dayData = {
date: hourlyData.time[startHour].split('T')[0],
pm10: {
avg: 0,
max: 0,
min: Infinity,
},
pm2_5: {
avg: 0,
max: 0,
min: Infinity,
},
ozone: {
avg: 0,
max: 0,
min: Infinity,
},
european_aqi: {
avg: 0,
max: 0,
min: Infinity,
},
us_aqi: {
avg: 0,
max: 0,
min: Infinity,
},
uv_index_max: 0,
};
const validHours = endHour - startHour;
for (let hour = startHour; hour < endHour; hour++) {
// PM10
dayData.pm10.avg += hourlyData.pm10[hour];
dayData.pm10.max = Math.max(dayData.pm10.max, hourlyData.pm10[hour]);
dayData.pm10.min = Math.min(dayData.pm10.min, hourlyData.pm10[hour]);
// PM2.5
dayData.pm2_5.avg += hourlyData.pm2_5[hour];
dayData.pm2_5.max = Math.max(dayData.pm2_5.max, hourlyData.pm2_5[hour]);
dayData.pm2_5.min = Math.min(dayData.pm2_5.min, hourlyData.pm2_5[hour]);
// Ozone
dayData.ozone.avg += hourlyData.ozone[hour];
dayData.ozone.max = Math.max(dayData.ozone.max, hourlyData.ozone[hour]);
dayData.ozone.min = Math.min(dayData.ozone.min, hourlyData.ozone[hour]);
// European AQI
dayData.european_aqi.avg += hourlyData.european_aqi[hour];
dayData.european_aqi.max = Math.max(dayData.european_aqi.max, hourlyData.european_aqi[hour]);
dayData.european_aqi.min = Math.min(dayData.european_aqi.min, hourlyData.european_aqi[hour]);
// US AQI
dayData.us_aqi.avg += hourlyData.us_aqi[hour];
dayData.us_aqi.max = Math.max(dayData.us_aqi.max, hourlyData.us_aqi[hour]);
dayData.us_aqi.min = Math.min(dayData.us_aqi.min, hourlyData.us_aqi[hour]);
// UV Index
dayData.uv_index_max = Math.max(dayData.uv_index_max, hourlyData.uv_index[hour]);
}
// Calculate averages
dayData.pm10.avg = Math.round(dayData.pm10.avg / validHours);
dayData.pm2_5.avg = Math.round(dayData.pm2_5.avg / validHours);
dayData.ozone.avg = Math.round(dayData.ozone.avg / validHours);
dayData.european_aqi.avg = Math.round(dayData.european_aqi.avg / validHours);
dayData.us_aqi.avg = Math.round(dayData.us_aqi.avg / validHours);
const avgEuropeanLevel = getAQILevel(dayData.european_aqi.avg, 'european');
const avgUsLevel = getAQILevel(dayData.us_aqi.avg, 'us');
result.daily_summary.push({
date: dayData.date,
pm10_avg: `${dayData.pm10.avg} μg/m³`,
pm10_range: `${dayData.pm10.min}-${dayData.pm10.max} μg/m³`,
pm2_5_avg: `${dayData.pm2_5.avg} μg/m³`,
pm2_5_range: `${dayData.pm2_5.min}-${dayData.pm2_5.max} μg/m³`,
ozone_avg: `${dayData.ozone.avg} μg/m³`,
ozone_range: `${dayData.ozone.min}-${dayData.ozone.max} μg/m³`,
european_aqi_avg: dayData.european_aqi.avg,
european_aqi_level: avgEuropeanLevel.description,
us_aqi_avg: dayData.us_aqi.avg,
us_aqi_level: avgUsLevel.description,
uv_index_max: dayData.uv_index_max,
});
airQualityText += `## ${dayData.date}
- **European AQI:** ${dayData.european_aqi.avg} (${avgEuropeanLevel.description})
- **US AQI:** ${dayData.us_aqi.avg} (${avgUsLevel.description})
- **PM2.5:** ${dayData.pm2_5.avg} μg/m³ (range: ${dayData.pm2_5.min}-${dayData.pm2_5.max})
- **PM10:** ${dayData.pm10.avg} μg/m³ (range: ${dayData.pm10.min}-${dayData.pm10.max})
- **Ozone:** ${dayData.ozone.avg} μg/m³ (range: ${dayData.ozone.min}-${dayData.ozone.max})
- **Max UV Index:** ${dayData.uv_index_max}
`;
}
// Include sample hourly data for the first day
if (hourlyData.time.length > 0) {
airQualityText += `## Sample Hourly Data (First 12 Hours)\n\n`;
for (let i = 0; i < Math.min(12, hourlyData.time.length); i++) {
const time = hourlyData.time[i];
const europeanAqi = hourlyData.european_aqi[i];
const usAqi = hourlyData.us_aqi[i];
const pm25 = hourlyData.pm2_5[i];
airQualityText += `**${time}** - European AQI: ${europeanAqi}, US AQI: ${usAqi}, PM2.5: ${pm25} μg/m³\n`;
}
}
}
airQualityText += `\n## Health Recommendations
### European AQI Scale:
- **0-20:** Good - No health implications
- **21-40:** Fair - Unusually sensitive individuals may experience respiratory symptoms
- **41-60:** Moderate - Sensitive individuals may experience respiratory symptoms
- **61-80:** Poor - Everyone may begin to experience respiratory symptoms
- **81-100:** Very Poor - Health warnings of emergency conditions
- **>100:** Extremely Poor - Health alert: everyone may experience serious health effects
### US AQI Scale:
- **0-50:** Good - Air quality is satisfactory
- **51-100:** Moderate - Unusually sensitive people should consider limiting outdoor exertion
- **101-150:** Unhealthy for Sensitive Groups - Active children and adults with respiratory disease should limit outdoor exertion
- **151-200:** Unhealthy - Everyone may begin to experience health effects
- **201-300:** Very Unhealthy - Health alert: everyone may experience serious health effects
- **>300:** Hazardous - Health warnings of emergency conditions
`;
return {
content: [
{
type: 'text',
text: airQualityText,
},
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
throw new Error(`Failed to fetch air quality data: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}