feat: Add support for day/night weather icons

This commit is contained in:
ChuckBuilds
2025-04-30 12:32:55 -05:00
parent c649fabd68
commit afa11d69c6
2 changed files with 55 additions and 120 deletions

View File

@@ -8,119 +8,49 @@ class WeatherIcons:
DEFAULT_ICON = "not-available.png" DEFAULT_ICON = "not-available.png"
DEFAULT_SIZE = 64 # Default size, should match icons but can be overridden DEFAULT_SIZE = 64 # Default size, should match icons but can be overridden
# Mapping from OpenWeatherMap icon codes to our filenames
# See: https://openweathermap.org/weather-conditions#Icon-list
ICON_MAP = {
# Day icons
"01d": "clear-day.png",
"02d": "partly-cloudy-day.png", # Few clouds
"03d": "cloudy.png", # Scattered clouds
"04d": "overcast-day.png", # Broken clouds / Overcast
"09d": "drizzle.png", # Shower rain (using drizzle)
"10d": "partly-cloudy-day-rain.png", # Rain
"11d": "thunderstorms-day.png", # Thunderstorm
"13d": "partly-cloudy-day-snow.png", # Snow
"50d": "mist.png", # Mist (can use fog, haze etc. too)
# Night icons
"01n": "clear-night.png",
"02n": "partly-cloudy-night.png",# Few clouds
"03n": "cloudy.png", # Scattered clouds (same as day)
"04n": "overcast-night.png", # Broken clouds / Overcast
"09n": "drizzle.png", # Shower rain (using drizzle, same as day)
"10n": "partly-cloudy-night-rain.png", # Rain
"11n": "thunderstorms-night.png", # Thunderstorm
"13n": "partly-cloudy-night-snow.png", # Snow
"50n": "mist.png", # Mist (same as day)
# Add mappings for specific conditions if needed, although OWM codes are preferred
"tornado": "tornado.png",
"hurricane": "hurricane.png",
"wind": "wind.png", # Generic wind if code is not specific enough
}
@staticmethod @staticmethod
def _get_icon_filename(condition: str) -> str: def _get_icon_filename(icon_code: str) -> str:
"""Maps a weather condition string to an icon filename.""" """Maps an OpenWeatherMap icon code (e.g., '01d', '10n') to an icon filename."""
# Normalize the input condition string filename = WeatherIcons.ICON_MAP.get(icon_code, WeatherIcons.DEFAULT_ICON)
condition = condition.lower().strip() print(f"[WeatherIcons] Mapping icon code '{icon_code}' to filename: '{filename}'")
print(f"[WeatherIcons] Determining icon for condition: '{condition}'")
filename = WeatherIcons.DEFAULT_ICON # Start with default # Check if the mapped filename exists, otherwise use default
# --- Severe / Extreme --- (Checked first)
if "tornado" in condition: filename = "tornado.png"
elif "hurricane" in condition: filename = "hurricane.png"
elif "squall" in condition: filename = "wind.png" # Or potentially extreme.png? Using wind for now.
elif "extreme" in condition: # Look for combined extreme conditions
if "thunderstorm" in condition or "thunder" in condition or "storm" in condition:
if "rain" in condition: filename = "thunderstorms-day-extreme-rain.png" # Default day
elif "snow" in condition: filename = "thunderstorms-day-extreme-snow.png" # Default day
else: filename = "thunderstorms-day-extreme.png" # Default day
elif "rain" in condition: filename = "extreme-rain.png"
elif "snow" in condition: filename = "extreme-snow.png"
elif "sleet" in condition: filename = "extreme-sleet.png"
elif "drizzle" in condition: filename = "extreme-drizzle.png"
elif "hail" in condition: filename = "extreme-hail.png"
elif "fog" in condition: filename = "extreme-day-fog.png" # Default day
elif "haze" in condition: filename = "extreme-day-haze.png" # Default day
elif "smoke" in condition: filename = "extreme-day-smoke.png" # Default day
else: filename = "extreme-day.png" # Default day
# --- Thunderstorms ---
elif "thunderstorm" in condition or "thunder" in condition or "storm" in condition:
if "overcast" in condition:
if "rain" in condition: filename = "thunderstorms-overcast-rain.png"
elif "snow" in condition: filename = "thunderstorms-overcast-snow.png"
else: filename = "thunderstorms-overcast.png"
# Simple thunderstorm conditions
elif "rain" in condition: filename = "thunderstorms-day-rain.png" # Default day
elif "snow" in condition: filename = "thunderstorms-day-snow.png" # Default day
else: filename = "thunderstorms-day.png" # Default day
# --- Precipitation --- (Excluding thunderstorms covered above)
elif "sleet" in condition:
if "overcast" in condition: filename = "overcast-day-sleet.png" # Default day
elif "partly cloudy" in condition or "scattered" in condition or "few" in condition or "broken" in condition: # Approximating partly cloudy
filename = "partly-cloudy-day-sleet.png" # Default day
else: filename = "sleet.png"
elif "snow" in condition:
if "overcast" in condition: filename = "overcast-day-snow.png" # Default day
elif "partly cloudy" in condition or "scattered" in condition or "few" in condition or "broken" in condition:
filename = "partly-cloudy-day-snow.png" # Default day
elif "wind" in condition: filename = "wind-snow.png"
else: filename = "snow.png"
elif "rain" in condition:
if "overcast" in condition: filename = "overcast-day-rain.png" # Default day
elif "partly cloudy" in condition or "scattered" in condition or "few" in condition or "broken" in condition:
filename = "partly-cloudy-day-rain.png" # Default day
else: filename = "rain.png"
elif "drizzle" in condition:
if "overcast" in condition: filename = "overcast-day-drizzle.png" # Default day
elif "partly cloudy" in condition or "scattered" in condition or "few" in condition or "broken" in condition:
filename = "partly-cloudy-day-drizzle.png" # Default day
else: filename = "drizzle.png"
elif "hail" in condition:
if "overcast" in condition: filename = "overcast-day-hail.png" # Default day
elif "partly cloudy" in condition or "scattered" in condition or "few" in condition or "broken" in condition:
filename = "partly-cloudy-day-hail.png" # Default day
else: filename = "hail.png"
# --- Obscurations (Fog, Mist, Haze, Smoke, Dust, Sand, Ash) ---
elif "fog" in condition:
if "overcast" in condition: filename = "overcast-day-fog.png" # Default day
elif "partly cloudy" in condition or "scattered" in condition or "few" in condition or "broken" in condition:
filename = "partly-cloudy-day-fog.png" # Default day
else: filename = "fog-day.png" # Default day
elif "mist" in condition: filename = "mist.png"
elif "haze" in condition:
if "overcast" in condition: filename = "overcast-day-haze.png" # Default day
elif "partly cloudy" in condition or "scattered" in condition or "few" in condition or "broken" in condition:
filename = "partly-cloudy-day-haze.png" # Default day
else: filename = "haze-day.png" # Default day
elif "smoke" in condition:
if "overcast" in condition: filename = "overcast-day-smoke.png" # Default day
elif "partly cloudy" in condition or "scattered" in condition or "few" in condition or "broken" in condition:
filename = "partly-cloudy-day-smoke.png" # Default day
else: filename = "smoke.png"
elif "dust" in condition:
filename = "dust-day.png" # Default day
elif "sand" in condition: filename = "dust-day.png" # Map sand to dust (day)
elif "ash" in condition: filename = "smoke.png" # Map ash to smoke
# --- Clouds --- (No precipitation, no obscuration)
elif "overcast" in condition: # Solid cloud cover
filename = "overcast-day.png" # Default day
elif "broken clouds" in condition or "scattered clouds" in condition or "partly cloudy" in condition: # Partial cover
filename = "partly-cloudy-day.png" # Default day
elif "few clouds" in condition: # Minimal clouds
filename = "partly-cloudy-day.png" # Use partly cloudy day for few clouds
elif "clouds" in condition: # Generic cloudy
filename = "cloudy.png"
# --- Clear ---
elif "clear" in condition or "sunny" in condition:
filename = "clear-day.png" # Default day
# --- Wind (if no other condition matched significantly) ---
elif "wind" in condition: filename = "wind.png"
# --- Final Check ---
# Check if the determined filename exists, otherwise use default
potential_path = os.path.join(WeatherIcons.ICON_DIR, filename) potential_path = os.path.join(WeatherIcons.ICON_DIR, filename)
if not os.path.exists(potential_path): if not os.path.exists(potential_path):
# If a specific icon was determined but not found, log warning and use default # If a specific icon was determined but not found, log warning and use default
if filename != WeatherIcons.DEFAULT_ICON: if filename != WeatherIcons.DEFAULT_ICON:
print(f"Warning: Determined icon '{filename}' not found at '{potential_path}'. Falling back to default.") print(f"Warning: Mapped icon file '{filename}' not found at '{potential_path}'. Falling back to default.")
filename = WeatherIcons.DEFAULT_ICON filename = WeatherIcons.DEFAULT_ICON
# Check if default exists # Check if default exists
@@ -132,9 +62,9 @@ class WeatherIcons:
return filename return filename
@staticmethod @staticmethod
def load_weather_icon(condition: str, size: int = DEFAULT_SIZE) -> Image.Image | None: def load_weather_icon(icon_code: str, size: int = DEFAULT_SIZE) -> Image.Image | None:
"""Loads, converts, and resizes the appropriate weather icon. Returns None on failure.""" """Loads, converts, and resizes the appropriate weather icon based on the OWM code. Returns None on failure."""
filename = WeatherIcons._get_icon_filename(condition) filename = WeatherIcons._get_icon_filename(icon_code)
icon_path = os.path.join(WeatherIcons.ICON_DIR, filename) icon_path = os.path.join(WeatherIcons.ICON_DIR, filename)
try: try:
@@ -155,9 +85,9 @@ class WeatherIcons:
return None return None
@staticmethod @staticmethod
def draw_weather_icon(image: Image.Image, condition: str, x: int, y: int, size: int = DEFAULT_SIZE): def draw_weather_icon(image: Image.Image, icon_code: str, x: int, y: int, size: int = DEFAULT_SIZE):
"""Loads the appropriate weather icon and pastes it onto the target PIL Image object.""" """Loads the appropriate weather icon based on OWM code and pastes it onto the target PIL Image object."""
icon_to_draw = WeatherIcons.load_weather_icon(condition, size) icon_to_draw = WeatherIcons.load_weather_icon(icon_code, size)
if icon_to_draw: if icon_to_draw:
# Create a thresholded mask from the icon's alpha channel # Create a thresholded mask from the icon's alpha channel
# to remove faint anti-aliasing pixels when pasting on black bg. # to remove faint anti-aliasing pixels when pasting on black bg.
@@ -173,7 +103,7 @@ class WeatherIcons:
# Paste the icon directly with its original alpha channel # Paste the icon directly with its original alpha channel
image.paste(icon_to_draw, (x, y), icon_to_draw) image.paste(icon_to_draw, (x, y), icon_to_draw)
except Exception as e: except Exception as e:
print(f"Error processing or pasting icon for condition '{condition}' at ({x},{y}): {e}") print(f"Error processing or pasting icon for code '{icon_code}' at ({x},{y}): {e}")
# Fallback or alternative handling if needed # Fallback or alternative handling if needed
# try: # try:
# # Fallback: Try pasting with original alpha if thresholding fails # # Fallback: Try pasting with original alpha if thresholding fails
@@ -183,7 +113,7 @@ class WeatherIcons:
pass pass
else: else:
# Optional: Draw a placeholder if icon loading fails completely # Optional: Draw a placeholder if icon loading fails completely
print(f"Could not load icon for condition '{condition}' to draw at ({x},{y})") print(f"Could not load icon for code '{icon_code}' to draw at ({x},{y})")
@staticmethod @staticmethod
def draw_sun(draw: ImageDraw, x: int, y: int, size: int = 16, color: tuple = (255, 200, 0)): def draw_sun(draw: ImageDraw, x: int, y: int, size: int = 16, color: tuple = (255, 200, 0)):

