From 08cf29a3b68a0ed5b90fc5630f04cbfda6702dc8 Mon Sep 17 00:00:00 2001 From: Chuck Date: Sun, 15 Feb 2026 18:29:33 -0500 Subject: [PATCH 1/6] fix(vegas): return individual game cards from get_vegas_content() All 6 sports plugins were returning scroll_helper.cached_image (a pre-composed scroll strip with 192px leading blank padding) or None from get_vegas_content(). This wasted space in Vegas mode's scroll and prevented proper content display. Changes: - Store individual content_items on ScrollDisplay as _vegas_content_items before passing to create_scrolling_image() - Update get_vegas_content() in all 6 managers to return individual game card images instead of the scroll strip - Fix soccer ScrollHelper constructor (3-arg signature + setter methods) - Fix soccer is_scroll_complete() method name This matches the Olympics pattern of returning a list of individual card images, letting Vegas compose them with its own gaps. Affected plugins: hockey, soccer, baseball, basketball, football, ufc Co-Authored-By: Claude Opus 4.6 --- plugins/baseball-scoreboard/manager.py | 34 ++++++++---- plugins/baseball-scoreboard/scroll_display.py | 4 ++ plugins/basketball-scoreboard/manager.py | 37 +++++++++---- .../basketball-scoreboard/scroll_display.py | 6 +- plugins/football-scoreboard/manager.py | 34 ++++++++---- plugins/football-scoreboard/scroll_display.py | 6 +- plugins/hockey-scoreboard/manager.py | 50 +++++++++++------ plugins/hockey-scoreboard/scroll_display.py | 4 ++ plugins/soccer-scoreboard/manager.py | 55 ++++++++++++------- plugins/soccer-scoreboard/scroll_display.py | 4 ++ plugins/ufc-scoreboard/manager.py | 32 +++++++++-- plugins/ufc-scoreboard/scroll_display.py | 4 ++ 12 files changed, 191 insertions(+), 79 deletions(-) diff --git a/plugins/baseball-scoreboard/manager.py b/plugins/baseball-scoreboard/manager.py index 45e77db..a637d2f 100644 --- a/plugins/baseball-scoreboard/manager.py +++ b/plugins/baseball-scoreboard/manager.py @@ -3626,20 +3626,34 @@ 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 + + if not self._scroll_manager.has_cached_content(): + self.logger.info("[Baseball Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + + # Collect individual game card images for Vegas (not the scroll strip) + images = [] + for scroll_display in self._scroll_manager._scroll_displays.values(): + vegas_items = getattr(scroll_display, '_vegas_content_items', None) + if vegas_items: + images.extend(vegas_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/scroll_display.py b/plugins/baseball-scoreboard/scroll_display.py index 90e782a..bac2f31 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, diff --git a/plugins/basketball-scoreboard/manager.py b/plugins/basketball-scoreboard/manager.py index 1fbee1a..4831c37 100644 --- a/plugins/basketball-scoreboard/manager.py +++ b/plugins/basketball-scoreboard/manager.py @@ -3251,21 +3251,34 @@ 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 + + if not self._scroll_manager.has_cached_content(): + self.logger.info("[Basketball Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + + # Collect individual game card images for Vegas (not the scroll strip) + images = [] + for scroll_display in self._scroll_manager._scroll_displays.values(): + vegas_items = getattr(scroll_display, '_vegas_content_items', None) + if vegas_items: + images.extend(vegas_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/scroll_display.py b/plugins/basketball-scoreboard/scroll_display.py index 6f57396..a835e8d 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( diff --git a/plugins/football-scoreboard/manager.py b/plugins/football-scoreboard/manager.py index 5f7bb0a..2ebe769 100644 --- a/plugins/football-scoreboard/manager.py +++ b/plugins/football-scoreboard/manager.py @@ -3376,20 +3376,34 @@ 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 + + if not self._scroll_manager.has_cached_content(): + self.logger.info("[Football Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + + # Collect individual game card images for Vegas (not the scroll strip) + images = [] + for scroll_display in self._scroll_manager._scroll_displays.values(): + vegas_items = getattr(scroll_display, '_vegas_content_items', None) + if vegas_items: + images.extend(vegas_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/scroll_display.py b/plugins/football-scoreboard/scroll_display.py index ea8d532..e5f03cb 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( diff --git a/plugins/hockey-scoreboard/manager.py b/plugins/hockey-scoreboard/manager.py index 7bf0e04..e5cd689 100644 --- a/plugins/hockey-scoreboard/manager.py +++ b/plugins/hockey-scoreboard/manager.py @@ -3088,28 +3088,42 @@ 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 + + # 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 + + if not has_content: + self.logger.info("[Hockey Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + + # Collect individual game card images for Vegas (not the scroll strip) + images = [] + for scroll_display in self._scroll_manager._scroll_displays.values(): + vegas_items = getattr(scroll_display, '_vegas_content_items', None) + if vegas_items: + images.extend(vegas_items) - if not has_content: - self.logger.info("[Hockey Vegas] Triggering scroll content generation") - self._ensure_scroll_content_for_vegas() + 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/scroll_display.py b/plugins/hockey-scoreboard/scroll_display.py index daa06ec..7bc41be 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, diff --git a/plugins/soccer-scoreboard/manager.py b/plugins/soccer-scoreboard/manager.py index 94cd0e2..8c212b4 100644 --- a/plugins/soccer-scoreboard/manager.py +++ b/plugins/soccer-scoreboard/manager.py @@ -2074,30 +2074,43 @@ 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 + + # 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 + 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() + + # Collect individual game card images for Vegas (not the scroll strip) + images = [] + for scroll_display in self._scroll_manager._scroll_displays.values(): + vegas_items = getattr(scroll_display, '_vegas_content_items', None) + if vegas_items: + images.extend(vegas_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/scroll_display.py b/plugins/soccer-scoreboard/scroll_display.py index fc1eeb2..bb72c4d 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, diff --git a/plugins/ufc-scoreboard/manager.py b/plugins/ufc-scoreboard/manager.py index 41bbdb3..bc368e9 100644 --- a/plugins/ufc-scoreboard/manager.py +++ b/plugins/ufc-scoreboard/manager.py @@ -1034,13 +1034,33 @@ 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 + + if not self._scroll_manager.has_cached_content(): + self.logger.info("[UFC Vegas] Triggering scroll content generation") + self._ensure_scroll_content_for_vegas() + + # Collect individual game card images for Vegas (not the scroll strip) + images = [] + for scroll_display in self._scroll_manager._scroll_displays.values(): + vegas_items = getattr(scroll_display, '_vegas_content_items', None) + if vegas_items: + images.extend(vegas_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/scroll_display.py b/plugins/ufc-scoreboard/scroll_display.py index 43bb959..a2d6651 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 @@ -294,6 +295,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( From 294a1ba65c96e33ae7c6d49c4f9af5e89a685119 Mon Sep 17 00:00:00 2001 From: Chuck Date: Sun, 15 Feb 2026 18:42:36 -0500 Subject: [PATCH 2/6] fix(vegas): remove logo overhang that clips in Vegas scroll mode Logos were positioned with negative x offsets (away at -10, home at display_width - logo_width + 10) which worked for full-screen display but got clipped by PIL when rendering individual game cards for Vegas mode. Position logos flush with card edges instead. Affects: hockey, baseball (all 3 render modes), basketball, football. Co-Authored-By: Claude Opus 4.6 --- plugins/baseball-scoreboard/game_renderer.py | 12 ++++++------ plugins/basketball-scoreboard/game_renderer.py | 6 +++--- plugins/football-scoreboard/game_renderer.py | 6 +++--- plugins/hockey-scoreboard/game_renderer.py | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) 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/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/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/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) From d613e560dc46805f791b121ba59a5b12222c603d Mon Sep 17 00:00:00 2001 From: Chuck Date: Sun, 15 Feb 2026 18:54:01 -0500 Subject: [PATCH 3/6] fix(vegas): return all weather display modes in Vegas scroll Weather plugin's get_vegas_content() was returning None, so Vegas mode only captured whichever single display was active at the time (usually hourly). Now renders all enabled modes (current conditions, hourly forecast, daily forecast) and returns them as separate images. Co-Authored-By: Claude Opus 4.6 --- plugins/ledmatrix-weather/manager.py | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/plugins/ledmatrix-weather/manager.py b/plugins/ledmatrix-weather/manager.py index 29e613d..49d19cc 100644 --- a/plugins/ledmatrix-weather/manager.py +++ b/plugins/ledmatrix-weather/manager.py @@ -835,6 +835,51 @@ def _display_daily_forecast(self) -> None: 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 = [] + + # Save state caches so we can force re-render without side effects + saved_weather = self.last_weather_state + saved_hourly = self.last_hourly_state + saved_daily = self.last_daily_state + + try: + if self.show_current: + self.last_weather_state = None + self._display_current_weather() + if self.display_manager.image: + images.append(self.display_manager.image.copy()) + + if self.show_hourly and self.hourly_forecast: + self.last_hourly_state = None + self._display_hourly_forecast() + if self.display_manager.image: + images.append(self.display_manager.image.copy()) + + if self.show_daily and self.daily_forecast: + self.last_daily_state = None + self._display_daily_forecast() + if self.display_manager.image: + images.append(self.display_manager.image.copy()) + finally: + self.last_weather_state = saved_weather + self.last_hourly_state = saved_hourly + self.last_daily_state = saved_daily + + 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) From 615ccb9db7455eae213bd37d38ba37293212afce Mon Sep 17 00:00:00 2001 From: Chuck Date: Sun, 15 Feb 2026 19:11:42 -0500 Subject: [PATCH 4/6] fix(vegas): align cache checks with _vegas_content_items and remove side effects - Sports plugins (hockey, basketball, football, baseball): Replace has_cached_content()/cached_image checks with _vegas_content_items checks so regeneration triggers correctly when Vegas items are missing. - UFC: Handle flat ScrollDisplayManager that has _vegas_content_items directly (no _scroll_displays dict), fixing AttributeError. - Weather: Extract pure _render_*_image() methods that return PIL Images without touching display_manager. get_vegas_content() now calls these instead of the _display_*() methods that mutate display state. Co-Authored-By: Claude Opus 4.6 --- plugins/baseball-scoreboard/manager.py | 8 +- plugins/basketball-scoreboard/manager.py | 8 +- plugins/football-scoreboard/manager.py | 8 +- plugins/hockey-scoreboard/manager.py | 12 +- plugins/ledmatrix-weather/manager.py | 290 +++++++++++------------ plugins/ufc-scoreboard/manager.py | 13 +- 6 files changed, 168 insertions(+), 171 deletions(-) diff --git a/plugins/baseball-scoreboard/manager.py b/plugins/baseball-scoreboard/manager.py index a637d2f..7508a23 100644 --- a/plugins/baseball-scoreboard/manager.py +++ b/plugins/baseball-scoreboard/manager.py @@ -3635,7 +3635,13 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - if not self._scroll_manager.has_cached_content(): + # Check if any scroll display has vegas content items + has_content = any( + getattr(sd, '_vegas_content_items', None) + for sd in self._scroll_manager._scroll_displays.values() + ) + + if not has_content: self.logger.info("[Baseball Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() diff --git a/plugins/basketball-scoreboard/manager.py b/plugins/basketball-scoreboard/manager.py index 4831c37..be16bb6 100644 --- a/plugins/basketball-scoreboard/manager.py +++ b/plugins/basketball-scoreboard/manager.py @@ -3260,7 +3260,13 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - if not self._scroll_manager.has_cached_content(): + # Check if any scroll display has vegas content items + has_content = any( + getattr(sd, '_vegas_content_items', None) + for sd in self._scroll_manager._scroll_displays.values() + ) + + if not has_content: self.logger.info("[Basketball Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() diff --git a/plugins/football-scoreboard/manager.py b/plugins/football-scoreboard/manager.py index 2ebe769..dc90d7c 100644 --- a/plugins/football-scoreboard/manager.py +++ b/plugins/football-scoreboard/manager.py @@ -3385,7 +3385,13 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - if not self._scroll_manager.has_cached_content(): + # Check if any scroll display has vegas content items + has_content = any( + getattr(sd, '_vegas_content_items', None) + for sd in self._scroll_manager._scroll_displays.values() + ) + + if not has_content: self.logger.info("[Football Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() diff --git a/plugins/hockey-scoreboard/manager.py b/plugins/hockey-scoreboard/manager.py index e5cd689..39f38f0 100644 --- a/plugins/hockey-scoreboard/manager.py +++ b/plugins/hockey-scoreboard/manager.py @@ -3097,13 +3097,11 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - # 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 + # Check if any scroll display has vegas content items + has_content = any( + getattr(sd, '_vegas_content_items', None) + for sd in self._scroll_manager._scroll_displays.values() + ) if not has_content: self.logger.info("[Hockey Vegas] Triggering scroll content generation") diff --git a/plugins/ledmatrix-weather/manager.py b/plugins/ledmatrix-weather/manager.py index 49d19cc..18740c3 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.error(f"Error rendering current weather: {e}") + 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,102 +717,116 @@ 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.error(f"Error rendering hourly forecast: {e}") + 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.error(f"Error rendering daily forecast: {e}") + 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}") @@ -842,33 +837,20 @@ def get_vegas_content(self): images = [] - # Save state caches so we can force re-render without side effects - saved_weather = self.last_weather_state - saved_hourly = self.last_hourly_state - saved_daily = self.last_daily_state - - try: - if self.show_current: - self.last_weather_state = None - self._display_current_weather() - if self.display_manager.image: - images.append(self.display_manager.image.copy()) - - if self.show_hourly and self.hourly_forecast: - self.last_hourly_state = None - self._display_hourly_forecast() - if self.display_manager.image: - images.append(self.display_manager.image.copy()) - - if self.show_daily and self.daily_forecast: - self.last_daily_state = None - self._display_daily_forecast() - if self.display_manager.image: - images.append(self.display_manager.image.copy()) - finally: - self.last_weather_state = saved_weather - self.last_hourly_state = saved_hourly - self.last_daily_state = saved_daily + 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) diff --git a/plugins/ufc-scoreboard/manager.py b/plugins/ufc-scoreboard/manager.py index bc368e9..bfce9f0 100644 --- a/plugins/ufc-scoreboard/manager.py +++ b/plugins/ufc-scoreboard/manager.py @@ -1043,16 +1043,15 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, "_scroll_manager") or not self._scroll_manager: return None - if not self._scroll_manager.has_cached_content(): + # UFC's ScrollDisplayManager is flat (no _scroll_displays dict) + vegas_items = getattr(self._scroll_manager, '_vegas_content_items', None) + + if not vegas_items: self.logger.info("[UFC Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() + vegas_items = getattr(self._scroll_manager, '_vegas_content_items', None) - # Collect individual game card images for Vegas (not the scroll strip) - images = [] - for scroll_display in self._scroll_manager._scroll_displays.values(): - vegas_items = getattr(scroll_display, '_vegas_content_items', None) - if vegas_items: - images.extend(vegas_items) + images = list(vegas_items) if vegas_items else [] if images: total_width = sum(img.width for img in images) From 0e299f8e2120432f99c2301d9531e9f20cbec865 Mon Sep 17 00:00:00 2001 From: Chuck Date: Sun, 15 Feb 2026 19:18:29 -0500 Subject: [PATCH 5/6] refactor(vegas): encapsulate _scroll_displays access and fix logging - Add get_all_vegas_content_items() to all ScrollDisplayManagers, replacing direct _scroll_displays iteration in get_vegas_content(). UFC's flat manager returns its own items; others iterate displays. - Weather: use logger.exception() in _render_*_image() catch blocks to preserve stack traces for debugging rendering failures. Co-Authored-By: Claude Opus 4.6 --- plugins/baseball-scoreboard/manager.py | 16 +++----------- plugins/baseball-scoreboard/scroll_display.py | 9 ++++++++ plugins/basketball-scoreboard/manager.py | 16 +++----------- .../basketball-scoreboard/scroll_display.py | 9 ++++++++ plugins/football-scoreboard/manager.py | 16 +++----------- plugins/football-scoreboard/scroll_display.py | 11 +++++++++- plugins/hockey-scoreboard/manager.py | 16 +++----------- plugins/hockey-scoreboard/scroll_display.py | 9 ++++++++ plugins/ledmatrix-weather/manager.py | 6 +++--- plugins/soccer-scoreboard/manager.py | 21 ++++--------------- plugins/soccer-scoreboard/scroll_display.py | 9 ++++++++ plugins/ufc-scoreboard/manager.py | 9 +++----- plugins/ufc-scoreboard/scroll_display.py | 4 ++++ 13 files changed, 72 insertions(+), 79 deletions(-) diff --git a/plugins/baseball-scoreboard/manager.py b/plugins/baseball-scoreboard/manager.py index 7508a23..ea03c08 100644 --- a/plugins/baseball-scoreboard/manager.py +++ b/plugins/baseball-scoreboard/manager.py @@ -3635,22 +3635,12 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - # Check if any scroll display has vegas content items - has_content = any( - getattr(sd, '_vegas_content_items', None) - for sd in self._scroll_manager._scroll_displays.values() - ) + images = self._scroll_manager.get_all_vegas_content_items() - if not has_content: + if not images: self.logger.info("[Baseball Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() - - # Collect individual game card images for Vegas (not the scroll strip) - images = [] - for scroll_display in self._scroll_manager._scroll_displays.values(): - vegas_items = getattr(scroll_display, '_vegas_content_items', None) - if vegas_items: - images.extend(vegas_items) + images = self._scroll_manager.get_all_vegas_content_items() if images: total_width = sum(img.width for img in images) diff --git a/plugins/baseball-scoreboard/scroll_display.py b/plugins/baseball-scoreboard/scroll_display.py index bac2f31..c7cfea0 100644 --- a/plugins/baseball-scoreboard/scroll_display.py +++ b/plugins/baseball-scoreboard/scroll_display.py @@ -661,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/manager.py b/plugins/basketball-scoreboard/manager.py index be16bb6..b2fde27 100644 --- a/plugins/basketball-scoreboard/manager.py +++ b/plugins/basketball-scoreboard/manager.py @@ -3260,22 +3260,12 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - # Check if any scroll display has vegas content items - has_content = any( - getattr(sd, '_vegas_content_items', None) - for sd in self._scroll_manager._scroll_displays.values() - ) + images = self._scroll_manager.get_all_vegas_content_items() - if not has_content: + if not images: self.logger.info("[Basketball Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() - - # Collect individual game card images for Vegas (not the scroll strip) - images = [] - for scroll_display in self._scroll_manager._scroll_displays.values(): - vegas_items = getattr(scroll_display, '_vegas_content_items', None) - if vegas_items: - images.extend(vegas_items) + images = self._scroll_manager.get_all_vegas_content_items() if images: total_width = sum(img.width for img in images) diff --git a/plugins/basketball-scoreboard/scroll_display.py b/plugins/basketball-scoreboard/scroll_display.py index a835e8d..85cfb8d 100644 --- a/plugins/basketball-scoreboard/scroll_display.py +++ b/plugins/basketball-scoreboard/scroll_display.py @@ -671,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/manager.py b/plugins/football-scoreboard/manager.py index dc90d7c..5804c84 100644 --- a/plugins/football-scoreboard/manager.py +++ b/plugins/football-scoreboard/manager.py @@ -3385,22 +3385,12 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - # Check if any scroll display has vegas content items - has_content = any( - getattr(sd, '_vegas_content_items', None) - for sd in self._scroll_manager._scroll_displays.values() - ) + images = self._scroll_manager.get_all_vegas_content_items() - if not has_content: + if not images: self.logger.info("[Football Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() - - # Collect individual game card images for Vegas (not the scroll strip) - images = [] - for scroll_display in self._scroll_manager._scroll_displays.values(): - vegas_items = getattr(scroll_display, '_vegas_content_items', None) - if vegas_items: - images.extend(vegas_items) + images = self._scroll_manager.get_all_vegas_content_items() if images: total_width = sum(img.width for img in images) diff --git a/plugins/football-scoreboard/scroll_display.py b/plugins/football-scoreboard/scroll_display.py index e5f03cb..6a04afc 100644 --- a/plugins/football-scoreboard/scroll_display.py +++ b/plugins/football-scoreboard/scroll_display.py @@ -634,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/manager.py b/plugins/hockey-scoreboard/manager.py index 39f38f0..284cf77 100644 --- a/plugins/hockey-scoreboard/manager.py +++ b/plugins/hockey-scoreboard/manager.py @@ -3097,22 +3097,12 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - # Check if any scroll display has vegas content items - has_content = any( - getattr(sd, '_vegas_content_items', None) - for sd in self._scroll_manager._scroll_displays.values() - ) + images = self._scroll_manager.get_all_vegas_content_items() - if not has_content: + if not images: self.logger.info("[Hockey Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() - - # Collect individual game card images for Vegas (not the scroll strip) - images = [] - for scroll_display in self._scroll_manager._scroll_displays.values(): - vegas_items = getattr(scroll_display, '_vegas_content_items', None) - if vegas_items: - images.extend(vegas_items) + images = self._scroll_manager.get_all_vegas_content_items() if images: total_width = sum(img.width for img in images) diff --git a/plugins/hockey-scoreboard/scroll_display.py b/plugins/hockey-scoreboard/scroll_display.py index 7bc41be..9a8d278 100644 --- a/plugins/hockey-scoreboard/scroll_display.py +++ b/plugins/hockey-scoreboard/scroll_display.py @@ -650,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 18740c3..e270020 100644 --- a/plugins/ledmatrix-weather/manager.py +++ b/plugins/ledmatrix-weather/manager.py @@ -616,7 +616,7 @@ def _render_current_weather_image(self) -> Optional[Image.Image]: return img except Exception as e: - self.logger.error(f"Error rendering current weather: {e}") + self.logger.exception("Error rendering current weather") return None def _display_current_weather(self) -> None: @@ -735,7 +735,7 @@ def _render_hourly_forecast_image(self) -> Optional[Image.Image]: return img except Exception as e: - self.logger.error(f"Error rendering hourly forecast: {e}") + self.logger.exception("Error rendering hourly forecast") return None def _display_hourly_forecast(self) -> None: @@ -806,7 +806,7 @@ def _render_daily_forecast_image(self) -> Optional[Image.Image]: return img except Exception as e: - self.logger.error(f"Error rendering daily forecast: {e}") + self.logger.exception("Error rendering daily forecast") return None def _display_daily_forecast(self) -> None: diff --git a/plugins/soccer-scoreboard/manager.py b/plugins/soccer-scoreboard/manager.py index 8c212b4..18a65c1 100644 --- a/plugins/soccer-scoreboard/manager.py +++ b/plugins/soccer-scoreboard/manager.py @@ -2083,25 +2083,12 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, '_scroll_manager') or not self._scroll_manager: return None - # 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 - if (helper.cached_image is not None and - getattr(helper, 'cache_type', None) == 'mixed'): - has_vegas_content = True - - if not has_vegas_content: + 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() - - # Collect individual game card images for Vegas (not the scroll strip) - images = [] - for scroll_display in self._scroll_manager._scroll_displays.values(): - vegas_items = getattr(scroll_display, '_vegas_content_items', None) - if vegas_items: - images.extend(vegas_items) + images = self._scroll_manager.get_all_vegas_content_items() if images: total_width = sum(img.width for img in images) diff --git a/plugins/soccer-scoreboard/scroll_display.py b/plugins/soccer-scoreboard/scroll_display.py index bb72c4d..caaceb2 100644 --- a/plugins/soccer-scoreboard/scroll_display.py +++ b/plugins/soccer-scoreboard/scroll_display.py @@ -656,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 bfce9f0..05f3f51 100644 --- a/plugins/ufc-scoreboard/manager.py +++ b/plugins/ufc-scoreboard/manager.py @@ -1043,15 +1043,12 @@ def get_vegas_content(self) -> Optional[Any]: if not hasattr(self, "_scroll_manager") or not self._scroll_manager: return None - # UFC's ScrollDisplayManager is flat (no _scroll_displays dict) - vegas_items = getattr(self._scroll_manager, '_vegas_content_items', None) + images = self._scroll_manager.get_all_vegas_content_items() - if not vegas_items: + if not images: self.logger.info("[UFC Vegas] Triggering scroll content generation") self._ensure_scroll_content_for_vegas() - vegas_items = getattr(self._scroll_manager, '_vegas_content_items', None) - - images = list(vegas_items) if vegas_items else [] + images = self._scroll_manager.get_all_vegas_content_items() if images: total_width = sum(img.width for img in images) diff --git a/plugins/ufc-scoreboard/scroll_display.py b/plugins/ufc-scoreboard/scroll_display.py index a2d6651..42122e4 100644 --- a/plugins/ufc-scoreboard/scroll_display.py +++ b/plugins/ufc-scoreboard/scroll_display.py @@ -200,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], From a482b83d29be0ed836dae49d08f79c4fd04efc8a Mon Sep 17 00:00:00 2001 From: Chuck Date: Sun, 15 Feb 2026 19:22:07 -0500 Subject: [PATCH 6/6] chore: bump plugin versions for Vegas content changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit baseball-scoreboard: 2.0.0 → 2.0.1 basketball-scoreboard: 1.1.0 → 1.1.1 football-scoreboard: 2.1.0 → 2.1.1 hockey-scoreboard: 1.1.0 → 1.1.1 soccer-scoreboard: 1.3.0 → 1.3.1 ufc-scoreboard: 1.0.0 → 1.0.1 ledmatrix-weather: 2.1.0 → 2.1.1 Co-Authored-By: Claude Opus 4.6 --- plugins.json | 30 ++++++++++----------- plugins/baseball-scoreboard/manifest.json | 2 +- plugins/basketball-scoreboard/manifest.json | 2 +- plugins/football-scoreboard/manifest.json | 2 +- plugins/hockey-scoreboard/manifest.json | 2 +- plugins/ledmatrix-weather/manifest.json | 2 +- plugins/soccer-scoreboard/manifest.json | 2 +- plugins/ufc-scoreboard/manifest.json | 2 +- 8 files changed, 22 insertions(+), 22 deletions(-) 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/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/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/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/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/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/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/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": [ {