From 0ec13bee76a8745f2448d7d7b11f7470bb2a3656 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 20:24:59 -0400 Subject: [PATCH 01/13] =?UTF-8?q?fix(plugins):=20eight=20validation=20find?= =?UTF-8?q?ings=20=E2=80=94=20fonts,=20fit-checks,=20skip=20contract,=20re?= =?UTF-8?q?gistry=20ids?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while installing and validating every catalogued plugin on a 256x64 rig. Each fix is independent; grouped because they are all small. hello-world called font_manager.get_font(element_key) with no size. get_font takes (family, size_px), so every frame raised TypeError into a swallowing except and the plugin always fell back to its bundled BDF -- in the template plugin other people copy. Now uses resolve_font(), the accessor for a registered element, with the family and size pulled from one constant so the register and lookup calls cannot drift. Closes #448. christmas-countdown hardcoded lines = ["MERRY", "CHRISTMAS"] on the day itself while the countdown path a few lines below already shortens to "XMAS" when use_xmas says it will not fit. On 64-wide panels the word ran 3px off the edge, for the one day a year that branch is reached. Closes #450. static-image's harness fixture was addressed as "plugins/static-image/..." which only resolves when the plugins directory is called "plugins". On a rig using plugin-repos/ the image was missing, the render was blank, and all eight goldens reported drift -- which reads as a rendering regression, not a missing file. The path is now relative to the plugin, and _resolve_image_path() gained the plugin's own directory as a base so any bundled asset resolves. Closes #453. ledmatrix-leaderboard's scroll-budget warning measured the shortfall over image_width while triggering on image_width + display_width inflated by the safety buffer, so it under-reported the loss and, when only the buffer pushed it over, printed "roughly the last 0% of the list will not be reached". It now measures over the same distance it triggers on and stays quiet when the content actually fits. Closes #454. plugins.json listed leaderboard/music/stocks/weather under ids that disagree with the manifests in their plugin_path. The store therefore offered four already-installed plugins, and installing one deleted the working copy outside the rollback net. Closes #455. nfl-draft returned None from display() on its off-season path. The controller only skips a mode on a boolean False, so "off-season silence" became a black panel held for display_duration -- 60s by default, every rotation, for the ~11 months a year that are not draft season. Closes #456 (plugins side). cozette.bdf was removed from the core's bundled fonts but was still offered in seven plugins' font pickers, where choosing it can only fail. The same cleanup already landed for news and clock-simple. Closes #460. baseball-scoreboard's _make_render_live fixture builds its subject with object.__new__ and omits _font_cache, which SportsCore.__init__ creates and the at-bat card's font ladder needs, so four checks failed on an empty canvas. Closes #463. Verified on the rig: hello-world renders with no TypeError and its fill extent goes 58%->98% (the font-manager face is finally used), christmas-countdown passes all 8 sizes frozen to Dec 25 where 2 failed before, with no change on other dates. --- plugins.json | 8 ++--- plugins/afl-scoreboard/config_schema.json | 18 ++++------- .../test_pitcher_batter_last_play.py | 7 ++++ .../basketball-scoreboard/config_schema.json | 18 ++++------- plugins/calendar/config_schema.json | 6 ++-- plugins/christmas-countdown/manager.py | 8 +++-- .../football-scoreboard/config_schema.json | 18 ++++------- plugins/hello-world/manager.py | 32 +++++++++++++++---- plugins/ledmatrix-leaderboard/manager.py | 11 ++++++- plugins/ledmatrix-music/config_schema.json | 9 ++---- plugins/nfl-draft/manager.py | 24 ++++++++++---- plugins/nrl-scoreboard/config_schema.json | 18 ++++------- plugins/soccer-scoreboard/config_schema.json | 18 ++++------- plugins/static-image/manager.py | 11 ++++++- plugins/static-image/test/harness.json | 4 +-- 15 files changed, 116 insertions(+), 94 deletions(-) diff --git a/plugins.json b/plugins.json index 5d894283..941fb5d7 100644 --- a/plugins.json +++ b/plugins.json @@ -388,7 +388,7 @@ "latest_version": "1.2.3" }, { - "id": "leaderboard", + "id": "ledmatrix-leaderboard", "name": "Sports Leaderboard", "description": "Displays scrolling leaderboards and standings for multiple sports leagues including NFL, NBA, MLB, NCAA Football, NCAA Basketball, and more", "author": "ChuckBuilds", @@ -513,7 +513,7 @@ "latest_version": "1.2.2" }, { - "id": "music", + "id": "ledmatrix-music", "name": "Music Player - Now Playing", "description": "Real-time now playing display for Spotify and YouTube Music with album art, scrolling text, and progress bars", "author": "ChuckBuilds", @@ -812,7 +812,7 @@ "latest_version": "2.6.1" }, { - "id": "stocks", + "id": "ledmatrix-stocks", "name": "Stock & Crypto Ticker", "description": "Displays stock tickers with prices, changes, and optional charts for stocks and cryptocurrencies. Supports scroll and switch display modes.", "author": "LEDMatrix Team", @@ -909,7 +909,7 @@ "icon": "fas fa-fist-raised" }, { - "id": "weather", + "id": "ledmatrix-weather", "name": "Weather Display", "description": "Comprehensive weather display with current conditions, hourly forecast, daily forecast, almanac (sunrise/sunset, moon phase), animated precipitation radar on a real OpenStreetMap basemap (with nowcast forecast frames), weather alerts, UV index, wind direction, and weather icons. Powered by Open-Meteo (free, no API key) + RainViewer.", "author": "ChuckBuilds", diff --git a/plugins/afl-scoreboard/config_schema.json b/plugins/afl-scoreboard/config_schema.json index f96376b1..509a582e 100644 --- a/plugins/afl-scoreboard/config_schema.json +++ b/plugins/afl-scoreboard/config_schema.json @@ -776,8 +776,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf", "x-advanced": true @@ -832,8 +831,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf", "x-advanced": true @@ -888,8 +886,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf", "x-advanced": true @@ -944,8 +941,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf", "x-advanced": true @@ -1000,8 +996,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf", "x-advanced": true @@ -1095,8 +1090,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf", "x-advanced": true diff --git a/plugins/baseball-scoreboard/test_pitcher_batter_last_play.py b/plugins/baseball-scoreboard/test_pitcher_batter_last_play.py index 31b9dc1e..0502e066 100644 --- a/plugins/baseball-scoreboard/test_pitcher_batter_last_play.py +++ b/plugins/baseball-scoreboard/test_pitcher_batter_last_play.py @@ -242,6 +242,13 @@ def _make_render_live(width, height): live.config = {} live.show_pitcher_batter = True live.show_last_play = True + # SportsCore.__init__ creates these; object.__new__ skips it, and the + # at-bat card's font ladder reads _font_cache on its first draw. Without + # them every render here raised AttributeError, was swallowed by the + # plugin's except, and drew nothing -- so four checks below failed on an + # empty canvas rather than on anything the renderer did wrong. + live._font_cache = {} + live._bdf_native_size_cache = {} import logging live.logger = logging.getLogger("test_at_bat_info_render") return live diff --git a/plugins/basketball-scoreboard/config_schema.json b/plugins/basketball-scoreboard/config_schema.json index d9736217..12ff7271 100644 --- a/plugins/basketball-scoreboard/config_schema.json +++ b/plugins/basketball-scoreboard/config_schema.json @@ -2313,8 +2313,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -2369,8 +2368,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -2425,8 +2423,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -2481,8 +2478,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf" }, @@ -2537,8 +2533,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf" }, @@ -2632,8 +2627,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, diff --git a/plugins/calendar/config_schema.json b/plugins/calendar/config_schema.json index ddab8df0..e722c67a 100644 --- a/plugins/calendar/config_schema.json +++ b/plugins/calendar/config_schema.json @@ -105,8 +105,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf", "x-advanced": true @@ -141,8 +140,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf", "x-advanced": true diff --git a/plugins/christmas-countdown/manager.py b/plugins/christmas-countdown/manager.py index e2e7a650..3b757721 100644 --- a/plugins/christmas-countdown/manager.py +++ b/plugins/christmas-countdown/manager.py @@ -487,8 +487,12 @@ def display(self, force_clear: bool = False) -> None: # Stack text on right side # Split message into words/lines for stacking if self.is_christmas or self.days_until_christmas == 0: - # "MERRY CHRISTMAS" - split into two lines - lines = ["MERRY", "CHRISTMAS"] + # "MERRY CHRISTMAS" - split into two lines. Honour the same + # use_xmas fit-check the countdown lines use: "CHRISTMAS" does + # not fit the right half of a 64px panel, and hardcoding it + # here ran the word 3px off the edge for the whole of Dec 25 -- + # the one day this branch is ever reached. + lines = ["MERRY", "XMAS" if use_xmas else "CHRISTMAS"] else: # Countdown message - split intelligently if use_xmas: diff --git a/plugins/football-scoreboard/config_schema.json b/plugins/football-scoreboard/config_schema.json index 15d64dfb..8b2d33e2 100644 --- a/plugins/football-scoreboard/config_schema.json +++ b/plugins/football-scoreboard/config_schema.json @@ -1249,8 +1249,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -1305,8 +1304,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -1361,8 +1359,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -1417,8 +1414,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf" }, @@ -1473,8 +1469,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf" }, @@ -1568,8 +1563,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, diff --git a/plugins/hello-world/manager.py b/plugins/hello-world/manager.py index 2c945778..0642c34a 100644 --- a/plugins/hello-world/manager.py +++ b/plugins/hello-world/manager.py @@ -23,7 +23,14 @@ class HelloWorldPlugin(BasePlugin): Displays a customizable greeting message with the current time. Demonstrates basic plugin functionality. """ - + + # (family, size_px) for each registered element. Kept in one place so the + # register_manager_font() call and the resolve_font() lookup cannot drift: + # resolve_font takes the family and size the element was registered with, + # and a mismatch there is silent. + MESSAGE_FONT = ("press_start", 10) + TIME_FONT = ("press_start", 8) + def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager): """Initialize the Hello World plugin.""" super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager) @@ -58,8 +65,8 @@ def _register_fonts(self): font_manager.register_manager_font( manager_id=self.plugin_id, element_key=f"{self.plugin_id}.message", - family="press_start", - size_px=10, + family=self.MESSAGE_FONT[0], + size_px=self.MESSAGE_FONT[1], color=self.color ) @@ -67,8 +74,8 @@ def _register_fonts(self): font_manager.register_manager_font( manager_id=self.plugin_id, element_key=f"{self.plugin_id}.time", - family="press_start", - size_px=8, + family=self.TIME_FONT[0], + size_px=self.TIME_FONT[1], color=self.time_color ) @@ -145,8 +152,19 @@ def display(self, force_clear=False): try: if hasattr(self.plugin_manager, 'font_manager'): font_manager = self.plugin_manager.font_manager - message_font = font_manager.get_font(f"{self.plugin_id}.message") - time_font = font_manager.get_font(f"{self.plugin_id}.time") + # resolve_font() is the accessor for a registered element -- + # it honours user overrides and takes the same (family, + # size_px) the element was registered with. get_font() is + # the lower-level family lookup and needs a family name plus + # a size; calling it with an element key and no size raised + # TypeError on every frame, so this whole branch was dead + # and the bundled BDF was always used instead. + message_font = font_manager.resolve_font( + f"{self.plugin_id}.message", self.MESSAGE_FONT[0], + self.MESSAGE_FONT[1], plugin_id=self.plugin_id) + time_font = font_manager.resolve_font( + f"{self.plugin_id}.time", self.TIME_FONT[0], + self.TIME_FONT[1], plugin_id=self.plugin_id) except Exception as e: self.logger.warning(f"Error getting fonts from font manager: {e}") diff --git a/plugins/ledmatrix-leaderboard/manager.py b/plugins/ledmatrix-leaderboard/manager.py index 8a3c16b4..9ac87757 100644 --- a/plugins/ledmatrix-leaderboard/manager.py +++ b/plugins/ledmatrix-leaderboard/manager.py @@ -431,7 +431,16 @@ def _warn_if_content_will_be_truncated(self, image_width: int) -> None: if required <= budget: return + # Measure the shortfall over the same distance `required` used -- + # content plus the lead-in -- not over image_width alone. Mixing the + # two under-reported how much is lost, and when only the safety + # buffer pushed `required` over the budget the subtraction went + # negative and the clamp printed the self-contradictory "roughly the + # last 0% of the list will not be reached". + total_travel = image_width + self.display_width shown_px = budget * pixels_per_second + if shown_px >= total_travel: + return # fits without the buffer; not worth a warning limiter = ("the core's display.dynamic_duration.max_duration_seconds" if core_cap <= min(self.max_duration, self.dynamic_duration_cap) else "this plugin's global.dynamic_duration settings") @@ -441,7 +450,7 @@ def _warn_if_content_will_be_truncated(self, image_width: int) -> None: "of the list will not be reached before the display moves on. Raise that " "cap, increase the scroll speed, or lower top_teams.", image_width, required, pixels_per_second, budget, limiter, - max(0.0, 100.0 * (1.0 - shown_px / max(image_width, 1))), + 100.0 * (total_travel - shown_px) / max(image_width, 1), ) except Exception as e: # pragma: no cover - diagnostics only self.logger.debug("Could not evaluate content duration budget: %s", e) diff --git a/plugins/ledmatrix-music/config_schema.json b/plugins/ledmatrix-music/config_schema.json index b2fb081b..35633805 100644 --- a/plugins/ledmatrix-music/config_schema.json +++ b/plugins/ledmatrix-music/config_schema.json @@ -225,8 +225,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -267,8 +266,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "5x7.bdf" }, @@ -309,8 +307,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "5x7.bdf" }, diff --git a/plugins/nfl-draft/manager.py b/plugins/nfl-draft/manager.py index 2b2cb9ad..1fa2cca2 100644 --- a/plugins/nfl-draft/manager.py +++ b/plugins/nfl-draft/manager.py @@ -1195,12 +1195,19 @@ def update(self) -> None: except Exception as e: self.logger.error(f"Error updating draft data: {e}", exc_info=True) - def display(self, force_clear: bool = False) -> None: + def display(self, force_clear: bool = False) -> bool: """ Render the draft picks to the LED matrix. Uses ScrollHelper to create smooth horizontal scrolling. + Returns False when there is nothing to show, so the display controller + rotates on instead of holding the frame. The controller only skips on a + boolean False (display_controller.py checks isinstance(result, bool)), + so returning None left the panel black for the whole display_duration -- + 60s by default, every rotation, for the ~11 months a year that are not + draft season. + Args: force_clear: If True, clear display before rendering """ @@ -1211,17 +1218,17 @@ def display(self, force_clear: bool = False) -> None: picks_loaded = bool(self.draft_picks) status = self.draft_status - # Off-season / expired post-draft window: render nothing + # Off-season / expired post-draft window: render nothing. No need to + # paint the blank frame first -- returning False means the controller + # never shows it. if status == "complete" and not self._is_post_draft_window(): - self._display_blank() - return + return False if status not in ("live", "complete", "simulate") and self._is_off_season(): - self._display_blank() - return + return False if not picks_loaded: self._display_no_data() - return + return True try: # Vegas clears scroll_helper.cached_image whenever this plugin reports @@ -1243,10 +1250,13 @@ def display(self, force_clear: bool = False) -> None: # Set image to display manager self.display_manager.image = visible_image self.display_manager.update_display() + return True + return False except Exception as e: self.logger.error(f"Error displaying draft: {e}") self._display_error() + return True def _display_blank(self) -> None: """Render a solid black frame (off-season silence — no text, no errors).""" diff --git a/plugins/nrl-scoreboard/config_schema.json b/plugins/nrl-scoreboard/config_schema.json index 7d7193f6..e6c1f9c0 100644 --- a/plugins/nrl-scoreboard/config_schema.json +++ b/plugins/nrl-scoreboard/config_schema.json @@ -772,8 +772,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -826,8 +825,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -880,8 +878,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -934,8 +931,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf" }, @@ -988,8 +984,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf" }, @@ -1081,8 +1076,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, diff --git a/plugins/soccer-scoreboard/config_schema.json b/plugins/soccer-scoreboard/config_schema.json index b3990cb2..4dc31208 100644 --- a/plugins/soccer-scoreboard/config_schema.json +++ b/plugins/soccer-scoreboard/config_schema.json @@ -5722,8 +5722,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -5776,8 +5775,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -5830,8 +5828,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, @@ -5884,8 +5881,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf" }, @@ -5938,8 +5934,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "4x6-font.ttf" }, @@ -6031,8 +6026,7 @@ "4x6-font.ttf", "5by7.regular.ttf", "5x7.bdf", - "4x6.bdf", - "cozette.bdf" + "4x6.bdf" ], "default": "PressStart2P-Regular.ttf" }, diff --git a/plugins/static-image/manager.py b/plugins/static-image/manager.py index 897de926..4ba0509b 100644 --- a/plugins/static-image/manager.py +++ b/plugins/static-image/manager.py @@ -372,7 +372,16 @@ def _resolve_image_path(self, image_path: str) -> str: project_path = project_root / image_path if project_path.exists(): return str(project_path) - + + # Try relative to this plugin's own directory, so a bundled asset + # resolves whatever the plugins directory is called. Installs are not + # all under "plugins/" -- check_plugin.py takes --plugin-dir and a rig + # may use "plugin-repos/", in which case the project-root attempt above + # silently misses and the image reads as absent. + plugin_path = Path(__file__).resolve().parent / image_path + if plugin_path.exists(): + return str(plugin_path) + # Try as-is in case it's already resolved return image_path diff --git a/plugins/static-image/test/harness.json b/plugins/static-image/test/harness.json index c2be0ca7..7d819dfe 100644 --- a/plugins/static-image/test/harness.json +++ b/plugins/static-image/test/harness.json @@ -1,8 +1,8 @@ { - "_comment": "Deterministic harness config: renders a bundled test-pattern PNG (paths resolve relative to the core working directory, then the project root; the harness runs from a core checkout with --plugin-dir pointing here, so an absolute-safe relative path into this plugin's test fixtures is used). fit_to_display scales the 64x32 pattern to each panel deterministically.", + "_comment": "Deterministic harness config: renders a bundled test-pattern PNG (the path is relative to this plugin's own directory, which is the only base that holds however the plugins directory is named -- 'plugins/...' assumed one particular --plugin-dir and silently missed on a rig using plugin-repos/, leaving the render blank and every golden 'drifting'). fit_to_display scales the 64x32 pattern to each panel deterministically.", "config": { "enabled": true, - "images": ["plugins/static-image/test/fixtures/test-pattern.png"], + "images": ["test/fixtures/test-pattern.png"], "fit_to_display": true, "preserve_aspect_ratio": false } From 333023c2a37329451e07af7febfd07b57b14586a Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 20:32:39 -0400 Subject: [PATCH 02/13] fix(on-air): hold the blank frame for 1s, not the configured display_duration display() deliberately returns True when the light is off, because returning False during an on-demand request drops the controller into its "Initializing" state before the stop is processed. The comment justifying that said the black frame is cycled past "nearly instantly" because get_display_duration() is 1s. It never was: the method returns the configured display_duration, which defaults to 5 and allows up to 60, and has done since v1.0.0. So a rig with on-air enabled but not broadcasting -- the normal state most of the time -- went black for 5s on every rotation. get_display_duration() now reports IDLE_DURATION while the light is off, which makes the existing comment true, and the comment is corrected to say what it actually does. The configured duration still applies when there is a tally to show. Closes #457. --- plugins/on-air/manager.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/on-air/manager.py b/plugins/on-air/manager.py index 6eaade56..74974658 100644 --- a/plugins/on-air/manager.py +++ b/plugins/on-air/manager.py @@ -49,6 +49,11 @@ def _rgb(value, default) -> Tuple[int, int, int]: class OnAirPlugin(BasePlugin): """ON AIR sign driven by MQTT with HA auto-discovery.""" + # Seconds to hold the blank frame when the light is off. Short enough + # that the rotation steps past it, non-zero so the controller still + # gets a frame. + IDLE_DURATION = 1.0 + def __init__(self, plugin_id: str, config: Dict[str, Any], display_manager, cache_manager, plugin_manager): super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager) @@ -204,8 +209,10 @@ def display(self, force_clear: bool = False) -> bool: # When off, render a plain black frame rather than returning False. # Returning False while still in on-demand mode causes the display # controller to fall to its "Initializing" state before the stop - # request is processed. The 1s get_display_duration() cycles past - # this black frame nearly instantly in normal rotation. + # request is processed. get_display_duration() reports IDLE_DURATION + # while the light is off so the rotation cycles past this black frame + # quickly -- the configured display_duration (5s by default, up to 60) + # applies only when there is a tally to show. fill = bg_color if active else (0, 0, 0) canvas = Image.new('RGB', (dw, dh), fill) draw = ImageDraw.Draw(canvas) @@ -263,6 +270,16 @@ def _draw_centered_text(self, draw: ImageDraw.ImageDraw, centered=True) def get_display_duration(self) -> float: + # A dark tally light has nothing to show, so hold it only long enough + # for the rotation to step past. display() deliberately returns True + # while off (see the note there), which means the controller does not + # skip the mode -- without this the panel sat black for the configured + # display_duration, 5s by default and up to 60s, on every rotation of + # an enabled-but-not-broadcasting plugin. + with self.state_lock: + active = self.on_air + if not active: + return self.IDLE_DURATION return float(self.config.get('display_duration', 5)) def on_enable(self) -> None: From ee675ff4a71b4f8016deb88642981d2566129fde Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 20:53:36 -0400 Subject: [PATCH 03/13] fix(layout): size 7-segment digits around their spacing, fit geochron's 12h clock 7-segment-clock's _calculate_scale_factor solved the scale against the digits and separators only, then the renderer added spacing_gaps * digit_spacing * scale on top. The clock was therefore sized to the panel and *then* made wider, so it ran off the right edge for any digit_spacing >= 3 -- a range config_schema advertises all the way to 10. The gaps scale with the digits, so they belong in the width the scale is solved against. Measured across the full schema range (failures out of 8 sizes): spacing 0 2 3 5 8 10 before 0 0 2 3 6 6 after 0 0 0 0 0 0 including the worst case, 12-hour format with a leading zero at spacing 10. This changes the rendering at the default spacing of 2 as well -- correctly, the digits are now sized to leave room for the gaps -- so two goldens are refreshed. Only 64x64 and 128x96 moved; the other six are byte-identical, which is the expected shape: those are the sizes where the width constraint binds. Both refreshed renders sit well inside the panel (64x64 spans x=12..56, 128x96 x=21..118). geochron's sidebar clock ran off a 128x32 panel with clock_format "12h": the meridiem adds two characters the 24-hour string does not carry, and the sidebar rows were never measured. build_readout now takes a measure callable -- the manager passes draw.textlength with the real font -- and drops the seconds when the full string will not fit, which is the same trade the featured-city row already makes a few lines below. 1 failure -> 0, with the default 24h path unchanged and its goldens still passing. Closes #449, closes #458. --- plugins/7-segment-clock/manager.py | 10 +++++++ .../test/golden/128x96/7-segment-clock.png | Bin 3381 -> 2743 bytes .../test/golden/64x64/7-segment-clock.png | Bin 299 -> 985 bytes plugins/geochron/geochron_renderer.py | 25 +++++++++++++++--- plugins/geochron/manager.py | 1 + 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/plugins/7-segment-clock/manager.py b/plugins/7-segment-clock/manager.py index 83a77113..7dcc703f 100644 --- a/plugins/7-segment-clock/manager.py +++ b/plugins/7-segment-clock/manager.py @@ -290,11 +290,21 @@ def _calculate_scale_factor( """ # Calculate base width needed for the time string base_width = 0 + element_count = 0 for item in digits: if item == ":": base_width += self.separator_width + element_count += 1 elif item is not None: base_width += self.digit_width + element_count += 1 + + # The gaps scale with the digits, so they belong in the width the scale + # is solved against. Sizing the digits to the panel and then adding the + # spacing on top is what pushed the clock off the right edge for any + # digit_spacing >= 3, a range the schema advertises up to 10. + if element_count > 1: + base_width += (element_count - 1) * self.digit_spacing base_height = self.digit_height diff --git a/plugins/7-segment-clock/test/golden/128x96/7-segment-clock.png b/plugins/7-segment-clock/test/golden/128x96/7-segment-clock.png index 93e529ffcc2b835d845001b340a76be36d5a9977..6c6ab7ebc0d04aa85ddc75c36694748a03c2dea0 100644 GIT binary patch delta 2736 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 w>=(^b diff --git a/plugins/7-segment-clock/test/golden/64x64/7-segment-clock.png b/plugins/7-segment-clock/test/golden/64x64/7-segment-clock.png index 41d4adb8c7d1e90ec239c3a730edf80c3f159d06..56d55635e32e9cc9447f159ae1508d2847ae400d 100644 GIT binary patch delta 964 zcmV;#13Uby0@(+UBYy*+NklifmAYeNta3$15?#Z6{1Yt zP*=VH8#@y_6G%wi*pZkKVuXdYJ5{Hs0;v*JYCDeKU|yA%ls+f$#2fsZTIc-tU2=9V z2VgK53-Bn+Qu&)O4Ey~)01!e_)7417 z-w(rZVNWUT^?IJ?O{dcWnjg1D(|d%_&d$#5?QN}ALkI~Wf*{!2+w*;Ye}6v=L&tIM z?(VLyuWj3AjDKZC)cDcS(ZRt1rF4CL{r>*`@$vED;lXuXjBx=?T^z>&fUB!3Ddl`V z2Y~r}E~Oj{1}RIGW}lJ0Iv5PTs9DkpKuQS!Z*OmL9IITkEGv%V$z(zZSzB9Ew?_yO zMbQc!Aw;QEQV!X+J)h4flgSq~wF3AsEtS+xP#}a#rGFCV+_EhF=NmhjS1j$+U+)Dtle(^yCoo308tdhalH5&$MNdzW!p9(#J25k)Xbu_iPb5wwY7y1 zQYVs>5+PKrR#PqtA%qa>_jo+Myu2Kb$BU~N zv9SRFuIu`~FNBbP!WipzyGy5cyIsau%3cWJ`+xqTn9HjPg-@X%2+HMhHZ?KEv)N2J zl*D!2*=&|A3WN~laybZs0-B0~DWsDU$rRE_4;@WSZCdYb;P$|s?nb& zM+&7X$B+4mPedu@QX7S;GzlS&TaZNt2p m27|$1Fc=I5gTY|Gec1k4R`rgR{%(Pa+ zux$VU0002+R#L~FoUdob$y=bPG&iHv3TZ5$9cnGk<|*1%t}!^YSNMI3y){ITHw-qvGOQ&gLRQl$g!q z8~FDqu|6~~8yc8Rf0SqjBt1%q`O5Y3C(xAsJzvF~V+B4p0$N^=zYhQa0002Md-(!J XvZY5JECXHu015yANkvXXu0mjf?PGoI diff --git a/plugins/geochron/geochron_renderer.py b/plugins/geochron/geochron_renderer.py index 83fd8914..768b4ce8 100644 --- a/plugins/geochron/geochron_renderer.py +++ b/plugins/geochron/geochron_renderer.py @@ -196,7 +196,7 @@ def format_clock(dt, fmt="24h", show_seconds=True): def build_readout(layout, dt_utc, local_dt, subsolar_lat, subsolar_lon, featured_city, - clock_format="24h", show_seconds=True, row_h=ROW_H): + clock_format="24h", show_seconds=True, row_h=ROW_H, measure=None): """Build the text rows for the digital clock / info readout. Returns a dict: @@ -207,20 +207,39 @@ def build_readout(layout, dt_utc, local_dt, subsolar_lat, subsolar_lon, featured featured_city, if given, is a dict with "name" and "local_dt" (already converted to that city's timezone by the caller). + + measure, if given, is a callable returning the pixel width of a string in + the font the caller will draw with. It is used to decide whether the clock + still fits the sidebar; without it a 4px-per-character estimate is used, + matching the pixel font this layout is designed around. """ L = layout + if measure is None: + def measure(text): + return len(text) * 4 if L["mode"] == "wide_sidebar": # The sidebar is narrow (~32px), so labels like "UTC"/"LCL" and full # ISO dates don't fit a 4px-wide pixel font. Row order conveys # meaning instead: UTC time, date, local time (if configured), # subsolar coordinates, featured city. + # 12-hour format appends AM/PM, two characters the 24-hour string does + # not carry, and with seconds that pushed the clock past the sidebar and + # off the panel. Drop the seconds rather than the meridiem when the full + # string will not fit -- the same trade the featured-city row below + # already makes for the same reason. + clock_seconds = show_seconds + if clock_seconds: + usable = L["sidebar_w"] - 2 + if measure(format_clock(dt_utc, clock_format, True)) > usable: + clock_seconds = False + rows = [ - (format_clock(dt_utc, clock_format, show_seconds), "primary"), + (format_clock(dt_utc, clock_format, clock_seconds), "primary"), (dt_utc.strftime("%m-%d"), "secondary"), ] if local_dt is not None: - rows.append((format_clock(local_dt, clock_format, show_seconds), "primary")) + rows.append((format_clock(local_dt, clock_format, clock_seconds), "primary")) rows.append((f"{subsolar_lat:+.0f},{subsolar_lon:+.0f}", "secondary")) if featured_city is not None and L["sidebar_w"] >= 50: name = featured_city["name"][:6] diff --git a/plugins/geochron/manager.py b/plugins/geochron/manager.py index 5bf4e98f..4aa6b8c9 100644 --- a/plugins/geochron/manager.py +++ b/plugins/geochron/manager.py @@ -262,6 +262,7 @@ def _draw_readout(self, draw, layout): readout = gr.build_readout( layout, now_utc, local_dt, self._subsolar_lat, self._subsolar_lon, featured_city, self.clock_format, self.show_seconds, + measure=lambda text: draw.textlength(text, font=self.font), ) primary = self.colors["text_primary_color"] From 11aac51206e6d1a7646b4a501b0716db7995e7cf Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 21:06:32 -0400 Subject: [PATCH 04/13] fix(calendar): load .bdf through FreeType, not the legacy PIL bitmap reader calendar routed .bdf files to ImageFont.load(), which is Pillow's legacy PIL-bitmap reader: it expects a .pil metrics file plus a .pbm/.gif/.png glyph image and has never read BDF. Every .bdf in the font picker failed with "cannot find glyph data file" and fell back to the default font, so three of the six offered faces rendered identically and choosing one appeared to do nothing. FreeType reads BDF, so one loader covers .ttf and .bdf alike -- with a retry at the size the file declares, because a bitmap face exists at exactly the one pixel size it was drawn at and FreeType rejects any other with "invalid pixel size". That is the same shape clock-simple, news, countdown, tide-display and jellyfin-now-playing already use; this brings calendar in line. Verified on the rig by hashing the 128x64 render for each face, twice: before after PressStart2P-Regular.ttf 5c68bd5b distinct 4x6-font.ttf 8e0533b7 distinct 5by7.regular.ttf c0ae264c distinct 5x7.bdf \ both the default b8789a63 distinct 4x6.bdf / font fallback 4619e214 distinct Five faces, five distinct renders, identical across both runs, and no load warnings left. Previously the two .bdf faces were byte-identical to each other. Refs #459. The other half of that issue -- baseball/football/hockey/lacrosse expecting a pre-converted .pil that ships nowhere -- is left for now: the test rig's core is on a branch that predates src/common/sports_shared, so those four plugins cannot currently be loaded there to verify a change. --- plugins/calendar/manager.py | 54 ++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/plugins/calendar/manager.py b/plugins/calendar/manager.py index 8ce3e77a..5d17dbe4 100644 --- a/plugins/calendar/manager.py +++ b/plugins/calendar/manager.py @@ -231,30 +231,48 @@ def _load_font_by_type(self, font_path: str, font_size: int, self.logger.info(f"Using default font for {font_name}") return ImageFont.load_default() - # Get file extension to determine loader type - _, ext = os.path.splitext(font_path.lower()) - - # Try BDF bitmap font loader - if ext == '.bdf': - try: - font = ImageFont.load(font_path) - self.logger.debug(f"Loaded BDF font for {font_name}: {font_path}") - return font - except Exception as e: - self.logger.warning(f"Failed to load BDF font for {font_name}: {e}") - self.logger.info(f"Using default font for {font_name}") - return ImageFont.load_default() - - # Try TrueType font loader (for .ttf, .otf, and other formats) + # FreeType handles .ttf and (at its native size) .bdf faces, so one + # loader covers both. ImageFont.load() was used for .bdf, but that is + # Pillow's legacy PIL-bitmap reader: it wants a .pil metrics file plus a + # .pbm/.gif/.png glyph image and has never read BDF, so it failed with + # "cannot find glyph data file" and every .bdf in the picker silently + # fell back to the default font. try: font = ImageFont.truetype(font_path, font_size) - self.logger.debug(f"Loaded TrueType font for {font_name}: {font_path}") + self.logger.debug(f"Loaded font for {font_name}: {font_path}") return font - except Exception as e: - self.logger.warning(f"Failed to load TrueType font for {font_name}: {e}") + except OSError as e: + # A .bdf is a bitmap face and exists at exactly the one pixel size + # it was drawn at; FreeType rejects any other with "invalid pixel + # size". Retry at the size the file declares. + native = self._bdf_pixel_size(font_path) + if native is not None and native != font_size: + try: + font = ImageFont.truetype(font_path, native) + self.logger.debug( + "Loaded bitmap font for %s at its native size %d (requested %d)", + font_name, native, font_size) + return font + except OSError: + pass + self.logger.warning(f"Failed to load font for {font_name}: {e}") self.logger.info(f"Using default font for {font_name}") return ImageFont.load_default() + @staticmethod + def _bdf_pixel_size(path: str): + """The pixel size a .bdf font declares, or None if it does not.""" + try: + with open(path, "r", encoding="latin-1") as handle: + for line in handle: + if line.startswith("PIXEL_SIZE"): + return int(line.split()[1]) + if line.startswith("CHARS"): + break # past the header; no point reading the glyphs + except (OSError, ValueError, IndexError): + return None + return None + def _get_display_dimensions(self, width: Optional[int] = None, height: Optional[int] = None) -> tuple: """ Get display dimensions dynamically. From d75c12a076baa78f1990690f19caafe2905187d1 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 21:23:03 -0400 Subject: [PATCH 05/13] fix(olympics): roll over to the next Games, and fit the countdown to the panel The Games were a single CURRENT_OLYMPICS dict pinned to Milano Cortina 2026. Once those closed nothing advanced, so the countdown went negative and the plugin rendered "-210 DAYS UNTIL WINTER OLYMPICS" -- counting further down every day, and naming the wrong season. OLYMPIC_GAMES is now an ordered table and current_games() returns the Games in progress or the next still to come. Milano Cortina 2026 and Los Angeles 2028 are listed; when the table is exhausted the fetches and the data package report no Games and log which file to add the next one to, so the failure mode is a skipped screen rather than a wrong one. _calculate_days_until also clamps at zero, so a stale table cannot put a negative on the panel whatever the caller does. The countdown card assumed a 256px panel: the logo took the left half and the text was drawn into the right half without ever being measured, so "DAYS UNTIL" and "OLYMPICS" ran off the edge on six of eight sizes. Lines are now measured in the font that will draw them and stepped down to shorter wording, and where even that cannot fit -- at 64px wide the right half is about three characters -- the logo is dropped and the text takes the full width. A countdown nobody can read is worth less than the rings. failures across the 8 sizes: 6 -> 0 Verified on the rig: 256x128 now reads "679 DAYS UNTIL SUMMER OLYMPICS", which matches (date(2028,7,14) - today).days exactly, with the rings intact. Closes #451, closes #452. --- plugins/olympics/data/olympics_api.py | 88 +++++++++++++++--- .../olympics/renderers/countdown_renderer.py | 93 ++++++++++++++++--- 2 files changed, 153 insertions(+), 28 deletions(-) diff --git a/plugins/olympics/data/olympics_api.py b/plugins/olympics/data/olympics_api.py index 4cc3833d..292dfadf 100644 --- a/plugins/olympics/data/olympics_api.py +++ b/plugins/olympics/data/olympics_api.py @@ -34,14 +34,47 @@ def _utcnow() -> datetime: logger = logging.getLogger(__name__) -# Current Olympics configuration -CURRENT_OLYMPICS = { - 'name': 'Milano Cortina 2026', - 'type': 'winter', - 'opening': datetime(2026, 2, 6), - 'closing': datetime(2026, 2, 22), - 'base_url': 'https://www.olympics.com/en/milano-cortina-2026', -} +# Known Games, oldest first. current_games() walks this and returns the one in +# progress, or the next one still to come. +# +# This used to be a single CURRENT_OLYMPICS dict pinned to Milano Cortina 2026. +# Once those Games closed the countdown simply went negative -- the plugin +# rendered "-210 DAYS UNTIL WINTER OLYMPICS" and would have kept counting +# downwards for ever. +# +# Add each Games here as its dates are confirmed. When the table runs out the +# plugin reports no Games rather than inventing a date, so the failure mode is a +# skipped screen and a log line, not a wrong one. +OLYMPIC_GAMES = [ + { + 'name': 'Milano Cortina 2026', + 'type': 'winter', + 'opening': datetime(2026, 2, 6), + 'closing': datetime(2026, 2, 22), + 'base_url': 'https://www.olympics.com/en/milano-cortina-2026', + }, + { + 'name': 'Los Angeles 2028', + 'type': 'summer', + 'opening': datetime(2028, 7, 14), + 'closing': datetime(2028, 7, 30), + 'base_url': 'https://www.olympics.com/en/los-angeles-2028', + }, +] + + +def current_games(now: Optional[datetime] = None) -> Optional[Dict[str, Any]]: + """The Games in progress or the next still to come; None if none are known. + + Returning None is deliberate: it lets the caller show nothing, which is + honest, instead of counting down to a date that has passed. + """ + if now is None: + now = _utcnow() + for games in OLYMPIC_GAMES: + if now <= games['closing']: + return games + return None # Cache configuration (in seconds) CACHE_DURATION = { @@ -286,7 +319,11 @@ def _scrape_medals(self) -> List[MedalCount]: the key 'result_medals_data'. This is much more reliable than trying to parse the client-side rendered HTML. """ - url = f"{CURRENT_OLYMPICS['base_url']}/medals" + games = current_games() + if games is None: + logger.info("No upcoming Olympics in OLYMPIC_GAMES; nothing to fetch") + return [] + url = f"{games['base_url']}/medals" page_content = self._fetch_page(url) if not page_content: @@ -594,7 +631,11 @@ def _scrape_schedule(self) -> List[OlympicEvent]: the key 'result_schedule_data'. Each day has its own key like 'initialSchedule_2026-02-08'. """ - url = f"{CURRENT_OLYMPICS['base_url']}/schedule" + games = current_games() + if games is None: + logger.info("No upcoming Olympics in OLYMPIC_GAMES; nothing to fetch") + return [] + url = f"{games['base_url']}/schedule" page_content = self._fetch_page(url) if not page_content: @@ -878,7 +919,11 @@ def _scrape_results(self) -> List[EventResult]: The medals page contains individual medal winners which we can group by event to create result entries. """ - url = f"{CURRENT_OLYMPICS['base_url']}/medals" + games = current_games() + if games is None: + logger.info("No upcoming Olympics in OLYMPIC_GAMES; nothing to fetch") + return [] + url = f"{games['base_url']}/medals" page_content = self._fetch_page(url) if not page_content: @@ -999,8 +1044,21 @@ def get_olympics_data(self) -> OlympicsData: OlympicsData with all available information """ now = _utcnow() - opening = CURRENT_OLYMPICS['opening'] - closing = CURRENT_OLYMPICS['closing'] + games = current_games(now) + if games is None: + # Past every Games we know about. Report an empty, inactive package + # so the plugin renders nothing rather than a negative countdown. + logger.warning( + "No Olympics on or after %s in OLYMPIC_GAMES -- add the next " + "Games to data/olympics_api.py", now.date()) + return OlympicsData( + is_active=False, games_name="", games_type="", + opening_date=None, closing_date=None, medal_counts=[], + upcoming_events=[], live_events=[], recent_results=[], + last_updated=now, + ) + opening = games['opening'] + closing = games['closing'] is_active = opening <= now <= closing @@ -1015,8 +1073,8 @@ def get_olympics_data(self) -> OlympicsData: return OlympicsData( is_active=is_active, - games_name=CURRENT_OLYMPICS['name'], - games_type=CURRENT_OLYMPICS['type'], + games_name=games['name'], + games_type=games['type'], opening_date=opening, closing_date=closing, medal_counts=medals, diff --git a/plugins/olympics/renderers/countdown_renderer.py b/plugins/olympics/renderers/countdown_renderer.py index 281f7f48..61121bda 100644 --- a/plugins/olympics/renderers/countdown_renderer.py +++ b/plugins/olympics/renderers/countdown_renderer.py @@ -146,13 +146,62 @@ def _get_logo_image(self, width: int, height: int) -> Image.Image: return self._draw_olympics_rings(width, height) def _calculate_days_until(self, target_date: datetime) -> int: - """Calculate days until target date.""" + """Whole days from today to target_date, never negative. + + A past target means the caller is working from a stale Games table. + Clamping here keeps "-210 DAYS UNTIL WINTER OLYMPICS" off the panel + whatever the data says; data/olympics_api.current_games() is what + actually picks the right Games. + """ today = date.today() if isinstance(target_date, datetime): target = target_date.date() else: target = target_date - return (target - today).days + return max(0, (target - today).days) + + # Progressively shorter stand-ins, tried in order when a line will not fit. + _SHORTER = { + "DAYS UNTIL": ["DAYS TO", "DAYS"], + "OLYMPICS": ["OLYMPIC", "GAMES"], + "CLOSING": ["CLOSE"], + "OPENING": ["OPEN"], + "WINTER": ["WNTR"], + "SUMMER": ["SMMR"], + } + + def _measure(self, text: str) -> int: + """Width of text in the font draw_text will use, 0 if unmeasurable.""" + try: + dm = self.display_manager + if hasattr(dm, "get_text_width"): + return int(dm.get_text_width(text)) + if hasattr(dm, "draw") and hasattr(dm.draw, "textlength"): + return int(dm.draw.textlength(text, font=getattr(dm, "regular_font", None))) + except Exception: + pass + return 0 + + def _fit_lines(self, lines, available: int): + """Swap in shorter wording for any line too wide for `available`. + + Unmeasurable fonts return 0 from _measure and leave the text alone, so + this can only shorten when it knows it has to. + """ + if available <= 0: + return lines + out = [] + for line in lines: + if self._measure(line) <= available: + out.append(line) + continue + replaced = line + for candidate in self._SHORTER.get(line, []): + replaced = candidate + if self._measure(candidate) <= available: + break + out.append(replaced) + return out def render_countdown_card(self, target_date: datetime, _games_name: str, games_type: str, is_closing: bool = False, @@ -257,16 +306,34 @@ def display_countdown(self, target_date: datetime, _games_name: str, left_half_width = width // 2 right_half_width = width - left_half_width - # Draw logo on left - logo_margin = 2 - logo_width = left_half_width - (2 * logo_margin) - logo_height = height - (2 * logo_margin) - logo = self._get_logo_image(logo_width, logo_height) - - if logo.mode == 'RGBA': - self.display_manager.image.paste(logo, (logo_margin, logo_margin), logo) - else: - self.display_manager.image.paste(logo, (logo_margin, logo_margin)) + # The wording is sized for a 256px panel; on anything narrower + # "DAYS UNTIL" and "OLYMPICS" ran off the right edge, clipped mid-word. + # Step down to shorter equivalents until every line fits the half we + # actually have. Measured in the font that will draw them, so this + # follows a font change rather than assuming a character width. + lines = self._fit_lines(lines, right_half_width - 4) + + # On a narrow panel the logo leaves too little for any wording -- at + # 64px wide the right half is 32px, roughly three characters. Give the + # text the whole panel instead of clipping it; a countdown nobody can + # read is worth less than the rings. + show_logo = all( + self._measure(line) <= right_half_width - 4 for line in lines + ) + if not show_logo: + lines = self._fit_lines(lines, width - 4) + + # Draw logo on left (skipped when the text needs the full width) + if show_logo: + logo_margin = 2 + logo_width = left_half_width - (2 * logo_margin) + logo_height = height - (2 * logo_margin) + logo = self._get_logo_image(logo_width, logo_height) + + if logo.mode == 'RGBA': + self.display_manager.image.paste(logo, (logo_margin, logo_margin), logo) + else: + self.display_manager.image.paste(logo, (logo_margin, logo_margin)) # Draw text on right, centered line_height = height // len(lines) @@ -274,7 +341,7 @@ def display_countdown(self, target_date: datetime, _games_name: str, for i, line in enumerate(lines): y = start_y + (i * line_height) - center_x = left_half_width + (right_half_width // 2) + center_x = (left_half_width + (right_half_width // 2)) if show_logo else (width // 2) # Use display_manager's draw_text for proper font handling self.display_manager.draw_text( From 20a6fdcfc5e4f87512da1fa471ae6644d78ee37c Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 21:51:02 -0400 Subject: [PATCH 06/13] fix(flights): bring two stale test doubles back in line with the code test_vegas_map_parity.py failed 4 of 21 on main, both causes stand-ins that had drifted from what they stand in for. fake_fetch took (x, y, zoom) while _fetch_tile gained an allow_network keyword that manager.py passes by name, so every path reaching it raised TypeError. The keyword is now accepted and recorded alongside the coordinates, so a test can assert that a cache-only render never asks to open a socket -- which is the whole point of that parameter and nothing checks it today. Underneath that, FakeLogger implemented debug/info/warning only, and the plugin guards expensive debug formatting behind isEnabledFor. Added, reporting False, along with error/exception. 4 failed, 17 passed -> 21 passed Worth recording that the map-cache code itself was never at fault: with the doubles corrected every assertion in TestMapBackgroundCacheIsSizeAware passes as written. Closes #465. --- .../test_vegas_map_parity.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/ledmatrix-flights/test_vegas_map_parity.py b/plugins/ledmatrix-flights/test_vegas_map_parity.py index 08f9f7c2..b15060a7 100644 --- a/plugins/ledmatrix-flights/test_vegas_map_parity.py +++ b/plugins/ledmatrix-flights/test_vegas_map_parity.py @@ -46,6 +46,19 @@ def info(self, *a, **k): def warning(self, *a, **k): pass + def error(self, *a, **k): + pass + + def exception(self, *a, **k): + pass + + # The plugin guards expensive debug formatting behind isEnabledFor, which + # every real logger has. Reporting False keeps that formatting out of the + # test, and stops the guard raising AttributeError on a stand-in that only + # implemented the three methods it happened to need at the time. + def isEnabledFor(self, level): + return False + class FakeDisplayManager: def __init__(self, width=W, height=H): @@ -261,8 +274,12 @@ def enable_map_background(plugin, tmp_path): calls = [] - def fake_fetch(x, y, zoom): - calls.append((x, y, zoom)) + # allow_network mirrors _fetch_tile's real signature -- manager.py calls it + # by keyword, and a three-argument stand-in raised TypeError on every path + # that reached it. Recorded alongside the coordinates so a test can assert + # the cache-only render never asks to open a socket. + def fake_fetch(x, y, zoom, allow_network=True): + calls.append((x, y, zoom, allow_network)) # A gradient rather than a flat fill, so a wrongly-scaled crop shows up. tile = Image.new('RGB', (plugin.tile_size, plugin.tile_size)) tile.putdata([ From c65ad1581263bee0f254ab65c180916f24bea2d5 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 21:53:00 -0400 Subject: [PATCH 07/13] fix(scripts): stop reporting pytest modules as passing without running them run_plugin_tests.py runs each discovered test_*.py as a subprocess and reads the exit code. That is right for the standalone scripts most plugin tests are, but 11 files at plugin root are pytest modules -- `def test_*` / `class Test*` with no `__main__` guard. Running one of those as a script imports it, defines the tests, executes none, and exits 0, which this runner reported as a pass. Two of the eleven were failing the whole time: four map-cache tests in ledmatrix-flights and one in news. Files are now classified and pytest modules go to pytest, with its outcome mapped onto the same PASS/SKIP/FAIL codes so the summary and the CI gate are unchanged in shape. pytest's "no tests collected" (exit 5) is reported as a failure rather than a pass: for a file we classified as a pytest module it means the classification was wrong, which is exactly the condition this bug hid behind. Same two plugins, before and after: old: [pass] test_vegas_map_parity.py [pass] test_news_ticker.py 13 passed, 0 skipped, 0 failed new: [pass] test_vegas_map_parity.py [FAIL] test_news_ticker.py -- FAILED test_parked_page_rolls_over_when_the_controller_never_resets 19 passed, 0 skipped, 1 failed (The flights file passes on this branch because its stale doubles are fixed in the preceding commit; the news failure belongs to PR #462, which changes news/manager.py without updating its FakeDisplayManager.) Closes #464. --- scripts/run_plugin_tests.py | 61 ++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/scripts/run_plugin_tests.py b/scripts/run_plugin_tests.py index f50baa41..e5042e32 100644 --- a/scripts/run_plugin_tests.py +++ b/scripts/run_plugin_tests.py @@ -43,6 +43,7 @@ import argparse import os +import re import subprocess import sys from pathlib import Path @@ -53,6 +54,61 @@ PASS, FAIL, SKIP = 0, 1, 2 +def is_pytest_module(script: Path) -> bool: + """True when a file is a pytest module rather than a standalone script. + + These declare `def test_*` / `class Test*` and have no `__main__` guard, so + running them the way run_one() does merely imports them: the test functions + are defined, none are called, and the process exits 0. That is reported as a + pass, which is how four real failures in ledmatrix-flights and one in news + sat green in CI. Hand such files to pytest instead. + """ + try: + src = script.read_text(encoding="utf-8", errors="replace") + except OSError: + return False + has_items = re.search(r"^\s*(def test_|class Test|async def test_)", src, re.M) + has_main_guard = "__main__" in src and "__name__" in src + return bool(has_items and not has_main_guard) + + +def run_pytest_module(script: Path, core: Path | None, timeout: int) -> tuple[int, str]: + """Run one pytest module and map its outcome onto the pass/skip/fail codes.""" + env = dict(os.environ) + if core: + core = core.resolve() + env["PYTHONPATH"] = f"{core}{os.pathsep}{env.get('PYTHONPATH', '')}" + env["LEDMATRIX_CORE"] = str(core) + try: + proc = subprocess.run( + [sys.executable, "-m", "pytest", script.name, "-q", "--no-header", + "--tb=line", "-p", "no:cacheprovider"], + cwd=script.parent, env=env, capture_output=True, + text=True, timeout=timeout, stdin=subprocess.DEVNULL, + ) + except subprocess.TimeoutExpired: + return FAIL, f"timed out after {timeout}s" + + out = (proc.stdout or "") + (proc.stderr or "") + summary = "" + for line in reversed(out.splitlines()): + if "passed" in line or "failed" in line or "error" in line: + summary = line.strip("= ").strip() + break + if proc.returncode == 0: + return PASS, "" + # pytest exit 5 is "no tests collected" -- for a file we classified as a + # pytest module that means the classification was wrong, not that the file + # is fine. Surface it rather than swallowing it as a pass. + if proc.returncode == 5: + return FAIL, "pytest collected no tests from a file that looks like a pytest module" + failed = [ln.strip() for ln in out.splitlines() if ln.startswith("FAILED") or ln.startswith("ERROR")] + reason = "; ".join(f[:100] for f in failed[:3]) or summary or f"exit {proc.returncode}" + if len(failed) > 3: + reason += f" (+{len(failed) - 3} more)" + return FAIL, reason + + def run_one(script: Path, core: Path | None, timeout: int) -> tuple[int, str]: env = dict(os.environ) if core: @@ -146,7 +202,10 @@ def main() -> int: continue print(f"\n{pid}") for script in scripts: - status, detail = run_one(script, args.core, args.timeout) + if is_pytest_module(script): + status, detail = run_pytest_module(script, args.core, args.timeout) + else: + status, detail = run_one(script, args.core, args.timeout) totals[status] += 1 label = {PASS: "pass", SKIP: "SKIP", FAIL: "FAIL"}[status] print(f" [{label}] {script.name}" + (f" -- {detail}" if detail else "")) From a8f30d1d665193e47453d63ab679e3a064521616 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 22:50:55 -0400 Subject: [PATCH 08/13] fix(soccer): declare the league-qualified display modes the plugin actually registers The manifest declared soccer_live / soccer_recent / soccer_upcoming. At runtime the plugin registers a mode per configured league -- soccer_eng.1_live, soccer_esp.1_recent and so on -- so none of the three declared names existed. Anything driven off the manifest pointed at modes that are not there: on-demand requests by mode, display.plugin_rotation_order, and the UI's mode lists. The schema defines ten built-in leagues, so the manifest now enumerates all thirty modes, matching what the sibling multi-league plugins already do (baseball declares its milb and ncaa modes even though both are disabled by default). The seven leagues enabled out of the box produce the 21 modes observed on a live rig; the remaining three are declared for when they are switched on. Not covered: custom_leagues lets a user add any ESPN league code, and those modes cannot be enumerated ahead of time. Declaring the built-ins fixes the default install and every preset league; an open-ended set would need the core to accept a prefix or pattern, which is a larger change than this. Refs #461. --- plugins/soccer-scoreboard/manifest.json | 37 +++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 94247633..d0ee3286 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -21,9 +21,36 @@ "live-scores" ], "display_modes": [ - "soccer_live", - "soccer_recent", - "soccer_upcoming" + "soccer_eng.1_live", + "soccer_eng.1_recent", + "soccer_eng.1_upcoming", + "soccer_esp.1_live", + "soccer_esp.1_recent", + "soccer_esp.1_upcoming", + "soccer_fifa.world_live", + "soccer_fifa.world_recent", + "soccer_fifa.world_upcoming", + "soccer_fra.1_live", + "soccer_fra.1_recent", + "soccer_fra.1_upcoming", + "soccer_ger.1_live", + "soccer_ger.1_recent", + "soccer_ger.1_upcoming", + "soccer_ita.1_live", + "soccer_ita.1_recent", + "soccer_ita.1_upcoming", + "soccer_por.1_live", + "soccer_por.1_recent", + "soccer_por.1_upcoming", + "soccer_uefa.champions_live", + "soccer_uefa.champions_recent", + "soccer_uefa.champions_upcoming", + "soccer_uefa.europa_live", + "soccer_uefa.europa_recent", + "soccer_uefa.europa_upcoming", + "soccer_usa.1_live", + "soccer_usa.1_recent", + "soccer_usa.1_upcoming" ], "versions": [ { @@ -337,7 +364,7 @@ { "released": "2026-07-29", "version": "2.5.0", - "notes": "Corrected every team code in TEAMS.md against ESPN's live data \u2014 Manchester United is MAN (not MUN), Manchester City MNC (not MCI), Real Madrid RMA, and Ligue 1 had eight wrong codes. The plugin now also says why a league is empty: an unrecognised favorite team logs a warning naming the closest match, while a correct code in a league with no fixtures yet logs the date the season starts.", + "notes": "Corrected every team code in TEAMS.md against ESPN's live data — Manchester United is MAN (not MUN), Manchester City MNC (not MCI), Real Madrid RMA, and Ligue 1 had eight wrong codes. The plugin now also says why a league is empty: an unrecognised favorite team logs a warning naming the closest match, while a correct code in a league with no fixtures yet logs the date the season starts.", "ledmatrix_min": "2.0.0" }, { @@ -367,7 +394,7 @@ { "released": "2026-07-02", "version": "2.2.0", - "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores \u2014 spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league, including custom leagues.", + "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores — spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league, including custom leagues.", "ledmatrix_min": "2.0.0" }, { From b1017f489afb3f6a0e8bc1bba4a9f8ab0fc9e94f Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 23:08:02 -0400 Subject: [PATCH 09/13] fix(sports): load .bdf through FreeType in the four remaining renderers baseball and football looked for a pre-converted .pil/.pbm pair next to the .bdf; hockey and lacrosse did not attempt BDF at all, warning "not supported; convert to PILfont format" and falling back. No .pil ships anywhere in the project, so in practice every .bdf face in those pickers rendered as the default font. The premise was wrong: FreeType reads BDF, which is what the core's FontManager and clock-simple, news, countdown, tide-display, jellyfin-now-playing and calendar all already do. All four now call ImageFont.truetype() and retry at the size the file declares, since a bitmap face exists at exactly one pixel size and FreeType rejects any other with "invalid pixel size". The three stale comments asserting truetype() cannot read BDF are corrected, since that claim is what kept propagating. Verified against a core checkout of main (the soak rig's core is on a branch predating src/common/sports_shared, so these four cannot load there): _bdf_pixel_size("5x7.bdf") = 7, _bdf_pixel_size("4x6.bdf") = 6 truetype(5x7.bdf, 8) -> OSError "invalid pixel size"; at native 7 -> loads truetype(4x6.bdf, 8) -> OSError "invalid pixel size"; at native 6 -> loads so the retry is doing exactly the work the fallback used to skip. Honest limit on the verification: the render-level A/B was inconclusive. With no live game data the 128x64 card does not draw the customized text elements, so all three font choices hash identically before and after. The loader is unit-verified; a render-level check wants seeded fixture data, which the harness files for these plugins do not currently carry. Closes #459. --- plugins/baseball-scoreboard/game_renderer.py | 43 +++++++++++++++----- plugins/football-scoreboard/game_renderer.py | 34 ++++++++-------- plugins/hockey-scoreboard/sports.py | 35 ++++++++++++---- plugins/lacrosse-scoreboard/game_renderer.py | 23 +++++++---- plugins/lacrosse-scoreboard/sports.py | 35 ++++++++++++---- 5 files changed, 118 insertions(+), 52 deletions(-) diff --git a/plugins/baseball-scoreboard/game_renderer.py b/plugins/baseball-scoreboard/game_renderer.py index f10aa832..fcdfe242 100644 --- a/plugins/baseball-scoreboard/game_renderer.py +++ b/plugins/baseball-scoreboard/game_renderer.py @@ -96,6 +96,20 @@ def resolve_font_name(font_name: str) -> str: _DERIVE_TOP_SPAN = object() + +def _bdf_pixel_size(path): + """The pixel size a .bdf font declares, or None if it does not.""" + try: + with open(path, "r", encoding="latin-1") as handle: + for line in handle: + if line.startswith("PIXEL_SIZE"): + return int(line.split()[1]) + if line.startswith("CHARS"): + break # past the header; no point reading the glyphs + except (OSError, ValueError, IndexError): + return None + return None + class GameRenderer(SportsGameRendererMixin): """Renders individual baseball game cards as PIL Images.""" @@ -226,18 +240,25 @@ def _load_custom_font(self, element_config: Dict[str, Any], default_size: int = if font_path.lower().endswith('.ttf') or font_path.lower().endswith('.otf'): return ImageFont.truetype(font_path, font_size) elif font_path.lower().endswith('.bdf'): - # BDF fonts require pre-conversion: pilfont.py font.bdf -> font.pil + font.pbm - pil_font_path = font_path.rsplit('.', 1)[0] + '.pil' - if os.path.exists(pil_font_path): - try: - return ImageFont.load(pil_font_path) - except Exception as e: - self.logger.warning(f"Failed to load pre-converted BDF font {pil_font_path}: {e}") - else: + # FreeType reads BDF directly -- no pre-conversion needed. + # This used to look for a .pil/.pbm pair produced by + # pilfont.py, and no .pil ships anywhere, so every .bdf in + # the picker warned and fell back to the default font. + try: + return ImageFont.truetype(font_path, font_size) + except OSError: + # A bitmap face exists at exactly the size it was drawn + # at; FreeType rejects any other with "invalid pixel + # size". Retry at the size the file declares. + native = _bdf_pixel_size(font_path) + if native is not None and native != font_size: + try: + return ImageFont.truetype(font_path, native) + except OSError: + pass self.logger.warning( - f"BDF font {font_name} requires conversion. " - f"Run: pilfont.py {font_path}" - ) + f"Could not load BDF font {font_name} at {font_size} " + f"or its native size") else: self.logger.warning(f"Unknown font file type: {font_name}") else: diff --git a/plugins/football-scoreboard/game_renderer.py b/plugins/football-scoreboard/game_renderer.py index 390090d7..49667be0 100644 --- a/plugins/football-scoreboard/game_renderer.py +++ b/plugins/football-scoreboard/game_renderer.py @@ -529,22 +529,24 @@ def _load_custom_font(self, element_config: Dict[str, Any], default_size: int = # TTF/OTF fonts - use ImageFont.truetype() return ImageFont.truetype(font_path, font_size) elif font_path.lower().endswith('.bdf'): - # BDF fonts - ImageFont.truetype() does NOT support BDF files - # Option (b): Try to load pre-converted .pil/.pbm file (recommended approach) - # Use pilfont.py to convert: pilfont.py font.bdf (creates font.pil and font.pbm) - pil_font_path = font_path.rsplit('.', 1)[0] + '.pil' - if os.path.exists(pil_font_path): - try: - font = ImageFont.load(pil_font_path) - self.logger.debug(f"Loaded BDF font from pre-converted PIL file: {pil_font_path}") - return font - except Exception: - # Pre-converted file exists but failed to load - will fall through to fallback - pass - - # If no pre-converted file or loading failed, BDF cannot be loaded directly - # Note: PIL.BdfFontFile doesn't exist in standard Pillow, so pre-conversion is required - # The warning will be logged only if fallback also fails (see below) + # FreeType reads BDF directly. The pre-conversion dance + # below assumed otherwise; no .pil ships anywhere, so every + # .bdf in the picker fell back to the default font. + try: + return ImageFont.truetype(font_path, font_size) + except OSError: + # A bitmap face exists at exactly the size it was drawn + # at; FreeType rejects any other with "invalid pixel + # size". Retry at the size the file declares. + native = _bdf_pixel_size(font_path) + if native is not None and native != font_size: + try: + return ImageFont.truetype(font_path, native) + except OSError: + pass + self.logger.warning( + f"Could not load BDF font {font_name} at {font_size} " + f"or its native size") else: self.logger.warning(f"Unknown font file type: {font_name}, trying fallback") else: diff --git a/plugins/hockey-scoreboard/sports.py b/plugins/hockey-scoreboard/sports.py index 10b05e99..a4dc3eaf 100644 --- a/plugins/hockey-scoreboard/sports.py +++ b/plugins/hockey-scoreboard/sports.py @@ -455,15 +455,32 @@ def _load_custom_font_from_element_config( self.logger.debug(f"Loaded font: {font_name} at size {font_size}") return font elif font_path.lower().endswith('.bdf'): - # BDF fonts are not supported by ImageFont.truetype() - # To use BDF fonts, convert to PILfont format using pilfont.py: - # python -m PIL.pilfont font.bdf - # This creates .pil and .pbm files that can be loaded with ImageFont.load() - self.logger.warning( - f"BDF font '{font_name}' not supported; convert to PILfont format " - f"using 'python -m PIL.pilfont {font_path}' then use the .pil file. " - "Falling back to default font." - ) + # FreeType reads BDF, so truetype() handles a .bdf directly + # -- the core's FontManager and several plugins already do + # this. The old note here claimed otherwise and pointed at + # pilfont.py, so every .bdf face in the picker warned and + # fell back to the default font. + try: + font = ImageFont.truetype(font_path, font_size) + self.logger.debug(f"Loaded BDF font: {font_name} at size {font_size}") + return font + except OSError: + # A bitmap face exists at exactly the size it was drawn + # at; FreeType rejects any other with "invalid pixel + # size". Retry at the size the file declares. + native = _bdf_pixel_size(font_path) + if native is not None and native != font_size: + try: + font = ImageFont.truetype(font_path, native) + self.logger.debug( + f"Loaded BDF font {font_name} at its native size {native} " + f"(requested {font_size})") + return font + except OSError: + pass + self.logger.warning( + f"Could not load BDF font '{font_name}' at {font_size} " + "or its native size; falling back to default font.") # Fall through to default else: self.logger.warning(f"Unknown font file type: {font_name}, using default") diff --git a/plugins/lacrosse-scoreboard/game_renderer.py b/plugins/lacrosse-scoreboard/game_renderer.py index 4d25bd29..1ddbe53f 100644 --- a/plugins/lacrosse-scoreboard/game_renderer.py +++ b/plugins/lacrosse-scoreboard/game_renderer.py @@ -220,15 +220,24 @@ def _load_custom_font(self, element_config: Dict[str, Any], default_size: int = if font_path.lower().endswith(('.ttf', '.otf')): return ImageFont.truetype(font_path, font_size) elif font_path.lower().endswith('.bdf'): - # ImageFont.truetype does not support bitmap (BDF) fonts. - # Use ImageFont.load for BDFs; note that BDFs are bitmap - # fonts and ignore font_size — the glyph size is baked in. + # FreeType reads BDF directly. The pre-conversion dance + # below assumed otherwise; no .pil ships anywhere, so every + # .bdf in the picker fell back to the default font. try: - return ImageFont.load(font_path) - except Exception as e: + return ImageFont.truetype(font_path, font_size) + except OSError: + # A bitmap face exists at exactly the size it was drawn + # at; FreeType rejects any other with "invalid pixel + # size". Retry at the size the file declares. + native = _bdf_pixel_size(font_path) + if native is not None and native != font_size: + try: + return ImageFont.truetype(font_path, native) + except OSError: + pass self.logger.warning( - f"Could not load BDF font {font_name}: {e}; using default" - ) + f"Could not load BDF font {font_name} at {font_size} " + f"or its native size") except Exception as e: self.logger.error(f"Error loading font {font_name}: {e}") diff --git a/plugins/lacrosse-scoreboard/sports.py b/plugins/lacrosse-scoreboard/sports.py index e5018304..dcda7a1b 100644 --- a/plugins/lacrosse-scoreboard/sports.py +++ b/plugins/lacrosse-scoreboard/sports.py @@ -456,15 +456,32 @@ def _load_custom_font_from_element_config( self.logger.debug(f"Loaded font: {font_name} at size {font_size}") return font elif font_path.lower().endswith('.bdf'): - # BDF fonts are not supported by ImageFont.truetype() - # To use BDF fonts, convert to PILfont format using pilfont.py: - # python -m PIL.pilfont font.bdf - # This creates .pil and .pbm files that can be loaded with ImageFont.load() - self.logger.warning( - f"BDF font '{font_name}' not supported; convert to PILfont format " - f"using 'python -m PIL.pilfont {font_path}' then use the .pil file. " - "Falling back to default font." - ) + # FreeType reads BDF, so truetype() handles a .bdf directly + # -- the core's FontManager and several plugins already do + # this. The old note here claimed otherwise and pointed at + # pilfont.py, so every .bdf face in the picker warned and + # fell back to the default font. + try: + font = ImageFont.truetype(font_path, font_size) + self.logger.debug(f"Loaded BDF font: {font_name} at size {font_size}") + return font + except OSError: + # A bitmap face exists at exactly the size it was drawn + # at; FreeType rejects any other with "invalid pixel + # size". Retry at the size the file declares. + native = _bdf_pixel_size(font_path) + if native is not None and native != font_size: + try: + font = ImageFont.truetype(font_path, native) + self.logger.debug( + f"Loaded BDF font {font_name} at its native size {native} " + f"(requested {font_size})") + return font + except OSError: + pass + self.logger.warning( + f"Could not load BDF font '{font_name}' at {font_size} " + "or its native size; falling back to default font.") # Fall through to default else: self.logger.warning(f"Unknown font file type: {font_name}, using default") From e0f38957e5e20d1926e89f887799a27a7d405c56 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 23:21:49 -0400 Subject: [PATCH 10/13] chore: bump versions for the 19 plugins changed here, and sync the registry CLAUDE.md is explicit that a monorepo plugin change needs a manifest version bump plus update_registry.py, or the store never offers the update and users never receive the fix. Nine commits of fixes went in without it; this catches them all up. ledmatrix-flights is deliberately not bumped: its only change is to a test file (the stale fake_fetch / FakeLogger doubles), which ships nothing to users. The CI workflow draws the same line, skipping the version gate for test-only changes. 7-segment-clock 1.0.5 -> 1.0.6 ledmatrix-leaderboard 1.3.4 -> 1.3.5 afl-scoreboard 1.22.3 -> 1.22.4 ledmatrix-music 1.4.0 -> 1.4.1 baseball-scoreboard 1.40.3 -> 1.40.4 nfl-draft 1.4.4 -> 1.4.5 basketball-scoreboard 1.29.4 -> 1.29.5 nrl-scoreboard 1.21.3 -> 1.21.4 calendar 1.2.5 -> 1.2.6 olympics 2.0.3 -> 2.0.4 christmas-countdown 1.0.3 -> 1.0.4 on-air 1.2.7 -> 1.2.8 football-scoreboard 3.5.1 -> 3.5.2 soccer-scoreboard 2.24.4 -> 2.24.5 geochron 1.0.4 -> 1.0.5 static-image 1.0.7 -> 1.0.8 hello-world 1.1.0 -> 1.1.1 hockey-scoreboard 1.25.3 -> 1.25.4 lacrosse-scoreboard 1.24.4 -> 1.24.5 --- plugins.json | 38 ++++++++++----------- plugins/7-segment-clock/manifest.json | 2 +- plugins/afl-scoreboard/manifest.json | 2 +- plugins/baseball-scoreboard/manifest.json | 8 ++--- plugins/basketball-scoreboard/manifest.json | 4 +-- plugins/calendar/manifest.json | 2 +- plugins/christmas-countdown/manifest.json | 2 +- plugins/football-scoreboard/manifest.json | 4 +-- plugins/geochron/manifest.json | 2 +- plugins/hello-world/manifest.json | 2 +- plugins/hockey-scoreboard/manifest.json | 2 +- plugins/lacrosse-scoreboard/manifest.json | 4 +-- plugins/ledmatrix-leaderboard/manifest.json | 2 +- plugins/ledmatrix-music/manifest.json | 2 +- plugins/nfl-draft/manifest.json | 2 +- plugins/nrl-scoreboard/manifest.json | 2 +- plugins/olympics/manifest.json | 2 +- plugins/on-air/manifest.json | 4 +-- plugins/soccer-scoreboard/manifest.json | 2 +- plugins/static-image/manifest.json | 2 +- 20 files changed, 45 insertions(+), 45 deletions(-) diff --git a/plugins.json b/plugins.json index 941fb5d7..a27174d9 100644 --- a/plugins.json +++ b/plugins.json @@ -50,7 +50,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.0.5" + "latest_version": "1.0.6" }, { "id": "baseball-scoreboard", @@ -76,7 +76,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.40.3" + "latest_version": "1.40.4" }, { "id": "basketball-scoreboard", @@ -101,7 +101,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.29.4" + "latest_version": "1.29.5" }, { "id": "calendar", @@ -124,7 +124,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.2.5" + "latest_version": "1.2.6" }, { "id": "christmas-countdown", @@ -146,7 +146,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.0.3" + "latest_version": "1.0.4" }, { "id": "clock-simple", @@ -240,7 +240,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "3.5.1" + "latest_version": "3.5.2" }, { "id": "geochron", @@ -266,7 +266,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.0.4", + "latest_version": "1.0.5", "icon": "fa-globe" }, { @@ -311,7 +311,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.1.0" + "latest_version": "1.1.1" }, { "id": "hockey-scoreboard", @@ -335,7 +335,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.25.3", + "latest_version": "1.25.4", "icon": "fas fa-hockey-puck" }, { @@ -359,7 +359,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.24.4", + "latest_version": "1.24.5", "icon": "fas fa-baseball-ball" }, { @@ -412,7 +412,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.3.4" + "latest_version": "1.3.5" }, { "id": "ledmatrix-flights", @@ -533,7 +533,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.4.0" + "latest_version": "1.4.1" }, { "id": "news", @@ -581,7 +581,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.2.7", + "latest_version": "1.2.8", "icon": "fa-circle-dot" }, { @@ -605,7 +605,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.4.4", + "latest_version": "1.4.5", "icon": "fas fa-football-ball" }, { @@ -681,7 +681,7 @@ "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "2.0.3" + "latest_version": "2.0.4" }, { "id": "pga-tour-leaderboard", @@ -760,7 +760,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "2.24.4" + "latest_version": "2.24.5" }, { "id": "static-image", @@ -783,7 +783,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.0.7" + "latest_version": "1.0.8" }, { "id": "stock-news", @@ -1048,7 +1048,7 @@ "downloads": 0, "verified": true, "screenshot": "", - "latest_version": "1.22.3", + "latest_version": "1.22.4", "last_updated": "2026-09-02" }, { @@ -1095,7 +1095,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.21.3" + "latest_version": "1.21.4" }, { "id": "jellyfin-now-playing", diff --git a/plugins/7-segment-clock/manifest.json b/plugins/7-segment-clock/manifest.json index 6eeb7c08..fcc2bdbf 100644 --- a/plugins/7-segment-clock/manifest.json +++ b/plugins/7-segment-clock/manifest.json @@ -1,7 +1,7 @@ { "id": "7-segment-clock", "name": "7-Segment Clock", - "version": "1.0.5", + "version": "1.0.6", "description": "Display a retro-style 7-segment clock with customizable colors", "author": "LEDMatrix", "entry_point": "manager.py", diff --git a/plugins/afl-scoreboard/manifest.json b/plugins/afl-scoreboard/manifest.json index b81d91d9..8e35c03f 100644 --- a/plugins/afl-scoreboard/manifest.json +++ b/plugins/afl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "afl-scoreboard", "name": "AFL Scoreboard", - "version": "1.22.3", + "version": "1.22.4", "author": "ChuckBuilds", "description": "Live, recent, and upcoming AFL (Australian Football League) games with real-time scores and game status.", "category": "sports", diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index c9f55f7d..bb2f1714 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.40.3", + "version": "1.40.4", "update_interval": 60, "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", @@ -412,7 +412,7 @@ { "released": "2026-07-08", "version": "1.14.2", - "notes": "Shrink the Traditional Scoreboard's ball/strike/out circle indicators further (they were still a bit overpowering) and move the batting-team \u25b2/\u25bc indicator out of the At Bat column into the header row's empty team-column cell, right next to the inning numbers.", + "notes": "Shrink the Traditional Scoreboard's ball/strike/out circle indicators further (they were still a bit overpowering) and move the batting-team ▲/▼ indicator out of the At Bat column into the header row's empty team-column cell, right next to the inning numbers.", "ledmatrix_min": "2.0.0" }, { @@ -430,7 +430,7 @@ { "released": "2026-07-07", "version": "1.13.1", - "notes": "Fix the Traditional Scoreboard's At Bat side panel (added in 1.13.0) clipping its ball/strike/out dots off the right edge of the display -- the fit check compared leftover space against a flush-left grid, but the grid is actually centered, so it was eating into the panel's reserved space from the left too. Also account for the Outs row's extra batting-team \u25b2/\u25bc arrow, which wasn't factored into the width check at all.", + "notes": "Fix the Traditional Scoreboard's At Bat side panel (added in 1.13.0) clipping its ball/strike/out dots off the right edge of the display -- the fit check compared leftover space against a flush-left grid, but the grid is actually centered, so it was eating into the panel's reserved space from the left too. Also account for the Outs row's extra batting-team ▲/▼ arrow, which wasn't factored into the width check at all.", "ledmatrix_min": "2.0.0" }, { @@ -472,7 +472,7 @@ { "released": "2026-07-02", "version": "1.7.0", - "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores \u2014 spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league.", + "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores — spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league.", "ledmatrix_min": "2.0.0" }, { diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 0d0ee2a3..8bc19bc9 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.29.4", + "version": "1.29.5", "update_interval": 60, "description": "Live, recent, and upcoming basketball games across NBA, NCAA Men's, NCAA Women's, and WNBA with real-time scores, schedules, and March Madness tournament support", "author": "ChuckBuilds", @@ -347,7 +347,7 @@ { "released": "2026-07-02", "version": "1.6.0", - "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores \u2014 spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league.", + "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores — spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league.", "ledmatrix_min": "2.0.0" }, { diff --git a/plugins/calendar/manifest.json b/plugins/calendar/manifest.json index eab688a3..0e4390ba 100644 --- a/plugins/calendar/manifest.json +++ b/plugins/calendar/manifest.json @@ -1,7 +1,7 @@ { "id": "calendar", "name": "Google Calendar", - "version": "1.2.5", + "version": "1.2.6", "author": "ChuckBuilds", "description": "Display upcoming events from Google Calendar with date, time, and event details. Shows next 1-3 events with automatic rotation and timezone support.", "category": "productivity", diff --git a/plugins/christmas-countdown/manifest.json b/plugins/christmas-countdown/manifest.json index 0e4e6d4f..8526f7f4 100644 --- a/plugins/christmas-countdown/manifest.json +++ b/plugins/christmas-countdown/manifest.json @@ -1,7 +1,7 @@ { "id": "christmas-countdown", "name": "Christmas Countdown", - "version": "1.0.3", + "version": "1.0.4", "author": "ChuckBuilds", "description": "Display a countdown to Christmas with a stylized Christmas tree logo and festive text", "category": "holiday", diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index 61e975e3..f10543b2 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "3.5.1", + "version": "3.5.2", "update_interval": 60, "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", @@ -446,7 +446,7 @@ "released": "2026-07-10", "version": "2.8.0", "ledmatrix_min_version": "2.0.0", - "notes": "Adaptive layout (beta, opt-in): set layout_mode: \"adaptive\" to scale fonts/logos/regions to any panel size. Default stays \"classic\" \u2014 rendering is unchanged unless you opt in; switch back to classic in config to revert without reinstalling. Adaptive mode also applies customization.layout x/y offsets in scroll mode (classic scroll never did). User-configured fonts win over adaptive sizing." + "notes": "Adaptive layout (beta, opt-in): set layout_mode: \"adaptive\" to scale fonts/logos/regions to any panel size. Default stays \"classic\" — rendering is unchanged unless you opt in; switch back to classic in config to revert without reinstalling. Adaptive mode also applies customization.layout x/y offsets in scroll mode (classic scroll never did). User-configured fonts win over adaptive sizing." }, { "released": "2026-07-08", diff --git a/plugins/geochron/manifest.json b/plugins/geochron/manifest.json index a7ba75df..64079097 100644 --- a/plugins/geochron/manifest.json +++ b/plugins/geochron/manifest.json @@ -1,7 +1,7 @@ { "id": "geochron", "name": "Geochron World Clock", - "version": "1.0.4", + "version": "1.0.5", "author": "ChuckBuilds", "description": "High-fidelity world map clock showing the real-time day/night terminator with civil/nautical/astronomical twilight bands, the subsolar point, configurable city markers, and a digital UTC/local clock - a classic Geochron, reimagined for LED matrix panels of any size.", "entry_point": "manager.py", diff --git a/plugins/hello-world/manifest.json b/plugins/hello-world/manifest.json index 40666285..5555e9e0 100644 --- a/plugins/hello-world/manifest.json +++ b/plugins/hello-world/manifest.json @@ -1,7 +1,7 @@ { "id": "hello-world", "name": "Hello World", - "version": "1.1.0", + "version": "1.1.1", "author": "ChuckBuilds", "description": "A simple test plugin that displays a customizable message", "entry_point": "manager.py", diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index c6337a8a..11dd8669 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.25.3", + "version": "1.25.4", "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/lacrosse-scoreboard/manifest.json b/plugins/lacrosse-scoreboard/manifest.json index 545c7fc1..e2deb038 100644 --- a/plugins/lacrosse-scoreboard/manifest.json +++ b/plugins/lacrosse-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "lacrosse-scoreboard", "name": "Lacrosse Scoreboard", - "version": "1.24.4", + "version": "1.24.5", "author": "ChuckBuilds", "description": "Live, recent, and upcoming NCAA men's and women's lacrosse games with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/lacrosse-scoreboard", @@ -360,7 +360,7 @@ { "released": "2026-07-02", "version": "1.3.0", - "notes": "Add exclude_teams (hide specific teams from the live rotation and recent/final scores \u2014 spoiler protection, takes precedence over favorite_teams/show_all_live) and favorite_live_boost (tune how many more turns your favorite's live game gets in the rotation vs other live games, 1 = even rotation, default 2).", + "notes": "Add exclude_teams (hide specific teams from the live rotation and recent/final scores — spoiler protection, takes precedence over favorite_teams/show_all_live) and favorite_live_boost (tune how many more turns your favorite's live game gets in the rotation vs other live games, 1 = even rotation, default 2).", "ledmatrix_min": "2.0.0" }, { diff --git a/plugins/ledmatrix-leaderboard/manifest.json b/plugins/ledmatrix-leaderboard/manifest.json index 995cd700..0cd004d4 100644 --- a/plugins/ledmatrix-leaderboard/manifest.json +++ b/plugins/ledmatrix-leaderboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-leaderboard", "name": "Sports Leaderboard", - "version": "1.3.4", + "version": "1.3.5", "description": "Displays scrolling leaderboards and standings for multiple sports leagues including NFL, NBA, MLB, NCAA Football, NCAA Basketball, and more", "author": "ChuckBuilds", "entry_point": "manager.py", diff --git a/plugins/ledmatrix-music/manifest.json b/plugins/ledmatrix-music/manifest.json index d916eac8..49e67153 100644 --- a/plugins/ledmatrix-music/manifest.json +++ b/plugins/ledmatrix-music/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-music", "name": "Music Player - Now Playing", - "version": "1.4.0", + "version": "1.4.1", "description": "Real-time now playing display for Spotify and YouTube Music with album art, scrolling text, and progress bars", "author": "ChuckBuilds", "entry_point": "manager.py", diff --git a/plugins/nfl-draft/manifest.json b/plugins/nfl-draft/manifest.json index 884bb2ec..f4627e2e 100644 --- a/plugins/nfl-draft/manifest.json +++ b/plugins/nfl-draft/manifest.json @@ -1,7 +1,7 @@ { "id": "nfl-draft", "name": "NFL Draft", - "version": "1.4.4", + "version": "1.4.5", "author": "ChuckBuilds", "description": "Displays projected NFL draft picks from ESPN with live draft tracking support during the annual NFL Draft event. Includes simulate_live mode to replay a completed draft using real ESPN core API data. Shows team logos, player names, positions, and pick numbers in a scrolling display.", "entry_point": "manager.py", diff --git a/plugins/nrl-scoreboard/manifest.json b/plugins/nrl-scoreboard/manifest.json index b0ddc99e..22c009af 100644 --- a/plugins/nrl-scoreboard/manifest.json +++ b/plugins/nrl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "nrl-scoreboard", "name": "NRL Scoreboard", - "version": "1.21.3", + "version": "1.21.4", "author": "ChuckBuilds", "description": "Live, recent, and upcoming NRL (National Rugby League) games with real-time scores and game status.", "category": "sports", diff --git a/plugins/olympics/manifest.json b/plugins/olympics/manifest.json index e5794984..925a412c 100644 --- a/plugins/olympics/manifest.json +++ b/plugins/olympics/manifest.json @@ -1,7 +1,7 @@ { "id": "olympics", "name": "Olympics", - "version": "2.0.3", + "version": "2.0.4", "author": "ChuckBuilds", "description": "Enhanced Olympics plugin with live medal counts, upcoming events, results, and countdown. Supports Vegas scroll mode and regular display mode.", "category": "sports", diff --git a/plugins/on-air/manifest.json b/plugins/on-air/manifest.json index cf8a4334..8037362a 100644 --- a/plugins/on-air/manifest.json +++ b/plugins/on-air/manifest.json @@ -1,9 +1,9 @@ { "id": "on-air", "name": "On Air Light", - "version": "1.2.7", + "version": "1.2.8", "author": "ChuckBuilds", - "description": "Retro broadcast ON AIR tally light. Activate remotely via MQTT or Home Assistant to signal you're on a call, recording, or live \u2014 stays on until you turn it off.", + "description": "Retro broadcast ON AIR tally light. Activate remotely via MQTT or Home Assistant to signal you're on a call, recording, or live — stays on until you turn it off.", "entry_point": "manager.py", "class_name": "OnAirPlugin", "category": "utility", diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index d0ee3286..fe682bfa 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "2.24.4", + "version": "2.24.5", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, Champions League, Europa League, and FIFA World Cup", "category": "sports", diff --git a/plugins/static-image/manifest.json b/plugins/static-image/manifest.json index 5b329973..9153f03a 100644 --- a/plugins/static-image/manifest.json +++ b/plugins/static-image/manifest.json @@ -1,7 +1,7 @@ { "id": "static-image", "name": "Static Image Display", - "version": "1.0.7", + "version": "1.0.8", "author": "ChuckBuilds", "description": "Display static images on your LED matrix with automatic scaling, aspect ratio preservation, and transparency support. Perfect for logos, artwork, or custom graphics.", "entry_point": "manager.py", From 7a81308c76976f2267f06ea58dc9ff423d26ea9a Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Sat, 5 Sep 2026 17:44:08 -0400 Subject: [PATCH 11/13] fix(hello-world): fit the greeting to the panel; add the versions[] entries CI gates on Two things the on-hardware validation of this branch turned up. hello-world regressed to 3 overflow failures at 64x32, 64x64 and 96x48. The #448 fix is correct -- the font-manager branch finally executes -- but that means the registered 10px face is now used where the bundled BDF fallback used to be, and the plugin never measured its text. It now steps the size down until the string fits, which is what the template plugin should have been demonstrating. 5 PASS / 3 FAIL -> 8 PASS / 0 FAIL. The manifest bumps in the previous commit changed `version` without adding the matching `versions[]` entry. CLAUDE.md is explicit that versions[0] is the entry the store and loader actually read, and scripts/check_manifest_version_fields.py gates it. Each of the 19 now carries a new top entry with its floor renamed to `ledmatrix_min_version`, which is what that gate asks for. scripts/check_manifest_version_fields.py <19 ids> OK: 19 plugin(s) checked, newest version entries are current. ledmatrix-flights still gets no bump. Its only change is a root-level test_*.py, which the CI workflow explicitly excludes from the shipped-code set alongside test/ -- "they change nothing a user receives". --- plugins/7-segment-clock/manifest.json | 6 ++++ plugins/afl-scoreboard/manifest.json | 6 ++++ plugins/baseball-scoreboard/manifest.json | 6 ++++ plugins/basketball-scoreboard/manifest.json | 6 ++++ plugins/calendar/manifest.json | 6 ++++ plugins/christmas-countdown/manifest.json | 6 ++++ plugins/football-scoreboard/manifest.json | 6 ++++ plugins/geochron/manifest.json | 6 ++++ plugins/hello-world/manager.py | 35 +++++++++++++++++---- plugins/hello-world/manifest.json | 6 ++++ plugins/hockey-scoreboard/manifest.json | 6 ++++ plugins/lacrosse-scoreboard/manifest.json | 6 ++++ plugins/ledmatrix-leaderboard/manifest.json | 6 ++++ plugins/ledmatrix-music/manifest.json | 6 ++++ plugins/nfl-draft/manifest.json | 6 ++++ plugins/nrl-scoreboard/manifest.json | 6 ++++ plugins/olympics/manifest.json | 6 ++++ plugins/on-air/manifest.json | 6 ++++ plugins/soccer-scoreboard/manifest.json | 6 ++++ plugins/static-image/manifest.json | 6 ++++ 20 files changed, 143 insertions(+), 6 deletions(-) diff --git a/plugins/7-segment-clock/manifest.json b/plugins/7-segment-clock/manifest.json index fcc2bdbf..a6171216 100644 --- a/plugins/7-segment-clock/manifest.json +++ b/plugins/7-segment-clock/manifest.json @@ -24,6 +24,12 @@ "default_duration": 15, "config_schema": "config_schema.json", "versions": [ + { + "version": "1.0.6", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-02", "version": "1.0.5", diff --git a/plugins/afl-scoreboard/manifest.json b/plugins/afl-scoreboard/manifest.json index 8e35c03f..56103e87 100644 --- a/plugins/afl-scoreboard/manifest.json +++ b/plugins/afl-scoreboard/manifest.json @@ -18,6 +18,12 @@ "afl_upcoming" ], "versions": [ + { + "version": "1.22.4", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-04", "version": "1.22.3", diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index bb2f1714..c4fcc22c 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -31,6 +31,12 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "version": "1.40.4", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-04", "version": "1.40.3", diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 8bc19bc9..221f9d07 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -19,6 +19,12 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "version": "1.29.5", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-04", "version": "1.29.4", diff --git a/plugins/calendar/manifest.json b/plugins/calendar/manifest.json index 0e4390ba..7263e979 100644 --- a/plugins/calendar/manifest.json +++ b/plugins/calendar/manifest.json @@ -35,6 +35,12 @@ } ], "versions": [ + { + "version": "1.2.6", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-03", "version": "1.2.5", diff --git a/plugins/christmas-countdown/manifest.json b/plugins/christmas-countdown/manifest.json index 8526f7f4..22914afb 100644 --- a/plugins/christmas-countdown/manifest.json +++ b/plugins/christmas-countdown/manifest.json @@ -17,6 +17,12 @@ "christmas-countdown" ], "versions": [ + { + "version": "1.0.4", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-02", "version": "1.0.3", diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index f10543b2..72773fb9 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -25,6 +25,12 @@ "ncaa_fb_live" ], "versions": [ + { + "version": "3.5.2", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-04", "version": "3.5.1", diff --git a/plugins/geochron/manifest.json b/plugins/geochron/manifest.json index 64079097..9367d27e 100644 --- a/plugins/geochron/manifest.json +++ b/plugins/geochron/manifest.json @@ -24,6 +24,12 @@ ">=2.0.0" ], "versions": [ + { + "version": "1.0.5", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "version": "1.0.4", "released": "2026-09-02", diff --git a/plugins/hello-world/manager.py b/plugins/hello-world/manager.py index 0642c34a..5bb5166c 100644 --- a/plugins/hello-world/manager.py +++ b/plugins/hello-world/manager.py @@ -83,6 +83,29 @@ def _register_fonts(self): except Exception as e: self.logger.warning(f"Error registering fonts: {e}") + def _fitted_font(self, font_manager, element_key, spec, text, width): + """The registered face, stepped down until `text` fits `width`. + + The registered sizes suit a 128px-wide panel. Asking for them + unconditionally overflowed 64x32, 64x64 and 96x48 -- which only became + visible once the font-manager branch started working at all, since the + bundled BDF it used to fall back to is smaller. Measure and shrink + rather than assume a panel size. + """ + family, size = spec + usable = max(1, width - 2) + for size_px in range(size, 3, -1): + font = font_manager.resolve_font(element_key, family, size_px, + plugin_id=self.plugin_id) + if font is None: + return None + try: + if self.display_manager.get_text_width(text, font) <= usable: + return font + except Exception: # noqa: BLE001 - unmeasurable font, take it as-is + return font + return font + def _load_font(self): """Load the 6x9 BDF font for text rendering.""" if freetype is None: @@ -159,12 +182,12 @@ def display(self, force_clear=False): # a size; calling it with an element key and no size raised # TypeError on every frame, so this whole branch was dead # and the bundled BDF was always used instead. - message_font = font_manager.resolve_font( - f"{self.plugin_id}.message", self.MESSAGE_FONT[0], - self.MESSAGE_FONT[1], plugin_id=self.plugin_id) - time_font = font_manager.resolve_font( - f"{self.plugin_id}.time", self.TIME_FONT[0], - self.TIME_FONT[1], plugin_id=self.plugin_id) + message_font = self._fitted_font( + font_manager, f"{self.plugin_id}.message", + self.MESSAGE_FONT, self.message, width) + time_font = self._fitted_font( + font_manager, f"{self.plugin_id}.time", + self.TIME_FONT, self.current_time_str or "00:00", width) except Exception as e: self.logger.warning(f"Error getting fonts from font manager: {e}") diff --git a/plugins/hello-world/manifest.json b/plugins/hello-world/manifest.json index 5555e9e0..60c0fc80 100644 --- a/plugins/hello-world/manifest.json +++ b/plugins/hello-world/manifest.json @@ -16,6 +16,12 @@ "hello-world" ], "versions": [ + { + "version": "1.1.1", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-02", "version": "1.1.0", diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index 11dd8669..58681c41 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -54,6 +54,12 @@ } ], "versions": [ + { + "version": "1.25.4", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-04", "version": "1.25.3", diff --git a/plugins/lacrosse-scoreboard/manifest.json b/plugins/lacrosse-scoreboard/manifest.json index e2deb038..d95bac9f 100644 --- a/plugins/lacrosse-scoreboard/manifest.json +++ b/plugins/lacrosse-scoreboard/manifest.json @@ -50,6 +50,12 @@ } ], "versions": [ + { + "version": "1.24.5", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-04", "version": "1.24.4", diff --git a/plugins/ledmatrix-leaderboard/manifest.json b/plugins/ledmatrix-leaderboard/manifest.json index 0cd004d4..5ff689c0 100644 --- a/plugins/ledmatrix-leaderboard/manifest.json +++ b/plugins/ledmatrix-leaderboard/manifest.json @@ -31,6 +31,12 @@ "requirements_file": "requirements.txt", "min_ledmatrix_version": "2.0.0", "versions": [ + { + "version": "1.3.5", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-03", "version": "1.3.4", diff --git a/plugins/ledmatrix-music/manifest.json b/plugins/ledmatrix-music/manifest.json index 49e67153..228fd87e 100644 --- a/plugins/ledmatrix-music/manifest.json +++ b/plugins/ledmatrix-music/manifest.json @@ -66,6 +66,12 @@ } ], "versions": [ + { + "version": "1.4.1", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "version": "1.4.0", "released": "2026-09-03", diff --git a/plugins/nfl-draft/manifest.json b/plugins/nfl-draft/manifest.json index f4627e2e..283fb7cf 100644 --- a/plugins/nfl-draft/manifest.json +++ b/plugins/nfl-draft/manifest.json @@ -24,6 +24,12 @@ ], "icon": "fas fa-football-ball", "versions": [ + { + "version": "1.4.5", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-03", "version": "1.4.4", diff --git a/plugins/nrl-scoreboard/manifest.json b/plugins/nrl-scoreboard/manifest.json index 22c009af..b4a7ee7e 100644 --- a/plugins/nrl-scoreboard/manifest.json +++ b/plugins/nrl-scoreboard/manifest.json @@ -18,6 +18,12 @@ "nrl_upcoming" ], "versions": [ + { + "version": "1.21.4", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-04", "version": "1.21.3", diff --git a/plugins/olympics/manifest.json b/plugins/olympics/manifest.json index 925a412c..57a17495 100644 --- a/plugins/olympics/manifest.json +++ b/plugins/olympics/manifest.json @@ -37,6 +37,12 @@ "country_tracking": true }, "versions": [ + { + "version": "2.0.4", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-03", "version": "2.0.3", diff --git a/plugins/on-air/manifest.json b/plugins/on-air/manifest.json index 8037362a..e18ebca7 100644 --- a/plugins/on-air/manifest.json +++ b/plugins/on-air/manifest.json @@ -22,6 +22,12 @@ ">=2.0.0" ], "versions": [ + { + "version": "1.2.8", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-03", "version": "1.2.7", diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index fe682bfa..3d47bc77 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -53,6 +53,12 @@ "soccer_usa.1_upcoming" ], "versions": [ + { + "version": "2.24.5", + "released": "2026-09-05", + "ledmatrix_min_version": "3.3.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-04", "version": "2.24.4", diff --git a/plugins/static-image/manifest.json b/plugins/static-image/manifest.json index 9153f03a..d230f059 100644 --- a/plugins/static-image/manifest.json +++ b/plugins/static-image/manifest.json @@ -18,6 +18,12 @@ "static_image" ], "versions": [ + { + "version": "1.0.8", + "released": "2026-09-05", + "ledmatrix_min_version": "2.0.0", + "notes": "Validation-sweep fixes; see the PR for the specific defect." + }, { "released": "2026-09-03", "version": "1.0.7", From bab6e8cdbb38cffe8e383a2b7c51b6254b5ae222 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 19:13:19 -0400 Subject: [PATCH 12/13] fix: resolve the CI failures on #466 The `safety` job failed on two steps. Both are fixed, and one of them was a bug of mine that no test could have caught. _bdf_pixel_size() was called in five sports plugins and defined in one. The other four raise NameError -- and the call sits inside an `except OSError:` handler that only runs when a BDF font is requested at a size it was not drawn at, which is precisely the case the fix exists for and precisely the case nothing exercised. Every unit test passed, every plugin imported, and the fix was simply dead in four of five. pylint --errors-only reports it as E0602; it was two of Codacy's four new issues. scripts/test_module_helpers_defined.py now checks the general invariant across all 493 plugin files. Placing that helper broke scripts/test_logo_placeholder_refresh.py, which captures _logo_needs_refresh with a regex running to the next "\n\nclass ", so anything between the two reads as part of the helper and the byte-identity check fails. Moved above it. Worth knowing the guard carries that implicit layout constraint. The soccer favourite-team tests failed with ModuleNotFoundError on src.common.sports_shared. Seven of the eight soccer tests point src.__path__ at a real core so genuine submodules resolve while the stubs stay stubbed; this file never got that block, so it broke the day sports.py started importing sports_shared. It read as passing because the CI runner was reporting pytest files without running them -- fixing that runner is what made these 22 failures visible. Closes #468. christmas-countdown and geochron golden drift is NOT from this PR: unmodified main produces byte-identical drift (180/227px and 200/243px, same max deltas), and CI reports those same numbers. The goldens predate the harness setting fontmode="1" to match the panel, so they carry antialiased text the panel cannot show. Refreshed; the rendered text is unchanged, only its antialiasing. This PR is simply the first to touch those two plugins and make CI render them. Codacy's other two: B110 in the olympics measurement guard, narrowed from a bare `except Exception: pass` so a real display-manager fault surfaces instead of silently disabling text fitting; and B603 on the pytest subprocess, annotated with the repo's existing nosec convention (fixed interpreter, argument list, no shell). Bandit is clean. Verified as CI runs it: safety harness passes for all 20 changed plugins, 212 passed / 1 skipped / 0 failed on the unit tests, all repo guards pass except test_odds_centre_collision.py, which fails identically on main and is untouched here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9 --- .../golden/128x32/christmas-countdown.png | Bin 1543 -> 1425 bytes .../golden/128x64/christmas-countdown.png | Bin 4119 -> 3974 bytes .../golden/128x96/christmas-countdown.png | Bin 4202 -> 4053 bytes .../golden/256x32/christmas-countdown.png | Bin 1606 -> 1481 bytes .../test/golden/64x32/christmas-countdown.png | Bin 1443 -> 1333 bytes .../test/golden/64x64/christmas-countdown.png | Bin 1491 -> 1372 bytes .../test/golden/96x48/christmas-countdown.png | Bin 2649 -> 2529 bytes plugins/football-scoreboard/game_renderer.py | 14 +++ .../geochron/test/golden/128x32/geochron.png | Bin 4663 -> 4381 bytes .../geochron/test/golden/128x64/geochron.png | Bin 8781 -> 8545 bytes .../geochron/test/golden/128x96/geochron.png | Bin 12497 -> 12273 bytes .../geochron/test/golden/256x128/geochron.png | Bin 27065 -> 26870 bytes .../geochron/test/golden/256x32/geochron.png | Bin 7656 -> 7368 bytes .../geochron/test/golden/64x32/geochron.png | Bin 2562 -> 2317 bytes .../geochron/test/golden/64x64/geochron.png | Bin 5203 -> 4905 bytes .../geochron/test/golden/96x48/geochron.png | Bin 0 -> 5111 bytes plugins/hockey-scoreboard/sports.py | 14 +++ plugins/lacrosse-scoreboard/game_renderer.py | 14 +++ plugins/lacrosse-scoreboard/sports.py | 14 +++ .../olympics/renderers/countdown_renderer.py | 8 +- .../test_favorite_team_diagnostics.py | 22 ++++ scripts/run_plugin_tests.py | 11 +- scripts/test_module_helpers_defined.py | 99 ++++++++++++++++++ 23 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 plugins/geochron/test/golden/96x48/geochron.png create mode 100644 scripts/test_module_helpers_defined.py diff --git a/plugins/christmas-countdown/test/golden/128x32/christmas-countdown.png b/plugins/christmas-countdown/test/golden/128x32/christmas-countdown.png index b2fbefd013061333e424d8f85fe9ea9c03a3047c..f1653ee7acf372ea016440417189aa80cf61ac67 100644 GIT binary patch delta 1406 zcmV-^1%dj943P_vB!5^*L_t(|ob8%fZyQw<$IrQURpD6sdx+sHBZr=$6=NQpa%|$MH7Xy@!XC;$-aDA$F6v z`N1P;#9C`}Dg zSa-M4*Jnl|p+cd&uu#tBDy}QrGo_tpZMmuHw0$%-03sNMa_m_2(g&}fdo}dk#Ox2> zKe%xtozL6Up)#m;_6^2pIjosMhQW>=J^0D_z?nl=K07DJ#^R=_BDP7Cy8`kZx5);L z6+i?4SSqn?wH-8XEu+Diycntn%Qae(lPlou)H_m`%_&uh?nu-%s_GHqn#7^{(3(u{Vvvbb!;Q;^mL(0vt#H3ua&@%%18no@R zsR0m30t7HLjA6c#{>bG6<@bh=igMLb-MX3VE~=dht2EK_o?tTrAOIq~@eVux zvKT#S@qdsU2}>7c@nrwc6PiYIOabc9YN)NvS2gtwfItLFkewQ5T>_VqUE$;TZ!#SzYXJr`tXRCxk-q7xFDjcC{ODn{*#njeejeFKmYA0%6G0_78Zm5X5`UyMI|T=sJ$6W_5gFHu&P$$>kILuX~wammd_% zs`k({^CDNVAE`yn^crO^zf-%RL7kf7iG*f_r@2i$jO92Sd1J{zVL8F)v(tnd? zf$pCTe#0k|>3Jc!U6!7uPGx7AM9Ed&L&$+4%3Z(w8>-0-2aF;F9ek2Q(;i%gsHsn!e+XvH9+=j>HpFqiQ`hru^vMNl{qpt{@OU2>ghlzJh{epH(_5Sr!_nN^+YcrV3S0O z>mv;M!jym5=E=NC8jnOo#u(?EbEa?wfP|EuC)Q~CM*Hlu&pv+s0qGiRL?2$!FaQ7m M07*qoM6N<$f~?xM7ytkO delta 1525 zcmV)C$#@W)k^#WJ@i%y)Q3t{)CLMP5J3$wfq+9|dyU`rzNZJ{Xp`8n zLt>Ix`J84~GyaWce(yD~%PzY#hf*TTyw5AhGEXVl>g}{027g3dkI{w`HwxdXb2-E$M*{>p=AK#ghCmFQSQR(@1I)!Xa>#x*ez}TwfC$`EmHcm4O zAOIj8l1gdJ_~U<;uf~m($D@9BjvaWDW1VFLO&MNQplzHrvmgTI03PDoPy95S_{5}< z@<)S&6@Q()#yb&)DScBnvt0<*Y`a>24cAgqxC*q*iVAC8009WWiTAk=-?B%JXw>Hn z`5luu#UtUrCPk6OIUcMNv!Sdj`)oIjT@V3dpdRk%1Q)c4Iu|VK^x~X&;f817r@+|s z_|OCmL?~k*Ed}!JPOuhWPc)vRx(c+-qRHWLB7eQV;D1)6@r?iPhrU!nQo^3%l@y(v zH#OnlX(|N>SSRG8OR1;oYDJ7KAB1Ybc@t@P0fc}MgvZ5fk1T(yoX>{NP2^4wYRBYC z@=NWP^Vbx^?i&*V2MA+3`}||IL3H^THJgaK2C+Inupt`80LB0YPZviM*6l$d7fZ4~ z{C~Y6j11|6yr_G+t}iCyD$9=3)NQ9@^3fo`0HO9sTn?u?czG89hQJ<$*Ciui_Psg~ z3<`Tgswj@84to8-lJruNZbZSs(;hK=x%xZ0LeKGC6uo40#1W zc*H%unZB!d2{gk?3&q01oN;%g%dj*Hf`9+)dv5^fq>6xlDP5o?Su1&7{q}Ps5p+wm zDCENtt7n+&4?Ep_kH~Y2>EeSq;b9q%ijqYfUrq(k!OhXO($EFbKIMK|@tTIA2x*DD z_SpOS=#gh*^uL+Z)n^ZiWkGzRC^?M>AmXe6vdWz zhhpoumHIKjaagHnMlWXldj)EV{*Y1`5i3mN81u?Pu}oQkGc#5;RnDa=dVk*8{<`mG z#UVDr9UEHY1(wuLL#qdBYb-ZsqoR0?zT%X>cZT~keC!NAava0)e>gkYr6mO;3+C*o z;|JjC{#SiPu;w>b99-S60`9fJ<)3jHEm?y#dqD(LARG?|wq@#bNh@7;V)4R_dkIMf zp^O&^Y3Ywe+l#s`h`dnlpKe`#kuSiS=f_}Mc0QN4OiL6++u^_a;Tok>7I{hJRW)5I zl^lD={5`8_x>?oS!vKIN5CVGF=}*-4FKHe#2q7HDQA#Q0c*+CJb{xyHtF$a=mtA(* bWi|f;NN9b9+^W+F00000NkvXXu0mjf?1bNa diff --git a/plugins/christmas-countdown/test/golden/128x64/christmas-countdown.png b/plugins/christmas-countdown/test/golden/128x64/christmas-countdown.png index ee199ebe4fe3a97d3a36f45fc21577972be286a7..dfedaf0643b268a0bf2f002e4eeef2f9f0b82094 100644 GIT binary patch literal 3974 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 literal 4119 zcmV+y5a{oTP)gt}Jlbqr3k|;`~PJ1oe@)hlRt-b8^ZV&`UfCUm{!!wJ`jl< z+LX!fp;1k+`|tU``p>FvfL-ijCj)zYI|M}R92r9Zu$G8`=>21F#eD`KdU!8H6aqPC zoWFl00&B?_t2G&8tp#g$Ik*MAv4GC37lKL2Jx{nUcU>VR2O^c2I5u%?qDU)cw6?pZ z(2d?$0As)yx-L(rom|e%<=jk0Iu0==;@GrWs?mrWjW`GZfVAG#|No%}1Rw$c3c);2 zj*K-- z`8Q@Ro&kHd*;I{290W-x67)juX8}DSfDnOmbfrkAokBsL+%uJx|6ro)(&Z0$e|cy^ z&R@%4zA8OW2*I$&sqftl)|0ys0U`i`5Tder0`x;70*{KqG zBJi_=UJ*dng0(hDC`lsb-p3~=0nnC!y3j;x)Y?;7_OJtIJCqYW<2w}+&)gXsacdhgiENMEfmu>2ei z^Zf>{C>EO}xC`_@z1E^R5AYa36`-c!{f zv71@{=z{~#Jo#*)osVl`=t6ZWy-~zy7TO+Gy>jB2Vm%au|1O0#g z`d}(r{MDaM;(hySpMSyQSN{4x{!sBf+(_4jHmw;jUD)R`9++LfypVDDp?q?_fvt%GCwd%p!=dXh2N?Mb|k zUbr9v@I2!PhyPA3cMU>X3{PCNh8!>n}E7J$HAEIuIeL!y1v4E_Q|=b$vIDS?<_(4=(A z9xY)mqxTQM5#4&b;kE+q1PQu;;Wml|wKdhB2M}F_Shx3|-hv6UIRX%XfcY?V%E1`@ z#O^&pbqkII!og!MJ#>FEIt)D27taZFbCs~PbfY1J7EC082H5V=_yd^yP>W*o*c1Us zKmdWdc8)apy^{&ZPQl zN&nZ~nQN`pn=3A7`=5~65?uOM1!g<4Q0sC>Ta8T-z+Hq9VbcUaK})ywkcx-u*RKqv z)a6-zsm9yLbsJ=)X(GPxW`6wTFBtt}#TWZZES>--4XU|ayij57K?Xj)>#zb)+%7H% z0SJ(&-Ivlu+nfpG;o|hWLM6+r=;fLoAB>BWy0%2C3oaBWZu-veE}tAX5B<&`{oWhj zy>Vrwf5aL79L>J0kO6IjCHn4)cfA>Rj%1N^Y zGBAtZa{u$&E$vbM21XYE1zN+?MJ-%_Pc;xsulbx#1+YFovi4LL01mGW#I36Zo5v<% z06<~fam&z}wM^R4MdjZZNS%CW-^uA|Tsg|^sZm>awE4<={+x6xq{Oh0erlzHVO152 zW`2nwnUz`MK5mmR0BfPxSv$q9jI%SC?zO}X5`ci&L6J?f>RXAKgZ=~VKRsxpiz_dl zc_DhGIkwMh3Gcs_Qzwqq?%5L_2;%_ZX3Xl2H?)tM4XoCnWDTp?zAWLBJkd~_+7>

