import requests import json from datetime import datetime, date from typing import Dict, Any, Optional import logging # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class WeatherService: """Service class for weather data integration""" def __init__(self, api_key: str, base_url: str = "http://api.openweathermap.org/data/2.5"): """Initialize weather service""" self.api_key = api_key self.base_url = base_url def get_current_weather(self, latitude: float, longitude: float) -> Dict[str, Any]: """ Get current weather data for a location Args: latitude: Location latitude longitude: Location longitude Returns: Dictionary with weather data """ url = f"{self.base_url}/weather" params = { 'lat': latitude, 'lon': longitude, 'appid': self.api_key, 'units': 'metric' # Celsius } try: response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() # Keep the original format for template compatibility logger.info(f"Weather data fetched successfully for coordinates ({latitude}, {longitude})") return data except requests.exceptions.RequestException as e: logger.error(f"Error fetching weather data: {str(e)}") return self._get_fallback_weather() except Exception as e: logger.error(f"Unexpected error in weather service: {str(e)}") return self._get_fallback_weather() def get_weather_forecast(self, latitude: float, longitude: float, days: int = 5) -> list: """ Get weather forecast for multiple days Args: latitude: Location latitude longitude: Location longitude days: Number of days to forecast (max 5 for free tier) Returns: List of weather data for each day """ url = f"{self.base_url}/forecast" params = { 'lat': latitude, 'lon': longitude, 'appid': self.api_key, 'units': 'metric' } try: response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() forecast_data = [] # Group forecasts by date daily_forecasts = {} for item in data['list']: forecast_date = datetime.fromtimestamp(item['dt']).date() if forecast_date not in daily_forecasts: daily_forecasts[forecast_date] = [] daily_forecasts[forecast_date].append(item) # Process each day's data for forecast_date, day_data in list(daily_forecasts.items())[:days]: temps = [item['main']['temp'] for item in day_data] humidity_vals = [item['main']['humidity'] for item in day_data] rainfall = sum([item.get('rain', {}).get('3h', 0) for item in day_data]) daily_weather = { 'date': forecast_date, 'temperature_min': min(temps), 'temperature_max': max(temps), 'humidity': sum(humidity_vals) / len(humidity_vals), 'rainfall': rainfall, 'wind_speed': day_data[0]['wind'].get('speed', 0) * 3.6, 'weather_condition': day_data[0]['weather'][0].get('description', '').title(), 'weather_main': day_data[0]['weather'][0].get('main', '') } forecast_data.append(daily_weather) logger.info(f"Weather forecast fetched successfully for {days} days") return forecast_data except requests.exceptions.RequestException as e: logger.error(f"Error fetching weather forecast: {str(e)}") return [self._get_fallback_weather() for _ in range(days)] except Exception as e: logger.error(f"Unexpected error in weather forecast: {str(e)}") return [self._get_fallback_weather() for _ in range(days)] def get_weather_alerts(self, latitude: float, longitude: float) -> list: """ Get weather alerts for a location Args: latitude: Location latitude longitude: Location longitude Returns: List of weather alerts """ # Using One Call API for alerts (requires separate endpoint) url = f"{self.base_url.replace('/data/2.5', '/data/3.0')}/onecall" params = { 'lat': latitude, 'lon': longitude, 'appid': self.api_key, 'exclude': 'minutely,hourly' } try: response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() alerts = data.get('alerts', []) processed_alerts = [] for alert in alerts: processed_alert = { 'sender_name': alert.get('sender_name', 'Weather Service'), 'event': alert.get('event', 'Weather Alert'), 'description': alert.get('description', 'No description available'), 'start_time': datetime.fromtimestamp(alert.get('start', 0)), 'end_time': datetime.fromtimestamp(alert.get('end', 0)), 'tags': alert.get('tags', []) } processed_alerts.append(processed_alert) return processed_alerts except Exception as e: logger.warning(f"Weather alerts not available: {str(e)}") return [] def _get_fallback_weather(self) -> Dict[str, Any]: """Provide fallback weather data when API fails""" # Match the OpenWeatherMap API structure for compatibility with the template current_time = datetime.now() return { 'name': 'Unknown Location', 'main': { 'temp': 25.0, 'temp_min': 20.0, 'temp_max': 30.0, 'humidity': 60.0, 'pressure': 1013.0 }, 'weather': [ { 'main': 'Unknown', 'description': 'Weather data unavailable', 'icon': '03d' # Default cloud icon } ], 'wind': { 'speed': 10.0 / 3.6, # km/h to m/s 'deg': 180 }, 'clouds': { 'all': 50 }, 'visibility': 10000, # m 'sys': { 'sunrise': int(current_time.replace(hour=6, minute=0).timestamp()), 'sunset': int(current_time.replace(hour=18, minute=0).timestamp()) }, 'dt': int(current_time.timestamp()) } def is_weather_suitable_for_activity(self, weather_data: Dict[str, Any], activity_type: str) -> bool: """ Check if weather is suitable for a specific farming activity Args: weather_data: Current weather data activity_type: Type of farming activity Returns: Boolean indicating if weather is suitable """ temperature = weather_data.get('main', {}).get('temp', 25) humidity = weather_data.get('main', {}).get('humidity', 60) rainfall = 0 # OpenWeatherMap doesn't provide rainfall in the base API wind_speed = weather_data.get('wind', {}).get('speed', 0) * 3.6 # Convert m/s to km/h activity_guidelines = { 'irrigation': { 'max_temperature': 35, 'max_rainfall': 5, 'max_wind_speed': 20 }, 'fertilizer': { 'max_temperature': 30, 'max_rainfall': 2, 'max_wind_speed': 15 }, 'pesticide': { 'max_temperature': 28, 'max_rainfall': 1, 'max_wind_speed': 10, 'min_humidity': 40 }, 'harvesting': { 'max_rainfall': 1, 'max_wind_speed': 25 }, 'planting': { 'max_rainfall': 10, 'max_wind_speed': 20, 'min_temperature': 15 } } guidelines = activity_guidelines.get(activity_type, {}) # Check each constraint if guidelines.get('max_temperature') and temperature > guidelines['max_temperature']: return False if guidelines.get('min_temperature') and temperature < guidelines['min_temperature']: return False if guidelines.get('max_rainfall') and rainfall > guidelines['max_rainfall']: return False if guidelines.get('max_wind_speed') and wind_speed > guidelines['max_wind_speed']: return False if guidelines.get('min_humidity') and humidity < guidelines['min_humidity']: return False return True class AgroWeatherService(WeatherService): """Extended weather service with agricultural focus""" def get_crop_specific_weather_advice(self, weather_data: Dict[str, Any], crop_type: str) -> Dict[str, str]: """ Get crop-specific weather advice Args: weather_data: Current weather data crop_type: Type of crop Returns: Dictionary with weather-based advice """ temperature = weather_data.get('main', {}).get('temp', 25) rainfall = 0 # OpenWeatherMap doesn't provide rainfall in the base API humidity = weather_data.get('main', {}).get('humidity', 60) crop_advice = { 'rice': self._get_rice_weather_advice(temperature, rainfall, humidity), 'wheat': self._get_wheat_weather_advice(temperature, rainfall, humidity), 'cotton': self._get_cotton_weather_advice(temperature, rainfall, humidity), 'sugarcane': self._get_sugarcane_weather_advice(temperature, rainfall, humidity), 'maize': self._get_maize_weather_advice(temperature, rainfall, humidity) } return crop_advice.get(crop_type.lower(), { 'advice': 'Monitor weather conditions regularly', 'warning': 'Follow general farming practices' }) def _get_rice_weather_advice(self, temp: float, rainfall: float, humidity: float) -> Dict[str, str]: """Get rice-specific weather advice""" if rainfall > 50: return {'advice': 'Ensure proper drainage', 'warning': 'Risk of flooding'} elif temp > 35: return {'advice': 'Increase irrigation frequency', 'warning': 'High temperature stress'} elif humidity < 70: return {'advice': 'Monitor for pest activity', 'warning': 'Low humidity may affect growth'} else: return {'advice': 'Good conditions for rice growth', 'warning': 'Continue normal operations'} def _get_wheat_weather_advice(self, temp: float, rainfall: float, humidity: float) -> Dict[str, str]: """Get wheat-specific weather advice""" if temp < 10: return {'advice': 'Protect from frost', 'warning': 'Cold weather may damage crop'} elif rainfall > 25: return {'advice': 'Avoid field operations', 'warning': 'Excess moisture may cause fungal diseases'} elif temp > 30: return {'advice': 'Harvest if ready', 'warning': 'High temperature may reduce grain quality'} else: return {'advice': 'Optimal conditions for wheat', 'warning': 'Continue scheduled activities'} def _get_cotton_weather_advice(self, temp: float, rainfall: float, humidity: float) -> Dict[str, str]: """Get cotton-specific weather advice""" if humidity > 85: return {'advice': 'Monitor for bollworm', 'warning': 'High humidity favors pest development'} elif temp < 15: return {'advice': 'Delay planting', 'warning': 'Cotton needs warm weather for germination'} elif rainfall > 30: return {'advice': 'Ensure drainage', 'warning': 'Waterlogging can damage cotton plants'} else: return {'advice': 'Good conditions for cotton', 'warning': 'Monitor for pest activity'} def _get_sugarcane_weather_advice(self, temp: float, rainfall: float, humidity: float) -> Dict[str, str]: """Get sugarcane-specific weather advice""" if temp > 38: return {'advice': 'Increase irrigation', 'warning': 'High temperature stress on sugarcane'} elif rainfall < 5 and humidity < 60: return {'advice': 'Irrigate immediately', 'warning': 'Drought conditions detected'} elif rainfall > 100: return {'advice': 'Avoid harvesting', 'warning': 'Excess water may reduce sugar content'} else: return {'advice': 'Favorable conditions', 'warning': 'Continue normal care'} def _get_maize_weather_advice(self, temp: float, rainfall: float, humidity: float) -> Dict[str, str]: """Get maize-specific weather advice""" if temp < 12: return {'advice': 'Delay sowing', 'warning': 'Cold weather affects maize germination'} elif rainfall > 40: return {'advice': 'Check for waterlogging', 'warning': 'Excess water may cause root rot'} elif temp > 35 and humidity < 50: return {'advice': 'Irrigate and mulch', 'warning': 'Hot and dry conditions stress maize'} else: return {'advice': 'Good growing conditions', 'warning': 'Monitor for pest activity'}