Spaces:
Sleeping
Sleeping
""" | |
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) | |