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
5 changes: 2 additions & 3 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions plugins/football-scoreboard/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 11 additions & 3 deletions plugins/football-scoreboard/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
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.8",
"version": "3.18.9",
"update_interval": 60,
"author": "ChuckBuilds",
"class_name": "FootballScoreboardPlugin",
Expand All @@ -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",
Expand Down
128 changes: 128 additions & 0 deletions plugins/football-scoreboard/test_celebration_is_a_live_mode.py
Original file line number Diff line number Diff line change
@@ -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: <core-venv>/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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading