mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-12 05:42:59 +00:00
feat: Add support for day/night weather icons
This commit is contained in:
@@ -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)):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user