diff --git a/config/config.json b/config/config.json index 01b7f8a3..2d93999f 100644 --- a/config/config.json +++ b/config/config.json @@ -20,7 +20,7 @@ "disable_hardware_pulsing": true, "inverse_colors": false, "show_refresh_rate": true, - "limit_refresh_rate_hz": 100 + "limit_refresh_rate_hz": 0 }, "runtime": { "gpio_slowdown": 2 diff --git a/src/display_controller.py b/src/display_controller.py index 4cc2fa31..8a9ce615 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -19,6 +19,8 @@ class DisplayController: self.weather = WeatherManager(self.config, self.display_manager) self.current_display = 'clock' self.last_switch = time.time() + self.hourly_index = 0 + self.last_hourly_update = time.time() logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager)) def run(self): @@ -27,26 +29,48 @@ class DisplayController: while True: current_time = time.time() rotation_interval = self.config['display'].get('rotation_interval', 15) + hourly_scroll_interval = 3 # Show each hour for 3 seconds # Track if we're switching modes switching_modes = False # Switch display if interval has passed if current_time - self.last_switch > rotation_interval: - logger.info("Switching display from %s to %s", - self.current_display, - 'weather' if self.current_display == 'clock' else 'clock') - self.current_display = 'weather' if self.current_display == 'clock' else 'clock' + # Cycle through: clock -> current weather -> hourly forecast -> daily forecast + if self.current_display == 'clock': + self.current_display = 'weather' + elif self.current_display == 'weather': + self.current_display = 'hourly' + self.hourly_index = 0 + self.last_hourly_update = current_time + elif self.current_display == 'hourly': + self.current_display = 'daily' + else: # daily + self.current_display = 'clock' + + logger.info("Switching display to: %s", self.current_display) self.last_switch = current_time switching_modes = True + # Update hourly forecast index if needed + if self.current_display == 'hourly' and current_time - self.last_hourly_update > hourly_scroll_interval: + self.hourly_index = (self.hourly_index + 1) % 6 # We show 6 hours + self.last_hourly_update = current_time + switching_modes = True # Force clear for new hour + # Display current screen if self.current_display == 'clock': logger.debug("Updating clock display") self.clock.display_time(force_clear=switching_modes) - else: - logger.debug("Updating weather display") + elif self.current_display == 'weather': + logger.debug("Updating current weather display") self.weather.display_weather(force_clear=switching_modes) + elif self.current_display == 'hourly': + logger.debug("Updating hourly forecast display") + self.weather.display_hourly_forecast(self.hourly_index, force_clear=switching_modes) + else: # daily + logger.debug("Updating daily forecast display") + self.weather.display_daily_forecast(force_clear=switching_modes) # Sleep for 0.5 seconds since we only need to check for second changes time.sleep(0.5) diff --git a/src/weather_manager.py b/src/weather_manager.py index db0b3ea5..61983a13 100644 --- a/src/weather_manager.py +++ b/src/weather_manager.py @@ -1,9 +1,29 @@ import requests import time -from typing import Dict, Any +from datetime import datetime +from typing import Dict, Any, List from PIL import Image, ImageDraw class WeatherManager: + # Weather condition to emoji mapping + WEATHER_ICONS = { + 'Clear': '☀️', + 'Clouds': '☁️', + 'Rain': '🌧️', + 'Snow': '❄️', + 'Thunderstorm': '⛈️', + 'Drizzle': '🌦️', + 'Mist': '🌫️', + 'Fog': '🌫️', + 'Haze': '🌫️', + 'Smoke': '🌫️', + 'Dust': '🌫️', + 'Sand': '🌫️', + 'Ash': '🌫️', + 'Squall': '💨', + 'Tornado': '🌪️' + } + def __init__(self, config: Dict[str, Any], display_manager): self.config = config self.display_manager = display_manager @@ -11,25 +31,93 @@ class WeatherManager: self.location = config.get('location', {}) self.last_update = 0 self.weather_data = None + self.forecast_data = None + self.hourly_forecast = None + self.daily_forecast = None def _fetch_weather(self) -> None: - """Fetch weather data from OpenWeatherMap API.""" - api_key = self.weather_config['api_key'] + """Fetch current weather and forecast data from OpenWeatherMap API.""" + api_key = self.weather_config.get('api_key') + if not api_key: + print("No API key configured for weather") + return + city = self.location['city'] state = self.location['state'] country = self.location['country'] units = self.weather_config.get('units', 'imperial') - url = f"http://api.openweathermap.org/data/2.5/weather?q={city},{state},{country}&appid={api_key}&units={units}" + # Get current weather + current_url = f"http://api.openweathermap.org/data/2.5/weather?q={city},{state},{country}&appid={api_key}&units={units}" + + # Get forecast data (includes hourly and daily) + forecast_url = f"http://api.openweathermap.org/data/2.5/forecast?q={city},{state},{country}&appid={api_key}&units={units}" try: - response = requests.get(url) + # Fetch current weather + response = requests.get(current_url) response.raise_for_status() self.weather_data = response.json() + + # Fetch forecast + response = requests.get(forecast_url) + response.raise_for_status() + self.forecast_data = response.json() + + # Process forecast data + self._process_forecast_data() + self.last_update = time.time() except Exception as e: print(f"Error fetching weather data: {e}") self.weather_data = None + self.forecast_data = None + + def _process_forecast_data(self) -> None: + """Process the forecast data into hourly and daily forecasts.""" + if not self.forecast_data: + return + + # Process hourly forecast (next 6 hours) + self.hourly_forecast = [] + for item in self.forecast_data['list'][:6]: # First 6 entries (3 hours each) + hour = datetime.fromtimestamp(item['dt']).strftime('%I%p') + temp = round(item['main']['temp']) + condition = item['weather'][0]['main'] + icon = self.WEATHER_ICONS.get(condition, '❓') + self.hourly_forecast.append({ + 'hour': hour, + 'temp': temp, + 'condition': condition, + 'icon': icon + }) + + # Process daily forecast (next 3 days) + daily_data = {} + for item in self.forecast_data['list']: + date = datetime.fromtimestamp(item['dt']).strftime('%Y-%m-%d') + if date not in daily_data: + daily_data[date] = { + 'temps': [], + 'conditions': [] + } + daily_data[date]['temps'].append(item['main']['temp']) + daily_data[date]['conditions'].append(item['weather'][0]['main']) + + # Calculate daily summaries + self.daily_forecast = [] + for date, data in list(daily_data.items())[:3]: # First 3 days + avg_temp = round(sum(data['temps']) / len(data['temps'])) + # Get most common condition for the day + condition = max(set(data['conditions']), key=data['conditions'].count) + icon = self.WEATHER_ICONS.get(condition, '❓') + display_date = datetime.strptime(date, '%Y-%m-%d').strftime('%a %d') + self.daily_forecast.append({ + 'date': display_date, + 'temp': avg_temp, + 'condition': condition, + 'icon': icon + }) def get_weather(self) -> Dict[str, Any]: """Get current weather data, fetching new data if needed.""" @@ -40,16 +128,51 @@ class WeatherManager: return self.weather_data def display_weather(self, force_clear: bool = False) -> None: - """Display weather information on the LED matrix.""" + """Display current weather information on the LED matrix.""" weather_data = self.get_weather() if not weather_data: return temp = round(weather_data['main']['temp']) condition = weather_data['weather'][0]['main'] + icon = self.WEATHER_ICONS.get(condition, '❓') - # Format the display string with both temp and condition - display_text = f"{temp}°F\n{condition}" + # Format the display string with temp, icon, and condition + display_text = f"{temp}°F {icon}\n{condition}" # Draw both lines at once using the multi-line support in draw_text + self.display_manager.draw_text(display_text, force_clear=force_clear) + + def display_hourly_forecast(self, index: int = 0, force_clear: bool = False) -> None: + """Display hourly forecast information, showing one time slot at a time.""" + if not self.hourly_forecast: + self.get_weather() # This will also update forecasts + if not self.hourly_forecast: + return + + # Get the forecast for the current index + forecast = self.hourly_forecast[index % len(self.hourly_forecast)] + + # Format the display string + display_text = f"{forecast['hour']}\n{forecast['temp']}°F {forecast['icon']}" + + # Draw the forecast + self.display_manager.draw_text(display_text, force_clear=force_clear) + + def display_daily_forecast(self, force_clear: bool = False) -> None: + """Display 3-day forecast information.""" + if not self.daily_forecast: + self.get_weather() # This will also update forecasts + if not self.daily_forecast: + return + + # Create a compact display of all three days + lines = [] + for day in self.daily_forecast: + lines.append(f"{day['date']}: {day['temp']}°F {day['icon']}") + + # Join all lines with newlines + display_text = "\n".join(lines) + + # Draw the forecast self.display_manager.draw_text(display_text, force_clear=force_clear) \ No newline at end of file