From 0aa4bf474b27ec2f75696d3b5fc69eca810403ae Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:55:12 -0400 Subject: [PATCH 1/2] docs(baseball-scoreboard): document every setting, with a real live scorebug Documentation only for the plugin; no behaviour change. Two additions to the shared doc tooling, both forced by this plugin. The README covered a fraction of the plugin's 279 settings and had no images. It now leads with the structural thing that actually trips people up: almost every setting lives inside a per-league block (mlb / milb / ncaa_baseball) and only nine are plugin-wide, so favorite_teams at the top level does nothing at all. Three leagues means three independent favourite lists, three sets of durations, three sets of filtering. Game selection is written out: the three paths, and that upcoming_games_to_show is a per-team budget under show_favorite_teams_only but a total otherwise. The four extra baseball screens (pitcher/batter, last play, player card, traditional scoreboard) are documented as off-by-default and each costing an extra per-game fetch, along with the fact that show_last_play only adds a field to the pitcher/batter screen rather than being a screen of its own. show_odds is described but deliberately not screenshotted. It is fetched asynchronously on a background thread after the card is drawn, so a single frame cannot show it -- that is a limit of how these images are made, not a plugin fault, and claiming otherwise would have been wrong. Tooling: http_replay -------------------- The recent and upcoming managers read through the cache, but the live manager calls _fetch_todays_games() straight over HTTP with no cache read, so seeded cache fixtures cannot reach the live screen -- the plugin's own harness works around this with a synthetic built-in game. Rather than document baseball's headline screen with synthetic data, shots can now name a recorded-responses file. Only matching URLs are served locally; everything else, logo downloads included, still goes to the network. The live shot is a genuine MLB game in progress on 2 September 2026, and it re-renders identically every time. The recorded payloads are stripped of fields the plugin never reads, which took them from 2.1MB to 660KB. That was verified rather than assumed: rendering against the full and stripped recordings produces byte-identical PNGs. Tooling: deep merge ------------------- Shot config was merged over defaults with a shallow dict.update(), so a shot overriding one key of a league block replaced the whole block -- including mlb.enabled -- and the plugin rendered nothing for a reason that looked nothing like the cause. Config overlays now merge recursively. This affected every plugin with nested config; baseball is just where it first bit. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- .../baseball-scoreboard/display-modes.png | Bin 0 -> 28309 bytes .../fixtures/live-today.json | 1 + .../fixtures/season-schedule.json | 1 + docs/assets/baseball-scoreboard/hero.png | Bin 0 -> 4657 bytes .../baseball-scoreboard/panel-sizes.png | Bin 0 -> 36472 bytes docs/assets/baseball-scoreboard/shots.json | 301 ++++++ .../baseball-scoreboard/show-records.png | Bin 0 -> 20874 bytes plugins.json | 2 +- plugins/baseball-scoreboard/README.md | 879 ++++++++---------- plugins/baseball-scoreboard/manifest.json | 9 +- .../docs_render_support/_docs_http_replay.py | 90 ++ scripts/docs_render_support/sitecustomize.py | 8 + scripts/render_docs_assets.py | 37 +- 14 files changed, 808 insertions(+), 522 deletions(-) create mode 100644 docs/assets/baseball-scoreboard/display-modes.png create mode 100644 docs/assets/baseball-scoreboard/fixtures/live-today.json create mode 100644 docs/assets/baseball-scoreboard/fixtures/season-schedule.json create mode 100644 docs/assets/baseball-scoreboard/hero.png create mode 100644 docs/assets/baseball-scoreboard/panel-sizes.png create mode 100644 docs/assets/baseball-scoreboard/shots.json create mode 100644 docs/assets/baseball-scoreboard/show-records.png create mode 100644 scripts/docs_render_support/_docs_http_replay.py diff --git a/README.md b/README.md index 13432c47..9abd9214 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \ | [Football Scoreboard](./plugins/football-scoreboard/) | NFL & NCAA Football live scores, down/distance, possession | | | [Hockey Scoreboard](./plugins/hockey-scoreboard/) | NHL & NCAA Hockey live scores and schedules | | | [Basketball Scoreboard](./plugins/basketball-scoreboard/) | NBA, NCAA & WNBA live scores and schedules | | -| [Baseball Scoreboard](./plugins/baseball-scoreboard/) | MLB, MiLB & NCAA Baseball live scores | | +| [Baseball Scoreboard](./plugins/baseball-scoreboard/) | MLB, MiLB & NCAA Baseball live scores | baseball-scoreboard on an LED panel | | [Soccer Scoreboard](./plugins/soccer-scoreboard/) | Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS | | | [Lacrosse Scoreboard](./plugins/lacrosse-scoreboard/) | NCAA lacrosse live scores and schedules | | | [Cricket Scoreboard](./plugins/cricket-scoreboard/) | Live, recent & upcoming international (Test/ODI/T20I) and major domestic cricket | | diff --git a/docs/assets/baseball-scoreboard/display-modes.png b/docs/assets/baseball-scoreboard/display-modes.png new file mode 100644 index 0000000000000000000000000000000000000000..c1cef3dea0a25a19a5c0837dc0150adff560ce6e GIT binary patch literal 28309 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 V@YeW!IY4-sU9~bUz2bWBe*s^F)4Biv literal 0 HcmV?d00001 diff --git a/docs/assets/baseball-scoreboard/panel-sizes.png b/docs/assets/baseball-scoreboard/panel-sizes.png new file mode 100644 index 0000000000000000000000000000000000000000..cf4cbaea93d5259bd7e0ad408e1d70800550f0df GIT binary patch literal 36472 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�4KCD`C2MbY!YkRtCK-N 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 R2>v31fcp{)?*HcdzW`Di59R;> literal 0 HcmV?d00001 diff --git a/plugins.json b/plugins.json index 610c1736..7cb5ef7a 100644 --- a/plugins.json +++ b/plugins.json @@ -76,7 +76,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.37.1" + "latest_version": "1.37.2" }, { "id": "basketball-scoreboard", diff --git a/plugins/baseball-scoreboard/README.md b/plugins/baseball-scoreboard/README.md index 0502411d..23f60e72 100644 --- a/plugins/baseball-scoreboard/README.md +++ b/plugins/baseball-scoreboard/README.md @@ -1,611 +1,462 @@ ------------------------------------------------------------------------------------ -### Connect with 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 - - Buy Me a Coffee: https://buymeacoffee.com/chuckbuilds - - Ko-fi: https://ko-fi.com/chuckbuilds/ - ------------------------------------------------------------------------------------ - -# Baseball Scoreboard Plugin - -A plugin for LEDMatrix that displays live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball leagues. - -## Features - -- **Multiple League Support**: MLB, MiLB (Minor League Baseball), NCAA Baseball -- **Live Game Tracking**: Real-time scores, innings, time remaining -- **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 - -## Configuration - -### Global Settings - -- `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 game 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. +# Baseball Scoreboard + +Live, recent, and upcoming baseball on your LED matrix across three leagues — +**MLB**, **MiLB** (Minor League Baseball), and **NCAA Baseball**. The live +screen is a real scorebug: the score, the inning and whether it's the top or +bottom, the bases, the outs, and the count. + +![A live MLB game on a 128x32 panel: San Diego 3, Cincinnati 4, top of the 4th, +bases empty, two out](../../docs/assets/baseball-scoreboard/hero.png) + +*Every image in this README is real plugin output, rendered at the true panel +size from a recorded ESPN response and then scaled up so the pixels stay pixels. +The live shot is an actual game in progress on 2 September 2026 — the score, +inning, bases, outs and count are what the panel showed at that moment.* + +--- + +## Table of Contents + +1. [What's On Screen](#whats-on-screen) +2. [Quick Start](#quick-start) +3. [Three Leagues, Three Config Blocks](#three-leagues-three-config-blocks) +4. [How Games Are Picked](#how-games-are-picked) +5. [Configuration Reference](#configuration-reference) + - [Plugin-wide settings](#plugin-wide-settings) + - [Per-league: teams and filtering](#per-league-teams-and-filtering) + - [Per-league: display modes](#per-league-display-modes) + - [Per-league: how many games](#per-league-how-many-games) + - [Per-league: durations](#per-league-durations) + - [Per-league: update intervals](#per-league-update-intervals) + - [Per-league: what appears on the card](#per-league-what-appears-on-the-card) + - [The extra baseball screens](#the-extra-baseball-screens) + - [Fonts, colours and offsets](#fonts-colours-and-offsets) +6. [Panel Sizes](#panel-sizes) +7. [Troubleshooting](#troubleshooting) +8. [Development](#development) +9. [Support](#support) + +--- + +## What's On Screen + +Each league contributes three display modes, so the plugin exposes nine in +total: `mlb_live`, `mlb_recent`, `mlb_upcoming`, and the same trio for `milb_` +and `ncaa_baseball_`. Every one can be enabled or disabled independently. + +![The three MLB display modes on a 128x32 panel: a live scorebug, a final +score, and an upcoming game with its first-pitch +time](../../docs/assets/baseball-scoreboard/display-modes.png) + +| Mode | Shows | Centre of the card | +|------|-------|--------------------| +| `*_live` | Games in progress | Inning with a top/bottom arrow, the bases diamond, outs, and the count | +| `*_recent` | Completed games | `Final`, the score, and the date | +| `*_upcoming` | Scheduled games | `Next Game`, the date and first pitch in your timezone | + +Reading the live scorebug, from the hero image above: + +```text + ▲4 top of the 4th (▼ for the bottom) + ● ◇ bases: filled = runner on. The diamond is + ● ◇ ◇ second at the top, third left, first right + ◌ outs: filled = out recorded, hollow = still to come + 3-3 the count, balls-strikes +``` - **Leftover `"UTC"` from an older version?** Before the write-back fix this - plugin could persist `"timezone": "UTC"` into your saved config, where it - then shadowed your real global timezone. That stale value is now detected - and ignored automatically whenever your global or system timezone disagrees - — no manual edit needed. If you genuinely want UTC here, set `Etc/UTC`, - which is always honored. +The away team is always on the left and the home team on the right. +--- -### Per-League Settings +## Quick Start -#### MLB Configuration +The minimum useful configuration is one league and your teams: ```json { - "mlb": { + "baseball-scoreboard": { "enabled": true, - "favorite_teams": ["NYY", "BOS", "LAD"], - "display_modes": { - "show_live": true, - "show_recent": true, - "show_upcoming": true + "mlb": { + "enabled": true, + "favorite_teams": ["STL", "LAD"] }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 + "milb": { "enabled": false }, + "ncaa_baseball": { "enabled": false } } } ``` -#### MiLB Configuration +Turning off the leagues you don't follow matters more here than in a +single-league plugin: each enabled league fetches on its own schedule, and +NCAA Baseball in particular is out of season for most of the year, so leaving +it on costs requests for screens that will never have anything to show. -```json -{ - "milb": { - "enabled": true, - "favorite_teams": ["DUR", "SWB", "MEM"], - "display_modes": { - "show_live": true, - "show_recent": true, - "show_upcoming": true - }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 - } -} -``` +--- -#### NCAA Baseball Configuration +## Three Leagues, Three Config Blocks + +This is the biggest structural difference from the single-league scoreboards. +Almost every setting lives **inside a league block**, not at the top level: ```json { - "ncaa_baseball": { + "baseball-scoreboard": { "enabled": true, - "favorite_teams": ["LSU", "FLA", "VANDY"], - "display_modes": { - "show_live": true, - "show_recent": true, - "show_upcoming": true - }, - "recent_games_to_show": 5, - "upcoming_games_to_show": 10 + "timezone": "America/Chicago", + + "mlb": { "enabled": true, "favorite_teams": ["STL"] }, + "milb": { "enabled": true, "favorite_teams": ["MEM"] }, + "ncaa_baseball": { "enabled": false } } } ``` -### Filtering & Live Priority +Only nine settings are plugin-wide — see +[Plugin-wide settings](#plugin-wide-settings). Everything else +(`favorite_teams`, `display_modes`, durations, update intervals, filtering, +game limits, display options) exists **once per league** and is configured +independently. Setting `favorite_teams` at the top level does nothing. -Each league's `filtering` block controls which live games are shown, and -`favorite_teams` / `exclude_teams` control which teams are eligible: +The practical consequence: if you follow one MLB team and one MiLB affiliate, +you set two separate `favorite_teams` lists, and you can give them different +durations and different filtering. -```json -{ - "mlb": { - "favorite_teams": ["SF"], - "exclude_teams": [], - "filtering": { - "show_favorite_teams_only": false, - "show_all_live": false, - "favorite_live_boost": 2 - } - } -} -``` +--- -- `favorite_teams`: teams you follow. When a favorite is live, it's always - queued first as soon as the live rotation refreshes. -- `exclude_teams`: teams to always hide from the live rotation **and** - recent/final scores (e.g. to avoid spoilers if you're watching delayed). - This always wins — even over `show_all_live` or a team also listed in - `favorite_teams`. -- `show_favorite_teams_only`: only show live games involving a favorite team. -- `show_all_live`: show every live game regardless of favorites. -- With both `show_favorite_teams_only` and `show_all_live` off, every live - game is shown and rotated evenly — this is the same set as `show_all_live`, - the difference is in *how* they rotate (see `favorite_live_boost` below). -- `favorite_live_boost` (1-5, default 2): how many turns your favorite's live - game gets in the rotation for every 1 turn other live games get. Set to `1` - for perfectly even rotation. Only matters when more than one live game is - eligible to show (i.e. not when `show_favorite_teams_only` is on with a - single favorite). - -#### Shorter dwell for non-favorite live games - -`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. It sits next to `live_game_duration` in each league -block. - -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`. - -| Favorite teams set? | Non-favorite games shown? | Live game has a favorite? | Duration used | -|---|---|---|---| -| 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) | - -## Display Modes - -The plugin registers per-league granular modes in `manifest.json`. The -display controller rotates through any that are enabled: - -**MLB:** `mlb_live`, `mlb_recent`, `mlb_upcoming` -**MiLB:** `milb_live`, `milb_recent`, `milb_upcoming` -**NCAA Baseball:** `ncaa_baseball_live`, `ncaa_baseball_recent`, `ncaa_baseball_upcoming` - -Toggle individual modes per league with the `show_live` / `show_recent` -/ `show_upcoming` flags inside each league's `display_modes` block. - -## Traditional Scoreboard Screen - -A dedicated full-screen view styled after a real outfield ballpark -scoreboard: an inning-by-inning line score with R/H/E, the current -inning highlighted, small team logos and team-colored abbreviations -when there's room, and (for live at-bats) a compact column of lit -ball/strike/out indicators. Available for **MLB and NCAA Baseball -only** (MiLB's data doesn't come from ESPN's API in the same shape, so -it isn't wired up for that league). - -### How it works - -This isn't a separate display mode you select — it periodically -*rotates into* whichever game the normal live/recent rotation is -already showing, replacing the usual compact scorebug for a few -seconds at a time, then reverting. Nothing needs to be "currently -selected" for it to appear; as long as the toggle is on, it takes over -automatically on its own timer while a live or final game is on screen. - -The layout adapts to your display size automatically: -- The font auto-fits as large as your panel allows (see `font_size`). -- The ball/strike/out column only appears if there's enough width to - fit it without shrinking the number of innings shown; on very narrow - displays it's dropped entirely rather than clipped or forced in. -- Team logos only appear if there's leftover width to spare after - everything else — they never cost a displayed inning or push out the - ball/strike/out column. - -### Enabling it - -Turn it on per league under that league's `display_options`: +## How Games Are Picked -```json -{ - "mlb": { - "display_options": { - "show_traditional_scoreboard": true - } - } -} -``` +Selection runs per league, per mode, on every update. Which of three code paths +runs depends on two settings in that league's block: whether `favorite_teams` +is empty, and whether `filtering.show_favorite_teams_only` is on. -The same flag exists under `ncaa_baseball.display_options`. It's off -by default. +**1. No favourites** (`favorite_teams: []`) — the next *N* games league-wide, +sorted by time. -### Toggles and customization +**2. Favourites, exclusively** (`show_favorite_teams_only: true`) — only games +involving your teams, with a per-team budget: each favourite gets up to +`game_limits.upcoming_games_to_show` games, and a game between two favourites +counts toward both. -All of the following live under `customization.traditional_scoreboard` -in the config (this block is shared across leagues that support the -screen): +**3. Favourites first, then others** (`show_favorite_teams_only: false`) — your +favourites' games first, then a top-up of `game_limits.other_upcoming_games_to_show` +non-favourite games, with the combined list re-sorted by start time so the +cards still read as a schedule. The non-favourite games come from a window that +advances every `other_rotation_interval_seconds` (default 30 minutes), so the +board works through the day rather than resampling the same handful. -| Option | Default | What it does | -|---|---|---| -| `game_scope` | `"both"` | Which games this screen rotates in for. `"live"` — only during live action. `"recent"` — only for final/completed games (handy for glancing at the final line score and picking out the winner without watching the whole game). `"both"` — either. | -| `favorites_only` | `false` | When `true`, only rotates in for games involving one of this league's `favorite_teams`. This is independent of `show_all_live`/`show_favorite_teams_only` (which control the *normal* rotation) — so you can watch every team's live games in the compact scorebug, but reserve the full-screen ballpark treatment for your own team. Has no effect if `favorite_teams` is empty. | -| `dwell_seconds` | `6` | How many seconds this screen stays on screen each time it rotates in. | -| `interval_seconds` | `30` | How often (in seconds) it rotates in. | -| `font` | `"9x15.bdf"` | Font for all text on this screen. The default is a clean, bold bitmap font sized to fit the display; a fixed-size `.bdf` font always renders at its own native pixel size (with an automatic fallback to a smaller sibling font if your display is too small to fit it) rather than scaling to `font_size`. Use a scalable `.ttf` font (e.g. `"press_start"` for a chunkier 8-bit retro look) if you want `font_size` to directly control the size. | -| `font_size` | `24` | Maximum font size cap, for scalable `.ttf` fonts only (ignored by fixed-size `.bdf` fonts like the default). The screen auto-fits the largest text that still leaves room for the ball/strike/out column, so the default effectively means "as big as the display allows" — lower it to force a smaller, more consistent size. | -| `use_team_colors` | `true` | Color each team's abbreviation with their real ESPN team colors (brightness-adjusted for legibility on black) instead of a flat `text_color`. | -| `show_team_color_backgrounds` | `true` | Tint each team's row with a subtle (~12% brightness) wash of their real ESPN color, plus a solid team-color accent strip on the left edge — a colorful ballpark look. Requires `use_team_colors`; a team with no ESPN color simply gets no tint. Text stays legible because it's drawn with a black outline over the wash. | -| `show_logos` | `true` | Show a small team logo beside each abbreviation when there's spare width (see "How it works" above). | -| `show_dividers` | `true` | Draw thin 1px grid lines between innings, rows, and the R/H/E columns for readability. | -| `highlight_winner` | `true` | On a final game, color the winning team's run total in `winner_color` so the winner is obvious at a glance instead of having to compare both R values yourself. Pairs naturally with `game_scope: "recent"`. No effect on live games. | -| `text_color` | `[255, 255, 255]` | `[R, G, B]` for score digits, and team abbreviations when `use_team_colors` is off or a team's color is unavailable. | -| `header_color` | `[180, 180, 180]` | `[R, G, B]` for the inning-number and R/H/E header row. | -| `highlight_color` | `[255, 140, 0]` | `[R, G, B]` accent color for the current-inning highlight, the batting-team ▲/▼ indicator, and lit ball/strike/out indicators. | -| `divider_color` | `[90, 90, 90]` | `[R, G, B]` for the grid divider lines. | -| `winner_color` | `[0, 200, 0]` | `[R, G, B]` for the winning team's run total on a final game (see `highlight_winner`). | - -Example — only show this screen for your favorite team, and only once -the game is final (a simple "check the final score" use case): +**What the numbers mean changes with the path.** This is the single most +confusing thing in the configuration: -```json -{ - "mlb": { - "display_options": { - "show_traditional_scoreboard": true - } - }, - "customization": { - "traditional_scoreboard": { - "game_scope": "recent", - "favorites_only": true - } - } -} -``` +| Selection path | `upcoming_games_to_show` means | +|----------------|-------------------------------| +| No favourites | A **total** across the league | +| Favourites, exclusively | A budget **per favourite team** | +| Favourites first, then others | A **total** for the favourites portion only | -## Pitcher / Batter / Last Play Screen +Three favourite teams with a value of `3` is up to nine cards in the exclusive +path and three in the others. -A dedicated full-screen view showing the current at-bat's pitcher, -batter, and a short code for the most recently completed play (`1B`, -`HR`, `K`, `BB`, etc.), replacing the normal scorebug for a few -seconds at a time. The pitcher and batter lines are labeled in full — -`Pitcher: G. Cole` / `Batter: J. Soto` — so there's no ambiguity with -the grid's `B` (Balls) indicator. Available for **MLB and NCAA Baseball only**, and -**live games only** — this data only exists during an actual live -at-bat, so unlike the Traditional Scoreboard there's no `game_scope` -option (nothing analogous exists for a final or upcoming game). +Two more rules that apply on top: -Text is centered both horizontally and vertically, and auto-fits the -largest font that still fits every line's actual text — a long name -falls back to a smaller font (and, as a last resort, gets truncated) -before it would otherwise run off the edge. +- `exclude_teams` beats everything — a team listed there is hidden from the + live rotation and from finished scores even if it is also a favourite. That + is what makes it useful for spoiler protection. +- If `show_favorite_teams_only` is `true` but `favorite_teams` is empty, the + filter is skipped entirely and you get path 1. An empty favourites list never + means "show nothing". -### Enabling it +**Selection can only choose from games that were fetched.** +`schedule_lookback_days` (default `14`) and `schedule_lookahead_days` +(default `7`) bound the window. A fixture beyond the lookahead horizon cannot +appear no matter how high you set the limits. -Turn on the parts you want per league under that league's -`display_options`: +### Live games -```json -{ - "mlb": { - "display_options": { - "show_pitcher_batter": true, - "show_last_play": true - } - } -} -``` +The live screen has its own selection. By default it shows only live games +involving your favourites; `filtering.show_all_live: true` includes every live +game. `filtering.favorite_live_boost` (default `2`) gives your team's game that +many turns per one turn for other live games, and queues it first when the +rotation refreshes. `live_priority` (default `false` here) lets a live game +interrupt the normal mode rotation. -Both flags exist under `ncaa_baseball.display_options` too, and both -default to off. You can enable just one (e.g. only `show_last_play` -for a compact "what just happened" ticker). +`stale_game_timeout` (default `300`) drops a live game that has gone that long +without an update, which is what stops a suspended or abandoned game holding a +slot forever. -### Toggles and customization +--- -All of the following live under `customization.at_bat_info`: +## Configuration Reference -| Option | Default | What it does | -|---|---|---| -| `favorites_only` | `false` | Only rotates in for games involving one of this league's `favorite_teams` — independent of `show_all_live`/`show_favorite_teams_only`, which control the *normal* rotation. Has no effect if `favorite_teams` is empty. | -| `dwell_seconds` | `4` | How many seconds this screen stays on screen each time it rotates in. | -| `interval_seconds` | `25` | How often (in seconds) it rotates in. | -| `font` | `"9x15.bdf"` | Font for all text on this screen. The default auto-fits as large as the display and each line's actual text allow, falling back to a smaller same-family font rather than overflowing. Use a scalable `.ttf` font (e.g. `"press_start"`) if you want `font_size` to directly control the size. | -| `font_size` | `24` | Maximum font size cap, for scalable `.ttf` fonts only (ignored by fixed-size `.bdf` fonts like the default). Lower it to force a smaller, more consistent size. | -| `use_team_colors` | `true` | Color the pitcher's name with the fielding team's real ESPN color and the batter's name with the batting team's color, instead of the flat colors below. | -| `pitcher_color` | `[255, 255, 255]` | `[R, G, B]` for the pitcher line when `use_team_colors` is off or unavailable. | -| `batter_color` | `[255, 255, 0]` | `[R, G, B]` for the batter line when `use_team_colors` is off or unavailable. | -| `last_play_color` | `[0, 255, 255]` | `[R, G, B]` for the last-play code line (always this flat color — there's no "team" a play code belongs to). | - -## Player Card Screen - -A dedicated full-screen "baseball card" that rotates in for the current -batter (and optionally the pitcher): a **headshot image**, **jersey -number**, **position**, **bat/throw hand**, and **season stats** — -`AVG` / `HR` / `RBI` for hitters, `ERA` / `W-L` / `K` for pitchers. -The headshot and bio are fetched from ESPN's athlete API and cached -(in memory and on disk under `assets/headshots/`, which is gitignored). -Available for **MLB and NCAA Baseball only** and **live games only** -(the pitcher/batter are only known during a live at-bat); MiLB is -skipped because it has no ESPN player data. - -The layout adapts to your panel size: on larger displays the headshot -sits on the left with a team-colored frame and the text stacks beside -it; on tiny panels (e.g. 64×32) the headshot is hidden and a compact -two-line text card is shown instead. If a headshot can't be loaded the -card renders text-only, and if no bio is available yet the card is -simply skipped that rotation (never shown blank). - -### Enabling it - -Turn it on per league under that league's `display_options`: +Options marked **advanced** are behind the "advanced" toggle in the web UI and +are safe to ignore. -```json -{ - "mlb": { - "display_options": { - "show_player_card": true - } - } -} -``` +### Plugin-wide settings -The same flag exists under `ncaa_baseball.display_options`, off by -default. Enabling it works alongside (and independently of) the -Pitcher/Batter and Traditional Scoreboard screens — each rotates in on -its own schedule. +These nine sit at the top level, outside any league block. -### Toggles and customization +| Option | Default | What it does | +|--------|---------|--------------| +| `enabled` | `true` | Whether the plugin takes part in the rotation at all | +| `display_duration` | `30` | Seconds each mode holds the panel before the rotation moves on | +| `game_display_duration` | `15` | Seconds each individual game shows before the next one within the same mode | +| `update_interval` | `3600` | Base fetch interval | +| `timezone` | `""` | **Advanced.** IANA timezone for start times, e.g. `America/Chicago`. Blank follows the global LEDMatrix timezone, then the system one | +| `schedule_lookback_days` | `14` | **Advanced.** How far back the recent screens can see | +| `schedule_lookahead_days` | `7` | **Advanced.** How far ahead the upcoming screens can see | +| `no_data_interval_seconds` | `300` | **Advanced.** Gap between live checks when nothing is on, backing off the longer it stays quiet | +| `live_idle_max_interval_seconds` | `900` | **Advanced.** Ceiling for that back-off | -All of the following live under `customization.player_card`: +Baseball is a daily sport in season and dormant out of it, so the back-off pair +matters: out of season the plugin settles to one check every 15 minutes rather +than one every five. -| Option | Default | What it does | -|---|---|---| -| `show_batter` | `true` | Show a card for the current batter. | -| `show_pitcher` | `false` | Also show a card for the current pitcher (the batter is preferred when both are available). | -| `favorites_only` | `false` | Only rotate the card in for games involving one of this league's `favorite_teams`. Has no effect if `favorite_teams` is empty. | -| `dwell_seconds` | `6` | How many seconds the card stays on screen each time it rotates in. | -| `interval_seconds` | `40` | How often (in seconds) the card rotates in. | -| `font` | `"9x15.bdf"` | Font for the card's text; auto-fits within the space beside the headshot. Use a scalable `.ttf` font (e.g. `"press_start"`) if you want `font_size` to directly control the size. | -| `font_size` | `24` | Maximum font size cap, for scalable `.ttf` fonts only (ignored by fixed-size `.bdf` fonts like the default). | -| `use_team_colors` | `true` | Color the player's name with their real ESPN team color instead of the flat `text_color`. | -| `use_team_colors_border` | `true` | Draw the headshot's frame in the player's team color instead of the flat `border_color`. | -| `border_color` | `[255, 200, 0]` | `[R, G, B]` for the headshot frame when `use_team_colors_border` is off or the team color is unavailable. | -| `text_color` | `[255, 255, 255]` | `[R, G, B]` for the name (when team colors are off) and the jersey/position/bat-throw line. | -| `stat_color` | `[0, 220, 255]` | `[R, G, B]` for the season-stats line. | +### Per-league: teams and filtering -## Team Abbreviations +Set these inside `mlb`, `milb`, or `ncaa_baseball`. -### MLB Teams -Common abbreviations: NYY (Yankees), BOS (Red Sox), LAD (Dodgers), HOU (Astros), ATL (Braves), PHI (Phillies), TOR (Blue Jays), TB (Rays), MIL (Brewers), CHC (Cubs), CIN (Reds), PIT (Pirates), STL (Cardinals), MIN (Twins), CLE (Guardians), CHW (White Sox), DET (Tigers), KC (Royals), LAA (Angels), OAK (Athletics), SEA (Mariners), TEX (Rangers), ARI (Diamondbacks), COL (Rockies), SD (Padres), SF (Giants), BAL (Orioles), MIA (Marlins), NYM (Mets), WAS (Nationals) +| Option | Default | What it does | +|--------|---------|--------------| +| `enabled` | `true` | Whether this league is fetched and displayed at all | +| `favorite_teams` | `[]` | Team abbreviations, e.g. `["STL", "LAD"]` | +| `exclude_teams` | `[]` | Teams to always hide, from live *and* finished scores | +| `filtering.show_favorite_teams_only` | `true` | Restrict to games involving `favorite_teams` | +| `filtering.show_all_live` | `false` | Show every live game, not just favourites' | +| `filtering.favorite_live_boost` | `2` | **Advanced.** Turns your favourite's live game gets per turn for others | -### MiLB Teams -Common abbreviations vary by league and level (AAA, AA, A+, A, etc.). Examples: DUR (Durham Bulls), SWB (Scranton/Wilkes-Barre RailRiders), MEM (Memphis Redbirds), etc. +Abbreviations are ESPN's, not the club name — `STL`, `LAD`, `NYY`, `SD`, `CIN`. +If you are unsure of one, enable debug logging and the plugin logs +`home_abbr` and `away_abbr` for every game it processes. -### NCAA Baseball Teams -Common abbreviations: LSU (LSU), FLA (Florida), VANDY (Vanderbilt), ARK (Arkansas), MISS (Ole Miss), TAMU (Texas A&M), TENN (Tennessee), UK (Kentucky), UGA (Georgia), BAMA (Alabama), AUB (Auburn), SCAR (South Carolina), CLEM (Clemson), FSU (Florida State), MIA (Miami), UNC (North Carolina), DUKE, WAKE (Wake Forest), VT (Virginia Tech), LOU (Louisville) +### Per-league: display modes -## Background Service +| Option | Default | What it does | +|--------|---------|--------------| +| `display_modes.show_live` | `true` | Enable this league's live screen | +| `display_modes.show_recent` | `true` | Enable its recent screen | +| `display_modes.show_upcoming` | `true` | Enable its upcoming screen | +| `display_modes.live_display_mode` | `switch` | `switch` = one full-screen game at a time; `scroll` = all games scrolling sideways | +| `display_modes.recent_display_mode` | `switch` | As above, for recent | +| `display_modes.upcoming_display_mode` | `switch` | As above, for upcoming | -The plugin uses background data fetching for efficient API calls: +Every screenshot in this README is `switch` mode. `scroll` draws a compact card +per game and scrolls the strip, which fits more games on a long panel at the +cost of size; the `scroll_card` and `scroll_settings` groups only affect it. -- Requests timeout after 30 seconds (configurable) -- Up to 3 retries for failed requests -- Priority level 2 (medium priority) +### Per-league: how many games -## Data Source +| Option | Default | What it does | +|--------|---------|--------------| +| `game_limits.recent_games_to_show` | `5` | Finished games — see [what the numbers mean](#how-games-are-picked) | +| `game_limits.upcoming_games_to_show` | `1` | Scheduled games — same caveat | +| `game_limits.other_recent_games_to_show` | `5` | **Advanced.** Non-favourite finished games, in the favourites-first path | +| `game_limits.other_upcoming_games_to_show` | `1` | **Advanced.** Non-favourite scheduled games, same path | +| `game_limits.other_rotation_interval_seconds` | `1800` | **Advanced.** How often the non-favourite window advances | +| `game_limits.other_games_min_quality` | `ranked` | **Advanced.** Which non-favourite games earn a slot. Meaningful for NCAA, where a national ranking exists | +| `game_limits.other_games_divisions` | `["fbs"]` | **Advanced.** Divisions non-favourite games may come from. NCAA only | + +Note the asymmetric defaults: five recent games but one upcoming. That suits +baseball's daily schedule — yesterday produced a full slate of finals worth +rotating through, while "the next game" is usually the only upcoming one you +care about. + +### Per-league: durations -Game data is fetched from ESPN's public API endpoints for all supported baseball leagues. +| Option | Default | What it does | +|--------|---------|--------------| +| `live_game_duration` | `30` | Seconds per live game before rotating to the next | +| `non_favorite_live_game_duration` | `0` | **Advanced.** Separate, usually shorter duration for live games with no favourite in them. `0` means "use `live_game_duration` for everything" | +| `recent_game_duration` | `15` | **Advanced.** Seconds per finished game | +| `upcoming_game_duration` | `15` | **Advanced.** Seconds per scheduled game | +| `live_priority` | `false` | Let a live game in this league interrupt the normal rotation | +| `mode_durations.*` | `null` | **Advanced.** Fixed total duration for a whole mode, overriding the per-game maths | +| `dynamic_duration.enabled` | `false` | **Advanced.** Size a mode's duration from how many games it actually has | -## Dependencies +`non_favorite_live_game_duration` is the setting for a full slate: with fifteen +games on at once, `live_game_duration: 30` and +`non_favorite_live_game_duration: 8` keeps your club's game on screen while the +rest still tick past. -This plugin requires the main LEDMatrix installation and inherits functionality from the Baseball base classes. +### Per-league: update intervals -## Installation +All **advanced**. The defaults are tuned for a Raspberry Pi that is also driving +a panel. -The easiest way is the Plugin Store in the LEDMatrix web UI: +| Option | Default | What it does | +|--------|---------|--------------| +| `live_update_interval` | `30` | How often live game data is refreshed | +| `recent_update_interval` | `3600` | How often the finished-games list is rebuilt | +| `upcoming_update_interval` | `3600` | How often the upcoming list is rebuilt | +| `update_interval_seconds` | `3600` | Base fetch interval for this league | +| `stale_game_timeout` | `300` | How long a live game may go without an update before it is dropped | -1. Open `http://your-pi-ip:5000` -2. Open the **Plugin Manager** tab -3. Find **Baseball Scoreboard** in the **Plugin Store** section and click - **Install** -4. Open the plugin's tab in the second nav row to configure favorite - teams and per-league preferences +### Per-league: what appears on the card -Manual install: copy this directory into your LEDMatrix -`plugins_directory` (default `plugin-repos/`) and restart the display -service. +| Option | Default | What it does | +|--------|---------|--------------| +| `display_options.show_records` | `false` | **Advanced.** Each team's win-loss record in the bottom corners | +| `display_options.show_odds` | `true` | Draw the betting line when ESPN has one | +| `display_options.show_ranking` | `false` | **Advanced.** Rank badge. Meaningful for NCAA; MLB and MiLB publish no poll | +| `display_options.show_series_summary` | `false` | **Advanced.** Where the teams stand in the current series | -## Favorite Team Result Colors +![The same finished game with show_records off and on; with it on, 69-70 and +82-56 appear in the bottom corners](../../docs/assets/baseball-scoreboard/show-records.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. +`show_odds` is not shown here because it cannot be captured in a still: odds +are fetched asynchronously on a background thread after the card is first +drawn, so the line appears a moment later, once the fetch returns. That also +means it costs an extra request per selected game — worth turning off if you +do not want the line. -```json -{ - "customization": { - "favorite_result_colors": { - "enabled": true, - "win_color": [0, 255, 0], - "loss_color": [255, 0, 0], - "tie_color": [255, 200, 0] - } - } -} -``` +### The extra baseball screens -- Off by default. Until you enable it the score is drawn in the plain white the - scorebug uses everywhere else. (Before this release the scroll/Vegas recent - card drew the final score gold, out of step with the switch view and with - every other scoreboard; it is white now.) -- 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. +Baseball has more to say than most sports mid-at-bat, so the plugin can +periodically take over with a dedicated screen. All are **off by default** and +all cost an extra per-game data fetch. -## Troubleshooting +| Option | Default | What it does | +|--------|---------|--------------| +| `display_options.show_pitcher_batter` | `false` | A screen naming the current pitcher and batter during a live at-bat | +| `display_options.show_last_play` | `false` | Adds a short code for the last completed play (`1B`, `HR`, `K`, `BB`) to that screen | +| `display_options.show_player_card` | `false` | A full card for the current batter: headshot, number, position, bat/throw and season stats | +| `display_options.show_traditional_scoreboard` | `false` | A full-screen ballpark scoreboard: inning-by-inning line score, R/H/E, and an at-bat panel | + +`show_last_play` only does anything with `show_pitcher_batter` on — it adds a +field to that screen rather than being a screen of its own. -- **Game times look like UTC** (a 6:45pm Central first pitch 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`. A `timezone` entry stuck on `"UTC"` from a version - before 1.20.0 no longer needs clearing — since 1.20.1 it's ignored - automatically whenever your global or system timezone disagrees. (Set - `Etc/UTC` if you actually want UTC; that's always honored.) -- **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/ directory -- **Slow updates**: Adjust the update interval in league configuration -- **API errors**: Check your internet connection and ESPN API availability +These need panel height to be legible. The traditional scoreboard in particular +wants 64 rows or more; on a 32-row panel there is no room for a line score. -## Vegas ticker: seeing live games more often +### Fonts, colours and offsets -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: +`customization` restyles each text element independently, and is plugin-wide +rather than per-league. Each group takes `font`, `font_size` and `text_color` +(an RGB array): ```json { - "display": { - "vegas_scroll": { - "live_in_ticker": true, - "live_weight": 3, - "favorite_live_weight": 5 - } + "customization": { + "score_text": { "font_size": 12, "text_color": [255, 220, 0] } } } ``` -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*. +`customization.layout` nudges individual elements by `x_offset` / `y_offset` +for panels where something sits slightly wrong. -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. +## Panel Sizes -## 🎯 Which Games Get Shown +The scoreboard lays itself out from the panel dimensions rather than assuming a +size. -**`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. +![The same live game rendered on 64x32, 128x32, 128x64 and 256x32 +panels](../../docs/assets/baseball-scoreboard/panel-sizes.png) -Which mode you are in depends on whether `favorite_teams` is set and whether `show_favorite_teams_only` is on: +- **64×32** is tight for baseball specifically: the bases diamond, outs and + count all compete for the centre with the score. It works, but a wider panel + is much easier to read across a room. +- **128×32** is the size everything is tuned for. +- **128×64** gives the scorebug room and is what the traditional scoreboard and + player-card screens want. +- **256×32** keeps element sizes and centres them, so a long chain buys width + rather than a bigger scoreboard — `scroll` mode is what makes a long panel + pay off. -| `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**. | +--- -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 settings - -| 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. | -| `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. | - -**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. - -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. - -### 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. - -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. - -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. - -> `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. +## Troubleshooting -## Matchup separator and the upcoming card middle +**Nothing shows for a league I enabled.** +Check that league's `display_modes` — all three can be off. Then check whether +the league is in season: NCAA Baseball runs roughly February to June, and MiLB +finishes before MLB does. + +**The upcoming screen is empty but I know there are games.** +`schedule_lookahead_days` defaults to `7`; during an All-Star break or between +series the next fixture can sit beyond it. The other common cause is +`show_favorite_teams_only: true` with favourites who are not playing. + +**I see far more games than I asked for.** +`upcoming_games_to_show` is a *per-team* budget when `show_favorite_teams_only` +is on. See [How Games Are Picked](#how-games-are-picked). + +**My settings seem to be ignored.** +Check they are inside the league block. `favorite_teams` at the top level does +nothing — it has to be `mlb.favorite_teams`. See +[Three Leagues, Three Config Blocks](#three-leagues-three-config-blocks). + +**Start times are wrong by hours.** +Set `timezone`. ESPN reports first pitch in UTC and the plugin converts on +display. + +**A team shows as a grey box instead of a logo.** +Logos are downloaded on demand and cached. A failed download used to be cached +permanently; on a current core it is retried automatically after six hours. If +you are on an older core, delete the undersized file (a real logo is tens of +kilobytes) and restart. + +**The pitcher/batter or player-card screen never appears.** +Both need an extra per-game fetch and only appear during a live at-bat. They +also need panel height — check a 128×64 panel before concluding they are +broken. + +--- + +## Development + +### File structure + +```text +baseball-scoreboard/ +├── manifest.json # Metadata and version history +├── manager.py # BaseballScoreboardPlugin: league and mode routing +├── mlb_managers.py # MLB fetching and cache keys +├── milb_managers.py # MiLB fetching +├── ncaa_baseball_managers.py # NCAA fetching +├── sports.py # Shared sports engine: selection, extraction, rendering +├── logo_manager.py # Logo loading and download +├── config_schema.json # Settings schema; source of truth for defaults +└── test_*.py # Standalone regression tests +``` -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. +`sports.py` and the other shared files are **copies** carried by every sports +scoreboard in this monorepo. A fix in one must be ported to its siblings in the +same change — see +[docs/plugin-development/08-shared-sports-code.md](../../docs/plugin-development/08-shared-sports-code.md). -| 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. | +### Data source -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. +ESPN's public scoreboard APIs for `baseball/mlb`, +`baseball/college-baseball` and the MiLB feeds. No API key required. -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. +One wrinkle worth knowing if you are testing: the recent and upcoming managers +read through the cache, but the live manager calls +`_fetch_todays_games()` straight over HTTP with no cache read. Seeded cache +fixtures therefore cannot drive the live screen. -Example: +### Regenerating the images in this README -```json -{ - "scroll_card": { - "vs_text": "@", - "switch_upcoming_center": "vs", - "date_format": "weekday" - } -} +```bash +python scripts/render_docs_assets.py --plugin baseball-scoreboard ``` -### 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. +The fixtures under `docs/assets/baseball-scoreboard/fixtures/` are real ESPN +responses captured on 2 September 2026. Because the live manager bypasses the +cache, its screen is fed by a **recorded HTTP response** rather than seeded +cache data — that is what `http_replay` in `shots.json` does. The result is +real data that re-renders identically every time, which +`--check` verifies. -| 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. +## Support -```json -{ - "customization": { - "score_text": { "text_color": [255, 200, 0] }, - "status_text": { "text_color": "#00A0FF" } - } -} -``` +- YouTube: +- Instagram: +- Discord: +- Sponsor: [GitHub Sponsors](https://github.com/sponsors/ChuckBuilds) · + [Buy Me a Coffee](https://buymeacoffee.com/chuckbuilds) · + [Ko-fi](https://ko-fi.com/chuckbuilds/) -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. +Released under the GNU General Public License v3.0 — see [LICENSE](LICENSE). diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index f71a76b1..cb630c1d 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "baseball-scoreboard", "name": "Baseball Scoreboard", - "version": "1.37.1", + "version": "1.37.2", "update_interval": 60, "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", @@ -31,6 +31,13 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "version": "1.37.2", + "released": "2026-09-02", + "ledmatrix_min_version": "3.2.0", + "notes": "Documentation only, no behaviour change. Rewrites the README around real rendered screenshots and documents every configuration option. Leads with the structural thing that trips people up: almost every setting lives inside a per-league block (mlb/milb/ncaa_baseball), and only nine are plugin-wide, so favorite_teams at the top level does nothing. Explains the three game-selection paths and that upcoming_games_to_show is a per-team budget under show_favorite_teams_only but a total otherwise. Documents the four extra baseball screens (pitcher/batter, last play, player card, traditional scoreboard), all off by default and all costing an extra per-game fetch, and notes show_odds is fetched asynchronously so the line appears a moment after the card is drawn.", + "changelog": "Documentation only, no behaviour change. Rewrites the README around real rendered screenshots and documents every configuration option. Leads with the structural thing that trips people up: almost every setting lives inside a per-league block (mlb/milb/ncaa_baseball), and only nine are plugin-wide, so favorite_teams at the top level does nothing. Explains the three game-selection paths and that upcoming_games_to_show is a per-team budget under show_favorite_teams_only but a total otherwise. Documents the four extra baseball screens (pitcher/batter, last play, player card, traditional scoreboard), all off by default and all costing an extra per-game fetch, and notes show_odds is fetched asynchronously so the line appears a moment after the card is drawn." + }, { "version": "1.37.1", "released": "2026-09-02", diff --git a/scripts/docs_render_support/_docs_http_replay.py b/scripts/docs_render_support/_docs_http_replay.py new file mode 100644 index 00000000..9082ba5f --- /dev/null +++ b/scripts/docs_render_support/_docs_http_replay.py @@ -0,0 +1,90 @@ +"""Serve recorded HTTP responses to a documentation render. + +Some managers fetch straight from the network with no cache read, so the +renderer's ``--mock-data`` (which seeds the cache) cannot reach them. Without +this, those screens could only be documented with synthetic data or with a +one-off live capture that no later run could reproduce. + +A replay file records real responses, so the render is both real data and +repeatable:: + + {"matches": [ + {"url_contains": "baseball/mlb/scoreboard", "body": { ...ESPN JSON... }} + ]} + +Only matching URLs are intercepted. Everything else -- logo downloads in +particular -- goes to the real network untouched. +""" + +import json +import os + + +class _ReplayResponse: + """The slice of requests.Response that plugin fetch paths actually use.""" + + def __init__(self, payload): + self._payload = payload + self.status_code = 200 + self.headers = {"content-type": "application/json"} + self.encoding = "utf-8" + + def json(self, **_kwargs): + return self._payload + + @property + def text(self): + return json.dumps(self._payload) + + @property + def content(self): + return self.text.encode("utf-8") + + def raise_for_status(self): + return None + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False + + +def install(): + """Patch requests so recorded URLs are served locally. No-op if unset.""" + path = os.environ.get("LEDMATRIX_DOCS_HTTP_REPLAY") + if not path: + return + try: + import requests + except ImportError: + return + + with open(path, "r", encoding="utf-8") as handle: + matches = json.load(handle).get("matches", []) + if not matches: + return + + def _match(url): + for entry in matches: + if entry.get("url_contains", "") in url: + return entry.get("body") + return None + + real_session_get = requests.Session.get + real_get = requests.get + + def session_get(self, url, *args, **kwargs): + body = _match(str(url)) + if body is None: + return real_session_get(self, url, *args, **kwargs) + return _ReplayResponse(body) + + def plain_get(url, *args, **kwargs): + body = _match(str(url)) + if body is None: + return real_get(url, *args, **kwargs) + return _ReplayResponse(body) + + requests.Session.get = session_get + requests.get = plain_get diff --git a/scripts/docs_render_support/sitecustomize.py b/scripts/docs_render_support/sitecustomize.py index a2291c6a..20844d5e 100644 --- a/scripts/docs_render_support/sitecustomize.py +++ b/scripts/docs_render_support/sitecustomize.py @@ -44,3 +44,11 @@ def today(cls): _datetime_module.datetime = _FrozenDateTime _time_module.time = lambda: _timestamp + +# Recorded HTTP responses, for managers that fetch without reading the cache. +try: + import _docs_http_replay + + _docs_http_replay.install() +except Exception: # never let doc tooling break the render it is measuring + pass diff --git a/scripts/render_docs_assets.py b/scripts/render_docs_assets.py index d3c5864b..42a1d93d 100644 --- a/scripts/render_docs_assets.py +++ b/scripts/render_docs_assets.py @@ -53,7 +53,9 @@ Keys on a shot: ``name`` (required, becomes ``.png``), ``width``, ``height``, ``scale``, ``config`` (merged over schema defaults), ``mock_data`` (inline object, or a path relative to the shot list), ``skip_update``, -``freeze_time`` (ISO-8601 instant; pins "now" so the image is reproducible) and +``freeze_time`` (ISO-8601 instant; pins "now" so the image is reproducible), +``http_replay`` (a recorded-responses file, for managers that fetch without +reading the cache) and ``env`` (extra environment variables for the render subprocess). Anything omitted falls back to ``defaults``. Set ``"standalone": false`` on a shot that only exists to be pasted into a composite. @@ -117,6 +119,24 @@ } +def deep_merge(base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]: + """Merge ``overlay`` into ``base``, recursing into nested dicts. + + A shot that overrides one key of a per-league block must not wipe the rest + of it. With a shallow update, a shot setting only ``mlb.display_modes`` + replaces the whole ``mlb`` block from ``defaults`` -- including + ``mlb.enabled`` -- and the plugin renders nothing for reasons that look + nothing like the cause. + """ + merged = dict(base) + for key, value in overlay.items(): + if isinstance(value, dict) and isinstance(merged.get(key), dict): + merged[key] = deep_merge(merged[key], value) + else: + merged[key] = value + return merged + + def _load_font(weight: str, size: int): from PIL import ImageFont @@ -209,8 +229,7 @@ def render_shot( height = int(shot.get("height", defaults.get("height", DEFAULT_HEIGHT))) scale = int(shot.get("scale", defaults.get("scale", DEFAULT_SCALE))) - config = dict(defaults.get("config", {})) - config.update(shot.get("config", {})) + config = deep_merge(defaults.get("config", {}), shot.get("config", {})) raw_path = tmpdir / f"{name}-raw.png" renderer = core_repo / "scripts" / "render_plugin.py" @@ -243,11 +262,19 @@ def render_shot( # A frozen clock is what makes a README image reproducible: without it a # clock, a countdown, or a "starts in 2h" line differs on every run. freeze_time = shot.get("freeze_time", defaults.get("freeze_time")) - if freeze_time: - env["LEDMATRIX_DOCS_FREEZE_TIME"] = str(freeze_time) + http_replay = shot.get("http_replay", defaults.get("http_replay")) + if freeze_time or http_replay: support_dir = str(Path(__file__).resolve().parent / "docs_render_support") existing = env.get("PYTHONPATH") env["PYTHONPATH"] = f"{support_dir}{os.pathsep}{existing}" if existing else support_dir + if freeze_time: + env["LEDMATRIX_DOCS_FREEZE_TIME"] = str(freeze_time) + if http_replay: + replay_path = (shot_list_dir / http_replay).resolve() + if not replay_path.is_file(): + raise SystemExit( + f"http_replay file not found for shot '{name}': {replay_path}") + env["LEDMATRIX_DOCS_HTTP_REPLAY"] = str(replay_path) env.update({str(k): str(v) for k, v in defaults.get("env", {}).items()}) env.update({str(k): str(v) for k, v in shot.get("env", {}).items()}) From 03489916b62ef06ccdfd729c6021d18727bc7153 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:59:19 -0400 Subject: [PATCH 2/2] docs(baseball-scoreboard): restore detail the rewrite dropped Reviewing my own diff against main, the rewrite was a net loss of information: better organised, but several sections of hard-won documentation were gone. Restored and folded into the new structure. The worst of it was a correction to my own framing. upcoming_games_to_show is a POOL size, not a card count: the panel shows one game at a time and keeps its place between visits, so raising it lengthens the lap and any one game comes round LESS often. My version implied the opposite, which would have sent someone the wrong way. Also restored, all of it verified against the schema: - other_games_min_quality's real values (ranked/broadcast/any) and the fact that it is inert in MLB and MiLB, which publish no poll; other_games_divisions needs ESPN's FBS/FCS group rosters, which exist for college football and nothing else, so it is inert in this plugin entirely. I had described it as "NCAA only", which is wrong. - That favourites are never filtered by either setting, how the non-favourite pool is ordered, and that both filters fail open -- twice, including as a set. - The traditional scoreboard's full customization table (16 options), the at_bat_info table, and the player_card table. - That all four extra screens are MLB and NCAA only -- MiLB's data does not come from ESPN in the same shape -- and that the pitcher/batter and player card screens are live-games-only, which is why only the traditional scoreboard has a game_scope. - The whole scroll_card matchup-card section: separator, the two date formats and why their defaults differ, time format, swap behaviour. - The customization font/colour group table. - The stale "UTC" timezone migration note. - The non_favorite_live_game_duration truth table. - Favourite result colours, the Vegas ticker weighting, team abbreviations, installation, and per-league background_service. Checked mechanically as well as by eye: every config token the old README mentioned now appears in the new one, bar six bare names I document under their full path. All TOC anchors resolve. Co-Authored-By: Claude Opus 5 --- plugins/baseball-scoreboard/README.md | 323 ++++++++++++++++++++++++-- 1 file changed, 310 insertions(+), 13 deletions(-) diff --git a/plugins/baseball-scoreboard/README.md b/plugins/baseball-scoreboard/README.md index 23f60e72..bbebae44 100644 --- a/plugins/baseball-scoreboard/README.md +++ b/plugins/baseball-scoreboard/README.md @@ -19,30 +19,39 @@ inning, bases, outs and count are what the panel showed at that moment.* 1. [What's On Screen](#whats-on-screen) 2. [Quick Start](#quick-start) -3. [Three Leagues, Three Config Blocks](#three-leagues-three-config-blocks) -4. [How Games Are Picked](#how-games-are-picked) -5. [Configuration Reference](#configuration-reference) +3. [Installation](#installation) +4. [Three Leagues, Three Config Blocks](#three-leagues-three-config-blocks) +5. [How Games Are Picked](#how-games-are-picked) +6. [Configuration Reference](#configuration-reference) - [Plugin-wide settings](#plugin-wide-settings) - [Per-league: teams and filtering](#per-league-teams-and-filtering) - [Per-league: display modes](#per-league-display-modes) - [Per-league: how many games](#per-league-how-many-games) - [Per-league: durations](#per-league-durations) - [Per-league: update intervals](#per-league-update-intervals) + - [Per-league: background fetching](#per-league-background-fetching) - [Per-league: what appears on the card](#per-league-what-appears-on-the-card) - [The extra baseball screens](#the-extra-baseball-screens) + - [The matchup card: separator, date and time](#the-matchup-card-separator-date-and-time) + - [Favourite team result colours](#favourite-team-result-colours) + - [Seeing live games more often in the Vegas ticker](#seeing-live-games-more-often-in-the-vegas-ticker) - [Fonts, colours and offsets](#fonts-colours-and-offsets) -6. [Panel Sizes](#panel-sizes) -7. [Troubleshooting](#troubleshooting) -8. [Development](#development) -9. [Support](#support) +7. [Team Abbreviations](#team-abbreviations) +8. [Panel Sizes](#panel-sizes) +9. [Troubleshooting](#troubleshooting) +10. [Development](#development) +11. [Support](#support) --- ## What's On Screen Each league contributes three display modes, so the plugin exposes nine in -total: `mlb_live`, `mlb_recent`, `mlb_upcoming`, and the same trio for `milb_` -and `ncaa_baseball_`. Every one can be enabled or disabled independently. +total — `mlb_live`, `mlb_recent`, `mlb_upcoming`, `milb_live`, `milb_recent`, +`milb_upcoming`, `ncaa_baseball_live`, `ncaa_baseball_recent` and +`ncaa_baseball_upcoming`. Every one can be enabled or disabled independently, +via the `show_live` / `show_recent` / `show_upcoming` flags inside that +league's `display_modes` block. ![The three MLB display modes on a 128x32 panel: a live scorebug, a final score, and an upcoming game with its first-pitch @@ -93,6 +102,20 @@ it on costs requests for screens that will never have anything to show. --- +## Installation + +**From the Plugin Store (recommended).** Open the LEDMatrix web interface at +`http://:5000`, go to **Plugin Manager**, find **Baseball +Scoreboard** in the **Plugin Store** section, and click **Install**. Its own tab +appears in the second nav row for configuring leagues and favourite teams. + +**Manually.** Copy this directory into your LEDMatrix `plugins_directory` +(default `plugin-repos/`) and restart the display service. The plugin's +metadata, including the nine display modes it registers, lives in +`manifest.json`. + +--- + ## Three Leagues, Three Config Blocks This is the biggest structural difference from the single-league scoreboards. @@ -156,6 +179,39 @@ confusing thing in the configuration: Three favourite teams with a value of `3` is up to nine cards in the exclusive path and three in the others. +**And it is a pool size, not a card count.** This catches people out: the panel +shows one game at a time and keeps its place between visits, cycling through +the pool. So raising `upcoming_games_to_show` gives you a *longer lap* — any +one game comes round **less** often, not more. If you want your team's next +game on screen more, make the number smaller, not bigger. + +### Which non-favourite games fill the remaining slots + +Two settings decide what qualifies, and both have important limits in baseball: + +| Option | Values | Reality in this plugin | +|--------|--------|------------------------| +| `other_games_min_quality` | `ranked`, `broadcast`, `any` | `ranked` needs a national poll. MLB and MiLB publish none, so it lets every game through and no poll is requested. It only bites for NCAA Baseball | +| `other_games_divisions` | e.g. `["fbs"]` | Needs ESPN's FBS/FCS group rosters, which exist for **college football and nothing else**. Inert here — no lookup is made | + +**Your favourite teams are never filtered by either.** Follow a lower-division +side and its games always appear; these only decide what fills the *remaining* +slots. + +Within the non-favourite pool the better matchup leads and each team appears +once: it 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 starts soonest. Ties fall back to start time, and a league with no +poll — MLB, MiLB — simply stays chronological. Favourites are always ordered by +when they play; for your own team the next game is the point. + +Both filters **fail open**. If the data behind them cannot be fetched the game +is allowed through, and if the filters between them leave nothing at all — your +teams idle and every other game rejected — the unfiltered list is used instead. +A board showing filler beats a board showing nothing. 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. + Two more rules that apply on top: - `exclude_teams` beats everything — a team listed there is hidden from the @@ -201,6 +257,12 @@ These nine sit at the top level, outside any league block. | `game_display_duration` | `15` | Seconds each individual game shows before the next one within the same mode | | `update_interval` | `3600` | Base fetch interval | | `timezone` | `""` | **Advanced.** IANA timezone for start times, e.g. `America/Chicago`. Blank follows the global LEDMatrix timezone, then the system one | + +> **A leftover `"UTC"` from an older version?** This plugin used to persist +> `"timezone": "UTC"` into your saved config, where it then shadowed your real +> global timezone. That stale value is now detected and ignored automatically +> whenever your global or system timezone disagrees — no manual edit needed. If +> you genuinely want UTC here, set `Etc/UTC`, which is always honoured. | `schedule_lookback_days` | `14` | **Advanced.** How far back the recent screens can see | | `schedule_lookahead_days` | `7` | **Advanced.** How far ahead the upcoming screens can see | | `no_data_interval_seconds` | `300` | **Advanced.** Gap between live checks when nothing is on, backing off the longer it stays quiet | @@ -276,6 +338,17 @@ games on at once, `live_game_duration: 30` and `non_favorite_live_game_duration: 8` keeps your club's game on screen while the rest still tick past. +It only takes effect when favourites are configured **and** non-favourite live +games are actually being shown — otherwise there is nothing on screen to +shorten: + +| Favourites set? | Non-favourite games shown? | Game has a favourite? | Duration used | +|---|---|---|---| +| No | — | — | `live_game_duration` | +| Yes | No (`show_favorite_teams_only` on, `show_all_live` off) | yes | `live_game_duration` | +| Yes | Yes (`show_favorite_teams_only` off, or `show_all_live` on) | yes | `live_game_duration` | +| Yes | Yes | no | `non_favorite_live_game_duration`, when above `0` | + ### Per-league: update intervals All **advanced**. The defaults are tuned for a Raspberry Pi that is also driving @@ -289,6 +362,20 @@ a panel. | `update_interval_seconds` | `3600` | Base fetch interval for this league | | `stale_game_timeout` | `300` | How long a live game may go without an update before it is dropped | +### Per-league: background fetching + +Data is fetched on a background thread so the panel never stalls on the +network. Under each league's `background_service`, all **advanced**: + +| Option | Default | What it does | +|--------|---------|--------------| +| `enabled` | `true` | Fetch in the background rather than inline | +| `request_timeout` | `30` | Seconds before a request gives up | +| `max_retries` | `3` | Retries per failed request | +| `priority` | `2` | Queue priority against other plugins' fetches (medium) | + +--- + ### Per-league: what appears on the card | Option | Default | What it does | @@ -323,14 +410,204 @@ all cost an extra per-game data fetch. `show_last_play` only does anything with `show_pitcher_batter` on — it adds a field to that screen rather than being a screen of its own. -These need panel height to be legible. The traditional scoreboard in particular -wants 64 rows or more; on a 32-row panel there is no room for a line score. +**All four are MLB and NCAA Baseball only.** MiLB's data does not come from +ESPN in the same shape, so the flags exist under `milb.display_options` but +nothing is wired up behind them. The pitcher/batter and player-card screens are +additionally **live-games only**, since the current at-bat is the data they are +built from; only the traditional scoreboard has a `game_scope` option, because +only it has anything to say about a finished game. + +They also need panel height. The traditional scoreboard wants 64 rows or more +for a line score; the player card hides the headshot and falls back to a +compact two-line text card on a 64×32 panel. + +#### Traditional scoreboard: `customization.traditional_scoreboard` + +A full-screen ballpark scoreboard — inning-by-inning line score with R/H/E, the +current inning highlighted, and a lit ball/strike/out column during a live +at-bat. It is not a display mode you select: it periodically *rotates into* +whichever game the normal rotation is already showing, then reverts. + +| Option | Default | What it does | +|--------|---------|--------------| +| `game_scope` | `both` | `live`, `recent`, or `both`. `recent` is the "glance at the final line score" setting | +| `favorites_only` | `false` | Only rotate in for games involving this league's `favorite_teams`. Independent of the normal rotation's filtering, so you can watch every live game in the compact scorebug but reserve the full-screen treatment for your team | +| `dwell_seconds` | `6` | How long it stays each time it rotates in | +| `interval_seconds` | `30` | How often it rotates in | +| `font` | `9x15.bdf` | A fixed-size `.bdf` renders at its native pixel size with a smaller-sibling fallback; use a scalable `.ttf` (e.g. `press_start`) if you want `font_size` to control the size | +| `font_size` | `24` | Cap for scalable fonts only; ignored by `.bdf`. The screen auto-fits, so the default means "as big as the panel allows" | +| `use_team_colors` | `true` | Colour each abbreviation with the team's real ESPN colours instead of flat `text_color` | +| `show_team_color_backgrounds` | `true` | Tint each row with a ~12% wash of the team's colour plus an accent strip. Requires `use_team_colors` | +| `show_logos` | `true` | Small team logo beside each abbreviation when there is spare width | +| `show_dividers` | `true` | 1px grid lines between innings, rows and the R/H/E columns | +| `highlight_winner` | `true` | On a final game, colour the winner's run total in `winner_color`. Pairs with `game_scope: recent` | +| `text_color` | `[255, 255, 255]` | Score digits, and abbreviations when team colours are off | +| `header_color` | `[180, 180, 180]` | The inning-number and R/H/E header row | +| `highlight_color` | `[255, 140, 0]` | Current-inning highlight, the batting-team arrow, and lit ball/strike/out indicators | +| `divider_color` | `[90, 90, 90]` | The grid lines | +| `winner_color` | `[0, 200, 0]` | The winning team's run total on a final game | + +```json +{ + "mlb": { "display_options": { "show_traditional_scoreboard": true } }, + "customization": { + "traditional_scoreboard": { "game_scope": "recent", "favorites_only": true } + } +} +``` + +#### Pitcher / batter: `customization.at_bat_info` + +Names the current at-bat's pitcher and batter, labelled in full +(`Pitcher: G. Cole` / `Batter: J. Soto`) so there is no confusing the `B` with +the balls indicator. Text auto-fits, falling back to a smaller font — and only +as a last resort truncating — rather than running off the edge. + +| Option | Default | What it does | +|--------|---------|--------------| +| `favorites_only` | `false` | Only rotate in for favourites' games | +| `dwell_seconds` | `4` | How long it stays | +| `interval_seconds` | `25` | How often it rotates in | +| `font` | `9x15.bdf` | As above | +| `font_size` | `24` | Cap for scalable fonts only | +| `use_team_colors` | `true` | Pitcher in the fielding team's colour, batter in the batting team's | +| `pitcher_color` | `[255, 255, 255]` | Fallback when team colours are off | +| `batter_color` | `[255, 255, 0]` | Fallback when team colours are off | +| `last_play_color` | `[0, 255, 255]` | The last-play code — always flat, since a play code belongs to no team | + +#### Player card + +A "baseball card" for the current batter, and optionally the pitcher: headshot, +jersey number, position, bat/throw, and season stats (`AVG`/`HR`/`RBI` for +hitters, `ERA`/`W-L`/`K` for pitchers). Headshots come from ESPN's athlete API +and are cached in memory and on disk under `assets/headshots/`, which is +gitignored. If a headshot cannot be loaded the card renders text-only. + +Under `customization.player_card`: + +| Option | Default | What it does | +|--------|---------|--------------| +| `show_batter` | `true` | A card for the current batter | +| `show_pitcher` | `false` | Also a card for the pitcher. The batter wins when both are available | +| `favorites_only` | `false` | Only rotate in for favourites' games | +| `dwell_seconds` | `6` | How long the card stays | +| `interval_seconds` | `40` | How often it rotates in | +| `font` / `font_size` | `9x15.bdf` / `24` | As for the other screens; the cap applies to scalable fonts only | +| `use_team_colors` | `true` | The player's name in their team's ESPN colour | +| `use_team_colors_border` | `true` | The headshot frame in the team's colour | +| `border_color` | `[255, 200, 0]` | Frame colour when the team colour is off or unavailable | +| `text_color` | `[255, 255, 255]` | Name and the jersey/position/bat-throw line | +| `stat_color` | `[0, 220, 255]` | The season-stats line | + +### The matchup card: separator, date and time + +The **Matchup Card Layout** group (`scroll_card`, advanced) controls what sits +between the two logos before a game starts and how the date and time are +written. It applies to every display mode — the scroll ticker, the Vegas ticker +and the full-screen scoreboard. + +| Key | Default | Values | +|-----|---------|--------| +| `vs_text` | `VS` | Any short string: `VS`, `@`, `at`, `v`. The away team is on the left, so `@` and `at` read as "away at home". Blank draws nothing | +| `upcoming_center` | `vs` | Scroll and Vegas cards: `vs`, `date_time`, `none` | +| `switch_upcoming_center` | `date_time` | The full-screen scoreboard: `date_time`, `vs`, `none`, `inherit` | +| `date_format` | `abbrev` | Scroll and Vegas: `abbrev` (Sep 19), `numeric` (9/19), `day_first` (19 Sep), `numeric_day_first` (19/9), `weekday` (Fri Sep 19) | +| `switch_date_format` | `numeric` | The same set for the full-screen scoreboard, plus `inherit` | +| `time_format` | `12h` | `12h` (7:40PM) or `24h` (19:40) | +| `show_date` / `show_time` | `true` | Drop either line | +| `swap_date_time` | `false` | Swap the two lines. Each display starts from its own order, so this flips rather than forces: cards put the time on top, the full-screen stack puts the date on top | + +The two `*_date_format` keys have different defaults on purpose: the cards have +always written `Sep 19` and the full-screen scoreboard `9/19`, so one shared +default would have restyled one of them. + +Choosing the separator for the full-screen scoreboard moves the date and time +out of the middle and onto the top and bottom rows, and the "Next Game" header +gives up the top row to them. The `center_gap*` keys size the scroll and Vegas +card's middle strip only — the full-screen scoreboard pins its logos to the +panel edges and ignores them. + +```json +{ + "scroll_card": { + "vs_text": "@", + "switch_upcoming_center": "vs", + "date_format": "weekday" + } +} +``` + +### Favourite team result colours + +A run of games against the same opponent is hard to read at a glance: the same +two logos go past and only the digits change. Turn this on to colour a finished +game's score by how your team did — green for a win, red for a loss. + +```json +{ + "customization": { + "favorite_result_colors": { + "enabled": true, + "win_color": [0, 255, 0], + "loss_color": [255, 0, 0], + "tie_color": [255, 200, 0] + } + } +} +``` + +Off by default. Only *finished* games are coloured — live and upcoming cards are +untouched — and a game needs exactly one favourite in it: if neither side is a +favourite, or both are, the score keeps its normal colour. The three colours are +advanced settings; leave them alone for the defaults above. + +### Seeing live games more often in the Vegas ticker + +By default a live game **takes over** the display: the Vegas ticker stops and +this scoreboard goes full screen until the game ends. To keep the marquee +scrolling and still see scores, the settings live in the **core** LEDMatrix +config rather than in this plugin: + +```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 — so with a dozen plugins a score comes round once a lap. `live_weight` +applies whenever this scoreboard has any live game; `favorite_live_weight` +applies when one of your teams is playing, a distinction that has to be made +here because the core can tell *that* a game is live but not *whose*. + +The weight is per **plugin**, not per game: with fifteen games live this +scoreboard still occupies one slot at a time and picks between its own games +using `filtering.favorite_live_boost`. And more slots make the cycle *longer*, +not faster — everything else appears proportionally less often. ### Fonts, colours and offsets `customization` restyles each text element independently, and is plugin-wide -rather than per-league. Each group takes `font`, `font_size` and `text_color` -(an RGB array): +rather than per-league. Each group takes `font`, `font_size` and `text_color`, +and the colour applies to the text drawn in that element's face on the +full-screen scoreboard and on the scroll and Vegas cards alike. + +| Group | Covers | +|-------|--------| +| `score_text` | The score, and the matchup separator on an upcoming card | +| `period_text` | The clock, the inning, and the date and time on an upcoming scoreboard | +| `team_name` | Team names and abbreviations | +| `status_text` | Status lines such as "Next Game" | +| `detail_text` | Small detail lines | +| `rank_text` | Team rankings | + +Colours are `[r, g, b]` or `"#RRGGBB"`, and every default is white: ```json { @@ -345,6 +622,26 @@ for panels where something sits slightly wrong. --- +## Team Abbreviations + +`favorite_teams` and `exclude_teams` take ESPN's abbreviation, not the club +name. If you are unsure of one, enable debug logging — the plugin logs +`home_abbr` and `away_abbr` for every game it processes. + +**MLB.** `ARI` `ATL` `BAL` `BOS` `CHC` `CHW` `CIN` `CLE` `COL` `DET` `HOU` +`KC` `LAA` `LAD` `MIA` `MIL` `MIN` `NYM` `NYY` `OAK` `PHI` `PIT` `SD` `SEA` +`SF` `STL` `TB` `TEX` `TOR` `WAS` + +**NCAA Baseball.** `LSU` `FLA` `VANDY` `ARK` `MISS` `TAMU` `TENN` `UK` `UGA` +`BAMA` `AUB` `SCAR` `CLEM` `FSU` `MIA` `UNC` `DUKE` `WAKE` `VT` `LOU`, among +many others. + +**MiLB.** Abbreviations vary by league and level (AAA, AA, A+, A) — for example +`DUR` (Durham Bulls), `SWB` (Scranton/Wilkes-Barre RailRiders), `MEM` (Memphis +Redbirds). The debug-log trick is the reliable way to find an affiliate's code. + +--- + ## Panel Sizes The scoreboard lays itself out from the panel dimensions rather than assuming a