diff --git a/plugins.json b/plugins.json index 1994725..f91bd3b 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-02-14", + "last_updated": "2026-02-15", "plugins": [ { "id": "hello-world", @@ -59,10 +59,10 @@ "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", "branch": "main", "plugin_path": "plugins/ledmatrix-weather", - "latest_version": "2.1.0", + "latest_version": "2.1.1", "stars": 0, "downloads": 0, - "last_updated": "2026-02-13", + "last_updated": "2026-02-15", "verified": true, "screenshot": "" }, @@ -196,10 +196,10 @@ "plugin_path": "plugins/hockey-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-14", + "last_updated": "2026-02-15", "verified": true, "screenshot": "", - "latest_version": "1.1.0" + "latest_version": "1.1.1" }, { "id": "football-scoreboard", @@ -221,10 +221,10 @@ "plugin_path": "plugins/football-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-14", + "last_updated": "2026-02-15", "verified": true, "screenshot": "", - "latest_version": "2.1.0" + "latest_version": "2.1.1" }, { "id": "ufc-scoreboard", @@ -245,10 +245,10 @@ "plugin_path": "plugins/ufc-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-12", + "last_updated": "2026-02-15", "verified": true, "screenshot": "", - "latest_version": "1.0.0" + "latest_version": "1.0.1" }, { "id": "basketball-scoreboard", @@ -270,10 +270,10 @@ "plugin_path": "plugins/basketball-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-14", + "last_updated": "2026-02-15", "verified": true, "screenshot": "", - "latest_version": "1.1.0" + "latest_version": "1.1.1" }, { "id": "baseball-scoreboard", @@ -296,10 +296,10 @@ "plugin_path": "plugins/baseball-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-14", + "last_updated": "2026-02-15", "verified": true, "screenshot": "", - "latest_version": "2.0.0" + "latest_version": "2.0.1" }, { "id": "soccer-scoreboard", @@ -325,10 +325,10 @@ "plugin_path": "plugins/soccer-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-14", + "last_updated": "2026-02-15", "verified": true, "screenshot": "", - "latest_version": "1.3.0" + "latest_version": "1.3.1" }, { "id": "odds-ticker", diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 36c3fb8..0e5f552 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -170,8 +170,8 @@ def _render_live_game(self, game: Dict) -> Image.Image: center_y = self.display_height // 2 # Logos - main_img.paste(home_logo, (self.display_width - home_logo.width + 10, center_y - home_logo.height // 2), home_logo) - main_img.paste(away_logo, (-10, center_y - away_logo.height // 2), away_logo) + main_img.paste(home_logo, (self.display_width - home_logo.width, center_y - home_logo.height // 2), home_logo) + main_img.paste(away_logo, (0, center_y - away_logo.height // 2), away_logo) # Inning indicator (top center) inning_half = game.get('inning_half', 'top') @@ -307,8 +307,8 @@ def _render_recent_game(self, game: Dict) -> Image.Image: center_y = self.display_height // 2 # Logos (tighter fit for recent) - main_img.paste(home_logo, (self.display_width - home_logo.width + 2, center_y - home_logo.height // 2), home_logo) - main_img.paste(away_logo, (-2, center_y - away_logo.height // 2), away_logo) + main_img.paste(home_logo, (self.display_width - home_logo.width, center_y - home_logo.height // 2), home_logo) + main_img.paste(away_logo, (0, center_y - away_logo.height // 2), away_logo) # "Final" (top center) status_text = "Final" @@ -353,8 +353,8 @@ def _render_upcoming_game(self, game: Dict) -> Image.Image: center_y = self.display_height // 2 # Logos (tighter fit) - main_img.paste(home_logo, (self.display_width - home_logo.width + 2, center_y - home_logo.height // 2), home_logo) - main_img.paste(away_logo, (-2, center_y - away_logo.height // 2), away_logo) + main_img.paste(home_logo, (self.display_width - home_logo.width, center_y - home_logo.height // 2), home_logo) + main_img.paste(away_logo, (0, center_y - away_logo.height // 2), away_logo) # "Next Game" (top center) status_font = self.fonts['status'] if self.display_width <= 128 else self.fonts['time'] diff --git a/plugins/baseball-scoreboard/manager.py b/plugins/baseball-scoreboard/manager.py index 45e77db..ea03c08 100644 --- a/plugins/baseball-scoreboard/manager.py +++ b/plugins/baseball-scoreboard/manager.py @@ -3626,20 +3626,30 @@ def get_vegas_content(self) -> Optional[Any]: """ Get content for Vegas-style continuous scroll mode. - Returns None to let PluginAdapter auto-detect scroll_helper.cached_image. - Triggers scroll content generation if cache is empty to ensure Vegas - has content to display. + Triggers scroll content generation if cache is empty, then returns + the cached scroll image(s) for Vegas to compose into its scroll strip. Returns: - None - PluginAdapter will extract scroll_helper.cached_image automatically + List of PIL Images from scroll displays, or None if no content """ - # Ensure scroll content is generated for Vegas mode - if hasattr(self, '_scroll_manager') and self._scroll_manager: - if not self._scroll_manager.has_cached_content(): - self.logger.info("[Baseball Vegas] Triggering scroll content generation") - self._ensure_scroll_content_for_vegas() + if not hasattr(self, '_scroll_manager') or not self._scroll_manager: + return None + + images = self._scroll_manager.get_all_vegas_content_items() + + if not images: + self.logger.info("[Baseball Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + images = self._scroll_manager.get_all_vegas_content_items() + + if images: + total_width = sum(img.width for img in images) + self.logger.info( + "[Baseball Vegas] Returning %d image(s), %dpx total", + len(images), total_width + ) + return images - # Return None - PluginAdapter will auto-detect scroll_helper.cached_image return None def get_vegas_content_type(self) -> str: diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 706d69d..70e763b 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "baseball-scoreboard", "name": "Baseball Scoreboard", - "version": "2.0.0", + "version": "2.0.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", "category": "sports", diff --git a/plugins/baseball-scoreboard/scroll_display.py b/plugins/baseball-scoreboard/scroll_display.py index 90e782a..c7cfea0 100644 --- a/plugins/baseball-scoreboard/scroll_display.py +++ b/plugins/baseball-scoreboard/scroll_display.py @@ -110,6 +110,7 @@ def __init__( self._current_games: List[Dict] = [] self._current_game_type: str = "" self._current_leagues: List[str] = [] + self._vegas_content_items: List[Image.Image] = [] self._is_scrolling = False self._scroll_start_time: Optional[float] = None self._last_log_time: float = 0 @@ -381,6 +382,9 @@ def prepare_scroll_content( self.logger.warning("No game cards rendered") return False + # Store individual items for Vegas mode (avoids scroll_helper padding) + self._vegas_content_items = list(content_items) + # Create scrolling image using ScrollHelper self.scroll_helper.create_scrolling_image( content_items, @@ -657,3 +661,12 @@ def has_cached_content(self) -> bool: if scroll_display.scroll_helper.cached_image is not None: return True return False + + def get_all_vegas_content_items(self) -> list: + """Collect _vegas_content_items from all scroll displays.""" + items = [] + for sd in self._scroll_displays.values(): + vegas_items = getattr(sd, '_vegas_content_items', None) + if vegas_items: + items.extend(vegas_items) + return items diff --git a/plugins/basketball-scoreboard/game_renderer.py b/plugins/basketball-scoreboard/game_renderer.py index b99cc1a..02dce2a 100644 --- a/plugins/basketball-scoreboard/game_renderer.py +++ b/plugins/basketball-scoreboard/game_renderer.py @@ -317,11 +317,11 @@ def render_game_card( center_y = self.display_height // 2 # Draw logos - home_x = self.display_width - home_logo.width + 10 + home_x = self.display_width - home_logo.width home_y = center_y - (home_logo.height // 2) main_img.paste(home_logo, (home_x, home_y), home_logo) - - away_x = -10 + + away_x = 0 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) diff --git a/plugins/basketball-scoreboard/manager.py b/plugins/basketball-scoreboard/manager.py index 1fbee1a..b2fde27 100644 --- a/plugins/basketball-scoreboard/manager.py +++ b/plugins/basketball-scoreboard/manager.py @@ -3251,21 +3251,30 @@ def get_vegas_content(self) -> Optional[Any]: """ Get content for Vegas-style continuous scroll mode. - Returns None to let PluginAdapter auto-detect scroll_helper.cached_image. - Triggers scroll content generation if cache is empty to ensure Vegas - has content to display. + Triggers scroll content generation if cache is empty, then returns + the cached scroll image(s) for Vegas to compose into its scroll strip. Returns: - None - PluginAdapter will extract scroll_helper.cached_image automatically + List of PIL Images from scroll displays, or None if no content """ - # Ensure scroll content is generated for Vegas mode - if hasattr(self, '_scroll_manager') and self._scroll_manager: - # Check if any scroll display has content - if not self._scroll_manager.has_cached_content(): - self.logger.info("[Basketball Vegas] Triggering scroll content generation") - self._ensure_scroll_content_for_vegas() - - # Return None - PluginAdapter will auto-detect scroll_helper.cached_image + if not hasattr(self, '_scroll_manager') or not self._scroll_manager: + return None + + images = self._scroll_manager.get_all_vegas_content_items() + + if not images: + self.logger.info("[Basketball Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + images = self._scroll_manager.get_all_vegas_content_items() + + if images: + total_width = sum(img.width for img in images) + self.logger.info( + "[Basketball Vegas] Returning %d image(s), %dpx total", + len(images), total_width + ) + return images + return None def get_vegas_content_type(self) -> str: diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 28326f6..3505db3 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "basketball-scoreboard", "name": "Basketball Scoreboard", - "version": "1.1.0", + "version": "1.1.1", "description": "Live, recent, and upcoming basketball games across NBA, NCAA Men's, NCAA Women's, and WNBA with real-time scores and schedules", "author": "ChuckBuilds", "category": "sports", diff --git a/plugins/basketball-scoreboard/scroll_display.py b/plugins/basketball-scoreboard/scroll_display.py index 6f57396..85cfb8d 100644 --- a/plugins/basketball-scoreboard/scroll_display.py +++ b/plugins/basketball-scoreboard/scroll_display.py @@ -107,6 +107,7 @@ def __init__( self._current_games: List[Dict] = [] self._current_game_type: str = "" self._current_leagues: List[str] = [] + self._vegas_content_items: List[Image.Image] = [] self._is_scrolling = False self._scroll_start_time: Optional[float] = None self._last_log_time: float = 0 @@ -396,13 +397,16 @@ def prepare_scroll_content( self.logger.warning("No game cards rendered") return False + # Store individual items for Vegas mode (avoids scroll_helper padding) + self._vegas_content_items = list(content_items) + # Create scrolling image using ScrollHelper self.scroll_helper.create_scrolling_image( content_items, item_gap=gap_between_games, element_gap=0 # No element gap - each item is a complete game card ) - + # Log what we loaded league_summary = ", ".join([f"{league.upper()}({count})" for league, count in league_counts.items()]) self.logger.info( @@ -667,6 +671,15 @@ def has_cached_content(self) -> bool: return True return False + def get_all_vegas_content_items(self) -> list: + """Collect _vegas_content_items from all scroll displays.""" + items = [] + for sd in self._scroll_displays.values(): + vegas_items = getattr(sd, '_vegas_content_items', None) + if vegas_items: + items.extend(vegas_items) + return items + def clear_all(self) -> None: """Clear all scroll displays.""" for scroll_display in self._scroll_displays.values(): diff --git a/plugins/football-scoreboard/game_renderer.py b/plugins/football-scoreboard/game_renderer.py index bacafed..0b4e2af 100644 --- a/plugins/football-scoreboard/game_renderer.py +++ b/plugins/football-scoreboard/game_renderer.py @@ -326,11 +326,11 @@ def render_game_card( center_y = self.display_height // 2 # Draw logos - home_x = self.display_width - home_logo.width + 10 + home_x = self.display_width - home_logo.width home_y = center_y - (home_logo.height // 2) main_img.paste(home_logo, (home_x, home_y), home_logo) - - away_x = -10 + + away_x = 0 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) diff --git a/plugins/football-scoreboard/manager.py b/plugins/football-scoreboard/manager.py index 5f7bb0a..5804c84 100644 --- a/plugins/football-scoreboard/manager.py +++ b/plugins/football-scoreboard/manager.py @@ -3376,20 +3376,30 @@ def get_vegas_content(self) -> Optional[Any]: """ Get content for Vegas-style continuous scroll mode. - Returns None to let PluginAdapter auto-detect scroll_helper.cached_image. - Triggers scroll content generation if cache is empty to ensure Vegas - has content to display. + Triggers scroll content generation if cache is empty, then returns + the cached scroll image(s) for Vegas to compose into its scroll strip. Returns: - None - PluginAdapter will extract scroll_helper.cached_image automatically + List of PIL Images from scroll displays, or None if no content """ - # Ensure scroll content is generated for Vegas mode - if hasattr(self, '_scroll_manager') and self._scroll_manager: - if not self._scroll_manager.has_cached_content(): - self.logger.info("[Football Vegas] Triggering scroll content generation") - self._ensure_scroll_content_for_vegas() + if not hasattr(self, '_scroll_manager') or not self._scroll_manager: + return None + + images = self._scroll_manager.get_all_vegas_content_items() + + if not images: + self.logger.info("[Football Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + images = self._scroll_manager.get_all_vegas_content_items() + + if images: + total_width = sum(img.width for img in images) + self.logger.info( + "[Football Vegas] Returning %d image(s), %dpx total", + len(images), total_width + ) + return images - # Return None - PluginAdapter will auto-detect scroll_helper.cached_image return None def get_vegas_content_type(self) -> str: diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index 9ba3cb1..acb1b27 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "2.1.0", + "version": "2.1.1", "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", "description": "Standalone plugin for live, recent, and upcoming football games across NFL and NCAA Football with real-time scores, down/distance, possession, and game status. Now with organized nested config!", diff --git a/plugins/football-scoreboard/scroll_display.py b/plugins/football-scoreboard/scroll_display.py index ea8d532..6a04afc 100644 --- a/plugins/football-scoreboard/scroll_display.py +++ b/plugins/football-scoreboard/scroll_display.py @@ -96,6 +96,7 @@ def __init__( self._current_games: List[Dict] = [] self._current_game_type: str = "" self._current_leagues: List[str] = [] + self._vegas_content_items: List[Image.Image] = [] self._is_scrolling = False self._scroll_start_time: Optional[float] = None self._last_log_time: float = 0 @@ -360,13 +361,16 @@ def prepare_scroll_content( self.logger.warning("No game cards rendered") return False + # Store individual items for Vegas mode (avoids scroll_helper padding) + self._vegas_content_items = list(content_items) + # Create scrolling image using ScrollHelper self.scroll_helper.create_scrolling_image( content_items, item_gap=gap_between_games, element_gap=0 # No element gap - each item is a complete game card ) - + # Log what we loaded league_summary = ", ".join([f"{league.upper()}({count})" for league, count in league_counts.items()]) self.logger.info( @@ -630,7 +634,16 @@ def has_cached_content(self) -> bool: if scroll_display.scroll_helper.cached_image is not None: return True return False - + + def get_all_vegas_content_items(self) -> list: + """Collect _vegas_content_items from all scroll displays.""" + items = [] + for sd in self._scroll_displays.values(): + vegas_items = getattr(sd, '_vegas_content_items', None) + if vegas_items: + items.extend(vegas_items) + return items + def clear_all(self) -> None: """Clear all scroll displays.""" for scroll_display in self._scroll_displays.values(): diff --git a/plugins/hockey-scoreboard/game_renderer.py b/plugins/hockey-scoreboard/game_renderer.py index 3b0b81a..d3a7d46 100644 --- a/plugins/hockey-scoreboard/game_renderer.py +++ b/plugins/hockey-scoreboard/game_renderer.py @@ -364,12 +364,12 @@ def render_game_card( center_y = self.display_height // 2 - # Draw logos - home_x = self.display_width - home_logo.width + 10 + # Draw logos (flush with card edges to avoid clipping in scroll/Vegas mode) + home_x = self.display_width - home_logo.width home_y = center_y - (home_logo.height // 2) main_img.paste(home_logo, (home_x, home_y), home_logo) - away_x = -10 + away_x = 0 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) diff --git a/plugins/hockey-scoreboard/manager.py b/plugins/hockey-scoreboard/manager.py index 7bf0e04..284cf77 100644 --- a/plugins/hockey-scoreboard/manager.py +++ b/plugins/hockey-scoreboard/manager.py @@ -3088,28 +3088,30 @@ def get_vegas_content(self) -> Optional[Any]: """ Get content for Vegas-style continuous scroll mode. - Returns None to let PluginAdapter auto-detect scroll_helper.cached_image. - Triggers scroll content generation if cache is empty to ensure Vegas - has content to display. + Triggers scroll content generation if cache is empty, then returns + the cached scroll image(s) for Vegas to compose into its scroll strip. Returns: - None - PluginAdapter will extract scroll_helper.cached_image automatically - """ - # Ensure scroll content is generated for Vegas mode - if hasattr(self, '_scroll_manager') and self._scroll_manager: - # Check if any scroll display has content - has_content = False - for scroll_display in self._scroll_manager._scroll_displays.values(): - if hasattr(scroll_display, 'scroll_helper') and scroll_display.scroll_helper: - if scroll_display.scroll_helper.cached_image is not None: - has_content = True - break + List of PIL Images from scroll displays, or None if no content + """ + if not hasattr(self, '_scroll_manager') or not self._scroll_manager: + return None - if not has_content: - self.logger.info("[Hockey Vegas] Triggering scroll content generation") - self._ensure_scroll_content_for_vegas() + images = self._scroll_manager.get_all_vegas_content_items() + + if not images: + self.logger.info("[Hockey Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + images = self._scroll_manager.get_all_vegas_content_items() + + if images: + total_width = sum(img.width for img in images) + self.logger.info( + "[Hockey Vegas] Returning %d image(s), %dpx total", + len(images), total_width + ) + return images - # Return None - PluginAdapter will auto-detect scroll_helper.cached_image return None def get_vegas_content_type(self) -> str: diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index 4a3caf4..720d6ab 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.1.0", + "version": "1.1.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", diff --git a/plugins/hockey-scoreboard/scroll_display.py b/plugins/hockey-scoreboard/scroll_display.py index daa06ec..9a8d278 100644 --- a/plugins/hockey-scoreboard/scroll_display.py +++ b/plugins/hockey-scoreboard/scroll_display.py @@ -106,6 +106,7 @@ def __init__( self._current_games: List[Dict] = [] self._current_game_type: str = "" self._current_leagues: List[str] = [] + self._vegas_content_items: List[Image.Image] = [] self._is_scrolling = False self._scroll_start_time: Optional[float] = None self._last_log_time: float = 0 @@ -388,6 +389,9 @@ def prepare_scroll_content( self.logger.warning("No game cards rendered") return False + # Store individual items for Vegas mode (avoids scroll_helper padding) + self._vegas_content_items = list(content_items) + # Create scrolling image using ScrollHelper self.scroll_helper.create_scrolling_image( content_items, @@ -646,6 +650,15 @@ def get_dynamic_duration(self, game_type: Optional[str] = None) -> int: return scroll_display.get_dynamic_duration() + def get_all_vegas_content_items(self) -> list: + """Collect _vegas_content_items from all scroll displays.""" + items = [] + for sd in self._scroll_displays.values(): + vegas_items = getattr(sd, '_vegas_content_items', None) + if vegas_items: + items.extend(vegas_items) + return items + def clear_all(self) -> None: """Clear all scroll displays.""" for scroll_display in self._scroll_displays.values(): diff --git a/plugins/ledmatrix-weather/manager.py b/plugins/ledmatrix-weather/manager.py index 29e613d..e270020 100644 --- a/plugins/ledmatrix-weather/manager.py +++ b/plugins/ledmatrix-weather/manager.py @@ -536,49 +536,36 @@ def _display_no_data(self) -> None: self.display_manager.image = img self.display_manager.update_display() - def _display_current_weather(self) -> None: - """Display current weather conditions using comprehensive layout with icons.""" + def _render_current_weather_image(self) -> Optional[Image.Image]: + """Render current weather conditions to an Image without display side effects.""" try: - # Check if state has changed - current_state = self._get_weather_state() - if current_state == self.last_weather_state: - # No need to redraw, but still update display for web preview snapshot - self.display_manager.update_display() - return - - # Clear the display - self.display_manager.clear() - - # Create a new image for drawing - img = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) + width = self.display_manager.matrix.width + height = self.display_manager.matrix.height + img = Image.new('RGB', (width, height), (0, 0, 0)) draw = ImageDraw.Draw(img) - + # Get weather info temp = int(self.weather_data['main']['temp']) condition = self.weather_data['weather'][0]['main'] icon_code = self.weather_data['weather'][0]['icon'] humidity = self.weather_data['main']['humidity'] - pressure = self.weather_data['main']['pressure'] wind_speed = self.weather_data['wind'].get('speed', 0) - wind_deg = self.weather_data['wind'].get('deg', 0) # Wind direction not always provided + wind_deg = self.weather_data['wind'].get('deg', 0) uv_index = self.weather_data['main'].get('uvi', 0) - - # Get daily high/low from the first day of forecast temp_high = int(self.weather_data['main']['temp_max']) temp_low = int(self.weather_data['main']['temp_min']) - + # --- Top Left: Weather Icon --- icon_size = self.ICON_SIZE['extra_large'] icon_x = 1 - # Center the icon vertically in the top two-thirds of the display - available_height = (self.display_manager.matrix.height * 2) // 3 + available_height = (height * 2) // 3 icon_y = (available_height - icon_size) // 2 WeatherIcons.draw_weather_icon(img, icon_code, icon_x, icon_y, size=icon_size) - + # --- Top Right: Condition Text --- condition_font = self.display_manager.small_font condition_text_width = draw.textlength(condition, font=condition_font) - condition_x = self.display_manager.matrix.width - condition_text_width - 1 + condition_x = width - condition_text_width - 1 condition_y = 1 draw.text((condition_x, condition_y), condition, font=condition_font, fill=self.COLORS['text']) @@ -586,59 +573,66 @@ def _display_current_weather(self) -> None: temp_text = f"{temp}°" temp_font = self.display_manager.small_font temp_text_width = draw.textlength(temp_text, font=temp_font) - temp_x = self.display_manager.matrix.width - temp_text_width - 1 + temp_x = width - temp_text_width - 1 temp_y = condition_y + 8 draw.text((temp_x, temp_y), temp_text, font=temp_font, fill=self.COLORS['highlight']) - + # --- Right Side: High/Low Temperature --- high_low_text = f"{temp_low}°/{temp_high}°" high_low_font = self.display_manager.small_font high_low_width = draw.textlength(high_low_text, font=high_low_font) - high_low_x = self.display_manager.matrix.width - high_low_width - 1 + high_low_x = width - high_low_width - 1 high_low_y = temp_y + 8 draw.text((high_low_x, high_low_y), high_low_text, font=high_low_font, fill=self.COLORS['dim']) - + # --- Bottom: Additional Metrics --- - display_width = self.display_manager.matrix.width - section_width = display_width // 3 - y_pos = self.display_manager.matrix.height - 7 + section_width = width // 3 + y_pos = height - 7 font = self.display_manager.extra_small_font - # --- UV Index (Section 1) --- + # UV Index (Section 1) uv_prefix = "UV:" uv_value_text = f"{uv_index:.0f}" - prefix_width = draw.textlength(uv_prefix, font=font) value_width = draw.textlength(uv_value_text, font=font) total_width = prefix_width + value_width - start_x = (section_width - total_width) // 2 - - # Draw "UV:" prefix draw.text((start_x, y_pos), uv_prefix, font=font, fill=self.COLORS['dim']) - - # Draw UV value with color uv_color = self._get_uv_color(uv_index) draw.text((start_x + prefix_width, y_pos), uv_value_text, font=font, fill=uv_color) - - # --- Humidity (Section 2) --- + + # Humidity (Section 2) humidity_text = f"H:{humidity}%" humidity_width = draw.textlength(humidity_text, font=font) humidity_x = section_width + (section_width - humidity_width) // 2 draw.text((humidity_x, y_pos), humidity_text, font=font, fill=self.COLORS['dim']) - # --- Wind (Section 3) --- + # Wind (Section 3) wind_dir = self._get_wind_direction(wind_deg) wind_text = f"W:{wind_speed:.0f}{wind_dir}" wind_width = draw.textlength(wind_text, font=font) wind_x = (2 * section_width) + (section_width - wind_width) // 2 draw.text((wind_x, y_pos), wind_text, font=font, fill=self.COLORS['dim']) - - # Update the display - self.display_manager.image = img - self.display_manager.update_display() - self.last_weather_state = current_state + return img + except Exception as e: + self.logger.exception("Error rendering current weather") + return None + + def _display_current_weather(self) -> None: + """Display current weather conditions using comprehensive layout with icons.""" + try: + current_state = self._get_weather_state() + if current_state == self.last_weather_state: + self.display_manager.update_display() + return + + self.display_manager.clear() + img = self._render_current_weather_image() + if img: + self.display_manager.image = img + self.display_manager.update_display() + self.last_weather_state = current_state except Exception as e: self.logger.error(f"Error displaying current weather: {e}") @@ -695,40 +689,27 @@ def _get_daily_state(self) -> List[Dict[str, Any]]: for f in self.daily_forecast[:4] ] - def _display_hourly_forecast(self) -> None: - """Display hourly forecast with weather icons.""" + def _render_hourly_forecast_image(self) -> Optional[Image.Image]: + """Render hourly forecast to an Image without display side effects.""" try: if not self.hourly_forecast: - self.logger.warning("No hourly forecast data available, showing no data message") - self._display_no_data() - return - - # Check if state has changed - current_state = self._get_hourly_state() - if current_state == self.last_hourly_state: - # No need to redraw, but still update display for web preview snapshot - self.display_manager.update_display() - return - - # Clear the display - self.display_manager.clear() - - # Create a new image for drawing - img = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) + return None + + width = self.display_manager.matrix.width + height = self.display_manager.matrix.height + img = Image.new('RGB', (width, height), (0, 0, 0)) draw = ImageDraw.Draw(img) - - # Calculate layout based on matrix dimensions + hours_to_show = min(4, len(self.hourly_forecast)) - total_width = self.display_manager.matrix.width - section_width = total_width // hours_to_show + section_width = width // hours_to_show padding = max(2, section_width // 6) - + for i in range(hours_to_show): forecast = self.hourly_forecast[i] x = i * section_width + padding center_x = x + (section_width - 2 * padding) // 2 - - # Draw hour at top + + # Hour at top hour_text = forecast['hour'] hour_text = hour_text.replace(":00 ", "").replace("PM", "p").replace("AM", "a") hour_width = draw.textlength(hour_text, font=self.display_manager.small_font) @@ -736,105 +717,151 @@ def _display_hourly_forecast(self) -> None: hour_text, font=self.display_manager.small_font, fill=self.COLORS['text']) - - # Draw weather icon centered vertically between top/bottom text + + # Weather icon icon_size = self.ICON_SIZE['large'] - top_text_height = 8 - bottom_text_y = self.display_manager.matrix.height - 8 - available_height_for_icon = bottom_text_y - top_text_height - calculated_y = top_text_height + (available_height_for_icon - icon_size) // 2 - icon_y = (self.display_manager.matrix.height // 2) - 16 + icon_y = (height // 2) - 16 icon_x = center_x - icon_size // 2 WeatherIcons.draw_weather_icon(img, forecast['icon'], icon_x, icon_y, icon_size) - - # Draw temperature at bottom + + # Temperature at bottom temp_text = f"{forecast['temp']}°" temp_width = draw.textlength(temp_text, font=self.display_manager.small_font) - temp_y = self.display_manager.matrix.height - 8 + temp_y = height - 8 draw.text((center_x - temp_width // 2, temp_y), temp_text, font=self.display_manager.small_font, fill=self.COLORS['text']) - - # Update the display - self.display_manager.image = img - self.display_manager.update_display() - self.last_hourly_state = current_state + return img except Exception as e: - self.logger.error(f"Error displaying hourly forecast: {e}") - - def _display_daily_forecast(self) -> None: - """Display daily forecast with weather icons.""" + self.logger.exception("Error rendering hourly forecast") + return None + + def _display_hourly_forecast(self) -> None: + """Display hourly forecast with weather icons.""" try: - if not self.daily_forecast: + if not self.hourly_forecast: + self.logger.warning("No hourly forecast data available, showing no data message") self._display_no_data() return - - # Check if state has changed - current_state = self._get_daily_state() - if current_state == self.last_daily_state: - # No need to redraw, but still update display for web preview snapshot + + current_state = self._get_hourly_state() + if current_state == self.last_hourly_state: self.display_manager.update_display() return - - # Clear the display + self.display_manager.clear() - - # Create a new image for drawing - img = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) + img = self._render_hourly_forecast_image() + if img: + self.display_manager.image = img + self.display_manager.update_display() + self.last_hourly_state = current_state + except Exception as e: + self.logger.error(f"Error displaying hourly forecast: {e}") + + def _render_daily_forecast_image(self) -> Optional[Image.Image]: + """Render daily forecast to an Image without display side effects.""" + try: + if not self.daily_forecast: + return None + + width = self.display_manager.matrix.width + height = self.display_manager.matrix.height + img = Image.new('RGB', (width, height), (0, 0, 0)) draw = ImageDraw.Draw(img) - - # Calculate layout based on matrix dimensions for 3 days + days_to_show = min(3, len(self.daily_forecast)) if days_to_show == 0: - # Handle case where there's no forecast data after filtering draw.text((2, 2), "No daily forecast", font=self.display_manager.small_font, fill=self.COLORS['dim']) else: - total_width = self.display_manager.matrix.width - section_width = total_width // days_to_show - padding = max(2, section_width // 6) - + section_width = width // days_to_show + for i in range(days_to_show): forecast = self.daily_forecast[i] - x = i * section_width - center_x = x + section_width // 2 - - # Draw day name at top + center_x = i * section_width + section_width // 2 + + # Day name at top day_text = forecast['date'] day_width = draw.textlength(day_text, font=self.display_manager.small_font) draw.text((center_x - day_width // 2, 1), day_text, font=self.display_manager.small_font, fill=self.COLORS['text']) - - # Draw weather icon centered vertically between top/bottom text + + # Weather icon icon_size = self.ICON_SIZE['large'] - top_text_height = 8 - bottom_text_y = self.display_manager.matrix.height - 8 - available_height_for_icon = bottom_text_y - top_text_height - calculated_y = top_text_height + (available_height_for_icon - icon_size) // 2 - icon_y = (self.display_manager.matrix.height // 2) - 16 + icon_y = (height // 2) - 16 icon_x = center_x - icon_size // 2 WeatherIcons.draw_weather_icon(img, forecast['icon'], icon_x, icon_y, icon_size) - - # Draw high/low temperatures at bottom + + # High/low temperatures at bottom temp_text = f"{forecast['temp_low']} / {forecast['temp_high']}" temp_width = draw.textlength(temp_text, font=self.display_manager.extra_small_font) - temp_y = self.display_manager.matrix.height - 8 + temp_y = height - 8 draw.text((center_x - temp_width // 2, temp_y), temp_text, font=self.display_manager.extra_small_font, fill=self.COLORS['text']) - - # Update the display - self.display_manager.image = img - self.display_manager.update_display() - self.last_daily_state = current_state + return img + except Exception as e: + self.logger.exception("Error rendering daily forecast") + return None + + def _display_daily_forecast(self) -> None: + """Display daily forecast with weather icons.""" + try: + if not self.daily_forecast: + self._display_no_data() + return + + current_state = self._get_daily_state() + if current_state == self.last_daily_state: + self.display_manager.update_display() + return + + self.display_manager.clear() + img = self._render_daily_forecast_image() + if img: + self.display_manager.image = img + self.display_manager.update_display() + self.last_daily_state = current_state except Exception as e: self.logger.error(f"Error displaying daily forecast: {e}") + def get_vegas_content(self): + """Return images for all enabled weather display modes.""" + if not self.weather_data: + return None + + images = [] + + if self.show_current: + img = self._render_current_weather_image() + if img: + images.append(img) + + if self.show_hourly and self.hourly_forecast: + img = self._render_hourly_forecast_image() + if img: + images.append(img) + + if self.show_daily and self.daily_forecast: + img = self._render_daily_forecast_image() + if img: + images.append(img) + + if images: + total_width = sum(img.width for img in images) + self.logger.info( + "[Weather Vegas] Returning %d image(s), %dpx total", + len(images), total_width + ) + return images + + return None + def display_weather(self, force_clear: bool = False) -> None: """Display current weather (compatibility method for display controller).""" self.display('weather', force_clear) diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index 301950c..5c7c923 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-weather", "name": "Weather Display", - "version": "2.1.0", + "version": "2.1.1", "author": "ChuckBuilds", "class_name": "WeatherPlugin", "description": "Comprehensive weather display with current conditions, hourly forecast, daily forecast, UV index, wind direction, and weather icons. Features state caching, API counter tracking, and error handling. Powered by OpenWeatherMap API with beautiful weather icons.", diff --git a/plugins/soccer-scoreboard/manager.py b/plugins/soccer-scoreboard/manager.py index 94cd0e2..18a65c1 100644 --- a/plugins/soccer-scoreboard/manager.py +++ b/plugins/soccer-scoreboard/manager.py @@ -2074,30 +2074,30 @@ def get_vegas_content(self) -> Optional[Any]: """ Get content for Vegas-style continuous scroll mode. - Returns None to let PluginAdapter auto-detect scroll_helper.cached_image. - Triggers scroll content generation if cache is empty to ensure Vegas - has content to display. + Triggers scroll content generation if cache is empty, then returns + the cached scroll image(s) for Vegas to compose into its scroll strip. Returns: - None - PluginAdapter will extract scroll_helper.cached_image automatically + List of PIL Images from scroll displays, or None if no content """ - # Ensure scroll content is generated for Vegas mode - if hasattr(self, '_scroll_manager') and self._scroll_manager: - # Check if 'mixed' scroll display has Vegas content with cache_type marker - has_vegas_content = False - mixed_display = self._scroll_manager._scroll_displays.get('mixed') - if mixed_display and hasattr(mixed_display, 'scroll_helper') and mixed_display.scroll_helper: - helper = mixed_display.scroll_helper - # Verify both cached_image exists AND cache_type is 'mixed' - if (helper.cached_image is not None and - getattr(helper, 'cache_type', None) == 'mixed'): - has_vegas_content = True - - if not has_vegas_content: - self.logger.info("[Soccer Vegas] Triggering scroll content generation") - self._ensure_scroll_content_for_vegas() - - # Return None - PluginAdapter will auto-detect scroll_helper.cached_image + if not hasattr(self, '_scroll_manager') or not self._scroll_manager: + return None + + images = self._scroll_manager.get_all_vegas_content_items() + + if not images: + self.logger.info("[Soccer Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + images = self._scroll_manager.get_all_vegas_content_items() + + if images: + total_width = sum(img.width for img in images) + self.logger.info( + "[Soccer Vegas] Returning %d image(s), %dpx total", + len(images), total_width + ) + return images + return None def get_vegas_content_type(self) -> str: diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index e0f040e..170e569 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "1.3.0", + "version": "1.3.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and more", "category": "sports", diff --git a/plugins/soccer-scoreboard/scroll_display.py b/plugins/soccer-scoreboard/scroll_display.py index fc1eeb2..caaceb2 100644 --- a/plugins/soccer-scoreboard/scroll_display.py +++ b/plugins/soccer-scoreboard/scroll_display.py @@ -117,6 +117,7 @@ def __init__( self._current_games: List[Dict] = [] self._current_game_type: str = "" self._current_leagues: List[str] = [] + self._vegas_content_items: List[Image.Image] = [] self._is_scrolling: bool = False self._scroll_start_time: float = 0 self._frame_count: int = 0 @@ -354,6 +355,9 @@ def prepare_scroll_content( self.logger.warning("No game cards rendered") return False + # Store individual items for Vegas mode (avoids scroll_helper padding) + self._vegas_content_items = list(content_items) + # Create scrolling image using ScrollHelper self.scroll_helper.create_scrolling_image( content_items, @@ -652,6 +656,15 @@ def has_cached_content(self) -> bool: return True return False + def get_all_vegas_content_items(self) -> list: + """Collect _vegas_content_items from all scroll displays.""" + items = [] + for sd in self._scroll_displays.values(): + vegas_items = getattr(sd, '_vegas_content_items', None) + if vegas_items: + items.extend(vegas_items) + return items + def clear_all(self) -> None: """Clear all scroll displays.""" for scroll_display in self._scroll_displays.values(): diff --git a/plugins/ufc-scoreboard/manager.py b/plugins/ufc-scoreboard/manager.py index 41bbdb3..05f3f51 100644 --- a/plugins/ufc-scoreboard/manager.py +++ b/plugins/ufc-scoreboard/manager.py @@ -1034,13 +1034,29 @@ def get_vegas_content(self) -> Optional[Any]: """ Get content for Vegas-style continuous scroll mode. - Returns None to let PluginAdapter auto-detect scroll_helper.cached_image. - Triggers scroll content generation if cache is empty. + Triggers scroll content generation if cache is empty, then returns + the cached scroll image(s) for Vegas to compose into its scroll strip. + + Returns: + List of PIL Images from scroll displays, or None if no content """ - if hasattr(self, "_scroll_manager") and self._scroll_manager: - if not self._scroll_manager.has_cached_content(): - self.logger.info("[UFC Vegas] Triggering scroll content generation") - self._ensure_scroll_content_for_vegas() + if not hasattr(self, "_scroll_manager") or not self._scroll_manager: + return None + + images = self._scroll_manager.get_all_vegas_content_items() + + if not images: + self.logger.info("[UFC Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + images = self._scroll_manager.get_all_vegas_content_items() + + if images: + total_width = sum(img.width for img in images) + self.logger.info( + "[UFC Vegas] Returning %d image(s), %dpx total", + len(images), total_width + ) + return images return None diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index c99b6fc..187742e 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.0.0", + "version": "1.0.1", "author": "ChuckBuilds", "contributors": [ { diff --git a/plugins/ufc-scoreboard/scroll_display.py b/plugins/ufc-scoreboard/scroll_display.py index 43bb959..42122e4 100644 --- a/plugins/ufc-scoreboard/scroll_display.py +++ b/plugins/ufc-scoreboard/scroll_display.py @@ -82,6 +82,7 @@ def __init__( self._current_fights: List[Dict] = [] self._current_fight_type: str = "" self._current_leagues: List[str] = [] + self._vegas_content_items: List[Image.Image] = [] self._is_scrolling = False self._scroll_start_time: Optional[float] = None self._last_log_time: float = 0 @@ -199,6 +200,10 @@ def has_cached_content(self) -> bool: and self.scroll_helper.cached_image is not None ) + def get_all_vegas_content_items(self) -> list: + """Return _vegas_content_items (flat manager, no _scroll_displays).""" + return list(self._vegas_content_items) if self._vegas_content_items else [] + def prepare_and_display( self, fights: List[Dict], @@ -294,6 +299,9 @@ def prepare_and_display( self.logger.warning("No fight cards were rendered") return False + # Store individual items for Vegas mode (avoids scroll_helper padding) + self._vegas_content_items = list(content_items) + # Create scrolling image try: self.scroll_helper.create_scrolling_image(