Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions plugins/football-scoreboard/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 7 additions & 1 deletion plugins/football-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
11 changes: 10 additions & 1 deletion plugins/football-scoreboard/sports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 37 additions & 2 deletions plugins/football-scoreboard/test_kickoff_wakes_the_idle_poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
Loading