diff --git a/config/config.json b/config/config.json index 2d93999f..01b7f8a3 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": 0 + "limit_refresh_rate_hz": 100 }, "runtime": { "gpio_slowdown": 2 diff --git a/src/display_manager.py b/src/display_manager.py index 6b88303f..4e9f3ec0 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -1,8 +1,10 @@ from rgbmatrix import RGBMatrix, RGBMatrixOptions from PIL import Image, ImageDraw, ImageFont import time -from typing import Dict, Any +from typing import Dict, Any, List import logging +import math +from .weather_icons import WeatherIcons # Configure logging logging.basicConfig(level=logging.INFO) @@ -196,6 +198,111 @@ class DisplayManager: # Update the display with the visible portion self.matrix.SetImage(visible_portion) + def draw_sun(self, x: int, y: int, size: int = 16): + """Draw a sun icon using yellow circles and lines.""" + center = (x + size//2, y + size//2) + radius = size//3 + + # Draw the center circle + self.draw.ellipse([center[0]-radius, center[1]-radius, + center[0]+radius, center[1]+radius], + fill=(255, 255, 0)) # Yellow + + # Draw the rays + ray_length = size//4 + for angle in range(0, 360, 45): + rad = math.radians(angle) + start_x = center[0] + (radius * math.cos(rad)) + start_y = center[1] + (radius * math.sin(rad)) + end_x = center[0] + ((radius + ray_length) * math.cos(rad)) + end_y = center[1] + ((radius + ray_length) * math.sin(rad)) + self.draw.line([start_x, start_y, end_x, end_y], fill=(255, 255, 0), width=2) + + def draw_cloud(self, x: int, y: int, size: int = 16, color=(200, 200, 200)): + """Draw a cloud icon.""" + # Draw multiple circles to form a cloud shape + self.draw.ellipse([x+size//4, y+size//3, x+size//4+size//2, y+size//3+size//2], fill=color) + self.draw.ellipse([x+size//2, y+size//3, x+size//2+size//2, y+size//3+size//2], fill=color) + self.draw.ellipse([x+size//3, y+size//6, x+size//3+size//2, y+size//6+size//2], fill=color) + + def draw_rain(self, x: int, y: int, size: int = 16): + """Draw rain icon with cloud and droplets.""" + # Draw cloud + self.draw_cloud(x, y, size) + + # Draw rain drops + drop_color = (0, 0, 255) # Blue + drop_size = size//6 + for i in range(3): + drop_x = x + size//4 + (i * size//3) + drop_y = y + size//2 + self.draw.line([drop_x, drop_y, drop_x, drop_y+drop_size], + fill=drop_color, width=2) + + def draw_snow(self, x: int, y: int, size: int = 16): + """Draw snow icon with cloud and snowflakes.""" + # Draw cloud + self.draw_cloud(x, y, size) + + # Draw snowflakes + snow_color = (200, 200, 255) # Light blue + for i in range(3): + center_x = x + size//4 + (i * size//3) + center_y = y + size//2 + size//4 + # Draw a small star shape + for angle in range(0, 360, 60): + rad = math.radians(angle) + end_x = center_x + (size//8 * math.cos(rad)) + end_y = center_y + (size//8 * math.sin(rad)) + self.draw.line([center_x, center_y, end_x, end_y], + fill=snow_color, width=1) + + def draw_weather_icon(self, icon_type: str, x: int, y: int, size: int = 16): + """Draw a weather icon based on the condition type.""" + if icon_type == 'Clear': + self.draw_sun(x, y, size) + elif icon_type == 'Clouds': + self.draw_cloud(x, y, size) + elif icon_type in ['Rain', 'Drizzle']: + self.draw_rain(x, y, size) + elif icon_type == 'Snow': + self.draw_snow(x, y, size) + elif icon_type == 'Thunderstorm': + # Draw storm cloud with lightning + self.draw_cloud(x, y, size, color=(100, 100, 100)) + # Add lightning bolt + lightning_color = (255, 255, 0) # Yellow + points = [ + (x + size//2, y + size//2), + (x + size//2 - size//4, y + size//2 + size//4), + (x + size//2, y + size//2 + size//4), + (x + size//2 - size//4, y + size + size//4) + ] + self.draw.line(points, fill=lightning_color, width=2) + else: + # Default to a cloud for unknown conditions + self.draw_cloud(x, y, size) + + def draw_text_with_icons(self, text: str, icons: List[tuple] = None, x: int = None, y: int = None, + color: tuple = (255, 255, 255), force_clear: bool = False): + """Draw text with weather icons at specified positions.""" + if force_clear: + self.clear() + else: + self.image = Image.new('RGB', (self.matrix.width, self.matrix.height)) + self.draw = ImageDraw.Draw(self.image) + + # First draw the text + self.draw_text(text, x, y, color, force_clear=False) + + # Then draw any icons + if icons: + for icon_type, icon_x, icon_y in icons: + WeatherIcons.draw_weather_icon(self.draw, icon_type, icon_x, icon_y) + + # Update the display + self.update_display() + def cleanup(self): """Clean up resources.""" self.matrix.Clear() diff --git a/src/weather_icons.py b/src/weather_icons.py new file mode 100644 index 00000000..76782619 --- /dev/null +++ b/src/weather_icons.py @@ -0,0 +1,135 @@ +from PIL import Image, ImageDraw +import math + +class WeatherIcons: + @staticmethod + def draw_sun(draw: ImageDraw, x: int, y: int, size: int = 16, color: tuple = (255, 200, 0)): + """Draw a sun icon with rays.""" + center_x = x + size // 2 + center_y = y + size // 2 + radius = size // 3 + + # Draw main sun circle + draw.ellipse([ + center_x - radius, center_y - radius, + center_x + radius, center_y + radius + ], fill=color) + + # Draw rays + ray_length = size // 4 + for angle in range(0, 360, 45): + rad = math.radians(angle) + start_x = center_x + (radius * math.cos(rad)) + start_y = center_y + (radius * math.sin(rad)) + end_x = center_x + ((radius + ray_length) * math.cos(rad)) + end_y = center_y + ((radius + ray_length) * math.sin(rad)) + draw.line([start_x, start_y, end_x, end_y], fill=color, width=2) + + @staticmethod + def draw_cloud(draw: ImageDraw, x: int, y: int, size: int = 16, color: tuple = (200, 200, 200)): + """Draw a cloud icon.""" + # Draw multiple circles to form cloud shape + circle_size = size // 2 + positions = [ + (x + size//4, y + size//3), + (x + size//2, y + size//3), + (x + size//3, y + size//6) + ] + + for pos_x, pos_y in positions: + draw.ellipse([ + pos_x, pos_y, + pos_x + circle_size, pos_y + circle_size + ], fill=color) + + @staticmethod + def draw_rain(draw: ImageDraw, x: int, y: int, size: int = 16): + """Draw rain icon with cloud and droplets.""" + # Draw cloud first + WeatherIcons.draw_cloud(draw, x, y, size) + + # Draw rain drops + drop_color = (0, 150, 255) # Light blue + drop_length = size // 3 + drop_spacing = size // 4 + + for i in range(3): + drop_x = x + size//4 + (i * drop_spacing) + drop_y = y + size//2 + draw.line([ + drop_x, drop_y, + drop_x - 2, drop_y + drop_length + ], fill=drop_color, width=2) + + @staticmethod + def draw_snow(draw: ImageDraw, x: int, y: int, size: int = 16): + """Draw snow icon with cloud and snowflakes.""" + # Draw cloud first + WeatherIcons.draw_cloud(draw, x, y, size) + + # Draw snowflakes + snow_color = (200, 200, 255) # Light blue-white + flake_size = size // 6 + flake_spacing = size // 4 + + for i in range(3): + center_x = x + size//4 + (i * flake_spacing) + center_y = y + size//2 + + # Draw 6-point snowflake + for angle in range(0, 360, 60): + rad = math.radians(angle) + end_x = center_x + (flake_size * math.cos(rad)) + end_y = center_y + (flake_size * math.sin(rad)) + draw.line([center_x, center_y, end_x, end_y], fill=snow_color, width=1) + + @staticmethod + def draw_thunderstorm(draw: ImageDraw, x: int, y: int, size: int = 16): + """Draw thunderstorm icon with cloud and lightning.""" + # Draw dark cloud + WeatherIcons.draw_cloud(draw, x, y, size, color=(100, 100, 100)) + + # Draw lightning bolt + lightning_color = (255, 255, 0) # Yellow + bolt_points = [ + (x + size//2, y + size//3), + (x + size//2 - size//4, y + size//2), + (x + size//2, y + size//2), + (x + size//2 - size//4, y + size//2 + size//4) + ] + draw.line(bolt_points, fill=lightning_color, width=2) + + @staticmethod + def draw_mist(draw: ImageDraw, x: int, y: int, size: int = 16): + """Draw mist/fog icon.""" + mist_color = (200, 200, 200) # Light gray + wave_height = size // 4 + wave_spacing = size // 3 + + for i in range(3): + wave_y = y + size//3 + (i * wave_spacing) + draw.line([ + x + size//4, wave_y, + x + size//4 + size//2, wave_y + wave_height + ], fill=mist_color, width=2) + + @staticmethod + def draw_weather_icon(draw: ImageDraw, condition: str, x: int, y: int, size: int = 16): + """Draw the appropriate weather icon based on the condition.""" + condition = condition.lower() + + if 'clear' in condition or 'sunny' in condition: + WeatherIcons.draw_sun(draw, x, y, size) + elif 'cloud' in condition: + WeatherIcons.draw_cloud(draw, x, y, size) + elif 'rain' in condition or 'drizzle' in condition: + WeatherIcons.draw_rain(draw, x, y, size) + elif 'snow' in condition: + WeatherIcons.draw_snow(draw, x, y, size) + elif 'thunder' in condition or 'storm' in condition: + WeatherIcons.draw_thunderstorm(draw, x, y, size) + elif 'mist' in condition or 'fog' in condition or 'haze' in condition: + WeatherIcons.draw_mist(draw, x, y, size) + else: + # Default to cloud for unknown conditions + WeatherIcons.draw_cloud(draw, x, y, size) \ No newline at end of file diff --git a/src/weather_manager.py b/src/weather_manager.py index cde914cc..f2a11143 100644 --- a/src/weather_manager.py +++ b/src/weather_manager.py @@ -66,7 +66,7 @@ class WeatherManager: self.forecast_data = response.json() # Process forecast data - self._process_forecast_data() + self._process_forecast_data(self.forecast_data) self.last_update = time.time() except Exception as e: @@ -74,49 +74,35 @@ class WeatherManager: 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: + def _process_forecast_data(self, forecast_data: Dict[str, Any]) -> None: + """Process forecast data into hourly and daily forecasts.""" + if not forecast_data: return # Process hourly forecast (next 6 hours) + hourly = forecast_data.get('hourly', [])[:6] 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').lstrip('0') # Remove leading zero - temp = round(item['main']['temp']) - condition = item['weather'][0]['main'] - icon = self.WEATHER_ICONS.get(condition, '❓') + for hour in hourly: + dt = datetime.fromtimestamp(hour['dt']) + temp = round(hour['temp']) + condition = hour['weather'][0]['main'] self.hourly_forecast.append({ - 'hour': hour, + 'hour': dt.strftime('%I%p'), 'temp': temp, - 'condition': condition, - 'icon': icon + 'condition': condition }) # 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 + daily = forecast_data.get('daily', [])[1:4] # Skip today, get next 3 days self.daily_forecast = [] - for date, data in list(daily_data.items())[:3]: # First 3 days - avg_temp = round(sum(data['temps']) / len(data['temps'])) - 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') + for day in daily: + dt = datetime.fromtimestamp(day['dt']) + temp = round(day['temp']['day']) + condition = day['weather'][0]['main'] self.daily_forecast.append({ - 'date': display_date, - 'temp': avg_temp, - 'condition': condition, - 'icon': icon + 'date': dt.strftime('%a'), + 'temp': temp, + 'condition': condition }) def get_weather(self) -> Dict[str, Any]: @@ -135,13 +121,16 @@ class WeatherManager: temp = round(weather_data['main']['temp']) condition = weather_data['weather'][0]['main'] - icon = self.WEATHER_ICONS.get(condition, '❓') - # Format the display string with temp and large icon - display_text = f"{temp}°F\n{icon}" - - # Draw both lines at once using the multi-line support in draw_text - self.display_manager.draw_text(display_text, force_clear=force_clear) + # Draw temperature text and weather icon + text = f"{temp}°F" + icon_x = (self.display_manager.matrix.width - 20) // 2 # Center the 20px icon + icon_y = 2 # Near the top + self.display_manager.draw_text_with_icons( + text, + icons=[(condition, icon_x, icon_y)], + force_clear=force_clear + ) def display_hourly_forecast(self, scroll_amount: int = 0, force_clear: bool = False) -> None: """Display scrolling hourly forecast information.""" @@ -153,18 +142,29 @@ class WeatherManager: # Update scroll position self.scroll_position = scroll_amount - # Create the full scrolling text + # Create the full scrolling text with icons forecasts = [] - for forecast in self.hourly_forecast: - forecasts.append(f"{forecast['hour']}\n{forecast['temp']}°F\n{forecast['icon']}") - - # Join with some spacing between each forecast + icons = [] + x_offset = self.display_manager.matrix.width - scroll_amount + icon_size = 16 + + for i, forecast in enumerate(self.hourly_forecast): + # Add text + forecasts.append(f"{forecast['hour']}\n{forecast['temp']}°F") + + # Calculate icon position + icon_x = x_offset + (i * (icon_size * 3)) # Space icons out + icon_y = 2 # Near top + icons.append((forecast['condition'], icon_x, icon_y)) + + # Join with spacing display_text = " | ".join(forecasts) - # Draw the scrolling text - self.display_manager.draw_scrolling_text( + # Draw everything + self.display_manager.draw_text_with_icons( display_text, - self.scroll_position, + icons=icons, + x=x_offset, force_clear=force_clear ) @@ -175,13 +175,24 @@ class WeatherManager: if not self.daily_forecast: return - # Create a compact display of all three days + # Create text lines and collect icon information lines = [] - for day in self.daily_forecast: - lines.append(f"{day['date']}\n{day['temp']}°F {day['icon']}") + icons = [] + y_offset = 2 + icon_size = 16 + + for i, day in enumerate(self.daily_forecast): + lines.append(f"{day['date']}: {day['temp']}°F") + icons.append(( + day['condition'], + self.display_manager.matrix.width - icon_size - 2, # Right align + y_offset + (i * (icon_size + 2)) # Stack vertically + )) - # Join all lines with newlines + # Join lines and draw everything 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 + self.display_manager.draw_text_with_icons( + display_text, + icons=icons, + force_clear=force_clear + ) \ No newline at end of file