From 4a564204da55f06bc9dfd9e0bfda785e31fb19ac Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:00:59 -0400 Subject: [PATCH 1/3] docs(lacrosse-scoreboard): document all 170 settings, with real renders The README documented a fraction of the plugin's 170 settings and had no images. It now covers every schema leaf -- verified by a token audit -- and shows the three men's display modes, both leagues side by side, the show_records toggle, and the card at four panel sizes. The two league blocks are identical in every setting except one: ncaa_mens defaults to enabled and ncaa_womens does not. Documenting one table rather than two identical sets keeps 104 of the 170 leaves readable instead of duplicated. Two things the old README did not say: - display_options.show_records and show_ranking default to true per league, while defaults.show_records and defaults.show_ranking default to false, and the per-league copy wins. Because the UI writes schema defaults on save, changing the defaults copy appears to do nothing. Both the settings reference and troubleshooting now say so. - The favorites key here is teams.favorite_teams_only, not show_favorite_teams_only as in the single-league scoreboards, and it defaults to off rather than on. other_games_min_quality is live in this plugin -- NCAA lacrosse has a national poll to rank against -- unlike in nrl, where it is inert. other_games_divisions stays inert; it is a college football taxonomy. Renders come from docs/assets/lacrosse-scoreboard/shots.json. Lacrosse keeps one manager per league and mode as a plain attribute rather than in a dict, and only builds them when that league's enabled flag is set, so the fixture turns both leagues on. Real ESPN crests, not placeholders. check_plugin.py passes with no FAIL. Carries the docs-tooling changes from #423, which this render depends on. Co-Authored-By: Claude Opus 5 --- .../lacrosse-scoreboard/display-modes.png | Bin 0 -> 29689 bytes docs/assets/lacrosse-scoreboard/hero.png | Bin 0 -> 4795 bytes docs/assets/lacrosse-scoreboard/leagues.png | Bin 0 -> 15500 bytes .../lacrosse-scoreboard/panel-sizes.png | Bin 0 -> 25488 bytes docs/assets/lacrosse-scoreboard/shots.json | 919 ++++++++++++++++++ .../lacrosse-scoreboard/show-records.png | Bin 0 -> 21407 bytes plugins.json | 4 +- plugins/lacrosse-scoreboard/README.md | 857 +++++++++------- plugins/lacrosse-scoreboard/manifest.json | 14 +- scripts/docs_render_support/sitecustomize.py | 55 +- scripts/render_docs_assets.py | 3 + 11 files changed, 1494 insertions(+), 358 deletions(-) create mode 100644 docs/assets/lacrosse-scoreboard/display-modes.png create mode 100644 docs/assets/lacrosse-scoreboard/hero.png create mode 100644 docs/assets/lacrosse-scoreboard/leagues.png create mode 100644 docs/assets/lacrosse-scoreboard/panel-sizes.png create mode 100644 docs/assets/lacrosse-scoreboard/shots.json create mode 100644 docs/assets/lacrosse-scoreboard/show-records.png diff --git a/docs/assets/lacrosse-scoreboard/display-modes.png b/docs/assets/lacrosse-scoreboard/display-modes.png new file mode 100644 index 0000000000000000000000000000000000000000..45fa14dc863e8dc25c8a0963499b7f959d88d83b GIT binary patch literal 29689 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 literal 0 HcmV?d00001 diff --git a/docs/assets/lacrosse-scoreboard/hero.png b/docs/assets/lacrosse-scoreboard/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..5e3119dd2f84cb132db397b134034e569edf5b09 GIT binary patch literal 4795 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 C_ZWHr literal 0 HcmV?d00001 diff --git a/docs/assets/lacrosse-scoreboard/leagues.png b/docs/assets/lacrosse-scoreboard/leagues.png new file mode 100644 index 0000000000000000000000000000000000000000..dbd91580d09687e492d379f20c6eaf5dc60a44e4 GIT binary patch literal 15500 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 V6&mwA93;#X3NotF8Bz~k{4XhNsfz#r literal 0 HcmV?d00001 diff --git a/docs/assets/lacrosse-scoreboard/panel-sizes.png b/docs/assets/lacrosse-scoreboard/panel-sizes.png new file mode 100644 index 0000000000000000000000000000000000000000..70baa7a8b9d821c13d0585529e61a2f4de0de96a GIT binary patch literal 25488 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 literal 0 HcmV?d00001 diff --git a/docs/assets/lacrosse-scoreboard/shots.json b/docs/assets/lacrosse-scoreboard/shots.json new file mode 100644 index 00000000..6ae62790 --- /dev/null +++ b/docs/assets/lacrosse-scoreboard/shots.json @@ -0,0 +1,919 @@ +{ + "plugin": "lacrosse-scoreboard", + "_comment": "Lacrosse keeps one manager per league and mode as a plain attribute -- ncaa_mens_live, ncaa_womens_recent and so on -- rather than in a dict, and only builds them when that league's enabled flag is set, so the config below turns both leagues on. Every mode's display() returns early unless games_list is populated, and the live path also reads live_games and active_celebration. display_mode picks which of the six screens is drawn. The card reads home_logo_path/away_logo_path off the game dict as Path objects. Teams, scores and records are invented; the crests bundled in the core are grey placeholders, so they render as blocks.", + "defaults": { + "width": 128, + "height": 32, + "scale": 6, + "freeze_time": "2026-04-18T17:00:00+00:00", + "skip_update": true, + "config": { + "enabled": true, + "ncaa_mens": { + "enabled": true + }, + "ncaa_womens": { + "enabled": true + } + } + }, + "shots": [ + { + "name": "hero", + "display_mode": "lax_ncaa_mens_live", + "attrs": { + "ncaa_mens_live.games_list": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.current_game": { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + }, + "ncaa_mens_live.current_game_index": 0, + "ncaa_mens_live.last_game_switch": 9000000000.0, + "ncaa_mens_live.last_update": 9000000000.0, + "ncaa_mens_live.live_games": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.active_celebration": null + } + }, + { + "name": "m-live", + "display_mode": "lax_ncaa_mens_live", + "standalone": false, + "attrs": { + "ncaa_mens_live.games_list": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.current_game": { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + }, + "ncaa_mens_live.current_game_index": 0, + "ncaa_mens_live.last_game_switch": 9000000000.0, + "ncaa_mens_live.last_update": 9000000000.0, + "ncaa_mens_live.live_games": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.active_celebration": null + } + }, + { + "name": "m-recent", + "display_mode": "lax_ncaa_mens_recent", + "standalone": false, + "attrs": { + "ncaa_mens_recent.games_list": [ + { + "id": "2", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "16", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Final", + "status_text": "Final", + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": true, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_recent.current_game": { + "id": "2", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "16", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Final", + "status_text": "Final", + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": true, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + }, + "ncaa_mens_recent.current_game_index": 0, + "ncaa_mens_recent.last_game_switch": 9000000000.0, + "ncaa_mens_recent.last_update": 9000000000.0 + } + }, + { + "name": "m-upcoming", + "display_mode": "lax_ncaa_mens_upcoming", + "standalone": false, + "attrs": { + "ncaa_mens_upcoming.games_list": [ + { + "id": "3", + "away_abbr": "QUIN", + "home_abbr": "YALE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "", + "status_text": "", + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/QUIN.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/YALE.png" + } + ], + "ncaa_mens_upcoming.current_game": { + "id": "3", + "away_abbr": "QUIN", + "home_abbr": "YALE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "", + "status_text": "", + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/QUIN.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/YALE.png" + }, + "ncaa_mens_upcoming.current_game_index": 0, + "ncaa_mens_upcoming.last_game_switch": 9000000000.0, + "ncaa_mens_upcoming.last_update": 9000000000.0 + } + }, + { + "name": "w-live", + "display_mode": "lax_ncaa_womens_live", + "standalone": false, + "attrs": { + "ncaa_womens_live.games_list": [ + { + "id": "4", + "away_abbr": "HARV", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "6", + "home_score": "8", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q2 4:30", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_womens_logos/HARV.png", + "home_logo_path": "assets/sports/ncaa_womens_logos/DART.png" + } + ], + "ncaa_womens_live.current_game": { + "id": "4", + "away_abbr": "HARV", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "6", + "home_score": "8", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q2 4:30", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_womens_logos/HARV.png", + "home_logo_path": "assets/sports/ncaa_womens_logos/DART.png" + }, + "ncaa_womens_live.current_game_index": 0, + "ncaa_womens_live.last_game_switch": 9000000000.0, + "ncaa_womens_live.last_update": 9000000000.0, + "ncaa_womens_live.live_games": [ + { + "id": "4", + "away_abbr": "HARV", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "6", + "home_score": "8", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q2 4:30", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_womens_logos/HARV.png", + "home_logo_path": "assets/sports/ncaa_womens_logos/DART.png" + } + ], + "ncaa_womens_live.active_celebration": null + } + }, + { + "name": "rec-on", + "display_mode": "lax_ncaa_mens_upcoming", + "standalone": false, + "attrs": { + "ncaa_mens_upcoming.games_list": [ + { + "id": "3", + "away_abbr": "QUIN", + "home_abbr": "YALE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "", + "status_text": "", + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/QUIN.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/YALE.png" + } + ], + "ncaa_mens_upcoming.current_game": { + "id": "3", + "away_abbr": "QUIN", + "home_abbr": "YALE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "", + "status_text": "", + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/QUIN.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/YALE.png" + }, + "ncaa_mens_upcoming.current_game_index": 0, + "ncaa_mens_upcoming.last_game_switch": 9000000000.0, + "ncaa_mens_upcoming.last_update": 9000000000.0 + }, + "config": { + "enabled": true, + "ncaa_mens": { + "enabled": true + }, + "ncaa_womens": { + "enabled": true + }, + "defaults": { + "show_records": true + } + } + }, + { + "name": "rec-off", + "display_mode": "lax_ncaa_mens_upcoming", + "standalone": false, + "attrs": { + "ncaa_mens_upcoming.games_list": [ + { + "id": "3", + "away_abbr": "QUIN", + "home_abbr": "YALE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "", + "status_text": "", + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/QUIN.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/YALE.png" + } + ], + "ncaa_mens_upcoming.current_game": { + "id": "3", + "away_abbr": "QUIN", + "home_abbr": "YALE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "", + "status_text": "", + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/QUIN.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/YALE.png" + }, + "ncaa_mens_upcoming.current_game_index": 0, + "ncaa_mens_upcoming.last_game_switch": 9000000000.0, + "ncaa_mens_upcoming.last_update": 9000000000.0 + }, + "config": { + "enabled": true, + "ncaa_mens": { + "enabled": true + }, + "ncaa_womens": { + "enabled": true + }, + "defaults": { + "show_records": false + } + } + }, + { + "name": "size-64x32", + "width": 64, + "height": 32, + "display_mode": "lax_ncaa_mens_live", + "standalone": false, + "attrs": { + "ncaa_mens_live.games_list": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.current_game": { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + }, + "ncaa_mens_live.current_game_index": 0, + "ncaa_mens_live.last_game_switch": 9000000000.0, + "ncaa_mens_live.last_update": 9000000000.0, + "ncaa_mens_live.live_games": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.active_celebration": null + } + }, + { + "name": "size-128x32", + "width": 128, + "height": 32, + "display_mode": "lax_ncaa_mens_live", + "standalone": false, + "attrs": { + "ncaa_mens_live.games_list": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.current_game": { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + }, + "ncaa_mens_live.current_game_index": 0, + "ncaa_mens_live.last_game_switch": 9000000000.0, + "ncaa_mens_live.last_update": 9000000000.0, + "ncaa_mens_live.live_games": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.active_celebration": null + } + }, + { + "name": "size-128x64", + "width": 128, + "height": 64, + "display_mode": "lax_ncaa_mens_live", + "standalone": false, + "attrs": { + "ncaa_mens_live.games_list": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.current_game": { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + }, + "ncaa_mens_live.current_game_index": 0, + "ncaa_mens_live.last_game_switch": 9000000000.0, + "ncaa_mens_live.last_update": 9000000000.0, + "ncaa_mens_live.live_games": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.active_celebration": null + } + }, + { + "name": "size-256x32", + "width": 256, + "height": 32, + "display_mode": "lax_ncaa_mens_live", + "standalone": false, + "attrs": { + "ncaa_mens_live.games_list": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.current_game": { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + }, + "ncaa_mens_live.current_game_index": 0, + "ncaa_mens_live.last_game_switch": 9000000000.0, + "ncaa_mens_live.last_update": 9000000000.0, + "ncaa_mens_live.live_games": [ + { + "id": "1", + "away_abbr": "YALE", + "home_abbr": "DART", + "away_id": "1", + "home_id": "2", + "away_score": "11", + "home_score": "14", + "away_record": "9-3", + "home_record": "11-1", + "period_text": "Q3 8:12", + "status_text": "LIVE", + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/18", + "game_time": "1:00pm", + "away_logo_path": "assets/sports/ncaa_mens_logos/YALE.png", + "home_logo_path": "assets/sports/ncaa_mens_logos/DART.png" + } + ], + "ncaa_mens_live.active_celebration": null + } + } + ], + "composites": [ + { + "name": "display-modes", + "columns": 1, + "cells": [ + { + "shot": "m-live", + "label": "lax_ncaa_mens_live", + "sublabel": "quarter, clock and the running score" + }, + { + "shot": "m-recent", + "label": "lax_ncaa_mens_recent", + "sublabel": "the finished game" + }, + { + "shot": "m-upcoming", + "label": "lax_ncaa_mens_upcoming", + "sublabel": "face-off date and time" + } + ] + }, + { + "name": "leagues", + "columns": 1, + "cells": [ + { + "shot": "m-live", + "label": "ncaa_mens", + "sublabel": "lax_ncaa_mens_live" + }, + { + "shot": "w-live", + "label": "ncaa_womens", + "sublabel": "lax_ncaa_womens_live" + } + ] + }, + { + "name": "show-records", + "columns": 1, + "cells": [ + { + "shot": "rec-on", + "label": "show_records: true" + }, + { + "shot": "rec-off", + "label": "show_records: false" + } + ] + }, + { + "name": "panel-sizes", + "columns": 2, + "cells": [ + { + "shot": "size-64x32", + "label": "64 x 32" + }, + { + "shot": "size-128x32", + "label": "128 x 32" + }, + { + "shot": "size-128x64", + "label": "128 x 64" + }, + { + "shot": "size-256x32", + "label": "256 x 32" + } + ] + } + ] +} diff --git a/docs/assets/lacrosse-scoreboard/show-records.png b/docs/assets/lacrosse-scoreboard/show-records.png new file mode 100644 index 0000000000000000000000000000000000000000..d68f77892a55b496bc145ee0fee3c94e01a1a75a GIT binary patch literal 21407 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 **Breaking change in 1.1.0:** display modes gained a `lax_` prefix -> (e.g. `lax_ncaa_mens_recent`) to avoid colliding with the NCAA -> hockey modes exposed by `hockey-scoreboard`. If you pinned any of -> the old `ncaa_mens_*` / `ncaa_womens_*` names in -> `display_durations`, `rotation_order`, or anywhere else in -> `config.json`, update them to the new prefixed names. See the -> plugin [CHANGELOG](CHANGELOG.md) for the full mapping. - -## Features - -- **NCAA Men's Lacrosse** (Inside Lacrosse D1 Poll — top 20) -- **NCAA Women's Lacrosse** (Inside Lacrosse / IWLCA Coaches Top 25 Poll) -- **Live games** with quarter, clock, score, and optional shot totals -- **Recent (completed) games** with final score and OT indicator -- **Upcoming games** with start time, matchup, records, and rankings -- **Favorite team filtering** — pin specific teams, or use the dynamic shortcuts `NCAA_MENS_TOP_20`, `NCAA_MENS_TOP_10`, `NCAA_MENS_TOP_5`, `NCAA_WOMENS_TOP_25`, `NCAA_WOMENS_TOP_10`, `NCAA_WOMENS_TOP_5` to auto-track whichever teams are currently in the poll -- **Live priority** — force live favorite-team games to preempt the rotation -- **Per-mode display style** — `switch` (one game card rotating) or `scroll` (horizontal ticker), independently configurable for live, recent, and upcoming -- **Poll rank badges** — `#1`, `#2` overlays on team names, updated hourly from ESPN's public rankings feed -- **Element customization** — toggle records, rankings, odds, shot totals; override layout offsets for logos, score, and status text -- **Configurable durations, update intervals, and game counts** per league -- **Favorite Team Result Colors**: Optionally show a finished game's score in green when your favorite team won and red when it lost - -## Requirements - -- Python 3.9+ -- LEDMatrix core 2.0.0 or newer -- A minimum display of 64×32 (128×32 recommended for full scroll and scoreboard layouts) -- Internet access to reach the public ESPN API - -No API key is required. - -## Installation - -The easiest way is the Plugin Store in the LEDMatrix web UI: - -1. Open `http://your-pi-ip:5000` -2. Open the **Plugin Manager** tab -3. Find **Lacrosse Scoreboard** in the **Plugin Store** section and click - **Install** -4. Open the plugin's tab in the second nav row to configure favorite - teams - -On first launch, team logos for any teams in the current scoreboard -window will be downloaded to `assets/sports/ncaa_logos/` automatically. - -Manual install from source: - -```bash -cd /path/to/LEDMatrix -python -m pip install --user pillow requests pytz # see requirements.txt -cp -r /path/to/ledmatrix-plugins/plugins/lacrosse-scoreboard plugin-repos/ -sudo systemctl restart ledmatrix -``` - -Then add a `lacrosse-scoreboard` entry to your LEDMatrix `config.json` -(see **Configuration** below) — or just use the web UI to configure it. - -## Dependencies - -From `requirements.txt`: - -- `Pillow>=9.0.0` — image compositing and logo rendering -- `requests>=2.28.0` — ESPN API calls -- `pytz>=2022.1` — timezone conversion for game start times -- `urllib3>=1.26.0` — HTTP retry logic - -All dependencies are standard and already present in a typical LEDMatrix install. - -## Configuration - -The plugin config is split into per-league blocks. See `config_schema.json` for the authoritative list of fields and their defaults. Minimal working example: +# Lacrosse Scoreboard + +Live, recent, and upcoming **NCAA Men's and Women's Lacrosse** games on your +LEDMatrix display, from ESPN's public API. No API key required. + +![NCAA men's lacrosse live scorebug](../../docs/assets/lacrosse-scoreboard/hero.png) + +## Contents + +- [Quick start](#quick-start) +- [Team abbreviations](#team-abbreviations) +- [Display modes](#display-modes) +- [The two leagues](#the-two-leagues) +- [How games are chosen](#how-games-are-chosen) +- [Panel sizes](#panel-sizes) +- [Settings reference](#settings-reference) +- [Per-league settings](#per-league-settings) +- [Matchup separator and the upcoming card middle](#matchup-separator-and-the-upcoming-card-middle) +- [Text colours](#text-colours) +- [Favorite team result colours](#favorite-team-result-colours) +- [Vegas ticker: seeing live games more often](#vegas-ticker-seeing-live-games-more-often) +- [Data source](#data-source) +- [Requirements and installation](#requirements-and-installation) +- [Testing](#testing) +- [Troubleshooting](#troubleshooting) + +## Quick start + +1. Install **Lacrosse Scoreboard** from the LEDMatrix Plugin Store. +2. Turn on `enabled`, then turn on the league you want — `ncaa_mens` is on by + default, `ncaa_womens` is off. +3. Add your schools under that league's **Favorite Teams**, using the + **full-name abbreviations** described below. ```json { - "enabled": true, - "defaults": { - "display_duration": 15, - "show_records": true, - "show_ranking": true, - "show_odds": false - }, - "ncaa_mens": { - "enabled": true, - "display_modes": { - "live": true, - "live_display_mode": "switch", - "recent": true, - "recent_display_mode": "scroll", - "upcoming": true, - "upcoming_display_mode": "scroll" - }, - "teams": { - "favorite_teams": ["NCAA_MENS_TOP_10", "JOHNS HOPKINS"], - "favorite_teams_only": false, - "show_all_live": true, - "exclude_teams": [], - "favorite_live_boost": 2 - }, - "filtering": { - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 - }, - "live_priority": true - }, - "ncaa_womens": { + "lacrosse-scoreboard": { "enabled": true, - "display_modes": { - "live": true, - "live_display_mode": "switch", - "recent": true, - "recent_display_mode": "scroll", - "upcoming": true, - "upcoming_display_mode": "scroll" + "ncaa_mens": { + "enabled": true, + "teams": { + "favorite_teams": ["NCAA_MENS_TOP_10", "JOHNS HOPKINS"], + "favorite_teams_only": false + }, + "filtering": { + "recent_games_to_show": 3, + "upcoming_games_to_show": 5 + }, + "live_priority": true }, - "teams": { - "favorite_teams": ["MARYLAND", "NORTH CAROLINA", "SYRACUSE"], - "favorite_teams_only": false, - "show_all_live": true + "ncaa_womens": { + "enabled": true, + "teams": { "favorite_teams": ["NCAA_WOMENS_TOP_10"] } } } } ``` -### Display modes per league - -Each of live / recent / upcoming can be independently enabled and given its own display style: - -- `switch` — one game card at a time, rotating on a timer -- `scroll` — all matching games composited into a horizontal ticker that scrolls across the display - -### Live priority - -When `live_priority: true`, live games for configured favorite teams will interrupt the normal rotation whenever they are in progress. - -### Favorite live boost - -`teams.favorite_live_boost` (default `2`, range `1`-`5`) tunes how much extra -attention your favorite team gets *within* the live rotation itself: while a -favorite's game is live, it's always queued first whenever the rotation -refreshes, and gets that many turns for every 1 turn other live games get -(e.g. `favorite_live_boost: 2` with your favorite plus two other live games -rotates `[favorite, other1, favorite, other2]`). It never interrupts a game -already on screen — it just gets more/sooner turns. Set it to `1` for a -perfectly even rotation (the pre-1.3.0 default behavior). This is independent -of `live_priority`, which controls whether live games preempt the -recent/upcoming rotation at all. - -### Shorter dwell for non-favorite live games - -`display_durations.non_favorite_live` (0-120, default `0` = off) gives live -games that involve **none** of your favorite teams a shorter on-screen turn than -your favorites. For example `ncaa_mens.display_durations.live: 30` with -`ncaa_mens.display_durations.non_favorite_live: 5` shows your teams for 30s each -while everyone else's games flash by in 5s. - -This **only takes effect** when favorite teams are configured **and** -non-favorite live games are being shown — `favorite_teams_only` off, or -`show_all_live` on (otherwise non-favorite games are never on screen to -shorten). Leave it at `0` to display every live game for `display_durations.live`. - -| Favorite teams set? | Non-favorite games shown? | Live game has a favorite? | Duration used | -|---|---|---|---| -| No | — | — | `display_durations.live` (unchanged) | -| Yes | No (`favorite_teams_only` on, `show_all_live` off) | favorite | `display_durations.live` | -| Yes | Yes (`favorite_teams_only` off, or `show_all_live` on) | favorite | `display_durations.live` | -| Yes | Yes (`favorite_teams_only` off, or `show_all_live` on) | none | `display_durations.non_favorite_live` (when > 0) | - -### Excluding teams (spoiler protection) +## Team abbreviations -`teams.exclude_teams` (default `[]`) hides specific teams from **both** the -live rotation and the recent/final-scores display — useful if you plan to -watch a game delayed and don't want the score spoiled. It uses the same -full-name abbreviation format as `favorite_teams` (see below), and always -wins if a team appears in both lists. - -### Timezone - -- `timezone` (Advanced): IANA name used to display event start times, e.g. - `America/Chicago`. Leave blank (the default) to follow the LEDMatrix global - timezone; if that isn't set, the host system's timezone is used, and only if - neither is available do times fall back to UTC. - -## Team Abbreviations - -**Important — NCAA lacrosse uses full-name abbreviations, not the short codes you may be used to from the football, basketball, or hockey plugins.** ESPN's lacrosse feed returns team abbreviations like `NORTH CAROLINA`, `JOHNS HOPKINS`, `SAINT JOSEPH'S`, not `UNC` / `JHU` / `SJU`. Use the full-name form in `favorite_teams` or the matching will fail silently. - -A few recurring examples (use exactly as shown, uppercase, with spaces, apostrophes, and periods as they appear): +**NCAA lacrosse uses full-name abbreviations, not the short codes you know from +the football, basketball, or hockey plugins.** ESPN's lacrosse feed returns +`NORTH CAROLINA`, `JOHNS HOPKINS`, `SAINT JOSEPH'S` — not `UNC`, `JHU`, `SJU`. +Use the full-name form or the match fails silently. | Team | Abbreviation | |---|---| @@ -209,220 +81,356 @@ A few recurring examples (use exactly as shown, uppercase, with spaces, apostrop | Saint Joseph's | `SAINT JOSEPH'S` | | Mount St. Mary's | `MOUNT ST. MARY'S` | | William & Mary | `WILLIAM & MARY` | -| Long Island University | `LONG ISLAND UNIVERSI` *(ESPN truncates to 20 chars)* | +| Long Island University | `LONG ISLAND UNIVERSI` *(ESPN truncates to 20 characters)* | -If you're unsure of a team's exact abbreviation, hit the ESPN scoreboard endpoint directly and look at `events[].competitions[].competitors[].team.abbreviation`: +Uppercase, with spaces, apostrophes, and periods exactly as they appear. To +check one: ```bash -curl -s 'https://site.api.espn.com/apis/site/v2/sports/lacrosse/mens-college-lacrosse/scoreboard' \ - | python -m json.tool | grep -A1 abbreviation +curl -s 'https://site.api.espn.com/apis/site/v2/sports/lacrosse/mens-college-lacrosse/scoreboard' | python -m json.tool | grep -A1 abbreviation ``` ### Dynamic team shortcuts -Instead of listing abbreviations manually, use one of these tokens in `favorite_teams` to auto-expand to the current poll: +Rather than listing abbreviations by hand, put one of these tokens in +`favorite_teams` and it expands to the current poll: | Token | League | Expands to | |---|---|---| -| `NCAA_MENS_TOP_5` | Men's | Top 5 of Inside Lacrosse D1 Men's Poll | -| `NCAA_MENS_TOP_10` | Men's | Top 10 of Inside Lacrosse D1 Men's Poll | -| `NCAA_MENS_TOP_20` | Men's | Full top 20 (the entire men's poll) | -| `NCAA_WOMENS_TOP_5` | Women's | Top 5 of IWLCA Coaches Poll | -| `NCAA_WOMENS_TOP_10` | Women's | Top 10 of IWLCA Coaches Poll | -| `NCAA_WOMENS_TOP_25` | Women's | Full top 25 | +| `NCAA_MENS_TOP_5` | Men's | Top 5 of the Inside Lacrosse D1 Men's Poll | +| `NCAA_MENS_TOP_10` | Men's | Top 10 of the same poll | +| `NCAA_MENS_TOP_20` | Men's | The full top 20 | +| `NCAA_WOMENS_TOP_5` | Women's | Top 5 of the IWLCA Coaches Poll | +| `NCAA_WOMENS_TOP_10` | Women's | Top 10 of the same poll | +| `NCAA_WOMENS_TOP_25` | Women's | The full top 25 | -Tokens can be mixed with literal abbreviations: `["NCAA_MENS_TOP_10", "JOHNS HOPKINS", "PRINCETON"]` tracks the current top 10 *plus* any of those two teams that aren't already in it. +Tokens mix with literal abbreviations: +`["NCAA_MENS_TOP_10", "JOHNS HOPKINS", "PRINCETON"]` tracks the current top ten +*plus* either of those two schools that is not already in it. -## Display Modes (plugin-level) +## Display modes -The plugin exposes six granular display modes the LEDMatrix host rotation can cycle through: +Six modes — three per league — that the LEDMatrix host rotation cycles through +independently. -- `lax_ncaa_mens_live`, `lax_ncaa_mens_recent`, `lax_ncaa_mens_upcoming` -- `lax_ncaa_womens_live`, `lax_ncaa_womens_recent`, `lax_ncaa_womens_upcoming` +![The three men's display modes](../../docs/assets/lacrosse-scoreboard/display-modes.png) -## Data Source +| Mode | Shows | Top line | +|---|---|---| +| `lax_ncaa_mens_live` | Men's games in progress | Quarter and clock — `Q3 8:12`, or `OT1` past the fourth | +| `lax_ncaa_mens_recent` | Finished men's games | `Final`, or `Final/OT` | +| `lax_ncaa_mens_upcoming` | Scheduled men's games | `Next Game`, then the date and start time | +| `lax_ncaa_womens_live` | Women's games in progress | As above | +| `lax_ncaa_womens_recent` | Finished women's games | As above | +| `lax_ncaa_womens_upcoming` | Scheduled women's games | As above | -Scores and schedules come from ESPN's public site API: +Each mode renders as **switch** (one game at a time, timed) or **scroll** (all +games scroll horizontally at high FPS), set per league and per mode with +`.display_modes._display_mode`. -- Men's scoreboard: `https://site.api.espn.com/apis/site/v2/sports/lacrosse/mens-college-lacrosse/scoreboard` -- Men's rankings: `https://site.api.espn.com/apis/site/v2/sports/lacrosse/mens-college-lacrosse/rankings` -- Women's scoreboard: `https://site.api.espn.com/apis/site/v2/sports/lacrosse/womens-college-lacrosse/scoreboard` -- Women's rankings: `https://site.api.espn.com/apis/site/v2/sports/lacrosse/womens-college-lacrosse/rankings` +## The two leagues -Team logos are fetched from `https://a.espncdn.com/i/teamlogos/ncaa/500/{team_id}.png` and cached locally under `assets/sports/ncaa_logos/`. +The two leagues are configured separately and have their own managers, their own +favorites, and their own poll. -## Favorite Team Result Colors +![Men's and women's live cards](../../docs/assets/lacrosse-scoreboard/leagues.png) -A run of games against the same opponent is hard to read at a glance: in scroll -and Vegas mode the same two logos go past several times and only the digits -change. Turn on **Customization -> Favorite Team Result Colors** to color a -finished game's score by how your favorite team did - green for a win, red for -a loss. +Their config blocks are **identical in every setting except one**: `ncaa_mens` +defaults to `enabled: true` and `ncaa_womens` to `enabled: false`. Every table +under [Per-league settings](#per-league-settings) applies verbatim to both, with +the prefix `ncaa_mens.` or `ncaa_womens.`. -```json -{ - "customization": { - "favorite_result_colors": { - "enabled": true, - "win_color": [0, 255, 0], - "loss_color": [255, 0, 0], - "tie_color": [255, 200, 0] - } - } -} -``` +Both are spring sports: men's runs roughly January to late May, women's February +to late May. Outside that window ESPN returns an empty schedule and there is +nothing to draw. -- Off by default. Until you enable it the score keeps exactly the color it has - today. -- Only finished games are colored. Live and upcoming cards are untouched. -- A game needs exactly one favorite team. If neither side is a favorite, or both - are, the score keeps its normal color. -- Applies to both the one-game-at-a-time switch view and the scroll/Vegas - ticker. -- The three colors are Advanced settings; leave them alone for the defaults - above. +## How games are chosen -## Troubleshooting +**`upcoming_games_to_show` is not "how many cards you see".** It is the size of +a *pool*. The panel cycles through that pool one card at a time and keeps its +place between visits, so a pool of 3 means the board rotates through the same 3 +games until the schedule moves on. A bigger number gives you a *longer lap*, so +any one game comes round **less** often. +Which regime you are in depends on that league's `teams.favorite_teams` and +`teams.favorite_teams_only`: +| `favorite_teams` | `favorite_teams_only` | What you get | +|---|---|---| +| empty | either | The next N games league-wide, chronologically. Every game is a non-favorite game, so the `other_*` filters apply to all of them. | +| set | **on** | Only your schools. The limit is a budget **per team**. | +| set | **off** (default) | **Your schools first, then other games to fill.** Both limits are **totals**. | -- **Start times look like UTC** (a 6:45pm Central start showing as 11:45PM): - the plugin couldn't read your global timezone. Set `timezone` under the - plugin's Advanced Settings to your IANA zone, e.g. `America/Chicago`. -**My favorite team doesn't show up.** You're almost certainly using a short abbreviation like `UNC` or `JHU`. Lacrosse abbreviations are the full school name in uppercase — see **Team Abbreviations** above. +Note the key is `favorite_teams_only`, not `show_favorite_teams_only` as in the +single-league scoreboards, and it defaults to **off** here. -**No games appear at all.** NCAA lacrosse is a spring sport. Men's runs roughly January through late May; women's runs February through late May. Outside that window, the ESPN scoreboard endpoint returns an empty `events[]` array and the plugin has nothing to display. +Within the other-games pool the better matchup leads and each team appears once. +The pool is each team's *next* game ordered by the best poll position of either +side, with ties falling back to start time. Your favorite schools are ordered by +when they play, not by rank: for your own team the next game is the point. -**Rank badges (`#1`, `#2`) aren't appearing.** Ensure `display_options.show_ranking: true` (the default). Rankings are cached for 1 hour and are only populated for teams that appear in the current poll. Unranked teams show no badge, which is intentional. +### Variety comes from turnover -**Shot totals are always 0.** ESPN's lacrosse feed does not currently expose per-team shot counts in the `competitors[].statistics` array the way hockey does for saves. The `show_shots` toggle is wired but will remain empty until ESPN publishes the stat. Leave it off for now. +Rather than widening the pool, the non-favorite slice **moves**: the window +advances by its own width every `filtering.other_rotation_interval_seconds`, so +consecutive windows do not overlap and the board works through the schedule +instead of resampling the front of it. Your favorites are not rotated. -**Tournament games show `TBD` placeholders.** ESPN uses team IDs `-1` and `-2` for bracket slots where the opponent hasn't been determined yet. The plugin renders these as text placeholders — they'll resolve to real logos once the bracket is set. +Both filters **fail open**: if the data behind them cannot be fetched, the game +is allowed through. They fail open a second time as a set — if the filters +between them leave nothing at all, the unfiltered list is used instead. Setting +`other_upcoming_games_to_show` or `other_recent_games_to_show` to `0` is the one +way to ask for an empty slate, and that is honoured. -**A team's logo is missing or looks wrong.** Delete the cached logo at `assets/sports/ncaa_logos/{ABBR}.png` (use the exact file name, spaces and all) and the plugin will re-download it from ESPN on the next update. +> **`other_games_divisions` does nothing here.** It needs ESPN's FBS/FCS group +> rosters, which are a college *football* taxonomy; no lookup is even attempted +> for lacrosse, so every game passes. `other_games_min_quality` **does** work in +> this plugin, because NCAA lacrosse has a national poll to rank against — its +> `ranked` default restricts non-favorite games to those involving a poll team. +> (The schema's help text for it mentions a `broadcast` option the enum does not +> offer.) -## Testing +### Live rotation -A standalone smoke test is included at `test_lacrosse_plugin.py`: +When several games are live at once the rotation is weighted: a game involving +one of your schools gets `teams.favorite_live_boost` turns for every one turn +other live games get, and is queued first whenever the rotation refreshes. Set +it to `1` for even rotation. A live game the API stops reporting for +`update_intervals.stale_game_timeout` seconds is dropped, so an abandoned game +does not sit on the board forever. -```bash -cd plugins/lacrosse-scoreboard -python test_lacrosse_plugin.py -``` +## Panel sizes -It stubs the LEDMatrix host modules, imports every plugin module, exercises the dynamic team resolver against live ESPN rankings, and runs a 50-event window of both men's and women's scoreboard data through `Lacrosse._extract_game_details`, asserting that required fields are populated. No external test framework is required. +The scorebug is laid out from the panel dimensions rather than a fixed grid. -## 🎯 Which Games Get Shown +![Live card at four panel sizes](../../docs/assets/lacrosse-scoreboard/panel-sizes.png) -**`upcoming_games_to_show` is not "how many cards you see".** It is the size of a *pool*. The panel cycles through that pool one card at a time and keeps its place between visits, so a pool of 3 means the board rotates through the same 3 games until the schedule moves on. Making the number bigger gives you a *longer lap*, so any one game comes round **less** often. +At 64x32 the two crests and the centre column share very little room; 128x32 or +wider is a much better fit for this card. -Which mode you are in depends on whether `favorite_teams` is set and whether `show_favorite_teams_only` is on: +## Settings reference -| `favorite_teams` | `show_favorite_teams_only` | What you get | -|---|---|---| -| empty | either | The next N games league-wide, chronologically. Every game shown is a non-favorite game, so the two filters below apply to all of them. | -| set | **on** | Only your teams. The limit is a budget **per team**. | -| set | **off** | **Your teams first, then other games to fill.** Both limits are **totals**. | +Settings marked **Advanced** sit behind the *Advanced* toggle in the web UI. +Defaults are the schema defaults, which is what the web UI writes. + +### Plugin level + +| Key | Type | Default | What it does | +|---|---|---|---| +| `enabled` | boolean | `false` | Master on/off switch for the whole plugin. | +| `timezone` | string | `""` | **Advanced.** IANA zone for start times, e.g. `America/New_York`. Blank follows the LEDMatrix global timezone, then the host system's, then UTC. | +| `schedule_lookback_days` | 1–60 | `14` | **Advanced.** How far back to fetch for the Recent screens. | +| `schedule_lookahead_days` | 1–60 | `7` | **Advanced.** How far ahead to fetch for Upcoming. A game beyond this horizon is never fetched, so it cannot reach the board even though the date is known. | +| `no_data_interval_seconds` | 5–86400 | `300` | **Advanced.** Wait between live checks when nothing is live. Backs off further the longer nothing is found. | +| `live_idle_max_interval_seconds` | 5–86400 | `900` | **Advanced.** Ceiling for that back-off. Useful out of season. | + +### Defaults + +Fallbacks used when the corresponding per-league setting is absent. + +| Key | Type | Default | What it does | +|---|---|---|---| +| `defaults.display_duration` | 5–60 s | `15` | Per-game on-screen time. | +| `defaults.show_records` | boolean | `false` | Draw win-loss records. | +| `defaults.show_ranking` | boolean | `false` | Draw poll rank badges. | +| `defaults.show_odds` | boolean | `false` | **Advanced.** Draw betting odds. | +| `defaults.show_shots` | boolean | `false` | **Advanced.** Draw shot totals. See the troubleshooting note — ESPN does not currently publish them. | +| `defaults.update_interval_seconds` | 30–86400 s | `3600` | **Advanced.** Base data refresh cadence. | +| `defaults.season_cache_duration_seconds` | 3600–604800 s | `86400` | **Advanced.** How long season data is cached. | + +> **The per-league copy wins, and its default is different.** Each league's +> `display_options.show_records` and `show_ranking` default to **`true`** while +> `defaults.show_records` and `defaults.show_ranking` default to **`false`**. +> Because the web UI writes schema defaults on save, a saved config already +> carries the per-league `true`, and changing the `defaults` value then appears +> to do nothing. Change the per-league setting. + +![show_records on and off](../../docs/assets/lacrosse-scoreboard/show-records.png) + +## Per-league settings -The third row is what most people want, and it did not exist before: with the flag off, favorites used to be ignored *entirely*. +Every table below exists twice, once under `ncaa_mens` and once under +`ncaa_womens`, with identical types and defaults. `` stands for either. -### The settings +### Enable and priority -| Option | Default | Description | +| Key | Type | Default | What it does | +|---|---|---|---| +| `.enabled` | boolean | `true` for `ncaa_mens`, `false` for `ncaa_womens` | Build this league's managers at all. The only setting that differs between the two. | +| `.live_priority` | boolean | `false` | Let this league's live games interrupt the rotation and display immediately. | + +### Display modes + +| Key | Type | Default | |---|---|---| -| `upcoming_games_to_show` | varies | How many **favorite** upcoming games to show. | -| `recent_games_to_show` | varies | The same, for finished games. | -| `other_upcoming_games_to_show` | matches `upcoming_games_to_show` | How many **non-favorite** upcoming games to add. `0` gives you favorites only. | -| `other_recent_games_to_show` | matches `recent_games_to_show` | The same, for finished games. | -| `other_rotation_interval_seconds` | `1800` | How often the non-favorite slice advances. `0` pins it. | -| `other_games_min_quality` | `ranked` | Which non-favorite games qualify: `ranked`, `broadcast`, or `any`. | -| `other_games_divisions` | `["fbs"]` | Which divisions non-favorite games may come from. College football only — see the note below. | +| `.display_modes.live` | boolean | `true` | +| `.display_modes.recent` | boolean | `true` | +| `.display_modes.upcoming` | boolean | `true` | +| `.display_modes.live_display_mode` | `switch` \| `scroll` | `switch` | +| `.display_modes.recent_display_mode` | `switch` \| `scroll` | `switch` | +| `.display_modes.upcoming_display_mode` | `switch` \| `scroll` | `switch` | -**Your favorite teams are never filtered by the last two** — follow a smaller-division team and its games always appear. Those settings only decide what fills the *remaining* slots. +### Teams -Within the other-games pool, **the better matchup leads**, and each team appears once. The pool is each team's *next* game ordered by the best poll position of either side, so a top-five matchup sits in the first window rather than whichever kicks off soonest — and the #1 team's whole season does not sort above everyone else's opener. Ties fall back to kickoff order, and a league with no poll keeps chronological order. Your favorite teams are ordered by when they play, not by rank -- for your own team the next game is the point. +| Key | Type | Default | What it does | +|---|---|---|---| +| `.teams.favorite_teams` | array | `[]` | Schools to prioritise. Full-name abbreviations, or a dynamic token. | +| `.teams.favorite_teams_only` | boolean | `false` | Show only your schools' games. | +| `.teams.show_all_live` | boolean | `false` | Show every live game regardless of favorites. | +| `.teams.exclude_teams` | array | `[]` | **Advanced.** Schools to always hide, from the live rotation and from finals alike (spoiler protection). Takes precedence over `favorite_teams` and `show_all_live`. | +| `.teams.favorite_live_boost` | 1–5 | `2` | **Advanced.** Turns a favorite's live game gets per one turn for other live games. `1` is even rotation. | -### Variety comes from turnover +### Filtering -Rather than widening the pool, the non-favorite slice **moves**: the window advances by its own width every `other_rotation_interval_seconds`, so consecutive windows do not overlap and the board works through the schedule instead of resampling the front of it. Your favorites are not rotated — for upcoming games the soonest ones are the point. +| Key | Type | Default | What it does | +|---|---|---|---| +| `.filtering.recent_games_to_show` | 1–20 | `5` | Pool size for finished games. With favorites, per team; without, in total. | +| `.filtering.upcoming_games_to_show` | 1–50 | `10` | The same for scheduled games. | +| `.filtering.other_upcoming_games_to_show` | 0–20 | `10` | **Advanced.** How many non-favorite upcoming games to add. `0` gives favorites only. | +| `.filtering.other_recent_games_to_show` | 0–20 | `5` | **Advanced.** The same for finished games. | +| `.filtering.other_rotation_interval_seconds` | 0–86400 s | `1800` | **Advanced.** How often the non-favorite window advances. `0` pins it. | +| `.filtering.other_games_min_quality` | `any` \| `ranked` | `ranked` | **Advanced.** Restrict non-favorite games to those involving a poll team. Works in this plugin — lacrosse has a national poll. | +| `.filtering.other_games_divisions` | array | `["fbs"]` | **Advanced.** Inert here; a college football taxonomy. | -Both filters **fail open**: if the data behind them cannot be fetched, the game is allowed through. A board showing filler is a poor board; a board showing nothing is a broken one. +### Update intervals -They fail open a second time, as a set: if the filters between them leave **nothing at all** — your teams idle and every other game rejected — the unfiltered list is used instead. Setting `other_upcoming_games_to_show` or `other_recent_games_to_show` to `0` is the one way to ask for an empty slate, and that is honoured. +All **Advanced**. -> `other_games_min_quality` needs a national poll, which only the college leagues publish — set to `ranked` in a professional league it lets every game through, and no poll is requested. `other_games_divisions` needs ESPN's FBS/FCS group rosters, which exist for **college football and nothing else**: asked for any other college league they come back empty or 500, so the setting is inert here and no lookup is made. +| Key | Type | Default | What it does | +|---|---|---|---| +| `.update_intervals.base` | 60–900 s | `300` | Base data refresh. | +| `.update_intervals.live` | 10–300 s | `60` | Refresh while a game is live. | +| `.update_intervals.recent` | 60–86400 s | `3600` | Refresh for finished games. | +| `.update_intervals.upcoming` | 60–86400 s | `3600` | Refresh for the schedule. | +| `.update_intervals.odds` | 60–86400 s | `3600` | Refresh for betting odds. | +| `.update_intervals.stale_game_timeout` | 60–3600 s | `300` | Drop a live game the API has stopped updating. | +### Display durations -## License +All **Advanced**. -See `LICENSE` in this directory. +| Key | Type | Default | What it does | +|---|---|---|---| +| `.display_durations.base` | 5–60 s | `15` | Fallback per-game time. | +| `.display_durations.live` | 5–120 s | `15` | Per-game time for live games. Applies to games with a favorite when a non-favorite duration is set. | +| `.display_durations.non_favorite_live` | 0–120 s | `0` | Shorter turn for live games with no favorite. Only applies when favorites are set **and** non-favorite live games are shown. `0` means use the live duration for everything. | +| `.display_durations.recent` | 5–60 s | `15` | Per-game time on the Recent screen. | +| `.display_durations.upcoming` | 5–60 s | `15` | Per-game time on the Upcoming screen. | -## Vegas ticker: seeing live games more often +### Display options -By default a live game **takes over** the display: the Vegas ticker stops and -this scoreboard shows full screen until the game ends. If you would rather keep -the marquee scrolling and still see scores, set this in the core config: +| Key | Type | Default | What it does | +|---|---|---|---| +| `.display_options.show_records` | boolean | `true` | Draw win-loss records. Overrides `defaults.show_records`. | +| `.display_options.show_ranking` | boolean | `true` | Draw poll rank badges (`#1`, `#2`). Unranked teams show no badge, by design. | +| `.display_options.show_odds` | boolean | `false` | **Advanced.** Draw betting odds. | +| `.display_options.show_shots` | boolean | `false` | **Advanced.** Draw shot totals when ESPN publishes them. | -```json -{ - "display": { - "vegas_scroll": { - "live_in_ticker": true, - "live_weight": 3, - "favorite_live_weight": 5 - } - } -} -``` +### Mode durations -The ticker is otherwise a strict round robin — every plugin appears once per -cycle — so with a dozen plugins enabled a score comes round once a lap. These -weights let this plugin claim several slots per cycle, spaced evenly through -it rather than bunched together. +How long the *whole mode* holds the board before the core rotates on. `null` +means use the dynamic calculation. All **Advanced**, and read by the LEDMatrix +core rather than by this plugin, which is why they do not appear in the plugin's +own source. -`live_weight` applies whenever this scoreboard has a live game. -`favorite_live_weight` applies when one of your `favorite_teams` is playing, so -your team's game comes round more often than other live games. That distinction -has to be made here rather than in the core, which can tell *that* a game is -live but not *whose*. +| Key | Type | Default | +|---|---|---| +| `.mode_durations.live_mode_duration` | 10–600 s or `null` | `null` | +| `.mode_durations.recent_mode_duration` | 10–600 s or `null` | `null` | +| `.mode_durations.upcoming_mode_duration` | 10–600 s or `null` | `null` | -Two things to keep in mind: +When a mode cycles back it continues from the last game shown rather than +restarting, so nothing repeats within a lap. -- The weight is per **plugin**, not per game. With four games live this - scoreboard still occupies one slot at a time and picks between its own games - using `favorite_live_boost`; these weights control how often the scoreboard - itself comes round. -- More slots make the cycle **longer**, not faster — everything else appears - proportionally less often. And appearing more often only helps if the data is - fresh, which is governed by this plugin's own live update interval. +### Dynamic duration -## Matchup separator and the upcoming card middle +Sizes each mode's total time from how much there is to show. All **Advanced**. -The **Matchup Card Layout** section (advanced) controls what sits between the -two team logos before a game starts, and how the date and time are written. -These settings now apply to every display mode -- the scroll ticker, the Vegas -ticker, and the full-screen scoreboard -- rather than only the tickers. +| Key | Type | Default | +|---|---|---| +| `.dynamic_duration.enabled` | boolean | `false` | +| `.dynamic_duration.max_duration_seconds` | 60–600 s | — | +| `.dynamic_duration.modes.live.enabled` | boolean | `false` | +| `.dynamic_duration.modes.live.max_duration_seconds` | 60–600 s | — | +| `.dynamic_duration.modes.recent.enabled` | boolean | `false` | +| `.dynamic_duration.modes.recent.max_duration_seconds` | 60–600 s | — | +| `.dynamic_duration.modes.upcoming.enabled` | boolean | `false` | +| `.dynamic_duration.modes.upcoming.max_duration_seconds` | 60–600 s | — | -| Setting | Key | Default | What it does | +### Scroll settings + +All **Advanced**, and per league. + +| Key | Type | Default | What it does | +|---|---|---|---| +| `.scroll_settings.scroll_speed` | 1.0–200.0 px/s | `50.0` | Higher scrolls faster. | +| `.scroll_settings.scroll_delay` | 0.001–0.1 s | `0.01` | Frame delay; `0.01` is 100 FPS. Lower is smoother. | +| `.scroll_settings.gap_between_games` | 8–128 px | `48` | Gap between game cards. | +| `.scroll_settings.show_league_separators` | boolean | `true` | Draw NCAA league icons between leagues. | +| `.scroll_settings.dynamic_duration` | boolean | `true` | Size the scroll duration from the content width. | +| `.scroll_settings.game_card_width` | 32–512 px | `128` | Card width. Lower it on a multi-panel chain to fit more games on screen at once. | + +## Fonts, colours and layout + +Seven text elements, each with `font`, `font_size`, and `text_color`, under +`customization.`. All **Advanced**. Available faces: +`PressStart2P-Regular.ttf`, `4x6-font.ttf`, `5by7.regular.ttf`. + +| Element | Default font | Default size | Draws | |---|---|---|---| -| Matchup Separator | `vs_text` | `VS` | Text drawn between the teams: `VS`, `@`, `at`, `v`. The away team is always on the left, so `@` and `at` read as "away at home". Blank draws nothing. | -| Middle of an Upcoming Card | `upcoming_center` | `vs` | Scroll and Vegas cards: the separator, the date and time stacked, or nothing. | -| Middle of a Full-Screen Upcoming Scoreboard | `switch_upcoming_center` | `date_time` | The same choice for the full-screen scoreboard, plus `inherit` to follow the setting above. It defaults to the stacked date and time, which is what this display has always shown, so nothing changes until you pick something else. | -| Date Format | `date_format` | `abbrev` | How the scroll and Vegas cards write the date: `Sep 19`, `9/19`, `19 Sep`, `19/9`, or `Fri Sep 19`. | -| Full-Screen Date Format | `switch_date_format` | `numeric` | The same choice for the full-screen scoreboard, plus `inherit` to follow the row above. It has its own default because the two displays disagree about what is normal: the cards have always written `Sep 19` and the full-screen scoreboard `9/19`, so a single shared default would restyle one of them. | -| Time Format | `time_format` | `12h` | 12- or 24-hour clock. | -| Show Date / Show Time | `show_date`, `show_time` | `true` | Drop either line. | -| Swap Date and Time | `swap_date_time` | `false` | Swap the two lines over. Each display starts from its own order, so this flips them rather than forcing one: the scroll and Vegas cards put the time on top, the full-screen date/time stack puts the date on top. | +| `score_text` | `PressStart2P-Regular.ttf` | `10` | The score, and the matchup separator on an upcoming card | +| `period_text` | `PressStart2P-Regular.ttf` | `8` | The quarter and clock, and the date/time on an upcoming scoreboard | +| `team_name` | `PressStart2P-Regular.ttf` | `8` | Team names and abbreviations | +| `status_text` | `4x6-font.ttf` | `6` | Status lines such as "Next Game" | +| `detail_text` | `4x6-font.ttf` | `6` | Small detail lines | +| `rank_text` | `PressStart2P-Regular.ttf` | `10` | Poll rank badges | +| `odds_text` | `4x6-font.ttf` | `6` | Betting odds (defaults to green, `[0, 255, 0]`) | + +Odds font sizes snap to the face's pixel grid to stay crisp: `4x6-font.ttf` +snaps to 7, 14, 21; press_start to 8, 16. Every `customization.` object +sets `additionalProperties: false`. -Choosing the separator for the full-screen scoreboard moves the date and time -out of the middle and onto the top and bottom rows, the same way the scroll -card lays them out; the "Next Game" header gives up the top row to them. +### Layout offsets -The center-gap settings in the same section size the scroll and Vegas card's -middle strip only -- the full-screen scoreboard pins its logos to the panel -edges and is unaffected. +Nudge any element in pixels. All default to `0`, all are **Advanced**, all live +under `customization.layout.`, and all set +`additionalProperties: false`. -Example: +| Element | Keys | Measured from | +|---|---|---| +| `home_logo`, `away_logo` | `x_offset`, `y_offset` | Default logo position | +| `score` | `x_offset`, `y_offset` | Panel centre | +| `status_text` | `x_offset`, `y_offset` | Centre horizontally, top vertically | +| `date` | `x_offset`, `y_offset` | Centre horizontally, default position vertically | +| `time` | `x_offset`, `y_offset` | Centre horizontally, the date's position vertically | +| `records` | `away_x_offset`, `home_x_offset`, `y_offset` | Away from the left, home from the right, both from the bottom | + +## Matchup separator and the upcoming card middle + +The **Matchup Card Layout** section (`scroll_card`) controls what sits between +the two crests before a game starts, and how the date and time are written. +These settings are plugin-wide, not per league, and apply to every display mode. + +| Setting | Key | Default | What it does | +|---|---|---|---| +| Matchup Separator | `scroll_card.vs_text` | `VS` | Text between the teams: `VS`, `@`, `at`, `v`. The away side is always on the left, so `@` and `at` read as "away at home". Blank draws nothing. | +| Middle of an Upcoming Card | `scroll_card.upcoming_center` | `vs` | Scroll and Vegas cards: `vs`, `date_time`, or `none`. | +| Middle of a Full-Screen Upcoming Scoreboard | `scroll_card.switch_upcoming_center` | `date_time` | The same choice for the full-screen scoreboard, plus `inherit` to follow the row above. | +| Date Format | `scroll_card.date_format` | `abbrev` | Scroll and Vegas cards: `Sep 19`, `9/19`, `19 Sep`, `19/9`, or `Fri Sep 19`. | +| Full-Screen Date Format | `scroll_card.switch_date_format` | `numeric` | **Advanced.** The same for the full-screen scoreboard, plus `inherit`. It has its own default because the two displays disagree about what is normal. | +| Time Format | `scroll_card.time_format` | `12h` | 12- or 24-hour clock. | +| Show Date / Show Time | `scroll_card.show_date`, `scroll_card.show_time` | `true` | Drop either line. | +| Swap Date and Time | `scroll_card.swap_date_time` | `false` | Flip the two lines. Each display starts from its own order, so this flips rather than forces. | + +The centre-gap settings size the scroll and Vegas card's middle strip only — the +full-screen scoreboard pins its crests to the panel edges and is unaffected. + +| Key | Type | Default | What it does | +|---|---|---|---| +| `scroll_card.center_gap` | 0–64 px | unset | Pixels kept clear down the middle. Unset scales with card width; `0` restores edge-to-edge logos. | +| `scroll_card.center_gap_ratio` | 0.0–0.6 | `0.28` | **Advanced.** Fraction of card width used when the gap is not pinned. | +| `scroll_card.center_gap_min` | 0–64 px | `22` | **Advanced.** Floor for the scaled gap. | +| `scroll_card.center_gap_max` | 0–96 px | `40` | **Advanced.** Ceiling for the scaled gap. | ```json { @@ -434,24 +442,12 @@ Example: } ``` -### Text Colours - -Each text element in the **Customization** section carries a colour, and it now -applies to the text drawn in that element's face — on the full-screen scoreboard -and on the scroll and Vegas cards alike. Until this version the picker changed -only which font was loaded; every string was drawn white. +## Text colours -| Element | Key | Colours | -|---|---|---| -| Score | `score_text` | The score, and the matchup separator on an upcoming card | -| Period / clock | `period_text` | The clock, period, and the date and time on an upcoming scoreboard | -| Team name | `team_name` | Team names and abbreviations | -| Status | `status_text` | Status lines such as "Next Game" | -| Detail | `detail_text` | Small detail lines | -| Ranking | `rank_text` | Team rankings drawn in the ranking face | - -Colours are `[r, g, b]` or `"#RRGGBB"`, and every default is white, so a display -nobody has recoloured looks exactly as it did. +Each `customization..text_color` colours the text drawn in that +element's face, on the full-screen scoreboard and on the scroll and Vegas cards +alike. Colours are `[r, g, b]` or `"#RRGGBB"`. Every default is white except +`odds_text`, which is green. ```json { @@ -462,8 +458,169 @@ nobody has recoloured looks exactly as it did. } ``` -Two things keep their own colours on purpose: the betting-odds figures, which -are coloured by which side is favoured, and a finished game's score when -**Favorite Team Result Colors** is on — that tint wins, and your score colour -shows on every other game. Records and rankings drawn in the small fixed face -stay white; no element in the schema owns that face. +The betting-odds figures keep their own colours — they are tinted by which side +is favoured — and so does a finished game's score when **Favorite Team Result +Colors** is on. + +## Favorite team result colours + +A run of games against the same opponent is hard to read at a glance: in scroll +and Vegas mode the same two crests go past several times and only the digits +change. Turn this on to colour a finished game's score by how your school did. + +| Key | Type | Default | +|---|---|---| +| `customization.favorite_result_colors.enabled` | boolean | `false` | +| `customization.favorite_result_colors.win_color` | `[r, g, b]` | `[0, 255, 0]` | +| `customization.favorite_result_colors.loss_color` | `[r, g, b]` | `[255, 0, 0]` | +| `customization.favorite_result_colors.tie_color` | `[r, g, b]` | `[255, 200, 0]` | + +```json +{ + "customization": { + "favorite_result_colors": { + "enabled": true, + "win_color": [0, 255, 0], + "loss_color": [255, 0, 0], + "tie_color": [255, 200, 0] + } + } +} +``` + +- Only finished games are coloured; live and upcoming cards are untouched. +- A game needs **exactly one** favorite school. Neither side or both, and the + score keeps its normal colour. +- The three colours are Advanced settings. + +This tint is applied by the LEDMatrix core rather than by the plugin, which is +why the keys do not appear in this plugin's source. + +## Vegas ticker: seeing live games more often + +By default a live game **takes over** the display: the Vegas ticker stops and +this scoreboard shows full screen until the game ends. To keep the marquee +scrolling and still see scores, set this in the **core** config — not in this +plugin's settings: + +```json +{ + "display": { + "vegas_scroll": { + "live_in_ticker": true, + "live_weight": 3, + "favorite_live_weight": 5 + } + } +} +``` + +The ticker is otherwise a strict round robin — every plugin appears once per +cycle. These weights let this plugin claim several slots per cycle, spaced +evenly through it rather than bunched together. `live_weight` applies whenever +this scoreboard has a live game; `favorite_live_weight` applies when one of your +schools is playing. That distinction has to be made here rather than in the +core, which can tell *that* a game is live but not *whose*. + +- The weight is per **plugin**, not per game. With four games live this + scoreboard still occupies one slot at a time and picks between its own games + using `favorite_live_boost`. +- More slots make the cycle **longer**, not faster. + +## Data source + +ESPN's public site API, no key required: + +- Men's scoreboard: `https://site.api.espn.com/apis/site/v2/sports/lacrosse/mens-college-lacrosse/scoreboard` +- Men's rankings: `https://site.api.espn.com/apis/site/v2/sports/lacrosse/mens-college-lacrosse/rankings` +- Women's scoreboard: `https://site.api.espn.com/apis/site/v2/sports/lacrosse/womens-college-lacrosse/scoreboard` +- Women's rankings: `https://site.api.espn.com/apis/site/v2/sports/lacrosse/womens-college-lacrosse/rankings` + +Crests are fetched from `https://a.espncdn.com/i/teamlogos/ncaa/500/{team_id}.png` +and cached under `assets/sports/ncaa_logos/`. Rankings are cached for an hour. + +## Requirements and installation + +- Python 3.9+ +- LEDMatrix core 2.0.0 or newer +- A 64x32 panel minimum; 128x32 or wider recommended +- Internet access to reach ESPN + +Install from the Plugin Store in the LEDMatrix web UI: open +`http://your-pi-ip:5000`, go to **Plugin Manager**, find **Lacrosse Scoreboard** +under **Plugin Store**, and click **Install**. Crests download on first sight. + +Manual install from source: + +```bash +cd /path/to/LEDMatrix +python -m pip install --user pillow requests pytz +cp -r /path/to/ledmatrix-plugins/plugins/lacrosse-scoreboard plugin-repos/ +sudo systemctl restart ledmatrix +``` + +Dependencies, from `requirements.txt`: `Pillow>=9.0.0` (image compositing), +`requests>=2.28.0` (ESPN calls), `pytz>=2022.1` (timezone conversion), and +`urllib3>=1.26.0` (HTTP retry logic). All are present in a typical LEDMatrix +install. + +## Testing + +A standalone smoke test is included: + +```bash +cd plugins/lacrosse-scoreboard && python test_lacrosse_plugin.py +``` + +It stubs the LEDMatrix host modules, imports every plugin module, exercises the +dynamic team resolver against live ESPN rankings, and runs a 50-event window of +both men's and women's scoreboard data through `Lacrosse._extract_game_details`, +asserting the required fields are populated. No test framework required. + +The documentation images come from `docs/assets/lacrosse-scoreboard/shots.json` +and re-render with `python scripts/render_docs_assets.py --plugin +lacrosse-scoreboard --check`. + +## Troubleshooting + +**My favorite team doesn't show up.** You are almost certainly using a short +abbreviation like `UNC` or `JHU`. Lacrosse abbreviations are the full school +name in uppercase — see [Team abbreviations](#team-abbreviations). + +**No games appear at all.** Both are spring sports. Men's runs roughly January +through late May, women's February through late May. Outside that window ESPN +returns an empty schedule and there is nothing to draw. Check too that both +`enabled` and the league's own `enabled` are on — `ncaa_womens` is off by +default. + +**Records or rank badges are on when I turned them off.** You changed the +`defaults` copy. The per-league `display_options` copy wins and defaults to +`true`; change that one. + +**Start times look like UTC.** The plugin could not read your global timezone. +Set `timezone` under Advanced Settings to your IANA zone, e.g. +`America/New_York`. + +**Rank badges aren't appearing.** Confirm `.display_options.show_ranking` +is `true`. Rankings cache for an hour and only populate for teams in the current +poll; unranked teams show no badge by design. + +**Shot totals are always 0.** ESPN's lacrosse feed does not expose per-team shot +counts the way its hockey feed exposes saves. `show_shots` is wired but stays +empty until ESPN publishes the stat. Leave it off. + +**Tournament games show `TBD` placeholders.** ESPN uses team IDs `-1` and `-2` +for bracket slots with no opponent yet. These resolve to real crests once the +bracket is set. + +**A crest is missing or wrong.** Delete the cached file at +`assets/sports/ncaa_logos/{ABBR}.png` — the exact file name, spaces and all — +and it re-downloads on the next update. + +**A game I know about never appears.** It may be beyond +`schedule_lookahead_days` (default 7). A game outside that horizon is never +fetched. + +## License + +See `LICENSE`. diff --git a/plugins/lacrosse-scoreboard/manifest.json b/plugins/lacrosse-scoreboard/manifest.json index a115a0de..dc41ff7e 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.1", + "version": "1.24.2", "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", @@ -50,6 +50,16 @@ } ], "versions": [ + { + "version": "1.24.2", + "released": "2026-09-02", + "ledmatrix_min_version": "3.3.0", + "notes": "Fix every grid-snapped font rendering a pixel narrow. The shared code reads this plugin's config_schema.json to tell a default font size from one the user chose; a default gets snapped to the font's pixel grid, a choice is left alone. It located the schema by inspecting loaded modules, which fails under the real plugin loader -- the loader renames a plugin's modules and removes their original names, so nothing was left to inspect. The lookup returned nothing, every size then looked user-chosen, and the snap was skipped: 4x6-font.ttf drew at 6 instead of 7, which is 3-pixel-wide glyphs instead of 4. On a 256x64 panel the betting odds, team records and the date row were hard to read. The plugin now tells the shared code where it lives instead of leaving it to guess.", + "changelog": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage.", + "changes": [ + "Rewrote the README as a complete settings reference covering all 170 settings, with rendered examples of every display mode and both leagues." + ] + }, { "version": "1.24.1", "released": "2026-09-02", @@ -337,7 +347,7 @@ { "released": "2026-07-02", "version": "1.3.0", - "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).", + "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).", "ledmatrix_min": "2.0.0" }, { diff --git a/scripts/docs_render_support/sitecustomize.py b/scripts/docs_render_support/sitecustomize.py index 44eafa3a..64fbb438 100644 --- a/scripts/docs_render_support/sitecustomize.py +++ b/scripts/docs_render_support/sitecustomize.py @@ -89,12 +89,59 @@ def advance(seconds): _WANTED = _json.loads(_ATTRS) _TARGET = "src.plugin_system.plugin_loader" + def _resolve(instance, path): + """Walk a dotted path to the object that owns the final attribute. + + The sports scoreboards keep their per-mode state on sub-managers rather + than on the plugin -- self._managers["live"].current_game -- so a shot + that can only set top-level attributes cannot reach the game it wants + to draw. A segment is tried as a mapping key first, then as an + attribute, so both self._managers["live"] and self.live_manager work. + """ + parts = path.split(".") + target = instance + for part in parts[:-1]: + if hasattr(target, "get") and not hasattr(target, part): + nxt = target.get(part) + else: + nxt = getattr(target, part, None) + if nxt is None and hasattr(target, "get"): + nxt = target.get(part) + if nxt is None: + return None, None + target = nxt + return target, parts[-1] + + def _coerce(name, value): + """Turn JSON into the shapes the plugins actually hold. + + A shots file can only carry JSON, but plugin state is not all strings + and lists: colours are tuples throughout the core, and a logo field is + a pathlib.Path -- the sports renderers call logo_path.parent, so a + string there raises AttributeError and the card silently fails to draw. + """ + if isinstance(value, list) and len(value) == 3 and all( + isinstance(v, int) for v in value): + return tuple(value) # colours are tuples everywhere in the core + if isinstance(value, str) and name.endswith("_path") and value: + import pathlib as _pathlib + return _pathlib.Path(value) + if isinstance(value, dict): + return {k: _coerce(k, v) for k, v in value.items()} + if isinstance(value, list): + return [_coerce(name, v) for v in value] + return value + def _apply(instance): for name, value in _WANTED.items(): - if isinstance(value, list) and len(value) == 3 and all( - isinstance(v, int) for v in value): - value = tuple(value) # colours are tuples everywhere in the core - setattr(instance, name, value) + value = _coerce(name.rsplit(".", 1)[-1], value) + owner, attr = _resolve(instance, name) + if owner is None: + continue # the path does not exist on this plugin; leave it alone + if hasattr(owner, "__setitem__") and not hasattr(owner, attr): + owner[attr] = value + else: + setattr(owner, attr, value) return instance class _PatchingLoader: diff --git a/scripts/render_docs_assets.py b/scripts/render_docs_assets.py index fa38f5f9..2ca204e6 100644 --- a/scripts/render_docs_assets.py +++ b/scripts/render_docs_assets.py @@ -268,6 +268,9 @@ def render_shot( mock_path = resolve_mock_data(mock_spec, shot_list_dir, tmpdir, name) if mock_path: cmd += ["--mock-data", str(mock_path)] + display_mode = shot.get("display_mode", defaults.get("display_mode")) + if display_mode: + cmd += ["--display-mode", str(display_mode)] if shot.get("skip_update", defaults.get("skip_update", False)): cmd.append("--skip-update") From 075bb7cd664d66df4064de7c481b9c7329aab286 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:33:31 -0400 Subject: [PATCH 2/3] docs(lacrosse-scoreboard): keep the lax_ prefix upgrade notice The rewrite dropped the breaking-change notice from 1.1.0, when the display modes gained a lax_ prefix to stop colliding with the NCAA hockey modes. Anyone still holding an old pin in display_durations or rotation_order needs it, and the CHANGELOG mapping is the only other place it is written down. Co-Authored-By: Claude Opus 5 --- plugins/lacrosse-scoreboard/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/lacrosse-scoreboard/README.md b/plugins/lacrosse-scoreboard/README.md index d983536b..a6b9a007 100644 --- a/plugins/lacrosse-scoreboard/README.md +++ b/plugins/lacrosse-scoreboard/README.md @@ -5,6 +5,13 @@ LEDMatrix display, from ESPN's public API. No API key required. ![NCAA men's lacrosse live scorebug](../../docs/assets/lacrosse-scoreboard/hero.png) +> **Upgrading from before 1.1.0:** the display modes gained a `lax_` prefix +> (`lax_ncaa_mens_recent` rather than `ncaa_mens_recent`) so they no longer +> collide with the NCAA hockey modes that `hockey-scoreboard` exposes. If you +> pinned any of the old unprefixed names in `display_durations`, +> `rotation_order`, or anywhere else in `config.json`, update them. The +> [CHANGELOG](CHANGELOG.md) has the full mapping. + ## Contents - [Quick start](#quick-start) From cd8670ee3185067429fe4f6a9a7bafe64f114d0d Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:28:00 -0400 Subject: [PATCH 3/3] fix(lacrosse-scoreboard): repair the manifest after the merge The merge from main left the 1.24.2 entry holding both sides bodies -- two notes keys, two changelog keys, and a missing comma between them -- so manifest.json was not valid JSON and the version gate could not read it. Both sides had added a 1.24.2: main from the font fix in #409, this branch from the README rewrite. Rebuilt from main manifest, which is intact, with the docs change moved to a new 1.24.3 entry above it. That keeps main 1.24.2 exactly as merged and follows the shape the other docs PRs used on main -- the new entry carries the previous entry notes and changelog plus its own changes array. plugins.json regenerated: latest_version 1.24.3. Co-Authored-By: Claude Opus 5 --- plugins.json | 2 +- plugins/lacrosse-scoreboard/manifest.json | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins.json b/plugins.json index 6ee646c5..1750ced9 100644 --- a/plugins.json +++ b/plugins.json @@ -359,7 +359,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.24.2", + "latest_version": "1.24.3", "icon": "fas fa-baseball-ball" }, { diff --git a/plugins/lacrosse-scoreboard/manifest.json b/plugins/lacrosse-scoreboard/manifest.json index c65a06e8..14164aa5 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.2", + "version": "1.24.3", "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", @@ -51,14 +51,19 @@ ], "versions": [ { - "version": "1.24.2", + "version": "1.24.3", "released": "2026-09-02", "ledmatrix_min_version": "3.3.0", - "notes": "Fix every grid-snapped font rendering a pixel narrow. The shared code reads this plugin's config_schema.json to tell a default font size from one the user chose; a default gets snapped to the font's pixel grid, a choice is left alone. It located the schema by inspecting loaded modules, which fails under the real plugin loader -- the loader renames a plugin's modules and removes their original names, so nothing was left to inspect. The lookup returned nothing, every size then looked user-chosen, and the snap was skipped: 4x6-font.ttf drew at 6 instead of 7, which is 3-pixel-wide glyphs instead of 4. On a 256x64 panel the betting odds, team records and the date row were hard to read. The plugin now tells the shared code where it lives instead of leaving it to guess.", + "notes": "Render the font the settings page advertises. Each text element's schema names a default font, but several elements never passed it to the loader, so the loader used its own fallback instead: the status text -- and on hockey the detail and odds text too -- advertised the 4x6 font while actually drawing in PressStart2P. Choosing the font the picker already showed as active therefore changed the display, which looked like the picker was broken. Each element now renders what its schema declares. This affects panels 128 pixels wide and narrower, on upcoming screens only; wider panels draw that header in the time font and are unchanged. It also fixes a knock-on problem: \"Next Game\" needed 72px in the wrong font and had to shorten to \"Next\" on a 64px panel, where it now fits whole at 41px.", "changelog": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage.", "changes": [ "Rewrote the README as a complete settings reference covering all 170 settings, with rendered examples of every display mode and both leagues." ] + }, + { + "version": "1.24.2", + "released": "2026-09-02", + "ledmatrix_min_version": "3.3.0", "notes": "Render the font the settings page advertises. Each text element's schema names a default font, but several elements never passed it to the loader, so the loader used its own fallback instead: the status text -- and on hockey the detail and odds text too -- advertised the 4x6 font while actually drawing in PressStart2P. Choosing the font the picker already showed as active therefore changed the display, which looked like the picker was broken. Each element now renders what its schema declares. This affects panels 128 pixels wide and narrower, on upcoming screens only; wider panels draw that header in the time font and are unchanged. It also fixes a knock-on problem: \"Next Game\" needed 72px in the wrong font and had to shorten to \"Next\" on a 64px panel, where it now fits whole at 41px.", "changelog": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage." },