diff --git a/docs/assets/soccer-scoreboard/display-modes.png b/docs/assets/soccer-scoreboard/display-modes.png new file mode 100644 index 00000000..b8519aa8 Binary files /dev/null and b/docs/assets/soccer-scoreboard/display-modes.png differ diff --git a/docs/assets/soccer-scoreboard/final-states.png b/docs/assets/soccer-scoreboard/final-states.png new file mode 100644 index 00000000..8d39fc9e Binary files /dev/null and b/docs/assets/soccer-scoreboard/final-states.png differ diff --git a/docs/assets/soccer-scoreboard/hero.png b/docs/assets/soccer-scoreboard/hero.png new file mode 100644 index 00000000..f51c9fe7 Binary files /dev/null and b/docs/assets/soccer-scoreboard/hero.png differ diff --git a/docs/assets/soccer-scoreboard/panel-sizes.png b/docs/assets/soccer-scoreboard/panel-sizes.png new file mode 100644 index 00000000..ded027a0 Binary files /dev/null and b/docs/assets/soccer-scoreboard/panel-sizes.png differ diff --git a/docs/assets/soccer-scoreboard/period-states.png b/docs/assets/soccer-scoreboard/period-states.png new file mode 100644 index 00000000..38d3baad Binary files /dev/null and b/docs/assets/soccer-scoreboard/period-states.png differ diff --git a/docs/assets/soccer-scoreboard/shots.json b/docs/assets/soccer-scoreboard/shots.json new file mode 100644 index 00000000..96a7ca62 --- /dev/null +++ b/docs/assets/soccer-scoreboard/shots.json @@ -0,0 +1,1311 @@ +{ + "plugin": "soccer-scoreboard", + "_comment": "Soccer keeps one manager per league and mode as a plain attribute named from the league slug -- eng.1 becomes eng1_live, eng1_recent, eng1_upcoming -- and only builds them for leagues whose enabled flag is set, so the config below turns the Premier League on and everything else off. Unlike the other scoreboards there are only three display modes in total (soccer_live/recent/upcoming), shared across every league, rather than three per league. Every mode's display() returns early unless games_list is populated, and the live path also reads live_games and active_celebration. The card reads home_logo_path/away_logo_path off the game dict as Path objects, and period_text is what soccer_managers.py builds for that state: 1H, 2H, HALF, ET1, ET2, ETH, PEN while live, and Final, F/ET or F/Pen once over. Teams, scores and records are invented.", + "defaults": { + "width": 128, + "height": 32, + "scale": 6, + "freeze_time": "2026-04-12T14:00:00+00:00", + "skip_update": true, + "config": { + "enabled": true, + "leagues": { + "eng.1": { + "enabled": true + }, + "esp.1": { + "enabled": false + }, + "ger.1": { + "enabled": false + }, + "ita.1": { + "enabled": false + }, + "fra.1": { + "enabled": false + }, + "usa.1": { + "enabled": false + }, + "por.1": { + "enabled": false + } + } + } + }, + "shots": [ + { + "name": "hero", + "display_mode": "soccer_live", + "attrs": { + "eng1_live.games_list": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + }, + { + "name": "m-live", + "display_mode": "soccer_live", + "standalone": false, + "attrs": { + "eng1_live.games_list": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + }, + { + "name": "m-recent", + "display_mode": "soccer_recent", + "standalone": false, + "attrs": { + "eng1_recent.games_list": [ + { + "id": "2", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "Final", + "status_text": "Final", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": true, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_recent.current_game": { + "id": "2", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "Final", + "status_text": "Final", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": true, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_recent.current_game_index": 0, + "eng1_recent.last_game_switch": 9000000000.0, + "eng1_recent.last_update": 9000000000.0 + } + }, + { + "name": "m-upcoming", + "display_mode": "soccer_upcoming", + "standalone": false, + "attrs": { + "eng1_upcoming.games_list": [ + { + "id": "3", + "away_abbr": "MCI", + "home_abbr": "CHE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "", + "status_text": "", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/MCI.png", + "home_logo_path": "assets/sports/soccer_logos/CHE.png" + } + ], + "eng1_upcoming.current_game": { + "id": "3", + "away_abbr": "MCI", + "home_abbr": "CHE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "", + "status_text": "", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/MCI.png", + "home_logo_path": "assets/sports/soccer_logos/CHE.png" + }, + "eng1_upcoming.current_game_index": 0, + "eng1_upcoming.last_game_switch": 9000000000.0, + "eng1_upcoming.last_update": 9000000000.0 + } + }, + { + "name": "p-half", + "display_mode": "soccer_live", + "standalone": false, + "attrs": { + "eng1_live.games_list": [ + { + "id": "7", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "1", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "HALF", + "status_text": "HALF", + "odds": null, + "is_halftime": true, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "7", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "1", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "HALF", + "status_text": "HALF", + "odds": null, + "is_halftime": true, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "7", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "1", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "HALF", + "status_text": "HALF", + "odds": null, + "is_halftime": true, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + }, + { + "name": "p-et", + "display_mode": "soccer_live", + "standalone": false, + "attrs": { + "eng1_live.games_list": [ + { + "id": "8", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "ET2", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "8", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "ET2", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "8", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "ET2", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + }, + { + "name": "p-pen", + "display_mode": "soccer_live", + "standalone": false, + "attrs": { + "eng1_live.games_list": [ + { + "id": "9", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "PEN", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "9", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "PEN", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "9", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "PEN", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + }, + { + "name": "f-et", + "display_mode": "soccer_recent", + "standalone": false, + "attrs": { + "eng1_recent.games_list": [ + { + "id": "5", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "3", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "F/ET", + "status_text": "F/ET", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": true, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_recent.current_game": { + "id": "5", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "3", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "F/ET", + "status_text": "F/ET", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": true, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_recent.current_game_index": 0, + "eng1_recent.last_game_switch": 9000000000.0, + "eng1_recent.last_update": 9000000000.0 + } + }, + { + "name": "f-pen", + "display_mode": "soccer_recent", + "standalone": false, + "attrs": { + "eng1_recent.games_list": [ + { + "id": "6", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "F/Pen", + "status_text": "F/Pen", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": true, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_recent.current_game": { + "id": "6", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "2", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "F/Pen", + "status_text": "F/Pen", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": true, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_recent.current_game_index": 0, + "eng1_recent.last_game_switch": 9000000000.0, + "eng1_recent.last_update": 9000000000.0 + } + }, + { + "name": "rec-on", + "display_mode": "soccer_upcoming", + "standalone": false, + "attrs": { + "eng1_upcoming.games_list": [ + { + "id": "3", + "away_abbr": "MCI", + "home_abbr": "CHE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "", + "status_text": "", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/MCI.png", + "home_logo_path": "assets/sports/soccer_logos/CHE.png" + } + ], + "eng1_upcoming.current_game": { + "id": "3", + "away_abbr": "MCI", + "home_abbr": "CHE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "", + "status_text": "", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/MCI.png", + "home_logo_path": "assets/sports/soccer_logos/CHE.png" + }, + "eng1_upcoming.current_game_index": 0, + "eng1_upcoming.last_game_switch": 9000000000.0, + "eng1_upcoming.last_update": 9000000000.0 + }, + "config": { + "enabled": true, + "leagues": { + "eng.1": { + "enabled": true + }, + "esp.1": { + "enabled": false + }, + "ger.1": { + "enabled": false + }, + "ita.1": { + "enabled": false + }, + "fra.1": { + "enabled": false + }, + "usa.1": { + "enabled": false + }, + "por.1": { + "enabled": false + } + }, + "show_records": true + } + }, + { + "name": "rec-off", + "display_mode": "soccer_upcoming", + "standalone": false, + "attrs": { + "eng1_upcoming.games_list": [ + { + "id": "3", + "away_abbr": "MCI", + "home_abbr": "CHE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "", + "status_text": "", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/MCI.png", + "home_logo_path": "assets/sports/soccer_logos/CHE.png" + } + ], + "eng1_upcoming.current_game": { + "id": "3", + "away_abbr": "MCI", + "home_abbr": "CHE", + "away_id": "1", + "home_id": "2", + "away_score": "", + "home_score": "", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "", + "status_text": "", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": false, + "is_final": false, + "is_upcoming": true, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/MCI.png", + "home_logo_path": "assets/sports/soccer_logos/CHE.png" + }, + "eng1_upcoming.current_game_index": 0, + "eng1_upcoming.last_game_switch": 9000000000.0, + "eng1_upcoming.last_update": 9000000000.0 + }, + "config": { + "enabled": true, + "leagues": { + "eng.1": { + "enabled": true + }, + "esp.1": { + "enabled": false + }, + "ger.1": { + "enabled": false + }, + "ita.1": { + "enabled": false + }, + "fra.1": { + "enabled": false + }, + "usa.1": { + "enabled": false + }, + "por.1": { + "enabled": false + } + }, + "show_records": false + } + }, + { + "name": "size-64x32", + "width": 64, + "height": 32, + "display_mode": "soccer_live", + "standalone": false, + "attrs": { + "eng1_live.games_list": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + }, + { + "name": "size-128x32", + "width": 128, + "height": 32, + "display_mode": "soccer_live", + "standalone": false, + "attrs": { + "eng1_live.games_list": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + }, + { + "name": "size-128x64", + "width": 128, + "height": 64, + "display_mode": "soccer_live", + "standalone": false, + "attrs": { + "eng1_live.games_list": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + }, + { + "name": "size-256x32", + "width": 256, + "height": 32, + "display_mode": "soccer_live", + "standalone": false, + "attrs": { + "eng1_live.games_list": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.current_game": { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + }, + "eng1_live.current_game_index": 0, + "eng1_live.last_game_switch": 9000000000.0, + "eng1_live.last_update": 9000000000.0, + "eng1_live.live_games": [ + { + "id": "1", + "away_abbr": "ARS", + "home_abbr": "LIV", + "away_id": "1", + "home_id": "2", + "away_score": "1", + "home_score": "2", + "away_record": "18-6-4", + "home_record": "21-4-3", + "period_text": "2H", + "status_text": "LIVE", + "odds": null, + "is_halftime": false, + "is_period_break": false, + "is_live": true, + "is_final": false, + "is_upcoming": false, + "game_date": "4/12", + "game_time": "3:00pm", + "away_logo_path": "assets/sports/soccer_logos/ARS.png", + "home_logo_path": "assets/sports/soccer_logos/LIV.png" + } + ], + "eng1_live.active_celebration": null + } + } + ], + "composites": [ + { + "name": "display-modes", + "columns": 1, + "cells": [ + { + "shot": "m-live", + "label": "soccer_live", + "sublabel": "half and the running score" + }, + { + "shot": "m-recent", + "label": "soccer_recent", + "sublabel": "the finished match" + }, + { + "shot": "m-upcoming", + "label": "soccer_upcoming", + "sublabel": "kick-off date and time" + } + ] + }, + { + "name": "period-states", + "columns": 1, + "cells": [ + { + "shot": "m-live", + "label": "2H", + "sublabel": "second half; 1H before the break" + }, + { + "shot": "p-half", + "label": "HALF", + "sublabel": "half-time; ETH at the extra-time break" + }, + { + "shot": "p-et", + "label": "ET2", + "sublabel": "extra time, second half" + }, + { + "shot": "p-pen", + "label": "PEN", + "sublabel": "penalty shootout in progress" + } + ] + }, + { + "name": "final-states", + "columns": 1, + "cells": [ + { + "shot": "m-recent", + "label": "Final", + "sublabel": "decided in 90 minutes" + }, + { + "shot": "f-et", + "label": "F/ET", + "sublabel": "decided in extra time" + }, + { + "shot": "f-pen", + "label": "F/Pen", + "sublabel": "decided on penalties" + } + ] + }, + { + "name": "show-records", + "columns": 1, + "cells": [ + { + "shot": "rec-on", + "label": "show_records: true", + "sublabel": "the plugin-level key, which is the one that is read" + }, + { + "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/soccer-scoreboard/show-records.png b/docs/assets/soccer-scoreboard/show-records.png new file mode 100644 index 00000000..04243a04 Binary files /dev/null and b/docs/assets/soccer-scoreboard/show-records.png differ diff --git a/plugins/soccer-scoreboard/README.md b/plugins/soccer-scoreboard/README.md index 343249c6..dbd1e00e 100644 --- a/plugins/soccer-scoreboard/README.md +++ b/plugins/soccer-scoreboard/README.md @@ -1,374 +1,576 @@ ------------------------------------------------------------------------------------ ### Connect with ChuckBuilds -- Show support on Youtube: https://www.youtube.com/@ChuckBuilds +- Show support on YouTube: https://www.youtube.com/@ChuckBuilds - Stay in touch on Instagram: https://www.instagram.com/ChuckBuilds/ - Want to chat or need support? Reach out on the ChuckBuilds Discord: https://discord.com/invite/uW36dVAtcT -- Feeling Generous? Support the project: - - Github Sponsorship: https://github.com/sponsors/ChuckBuilds +- Feeling generous? Support the project: + - GitHub Sponsors: https://github.com/sponsors/ChuckBuilds - Buy Me a Coffee: https://buymeacoffee.com/chuckbuilds - - Ko-fi: https://ko-fi.com/chuckbuilds/ + - Ko-fi: https://ko-fi.com/chuckbuilds/ + +--- + +# Soccer Scoreboard + +Live, recent, and upcoming soccer on your LEDMatrix display — ten leagues built +in, and any other league ESPN covers can be added. From ESPN's public API, no +API key required. + +![Premier League live scorebug](../../docs/assets/soccer-scoreboard/hero.png) + +## Contents + +- [Quick start](#quick-start) +- [Display modes](#display-modes) +- [Match states](#match-states) +- [Supported leagues](#supported-leagues) +- [Adding another league](#adding-another-league) +- [FIFA World Cup](#fifa-world-cup) +- [Team abbreviations](#team-abbreviations) +- [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) +- [Fonts, colours and layout](#fonts-colours-and-layout) +- [Favorite team result colours](#favorite-team-result-colours) +- [Vegas ticker: seeing live games more often](#vegas-ticker-seeing-live-games-more-often) +- [Installation](#installation) +- [Troubleshooting](#troubleshooting) + +## Quick start + +1. Install **Soccer Scoreboard** from the LEDMatrix Plugin Store. +2. Turn on `enabled`, then the leagues you want. The seven domestic leagues are + on by default; the two UEFA competitions and the World Cup are off. +3. Add your clubs under each league's **Favorite Teams**, using ESPN + abbreviations. ------------------------------------------------------------------------------------ +```json +{ + "soccer-scoreboard": { + "enabled": true, + "show_records": true, + "leagues": { + "eng.1": { + "enabled": true, + "favorite_teams": ["LIV", "MCI", "ARS"], + "filtering": { "show_favorite_teams_only": false }, + "game_limits": { + "recent_games_to_show": 2, + "upcoming_games_to_show": 3 + } + }, + "esp.1": { + "enabled": true, + "favorite_teams": ["RMA", "BAR"] + } + } + } +} +``` -# Soccer Scoreboard Plugin +## Display modes -A plugin for LEDMatrix that displays live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, and FIFA World Cup. +Unlike the other scoreboards in this repo, soccer exposes **three modes in +total** — not three per league. Every enabled league feeds the same three +screens. -## Features +![The three display modes](../../docs/assets/soccer-scoreboard/display-modes.png) -- **Multiple League Support**: Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Champions League, Europa League, and more -- **Live Game Tracking**: Real-time scores, match time, and half information -- **Recent Games**: Recently completed games with final scores -- **Upcoming Games**: Scheduled games with start times -- **Favorite Teams**: Prioritize games involving your favorite teams -- **Background Data Fetching**: Efficient API calls without blocking display -- **Favorite Team Result Colors**: Optionally show a finished game's score in green when your favorite team won and red when it lost +| Mode | Shows | +|---|---| +| `soccer_live` | Matches in progress across every enabled league | +| `soccer_recent` | Recently finished matches | +| `soccer_upcoming` | Scheduled matches | -## Configuration +Each mode renders as **switch** (one match at a time, timed) or **scroll** (all +matches scroll horizontally at high FPS), set per league and per mode with +`leagues..display_modes._display_mode`. -### Global Settings +The mode toggles are named `live`, `recent`, `upcoming` — no `show_` prefix, +unlike the football and basketball scoreboards. -- `display_duration`: How long to show each game (5-60 seconds, default: 15) -- `show_records`: Display team win-loss records (default: false) -- `show_ranking`: Display team rankings when available (default: false) -- `background_service`: Configure API request settings -- `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. +## Match states -### Per-League Settings +Soccer has the richest status model of any scoreboard here, because a knockout +tie can run past 90 minutes and finish on penalties. The status area shows: -#### Premier League Configuration +![Live states: 2H, HALF, ET2, PEN](../../docs/assets/soccer-scoreboard/period-states.png) -```json -{ - "leagues": { - "eng.1": { - "enabled": true, - "favorite_teams": ["MUN", "LIV", "ARS"], - "display_modes": { - "live": true, - "recent": true, - "upcoming": true - }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 - } - } -} -``` +| While live | Meaning | +|---|---| +| `1H` / `2H` | First or second half | +| `HALF` | Half-time | +| `ET1` / `ET2` | Extra time, first or second half | +| `ETH` | Half-time of extra time | +| `PEN` | Penalty shootout in progress | -#### La Liga Configuration +![Final states: Final, F/ET, F/Pen](../../docs/assets/soccer-scoreboard/final-states.png) -```json -{ - "leagues": { - "esp.1": { - "enabled": true, - "favorite_teams": ["RM", "BAR", "ATM"], - "display_modes": { - "live": true, - "recent": true, - "upcoming": true - }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 - } - } -} -``` +| Once finished | Meaning | +|---|---| +| `Final` | Decided inside 90 minutes | +| `F/ET` | Decided in extra time | +| `F/Pen` | Decided on penalties | -#### Bundesliga Configuration +No configuration is involved — the state comes from the feed. -```json -{ - "leagues": { - "ger.1": { - "enabled": true, - "favorite_teams": ["BAY", "BVB", "RBL"], - "display_modes": { - "live": true, - "recent": true, - "upcoming": true - }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 - } - } -} -``` +## Supported leagues -#### Serie A Configuration +Ten leagues are built in, each with its own config block under `leagues`: -```json -{ - "leagues": { - "ita.1": { - "enabled": true, - "favorite_teams": ["JUV", "INT", "MIL"], - "display_modes": { - "live": true, - "recent": true, - "upcoming": true - }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 - } - } -} -``` +| Slug | League | Enabled by default | +|---|---|---| +| `eng.1` | Premier League (England) | Yes | +| `esp.1` | La Liga (Spain) | Yes | +| `ger.1` | Bundesliga (Germany) | Yes | +| `ita.1` | Serie A (Italy) | Yes | +| `fra.1` | Ligue 1 (France) | Yes | +| `usa.1` | MLS (USA) | Yes | +| `por.1` | Liga Portugal | Yes | +| `uefa.champions` | UEFA Champions League | No | +| `uefa.europa` | UEFA Europa League | No | +| `fifa.world` | FIFA World Cup | No | + +**`enabled` is the only setting whose default differs between them** — all ten +blocks are otherwise identical, with the same 53 settings each. Everything under +[Per-league settings](#per-league-settings) applies to all ten. + +## Adding another league + +Any other league ESPN covers can be added under **Add More Leagues** +(`custom_leagues`). Click **Add Item**, then fill in **both** a display name and +the ESPN league code — a row with a blank name will not save. + +| Field | Type | Default | What it does | +|---|---|---|---| +| `custom_leagues[].name` | string, 1–100 chars | — | Display name, e.g. `Liga MX`. | +| `custom_leagues[].league_code` | string, 1–50 chars | — | ESPN code, lowercase and dot-separated, e.g. `mex.1`. | +| `custom_leagues[].priority` | 1–100 or `null` | `50` | Display order; lower shows first. | -#### Ligue 1 Configuration +Common codes: -```json -{ - "leagues": { - "fra.1": { - "enabled": true, - "favorite_teams": ["PSG", "OM", "OL"], - "display_modes": { - "live": true, - "recent": true, - "upcoming": true - }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 - } - } -} -``` +| Code | League | +|---|---| +| `eng.2` | English Championship | +| `eng.3` | English League One | +| `eng.fa` | FA Cup | +| `eng.league_cup` | EFL (Carabao) Cup | +| `mex.1` | Liga MX | +| `arg.1` | Argentine Primera División | +| `bra.1` | Brasileirão Série A | +| `ned.1` | Eredivisie | +| `sco.1` | Scottish Premiership | +| `tur.1` | Turkish Süper Lig | +| `bel.1` | Belgian Pro League | +| `conmebol.libertadores` | Copa Libertadores | -#### MLS Configuration +Codes are exactly as they appear in ESPN's own URLs +(`espn.com/soccer/scoreboard/_/league/eng.2`). Per-league favorites, durations, +and display modes live behind the settings button on the league's row. -```json -{ - "leagues": { - "usa.1": { - "enabled": true, - "favorite_teams": ["LA", "SEA", "ATL"], - "display_modes": { - "live": true, - "recent": true, - "upcoming": true - }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 - } - } -} -``` +## FIFA World Cup -## Display Modes +Enable the **FIFA World Cup** league (`fifa.world`) to track World Cup 2026 +(June 11 – July 19, USA/Canada/Mexico). -The plugin supports three display modes: +- **To follow every match:** enable `fifa.world` and leave + `filtering.show_favorite_teams_only` off. +- **To follow one country:** enable `fifa.world`, set `favorite_teams` to that + country's ESPN abbreviation (`USA`, `ENG`, `BRA`), and turn + `filtering.show_favorite_teams_only` on. -1. **soccer_live**: Shows currently active games -2. **soccer_recent**: Shows recently completed games -3. **soccer_upcoming**: Shows scheduled upcoming games +Knockout ties use the extra-time and penalty states in +[Match states](#match-states). -## 🎯 Which Games Get Shown +## Team abbreviations -**`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. +`favorite_teams` takes the **ESPN API abbreviation** for each club (`"LIV"`, +`"MCI"`). Full club names are not accepted. -Which mode you are in depends on whether `favorite_teams` is set and whether `show_favorite_teams_only` is on: +See **[TEAMS.md](TEAMS.md)** for a complete list across all supported leagues. + +```json +"favorite_teams": ["LIV", "MCI", "ARS"] +``` + +> If you are unsure of an abbreviation, enable debug logging — the plugin logs +> `home_abbr` and `away_abbr` for every match it processes. + +## How games are chosen + +**`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 +matches until the schedule moves on. A bigger number gives you a *longer lap*, +so any one match comes round **less** often. + +Which regime you are in depends on that league's `favorite_teams` and +`filtering.show_favorite_teams_only`: | `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**. | +| empty | either | The next N matches league-wide, chronologically. Every match is a non-favorite match, so the `other_*` filters apply to all of them. | +| set | **on** (default) | Only your clubs. The limit is a budget **per team**. | +| set | **off** | **Your clubs first, then other matches to fill.** Both limits are **totals**. | -The third row is what most people want, and it did not exist before: with the flag off, favorites used to be ignored *entirely*. +### The selection settings -### The settings +Per league, under `game_limits`: | Option | Default | Description | |---|---|---| -| `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. | +| `recent_games_to_show` | `1` | Pool size for finished matches. | +| `upcoming_games_to_show` | `1` | The same for scheduled matches. | +| `other_recent_games_to_show` | `1` | How many **non-favorite** finished matches to add. `0` gives favorites only. | +| `other_upcoming_games_to_show` | `1` | The same for scheduled matches. | | `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. | +| `other_games_min_quality` | `ranked` | Which non-favorite matches qualify: `any` or `ranked`. | +| `other_games_divisions` | `["fbs"]` | Which divisions non-favorite matches may come from. | -**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. +The same seven keys also exist at the **plugin level**. `game_limits` wins where +the key is present, then the flat per-league key, then the plugin-level one. -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. +**Your favorite clubs are never filtered by the last two.** Those settings only +decide what fills the *remaining* slots. + +> **Both are inert in soccer.** `ranked` needs a national poll and the division +> filter needs ESPN's FBS/FCS group rosters — a college *football* taxonomy — so +> every match passes both and neither costs a request. The schema's help text +> for `other_games_min_quality` also mentions a `broadcast` option the enum does +> not offer; it was retired. ### Variety comes from turnover -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. +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 matches the soonest ones are the point. -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. +Both filters **fail open**: if the data behind them cannot be fetched, the match +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. -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. +### Live rotation and celebrations -> Both `other_games_min_quality` and `other_games_divisions` are inert in this plugin. `ranked` needs a national poll and the division filter needs ESPN's FBS/FCS group rosters; this league has neither, so every game passes both, and neither costs a request — no poll is fetched and no division lookup is made. +When several matches are live at once the rotation is weighted: a match +involving one of your clubs gets `filtering.favorite_live_boost` turns for every +one turn other live matches get, and is queued first whenever the rotation +refreshes. It never interrupts a match already on screen. Set it to `1` for even +rotation, and note it is independent of `live_priority`, which controls whether +live matches preempt the recent/upcoming rotation at all. +When a favorite club scores or wins a live match, the scorebug gives way to a +full-screen celebration — `celebration_enabled` (default on), +`celebration_duration` (default 8s), and `celebrate_opponent_goals` (default +off). -## Supported Leagues +A live match the API stops reporting for `stale_game_timeout` seconds is +dropped, so an abandoned match does not sit on the board forever. -The plugin supports the following soccer leagues: +### Shorter dwell for non-favorite live matches -- **eng.1**: Premier League (England) -- **esp.1**: La Liga (Spain) -- **ger.1**: Bundesliga (Germany) -- **ita.1**: Serie A (Italy) -- **fra.1**: Ligue 1 (France) -- **usa.1**: MLS (USA) -- **por.1**: Liga Portugal (Portugal) -- **uefa.champions**: UEFA Champions League -- **uefa.europa**: UEFA Europa League -- **fifa.world**: FIFA World Cup +`non_favorite_live_game_duration` (0–120, default `0` = off) gives live matches +involving **none** of your clubs a shorter turn. It only takes effect when +favorite teams are configured **and** non-favorite live matches are being shown +— `filtering.show_favorite_teams_only` off, or `filtering.show_all_live` on: -### Adding another league +| Favorites set? | Non-favorite matches shown? | Match has a favorite? | Duration used | +|---|---|---|---| +| No | — | — | `live_game_duration` | +| Yes | No | favorite | `live_game_duration` | +| Yes | Yes | favorite | `live_game_duration` | +| Yes | Yes | none | `non_favorite_live_game_duration`, when above `0` | -Any other league ESPN covers can be added under **Add More Leagues** in the plugin -settings. Click **Add Item**, then fill in **both** a display name and the ESPN -league code — a row with a blank name will not save. +`exclude_teams` hides clubs from **both** the live rotation and the +recent/final scores — useful when watching a match delayed. It always wins when +a club appears in both lists. -Common codes: +## Panel sizes -| Code | League | -| --- | --- | -| `eng.2` | English Championship | -| `eng.3` | English League One | -| `eng.fa` | FA Cup | -| `eng.league_cup` | EFL (Carabao) Cup | -| `mex.1` | Liga MX | -| `arg.1` | Argentine Primera División | -| `bra.1` | Brasileirão Série A | -| `ned.1` | Eredivisie | -| `sco.1` | Scottish Premiership | -| `tur.1` | Turkish Süper Lig | -| `bel.1` | Belgian Pro League | -| `conmebol.libertadores` | Copa Libertadores | +![Live card at four panel sizes](../../docs/assets/soccer-scoreboard/panel-sizes.png) -Codes are lowercase and dot-separated, exactly as they appear in ESPN's own URLs -(`espn.com/soccer/scoreboard/_/league/eng.2`). Per-league favorites, durations, -and display modes live behind the ⚙ button on the league's row. +The plugin passes the render-safety harness on all eight supported sizes. At +64x32 the two crests and the centre column share very little room; 128x32 or +wider is a much better fit. -## FIFA World Cup +## Settings reference -Enable the **FIFA World Cup** league from the plugin settings to track World Cup 2026 (June 11 – July 19, USA/Canada/Mexico). +Settings marked **Advanced** sit behind the *Advanced* toggle in the web UI. +Defaults are the schema defaults, which is what the web UI writes. -**To follow all games:** Enable `fifa.world` and leave `Show Favorite Teams Only` off. +### Plugin level -**To follow just your country:** Enable `fifa.world`, set `Favorite Teams` to your country's ESPN abbreviation (e.g. `USA`, `ENG`, `BRA`), and enable `Show Favorite Teams Only`. +| Key | Type | Default | What it does | +|---|---|---|---| +| `enabled` | boolean | `false` | Master on/off switch for the whole plugin. | +| `display_duration` | 5–60 s | `15` | Per-game on-screen time. | +| `game_display_duration` | 3–60 s | `15` | **Advanced.** Per-match time within a mode, where the league does not override it. | +| `live_game_duration` | 10–120 s | `30` | **Advanced.** Per-match time for live matches, where the league does not override it. | +| `show_records` | boolean | `false` | Draw win-draw-loss records in the bottom corners. | +| `show_ranking` | boolean | `false` | Draw table positions where available. | +| `show_odds` | boolean | `true` | Draw betting odds. | +| `show_favorite_teams_only` | boolean | `true` | Show only your clubs' matches, where the league does not override it. | +| `recent_games_to_show` | 1–20 | `1` | Pool size for finished matches, where the league does not override it. | +| `upcoming_games_to_show` | 1–20 | `1` | The same for scheduled matches. | +| `other_recent_games_to_show` | 0–20 | `1` | **Advanced.** Non-favorite finished matches to add. | +| `other_upcoming_games_to_show` | 0–20 | `1` | **Advanced.** The same for scheduled matches. | +| `other_rotation_interval_seconds` | 0–86400 s | `1800` | **Advanced.** How often the non-favorite slice advances. | +| `other_games_min_quality` | `any` \| `ranked` | `ranked` | **Advanced.** Inert in soccer — see above. | +| `other_games_divisions` | array | `["fbs"]` | **Advanced.** Inert in soccer — see above. | +| `update_interval_seconds` | 30–86400 s | `3600` | **Advanced.** Base data refresh cadence. | +| `live_update_interval` | 10–300 s | `30` | **Advanced.** Refresh cadence while a match is live. | +| `recent_update_interval` | 60–86400 s | `3600` | **Advanced.** Refresh cadence for finished matches. | +| `upcoming_update_interval` | 60–86400 s | `3600` | **Advanced.** Refresh cadence for the schedule. | +| `stale_game_timeout` | 60–3600 s | `300` | **Advanced.** Drop a live match the API has stopped updating. | +| `schedule_lookback_days` | 1–60 | `14` | **Advanced.** How far back to fetch for the Recent screen. | +| `schedule_lookahead_days` | 1–60 | `14` | **Advanced.** How far ahead to fetch for Upcoming. A fixture beyond this horizon is never fetched. | +| `no_data_interval_seconds` | 5–86400 s | `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 s | `900` | **Advanced.** Ceiling for that back-off. Useful out of season. | +| `timezone` | string | `""` | **Advanced.** IANA zone for kick-off times, e.g. `Europe/London`. Blank follows the LEDMatrix global timezone, then the host system's, then UTC. | + +![show_records on and off](../../docs/assets/soccer-scoreboard/show-records.png) + +> **`show_records`, `show_ranking` and `show_odds` are read from the plugin +> level only.** Each of the ten league blocks also declares a `display_options` +> object with the same three keys, but nothing reads it — verified by render in +> both directions, and by grepping both this plugin and the LEDMatrix core. +> Setting the per-league copy has no effect; set the plugin-level key above. +> This is the **opposite** of every other scoreboard in this repo, where the +> per-league copy wins, so do not carry that pattern across. Tracked as +> [issue #435](https://github.com/ChuckBuilds/ledmatrix-plugins/issues/435). + +### Background service + +All **Advanced**; the defaults suit a Pi and rarely want changing. + +| Key | Type | Default | +|---|---|---| +| `background_service.enabled` | boolean | `true` | +| `background_service.max_workers` | 1–10 | `3` | +| `background_service.request_timeout` | 5–120 s | `30` | +| `background_service.max_retries` | 1–10 | `3` | +| `background_service.priority` | 1–5 | `2` | -During knockout rounds, the status area shows: -- **ET1** / **ET2** — Extra Time first / second half -- **ETH** — Halftime of Extra Time -- **PEN** — Penalty Shootout in progress -- **F/ET** — Final, decided in Extra Time -- **F/Pen** — Final, decided on Penalties +## Per-league settings -## Team Names & Abbreviations +Every table below exists ten times, once per slug under `leagues`, with +identical keys and defaults except `enabled`. `` stands for any slug — +`leagues.eng.1`, `leagues.esp.1`, and so on. -The `favorite_teams` config field requires the **ESPN API abbreviation** for each team (e.g. `"LIV"`, `"MCI"`). Full team names are not supported. +### Teams and priority -See **[TEAMS.md](TEAMS.md)** for a complete list of abbreviations for all supported leagues. +| Key | Type | Default | What it does | +|---|---|---|---| +| `.enabled` | boolean | see [Supported leagues](#supported-leagues) | Build this league's managers at all. | +| `.favorite_teams` | array | `[]` | Clubs to prioritise, by ESPN abbreviation. | +| `.exclude_teams` | array | `[]` | Clubs to always hide, from the live rotation and from finals alike. Takes precedence over `favorite_teams` and `show_all_live`. | +| `.live_priority` | boolean | `true` | Let this league's live matches interrupt the rotation and display immediately. | -Example: -```json -"favorite_teams": ["LIV", "MCI", "ARS"] -``` +### Display modes + +| Key | Type | Default | +|---|---|---| +| `.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` | + +### Filtering -> **Tip:** If you're unsure of an abbreviation, enable debug logging — the plugin logs `home_abbr` and `away_abbr` for every game it processes. +| Key | Type | Default | What it does | +|---|---|---|---| +| `.filtering.show_favorite_teams_only` | boolean | `true` | Show only your clubs' matches. | +| `.filtering.show_all_live` | boolean | `false` | Show every live match regardless of favorites. `exclude_teams` still applies. | +| `.filtering.favorite_live_boost` | 1–5 | `2` | Turns a favorite's live match gets per one turn for other live matches. `1` is even rotation. | -## Filtering & Live Priority +### Game limits -Each league (and each custom league) has its own `filtering` block plus a couple of sibling settings: +See [The selection settings](#the-selection-settings). -| Setting | Default | Effect | +| Key | Type | Default | |---|---|---| -| `filtering.show_favorite_teams_only` | `true` | Only show games involving `favorite_teams`. | -| `filtering.show_all_live` | `false` | Overrides the above — show every live game, favorites or not. | -| `favorite_teams` | `[]` | Teams to prioritize (see above for abbreviation format). | -| `exclude_teams` | `[]` | Teams to always hide, from **both** the live rotation and Recent/Final scores — useful for spoiler protection if you're planning to watch a game delayed. Takes precedence over every other setting: an excluded team's games never show, even if `show_all_live` is on or the team is also listed in `favorite_teams`. | -| `filtering.favorite_live_boost` | `2` | How many turns your favorite's live game gets in the live rotation for every 1 turn other live games get. Your favorite's game is also always queued first the moment it goes live. Set to `1` for perfectly even rotation (no boost). Has no effect unless `favorite_teams` is configured and more than one game is live. | -| `non_favorite_live_game_duration` | `0` (off) | Seconds to show live games with **no** favorite team, so they flash by faster than your favorites (which keep `live_game_duration`). Only applies when `favorite_teams` is set **and** non-favorite live games are shown (`show_favorite_teams_only` off, or `show_all_live` on). `0` = every live game uses `live_game_duration` (no change). See below. | -| `live_priority` | varies | Lets this league's live games interrupt the recent/upcoming mode rotation (unrelated to which *specific* live game is shown — that's what `favorite_live_boost` controls). | - -Example: -```json -{ - "leagues": { - "eng.1": { - "favorite_teams": ["LIV"], - "exclude_teams": ["MUN"], - "filtering": { - "show_favorite_teams_only": false, - "show_all_live": false, - "favorite_live_boost": 3 - } - } - } -} -``` -With both `show_favorite_teams_only` and `show_all_live` off, all live games rotate evenly — except Liverpool's game shows 3× as often (and jumps to the front the instant it goes live) whenever they're playing, and Man United's games never appear in live or recent/final scores at all. +| `.game_limits.recent_games_to_show` | 1–20 | `1` | +| `.game_limits.upcoming_games_to_show` | 1–20 | `1` | +| `.game_limits.other_recent_games_to_show` | 0–20 | `1` | +| `.game_limits.other_upcoming_games_to_show` | 0–20 | `1` | +| `.game_limits.other_rotation_interval_seconds` | 0–86400 s | `1800` | +| `.game_limits.other_games_min_quality` | `any` \| `ranked` | `ranked` | +| `.game_limits.other_games_divisions` | array | `["fbs"]` | -### Shorter dwell for non-favorite live games +### Durations -`non_favorite_live_game_duration` (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 `live_game_duration: 30` with -`non_favorite_live_game_duration: 5` shows your teams for 30s each while everyone -else's games flash by in 5s. +| Key | Type | Default | What it does | +|---|---|---|---| +| `.live_game_duration` | number | `20` | Per-match time for live matches. Applies to matches with a favorite when a non-favorite duration is set. | +| `.non_favorite_live_game_duration` | number | `0` | Shorter turn for live matches with no favorite. `0` means use `live_game_duration` for everything. | +| `.recent_game_duration` | number | `15` | Per-match time on the Recent screen. | +| `.upcoming_game_duration` | number | `15` | Per-match time on the Upcoming screen. | -This **only takes effect** when favorite teams are configured **and** -non-favorite live games are being shown — `show_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 `live_game_duration`. +### Update intervals -| Favorite teams set? | Non-favorite games shown? | Live game has a favorite? | Duration used | +| Key | Type | Default | What it does | |---|---|---|---| -| No | — | — | `live_game_duration` (unchanged) | -| Yes | No (`show_favorite_teams_only` on, `show_all_live` off) | favorite | `live_game_duration` | -| Yes | Yes (`show_favorite_teams_only` off, or `show_all_live` on) | favorite | `live_game_duration` | -| Yes | Yes (`show_favorite_teams_only` off, or `show_all_live` on) | none | `non_favorite_live_game_duration` (when > 0) | +| `.update_interval_seconds` | number | `3600` | This league's base fetch cadence. | +| `.live_update_interval` | number | `30` | How often live match data refreshes. | +| `.recent_update_interval` | number | `3600` | How often the finished-matches list is rebuilt. This also sets how soon a match that has just ended can appear. | +| `.upcoming_update_interval` | number | `3600` | How often the upcoming-matches list is rebuilt. Selection and the non-favorite rotation both run on the display side, so this governs only the fetch. | +| `.stale_game_timeout` | number | `300` | Drop a live match the API has stopped updating. | + +### Celebrations + +| Key | Type | Default | +|---|---|---| +| `.celebration_enabled` | boolean | `true` | +| `.celebration_duration` | number | `8` | +| `.celebrate_opponent_goals` | boolean | `false` | -## Background Service +### Display options -The plugin uses background data fetching for efficient API calls: +| Key | Type | Default | +|---|---|---| +| `.display_options.show_records` | boolean | `false` | +| `.display_options.show_ranking` | boolean | `false` | +| `.display_options.show_odds` | boolean | `true` | -- Requests timeout after 30 seconds (configurable) -- Up to 3 retries for failed requests -- Priority level 2 (medium priority) +> **These three do nothing.** Nothing in the plugin or the core reads +> `leagues..display_options`; the plugin-level `show_records`, +> `show_ranking` and `show_odds` are what reach the card. See the note under +> [Plugin level](#plugin-level) and +> [issue #435](https://github.com/ChuckBuilds/ledmatrix-plugins/issues/435). -## Data Source +### Mode durations -Game data is fetched from ESPN's public API endpoints for all supported soccer leagues. +How long the *whole mode* holds the board before the core rotates on. `null` +uses the dynamic calculation. -## Dependencies +| 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` | -This plugin requires the main LEDMatrix installation and uses the plugin system base classes. +### Dynamic duration -## Installation +Sizes each mode's total time from how much there is to show. -The easiest way is the Plugin Store in the LEDMatrix web UI: +| Key | Type | Default | +|---|---|---| +| `.dynamic_duration.enabled` | boolean | `false` | +| `.dynamic_duration.min_duration_seconds` | 10–300 s | `30` | +| `.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 | — | + +### Scroll settings + +| Key | Type | Default | What it does | +|---|---|---|---| +| `.scroll_settings.scroll_speed` | number | `1.0` | Scroll speed in pixels per second. | +| `.scroll_settings.scroll_delay` | number | `0.01` | Frame delay; `0.01` is 100 FPS. Lower is smoother. | +| `.scroll_settings.gap_between_games` | number | `48` | Gap between match cards. | +| `.scroll_settings.show_league_separators` | boolean | `true` | Draw league icons between leagues. | +| `.scroll_settings.dynamic_duration` | boolean | `true` | Size the scroll duration from the content width. | +| `.scroll_settings.game_card_width` | number | `128` | Card width. Lower it on a multi-panel chain to fit more matches on screen at once. | -1. Open `http://your-pi-ip:5000` -2. Open the **Plugin Manager** tab -3. Find **Soccer Scoreboard** in the **Plugin Store** section and click - **Install** -4. Open the plugin's tab in the second nav row to configure leagues and - favorite teams +These are read through the LEDMatrix core's scroll base class rather than by +this plugin directly, which is why they do not appear in a grep of the plugin's +own `manager.py`. -Manual install: copy this directory into your LEDMatrix -`plugins_directory` (default `plugin-repos/`) and restart the display -service. +## Matchup separator and the upcoming card middle + +The **Matchup Card Layout** section (`scroll_card`) controls what sits between +the two crests before a match starts, and how the date and time are written. +Plugin-wide, not per league. -## Favorite Team Result Colors +| Setting | Key | Default | What it does | +|---|---|---|---| +| Matchup Separator | `scroll_card.vs_text` | `VS` | Text between the clubs: `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. | -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. +## Fonts, colours and layout + +Seven text elements, each with `font`, `font_size`, and `text_color`, under +`customization.`. + +| Element | Default font | Default size | Draws | +|---|---|---|---| +| `score_text` | `PressStart2P-Regular.ttf` | `10` | The score, and the matchup separator on an upcoming card | +| `period_text` | `PressStart2P-Regular.ttf` | `8` | The half and clock, and the date/time on an upcoming scoreboard | +| `team_name` | `PressStart2P-Regular.ttf` | `8` | Club names and abbreviations | +| `status_text` | `4x6-font.ttf` | `6` | Status lines such as the league name | +| `detail_text` | `4x6-font.ttf` | `6` | Small detail lines | +| `rank_text` | `PressStart2P-Regular.ttf` | `10` | Table positions | +| `odds_text` | `4x6-font.ttf` | `6` | Betting odds (defaults to green, `[0, 255, 0]`) | + +Colours are `[r, g, b]` or `"#RRGGBB"`. Every default is white except +`odds_text`. Odds sizes snap to the face's pixel grid to stay crisp: +`4x6-font.ttf` to 7, 14, 21; press_start to 8, 16. Every +`customization.` object sets `additionalProperties: false`. + +```json +{ + "customization": { + "score_text": { "text_color": [255, 200, 0] }, + "status_text": { "text_color": "#00A0FF" } + } +} +``` + +### Layout offsets + +Nudge any element in pixels. All default to `0`, all under +`customization.layout.`, and all set `additionalProperties: false`. + +| 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 | +| `odds` | `x_offset`, `y_offset` | Default odds position | + +## Favorite team result colours + +A run of matches 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 match's score by how your club +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 { @@ -383,35 +585,20 @@ a loss. } ``` -- 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. +- Only finished matches are coloured; live and upcoming cards are untouched. +- A match needs **exactly one** favorite club. Neither side or both, and the + score keeps its normal colour. A draw uses `tie_color`, which soccer sees far + more of than the other sports. +- The three colours are Advanced settings. -## Troubleshooting - -- **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`. -- **No games showing**: Check if leagues are enabled and API endpoints are accessible -- **Missing team logos**: Ensure team logo files exist in your assets/sports/soccer_logos/ directory -- **Slow updates**: Adjust the update interval in league configuration -- **API errors**: Check your internet connection and ESPN API availability - -## Advanced Configuration - -For more advanced users, you can add additional leagues by modifying the `ESPN_API_URLS` dictionary in the plugin code and updating the configuration schema accordingly. +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. If you would rather keep -the marquee scrolling and still see scores, set this in the core config: +By default a live match **takes over** the display. To keep the marquee +scrolling and still see scores, set this in the **core** config — not in this +plugin's settings: ```json { @@ -426,94 +613,65 @@ the marquee scrolling and still see scores, set this in the core config: ``` 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. - -`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*. - -Two things to keep in mind: - -- 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. +cycle. These weights let this plugin claim several slots per cycle, spaced +evenly through it. `live_weight` applies whenever this scoreboard has a live +match; `favorite_live_weight` when one of your clubs is playing. That +distinction has to be made here rather than in the core, which can tell *that* a +match is live but not *whose*. -## Matchup separator and the upcoming card middle +- The weight is per **plugin**, not per match. With four matches live this + scoreboard still occupies one slot at a time and picks between its own matches + using `favorite_live_boost`. +- More slots make the cycle **longer**, not faster. -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. +## Installation -| Setting | Key | Default | What it does | -|---|---|---|---| -| 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. | +From the Plugin Store in the LEDMatrix web UI: open `http://your-pi-ip:5000`, go +to **Plugin Manager**, find **Soccer Scoreboard** under **Plugin Store**, and +click **Install**. Then open the plugin's tab to pick your leagues and clubs. -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. +Match data comes from ESPN's public API. Crests download on first sight and +cache under `assets/sports/soccer_logos/`. The plugin requires the main +LEDMatrix installation and inherits from its soccer base classes. -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. +The documentation images come from `docs/assets/soccer-scoreboard/shots.json` +and re-render with `python scripts/render_docs_assets.py --plugin +soccer-scoreboard --check`. -Example: +## Troubleshooting -```json -{ - "scroll_card": { - "vs_text": "@", - "switch_upcoming_center": "vs", - "date_format": "weekday" - } -} -``` +**Nothing appears.** Check that `enabled` is on and at least one league's own +`enabled` is on. With `filtering.show_favorite_teams_only` at its default of +`true` and no `favorite_teams` set, there is nothing to select from. -### Text Colours +**Records, rankings or odds will not turn on.** You are probably setting the +per-league `display_options` copy, which nothing reads. Set the plugin-level +`show_records` / `show_ranking` / `show_odds` instead — +[issue #435](https://github.com/ChuckBuilds/ledmatrix-plugins/issues/435). -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. +**A club I follow never shows up.** `favorite_teams` needs the ESPN +abbreviation, not the club name — see [TEAMS.md](TEAMS.md). Enable debug logging +and the plugin prints `home_abbr` and `away_abbr` for every match it processes. -| 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 | +**A custom league will not save.** Both the name and the league code are +required; a row with a blank name is dropped. Codes are lowercase and +dot-separated, exactly as in ESPN's URLs. -Colours are `[r, g, b]` or `"#RRGGBB"`, and every default is white, so a display -nobody has recoloured looks exactly as it did. +**The same few matches keep repeating.** That is the pool cycling. Lower +`other_rotation_interval_seconds` for faster turnover rather than raising the +pool size — a larger pool makes the lap longer, so each match appears less +often, not more. -```json -{ - "customization": { - "score_text": { "text_color": [255, 200, 0] }, - "status_text": { "text_color": "#00A0FF" } - } -} -``` +**Start times look like UTC.** The plugin could not read your global timezone. +Set `timezone` under Advanced Settings to your IANA zone. + +**A finished match disappeared too soon.** Raise `schedule_lookback_days` +(default 14), or lower `recent_update_interval` if results are slow to appear. + +**A fixture I know about never appears.** It may be beyond +`schedule_lookahead_days` (default 14). A fixture outside that horizon is never +fetched. + +## License -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. +See `LICENSE`. diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 224fadd0..dc2acaf4 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -30,8 +30,11 @@ "version": "2.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." + "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 636 settings, with rendered examples of every display mode and every match state." + ] }, { "version": "2.24.1", @@ -321,7 +324,7 @@ { "released": "2026-07-29", "version": "2.5.0", - "notes": "Corrected every team code in TEAMS.md against ESPN's live data — Manchester United is MAN (not MUN), Manchester City MNC (not MCI), Real Madrid RMA, and Ligue 1 had eight wrong codes. The plugin now also says why a league is empty: an unrecognised favorite team logs a warning naming the closest match, while a correct code in a league with no fixtures yet logs the date the season starts.", + "notes": "Corrected every team code in TEAMS.md against ESPN's live data \u2014 Manchester United is MAN (not MUN), Manchester City MNC (not MCI), Real Madrid RMA, and Ligue 1 had eight wrong codes. The plugin now also says why a league is empty: an unrecognised favorite team logs a warning naming the closest match, while a correct code in a league with no fixtures yet logs the date the season starts.", "ledmatrix_min": "2.0.0" }, { @@ -351,7 +354,7 @@ { "released": "2026-07-02", "version": "2.2.0", - "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores — spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league, including custom leagues.", + "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores \u2014 spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league, including custom leagues.", "ledmatrix_min": "2.0.0" }, {