Weather forecast icon enhancements

using google icon set for weather
This commit is contained in:
Chuck
2025-04-07 21:44:38 -05:00
parent 32d685efe0
commit 953616a143
4 changed files with 309 additions and 56 deletions

View File

@@ -20,7 +20,7 @@
"disable_hardware_pulsing": true, "disable_hardware_pulsing": true,
"inverse_colors": false, "inverse_colors": false,
"show_refresh_rate": true, "show_refresh_rate": true,
"limit_refresh_rate_hz": 0 "limit_refresh_rate_hz": 100
}, },
"runtime": { "runtime": {
"gpio_slowdown": 2 "gpio_slowdown": 2

View File

@@ -1,8 +1,10 @@
from rgbmatrix import RGBMatrix, RGBMatrixOptions from rgbmatrix import RGBMatrix, RGBMatrixOptions
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import time import time
from typing import Dict, Any from typing import Dict, Any, List
import logging import logging
import math
from .weather_icons import WeatherIcons
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@@ -196,6 +198,111 @@ class DisplayManager:
# Update the display with the visible portion # Update the display with the visible portion
self.matrix.SetImage(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): def cleanup(self):
"""Clean up resources.""" """Clean up resources."""
self.matrix.Clear() self.matrix.Clear()

135
src/weather_icons.py Normal file
View File

@@ -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)

View File

@@ -66,7 +66,7 @@ class WeatherManager:
self.forecast_data = response.json() self.forecast_data = response.json()
# Process forecast data # Process forecast data
self._process_forecast_data() self._process_forecast_data(self.forecast_data)
self.last_update = time.time() self.last_update = time.time()
except Exception as e: except Exception as e:
@@ -74,49 +74,35 @@ class WeatherManager:
self.weather_data = None self.weather_data = None
self.forecast_data = None self.forecast_data = None
def _process_forecast_data(self) -> None: def _process_forecast_data(self, forecast_data: Dict[str, Any]) -> None:
"""Process the forecast data into hourly and daily forecasts.""" """Process forecast data into hourly and daily forecasts."""
if not self.forecast_data: if not forecast_data:
return return
# Process hourly forecast (next 6 hours) # Process hourly forecast (next 6 hours)
hourly = forecast_data.get('hourly', [])[:6]
self.hourly_forecast = [] self.hourly_forecast = []
for item in self.forecast_data['list'][:6]: # First 6 entries (3 hours each) for hour in hourly:
hour = datetime.fromtimestamp(item['dt']).strftime('%I%p').lstrip('0') # Remove leading zero dt = datetime.fromtimestamp(hour['dt'])
temp = round(item['main']['temp']) temp = round(hour['temp'])
condition = item['weather'][0]['main'] condition = hour['weather'][0]['main']
icon = self.WEATHER_ICONS.get(condition, '')
self.hourly_forecast.append({ self.hourly_forecast.append({
'hour': hour, 'hour': dt.strftime('%I%p'),
'temp': temp, 'temp': temp,
'condition': condition, 'condition': condition
'icon': icon
}) })
# Process daily forecast (next 3 days) # Process daily forecast (next 3 days)
daily_data = {} daily = forecast_data.get('daily', [])[1:4] # Skip today, get next 3 days
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 = [] self.daily_forecast = []
for date, data in list(daily_data.items())[:3]: # First 3 days for day in daily:
avg_temp = round(sum(data['temps']) / len(data['temps'])) dt = datetime.fromtimestamp(day['dt'])
condition = max(set(data['conditions']), key=data['conditions'].count) temp = round(day['temp']['day'])
icon = self.WEATHER_ICONS.get(condition, '') condition = day['weather'][0]['main']
display_date = datetime.strptime(date, '%Y-%m-%d').strftime('%a %d')
self.daily_forecast.append({ self.daily_forecast.append({
'date': display_date, 'date': dt.strftime('%a'),
'temp': avg_temp, 'temp': temp,
'condition': condition, 'condition': condition
'icon': icon
}) })
def get_weather(self) -> Dict[str, Any]: def get_weather(self) -> Dict[str, Any]:
@@ -135,13 +121,16 @@ class WeatherManager:
temp = round(weather_data['main']['temp']) temp = round(weather_data['main']['temp'])
condition = weather_data['weather'][0]['main'] condition = weather_data['weather'][0]['main']
icon = self.WEATHER_ICONS.get(condition, '')
# Format the display string with temp and large icon # Draw temperature text and weather icon
display_text = f"{temp}°F\n{icon}" text = f"{temp}°F"
icon_x = (self.display_manager.matrix.width - 20) // 2 # Center the 20px icon
# Draw both lines at once using the multi-line support in draw_text icon_y = 2 # Near the top
self.display_manager.draw_text(display_text, force_clear=force_clear) 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: def display_hourly_forecast(self, scroll_amount: int = 0, force_clear: bool = False) -> None:
"""Display scrolling hourly forecast information.""" """Display scrolling hourly forecast information."""
@@ -153,18 +142,29 @@ class WeatherManager:
# Update scroll position # Update scroll position
self.scroll_position = scroll_amount self.scroll_position = scroll_amount
# Create the full scrolling text # Create the full scrolling text with icons
forecasts = [] forecasts = []
for forecast in self.hourly_forecast: icons = []
forecasts.append(f"{forecast['hour']}\n{forecast['temp']}°F\n{forecast['icon']}") x_offset = self.display_manager.matrix.width - scroll_amount
icon_size = 16
# Join with some spacing between each forecast 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) display_text = " | ".join(forecasts)
# Draw the scrolling text # Draw everything
self.display_manager.draw_scrolling_text( self.display_manager.draw_text_with_icons(
display_text, display_text,
self.scroll_position, icons=icons,
x=x_offset,
force_clear=force_clear force_clear=force_clear
) )
@@ -175,13 +175,24 @@ class WeatherManager:
if not self.daily_forecast: if not self.daily_forecast:
return return
# Create a compact display of all three days # Create text lines and collect icon information
lines = [] lines = []
for day in self.daily_forecast: icons = []
lines.append(f"{day['date']}\n{day['temp']}°F {day['icon']}") y_offset = 2
icon_size = 16
# Join all lines with newlines 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 lines and draw everything
display_text = "\n".join(lines) display_text = "\n".join(lines)
self.display_manager.draw_text_with_icons(
# Draw the forecast display_text,
self.display_manager.draw_text(display_text, force_clear=force_clear) icons=icons,
force_clear=force_clear
)