weather-app_v1 / src /api /air_quality_client.py
chirfort's picture
Add Weather MCP Client and Server for Weather Intelligence Tools
bf6a6f7
"""
Air Quality API Client
Integration with AirNow API for air quality data
"""
import requests
import logging
from typing import List, Dict, Optional, Any
from datetime import datetime, timedelta
import json
logger = logging.getLogger(__name__)
class AirQualityClient:
"""Client for AirNow Air Quality API"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialize Air Quality client
Args:
api_key: AirNow API key (get from https://docs.airnowapi.org/account/request/)
If None, will provide setup instructions
"""
self.base_url = "https://www.airnowapi.org/aq"
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'WeatherAppPro/1.0 (enhanced-weather-app)'
})
def is_available(self) -> bool:
"""Check if Air Quality API is available with key"""
return self.api_key is not None
def get_current_aqi(self, lat: float, lon: float) -> Dict[str, Any]:
"""
Get current Air Quality Index for coordinates
Args:
lat: Latitude
lon: Longitude
Returns:
Dict with AQI data or empty dict if unavailable
"""
if not self.is_available():
return {}
try:
url = f"{self.base_url}/observation/latLong/current/"
params = {
'format': 'application/json',
'latitude': lat,
'longitude': lon,
'distance': 25, # 25 miles radius
'API_KEY': self.api_key
}
response = self.session.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if not data:
return {}
# Process the data
aqi_data = {
'timestamp': datetime.now().isoformat(),
'location': {
'latitude': lat,
'longitude': lon
},
'measurements': []
}
for measurement in data:
processed = self._process_aqi_measurement(measurement)
aqi_data['measurements'].append(processed)
# Calculate overall AQI
aqi_data['overall'] = self._calculate_overall_aqi(aqi_data['measurements'])
return aqi_data
except Exception as e:
logger.error(f"Error getting current AQI: {e}")
return {}
def get_aqi_forecast(self, lat: float, lon: float) -> List[Dict]:
"""
Get AQI forecast for coordinates
Args:
lat: Latitude
lon: Longitude
Returns:
List of forecast data
"""
if not self.is_available():
return []
try:
url = f"{self.base_url}/forecast/latLong/"
params = {
'format': 'application/json',
'latitude': lat,
'longitude': lon,
'distance': 25,
'API_KEY': self.api_key
}
response = self.session.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
forecast_data = []
for forecast in data:
processed = self._process_aqi_forecast(forecast)
forecast_data.append(processed)
return forecast_data
except Exception as e:
logger.error(f"Error getting AQI forecast: {e}")
return []
def get_aqi_by_zipcode(self, zipcode: str) -> Dict[str, Any]:
"""
Get current AQI by ZIP code
Args:
zipcode: US ZIP code
Returns:
Dict with AQI data
"""
if not self.is_available():
return {}
try:
url = f"{self.base_url}/observation/zipCode/current/"
params = {
'format': 'application/json',
'zipCode': zipcode,
'distance': 25,
'API_KEY': self.api_key
}
response = self.session.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if not data:
return {}
# Process the data similar to coordinates
aqi_data = {
'timestamp': datetime.now().isoformat(),
'location': {
'zipcode': zipcode
},
'measurements': []
}
for measurement in data:
processed = self._process_aqi_measurement(measurement)
aqi_data['measurements'].append(processed)
aqi_data['overall'] = self._calculate_overall_aqi(aqi_data['measurements'])
return aqi_data
except Exception as e:
logger.error(f"Error getting AQI by zipcode: {e}")
return {}
def _process_aqi_measurement(self, measurement: Dict) -> Dict:
"""Process raw AQI measurement data"""
return {
'date_observed': measurement.get('DateObserved'),
'hour_observed': measurement.get('HourObserved'),
'local_time_zone': measurement.get('LocalTimeZone'),
'reporting_area': measurement.get('ReportingArea'),
'state_code': measurement.get('StateCode'),
'latitude': measurement.get('Latitude'),
'longitude': measurement.get('Longitude'),
'parameter_name': measurement.get('ParameterName'),
'aqi': measurement.get('AQI'),
'category': self._get_aqi_category(measurement.get('AQI', 0)),
'site_name': measurement.get('SiteName'),
'agency_name': measurement.get('AgencyName')
}
def _process_aqi_forecast(self, forecast: Dict) -> Dict:
"""Process AQI forecast data"""
return {
'date_forecast': forecast.get('DateForecast'),
'reporting_area': forecast.get('ReportingArea'),
'state_code': forecast.get('StateCode'),
'latitude': forecast.get('Latitude'),
'longitude': forecast.get('Longitude'),
'parameter_name': forecast.get('ParameterName'),
'aqi': forecast.get('AQI'),
'category': self._get_aqi_category(forecast.get('AQI', 0)),
'action_day': forecast.get('ActionDay', False),
'discussion': forecast.get('Discussion', '')
}
def _calculate_overall_aqi(self, measurements: List[Dict]) -> Dict:
"""Calculate overall AQI from individual measurements"""
if not measurements:
return {}
# Find the highest AQI value (worst air quality)
max_aqi = 0
primary_pollutant = None
for measurement in measurements:
aqi = measurement.get('aqi', 0)
if aqi and aqi > max_aqi:
max_aqi = aqi
primary_pollutant = measurement.get('parameter_name')
return {
'aqi': max_aqi,
'category': self._get_aqi_category(max_aqi),
'primary_pollutant': primary_pollutant,
'health_message': self._get_health_message(max_aqi)
}
def _get_aqi_category(self, aqi: int) -> Dict[str, str]:
"""Get AQI category information"""
if aqi <= 50:
return {
'level': 'Good',
'color': 'Green',
'description': 'Air quality is satisfactory'
}
elif aqi <= 100:
return {
'level': 'Moderate',
'color': 'Yellow',
'description': 'Air quality is acceptable for most people'
}
elif aqi <= 150:
return {
'level': 'Unhealthy for Sensitive Groups',
'color': 'Orange',
'description': 'Sensitive individuals may experience problems'
}
elif aqi <= 200:
return {
'level': 'Unhealthy',
'color': 'Red',
'description': 'Everyone may experience problems'
}
elif aqi <= 300:
return {
'level': 'Very Unhealthy',
'color': 'Purple',
'description': 'Health warnings for everyone'
}
else:
return {
'level': 'Hazardous',
'color': 'Maroon',
'description': 'Emergency conditions affecting everyone'
}
def _get_health_message(self, aqi: int) -> str:
"""Get health message based on AQI"""
if aqi <= 50:
return "Air quality is good. Ideal for outdoor activities."
elif aqi <= 100:
return "Air quality is acceptable. Sensitive individuals should consider limiting prolonged outdoor exertion."
elif aqi <= 150:
return "Sensitive groups should reduce outdoor activities. Others can continue normal activities."
elif aqi <= 200:
return "Everyone should limit outdoor activities, especially prolonged exertion."
elif aqi <= 300:
return "Avoid outdoor activities. Stay indoors with windows closed."
else:
return "Health alert: avoid all outdoor activities. Emergency conditions."
def format_aqi_summary(self, aqi_data: Dict) -> str:
"""Format AQI data for display"""
if not aqi_data:
return "🌿 Air quality data not available"
overall = aqi_data.get('overall', {})
aqi = overall.get('aqi', 0)
category = overall.get('category', {})
level = category.get('level', 'Unknown')
health_message = overall.get('health_message', '')
primary_pollutant = overall.get('primary_pollutant', 'Unknown')
# Get emoji based on AQI level
if aqi <= 50:
emoji = "🟒"
elif aqi <= 100:
emoji = "🟑"
elif aqi <= 150:
emoji = "🟠"
elif aqi <= 200:
emoji = "πŸ”΄"
elif aqi <= 300:
emoji = "🟣"
else:
emoji = "πŸ”΄"
summary = f"{emoji} **Air Quality Index: {aqi}** ({level})\n"
summary += f"**Primary Pollutant:** {primary_pollutant}\n"
summary += f"**Health Advisory:** {health_message}\n"
# Add detailed measurements
measurements = aqi_data.get('measurements', [])
if measurements:
summary += "\n**Detailed Measurements:**\n"
for measurement in measurements[:3]: # Show top 3
param = measurement.get('parameter_name', 'Unknown')
aqi_val = measurement.get('aqi', 0)
area = measurement.get('reporting_area', 'Unknown')
summary += f"β€’ {param}: {aqi_val} AQI ({area})\n"
return summary
def get_setup_instructions(self) -> str:
"""Return instructions for setting up AirNow API access"""
return """
# 🌿 Air Quality Data Setup
To unlock air quality monitoring features:
## 1. Get a FREE AirNow API Key
1. Visit: https://docs.airnowapi.org/account/request/
2. Fill out the request form with your application details
3. You'll receive an API key via email (usually within 1-2 business days)
## 2. Configure Your API Key
Add your key to the environment:
```bash
$env:AIRNOW_API_KEY="your_key_here"
```
## 3. Available Air Quality Features
With the API key configured, you'll unlock:
- **Current Air Quality**: Real-time AQI readings
- **AQI Forecasts**: Next-day air quality predictions
- **Health Advisories**: Personalized health recommendations
- **Pollutant Details**: PM2.5, PM10, Ozone, NO2, SO2, CO levels
- **Location-based Data**: ZIP code and coordinate-based lookups
- **Multi-site Coverage**: Data from 2000+ monitoring sites
## 4. Enhanced Weather + Air Quality
The AI will be able to answer questions like:
- "What's the air quality in Los Angeles?"
- "Is it safe to exercise outdoors today?"
- "Air quality forecast for this weekend"
- "Compare air quality between cities"
**Note**: Basic weather features work without this key, but air quality
features will show setup instructions instead of data.
"""
def create_air_quality_client(api_key: Optional[str] = None) -> AirQualityClient:
"""Factory function to create Air Quality client"""
return AirQualityClient(api_key)