mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
Weather forecast icon enhancements
using google icon set for weather
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
135
src/weather_icons.py
Normal 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)
|
||||
@@ -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)
|
||||
self.display_manager.draw_text_with_icons(
|
||||
display_text,
|
||||
icons=icons,
|
||||
force_clear=force_clear
|
||||
)
|
||||
Reference in New Issue
Block a user