From 97dd081939b6707ece92151dcc6c6df10f6830ff Mon Sep 17 00:00:00 2001 From: Chuck Date: Mon, 16 Feb 2026 20:52:03 -0500 Subject: [PATCH 1/3] fix(baseball): handle missing count/outs data in NCAA baseball scorebugs - Detect when ESPN NCAA API lacks count/outs data (has_count_data flag) - Hide outs circles and balls-strikes count for NCAA games (data unavailable) - Bases diamond always renders (ESPN provides onFirst/onSecond/onThird) - Reduce live scorebug logo offset from 10px to 2px Co-Authored-By: Claude Opus 4.6 --- plugins/baseball-scoreboard/baseball.py | 191 ++++++++++-------- plugins/baseball-scoreboard/game_renderer.py | 54 ++--- .../ncaa_baseball_managers.py | 1 + 3 files changed, 132 insertions(+), 114 deletions(-) diff --git a/plugins/baseball-scoreboard/baseball.py b/plugins/baseball-scoreboard/baseball.py index dd7b1fd..6f11fe7 100644 --- a/plugins/baseball-scoreboard/baseball.py +++ b/plugins/baseball-scoreboard/baseball.py @@ -225,8 +225,13 @@ def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: # Get count and bases from situation situation = game_event["competitions"][0].get("situation", {}) + # Detect whether the API provides count/outs data + # NCAA baseball only provides onFirst/onSecond/onThird + has_count_data = "outs" in situation or "count" in situation or "balls" in situation + if is_favorite_game: self.logger.debug(f"Full situation data: {situation}") + self.logger.debug(f"has_count_data: {has_count_data}") # Get count from the correct location in the API response count = situation.get("count", {}) @@ -289,6 +294,7 @@ def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: strikes = 0 outs = 0 bases_occupied = [False, False, False] + has_count_data = False details.update( { @@ -300,6 +306,7 @@ def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: "strikes": strikes, "outs": outs, "bases_occupied": bases_occupied, + "has_count_data": has_count_data, "start_time": game_event["date"], "series_summary": series_summary, } @@ -434,14 +441,14 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: center_y = self.display_height // 2 - # Draw logos (shifted slightly more inward than NHL perhaps) + # Draw logos with slight edge bleed home_x = ( - self.display_width - home_logo.width + 10 + self.display_width - home_logo.width + 2 ) home_y = center_y - (home_logo.height // 2) main_img.paste(home_logo, (home_x, home_y), home_logo) - away_x = -10 + away_x = -2 away_y = center_y - (away_logo.height // 2) main_img.paste(away_logo, (away_x, away_y), away_logo) @@ -506,19 +513,22 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: bases_origin_x = (self.display_width - base_cluster_width) // 2 # Determine relative positions for outs based on inning half - if inning_half == "top": # Away batting, outs on left - outs_column_x = ( - bases_origin_x - spacing_between_bases_outs - out_cluster_width - ) - else: # Home batting, outs on right - outs_column_x = ( - bases_origin_x + base_cluster_width + spacing_between_bases_outs - ) + # Only compute outs column position when count data is available + has_count_data = game.get("has_count_data", True) + if has_count_data: + if inning_half == "top": # Away batting, outs on left + outs_column_x = ( + bases_origin_x - spacing_between_bases_outs - out_cluster_width + ) + else: # Home batting, outs on right + outs_column_x = ( + bases_origin_x + base_cluster_width + spacing_between_bases_outs + ) - # Calculate vertical alignment offset for outs column (center align with bases cluster) - outs_column_start_y = ( - overall_start_y + (base_cluster_height // 2) - (out_cluster_height // 2) - ) + # Calculate vertical alignment offset for outs column (center align with bases cluster) + outs_column_start_y = ( + overall_start_y + (base_cluster_height // 2) - (out_cluster_height // 2) + ) # --- Draw Bases (Diamonds) --- base_color_occupied = (255, 255, 255) @@ -570,84 +580,87 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: draw_overlay.polygon(poly1, outline=base_color_empty) # --- Draw Outs (Vertical Circles) --- - circle_color_out = (255, 255, 255) - circle_color_empty_outline = (100, 100, 100) - - for i in range(3): - cx = outs_column_x - cy = outs_column_start_y + i * ( - out_circle_diameter + out_vertical_spacing - ) - coords = [cx, cy, cx + out_circle_diameter, cy + out_circle_diameter] - if i < outs: - draw_overlay.ellipse(coords, fill=circle_color_out) - else: - draw_overlay.ellipse(coords, outline=circle_color_empty_outline) + # Only render outs and count when data is available (ESPN NCAA doesn't provide these) + if has_count_data: + circle_color_out = (255, 255, 255) + circle_color_empty_outline = (100, 100, 100) + + for i in range(3): + cx = outs_column_x + cy = outs_column_start_y + i * ( + out_circle_diameter + out_vertical_spacing + ) + coords = [cx, cy, cx + out_circle_diameter, cy + out_circle_diameter] + if i < outs: + draw_overlay.ellipse(coords, fill=circle_color_out) + else: + draw_overlay.ellipse(coords, outline=circle_color_empty_outline) # --- Draw Balls-Strikes Count (BDF Font) --- - balls = game.get("balls", 0) - strikes = game.get("strikes", 0) - - # Add debug logging for count with cooldown - current_time = time.time() - if ( - game["home_abbr"] in self.favorite_teams - or game["away_abbr"] in self.favorite_teams - ) and current_time - self.last_count_log_time >= self.count_log_interval: - self.logger.debug(f"Displaying count: {balls}-{strikes}") - self.logger.debug( - f"Raw count data: balls={game.get('balls')}, strikes={game.get('strikes')}" - ) - self.last_count_log_time = current_time - - count_text = f"{balls}-{strikes}" - bdf_font = self.display_manager.calendar_font - if not hasattr(self, '_bdf_font_sized'): - bdf_font.set_char_size(height=7 * 64) # Set 7px height once - self._bdf_font_sized = True - count_text_width = self.display_manager.get_text_width(count_text, bdf_font) - - # Position below the base/out cluster - cluster_bottom_y = ( - overall_start_y + base_cluster_height - ) # Find the bottom of the taller part (bases) - count_y = cluster_bottom_y + 2 # Start 2 pixels below cluster - - # Center horizontally within the BASE cluster width - count_x = bases_origin_x + (base_cluster_width - count_text_width) // 2 - - # Temporarily set draw object for BDF text rendering, then restore - original_draw = self.display_manager.draw - self.display_manager.draw = draw_overlay - try: - # Draw Balls-Strikes Count with outline using BDF font - outline_color_for_bdf = (0, 0, 0) - - # Draw outline - for dx_offset, dy_offset in [ - (-1, -1), - (-1, 0), - (-1, 1), - (0, -1), - (0, 1), - (1, -1), - (1, 0), - (1, 1), - ]: - self.display_manager._draw_bdf_text( - count_text, - count_x + dx_offset, - count_y + dy_offset, - color=outline_color_for_bdf, - font=bdf_font, + if has_count_data: + balls = game.get("balls", 0) + strikes = game.get("strikes", 0) + + # Add debug logging for count with cooldown + current_time = time.time() + if ( + game["home_abbr"] in self.favorite_teams + or game["away_abbr"] in self.favorite_teams + ) and current_time - self.last_count_log_time >= self.count_log_interval: + self.logger.debug(f"Displaying count: {balls}-{strikes}") + self.logger.debug( + f"Raw count data: balls={game.get('balls')}, strikes={game.get('strikes')}" ) + self.last_count_log_time = current_time + + count_text = f"{balls}-{strikes}" + bdf_font = self.display_manager.calendar_font + if not hasattr(self, '_bdf_font_sized'): + bdf_font.set_char_size(height=7 * 64) # Set 7px height once + self._bdf_font_sized = True + count_text_width = self.display_manager.get_text_width(count_text, bdf_font) + + # Position below the base/out cluster + cluster_bottom_y = ( + overall_start_y + base_cluster_height + ) # Find the bottom of the taller part (bases) + count_y = cluster_bottom_y + 2 # Start 2 pixels below cluster + + # Center horizontally within the BASE cluster width + count_x = bases_origin_x + (base_cluster_width - count_text_width) // 2 + + # Temporarily set draw object for BDF text rendering, then restore + original_draw = self.display_manager.draw + self.display_manager.draw = draw_overlay + try: + # Draw Balls-Strikes Count with outline using BDF font + outline_color_for_bdf = (0, 0, 0) + + # Draw outline + for dx_offset, dy_offset in [ + (-1, -1), + (-1, 0), + (-1, 1), + (0, -1), + (0, 1), + (1, -1), + (1, 0), + (1, 1), + ]: + self.display_manager._draw_bdf_text( + count_text, + count_x + dx_offset, + count_y + dy_offset, + color=outline_color_for_bdf, + font=bdf_font, + ) - # Draw main text - self.display_manager._draw_bdf_text( - count_text, count_x, count_y, color=text_color, font=bdf_font - ) - finally: - self.display_manager.draw = original_draw + # Draw main text + self.display_manager._draw_bdf_text( + count_text, count_x, count_y, color=text_color, font=bdf_font + ) + finally: + self.display_manager.draw = original_draw # Draw Team:Score at the bottom (matching main branch format) score_font = self.display_manager.font # Use PressStart2P diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index 0e5f552..9064568 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -210,13 +210,15 @@ def _render_live_game(self, game: Dict) -> Image.Image: overall_start_y = inning_bbox[3] + 1 bases_origin_x = (self.display_width - base_cluster_width) // 2 - # Outs column position + # Outs column position (only needed when count data is available) + has_count_data = game.get('has_count_data', True) out_cluster_height = 3 * out_circle_diameter + 2 * out_vertical_spacing - if inning_half == 'top': - outs_column_x = bases_origin_x - spacing_between_bases_outs - out_circle_diameter - else: - outs_column_x = bases_origin_x + base_cluster_width + spacing_between_bases_outs - outs_column_start_y = overall_start_y + (base_cluster_height // 2) - (out_cluster_height // 2) + if has_count_data: + if inning_half == 'top': + outs_column_x = bases_origin_x - spacing_between_bases_outs - out_circle_diameter + else: + outs_column_x = bases_origin_x + base_cluster_width + spacing_between_bases_outs + outs_column_start_y = overall_start_y + (base_cluster_height // 2) - (out_cluster_height // 2) # Draw bases as diamond polygons h_d = base_diamond_size // 2 @@ -243,25 +245,27 @@ def _render_live_game(self, game: Dict) -> Image.Image: poly1 = [(c1x, base_bottom_y + base_vert_spacing), (c1x + h_d, c1y), (c1x, c1y + h_d), (c1x - h_d, c1y)] draw.polygon(poly1, fill=base_fill if bases_occupied[0] else None, outline=base_outline) - # Outs circles - for i in range(3): - cx = outs_column_x - cy = outs_column_start_y + i * (out_circle_diameter + out_vertical_spacing) - coords = [cx, cy, cx + out_circle_diameter, cy + out_circle_diameter] - if i < outs: - draw.ellipse(coords, fill=(255, 255, 255)) - else: - draw.ellipse(coords, outline=(100, 100, 100)) - - # Balls-strikes count (below bases) - balls = game.get('balls', 0) - strikes = game.get('strikes', 0) - count_text = f"{balls}-{strikes}" - count_font = self.fonts['detail'] - count_width = draw.textlength(count_text, font=count_font) - count_y = overall_start_y + base_cluster_height + 2 - count_x = bases_origin_x + (base_cluster_width - count_width) // 2 - self._draw_text_with_outline(draw, count_text, (int(count_x), count_y), count_font) + # Outs circles (only when count data is available) + if has_count_data: + for i in range(3): + cx = outs_column_x + cy = outs_column_start_y + i * (out_circle_diameter + out_vertical_spacing) + coords = [cx, cy, cx + out_circle_diameter, cy + out_circle_diameter] + if i < outs: + draw.ellipse(coords, fill=(255, 255, 255)) + else: + draw.ellipse(coords, outline=(100, 100, 100)) + + # Balls-strikes count (below bases, only when count data is available) + if has_count_data: + balls = game.get('balls', 0) + strikes = game.get('strikes', 0) + count_text = f"{balls}-{strikes}" + count_font = self.fonts['detail'] + count_width = draw.textlength(count_text, font=count_font) + count_y = overall_start_y + base_cluster_height + 2 + count_x = bases_origin_x + (base_cluster_width - count_width) // 2 + self._draw_text_with_outline(draw, count_text, (int(count_x), count_y), count_font) # Team:Score at bottom corners score_font = self.fonts['score'] diff --git a/plugins/baseball-scoreboard/ncaa_baseball_managers.py b/plugins/baseball-scoreboard/ncaa_baseball_managers.py index e3d634c..bc6733a 100644 --- a/plugins/baseball-scoreboard/ncaa_baseball_managers.py +++ b/plugins/baseball-scoreboard/ncaa_baseball_managers.py @@ -209,6 +209,7 @@ def __init__(self, config: Dict[str, Any], display_manager, cache_manager): "away_logo_url": "", "status_text": "Bot 7th", "series_summary": "", + "has_count_data": False, } self.live_games = [self.current_game] self.logger.info( From eeb1f8bb37bb5ea6bfb63da35aad4c05296b3510 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 17 Feb 2026 10:22:25 -0500 Subject: [PATCH 2/3] fix(baseball): use league identifier for has_count_data detection Replace key-presence heuristic with deterministic self.league check to avoid false positives if ESPN ever adds null count keys to NCAA responses. Co-Authored-By: Claude Opus 4.6 --- plugins/baseball-scoreboard/baseball.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/baseball-scoreboard/baseball.py b/plugins/baseball-scoreboard/baseball.py index 6f11fe7..054c8a2 100644 --- a/plugins/baseball-scoreboard/baseball.py +++ b/plugins/baseball-scoreboard/baseball.py @@ -225,9 +225,9 @@ def _extract_game_details(self, game_event: Dict) -> Optional[Dict]: # Get count and bases from situation situation = game_event["competitions"][0].get("situation", {}) - # Detect whether the API provides count/outs data - # NCAA baseball only provides onFirst/onSecond/onThird - has_count_data = "outs" in situation or "count" in situation or "balls" in situation + # NCAA baseball API doesn't provide count/outs data (only onFirst/onSecond/onThird) + # Use league identifier for deterministic detection instead of key-presence heuristic + has_count_data = self.league != "college-baseball" if is_favorite_game: self.logger.debug(f"Full situation data: {situation}") From a3559fa7a208f0e5e2d73d08728c84c760330542 Mon Sep 17 00:00:00 2001 From: Chuck Date: Tue, 17 Feb 2026 10:28:26 -0500 Subject: [PATCH 3/3] chore(baseball): bump version to 1.3.1 for NCAA count detection fix Co-Authored-By: Claude Opus 4.6 --- plugins.json | 6 +++--- plugins/baseball-scoreboard/manifest.json | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins.json b/plugins.json index b95fcd0..0abb63d 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-02-15", + "last_updated": "2026-02-17", "plugins": [ { "id": "hello-world", @@ -296,10 +296,10 @@ "plugin_path": "plugins/baseball-scoreboard", "stars": 0, "downloads": 0, - "last_updated": "2026-02-15", + "last_updated": "2026-02-17", "verified": true, "screenshot": "", - "latest_version": "1.3.0" + "latest_version": "1.3.1" }, { "id": "soccer-scoreboard", diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 0c7f516..808fa83 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "baseball-scoreboard", "name": "Baseball Scoreboard", - "version": "1.3.0", + "version": "1.3.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", "category": "sports", @@ -30,6 +30,11 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "released": "2026-02-17", + "version": "1.3.1", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-02-15", "version": "1.3.0",