Files
LEDMatrix/src/common
ChuckandClaude Opus 5 81e1bc596f fix(sports): stop the idle back-off sleeping through a kickoff (#599)
* fix(sports): stop the idle back-off sleeping through a kickoff

A league with no live games backs its poll off as empty checks mount,
capped by live_idle_max_interval. The escalation counts empty looks and
nothing else, so a league three hours before kickoff is indistinguishable
from one three months out of season. Both reach the ceiling -- and the
ceiling then *is* the blind spot.

Measured on two rigs on 2026-09-19: gaps of up to 928s between looks, ten
of them at or above 900s. Reproduced in the wild on 2026-09-20, where an
unpatched rig sat for fifteen minutes with eight NFL games in progress and
had not noticed any of them. That is the "it doesn't pick up new live
games until I restart it" report -- restarting being the one thing that
forces an immediate look.

The clamp costs no extra request: the live fetch already downloads the
whole day's scoreboard, upcoming games included, so the earliest start
still ahead of us falls out of the payload the manager already has.
Before a kickoff the wait is shortened so it cannot run past it; just
after one, the live cadence is held for _KICKOFF_GRACE_SECONDS, because a
provider that has not yet flipped the status would otherwise look like
another empty check and escalate the back-off again, right when the game
is starting.

The grace window needed a second pass. A soak caught it as dead code: the
just-passed kickoff was replaced by the next fixture on the card the
instant it passed, `now < start` went true again, and the back-off
returned to its ceiling. Observed live -- the rig polled at 13:00:45,
found nothing because ESPN had not flipped the status, then went quiet for
a quarter of an hour. A kickoff inside the grace window is now kept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

* test(sports): pin absolute tolerances and correct a wrong grace expectation

pytest.approx defaults to a relative tolerance. On a unix timestamp that is
roughly 1790 seconds, so every kickoff assertion here was effectively
vacuous -- it called a kickoff half an hour away "equal". All seven now
pin abs=1.

That hid a wrong expectation. test_an_earlier_kickoff_still_wins_during_the_grace
asserted a game ten minutes out should displace one that kicked off moments
ago. It should not, and the code does not: while the grace holds, the wait
is the live cadence (30s), which is strictly tighter than clamping to the
nearer kickoff would give (~600s). Letting the candidate win would set a
ten-minute wait at the exact moment games are starting -- the dead grace
window this branch exists to fix.

The test now pins the real behaviour plus the safety property that makes it
correct, and is renamed to say what it checks.

Reported by CodeRabbit on the PR. The finding was right that code and test
disagreed; the suggested fix was the wrong way to resolve it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 18:25:33 -04:00
..
2025-12-27 14:15:49 -05:00
2025-12-27 14:15:49 -05:00

Common Utilities

This directory contains reusable utilities and helpers for LEDMatrix plugins and core modules.

Adaptive Layout & Images (src/adaptive_layout.py, src/adaptive_images.py)

The recommended way to lay out plugins that render legibly on any panel size (64x32 through 256x128+) without hand-tuned coordinates. Re-exported from src.common for convenience; canonical import paths are src.adaptive_layout / src.adaptive_images.

# Every BasePlugin already has self.layout and the draw helpers:
regs = scoreboard_regions(self.layout.bounds, ctx=self.layout)
self.draw_image(away_logo, regs.away_slot, mode="fill_height",
                crop_to_ink=True, cache_key=f"logo:{abbr}")
self.draw_fit(score_text, regs.score_area)     # largest crisp font that fits
self.draw_fit(status, regs.status_band)

Key pieces: Region (rect algebra: bands/columns/splits/offset), font ladders (LADDER_GRID, LADDER_ARCADE — discrete crisp sizes, never fractional scaling), LayoutContext (fit_text, fit_image, by_tier, px), and composite carvers scoreboard_regions() / media_row(). Full guide: docs/ADAPTIVE_LAYOUT.md.

Error Handling (error_handler.py)

Common error handling patterns and utilities:

  • handle_file_operation() - Handle file I/O with consistent error handling
  • handle_json_operation() - Handle JSON operations with error handling
  • safe_execute() - Safely execute operations with error handling
  • retry_on_failure() - Decorator for retrying failed operations
  • log_and_continue() - Log non-critical errors and continue
  • log_and_raise() - Log errors and raise exceptions

Example Usage

from src.common.error_handler import handle_json_operation, safe_execute

# Handle JSON loading
config = handle_json_operation(
    lambda: json.load(open('config.json')),
    "Failed to load config",
    logger,
    default={}
)

# Safe execution with error handling
result = safe_execute(
    lambda: risky_operation(),
    "Operation failed",
    logger,
    default=None
)

API Helpers (api_helper.py)

Utilities for making HTTP requests and handling API responses.

Configuration Helpers (config_helper.py)

Utilities for loading, saving, and validating configuration files.

Display Helpers (display_helper.py)

Utilities for rendering content to the LED matrix display.

Game Helpers (game_helper.py)

Utilities for processing game data and team information.

Logo Helpers (logo_helper.py)

Utilities for loading and managing team logos.

Text Helpers (text_helper.py)

Utilities for text processing and formatting.

Scroll Helpers (scroll_helper.py)

Utilities for scrolling text on the display.

General Utilities (utils.py)

General-purpose utility functions:

  • Team abbreviation normalization
  • Time formatting
  • Boolean parsing
  • Logger creation (deprecated - use src.logging_config.get_logger())

Permission Utilities (permission_utils.py)

Helpers for ensuring directory permissions and ownership are correct when running as a service (used by CacheManager to set up its persistent cache directory).

Best Practices

  1. Use centralized logging: Import from src.logging_config instead of creating loggers directly
  2. Use error handlers: Use error_handler utilities for consistent error handling
  3. Reuse utilities: Check existing utilities before creating new ones
  4. Document additions: Add documentation when adding new utilities