Restore working weather manager from main branch

This commit is contained in:
ChuckBuilds
2025-04-19 17:30:08 -05:00
parent 48909001e8
commit 0a609910c6

View File

@@ -1,15 +1,10 @@
import requests import requests
import time import time
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional from typing import Dict, Any, List
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
from .weather_icons import WeatherIcons from .weather_icons import WeatherIcons
from .cache_manager import CacheManager from .cache_manager import CacheManager
import logging
import threading
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from requests.exceptions import RequestException, SSLError, ConnectionError
class WeatherManager: class WeatherManager:
# Weather condition to larger colored icons (we'll use these as placeholders until you provide custom ones) # Weather condition to larger colored icons (we'll use these as placeholders until you provide custom ones)
@@ -32,17 +27,11 @@ class WeatherManager:
} }
def __init__(self, config: Dict[str, Any], display_manager): def __init__(self, config: Dict[str, Any], display_manager):
self.logger = logging.getLogger(__name__)
self.config = config self.config = config
self.display_manager = display_manager self.display_manager = display_manager
self.weather_config = config.get('weather', {}) self.weather_config = config.get('weather', {})
self.location = config.get('location', {}) self.location = config.get('location', {})
self._last_update = 0 self.last_update = 0
self._update_interval = config.get('update_interval', 600) # 10 minutes default
self._last_state = None
self._processing_lock = threading.Lock()
self._cached_processed_data = None
self._cache_timestamp = 0
self.weather_data = None self.weather_data = None
self.forecast_data = None self.forecast_data = None
self.hourly_forecast = None self.hourly_forecast = None
@@ -71,225 +60,140 @@ class WeatherManager:
self.last_hourly_state = None self.last_hourly_state = None
self.last_daily_state = None self.last_daily_state = None
# Configure retry strategy def _fetch_weather(self) -> None:
self.session = requests.Session() """Fetch current weather and forecast data from OpenWeatherMap API."""
retry_strategy = Retry( api_key = self.weather_config.get('api_key')
total=3, # number of retries if not api_key:
backoff_factor=0.5, # wait 0.5, 1, 2 seconds between retries print("No API key configured for weather")
status_forcelist=[500, 502, 503, 504], # HTTP status codes to retry on return
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("https://", adapter)
self.session.mount("http://", adapter)
def _process_forecast_data(self, data: Dict[str, Any]) -> Dict[str, Any]: # Try to get cached data first
"""Process forecast data with caching.""" cached_data = self.cache_manager.get_cached_data('weather')
current_time = time.time() if cached_data:
self.weather_data = cached_data.get('current')
self.forecast_data = cached_data.get('forecast')
if self.weather_data and self.forecast_data:
self._process_forecast_data(self.forecast_data)
self.last_update = time.time()
print("Using cached weather data")
return
# Check if we have valid cached processed data city = self.location['city']
if (self._cached_processed_data and state = self.location['state']
current_time - self._cache_timestamp < 60): # Cache for 1 minute country = self.location['country']
return self._cached_processed_data units = self.weather_config.get('units', 'imperial')
with self._processing_lock: # First get coordinates using geocoding API
# Double check after acquiring lock geo_url = f"http://api.openweathermap.org/geo/1.0/direct?q={city},{state},{country}&limit=1&appid={api_key}"
if (self._cached_processed_data and
current_time - self._cache_timestamp < 60):
return self._cached_processed_data
try:
# Process the data
processed_data = {
'current': self._process_current_conditions(data.get('current', {})),
'hourly': self._process_hourly_forecast(data.get('hourly', [])),
'daily': self._process_daily_forecast(data.get('daily', []))
}
# Cache the processed data
self._cached_processed_data = processed_data
self._cache_timestamp = current_time
return processed_data
except Exception as e:
self.logger.error(f"Error processing forecast data: {e}")
return {}
def _fetch_weather(self) -> Optional[Dict[str, Any]]:
"""Fetch weather data with optimized caching."""
current_time = time.time()
# Check if we need to update
if current_time - self._last_update < self._update_interval:
cached_data = self.cache_manager.get_cached_data('weather', max_age=self._update_interval)
if cached_data and self._is_valid_weather_data(cached_data):
return self._process_forecast_data(cached_data)
# If cache is invalid, continue to fetch new data
self.logger.debug("Cache invalid or expired, fetching new weather data")
try: try:
# Fetch new data from OpenWeatherMap API # Get coordinates
api_key = self.weather_config.get('api_key') response = requests.get(geo_url)
if not api_key: response.raise_for_status()
self.logger.error("No API key configured for OpenWeatherMap") geo_data = response.json()
return None
# Construct full location string if not geo_data:
city = self.location.get('city') print(f"Could not find coordinates for {city}, {state}")
state = self.location.get('state') return
country = self.location.get('country')
if not city: lat = geo_data[0]['lat']
self.logger.error("No city configured for weather") lon = geo_data[0]['lon']
return None
# Build location string with state and country if available # Get current weather and forecast using coordinates
location = city weather_url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}&units={units}"
if state: forecast_url = f"https://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={api_key}&units={units}"
location += f",{state}"
if country:
location += f",{country}"
units = self.weather_config.get('units', 'imperial')
# Only log at debug level since this happens frequently
self.logger.debug(f"Fetching weather for location: {location}, units: {units}")
# Fetch current weather # Fetch current weather
current_url = "https://api.openweathermap.org/data/2.5/weather" response = requests.get(weather_url)
params = { response.raise_for_status()
"q": location, self.weather_data = response.json()
"appid": api_key,
"units": units
}
try:
self.logger.debug(f"Making request to {current_url} with params: {params}")
response = self.session.get(current_url, params=params, timeout=10)
if response.status_code == 401:
self.logger.error("Invalid API key for OpenWeatherMap")
return None
response.raise_for_status()
current_data = response.json()
self.logger.debug(f"Current weather response: {current_data}")
except (ConnectionError, RequestException) as e:
self.logger.error(f"Network error fetching current weather: {e}")
# Try to use cached data as fallback
cached_data = self.cache_manager.get_cached_data('weather', max_age=self._update_interval)
if cached_data and self._is_valid_weather_data(cached_data):
self.logger.debug("Using cached weather data due to network error")
return self._process_forecast_data(cached_data)
return None
# Fetch forecast # Fetch forecast
forecast_url = "https://api.openweathermap.org/data/2.5/forecast" response = requests.get(forecast_url)
try: response.raise_for_status()
self.logger.debug(f"Making request to {forecast_url} with params: {params}") self.forecast_data = response.json()
response = self.session.get(forecast_url, params=params, timeout=10)
if response.status_code == 401:
self.logger.error("Invalid API key for OpenWeatherMap")
return None
response.raise_for_status()
forecast_data = response.json()
self.logger.debug(f"Forecast response: {forecast_data}")
except (ConnectionError, RequestException) as e:
self.logger.error(f"Network error fetching forecast: {e}")
# If we have current data but forecast failed, use cached forecast if available
cached_data = self.cache_manager.get_cached_data('weather', max_age=self._update_interval)
if cached_data and 'hourly' in cached_data and self._is_valid_weather_data(cached_data):
self.logger.debug("Using cached forecast data due to network error")
forecast_data = {'list': cached_data['hourly']}
else:
return None
# Combine the data # Process forecast data
data = { self._process_forecast_data(self.forecast_data)
"current": current_data,
"hourly": forecast_data.get("list", []), # Cache the new data
"daily": [] # Daily forecast will be processed from hourly data cache_data = {
'current': self.weather_data,
'forecast': self.forecast_data
} }
self.cache_manager.update_cache('weather', cache_data)
self._last_update = current_time self.last_update = time.time()
self.cache_manager.save_cache('weather', data) print("Weather data updated successfully")
return self._process_forecast_data(data)
except Exception as e: except Exception as e:
self.logger.error(f"Error fetching weather data: {e}") print(f"Error fetching weather data: {e}")
# Try to use cached data as fallback # If we have cached data, use it as fallback
cached_data = self.cache_manager.get_cached_data('weather', max_age=self._update_interval) if cached_data:
if cached_data and self._is_valid_weather_data(cached_data): self.weather_data = cached_data.get('current')
self.logger.debug("Using cached weather data as fallback") self.forecast_data = cached_data.get('forecast')
return self._process_forecast_data(cached_data) if self.weather_data and self.forecast_data:
return None self._process_forecast_data(self.forecast_data)
print("Using cached weather data as fallback")
else:
self.weather_data = None
self.forecast_data = None
def _is_valid_weather_data(self, data: Dict[str, Any]) -> bool: def _process_forecast_data(self, forecast_data: Dict[str, Any]) -> None:
"""Validate weather data structure.""" """Process forecast data into hourly and daily forecasts."""
try: if not forecast_data:
# Check if data has required fields return
if not data or not isinstance(data, dict):
return False
# Check current weather data # Process hourly forecast (next 5 hours)
current = data.get('current') hourly_list = forecast_data.get('list', [])[:5] # Changed from 6 to 5 to match image
if not current or not isinstance(current, dict): self.hourly_forecast = []
return False
# Check for required current weather fields for hour_data in hourly_list:
required_current_fields = ['temp', 'weather'] dt = datetime.fromtimestamp(hour_data['dt'])
if not all(field in current for field in required_current_fields): temp = round(hour_data['main']['temp'])
return False condition = hour_data['weather'][0]['main']
self.hourly_forecast.append({
'hour': dt.strftime('%I:00 %p').lstrip('0'), # Format as "2:00 PM"
'temp': temp,
'condition': condition
})
# Check weather description # Process daily forecast
weather = current.get('weather', []) daily_data = {}
if not weather or not isinstance(weather, list) or not weather[0].get('description'): full_forecast_list = forecast_data.get('list', []) # Use the full list
return False for item in full_forecast_list: # Iterate over the full list
date = datetime.fromtimestamp(item['dt']).strftime('%Y-%m-%d')
if date not in daily_data:
daily_data[date] = {
'temps': [],
'conditions': [],
'date': datetime.fromtimestamp(item['dt'])
}
daily_data[date]['temps'].append(item['main']['temp'])
daily_data[date]['conditions'].append(item['weather'][0]['main'])
# Check hourly forecast # Calculate daily summaries, excluding today
hourly = data.get('hourly', []) self.daily_forecast = []
if not hourly or not isinstance(hourly, list): today_str = datetime.now().strftime('%Y-%m-%d')
return False
return True # Sort data by date to ensure chronological order
except Exception as e: sorted_daily_items = sorted(daily_data.items(), key=lambda item: item[1]['date'])
self.logger.debug(f"Error validating weather data: {e}")
return False
def _process_current_conditions(self, current: Dict[str, Any]) -> Dict[str, Any]: # Filter out today's data and take the next 3 days
"""Process current conditions with minimal processing.""" future_days_data = [item for item in sorted_daily_items if item[0] != today_str][:3]
if not current:
return {}
return { for date_str, data in future_days_data:
'temp': current.get('temp', 0), temps = data['temps']
'feels_like': current.get('feels_like', 0), temp_high = round(max(temps))
'humidity': current.get('humidity', 0), temp_low = round(min(temps))
'wind_speed': current.get('wind_speed', 0), condition = max(set(data['conditions']), key=data['conditions'].count)
'description': current.get('weather', [{}])[0].get('description', ''),
'icon': current.get('weather', [{}])[0].get('icon', '')
}
def _process_hourly_forecast(self, hourly: List[Dict[str, Any]]) -> List[Dict[str, Any]]: self.daily_forecast.append({
"""Process hourly forecast with minimal processing.""" 'date': data['date'].strftime('%a'), # Day name (Mon, Tue, etc.)
if not hourly: 'date_str': data['date'].strftime('%m/%d'), # Date (4/8, 4/9, etc.)
return [] 'temp_high': temp_high,
'temp_low': temp_low,
return [{ 'condition': condition
'time': hour.get('dt', 0), })
'temp': hour.get('temp', 0),
'description': hour.get('weather', [{}])[0].get('description', ''),
'icon': hour.get('weather', [{}])[0].get('icon', '')
} for hour in hourly[:24]] # Only process next 24 hours
def _process_daily_forecast(self, daily: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Process daily forecast with minimal processing."""
if not daily:
return []
return [{
'time': day.get('dt', 0),
'temp_min': day.get('temp', {}).get('min', 0),
'temp_max': day.get('temp', {}).get('max', 0),
'description': day.get('weather', [{}])[0].get('description', ''),
'icon': day.get('weather', [{}])[0].get('icon', '')
} for day in daily[:7]] # Only process next 7 days
def get_weather(self) -> Dict[str, Any]: def get_weather(self) -> Dict[str, Any]:
"""Get current weather data, fetching new data if needed.""" """Get current weather data, fetching new data if needed."""
@@ -298,7 +202,7 @@ class WeatherManager:
# Check if we need to update based on time or if we have no data # Check if we need to update based on time or if we have no data
if (not self.weather_data or if (not self.weather_data or
current_time - self._last_update > update_interval): current_time - self.last_update > update_interval):
# Check if data has changed before fetching # Check if data has changed before fetching
current_state = self._get_weather_state() current_state = self._get_weather_state()