View File

@@ -164,10 +164,12 @@ class WeatherManager:
dt = datetime.fromtimestamp(hour_data['dt']) dt = datetime.fromtimestamp(hour_data['dt'])
temp = round(hour_data['temp']) temp = round(hour_data['temp'])
condition = hour_data['weather'][0]['main'] condition = hour_data['weather'][0]['main']
icon_code = hour_data['weather'][0]['icon']
self.hourly_forecast.append({ self.hourly_forecast.append({
'hour': dt.strftime('%I:00 %p').lstrip('0'), # Format as "2:00 PM" 'hour': dt.strftime('%I:00 %p').lstrip('0'), # Format as "2:00 PM"
'temp': temp, 'temp': temp,
'condition': condition 'condition': condition,
'icon': icon_code
}) })
# Process daily forecast # Process daily forecast
@@ -179,13 +181,15 @@ class WeatherManager:
temp_high = round(day_data['temp']['max']) temp_high = round(day_data['temp']['max'])
temp_low = round(day_data['temp']['min']) temp_low = round(day_data['temp']['min'])
condition = day_data['weather'][0]['main'] condition = day_data['weather'][0]['main']
icon_code = day_data['weather'][0]['icon']
self.daily_forecast.append({ self.daily_forecast.append({
'date': dt.strftime('%a'), # Day name (Mon, Tue, etc.) 'date': dt.strftime('%a'), # Day name (Mon, Tue, etc.)
'date_str': dt.strftime('%m/%d'), # Date (4/8, 4/9, etc.) 'date_str': dt.strftime('%m/%d'), # Date (4/8, 4/9, etc.)
'temp_high': temp_high, 'temp_high': temp_high,
'temp_low': temp_low, 'temp_low': temp_low,
'condition': condition 'condition': condition,
'icon': icon_code
}) })
def get_weather(self) -> Dict[str, Any]: def get_weather(self) -> Dict[str, Any]:
@@ -262,12 +266,13 @@ class WeatherManager:
# --- Top Left: Icon --- # --- Top Left: Icon ---
condition = weather_data['weather'][0]['main'] condition = weather_data['weather'][0]['main']
icon_code = weather_data['weather'][0]['icon']
icon_size = self.ICON_SIZE['extra_large'] # Use extra_large size icon_size = self.ICON_SIZE['extra_large'] # Use extra_large size
icon_x = 1 # Small padding from left edge icon_x = 1 # Small padding from left edge
# Center the icon vertically in the top two-thirds of the display # Center the icon vertically in the top two-thirds of the display
available_height = (self.display_manager.matrix.height * 2) // 3 # Use top 2/3 of screen available_height = (self.display_manager.matrix.height * 2) // 3 # Use top 2/3 of screen
icon_y = (available_height - icon_size) // 2 icon_y = (available_height - icon_size) // 2
WeatherIcons.draw_weather_icon(image, condition, icon_x, icon_y, size=icon_size) WeatherIcons.draw_weather_icon(image, icon_code, icon_x, icon_y, size=icon_size)
# --- Top Right: Condition Text --- # --- Top Right: Condition Text ---
condition_text = condition condition_text = condition
@@ -405,7 +410,7 @@ class WeatherManager:
calculated_y = top_text_height + (available_height_for_icon - icon_size) // 2 calculated_y = top_text_height + (available_height_for_icon - icon_size) // 2
icon_y = (self.display_manager.matrix.height // 2) - 16 icon_y = (self.display_manager.matrix.height // 2) - 16
icon_x = center_x - icon_size // 2 icon_x = center_x - icon_size // 2
WeatherIcons.draw_weather_icon(image, forecast['condition'], icon_x, icon_y, icon_size) WeatherIcons.draw_weather_icon(image, forecast['icon'], icon_x, icon_y, icon_size)
# Draw temperature at bottom # Draw temperature at bottom
temp_text = f"{forecast['temp']}°" temp_text = f"{forecast['temp']}°"
@@ -475,7 +480,7 @@ class WeatherManager:
calculated_y = top_text_height + (available_height_for_icon - icon_size) // 2 calculated_y = top_text_height + (available_height_for_icon - icon_size) // 2
icon_y = (self.display_manager.matrix.height // 2) - 16 icon_y = (self.display_manager.matrix.height // 2) - 16
icon_x = center_x - icon_size // 2 icon_x = center_x - icon_size // 2
WeatherIcons.draw_weather_icon(image, forecast['condition'], icon_x, icon_y, icon_size) WeatherIcons.draw_weather_icon(image, forecast['icon'], icon_x, icon_y, icon_size)
# Draw high/low temperatures at bottom (without degree symbol) # Draw high/low temperatures at bottom (without degree symbol)
temp_text = f"{forecast['temp_low']} / {forecast['temp_high']}" # Removed degree symbols temp_text = f"{forecast['temp_low']} / {forecast['temp_high']}" # Removed degree symbols