From d037b28752c1c8cddbe8619fe7e86f53daa098be Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:18:12 -0400 Subject: [PATCH 1/2] fix(football): a favourites-only board wakes only for its own kickoffs (3.18.7) The live loop offered every game on the scoreboard to the idle back-off as a kickoff to wake for. Each kickoff holds the poll at live cadence for 15 minutes, so on a college Saturday a favourites-only board fetched the whole 54-game scoreboard about every 30 s all day for games it then filtered out, and a kickoff already being waited on kept a favourite's later kickoff from being stored until its grace ran out (UGA 12:45 noticed at 12:59 in a replay of 2026-10-03). Only games the live filter would show are offered now. Co-Authored-By: Claude Opus 5.5 --- plugins/football-scoreboard/CHANGELOG.md | 20 ++++++++++ plugins/football-scoreboard/manifest.json | 8 +++- plugins/football-scoreboard/sports.py | 11 +++++- .../test_kickoff_wakes_the_idle_poll.py | 39 ++++++++++++++++++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/plugins/football-scoreboard/CHANGELOG.md b/plugins/football-scoreboard/CHANGELOG.md index d860c1e5..927ac757 100644 --- a/plugins/football-scoreboard/CHANGELOG.md +++ b/plugins/football-scoreboard/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [3.18.7] - 2026-10-04 + +### Fixed +- A favourites-only board (`show_favorite_teams_only`, the default) no longer + wakes for other teams' kickoffs. The live loop handed every game on the + scoreboard to the idle back-off as a kickoff to wake for, and each kickoff + holds the poll at `live_update_interval` for 15 minutes. A college Saturday + has one every half hour from late morning to midnight, so the board fetched + the whole 54-game scoreboard (865 KB decoded, 63 KB gzipped) about every + 30 s all day, for games it then filtered out. Worse, a kickoff it had + already woken for blocks a later one from being stored until its 15 minutes + are up: replaying 2026-10-03, the 12:30 games kept UGA's 12:45 kickoff + from being noticed until 12:59. Only games the live filter would show are offered now; the excluded + teams list is honoured the same way. Boards showing every live game + (`show_all_live`, or `show_favorite_teams_only` off) are unchanged. + Replaying the 2026-10-03 Saturday (09:00-03:00 ET, favourites UGA and AUB): + 1,285 -> 995 requests, 52.5 -> 38.3 MB on the wire (765 -> 558 MB decoded), + with the favourites' games shown from kickoff. `test_kickoff_wakes_the_idle_poll.py` + covers the filter. + ## [3.18.6] - 2026-10-04 ### Fixed diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index 0d49f876..dd727342 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.6", + "version": "3.18.7", "update_interval": 60, "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", @@ -25,6 +25,12 @@ "ncaa_fb_live" ], "versions": [ + { + "version": "3.18.7", + "released": "2026-10-04", + "ledmatrix_min_version": "3.8.0", + "notes": "A favourites-only board no longer wakes for other teams' kickoffs. Every kickoff on the scoreboard held the live poll at 30 s for 15 minutes, so on a college Saturday it fetched the whole scoreboard about every 30 s all day for games it then filtered out, and a crowd of earlier kickoffs could make it notice a favourite's kickoff up to 15 minutes late. Replaying the 2026-10-03 Saturday: 1,285 -> 995 requests and 52 -> 38 MB on the wire (765 -> 558 MB decoded); the favourite's game now shows from kickoff. Boards that show every live game are unchanged." + }, { "version": "3.18.6", "released": "2026-10-04", diff --git a/plugins/football-scoreboard/sports.py b/plugins/football-scoreboard/sports.py index 017eba22..ac2f26ae 100644 --- a/plugins/football-scoreboard/sports.py +++ b/plugins/football-scoreboard/sports.py @@ -3753,9 +3753,18 @@ def update(self): # starts, so it cannot sleep through a kickoff. # getattr-guarded: the core version floor is # advisory, so an older core must stay loadable. + # + # Only a game this board would show once it is live. + # Every kickoff holds the poll at live cadence for a + # quarter of an hour, and a college Saturday has one + # every half hour from late morning to midnight -- so + # a favourites-only board polled the 865 KB scoreboard + # every 30 s all day for games it then filtered out. _note_start = getattr( self, "_note_scheduled_start_candidate", None) - if _note_start is not None: + if _note_start is not None and details and self._classify_live_game( + details.get("home_abbr"), + details.get("away_abbr"))[0]: _note_start(details) if details: # Log game status for debugging - use INFO level to see what's happening diff --git a/plugins/football-scoreboard/test_kickoff_wakes_the_idle_poll.py b/plugins/football-scoreboard/test_kickoff_wakes_the_idle_poll.py index 5e19555e..5cf1f71c 100644 --- a/plugins/football-scoreboard/test_kickoff_wakes_the_idle_poll.py +++ b/plugins/football-scoreboard/test_kickoff_wakes_the_idle_poll.py @@ -16,7 +16,13 @@ what is pinned here is the *wiring*, which is what a future edit would silently drop: - * every event the live fetch returns is offered, upcoming ones included; + * every event the live fetch returns is offered, upcoming ones included -- + every event the board would show once live, that is: a favourites-only + board offers only its favourites' games, and an excluded team's game is + never offered. A kickoff holds the poll at live cadence for 15 minutes, + and a college Saturday has one every half hour, so offering games the + board then filters out kept a favourites-only board polling the whole + scoreboard every 30 s all day; * the offer happens even when nothing is live, which is the only case the back-off is running in; * the call is getattr-guarded, so a core without the method still loads. @@ -100,6 +106,7 @@ def _prime(live): live.sport = "football" live._games_lock = threading.RLock() live.favorite_teams = [] + live.exclude_teams = [] live.show_all_live = True live.show_favorite_teams_only = False live.show_odds = False @@ -115,9 +122,11 @@ def _prime(live): return live -def _drive(live, events, offered): +def _drive(live, events, offered, **settings): """Run one update() pass over a canned payload, recording what was offered.""" _prime(live) + for name, value in settings.items(): + setattr(live, name, value) live._fetch_data = lambda *a, **k: {"events": list(events)} live._extract_game_details = lambda game: dict(game) live._note_scheduled_start_candidate = lambda details: offered.append(details) @@ -146,6 +155,32 @@ def main(): check("the back-off's own case is covered", len(offered) == 3) check("and no game was treated as live", live.live_games == []) + print("\na favourites-only board offers only its favourites' games") + games = [ + {"id": "fav", "home_abbr": "UGA", "away_abbr": "VAN", + "is_live": False, "is_halftime": False, "is_final": False}, + {"id": "other", "home_abbr": "MSST", "away_abbr": "ALA", + "is_live": False, "is_halftime": False, "is_final": False}, + ] + offered = _drive(_live(), games, [], favorite_teams=["UGA"], + show_all_live=False, show_favorite_teams_only=True) + check("only the favourite's game was offered", + [d.get("id") for d in offered] == ["fav"]) + + offered = _drive(_live(), games, [], favorite_teams=["UGA"], + show_all_live=True, show_favorite_teams_only=True) + check("show_all_live still offers every game", + sorted(d.get("id") for d in offered) == ["fav", "other"]) + + offered = _drive(_live(), games, [], favorite_teams=["UGA"], + show_all_live=False, show_favorite_teams_only=False) + check("a board showing every live game offers every game", + sorted(d.get("id") for d in offered) == ["fav", "other"]) + + offered = _drive(_live(), games, [], exclude_teams=["ALA"]) + check("an excluded team's game is never offered", + [d.get("id") for d in offered] == ["fav"]) + print("\nthe call is getattr-guarded") live2 = _live() # A core without the method: the attribute simply is not there. From 3323ca2e8c2120da5850be1c200914419944a835 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:18:14 -0400 Subject: [PATCH 2/2] chore(registry): football-scoreboard 3.18.7 Co-Authored-By: Claude Opus 5.5 --- plugins.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins.json b/plugins.json index 666225d2..2ddaa265 100644 --- a/plugins.json +++ b/plugins.json @@ -283,9 +283,9 @@ "last_updated": "2026-10-04", "verified": true, "screenshot": "", - "latest_version": "3.18.6", + "latest_version": "3.18.7", "ledmatrix_min_version": "3.8.0", - "commit": "2bd32339d2935be52c510aeb83a03e1938f3d36a" + "commit": "d037b28752c1c8cddbe8619fe7e86f53daa098be" }, { "id": "geochron",