n$O zcTL_m{Y-_;!c8$RJzihZ#(}X(#x>3eKn`F(b^X5&@rhIb>$2_`L;yfKgvKSN>fkWl zmvRQBd;h@`w%nMFr&lAWx8q`;E6e;s!zt&SW_@6<%N`gUw#@?<7XPjtkuI=h@>+G1 z}?rYHS7!ddT-pkhg~bKAq<1WY-Gt$Dy+w3 zZqjaHGqlNwp7(;rSl}O|g==DQ8co$yGoGRI04`v+Au7RfZ^ zN9@E1Ei4J!awdvN2I=8LQDq>$))qriTvcLKBsI;>&AW-mTPAe( zJIo61i~wuN7~m;tEky_RW%CZlY$9zVluo&fd-mAS=L3!z9fXO!_NCL5_M68K=w`qw zO~_k))aa?2j1#@g{E(Bi)_{==!MZRaCN$Dojnli#n%zI zWpBj#-#=Web+_m4bUVdu6Mz5&nE_Erd3~AGMA}1<$>+nBR;2r7$x%gSi;RW^Gd?$t z|IWPg^rQ8l_2BIFYCF2}_Nse+h6*F*y-R5wC2b2jW+r5awzXghX^utqpcH4@Tlexz z_lCm{pw`uY#;%&H+Z$pRTkBe$zIBCQ150MNOF*Yca?`wj!fUSDsjAMUD#)XNQdwhX zge$A&xT9yMSSBd{(mkKMKK}f|)v@E_-?_Z}lb7CHF&;G}U7H7yi>2~dK&paD8J-0b zaj;}vG8qz*X>GTH((kNw;7kFK5jlWJ6Nt%_tY4(m3oH~_|vjlbyi5m z(kC&^xQS84Hi=BW>~hYwx*+~Y7qqb*xV5z{TpQjeaeD+H06{9p`bK3G8svlsT7f|# zL2iyQmzs-~X*)HNL=Y~&_0IDz|FD`3+a5GfT|dj}Rg2njQkVqb2odpiBUv`2gT(lQ zGTSFJCCne-VSNi<;{JaNt|I{FyK1hL+bGaJfDK%7doiO!Kp#KwML%CowdM>1red;a zfdBy$GMr;!lnY_@o^}rT{?x%{m7&fVv=FaQ!Zn2s0R;23XFvC1?n~n^cdnsBpHyfu zWtn8nt9EHBXs)W;e*U{2w-*Bl5TRW)b8p0W-0}JV9n>pi0<_u!5|`*?FzDvChFz&&6&7A6FWaSxW4ZN|fD#K@{v+a{3}c}R_jc5Lbk5gPO}B5ZvU zkzhOE4zeaLFq4(SW#dozV42K_=6s@JW7{M-(}o-XpKwn$H(KtWzIkKGDuo0taZYiJ z2mldd40#HTA;!om07N%Oi3?0Z{f55!ViOE7hIhehG(E!Y62KVxX~8(t+V&li<$Jx{ zq_cW4k{-7Ks-`g!83VrLSr<_mMHmaxg7F;V1^_?>MmS6mr<753iUOS(W$A(p>Piu` z7wpREs2wCpw6l7mw@X0h8%&*v$v6%_l41f7kjLB-h!h|qdXOJ;DYT9NV@M7NgQ!Sm z`m{q3&TGqUN;=hrgcPw5LrfB7t1~e;Xk%*3svXvb0N70bmkI3O?}hW0Ec4T@-!Id9 zSb7W*KsrpbBrVZh^i?L3Wf8PZbH*kOCep}^U7br%Lw%(kwiS_8?YrQ`u`R;J7v;%7 zQdw6o}=IY)~N>$ zIzc-U5;&Kh@A>JJln!GYXlM1GKY-0`&ILqdoO8w)A_D-GMC~Z_9YCg^{gtnTG5zr$ zo_+cYhlJxA197Z;&*7fK7~`C;UARlYMnObGWQ@^TCrN^c#u(1j~47$=hMN)XCESR&K<|`JkRqyDJ5f!h_u$m zB#|<$W`GP`=|(E?q?600I_A>hNhuXYQ5?rflI-NqM{LUcJD$W@gAjr-2GIE^A~8g= zWPv#69a)`V+p!p>RFWhe?OVIEa{q2@iU34J#10KYh>lP1xb==_U-$1sw66UQwALLj zw3D?DJ=q7+;h=L|A9NG2?p`{sW?k>Q0RVcq5vP0Be_;B<8VBpg?$cfDVi!9Y{|lRx Vc{Jl~8nyrc002ovPDHLkV1lu#?-~FA diff --git a/plugins/christmas-countdown/test/golden/128x96/christmas-countdown.png b/plugins/christmas-countdown/test/golden/128x96/christmas-countdown.png index 94d679af4c5eb495dd492b349c95b1d1ec4afece..aa0691d56ac0dd954ddb8333af31b39bb44fc945 100644 GIT binary patch literal 4053 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 Hu0mjfTtf0J literal 4202 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 S*)CQ90000&RLF delta 1590 zcmV-62Fdx!3&sqPBYy@BNkl2qf|2{#JLad#$rL_q=BS4u`|xaMpuTq9~%@Cn<_ZDSz2%aX32+1EQ|SXw8j{ zj<}&sqR`o?^!BR3pg)@}EG`yOsiJ8*TP+-?F^K_X6Bq@?ffSJ3VTm?R{R=<{6h%C6 zpyTw%@1A(u|MSGd-+sM!<3>D_DKWMU5^@@jA=mmUK$_VD2pid?D8l~zyFdTXdvwo* zFHSfkBVkpQ2!G*$Q2SC2fkuH4z<`6m#7>K}IqG`=#sH9f-Yj?=%OjOCMbjYwW8D6? zD=Q%ofG~hcEYyr_$OGi_*1Wd-`~B=i;nbZDGBdM~$=F*h8K+?h0SsV(w$qZlIQ1_8 zAt)Bjys=DukNe~cOQkqFYBQn4;Iadpb|MTAfK<#sfPc}Q6z7H5kOxp@oIX|B9ei-t zc2F1}9_$wO5-{*ApncFPR==$D?lNc$K;^ONJVTueKnNH^s9&1ujm&%%@&5SEq%y&v zkBoQ#fM)@1L%L%1FI94Y(KW@N*c#8!_|)?N1OPk-JozN119N{CF3%YWuUGr*4BPjn zz_=6OxPKU0(;#bn)~x~&FbD7w|6UTH>G&rmjTAl_AS~xB>!K53m@I9VoSX(CRPp`g z{?|~YBnnr9tXZ;x^)3Jb2*I)Ug%97dhY#u0?+gYUlQ_9UeSb~JGMf`bFkTI6Khm!1 zXR}i~1R`Jz)GHhw7eY4CWqXyT^B>!EEz2w;Fzc_glYr@UyL3jl`TE>$dfjJVnJYJVsknw&``1kY4b92*D* zE#K{Y8hLx~F7w|oO4`$o0jIS|17W}d(tkTG$;+Z<2;^1B;UjX`Cjrs};qmq4ZPiDh z8$O!NWfx|R+ru4(rCX2!r{8-60PhI24`NgGde^6l2YA~3?G=V2==9K>wA>f5x`u?_ zKBrUcl0{)LnY%YD-7nxl&SMeBpV0vD;ZRO9QZodiJ?h=0>N5>Pm69Iv+C$&#BY%gU zjMD$66PKUd%M~Q~k*a3wRI&6}%E>AJ2>XCf|e7!B=y&zRC z0Ammp;tPrKrIO7c5E^4*|3#x~-=6P3m`r3J=5;y0U>@rc^>Y1-$PeO-nzZ}T*u zRauE{YZXA7H~g)Z0}wbCOx+3gOMj9o5YbVyR_wU<+~DMFd_J3_#bw9RrK#k@TMKgz zB-+ggg+K_`{$^)olINLnr{5hrnS_EUEOE zWTe3yuKuk?+BNuqN@}aEQGeX3Zw0u|hE~5uQ|rhMvhFMp0hNd#!hvm>rP(DbS#V-= z*=u*=9tDIlQ6^+V9~R;?6IDYXvZ=s5z4{_GAnWQcgKgQ%nPtngWLdTy@w*?cP)Ze9 z^vI&7CG+{bWAo2rwFq^~on^~?5C9Mb!a(K`#&}rIZRH6#-^D oj%C?pwn0P=hr{7;IGmTw{}$4Hh1vA{m;e9(07*qoM6N<$g8b_OXaE2J diff --git a/plugins/christmas-countdown/test/golden/64x32/christmas-countdown.png b/plugins/christmas-countdown/test/golden/64x32/christmas-countdown.png index a3d4d442a726dc0be489f4b770b710bda84e7222..90c6fafab84d0cded2ab234c64ab5897006e5a8e 100644 GIT binary patch delta 1315 zcmV+;1>E|h3$+T6BYy<^NklpD6sdx+sJM+<=$1HXQpd3!$NOye9v%Y4>3A7CsgO`V z`C&Z1qxrsjmhW5xJi&hp47iSSR8_8U^aQ!C5Q6rX2kmA;EHVss^ytBl&V|k#y7bALa&9hVnkr(@^a}#=T?f?AKLSJmfQ^RR)TO7Z zcBSP?1R&b4{eRz`@#wqL8-XIwXn5i5gF%k2EEI4&*aDitvV(H@`yQ~x3Q3qXWc z%WXJS9)2*PUD~wsbVd-f34-^qey{lF(|#u)0!6^o;KF(PU@UuENNCQ_&Wy5Y1cJR( z0r~R&kuF4?mif^~03r}UVp3fkPcMC*4qbdHV=NF@bANOCzb_dU*v7KsR*)|;e)KD1 z1R&6l>5UTHiLO{g;QZ(*0D&Yx2qV)N<=goWT%K;e zGlf)_RZEd+f{eXiT-!GR+hWq!6M6|i07Q7@4R-E%F?-VD5jhi;F3S4Ji60jTmoe&2|0BXx}+MV$a zOSb)DNC89u1fe+NdGGp^TDiMPKSW1UFgs&UDViOM|F)4|E7JC9Ubrsz(og;63xkwI z_ZlFG9WWJJck=Go)02r*BC}L3FnzJ4oSursyztG&HZ;Wa0r&S5*hP4({j>+V) zQ-3dLsjv#5X4!+QrJH6L!E(a9RquHu2v_~Uh+5_pO7O;Qd8JQseW%oU2a0Qu6rmWl_CJ_<8V3Clpu!EeeBTOAy_+7t zHVEJYoqyd)25w;VpvpZhnlj_F8Ym8PM0m`oZ%L@JR@vJwG%KZ+U6mw>{A*JVu9UKfn{R#eG Z_zOF%QAKqu$f5uM002ovPDHLkV1nYycBB9R delta 1426 zcmV;D1#SAZ3Zn~INklLO4lqU(Meun=MG+VyJ%2w3qOQki!+(Q2A+&W!p#A;I@UR*P z_zH#c#zr}lshFnIsh5oq1F9*B0y97cNVk2rbpV7wQG_E$`YwI+-r0A2Kh3WE`pc7h z_u{#n&Ux29Z^O9Dz5|d>DnQ#gq$u?8;r*YT^PDq$-7`fFQtu zqrhx?7h6|=bAJG&R5Hu5W67ePtC$V}IPX>dT@(Tk0#M^pJ7}u{rINLl%Kx@hxIr&G z>LZJb>$#lWg`@LQ9PA8gRSORD}q*19*t|IV(wgl?vd!P@}9% zJv&8X3K4J)=AkEMXwW99RDYJ(<8}XwB3sS)|61}T3$hya6mKTk?0=f6iHFZKIY7X>(bFyIOu1HxsqIFn z*4)iJZ1e*mAOzuQDLbGj->6rzq02M5^P}2HrIPqU`}xXk)v$-A#K0lKd1wB|YDRRs zjap5jE+baw1>4ZD2RH{fJbjeKt^1>5E|%cG`v*gq7}G}uN%!>K*@&;Ecy^j4?>k-V zX@3ylfLMDZF2Yoe?gIcvV4o`JvJp3jj*kRG!MVjulFIXGVR|$Wu)GgTS(NO9`^j<0T_A%#2&IdM`7c||~cN}u0NKUBR0n&D-IVqtyRcsS8#SegaV zf9d@<0qkWW2wiPKTX%P2bNUX`i3s{-R(}-p;fOUbPKU!zzc3&PbR%7SvMer@@w6yg z#PQ`)0D8Ebc7%o$VngcVwCXhtLlx69d40wE#^kXVQ|!NmeYj*o>u*J4U0@^+u>-7MEW5Lv%z=$ZSln9PmuwrLC zSI6d-<7g>7s#;);i%h5N~|U-7s}gUq-7A z*&5F++o-5sW2iXiA6%fHgil@&CQe~|^>1n?`m}^-WWk+o^v8A?S=U+t*MFK+-SFyu zDr}*}I?V(^1vs!RQ(sP4>9Uj06y}!-vI4@mAQ2(}Ru}$nHeDPXWEg<9(sA%s%O7-NhIOaQp;IF@Bs g(OY+4;U&cXs-s-zTWUcM01E&B07*qoM6N<$f^Gb;mjD0& diff --git a/plugins/christmas-countdown/test/golden/64x64/christmas-countdown.png b/plugins/christmas-countdown/test/golden/64x64/christmas-countdown.png index 3de672e8f14ffe095eeb34c331c86fd56e459be4..84992bc51bcae0a325477f84c60a0672a46454a1 100644 GIT binary patch delta 1353 zcmV-P1-AOr3)~8jB!44GL_t(|ob8!gZyQAv$IqGBtM75#G;W=wX`86!CbfVX5EM|U zP!Rzn9(aNW9(m%^AR+M;`b>pD6sdx6QE?l!&|6}sNgc;_9N+ggGd!e%)bV9q$Jj{w zd$88t@$UKWnRA(00FU8n44imP`7!w0E%RR0HNPiL;8`CBy^-w5Ksnj<& z>&0Tjw)voqbA!}%1R`jfbmByK@%>jXyd3y;Y3=*(9^AN*EtO0$0&XB~25A~Oe*Eyq z7k%fBT>10@pP!HEx{T;{`VExs0)zkntkvwg${kfUOAVVNfDohb{9SsWR&zG;wjm#XhpHj$;cjhcxHe1ZF*qKB zxmj~sQcPd?_s#5jUhJHuxtqNId@A-GyYy@O7=I8DI;2acYGv(-XQv{uNOGl^BkFQN zIy)T-IsV(V9jLLHL-rprF!Rt)KHY08_F_GBhykC5XI@fbei=Yckw@1Hw{<^)VfksL zTFIoX+jC*dF&vPCi?6=`0DYEa@w8R3g$Ev2-$^o;fH9R;<#IgXL{E~*I3JUuibOUG z)qe+Rd9@Cys_GyQl=1-3Pjk9oy8DOGb_&r6{cb_`+m@xv1r?uL^FKd-dixap^Iq$M{-rssnr-xMQ9;F2#_@NN2KhQ$pjFj_k}cd)ruZF^3~hPTxG3h zD7B1zK7JGu;@Z|de?aj^Eo*}frLAb?0e=MUfNk*Tlvd3#?r^;#?w|L6O_S;D2IJJM zbH|XE3u}ZUr!pNENP!^oUd-QLx!}nN-kVQ5%&1lZ1fYP;>kVz3>a*0y2xf?^ZCOgC zUXmb4Kw|1R=1m(ZpeAy+?BYYiz3Kmet!!-)*)^C-R4jsjiR%YtCtE5v&;qWdFqH(0uIOnkJrJ#3{gt6XjJ!G6V)V}2^Zg7I787~ z19thhR3sEOa<)}zh=W)b?7hTZFXFpOHQX504DRe*L;3s3&i)ynP_ z0}%-!lu}BGL?r;hICmVj%Tt){;n63e%_r<>>BPQM?Qpd(G*FrnXwyMghA*H`hA*Jc z-M!%p=#%a*I1U^)MR)%a8Af6&*~8=D=upn!$xVvR8y2IIg_^8yYD+tW??TW7xLg3> z|HT7M&$wlO(UPGF=)7P*fQ_vYVFcd7d3bnuczAetczAet42Zu0?WIgcW_+ni00000 LNkvXXu0mjff@6yW delta 1473 zcmV;y1wQ)R3eyXaB!8MoL_t(|ob8$4Z&PI$$G`7+&pAD(UAt~8utCQdY-0p9VQQF0 zi3W+mh?;oem0o!1m4AYNg^3CO0(T}5qY)FLQxOLXSfFg{*yy^p>+kmbe!M7MTIkl6 zx^*V!bM-cPyFK6YexK)gpHsl4x3D(%an2-(i=0_DnWk;qw12B%wNG0DL{S(RP=<%q zK%lQsD6g-VGntBMI=dZQJBTfJFa|{tPn_t#^wFE=U+?>2Zsn&RAKkv4$mMj}2ge|^ zvq@2e6In*^QEehu42 z+ByMM#-)q;p}_J3+d+ADa(qY_V<7B@ENCHsF%Tg*DlH6$7rzdBue>p@%n`JyDGvbb zgDhxOMj1fxobZ&=c<5ivm(#1p<`FOd^kd2_3Cr_NRexa**--!kIe?e-jWIvZCO$EF zxctFUMnxyDiB6a>ukTfy+p?1Y1~7)V-W4vqYEPchc%L)wcTDCKPmTOKC(E=dh@f6q zqwRBmfe73yoS7AZHcQ2VWu0G*$(R4~#=j3tFU*Y3@<5mqfwff#T0lUg?qg)zjnC5J zp9?a75P$b?7Wrz%|Jzbuvfxoiyu}+yKDTCS(((7XC%{16*3&4!fN3BC1OWmLDHq+KU;INMOpfbEMOpXu-+x?BtfnYC!;^QM?&~Q5_kn33RV&VO zz-%`HKnNUAMcrd0%!8*#gQ4L3VkRkg7SiI((LliR-795Lvd0dXe}|x_&`mv!)_JgM zo=AJqV}~5@D(uW_a>yqE(z5X6R{EamW6%sAFBA*$m~n5i->@_blK;}%uK?(#Ee8Pt z2!BX*{@o@g@U;653zK0Ccz99DkA$tk31N7|84w3$QCLqGAH}4lGM0-Ti#dI{6o76n zr_ZPCbTPWmpX?hn zj5WI_*NSF2z~ERgH777CNvgm^M=e;nQqE&G%LR)HkvyfZE5FxzWr*1V&iDRZ)hxI?lB?*%~EG!;lN*oCv=F(e@0nlz(nA z4yI7@u91f)qre`axr^wQ1uxHi}Utaf99RpGo0xwu&?~N z91QepNz*7)sOvTstTnH6E$*$OXn%S#tpz-Vfs0HK;lQ>`J+@(`%T7L1SXe4}6fjPr z%$NvRUB=&Fr%q|%F~*oI%U-WnmSx*Pp;Y0VE3)X3MNQL6rIKlyFV+E4zz_i705Dg( z4sAW@NbOoN)XGAw+b3g85CqOS=Yq&ZfNaOHEW64Jgzn diff --git a/plugins/christmas-countdown/test/golden/96x48/christmas-countdown.png b/plugins/christmas-countdown/test/golden/96x48/christmas-countdown.png index 118927741406f427741d7e6c40eb11468d503782..92088691577126ddbd84581cc7087b44f71613cb 100644 GIT binary patch delta 2520 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 X7H9fLKqs`_00000NkvXXu0mjf|8rDz literal 4663 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 diff --git a/plugins/geochron/test/golden/128x64/geochron.png b/plugins/geochron/test/golden/128x64/geochron.png index d52c3d2664c7d67d5e71f08ce08ed7d86c8a7bea..aaecf954ed116e869b0a610f93a82ab0219c1873 100644 GIT binary patch literal 8545 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 bnehJwKIqwrS%l0T00000NkvXXu0mjfR>XeL literal 8781 zcmV-TBC_3yP)aL5jYuA8U>+q3%_Rj|t+N&R=<3XKtP>o!+ zsbuyOqk`ZKfyk&rtedcSAHg;P5hOrlfK$ zr$N5_Tpv8a?1ZWB^jSPXmr80J>Lo=m1fZL8z!VPXM0yeF+{5=keK3)#p+F z1kIicof%`$U%zhWP~-!I()8jJ+m{w@z3onZ-YhoVE8DR}Z8;#IJEnVT+9 zmrgT)#iumw(%>eI*3`xTRG2gnKuHGxR^FMi5tg3eQ@^`Dy4ha2Wa8)?1;01}`~ zWYk8ginvYfIqxeCMI>Q|pVS!G1Gwt;vAEG|6@pS@nk7u;6d46U1yEv--l8mhVT2n< zdfIk0Uc-r}?B-S6e2<>~PF@aL-Nm{$XRrw=lbPwIYOOcCPQKJDKMObBPvZ7z2%R~# zT7G&iIddy;WsKcbrn^)EqG+{~mYpld8h6AxQaM0vIAHZ7w{iJ|i1><)F*LE-yY`9* zuo0k$+Ir#(TX9ct8OG@L9pakODGdb)+fH9wb9L0=?zwe*l+6F$Wo1(ck8gStVg9SO z<>t0$-QczW=**h-d{qwJ=(ZJxi)rzxFnflsykQJE06vGYhKw2~svv4sM>19?0^pjg zQy8d#J=%S0%>>s9_1FRE%z;azfX_(XR`v9F z&9$f5%Xt0c`8mo8T`4zxZ+`g1pJMAulCQS%)pUEsfEd7yf0=r!Z@Z^ghFkfbIZLWa zMgSyjI(7Qw`~R&sTn$ljF!vkox>S6rE~4or8};hmf*?Z_IbT8*WLXp{3b7}jFWZat zwI7~1v2bVgqbTXp>2Kb2ku&9VZMIyh)E;^tyjNh+dk+E###t4VnO4}eOB-{i1aB{# z{^qA}nU(I~!zGGq38czdJ?cDRI9S}UbLSr>c!`> z;nh~U%)_jndio|lj*8V@=iKIvH|FDx%ssnB{nf=UbvE8PvG!2}fTFN=DS2#Z+So!_ zK~Pn)>>N?u1E5<=AN;br`YRYjTnkyb-?oxbVQ^^{pVX6I$kVw(OjXw<{<(MdTQ|9~ z)zGcKH#@yVs#KSrS;tpry2}|n&i~o97+uq=s=P}3QOUQ<;4m^EA~FDoh!mu1l#RlO z8Vyu}L;)n*1Bf9SoIE#BD3LP5-$o}UO#z%b{gsc{uyQLD9mIjF$VCQ8&P9wXkpuA9 zAFWtzUwgA3b*t&evS5$%Sl+w%!-0HnphX8BOyp)tTw6lvBg z*V`MHrk78xe*AZ{C!WUV{&>6gRTM?n|Ecfmdt{&juC0sd%!{Mxr`PjpP^|Y8ozKno z3_zV>%bu7%5tg3Z&PGMlscT=Ut5hfg@Xi9;K|w+%d*l`q%f+rUnPZT_vwgM*{fRF#j($u1+ zJu_T!*_I@;u#O~vtN^fyil}kQ9=;%J4Vh+5+3`Zvv3O7oB?1Wo*953_h6ZNH+WHuaP&-U#dgkhn68!kB_tW5Z=eM1dXVwoGnBcUX-Ru$OQNDe=UyD!lb<%-DB%2_5$1@8hwP*C%Kij(e_j6zDufBPCVfKk?@o~%T)lkdz z|8q9!$y49TT(?So#Gig{#aZ{kFI%7ga(+LVXD`iPeR>T58AB}2bIKaC+8~ydg1EuC z(E3zal~sZ=tFSUdSVeSx`k6Z$AL z*`OrWE}wD^K*y90ViAw)c?J#}SJ|s| zhesNUK;zc=G2UK0_s6%wpgZ{0bTqdW*pL{eLRCQ}&^q;dgS8KO>|rAXf?_vaanERz zSxyj7{d3j1uV}e#?!=$Hg0`@#cTsho&DT+`FTcrdpzOQxKbvEXSS<^|V zZU}J)!Sg5oyLb9)C%Jd49LwJKxsPk)a{!R`s%ve}L?Y1`OdwTNYvQP-02VHctBtAE zPc-gKmiMDU1?_2Uuj0~;A%Gv``B%PEw@+={zO~V5cjhj{SvlOwY0Av_PynqoZGHI< z@?ZXhc4t7Ew8)U|__HAdt-Y;02^of|#flYXUvj9}Sroa{wfb6%lFLe}T0;;PhycL$ zSoVq&sEKN0Y$%A70IZGj{1&B&qlzO}Mb0{Ft+B?si3y5O6Db)bKm#OB0O);c*aQ*d zb>a}#Kb(H-#i%p4jZY>Tv=7-BLqs7207g^hU>C28Hwq<~ZaN>?q_{l;tcLB*@>d5{ z)*r8{bq`3PsKUm&@f%{$J- zFaFVVr=soA_RLIQDU7!ZYn_S!;HqxboOrc9XwBkP0>ncC`O2V;;efM3@*;Q-o^YFp zV9edHk%(xL3siuVi7dEQ07V_j4H#o$$F10=u}dQtIUA`=1VG}QQ6hzbI7l#s#D}qf zEFyHoA2xT>lSoCHKkrz`7)hd;+*HBYYge>FfHtwv|K1^Pb?&k0AQBYOgl|d8KE9)L) zh)Ut5^dYH2D{-ATN@L!MgxOob8bm~?A+aW?sr3*8-()ZnGKnvW9B7Li9Gj{tGXofp z$NhfaOm`vp#ZU0nm;J3@%v+ido@an;!;DwSI5iJae>8bFkKQ6JUKnlPc1hv@AJt2n zm!@ndGwt#4%Bk5WO{ZHav9$)k&3{^)Uc&ZPefsw~?Qi>0y871q$=_Y+^}50YV9G=h zNb1sw5k~MDY8np9(TGQxJboI1vBTTDx}~anUz`X8Y9bN{C}9KQdVf+4KpD7ZsXX^M znh2owJ_an{3@MQq6RhQ6<^Fu`E^`G$WFC}DZKyWJ6h#5WJ3oy8{OpIOx6-b1CKEmN zdAJCORQ1s8?!I>rruJNzeP&Z;hj0#87cT$Z$#j|*pG*ep)rn`vTc7l2K402;#v%a* zbHDcq4ENS&%c7p0?Jg`P`R%Q>i}O$ZDQBAlAhva&I!v{?T<~_8m8EVE>%n#yjWw|D zUF3F+%-$JKFd!uuu8goY*&49n`p{QU)w~XY1%LriRCVn=vnL4-J|W*Sbz=$zT12epuUVEDXxVBF^=smISrm|4W=H|e4Dsr3+i&O-F^OBml8duqN zS(Q52DmS+La3n$Y=acubesa@W4Hi%dstV&!eDMhm56~w9JnyUlqrwvzA|f(|$dQSS zXc zZUBtRdR*4E_kn{-Fd8BXiSdc?M=-)f+P--fz?Z(*@AqetBx$$XaUAbxiJB<5gaJgU zx>)M#XFvO2Cw-|PdMK6O3uH7DcJ|34J%3r^ft%d~P?ua4!KQrT_ixWXQ}kw|Q(xO~ zvs>|04d9dE6>{94>#|5yRREG!RF|bIz4HQ~&PuK&1R8DEgCTE^oBfnylVqWcP?B+w zfcO_z034J1|Nc`EKgcJ(|10hGZ=05b!2$T;kKa?E6T6vi+HECq6qAhurJ1BpoaQ$! z&mS=vUpl}3=glp4|(NlA4GyNOcEjl0Omp7iLrX?U0V&K z;>tOLL{+M)&Q=|{+C&T>?T6U4C2t2Gd_nB-2nzraRL$~N#+_MRUm1#Ux7&^57=VZ{ zv$b}*KhjVCoUxby+y5aWLq--On&k?omxWDJ0Nt+e(*O(JcI)}GnhFShMR>CRR|%^ z^JbJZP225uUDsKbMNwp(ZC0ZwiU33=Z7mIol`e5hDF8|ZRM%Z=o1!s*A`8QzA8sm% z!M)BtS7z7wnQ&i;#F1JqZPO~@*z5H{a902X_#j#7>%jRa)OoPP01^`(I6e_n2*hje zPla1&thA<{zJ2~Xcb@!{H2}lw(b}hByfUC9~tri>JH2b{eO>$hKW1 zW1HRR0H`J8-iM%En;rEA)NpCV1AwwIW@i$B#6vtOPZa-w!O+ogPj3bCI<)JMM7A0U zxkdm!<5=)u$ABz~Azl5^Tzht=dos6am^sG)p8S)|^$({8H+Aib)&;IzG%iNey2lK4 zUipjc2Y=IfHH6GG^8Isl5IXwtz<9` z;vany+#>)20!vZ*R_r1h3><*igvf9y<`{{}Mjb*vplqW9rOi*m?=5f4oF5ObCDnF2 zYDv3~xwAChz-X1CNy15+W%*FpF^}hTZK}yEKJ}#&IknjUfsl z>|~o;&P7|BTPI%h7%px6v^9%>?L3r~QUH+GL7JFLp-d)*)U?a}X7LvwvQ|XZ2pPd1 zKb44VtbC}){xAY)9Q>l@I&dOvlmSE(5)&#pG$^~bKL9cgr6wK0>mdb1JUf-kI%zvy)P`(bM(YR|`i|2}dG8(=c`!R>W04mSBC4LgfI7?=~MmNQDp(58d zyBVHI*^8)gcw_=!4J^R7`OaUQ7hbHC}F4nb=<7&GPeN2R^&ehE^<6`+)m$4yp(x?Y&O~8|$P#p>aI{o2ZEt0N0QQyD5jlK}##4 z@>2Q0lXM+N#4-6teux5O%<>Dx%=s}%WUYzf$VHAN03b2>xTe`V3y&j|QK+c#Jr2|< zRYa+d*Xim{ll5P@mG@%+WhS5gs7G~YYPu6g5i^g+&J;6?v1u}>_3MaytWacewv@_80(z?6w?>rUpZyzS9h<$fe(N5gL zRO|e+=QeK)gGn=KZ}@H@(LQ}SR(<`i=AAQ7|CbGKHp{%8o1d$yYBU;IYtMW*w_8-9 z5F5eSNQJ7E+4Xnlif#S+lgG$aT$z`E-U5C8KMzWu4F}teXEiC>czPWMopx{QA7|Z0 z)=P4PvaYLPR<4bTum8cbN2Ch&hQpyT zW?wW+uu|J94^F7kGcmG*6H?bv;M1ywd{7fy=^F>1t}RK0(>&Kd{DmPh-Vlb z-mq0!XL-iV-EOzlYBh<9PJ61=;+ch-L;z-%5V-RE&{=T;*9axJhU!i*KxFLB=^%nC zxH4MXP*`n}ID%$XJFtr9(D+srmFH#=Bd`U>COkMVbnsZP7F90U)dkn)?corhx~`iz zb|05j;S#BDEU$muGBF=M=mAe52UUa+M8sO#!~r4rR+5^!PJ3e`3ej{rc#%3BvE_O= z?3{kl0GXUmQT7v0$+pBskN_AM0#`fU6hTy zrN*t$Ud$;ieGryo6d@e}v~$AA!nMS<7?xE9MvWmdV&a1ZZ0?;0hYElQjfoTy6(O)A zPKhbf%}K z0Mx#!)}{Dxd2}sqO;^+&{C7_9sY!N@ zL+yZZchg9X$c_>~gArIn1j-f?5ho@*aEs;V%^s^^cQs?n-F zJeK>aC5}uo6B+?9b7O@pabmoCw551b5<_uYqlB_7Z?gGu97irDtCl;xJLdtkZ=HSU z)QZ9wtuut}j*BUd66ai1dI0fwX7FCe8BgGB&5iH0>Y7;|M!W|;@Yn;GSnoL~xNlqY zKO*7ZE*u;Q@50O9xbVxLe=^%mTS**6iI}9)WW=ql&HYuUgX zOJ^_5uYb8PX+3!Vd4Nfl(3>^*z;#dA-kZ(TWT+w$daTzK$) zT3~6G0o92WicLt3A9M1+zWYEKaT~40u{P&|l>lfJuf2U@{gc>4`?>$nphQ--KAMTI zPvz?g*LvdAdYaTmg_K2wMLY{XR3Y$Tj{-Cp09@L;>Dxdy0!7l)z8B9u1)w~rNXtfO za(NHlXbdc&DG+EvVsJzuHQv#~y$2CEP-oGnb46XULaikLgU{M`-f5{GaQ5*LfQ$}r zxYbL^cvDe&01j-xP!SPk4g70I01fCQ&e}a(-{?5+W+T+J-u)GT_-FSOX(@!^T6!!2 zuxKL#m1fU$5)h>3;S8O4@I!5rXA}{ z1fx`UZvVUwz_^F=IFIodK#`D7jc@id2;6X=S^RIr03C;lLyC^c!nIUILqW$9fT{)- zp2&cqY$lT+wCzLegoc&9@Tut&U?#(yfj1HGV(^}P7Ii%YC&Fc@K^nwpC($OaZW@&Fr1npTj1AaZrA^Liq>7K@*#+d%lNqm-iPhUIcLc z`t>Z!vMjrP{Vr>>ENfbigRu7+z`O6hyE}f<@qSF6Nu@>z1iw3bwJ3%8mLn zYhaZ_LNE!xY3?6R;$Fc_+|Vq`Ha9l`3({Rj27}$}eP{qWolY~z{$aoKSHF>$Cuql3ZCl4CI0^=hi4&H+_UB)R&Bukw4Cr(^ z8ygz{`u+Z1C4KkZcb|X$`Mn+XBJ67S+}zw-Z@qQ@^>Lt)$FC2Tq-(2=ty5FSC*T;m5 z7cYL{+kbUn>ipQVk10eH90f;#lu3gM&z+gOb@^89L+LrMd{+9f zYlH|;y!_2WQV}~7<>hZ)_~DOs5bpbU*gdWH13mbBz}+=qSE={v`}5B~->kX-Ha9oV zoH?@(p~2aE=jhg5*n8hRlov#Rsv(VtV-o;$sc=ncPn7EhV-Jgx)U`QZh1+kUbhM2f1pzozVPQBJ@Z6Fn* z5NZvzkP`r=Q`-=bDJdIbG~ZroGT0FSn3RbCG0izIST&Oqx|8+7CSjK<+A|HBSxeFW z>aN{uqXfXF3Wds~rg4%u+CjQ&g8uez|Mu3cTYveNf4T2^@6Xp?f4$f1H3gU7{qA@7 zvgwHK2jaZ;+H1QAb|4_DjklvZ zIbpACCj0ff0{H&-zyIp1uQq(K*1r1ctEWz#dj0j+zx&`NUc5k}mdA^Ik zR|b1=dcEFgG}>*Ni^d8llM0a$YXMU5I%MA@Aar%NQDyPI7js#0qzQ2RY`+sY*zyl0etU!-`m@z*X#ZD zU;lN(qrFY-cKa~x-tF?e?|tuqIIq6?>T9pPwtIidW-1i-Ih_vGwyU-c6zy3}dI4yk zdgeA+CK4FWm1u5A(Kz%XumG1(5!v~Y53`1#9MlH|(0pBndA>Vcpm+NHU(h~XgY9KN6AcvUCD5XC|tqY;Zj6|y535tYVF z0kzjgHva}dVYE5uvFmV9``^G3@|RwE>9w*jDlx|$h)W}00000NkvXXu0mjf D;?v}s diff --git a/plugins/geochron/test/golden/128x96/geochron.png b/plugins/geochron/test/golden/128x96/geochron.png index 4219cd0e1185e9d0db062deef153e251cd2a2844..fddf693735333567fc52bc7d19399c6851f36b28 100644 GIT binary patch delta 12232 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⪚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 LMNUKnu0mjf`W+@> diff --git a/plugins/geochron/test/golden/256x32/geochron.png b/plugins/geochron/test/golden/256x32/geochron.png index 0962c99ba83648b46894d6dd5a774d51cfa69749..8d5f489b77f6cd5eaf8dea56e44305746573e7a2 100644 GIT binary patch literal 7368 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 Wy`ql%S$GBj0000BJ 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 P00000NkvXXu0mjfoZ!^U diff --git a/plugins/geochron/test/golden/64x64/geochron.png b/plugins/geochron/test/golden/64x64/geochron.png index b31462963bf5b55fb024904fa4aa9340a191ac71..20fcd8e346101ea42ebb07cddcf0277a8b09b6a6 100644 GIT binary patch literal 4905 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 b;dlQPEU+B2qkLow00000NkvXXu0mjfhWwqH literal 5203 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 N002ovPDHLkV1f+F9Yp{D diff --git a/plugins/geochron/test/golden/96x48/geochron.png b/plugins/geochron/test/golden/96x48/geochron.png new file mode 100644 index 0000000000000000000000000000000000000000..59e27efe58670c6d65f67d6f72ce5e303440b671 GIT binary patch literal 5111 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 bool: """True if this file is a placeholder stale enough to retry the real logo. diff --git a/plugins/lacrosse-scoreboard/game_renderer.py b/plugins/lacrosse-scoreboard/game_renderer.py index 1ddbe53f..c9ab9f94 100644 --- a/plugins/lacrosse-scoreboard/game_renderer.py +++ b/plugins/lacrosse-scoreboard/game_renderer.py @@ -72,6 +72,20 @@ def _resolve_font_path(path: str) -> str: RESAMPLE_FILTER = Image.LANCZOS +def _bdf_pixel_size(path): + """The pixel size a .bdf font declares, or None if it does not.""" + try: + with open(path, "r", encoding="latin-1") as handle: + for line in handle: + if line.startswith("PIXEL_SIZE"): + return int(line.split()[1]) + if line.startswith("CHARS"): + break # past the header; no point reading the glyphs + except (OSError, ValueError, IndexError): + return None + return None + + class GameRenderer(SportsGameRendererMixin): """ Renders individual game cards as PIL Images for display. diff --git a/plugins/lacrosse-scoreboard/sports.py b/plugins/lacrosse-scoreboard/sports.py index dcda7a1b..a13abc39 100644 --- a/plugins/lacrosse-scoreboard/sports.py +++ b/plugins/lacrosse-scoreboard/sports.py @@ -122,6 +122,20 @@ def _clamp_seconds(value: Any, fallback: int, low: int = 5, return max(low, min(high, seconds)) +def _bdf_pixel_size(path): + """The pixel size a .bdf font declares, or None if it does not.""" + try: + with open(path, "r", encoding="latin-1") as handle: + for line in handle: + if line.startswith("PIXEL_SIZE"): + return int(line.split()[1]) + if line.startswith("CHARS"): + break # past the header; no point reading the glyphs + except (OSError, ValueError, IndexError): + return None + return None + + def _logo_needs_refresh(logo_file) -> bool: """True if this file is a placeholder stale enough to retry the real logo. diff --git a/plugins/olympics/renderers/countdown_renderer.py b/plugins/olympics/renderers/countdown_renderer.py index 61121bda..329016dd 100644 --- a/plugins/olympics/renderers/countdown_renderer.py +++ b/plugins/olympics/renderers/countdown_renderer.py @@ -178,8 +178,12 @@ def _measure(self, text: str) -> int: return int(dm.get_text_width(text)) if hasattr(dm, "draw") and hasattr(dm.draw, "textlength"): return int(dm.draw.textlength(text, font=getattr(dm, "regular_font", None))) - except Exception: - pass + except (AttributeError, TypeError, ValueError, OSError) as err: + # An unmeasurable font is not an error here -- 0 means "no idea", + # and _fit_lines only shortens text when it knows it must. Narrowed + # from a bare `except Exception: pass` so a real fault in the + # display manager surfaces instead of silently disabling fitting. + logger.debug("Could not measure %r: %s", text, err) return 0 def _fit_lines(self, lines, available: int): diff --git a/plugins/soccer-scoreboard/test_favorite_team_diagnostics.py b/plugins/soccer-scoreboard/test_favorite_team_diagnostics.py index 98096960..ebb6a8af 100644 --- a/plugins/soccer-scoreboard/test_favorite_team_diagnostics.py +++ b/plugins/soccer-scoreboard/test_favorite_team_diagnostics.py @@ -74,6 +74,28 @@ def __init__(self, *a, **k): "src.plugin_system.base_plugin": bp_mod, }) + # The stubs above are plain ModuleTypes, so `from src.common.X import Y` + # fails with "'src.common' is not a package" even when a real core is on + # the path. Giving them a __path__ lets genuine submodules -- sports_shared, + # sports_card -- resolve from the core while the stubbed ones stay stubbed. + # Stubbing those too would make this test pass against dummies instead of + # the code under test. + # + # This file was the one of eight that never got this block, so it broke the + # day sports.py started importing src.common.sports_shared. It looked fine + # because the CI runner was reporting pytest files as passing without + # running them; fixing that runner is what made these 22 failures visible. + _core = os.environ.get("LEDMATRIX_CORE") or next( + (p for p in sys.path + if p and os.path.isdir(os.path.join(p, "src", "common"))), None) + if _core: + if "src" in sys.modules and not sys.modules["src"].__path__: + sys.modules["src"].__path__ = [os.path.join(_core, "src")] + if ("src.common" in sys.modules + and not sys.modules["src.common"].__path__): + sys.modules["src.common"].__path__ = [ + os.path.join(_core, "src", "common")] + # ESPN's real Premier League codes, as returned by its teams endpoint. PREMIER_LEAGUE = { 'ARS': 'Arsenal', 'AVL': 'Aston Villa', 'BOU': 'AFC Bournemouth', diff --git a/scripts/run_plugin_tests.py b/scripts/run_plugin_tests.py index e5042e32..8a37537d 100644 --- a/scripts/run_plugin_tests.py +++ b/scripts/run_plugin_tests.py @@ -44,7 +44,7 @@ import argparse import os import re -import subprocess +import subprocess # nosec B404 - runs repo-local test files, never a shell import sys from pathlib import Path @@ -80,7 +80,10 @@ def run_pytest_module(script: Path, core: Path | None, timeout: int) -> tuple[in env["PYTHONPATH"] = f"{core}{os.pathsep}{env.get('PYTHONPATH', '')}" env["LEDMATRIX_CORE"] = str(core) try: - proc = subprocess.run( + # nosec B603 - fixed interpreter (sys.executable) running pytest on a + # test file this script discovered in the repo; argument list, no + # shell, so nothing is word-split or expanded. + proc = subprocess.run( # nosec B603 [sys.executable, "-m", "pytest", script.name, "-q", "--no-header", "--tb=line", "-p", "no:cacheprovider"], cwd=script.parent, env=env, capture_output=True, @@ -121,7 +124,9 @@ def run_one(script: Path, core: Path | None, timeout: int) -> tuple[int, str]: # LEDMATRIX_CORE is that contract, and it is absolute. env["LEDMATRIX_CORE"] = str(core) try: - proc = subprocess.run( + # nosec B603 - same contract as run_pytest_module above: fixed + # interpreter, argument list, no shell. + proc = subprocess.run( # nosec B603 [sys.executable, script.name], cwd=script.parent, env=env, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL, diff --git a/scripts/test_module_helpers_defined.py b/scripts/test_module_helpers_defined.py new file mode 100644 index 00000000..20d82fe2 --- /dev/null +++ b/scripts/test_module_helpers_defined.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""A module-level helper must be defined in every module that calls it. + +Five sports plugins got the same BDF-font fix: load a .bdf through +ImageFont.truetype, and when FreeType rejects the requested size, retry at the +size the file declares. The retry needs a small `_bdf_pixel_size()` reader -- +which was added to one of the five and called in all five: + + except OSError: + native = _bdf_pixel_size(font_path) # NameError in four of them + +Nothing caught it. The call sits inside an `except OSError:` handler, so it +only runs when a BDF is requested at a non-native size -- which is precisely +the case the fix exists for, and precisely the case no test exercised. Every +unit test passed and the plugins imported fine; the fix was simply dead in +four plugins and raised NameError in the one situation it was written for. + +This checks the general invariant rather than that one helper: a call to a +bare `_name(...)` must resolve to something the module actually defines or +imports. `pylint --errors-only` reports the same class of fault (E0602) if you +would rather run that. + +Run: python scripts/test_module_helpers_defined.py +""" + +import ast +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + +#: Names that look module-level but are supplied by the runtime or a star +#: import. Kept explicit so an addition here is a deliberate decision. +ALLOWED_UNDEFINED = { + "__name__", "__file__", "__doc__", "__package__", "__spec__", +} + + +def _bound_names(tree: ast.AST) -> set: + """Every name the module binds: defs, classes, imports, assignments.""" + bound = set(dir(__builtins__)) | ALLOWED_UNDEFINED + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + bound.add(node.name) + elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store): + bound.add(node.id) + elif isinstance(node, (ast.Import, ast.ImportFrom)): + for alias in node.names: + bound.add(alias.asname or alias.name.split(".")[0]) + elif isinstance(node, (ast.arg,)): + bound.add(node.arg) + elif isinstance(node, ast.ExceptHandler) and node.name: + bound.add(node.name) + elif isinstance(node, ast.Global): + bound.update(node.names) + return bound + + +def check(path: Path) -> list: + """Underscore-prefixed calls in `path` that nothing in it defines.""" + try: + tree = ast.parse(path.read_text(encoding="utf-8")) + except (OSError, SyntaxError): + return [] + + bound = _bound_names(tree) + missing = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + fn = node.func + # Only bare `_name(...)`. An attribute call (self._x, mod._x) resolves + # at runtime against an object this check cannot see. + if isinstance(fn, ast.Name) and fn.id.startswith("_") and fn.id not in bound: + missing.append((node.lineno, fn.id)) + return missing + + +def main() -> int: + files = sorted(ROOT.glob("plugins/*/**/*.py")) + failures = [] + for path in files: + for lineno, name in check(path): + failures.append(f"{path.relative_to(ROOT)}:{lineno}: {name}() is never defined here") + + if failures: + print("A module-level helper is called but not defined.") + print("This raises NameError the first time that line runs:\n") + for f in failures: + print(f" {f}") + print("\nDefine the helper in each module that calls it, or import it.") + return 1 + + print(f"OK: every module-level helper call resolves ({len(files)} files checked)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 1065c046dc49591e7fff4c3774d1a34f331e052b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 20:41:22 -0400 Subject: [PATCH 13/13] chore: satisfy Codacy's subprocess checks on the test runner Codacy runs Bandit and Opengrep (its Semgrep fork). The pytest subprocess.run this PR adds to scripts/run_plugin_tests.py trips two Opengrep patterns on two different lines -- dangerous-subprocess-use-audit on the run( line and dangerous-subprocess-use-tainted-env-args on the argv line -- and a nosemgrep applies only to its own line, so each needs one. Suppression rather than a rewrite: fixed interpreter, argument list, no shell, so nothing is word-split or expanded. Applied to both call sites so they read consistently. The subprocess import is left exactly as main has it. Annotating it turned an untouched line into a changed one and made Codacy count a pre-existing finding as new. Codacy: 0 new issues, up to standards. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9 --- scripts/run_plugin_tests.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/scripts/run_plugin_tests.py b/scripts/run_plugin_tests.py index 8a37537d..3668349b 100644 --- a/scripts/run_plugin_tests.py +++ b/scripts/run_plugin_tests.py @@ -44,7 +44,7 @@ import argparse import os import re -import subprocess # nosec B404 - runs repo-local test files, never a shell +import subprocess import sys from pathlib import Path @@ -80,11 +80,12 @@ def run_pytest_module(script: Path, core: Path | None, timeout: int) -> tuple[in env["PYTHONPATH"] = f"{core}{os.pathsep}{env.get('PYTHONPATH', '')}" env["LEDMATRIX_CORE"] = str(core) try: - # nosec B603 - fixed interpreter (sys.executable) running pytest on a - # test file this script discovered in the repo; argument list, no - # shell, so nothing is word-split or expanded. - proc = subprocess.run( # nosec B603 - [sys.executable, "-m", "pytest", script.name, "-q", "--no-header", + # Fixed interpreter (sys.executable) running pytest on a test file this + # script discovered in the repo; argument list, no shell, so nothing is + # word-split or expanded. Same suppression pair the rest of the repo + # uses for this shape (see scripts/render_docs_assets.py). + proc = subprocess.run( # nosec B603 - no shell invoked (list-form argv) # nosemgrep + [sys.executable, "-m", "pytest", script.name, "-q", "--no-header", # nosemgrep "--tb=line", "-p", "no:cacheprovider"], cwd=script.parent, env=env, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL, @@ -124,10 +125,10 @@ def run_one(script: Path, core: Path | None, timeout: int) -> tuple[int, str]: # LEDMATRIX_CORE is that contract, and it is absolute. env["LEDMATRIX_CORE"] = str(core) try: - # nosec B603 - same contract as run_pytest_module above: fixed - # interpreter, argument list, no shell. - proc = subprocess.run( # nosec B603 - [sys.executable, script.name], + # Same contract as run_pytest_module above: fixed interpreter, + # argument list, no shell. + proc = subprocess.run( # nosec B603 - no shell invoked (list-form argv) # nosemgrep + [sys.executable, script.name], # nosemgrep cwd=script.parent, env=env, capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL, )