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,
|
"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
|
||||||
|
|||||||
@@ -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
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()
|
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
|
||||||
|
|
||||||
|
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)
|
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
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user