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,
"inverse_colors": false,
"show_refresh_rate": true,
"limit_refresh_rate_hz": 0
"limit_refresh_rate_hz": 100
},
"runtime": {
"gpio_slowdown": 2

View File

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

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()
# 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']}")
icons = []
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)
# 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
# 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)
# Draw the forecast
self.display_manager.draw_text(display_text, force_clear=force_clear)
self.display_manager.draw_text_with_icons(
display_text,
icons=icons,
force_clear=force_clear
)