diff --git a/plugins.json b/plugins.json index ade9d80b..8b2dd9d3 100644 --- a/plugins.json +++ b/plugins.json @@ -283,9 +283,8 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "3.18.8", - "ledmatrix_min_version": "3.8.0", - "commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c" + "latest_version": "3.18.9", + "ledmatrix_min_version": "3.8.0" }, { "id": "geochron", diff --git a/plugins/football-scoreboard/CHANGELOG.md b/plugins/football-scoreboard/CHANGELOG.md index 66ae225b..bd05efcb 100644 --- a/plugins/football-scoreboard/CHANGELOG.md +++ b/plugins/football-scoreboard/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [3.18.9] - 2026-10-05 + +### Fixed +- A win celebration in a league with `live_priority` off no longer makes + `has_live_content()` true. `get_live_modes()` only returns a celebrating + league's live mode when that league is enabled with `live_priority` on, so + `has_live_content()` reported live content that no live mode of this plugin + could show. It now applies the same `live_priority` gate (NFL, NCAA + football). + ## [3.18.8] - 2026-10-05 ### Fixed diff --git a/plugins/football-scoreboard/manager.py b/plugins/football-scoreboard/manager.py index 2d97da72..84152b95 100644 --- a/plugins/football-scoreboard/manager.py +++ b/plugins/football-scoreboard/manager.py @@ -2053,14 +2053,22 @@ def display(self, display_mode: str = None, force_clear: bool = False) -> bool: self.logger.error(f"Error in display method: {e}") return False - def _get_active_celebration_manager(self): + def _get_active_celebration_manager(self, live_priority_only=False): """Return the (league_key, live_manager) of an enabled league whose live - manager currently has an active score/win celebration, else None.""" + manager currently has an active score/win celebration, else None. + + ``live_priority_only`` also skips leagues with live priority off -- the + same gate get_live_modes() applies, so has_live_content() never reports + a celebration that no live mode can show. + """ if not self.is_enabled: return None for league_key in self._league_registry: if not self._league_registry[league_key].get("enabled", False): continue + if live_priority_only and not self._league_registry[league_key].get( + "live_priority", False): + continue live_manager = self._get_league_manager_for_mode(league_key, "live") if ( live_manager @@ -2150,7 +2158,7 @@ def has_live_content(self) -> bool: # An active celebration (notably a win, whose game has already left the # live list) must keep the live mode on screen. - if self._get_active_celebration_manager() is not None: + if self._get_active_celebration_manager(live_priority_only=True) is not None: return True # Live game counts per league, folded into the single throttled summary diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index 54cc48df..1f7ff46d 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.18.8", + "version": "3.18.9", "update_interval": 60, "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", @@ -25,6 +25,12 @@ "ncaa_fb_live" ], "versions": [ + { + "version": "3.18.9", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.0", + "notes": "A win celebration in a league with live_priority off no longer makes has_live_content() true. get_live_modes() only returns a celebrating league's live mode when that league is enabled with live_priority on, so has_live_content() reported live content that no live mode of this plugin could show; it now applies the same live_priority gate (NFL, NCAA football)." + }, { "version": "3.18.8", "released": "2026-10-05", diff --git a/plugins/football-scoreboard/test_celebration_is_a_live_mode.py b/plugins/football-scoreboard/test_celebration_is_a_live_mode.py new file mode 100644 index 00000000..8fabdcc6 --- /dev/null +++ b/plugins/football-scoreboard/test_celebration_is_a_live_mode.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +A win celebration must be selectable as a live mode. + +A win fires as the game goes final, so by the time it is on screen the game has +already left the league's live list. get_live_modes() returns a celebrating +league's live mode for that reason. has_live_content() must agree with it: it +counted a celebration in a league with live priority off, which get_live_modes() +never returns, so the controller saw live content with no mode of this plugin +to show for it. + +Covers, for every league: + 1. A celebration with an empty live list -> get_live_modes() returns that + league's live mode, and has_live_content() agrees. + 2. A celebration and a live game in the same league -> the mode appears once. + 3. A disabled league, or one with live priority off -> neither method reports + the celebration. + +Run: /bin/python plugins/football-scoreboard/test_celebration_is_a_live_mode.py +""" + +import os +import sys + +import pytest + +PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__)) +if PLUGIN_DIR not in sys.path: + sys.path.insert(0, PLUGIN_DIR) + +from manager import FootballScoreboardPlugin # noqa: E402 + +LEAGUES = ("nfl", "ncaa_fb") + + +class _QuietLogger: + def _drop(self, *a, **k): + pass + + debug = info = warning = error = _drop + + +class _Live: + """Stands in for a league's live manager. Has no _is_game_really_over, so + both methods skip that filter.""" + + def __init__(self, games=(), celebrating=False): + self.live_games = list(games) + self.favorite_teams = [] + self.celebrating = celebrating + + def has_active_celebration(self): + return self.celebrating + + +class _Stub: + """Carries just the attributes the two methods read, with the real methods + bound to it -- the full constructor needs a display manager.""" + + get_live_modes = FootballScoreboardPlugin.get_live_modes + has_live_content = FootballScoreboardPlugin.has_live_content + _get_active_celebration_manager = FootballScoreboardPlugin._get_active_celebration_manager + + def __init__(self, enabled=True, live_priority=True): + self.logger = _QuietLogger() + self.is_enabled = True + self._league_registry = {} + for league in LEAGUES: + setattr(self, f"{league}_enabled", enabled) + setattr(self, f"{league}_live_priority", live_priority) + setattr(self, f"{league}_live", _Live()) + self._league_registry[league] = { + "enabled": enabled, "live_priority": live_priority} + self._last_live_content_log = 0.0 + self._last_live_content_state = None + self._live_content_log_interval = 60.0 + + def _get_league_manager_for_mode(self, league, mode): + return getattr(self, f"{league}_live") if mode == "live" else None + + +def _game(): + return {"home_abbr": "KC", "away_abbr": "DAL", "is_final": False, "is_live": True} + + +@pytest.mark.parametrize("league", LEAGUES) +def test_celebration_with_empty_live_list_is_a_live_mode(league): + stub = _Stub() + getattr(stub, f"{league}_live").celebrating = True + + assert stub.get_live_modes() == [f"{league}_live"] + assert stub.has_live_content() is True + + +@pytest.mark.parametrize("league", LEAGUES) +def test_celebration_and_live_game_list_the_mode_once(league): + stub = _Stub() + live = getattr(stub, f"{league}_live") + live.celebrating = True + live.live_games.append(_game()) + + assert stub.get_live_modes() == [f"{league}_live"] + assert stub.has_live_content() is True + + +@pytest.mark.parametrize("league", LEAGUES) +@pytest.mark.parametrize("gate", ("enabled", "live_priority")) +def test_gated_off_league_does_not_report_its_celebration(league, gate): + stub = _Stub(**{gate: False}) + getattr(stub, f"{league}_live").celebrating = True + + assert stub.get_live_modes() == [] + assert stub.has_live_content() is False + + +def test_only_the_celebrating_league_is_returned(): + stub = _Stub() + stub.nfl_live.live_games.append(_game()) + stub.ncaa_fb_live.celebrating = True + + assert stub.get_live_modes() == ["nfl_live", "ncaa_fb_live"] + + +def test_no_celebration_and_no_games_is_not_live(): + stub = _Stub() + + assert stub.get_live_modes() == [] + assert stub.has_live_content() is False diff --git a/plugins/football-scoreboard/test_live_content_log_throttle.py b/plugins/football-scoreboard/test_live_content_log_throttle.py index e04b463b..49416c18 100644 --- a/plugins/football-scoreboard/test_live_content_log_throttle.py +++ b/plugins/football-scoreboard/test_live_content_log_throttle.py @@ -78,7 +78,7 @@ def __init__(self, nfl_games=(), ncaa_games=()): self._last_live_content_state = None self._live_content_log_interval = 60.0 - def _get_active_celebration_manager(self): + def _get_active_celebration_manager(self, live_priority_only=False): # No celebration in flight, so has_live_content() runs its full body # instead of short-circuiting to True. return None