led-ticker-baseball 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. led_ticker_baseball-0.1.0/.gitignore +13 -0
  2. led_ticker_baseball-0.1.0/.pre-commit-config.yaml +16 -0
  3. led_ticker_baseball-0.1.0/CLAUDE.md +143 -0
  4. led_ticker_baseball-0.1.0/LICENSE +21 -0
  5. led_ticker_baseball-0.1.0/Makefile +18 -0
  6. led_ticker_baseball-0.1.0/PKG-INFO +408 -0
  7. led_ticker_baseball-0.1.0/README.md +380 -0
  8. led_ticker_baseball-0.1.0/docs/attendance.gif +0 -0
  9. led_ticker_baseball-0.1.0/docs/ball-emoji-hires.png +0 -0
  10. led_ticker_baseball-0.1.0/docs/ball-emoji.png +0 -0
  11. led_ticker_baseball-0.1.0/docs/promotions.gif +0 -0
  12. led_ticker_baseball-0.1.0/docs/roll-transition.gif +0 -0
  13. led_ticker_baseball-0.1.0/docs/scores.gif +0 -0
  14. led_ticker_baseball-0.1.0/docs/standings.gif +0 -0
  15. led_ticker_baseball-0.1.0/docs/statcast.gif +0 -0
  16. led_ticker_baseball-0.1.0/docs/superpowers/plans/2026-06-11-promotions-widget.md +1536 -0
  17. led_ticker_baseball-0.1.0/docs/superpowers/plans/2026-06-12-statcast-widget.md +1517 -0
  18. led_ticker_baseball-0.1.0/docs/superpowers/plans/2026-06-13-attendance-widget.md +1898 -0
  19. led_ticker_baseball-0.1.0/docs/superpowers/plans/2026-06-14-demo-gifs-and-readme-patterns.md +280 -0
  20. led_ticker_baseball-0.1.0/docs/superpowers/plans/2026-06-15-attendance-abbr-and-probe-dry.md +482 -0
  21. led_ticker_baseball-0.1.0/docs/superpowers/plans/2026-06-15-statcast-team-filter.md +624 -0
  22. led_ticker_baseball-0.1.0/docs/superpowers/specs/2026-06-10-promotions-widget-design.md +180 -0
  23. led_ticker_baseball-0.1.0/docs/superpowers/specs/2026-06-11-statcast-widget-design.md +202 -0
  24. led_ticker_baseball-0.1.0/docs/superpowers/specs/2026-06-13-attendance-widget-design.md +287 -0
  25. led_ticker_baseball-0.1.0/docs/superpowers/specs/2026-06-14-demo-gifs-and-readme-patterns-design.md +211 -0
  26. led_ticker_baseball-0.1.0/docs/superpowers/specs/2026-06-14-statcast-team-filter-design.md +138 -0
  27. led_ticker_baseball-0.1.0/docs/superpowers/specs/2026-06-15-attendance-abbr-and-probe-dry-design.md +121 -0
  28. led_ticker_baseball-0.1.0/pyproject.toml +60 -0
  29. led_ticker_baseball-0.1.0/src/led_ticker_baseball/__init__.py +34 -0
  30. led_ticker_baseball-0.1.0/src/led_ticker_baseball/attendance.py +590 -0
  31. led_ticker_baseball-0.1.0/src/led_ticker_baseball/emoji.py +199 -0
  32. led_ticker_baseball-0.1.0/src/led_ticker_baseball/promotions.py +431 -0
  33. led_ticker_baseball-0.1.0/src/led_ticker_baseball/scores.py +1609 -0
  34. led_ticker_baseball-0.1.0/src/led_ticker_baseball/standings.py +304 -0
  35. led_ticker_baseball-0.1.0/src/led_ticker_baseball/statcast.py +476 -0
  36. led_ticker_baseball-0.1.0/src/led_ticker_baseball/teams.py +218 -0
  37. led_ticker_baseball-0.1.0/src/led_ticker_baseball/transition.py +606 -0
  38. led_ticker_baseball-0.1.0/tests/conftest.py +41 -0
  39. led_ticker_baseball-0.1.0/tests/test_attendance.py +963 -0
  40. led_ticker_baseball-0.1.0/tests/test_emoji.py +49 -0
  41. led_ticker_baseball-0.1.0/tests/test_import_purity.py +29 -0
  42. led_ticker_baseball-0.1.0/tests/test_lazy_palette.py +43 -0
  43. led_ticker_baseball-0.1.0/tests/test_promotions.py +659 -0
  44. led_ticker_baseball-0.1.0/tests/test_scoreboard.py +1064 -0
  45. led_ticker_baseball-0.1.0/tests/test_scores.py +1782 -0
  46. led_ticker_baseball-0.1.0/tests/test_smoke.py +30 -0
  47. led_ticker_baseball-0.1.0/tests/test_standings.py +534 -0
  48. led_ticker_baseball-0.1.0/tests/test_statcast.py +1011 -0
  49. led_ticker_baseball-0.1.0/tests/test_teams.py +96 -0
  50. led_ticker_baseball-0.1.0/tests/test_transition.py +497 -0
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .env
10
+
11
+ # Coverage artifacts
12
+ .coverage
13
+ .coverage.*
@@ -0,0 +1,16 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.4.0
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ - repo: local
9
+ hooks:
10
+ - id: pyright
11
+ name: pyright
12
+ entry: uv run pyright src
13
+ language: system
14
+ pass_filenames: false
15
+ always_run: true
16
+ stages: [pre-push]
@@ -0,0 +1,143 @@
1
+ # CLAUDE.md
2
+
3
+ Guidance for Claude Code when working in **led-ticker-baseball**, an external plugin for
4
+ [led-ticker](https://github.com/JamesAwesome/led-ticker).
5
+
6
+ `README.md` is the source of truth for the user-facing surface (widget options, team codes,
7
+ transition variants, install). This file keeps the **load-bearing invariants** a contributor
8
+ must respect, plus navigation aids. When a fact here and the README disagree about *how a
9
+ feature works*, the README wins; this file is the source of truth for *how to keep it working*.
10
+
11
+ ## Overview
12
+
13
+ This plugin contributes, via the `led_ticker.plugins` entry point, an MLB feature set that
14
+ used to live in led-ticker core (`type = "mlb"`):
15
+
16
+ - `baseball.scores` — live/final/preview scores; `ticker`, `scoreboard`, or `two_row` layout.
17
+ - `baseball.standings` — scrolling division standings (top-N + tracked teams), offseason-aware.
18
+ - `baseball.promotions` — upcoming home-game promotions (giveaways/theme nights); today-first with highlight/filter/limit knobs and offseason-aware fallbacks.
19
+ - `baseball.statcast` — daily Statcast superlatives (longest HR, hardest hit,
20
+ fastest/slowest pitch), league-wide or scoped to one team's players via an
21
+ optional `team`; from Baseball Savant's day CSV, schedule-gated.
22
+ - `baseball.attendance` — ballpark attendance: league-wide daily superlatives
23
+ (biggest/smallest crowd, fullest/emptiest park) or one team's game
24
+ (attendance + fill % + venue + weather); schedule-gated.
25
+ - `baseball.roll` / `baseball.roll_reverse` / `baseball.roll_alternating` — a rolling-baseball
26
+ sprite transition (lo-res 4-frame; procedural hi-res on the bigsign).
27
+ - `:baseball.ball:` — inline emoji (8×8 lo-res + 32×32 procedural hi-res).
28
+
29
+ The entry-point name `baseball` is the plugin namespace, so config `type`/transition/emoji
30
+ names are all `baseball.<name>` (see `register()` in `__init__.py`).
31
+
32
+ ## Commands
33
+
34
+ led-ticker is **not on PyPI**; it resolves from a sibling checkout via
35
+ `[tool.uv.sources] led-ticker = { path = "../led-ticker", editable = true }`. CI checks out
36
+ `led-ticker` next to this repo using a read-only deploy key (`LED_TICKER_DEPLOY_KEY`). The
37
+ sibling checkout matters at test time too: `pyproject.toml` puts `../led-ticker/tests/stubs`
38
+ on the pytest path so the rgbmatrix stub is importable headless.
39
+
40
+ ```bash
41
+ uv sync --extra dev # install deps (needs ../led-ticker checked out)
42
+ uv run pytest -q # full suite (asyncio_mode = "auto")
43
+ uv run ruff check src tests # lint — run before pushing
44
+ ```
45
+
46
+ Python **3.14+** only.
47
+
48
+ ## Package layout
49
+
50
+ ```
51
+ src/led_ticker_baseball/
52
+ __init__.py # register(api) entry point — the only place names are registered
53
+ emoji.py # :baseball.ball: — lo-res 8×8 (BALL) + procedural hi-res 32×32 (BALL_HIRES)
54
+ teams.py # shared MLB team colors/names/abbr tables, lazy palette, async resolve_team_id()
55
+ scores.py # baseball.scores widget (MLBScoreMonitor); ticker/scoreboard/two_row; game-state machine
56
+ standings.py # baseball.standings widget (MLBStandingsMonitor); top-N + tracked teams; offseason awareness
57
+ promotions.py # baseball.promotions widget (MLBPromotionsMonitor); home-game promos; today-first + fallback states
58
+ statcast.py # baseball.statcast widget (MLBStatcastMonitor); Savant day-CSV superlatives; schedule-gated
59
+ attendance.py # baseball.attendance widget (MLBAttendanceMonitor); league superlatives + team mode; schedule-gated
60
+ transition.py # baseball.roll* family; lo-res 4-frame + procedural hi-res rotation
61
+ ```
62
+
63
+ All five widget modules import the shared tables from `teams.py` (no widget reaches into
64
+ another widget). `transition.py` reuses the hi-res sprite generator from
65
+ `emoji.py`. These sibling intra-package imports are allowed; see the import contract below.
66
+
67
+ `register(api)` (in `__init__.py`):
68
+
69
+ ```python
70
+ def register(api):
71
+ api.widget("scores")(MLBScoreMonitor)
72
+ api.widget("standings")(MLBStandingsMonitor)
73
+ api.widget("promotions")(MLBPromotionsMonitor)
74
+ api.widget("statcast")(MLBStatcastMonitor)
75
+ api.widget("attendance")(MLBAttendanceMonitor)
76
+ api.transition("roll")(Baseball)
77
+ api.transition("roll_reverse")(BaseballReverse)
78
+ api.transition("roll_alternating")(BaseballAlternating)
79
+ api.emoji("ball", BALL)
80
+ api.hires_emoji("ball", BALL_HIRES)
81
+ ```
82
+
83
+ ## Load-bearing invariants
84
+
85
+ Each rule must hold when modifying the named area.
86
+
87
+ **Import only the public surface** — every `led_ticker` import MUST come from `led_ticker.plugin`,
88
+ never `led_ticker.<internal>`. Enforced by `tests/test_import_purity.py`, which AST-walks every
89
+ source file (catches `from`-imports *and* `import led_ticker.x` forms, not just a text grep).
90
+ Intra-package imports (`from led_ticker_baseball.teams import …`) are fine. If you need a core
91
+ symbol that isn't on `led_ticker.plugin.__all__`, that's a core API change — raise it upstream,
92
+ don't reach around the surface.
93
+
94
+ **Python 3.14 / PEP 649** — no `from __future__ import annotations` anywhere (same rule as core).
95
+ Bare `tuple[int, int, int]` annotations are fine. `ColorTuple` is defined locally in `teams.py`
96
+ because it isn't on the public surface.
97
+
98
+ **`validate_config()` contract** (`MLBScoreMonitor.validate_config`, `scores.py`) — a classmethod
99
+ run pre-coercion by the engine's `validate_widget_cfg`. It **returns `list[str]`** (does NOT raise);
100
+ the engine turns any returned message into a pre-flight `ValueError`. It reproduces the two
101
+ guardrails core formerly applied to `type = "mlb"`: (1) `layout` must be in
102
+ `("ticker", "scoreboard", "two_row")` (`_MLB_VALID_LAYOUTS`); (2) the per-row `top_*` knobs
103
+ (`_TWO_ROW_ONLY`) are rejected by name when `layout != "two_row"` — named, not silently ignored,
104
+ so stale configs surface.
105
+
106
+ **`teams.py` lazy palette is PEP 562** — module-level `__getattr__` exports the named colors
107
+ (`WIN_COLOR`/`LOSS_COLOR`/`LIVE_COLOR`/`CHALLENGE_COLOR`) so external code can
108
+ `from led_ticker_baseball.teams import WIN_COLOR`. **In-module use must call `_team_palette(name)`
109
+ directly** — PEP 562 `__getattr__` does NOT fire for bare-name lookups within the defining module.
110
+
111
+ **Team color lifting** (`_lift_color`, `teams.py`) — dark team colors are scaled so the peak RGB
112
+ channel is ≥ 120, keeping them legible on-panel at low brightness; hue/saturation are preserved
113
+ and already-bright teams are unchanged. Don't bypass it when adding team colors.
114
+
115
+ **Hi-res transition dispatch** — the `baseball.roll*` classes set `scale_switch_at = SNAP_THRESHOLD`
116
+ and branch on `is_scaled(canvas)` (bigsign / `ScaledCanvas`). The hi-res path paints physical LEDs
117
+ via `unwrap_to_real(canvas)` and snaps to incoming at `SNAP_THRESHOLD`. Sprite frames are 8
118
+ rotations at 45° (90° reads as alternating; 22.5° reads chaotic on small panels) and are
119
+ `@functools.cache`'d — geometry is deterministic. `is_scaled` / `unwrap_to_real` / `snap_reset` /
120
+ `SNAP_THRESHOLD` all come from `led_ticker.plugin`; don't hand-copy them back in.
121
+
122
+ **emoji ↔ transition coupling** — `transition.py` imports `_generate_baseball_hires` from
123
+ `emoji.py` **inside a function**, not at module top, to avoid a circular import. Keep it lazy.
124
+
125
+ ## Tests / CI
126
+
127
+ `uv run pytest -q` runs the suite (`tests/`):
128
+
129
+ - `test_import_purity.py` — the AST tripwire (public-surface-only). Treat a failure as a contract
130
+ violation, not a test to relax.
131
+ - `test_smoke.py` — loads the plugin through led-ticker's real plugin loader and asserts the
132
+ widgets/transitions/emoji register under the `baseball.*` namespace (entry-point wiring guard).
133
+ - `test_scores.py` / `test_scoreboard.py` / `test_standings.py` / `test_promotions.py` /
134
+ `test_statcast.py` / `test_attendance.py` / `test_transition.py` / `test_emoji.py` / `test_lazy_palette.py` — behavior + rendering coverage.
135
+
136
+ CI (`.github/workflows/ci.yml`): checks out this repo + led-ticker as siblings (deploy key),
137
+ Python 3.14, `uv sync --extra dev`, then `ruff check src tests` and `pytest -q`.
138
+
139
+ ## Adding to the plugin
140
+
141
+ Register the class in `register()` in `__init__.py` (`api.widget` / `api.transition` /
142
+ `api.emoji` / `api.hires_emoji`); it becomes `baseball.<name>`. Import any core dependency from
143
+ `led_ticker.plugin` only, and keep the import-purity test green.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Awesome
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ .PHONY: dev test lint format typecheck
2
+
3
+ dev: ## Install dev deps + pre-commit hooks
4
+ uv sync --extra dev
5
+ uv run pre-commit install
6
+ uv run pre-commit install --hook-type pre-push
7
+
8
+ test: ## Run tests with coverage
9
+ uv run pytest --cov=src --cov-report=term-missing
10
+
11
+ lint: ## Ruff lint
12
+ uv run ruff check src tests
13
+
14
+ format: ## Ruff format
15
+ uv run ruff format src tests
16
+
17
+ typecheck: ## Pyright
18
+ uv run pyright src
@@ -0,0 +1,408 @@
1
+ Metadata-Version: 2.4
2
+ Name: led-ticker-baseball
3
+ Version: 0.1.0
4
+ Summary: MLB scores/standings widgets, baseball emoji, and baseball transitions for led-ticker.
5
+ Project-URL: Homepage, https://docs.ledticker.dev
6
+ Project-URL: Repository, https://github.com/JamesAwesome/led-ticker-plugins
7
+ Project-URL: Issues, https://github.com/JamesAwesome/led-ticker-plugins/issues
8
+ Author-email: James Awesome <james@morelli.nyc>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Requires-Python: >=3.14
17
+ Requires-Dist: aiohttp
18
+ Requires-Dist: led-ticker-core>=2.0
19
+ Requires-Dist: pillow
20
+ Provides-Extra: dev
21
+ Requires-Dist: pre-commit>=4.0; extra == 'dev'
22
+ Requires-Dist: pyright>=1.1; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
24
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
25
+ Requires-Dist: pytest>=8.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.4; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # led-ticker-baseball
30
+
31
+ MLB scores, standings, promotions, Statcast, and attendance widgets, a rolling-baseball sprite transition, and a `:baseball.ball:` emoji for [led-ticker](https://github.com/JamesAwesome/led-ticker). Live game data comes from MLB's free StatsAPI — no API key required.
32
+
33
+ ## Screenshots
34
+
35
+ ![baseball.scores — live game scores with team abbreviations in brand colors and win/loss coloring](docs/scores.gif)
36
+
37
+ ![baseball.standings — top-N plus tracked teams, each name in its brand color](docs/standings.gif)
38
+
39
+ ![baseball.roll — rolling-baseball sprite transition between widgets](docs/roll-transition.gif)
40
+
41
+ ![baseball.promotions — upcoming home-game giveaways and theme nights, highlighted promos in amber](docs/promotions.gif)
42
+
43
+ ![baseball.statcast — league-wide daily superlatives (longest HR, hardest hit, fastest/slowest pitch)](docs/statcast.gif)
44
+
45
+ ![baseball.attendance — ballpark crowds and conditions; league superlatives or one team's game](docs/attendance.gif)
46
+
47
+ The `:baseball.ball:` emoji (8×8 and the 32×32 hi-res upgrade on bigsign):
48
+
49
+ ![baseball.ball emoji](docs/ball-emoji.png) ![baseball.ball hi-res](docs/ball-emoji-hires.png)
50
+
51
+ ## Prerequisites
52
+
53
+ - A working [led-ticker](https://github.com/JamesAwesome/led-ticker) install.
54
+ - Internet access on the Pi (the widgets call MLB's free StatsAPI; no API key needed).
55
+
56
+ ## Install
57
+
58
+ This plugin auto-registers via the `led_ticker.plugins` entry point — once the package is installed, no `[plugins]` config change is needed.
59
+
60
+ **Into a containerized led-ticker (recommended):** the plugin is already listed in `config/requirements-plugins.example.txt`. Copy that to the live file and rebuild:
61
+
62
+ ```bash
63
+ # in your led-ticker checkout
64
+ cp config/requirements-plugins.example.txt config/requirements-plugins.txt
65
+ docker compose up -d --build
66
+ ```
67
+
68
+ That example file lists every first-party plugin — trim the live copy to just the ones you want. The baseball line is:
69
+
70
+ ```text
71
+ git+https://github.com/JamesAwesome/led-ticker-baseball.git@main
72
+ ```
73
+
74
+ **Standalone (a venv that already has led-ticker):**
75
+
76
+ ```bash
77
+ pip install "git+https://github.com/JamesAwesome/led-ticker-baseball.git@main"
78
+ ```
79
+
80
+ led-ticker isn't on PyPI, so this path only works where led-ticker is already installed. See the led-ticker [Plugins docs](https://docs.ledticker.dev/plugins/) for the constraint-based install the Docker image uses.
81
+
82
+ Once installed, the `baseball.scores` / `baseball.standings` / `baseball.promotions` / `baseball.statcast` / `baseball.attendance` widgets, the `baseball.roll*` transitions, and the `:baseball.ball:` emoji are available automatically.
83
+
84
+ ## Widgets
85
+
86
+ Each widget below is a `[[playlist.section.widget]]` block you add inside a playlist section of your `config/config.toml`. New to led-ticker configs? The [first-config tutorial](https://docs.ledticker.dev/tutorial/02-first-config/) walks through the overall structure — the blocks here show just the baseball-specific keys.
87
+
88
+ ### `baseball.scores`
89
+
90
+ Fetches live game state for a tracked team and renders its current series. Three layouts:
91
+
92
+ - **`layout = "ticker"` (default)** — a scrolling line. Pre-game `NYY @ BOS Today 7:05 PM`; live `NYY 3 BOS 5 ▲6 ◇◆◇ 1·2·1` (score + inning + bases + balls·strikes·outs in color); final `NYY 4 BOS 5 (Final)` (win green, loss red); postponed `NYY @ BOS (PPD: Rain)`. Spring Training / All-Star games append `(ST)` / `(ASG)`.
93
+ - **`layout = "scoreboard"`** — a two-column board for bigsign/longboi: away name+score left, home name+score right, center zone shows inning+outs (top) and B/S count + base diamonds (bottom). Names in brand colors; scores green/red on final; base diamonds yellow (occupied) / dim grey (empty). ABS-challenge dashes appear in the bottom corners when active.
94
+ - **`layout = "two_row"`** — a held top band (series title) over a scrolling bottom band (the per-game line). Use the `top_*` font options below to size the top band; sized for bigsign.
95
+
96
+ ```toml
97
+ [[playlist.section.widget]]
98
+ type = "baseball.scores"
99
+ team = "NYY"
100
+ timezone = "America/New_York" # set to your local timezone
101
+ ```
102
+
103
+ **`team` is the only required field** — everything below is optional tuning.
104
+
105
+ | Option | Type | Default | Description |
106
+ |--------|------|---------|-------------|
107
+ | `team` | string | required | MLB team abbreviation, 2–3 letters (e.g. `"NYY"`, `"KC"`, `"SD"`) — see [Team codes](#team-codes). Case-insensitive. |
108
+ | `layout` | string | `"ticker"` | `"ticker"`, `"scoreboard"`, or `"two_row"`. |
109
+ | `timezone` | string | `"America/New_York"` | IANA timezone for game-time formatting. |
110
+ | `padding` | int | `6` | Horizontal padding (logical px) after each message when scrolling (ticker). |
111
+ | `final_hold_hours` | int | `6` | Hours after a game ends to keep showing the final score. |
112
+ | `bg_color` | RGB list | none | Background fill behind all game messages. |
113
+ | `font_color` | RGB list / string / table | unset | Override all text color; default keeps per-segment brand/win-loss colors. |
114
+ | `font` | string | `"6x12"` | Font for names and scores. Hires name (e.g. `"Inter-Regular"`) needs `font_size`. |
115
+ | `font_size` | int | none | Point size; required for a hires (TTF/OTF) `font`. |
116
+ | `font_threshold` | int | `128` | Hires anti-alias threshold (0–255); `80` suits Inter Regular. |
117
+ | `small_font` | string | same as `font` | Center-zone font (scoreboard layout). |
118
+ | `small_font_size` | int | none | Point size for `small_font`. |
119
+ | `small_font_threshold` | int | same as `font_threshold` | Anti-alias threshold for `small_font`. |
120
+ | `top_font` | string | same as `font` | Top-band font (`two_row` layout only). |
121
+ | `top_font_size` | int | none | Point size for `top_font` (`two_row` only). |
122
+ | `top_font_threshold` | int | same as `font_threshold` | Anti-alias threshold for `top_font` (`two_row` only). |
123
+ | `top_row_height` | int | half the canvas | Height (logical px) of the held top band (`two_row` only). |
124
+ | `update_interval` | int | `300` | Seconds between StatsAPI fetches. |
125
+
126
+ > `top_*` options apply only with `layout = "two_row"` — the widget rejects them at config-load under other layouts.
127
+
128
+ ### `baseball.standings`
129
+
130
+ Fetches overall MLB standings and scrolls them as `rank. TeamName W-L GB`, each name in its brand color. Shows the top-N plus any tracked `teams` not already in that list. Offseason-aware: before the season starts it shows `Opens Mar 27`; between the World Series and Spring Training it keeps the prior final standings.
131
+
132
+ ```toml
133
+ [[playlist.section.widget]]
134
+ type = "baseball.standings"
135
+ teams = ["NYY", "BOS"]
136
+ ```
137
+
138
+ **`teams` is the only required field** — everything below is optional.
139
+
140
+ | Option | Type | Default | Description |
141
+ |--------|------|---------|-------------|
142
+ | `teams` | list of strings | required | Tracked team abbreviations (e.g. `["NYY", "BOS"]`); always shown even outside top-N. |
143
+ | `top_n` | int | `3` | Overall top teams to show before tracked teams. `0` = tracked only. |
144
+ | `title` | string | `"MLB Standings"` | Section header before the list. |
145
+ | `timezone` | string | `"America/New_York"` | IANA timezone for offseason detection / opening-day date. |
146
+ | `padding` | int | `6` | Horizontal padding (logical px) after each message. |
147
+ | `bg_color` | RGB list | none | Background fill behind the standings. |
148
+ | `font_color` | RGB list / string / table | unset | Override all text color; default keeps rank white + team brand colors. |
149
+ | `font` | string | `"6x12"` | BDF or hires font for standings text. |
150
+ | `update_interval` | int | `86400` | Seconds between fetches (24 h default; standings move slowly). |
151
+
152
+ ### `baseball.promotions`
153
+
154
+ Upcoming home-game promotions — giveaways and theme nights, e.g. the Blue Jays'
155
+ Loonie Dogs Night — for a tracked team, from the schedule API's promotions feed.
156
+ Shows today's promos when there's a home game today, otherwise the next home
157
+ game's, one scrolling line per promo led by the team abbreviation in its brand
158
+ color, with a grey date prefix: `TOR Jun 22 · Retro Domer Hat Giveaway`. Sponsor tails ("presented by …") are
159
+ stripped, and near-duplicate feed entries are collapsed. Promos matching
160
+ `highlight` render in amber and sort first.
161
+
162
+ ```toml
163
+ [[playlist.section.widget]]
164
+ type = "baseball.promotions"
165
+ team = "TOR"
166
+ highlight = ["Loonie Dogs"]
167
+ ```
168
+
169
+ **`team` is the only required field** — everything below is optional tuning.
170
+
171
+ | Option | Type | Default | Description |
172
+ |--------|------|---------|-------------|
173
+ | `team` | string | required | MLB team abbreviation — see [Team codes](#team-codes). Case-insensitive. |
174
+ | `highlight` | list of strings | `[]` | Case-insensitive substrings; matching promos render amber and sort first. |
175
+ | `filter` | list of strings | `[]` | If non-empty, only promos matching one of these substrings are shown. |
176
+ | `limit` | int | `0` | Max promo lines (`0` = all). Applied after highlight sorting, so highlighted promos are never the ones dropped. |
177
+ | `lookahead_days` | int | `14` | How far ahead to look for the next home game with promotions. |
178
+ | `update_interval` | int | `21600` | Seconds between refreshes (6 h — keeps the "Today" label honest after midnight). |
179
+ | `title` | string | `"<Team> Promos"` | Section title override. |
180
+ | `timezone` | string | `"America/New_York"` | IANA timezone governing "Today" and date labels. |
181
+ | `padding` | int | `6` | Horizontal padding (logical px) after each message when scrolling. |
182
+ | `bg_color` | RGB list | none | Background fill behind all messages. |
183
+ | `font_color` | RGB list / string / table | unset | RGB list tints the promo names; the team prefix, date label, and amber highlights keep their callout colors. A string/table provider overrides all text, as in the other widgets. |
184
+ | `font` | string | `"6x12"` | Display font. Hires name needs `font_size`. |
185
+
186
+ With nothing to show, the widget falls back to a team-prefixed
187
+ `Next home game: Jun 22` (promo-free homestand), `No home games soon`
188
+ (road trip), or `Opens <date>` / `Opens soon` (offseason).
189
+
190
+ ### `baseball.statcast`
191
+
192
+ League-wide daily Statcast superlatives — the longest home run, hardest-hit
193
+ ball, and fastest/slowest pitch across all of MLB — or, with a `team` set, the
194
+ same superlatives scoped to that team's own players.
195
+ Re-derived through the day as games progress. One scrolling line per stat with
196
+ the value in amber and the record holder's team abbreviation in its brand color:
197
+ `Today · Longest HR 463 ft — Butler OAK`. Mornings fall back to yesterday's
198
+ finals, labeled with the short date (`6/12 · …`). Data comes from Baseball
199
+ Savant's day CSV (an
200
+ undocumented endpoint — the widget refreshes at a polite default cadence and
201
+ skips the pull entirely when no games are live or newly final).
202
+
203
+ ```toml
204
+ [[playlist.section.widget]]
205
+ type = "baseball.statcast"
206
+ ```
207
+
208
+ **No required fields** — everything below is optional tuning.
209
+
210
+ | Option | Type | Default | Description |
211
+ |--------|------|---------|-------------|
212
+ | `team` | string | unset | Scope superlatives to this team's own players (e.g. a Phillies batter for `longest_hr`, a Phillies pitcher for `fastest_pitch`). Omit for league-wide. Case-insensitive — see [Team codes](#team-codes). |
213
+ | `stats` | list of strings | all four | Which lines to show, in display order: `"longest_hr"`, `"hardest_hit"`, `"fastest_pitch"`, `"slowest_pitch"`. |
214
+ | `update_interval` | int | `1800` | Seconds between refreshes (30 min). A ~10 KB schedule check skips the ~3 MB data pull when nothing changed. |
215
+ | `title` | string | `"Statcast"` | Section title override. |
216
+ | `timezone` | string | `"America/New_York"` | IANA timezone governing "Today" and the day rollover. |
217
+ | `padding` | int | `6` | Horizontal padding (logical px) after each message when scrolling. |
218
+ | `bg_color` | RGB list | none | Background fill behind all messages. |
219
+ | `font_color` | RGB list / string / table | unset | RGB list tints the stat label and name; the day label, amber value, and team abbr keep their callout colors. A string/table provider overrides all text, as in the other widgets. |
220
+ | `font` | string | `"6x12"` | Display font. Hires name needs `font_size`. |
221
+
222
+ The slowest-pitch line appends the pitch name when known (`69.6 mph (Slow
223
+ Curve)`) — that's where the eephus and position-player pitching comedy lives.
224
+ With no Statcast data for today or yesterday, the widget falls back to
225
+ `Next games: Mar 26` (offseason) or `No games soon`; a fetch failure shows
226
+ `No Data`.
227
+
228
+ With a team set, lines lead with the team abbreviation in its brand color and
229
+ drop the (now-redundant) trailing one:
230
+ `PHI Today · Longest HR 472 ft — Schwarber`. The off-day fallback then names
231
+ the team's next game (`Next game: Jun 20`) rather than the league slate.
232
+
233
+ ### `baseball.attendance`
234
+
235
+ Ballpark attendance and conditions. Two modes, chosen by whether you set a
236
+ `team`:
237
+
238
+ - **League-wide** (no `team`): the day's attendance superlatives —
239
+ `Today · Biggest crowd 45,123 — Dodger Stadium`, plus smallest crowd and
240
+ fullest/emptiest park by capacity %. Venue name in the home team's brand
241
+ color.
242
+ - **Team** (`team` set): that team's game —
243
+ `TOR · Rogers Centre 41,212 (90%) · 72° Clear, wind 5 mph, In From CF`.
244
+ Attendance and fill % appear once the game is final; venue and weather show
245
+ before that.
246
+
247
+ ```toml
248
+ [[playlist.section.widget]]
249
+ type = "baseball.attendance"
250
+ # team = "TOR" # set for team mode; omit for league-wide
251
+ ```
252
+
253
+ **No required fields** — everything is optional tuning.
254
+
255
+ | Option | Type | Default | Description |
256
+ |--------|------|---------|-------------|
257
+ | `team` | string | unset | Set → that team's game; omit → league-wide superlatives. |
258
+ | `stats` | list of strings | all four | League mode only, in display order: `"biggest_crowd"`, `"smallest_crowd"`, `"fullest"`, `"emptiest"`. |
259
+ | `update_interval` | int | `1800` | Seconds between refreshes (30 min). A ~47 KB schedule check skips the per-game fetches when nothing changed. |
260
+ | `title` | string | `"Attendance"` | Section title override. |
261
+ | `timezone` | string | `"America/New_York"` | IANA timezone for "Today" / day rollover. |
262
+ | `padding` | int | `6` | Horizontal padding (logical px) after each message. |
263
+ | `bg_color` | RGB list | none | Background fill behind all messages. |
264
+ | `font_color` | RGB list / string / table | unset | RGB list tints body text; the day label, amber value, and venue/team color keep their callout colors. A string/table provider overrides all text. |
265
+ | `font` | string | `"6x12"` | Display font. Hires name needs `font_size`. |
266
+
267
+ Fill % is omitted when a venue lists no capacity (spring sites). With nothing
268
+ final yet, the widget shows yesterday's data (short-date labeled, e.g.
269
+ `6/12 · …`); with no games at all it shows `Next game: Jun 20` (team) /
270
+ `Next games: Jun 20` (league); a fetch failure shows `No Data`.
271
+
272
+ ## Common patterns
273
+
274
+ Recipes that combine the widgets above. Each block shows just the
275
+ baseball-specific keys — drop the widgets into a playlist section of your
276
+ `config/config.toml` (see the [first-config tutorial](https://docs.ledticker.dev/tutorial/02-first-config/) for the surrounding structure).
277
+
278
+ ### My-team dashboard
279
+
280
+ One team across every widget, rotating with the rolling-baseball transition.
281
+
282
+ ```toml
283
+ [[playlist.section]]
284
+ mode = "swap"
285
+ transition = "baseball.roll_alternating"
286
+ hold_time = 8
287
+
288
+ [[playlist.section.widget]]
289
+ type = "baseball.scores"
290
+ team = "TOR"
291
+
292
+ [[playlist.section.widget]]
293
+ type = "baseball.standings"
294
+ teams = ["TOR"]
295
+
296
+ [[playlist.section.widget]]
297
+ type = "baseball.promotions"
298
+ team = "TOR"
299
+
300
+ [[playlist.section.widget]]
301
+ type = "baseball.attendance"
302
+ team = "TOR"
303
+ ```
304
+
305
+ Shows your team's current series, its place in the standings, its next
306
+ home-game promotions, and the crowd and conditions at its game.
307
+
308
+ ### League roundup
309
+
310
+ League-wide daily superlatives — omit `team` and both widgets run in league
311
+ mode.
312
+
313
+ ```toml
314
+ [[playlist.section]]
315
+ mode = "swap"
316
+ hold_time = 8
317
+ scroll_step_ms = 35
318
+
319
+ [[playlist.section.widget]]
320
+ type = "baseball.statcast"
321
+
322
+ [[playlist.section.widget]]
323
+ type = "baseball.attendance"
324
+ ```
325
+
326
+ Shows the day's longest home run, hardest-hit ball, and fastest and slowest
327
+ pitch, then the biggest and smallest crowd and the fullest and emptiest park
328
+ across all of MLB.
329
+
330
+ ### Gameday ticker
331
+
332
+ A minimal single-team scrolling line.
333
+
334
+ ```toml
335
+ [[playlist.section]]
336
+ mode = "swap"
337
+ hold_time = 6
338
+
339
+ [[playlist.section.widget]]
340
+ type = "baseball.scores"
341
+ team = "NYY"
342
+ ```
343
+
344
+ Shows just the tracked team's live, final, or upcoming game line.
345
+
346
+ ### Shared knobs
347
+
348
+ - Every widget accepts the standard `title`, `font`, `font_color`, `bg_color`,
349
+ `padding`, and `timezone` options — see each widget's table above.
350
+ - Put `:baseball.ball:` in a `[playlist.section.title]` message for a themed
351
+ header.
352
+ - **Pacing** is tuned with `hold_time` (dwell before and after a line) and
353
+ `scroll_step_ms` (scroll cadence — lower is faster). These are
354
+ [led-ticker section settings](https://docs.ledticker.dev/), not plugin
355
+ options; they control how an overflowing line (common in the statcast,
356
+ attendance, and promotions widgets) reads on the panel.
357
+
358
+ ## Team codes
359
+
360
+ All 30 teams (used by the scores, standings, promotions, statcast, and attendance widgets):
361
+
362
+ `ARI` D-backs · `ATL` Braves · `BAL` Orioles · `BOS` Red Sox · `CHC` Cubs · `CIN` Reds · `CLE` Guardians · `COL` Rockies · `CWS` White Sox · `DET` Tigers · `HOU` Astros · `KC` Royals · `LAA` Angels · `LAD` Dodgers · `MIA` Marlins · `MIL` Brewers · `MIN` Twins · `NYM` Mets · `NYY` Yankees · `OAK` Athletics · `PHI` Phillies · `PIT` Pirates · `SD` Padres · `SEA` Mariners · `SF` Giants · `STL` Cardinals · `TB` Rays · `TEX` Rangers · `TOR` Blue Jays · `WSH` Nationals
363
+
364
+ ## Transition
365
+
366
+ A rolling-baseball sprite transition, registered in three directions:
367
+
368
+ ```toml
369
+ transition = "baseball.roll" # left-to-right
370
+ # transition = "baseball.roll_reverse" # right-to-left
371
+ # transition = "baseball.roll_alternating" # alternates each use
372
+ ```
373
+
374
+ On a bigsign panel (`default_scale > 1`) the transition automatically renders a hi-res procedurally-rotated ball; on a smallsign it uses the 8-frame lo-res sprite.
375
+
376
+ ## Emoji
377
+
378
+ `:baseball.ball:` — a white ball with red stitching. Use it inline in any text-bearing widget:
379
+
380
+ ```toml
381
+ [[playlist.section.widget]]
382
+ type = "message"
383
+ text = ":baseball.ball: Play ball!"
384
+ ```
385
+
386
+ It renders as an 8×8 sprite, auto-upgrading to a 32×32 hi-res sprite on bigsign.
387
+
388
+ ## Development
389
+
390
+ led-ticker isn't on PyPI, so this plugin resolves it from a sibling checkout. Clone both side by side:
391
+
392
+ ```
393
+ ~/projects/.../led-ticker
394
+ ~/projects/.../led-ticker-baseball
395
+ ```
396
+
397
+ ```bash
398
+ uv sync --extra dev # resolves led-ticker from ../led-ticker
399
+ uv run pytest -q
400
+ uv run ruff check src tests
401
+ ```
402
+
403
+ The plugin imports only the public `led_ticker.plugin` surface — `tests/test_import_purity.py` enforces it.
404
+
405
+ ## Links
406
+
407
+ - [led-ticker](https://github.com/JamesAwesome/led-ticker) — the core project
408
+ - [Docs site](https://docs.ledticker.dev) · [Plugin system](https://docs.ledticker.dev/plugins/)