display.vegas_scroll.live_in_ticker now defaults to true: through a live game the marquee keeps running and the live scoreboard takes extra turns in it -- its cards updating in place while they scroll -- instead of the ticker giving way to the full-screen scoreboard. The new default would reach nobody on its own: every existing config holds an explicit false copied from the template (there was no control for it), and the template merge only adds missing keys. ConfigManager therefore turns a stored false on once, with a backup, and records live_in_ticker_migrated so a false chosen afterwards stays. The marker is never in the template. A "Keep live games in the ticker" checkbox under Vegas mode sets it. Tests that pin the full-screen takeover now say live_in_ticker=false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 KiB
Plugin API Reference
Complete API reference for plugin developers. This document describes all methods and properties available to plugins through the Display Manager, Cache Manager, and Plugin Manager.
Adaptive layout: every
BasePluginalso exposesself.layout,self.draw_fit(text, region)andself.draw_image(img, region, ...)— the recommended way to render text and images that scale to any panel size. See ADAPTIVE_LAYOUT.md.
Table of Contents
Manifest Required Fields
Three parts of core check manifest.json, each for a different set of
fields:
| Check | Fields | What happens when one is missing |
|---|---|---|
JSON schema, schema/manifest_schema.json |
id, name, version, author, entry_point, class_name, compatible_versions |
Install from URL logs a warning (PluginStoreManager._validate_manifest_schema()); nothing is refused |
Plugin Store install, src/plugin_system/store_install.py |
id, name, class_name, display_modes |
Install is refused. A registry install first tries to detect a missing class_name from the entry-point file |
Plugin loader, src/plugin_system/plugin_loader.py |
class_name |
The plugin fails to load |
Defaults and other uses:
entry_pointdefaults tomanager.py; the store writes the default back into the manifest on install.compatible_versions(a list of semver ranges such as">=2.0.0") is how the store decides whether a plugin can run on this core. An install is refused only when the field excludes the running version (compatibility.check()insrc/plugin_system/compatibility.py).versionis compared with the registry'slatest_versionto decide whether an update is available.- If
display_modesis empty at load time, the display controller uses the plugin id as the only mode.
Set all eight: id, name, version, author, entry_point,
class_name, display_modes, compatible_versions. That satisfies every
check. The schema lists the optional fields.
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"author": "YourName",
"entry_point": "manager.py",
"class_name": "MyPlugin",
"display_modes": ["my-plugin"],
"compatible_versions": [">=2.0.0"]
}
BasePlugin
All plugins must inherit from BasePlugin and implement the required methods. The base class provides access to managers and common functionality.
Available Properties
self.plugin_id # Plugin identifier (string)
self.config # Plugin configuration dictionary
self.display_manager # DisplayManager instance
self.cache_manager # CacheManager instance
self.plugin_manager # PluginManager instance
self.logger # Plugin-specific logger
self.enabled # Boolean enabled status
Required Methods
update() -> None
Fetch/update data for this plugin. Called on the plugin's update interval:
the value get_update_interval() returns when it returns a number, otherwise
the static interval: the update_interval in the plugin's manifest, else
update_interval in the plugin's section of config.json, else 60 seconds
(see get_update_interval() below).
Example:
def update(self):
cache_key = f"{self.plugin_id}_data"
cached = self.cache_manager.get(cache_key, max_age=3600)
if cached:
self.data = cached
return
self.data = self._fetch_from_api()
self.cache_manager.set(cache_key, self.data)
display(force_clear: bool = False) -> None
Render this plugin's display. Called during display rotation or when explicitly requested.
Parameters:
force_clear(bool): If True, clear display before rendering
Example:
def display(self, force_clear=False):
if force_clear:
self.display_manager.clear()
self.display_manager.draw_text(
"Hello, World!",
x=5, y=15,
color=(255, 255, 255)
)
self.display_manager.update_display()
Optional Methods
validate_config() -> bool
Validate plugin configuration. Override to implement custom validation.
Returns: True if config is valid, False otherwise
has_live_content() -> bool
Check if plugin currently has live content. Override for live priority plugins.
Returns: True if plugin has live content
get_live_modes() -> List[str]
Get list of display modes to show during live priority takeover.
Returns: List of mode names
cleanup() -> None
Clean up resources when plugin is unloaded. Override to close connections, stop threads, etc.
on_config_change(new_config: Dict[str, Any]) -> None
Called after the plugin's section of config.json changes -- a save in the
web UI, say. Every lifecycle hook runs in the display process, which is the
only process that runs plugins: the web interface writes config.json, and
the display's config watcher calls this with the prepared section. See
ARCHITECTURE.md.
In the display service it runs on the config watcher thread while holding
the plugin's lock, so it never overlaps your update() or display(). If
the plugin stays busy for more than 5 seconds, the change is applied later
from the update thread: as soon as the plugin is free, and before its next
update() at the latest.
on_enable() -> None
Called when the display loads the plugin enabled: at startup, or when it is switched on in the web UI.
on_disable() -> None
Called when the display unloads the plugin, e.g. when it is switched off in the web UI.
get_update_interval() -> Optional[float]
How often this plugin wants update() called right now, in seconds. The
manifest's update_interval is one static number; override this when the
right cadence depends on state only the plugin knows, e.g. poll every 15s
while a game is live and fall back to the manifest value otherwise.
Returns: seconds as a number, or None (the default) for no opinion.
How PluginManager (_get_plugin_update_interval in
src/plugin_system/plugin_manager.py) resolves the interval on each
scheduling tick:
- It calls
get_update_interval(). A number wins over everything below. Values underPluginManager.MIN_DYNAMIC_UPDATE_INTERVAL(5 seconds) are raised to it. - If the hook returns
None, raises, or returns something that isn't a finite number (abool, a string, NaN, infinity), it is ignored and the static interval applies: the manifest'supdate_interval, elseupdate_intervalin the plugin's section ofconfig.json, else 60 seconds.
The static value is cached per plugin until the plugin is loaded or
unloaded again, so editing update_interval in config takes effect on the
next reload. The hook's return value is never cached: it is called on every
tick of the display loop, so keep it to attribute reads (no config lookups,
no I/O, no locks a fetch might hold) and don't let it raise.
Example:
def get_update_interval(self):
# Fast while something is live, manifest default otherwise.
if any(m.live_games for m in self._live_managers):
return self.config.get("live_update_interval", 15)
return None
Added in core 3.4.0; older cores never call it, so a plugin that relies on
it should floor ledmatrix_min_version at 3.4.0.
get_display_duration() -> float
Get display duration for this plugin. Can be overridden for dynamic durations.
Returns: Duration in seconds
get_info() -> Dict[str, Any]
Return plugin info for display in web UI. Override to provide additional state information.
Dynamic-duration hooks
Plugins that render multi-step content (e.g. cycling through several games)
can extend their display time until they've shown everything. To opt in,
either set dynamic_duration.enabled: true in the plugin's config or
override supports_dynamic_duration().
supports_dynamic_duration() -> bool
Return True if this plugin should use dynamic durations. Default reads
config["dynamic_duration"]["enabled"].
get_dynamic_duration_cap() -> Optional[float]
Maximum number of seconds the controller will keep this plugin on screen
in dynamic mode. Default reads
config["dynamic_duration"]["max_duration_seconds"].
is_cycle_complete() -> bool
Override this to return True only after the plugin has rendered all of
its content for the current rotation. Default returns True immediately,
which means a single display() call counts as a full cycle.
reset_cycle_state() -> None
Called by the controller before each new dynamic-duration session. Reset internal counters/iterators here.
Live priority hooks
Live priority lets a plugin temporarily take over the rotation when it has
urgent content (live games, breaking news). Enable by setting
live_priority: true in the plugin's config and overriding
has_live_content().
has_live_priority() -> bool
Whether live priority is enabled in config (default reads
config["live_priority"]).
has_live_content() -> bool
Override to return True when the plugin currently has urgent content.
Default returns False.
get_live_modes() -> List[str]
List of display modes to show during a live takeover. Default returns the
plugin's display_modes from its manifest.
get_vegas_priority_weight() -> Optional[int]
How many slots per Vegas cycle this plugin should get. Default returns
None, which defers to the core.
The Vegas ticker is otherwise a strict round robin — every plugin appears exactly once per cycle — so with a dozen plugins enabled a live score can be minutes stale by the time it comes round. A weight of N gives the plugin N slots per cycle, spread evenly through it rather than clumped.
You usually do not need this. When the hook returns None, the core
already gives a plugin vegas_scroll.live_weight whenever
has_live_priority() and has_live_content() are both true. Live sports get
extra turns with no code at all.
Implement it only when the plugin knows something the core cannot. The motivating case is favorite teams — the core can see that a game is live, but not whose:
def get_vegas_priority_weight(self):
if not (self.has_live_priority() and self.has_live_content()):
return None # let the core decide
vegas = self.global_config.get('display', {}).get('vegas_scroll', {})
if self._favorite_is_live():
return vegas.get('favorite_live_weight', 5)
return vegas.get('live_weight', 3)
The weight is per plugin, not per game: a scoreboard showing four live games
still occupies one slot at a time and rotates its own games within it. Values
are clamped to 1–10 by the caller. An exception here is caught and logged, and
the core then falls back to its own live-content check — so a plugin whose
weight calculation is broken still gets live_weight for a game that really
is live, rather than being demoted to 1.
Only consulted while vegas_scroll.live_in_ticker is on (the default since
3.8.0). With it off live content preempts Vegas entirely and there is no
ticker to be weighted within. See
ADVANCED_FEATURES.md.
Vegas scroll hooks
Vegas mode shows multiple plugins as a single continuous scroll instead of rotating one at a time. Plugins control how their content appears via these hooks. See ADVANCED_FEATURES.md for the user side of Vegas mode.
Vegas participation
Each plugin takes part in Vegas mode in one of three ways:
| Participation | What Vegas does |
|---|---|
'scroll' |
The plugin's content (get_vegas_content()) scrolls by with everything else |
'pause' |
The scroll stops when the plugin's turn comes round; its display() draws it full screen for get_display_duration() seconds, then the scroll resumes |
'exclude' |
The plugin is left out of Vegas mode |
Declare the plugin's default in manifest.json:
{
"id": "my-alerts",
"vegas_participation": "pause"
}
The user can override it per plugin with vegas_participation in that
plugin's config section (it is one of the core-owned properties, see
PLUGIN_CONFIG_CORE_PROPERTIES.md).
Vegas resolves it in this order:
- the user's
vegas_participationconfig value; - the plugin's
get_vegas_participation()— the default implementation reads the manifest'svegas_participation, then derives a value from the legacy hooks below; - derived from the legacy hooks:
get_vegas_display_mode()returningVegasDisplayMode.STATIC→'pause'; otherwiseget_vegas_content_type()returning'none'→'exclude'; everything else →'scroll'.
Step 3 is exactly what Vegas did before participation existed, so a plugin
that declares nothing behaves as it always has. Manifest
vegas_participation is new in core 3.8.0; older cores ignore it and use
the legacy hooks.
get_vegas_participation() -> str
Returns 'scroll', 'pause' or 'exclude'. Override it only when the
answer depends on state — pause only while an alert is live, exclude while
there is nothing to show; for a fixed answer use the manifest. Vegas applies
the user's config value before calling an override, so an override does not
need to check it. A value that is not one of the three is ignored with a log
line and the legacy hooks decide.
def get_vegas_participation(self):
return 'pause' if self._alert_is_live() else 'scroll'
get_vegas_content() -> Optional[PIL.Image | List[PIL.Image] | None]
Return content to inject into the scroll. Multi-item plugins (sports,
odds, news) should return a list of PIL Images so each item scrolls
independently. Static plugins (clock, weather) can return a single image.
Returning None falls back to capturing whatever display() produces.
get_vegas_render_width() -> int
The width Vegas wants this plugin's content to occupy, from the plugin's
vegas_width_pct config value or the global
display.vegas_scroll.render_width_pct. Vegas also narrows
display_manager while it asks for content, so a plugin that sizes itself
from display_manager.width does not need to read this.
Live Vegas elements
New in core 3.8.0. Content from get_vegas_content() is baked into the
ticker's strip when the plugin's turn is prefetched, so a score drawn then
scrolls past with that score however many goals are scored while it crosses
the panel. A plugin that returns live elements instead gets them updated
in place: after its update() the ticker asks again, compares each element
with what the strip holds, and swaps the changed ones in between two frames
-- on screen included -- without anything next to them moving.
try:
from src.plugin_system.vegas_elements import VegasElement
except ImportError: # core older than 3.8.0: the hook is never called
VegasElement = None
class MyScoreboard(BasePlugin):
def get_vegas_elements(self):
if VegasElement is None:
return None
return [VegasElement(key=f"game:{g['id']}",
image=self._card(g), # cache by fingerprint
version=self._fingerprint(g)) # changes iff pixels would
for g in self.games]
VegasElement(key, image, version=None, live=True, refresh_hz=0.0):
| Field | Meaning |
|---|---|
key |
Names the element across redraws; unique in the list, stable for the same logical item ("game:nfl:401547417", "map"). |
image |
The element now, at the display's height. A live element's width must not depend on its data: a redraw at another width is never swapped in (it appears the next time the plugin comes round), because nothing on screen may move. |
version |
Anything hashable that changes exactly when the pixels would. Handed back with the same image object as last time, it lets the ticker skip converting the element; a new image is always converted and compared by its pixels, so a redraw for new settings is never missed. None means "compare pixels". |
live |
False places it as plain content (trimmed, never refreshed): separators, decoration. |
refresh_hz |
For content that changes with time rather than data (an aircraft moving between position reports): the ticker calls redraw_vegas_element() about this often while the element is on or near the screen, capped by vegas_scroll.live_max_hz and at 1 Hz without the rebuilt rgbmatrix binding. |
get_vegas_elements() -> Optional[List[VegasElement]] — called on the
ticker's background thread under the plugin's lock (never while update()
runs), on a canvas of its own and told its render width, exactly like
get_vegas_content(). It is called after every update() while any of the
plugin's elements is on or ahead of the screen, so it must be cheap when
nothing changed (cache images by version), idempotent, and must not fetch.
Return None to use get_vegas_content(), which a plugin must keep working
for older cores and for the paths that do not ask for elements (the ticker's
first strip, multi-display sync, the live_refresh switch).
redraw_vegas_element(key, width, height, at) -> Optional[PIL.Image] —
only for elements with refresh_hz. Called without the plugin's lock,
possibly while update() runs, so read only state update() replaces in one
assignment (an immutable snapshot), never state it mutates in place. at is
the time.monotonic() the pixels are expected on the panel: draw the element
as it should look then. Return exactly width x height, or None to skip
the tick.
notify_vegas_data_changed() — data that arrives outside update() (a
background thread, a push callback) calls this so the ticker redraws without
waiting for the next update(). Safe from any thread.
Live elements are never trimmed to their ink: the ticker pads each with
content_padding black columns either side, the margin trimming would have
left. A single element wider than the plugin's width budget
(vegas_max_width_screens, not counting that padding) is cropped like any
other content and scrolls by as plain, no longer live. The user can turn them off per plugin with vegas_live: false (a
core-owned property) or for the whole ticker with
display.vegas_scroll.live_refresh: false; they are always off under
multi-display sync.
scripts/check_plugin.py checks the contract for any plugin that implements
the hook (unique keys, height, width stable with no new data, redraw size,
slow calls) and prints a vegas elements row; the checks are in
src/plugin_system/testing/vegas.py. test/fixtures/plugins/vegas-live-stub
is a small working example.
Legacy: get_vegas_content_type() and get_vegas_display_mode()
Superseded by participation, and still read to derive it when neither the
user nor the manifest declares one (step 3 above). Only two answers ever
mattered: get_vegas_content_type() returning 'none', and
get_vegas_display_mode() returning VegasDisplayMode.STATIC.
get_vegas_content_type()returns'multi','static'or'none'(default'static').get_vegas_display_mode()returns aVegasDisplayModemember (not a string — the string'static'never paused anything). The default reads the plugin'svegas_modeconfig value ("scroll","fixed"or"static"), else maps content type'multi'toSCROLLand anything else toFIXED_SEGMENT.
SCROLL and FIXED_SEGMENT (and vegas_mode "scroll" and "fixed")
have always behaved identically: both scroll. The distinction is deprecated
and goes away in 3.9.0 — see Deprecated APIs.
Deprecated: get_supported_vegas_modes() and get_vegas_segment_width()
Never read by core, and removed in 3.9.0: calling the BasePlugin
implementation logs a deprecation warning. A plugin's own override keeps
working for the plugin itself. get_vegas_segment_width() read the
vegas_panel_count config value, which has never affected Vegas — a card's
width comes from get_vegas_content() and vegas_width_pct.
The full source for
BasePluginlives insrc/plugin_system/base_plugin.py. If a method here disagrees with the source, the source wins — please open an issue or PR to fix the doc.
Display Manager
The Display Manager handles all rendering operations on the LED matrix. Available as self.display_manager in plugins.
Properties
display_manager.width # Display width in pixels (int)
display_manager.height # Display height in pixels (int)
Core Methods
clear() -> None
Clear the display completely. Creates a new black image.
Note: Does not call update_display() automatically. Call update_display() after drawing new content.
Example:
self.display_manager.clear()
# Draw new content...
self.display_manager.update_display()
update_display() -> None
Update the physical display using double buffering. Call this after drawing all content.
Example:
self.display_manager.draw_text("Hello", x=10, y=10)
self.display_manager.update_display() # Actually show on display
Text Rendering
draw_text(text: str, x: int = None, y: int = None, color: tuple = (255, 255, 255), small_font: bool = False, font: ImageFont = None, centered: bool = False) -> None
Draw text on the canvas.
Parameters:
text(str): Text to displayx(int, optional): X position. IfNone, text is centered horizontally. Ifcentered=True, x is treated as center point.y(int, optional): Y position (default: 0, top of display)color(tuple): RGB color tuple (default: white)small_font(bool): Use small font if Truefont(ImageFont, optional): Custom font object (overrides small_font)centered(bool): If True, x is treated as center point; if False, x is left edge
Example:
# Centered text
self.display_manager.draw_text("Hello", color=(255, 255, 0))
# Left-aligned at specific position
self.display_manager.draw_text("World", x=10, y=20, color=(0, 255, 0))
# Centered at specific x position
self.display_manager.draw_text("Center", x=64, y=16, centered=True)
get_text_width(text: str, font) -> int
Get the width of text when rendered with the given font.
Parameters:
text(str): Text to measurefont: Font object (ImageFont or freetype.Face)
Returns: Width in pixels
Example:
width = self.display_manager.get_text_width("Hello", self.display_manager.regular_font)
x = (self.display_manager.width - width) // 2 # Center text
get_font_height(font) -> int
Get the height of the given font for line spacing purposes.
Parameters:
font: Font object (ImageFont or freetype.Face)
Returns: Height in pixels
Example:
font_height = self.display_manager.get_font_height(self.display_manager.regular_font)
y = 10 + font_height # Position next line
format_date_with_ordinal(dt: datetime) -> str
Format a datetime object into 'Mon Aug 30th' style with ordinal suffix.
Parameters:
dt: datetime object
Returns: Formatted date string
Example:
from datetime import datetime
date_str = self.display_manager.format_date_with_ordinal(datetime.now())
# Returns: "Jan 15th"
Image Rendering
The display manager doesn't provide a dedicated draw_image() method.
Instead, plugins paste directly onto the underlying PIL Image
(display_manager.image), then call update_display() to push the buffer
to the matrix.
from PIL import Image
logo = Image.open("assets/logo.png").convert("RGB")
self.display_manager.image.paste(logo, (10, 10))
self.display_manager.update_display()
For transparency support, paste using a mask:
icon = Image.open("assets/icon.png").convert("RGBA")
self.display_manager.image.paste(icon, (5, 5), icon)
self.display_manager.update_display()
This is the canonical way to render arbitrary images.
Weather Icons (deprecated)
Deprecated, removed in 3.8.0 — draw your own icons (the weather plugin ships
WeatherIcons). See Deprecated APIs.
draw_weather_icon(condition, x, y, size=16)— icon for a condition string such as"clear","clouds","rain","snow","storm"draw_sun(x, y, size=16),draw_cloud(x, y, size=16, color=(200, 200, 200)),draw_rain(x, y, size=16),draw_snow(x, y, size=16)draw_text_with_icons(text, icons=None, x=None, y=None, color=(255, 255, 255))— text plus a list of(icon_type, x, y)icons; callsupdate_display()
Scrolling State Management
For plugins that implement scrolling content, use these methods to coordinate with the display system.
set_scrolling_state(is_scrolling: bool, frame_hold: int = 1) -> None
Mark the display as scrolling or not scrolling, and set this scroll's frame
pacing. Call it when a scroll starts (calling it on every scroll frame is fine)
and with False when it stops.
Parameters:
is_scrolling(bool): True if currently scrolling, False otherwiseframe_hold(int, default 1): how many panel refreshes each pushed frame is held for (clamped to 1-255; ignored whenis_scrollingis False, which resets it to 1). Pass theframe_holdof the settingssrc.common.scroll_config.configure()returned. Added in core 3.4.0.
Why frame_hold matters: scroll_config.configure() snaps the speed to
one the panel can show in whole pixels and sets the ScrollHelper to advance a
fixed number of pixels on every presented frame -- no clock is consulted. The
panel presents frames at its refresh rate divided by the hold, so the hold is
part of the speed. Omit it and a 50 px/s scroll (1px every 2nd refresh on a
100 Hz panel) runs at 100 px/s. The hold is not applied by configure()
because it must not outlive the scroll: plugins share one display manager.
Example:
from src.common import scroll_config
from src.common.scroll_helper import ScrollHelper
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.scroll_helper = ScrollHelper(
self.display_manager.width, self.display_manager.height, self.logger)
# ...later, hand it content with self.scroll_helper.set_scrolling_image(img)
self.scroll_settings = scroll_config.configure(
self.scroll_helper,
plugin_config=self.config,
global_config=self.global_config,
display_manager=self.display_manager,
plugin_logger=self.logger,
)
def display(self, force_clear=False):
self.display_manager.set_scrolling_state(
True, frame_hold=self.scroll_settings.frame_hold)
self.scroll_helper.update_scroll_position()
self.display_manager.image = self.scroll_helper.get_visible_portion()
self.display_manager.update_display()
if self.scroll_helper.is_scroll_complete():
self.display_manager.set_scrolling_state(False)
Don't pace the loop with time.sleep(): update_display() blocks on the
panel's vsync, which is what paces a scroll. See docs/SCROLL_PERFORMANCE.md
for choosing a speed.
is_currently_scrolling() -> bool
Check if the display is currently in a scrolling state.
Returns: True if scrolling, False otherwise
defer_update(update_func: Callable, priority: int = 0) -> None
Defer an update function to be called when not scrolling. Useful for non-critical updates that should wait until scrolling completes.
Parameters:
update_func: Function to call when not scrollingpriority(int): Priority level (lower numbers = higher priority, default: 0)
Example:
def update(self):
# Critical update - do immediately
self.fetch_data()
# Non-critical update - defer until not scrolling
self.display_manager.defer_update(
lambda: self.update_cache_metadata(),
priority=1
)
process_deferred_updates() -> None
Process any deferred updates if not currently scrolling. Called automatically by the display controller, but can be called manually if needed.
Note: Plugins typically don't need to call this directly.
get_scrolling_stats() -> dict
Deprecated, removed in 3.8.0. See Deprecated APIs.
Get current scrolling statistics for debugging.
Returns: Dictionary with scrolling state information
Example:
stats = self.display_manager.get_scrolling_stats()
self.logger.debug(f"Scrolling: {stats['is_scrolling']}, Deferred: {stats['deferred_count']}")
Available Fonts
The Display Manager provides several pre-loaded fonts:
display_manager.regular_font # Press Start 2P, size 8
display_manager.small_font # Press Start 2P, size 8
display_manager.calendar_font # 5x7 BDF font
display_manager.extra_small_font # 4x6 TTF font, size 7 (6 snapped to its pixel grid)
display_manager.bdf_5x7_font # Alias for calendar_font
Cache Manager
The Cache Manager handles data caching to reduce API calls and improve performance. Available as self.cache_manager in plugins.
Basic Methods
get(key: str, max_age: int = 300) -> Optional[Dict[str, Any]]
Get data from cache if it exists and is not stale.
Parameters:
key(str): Cache keymax_age(int): Maximum age in seconds (default: 300)
Returns: Cached data dictionary, or None if not found or stale
Example:
cached = self.cache_manager.get("weather_data", max_age=600)
if cached:
return cached
set(key: str, data: Dict[str, Any], ttl: Optional[int] = None) -> None
Store data in cache with current timestamp.
Parameters:
key(str): Cache keydata(Dict): Data to cachettl(int, optional): Time-to-live in seconds (for compatibility)
Example:
self.cache_manager.set("weather_data", {
"temp": 72,
"condition": "sunny"
})
clear_cache(key: Optional[str] = None) -> None
Remove a specific cache entry, or all cache entries when called without arguments.
Parameters:
key(str, optional): Cache key to delete. If omitted, every cached entry (memory + disk) is cleared.
Example:
# Drop one stale entry
self.cache_manager.clear_cache("weather_data")
# Nuke everything (rare — typically only used by maintenance tooling)
self.cache_manager.clear_cache()
Advanced Methods
get_cached_data(key: str, max_age: int = 300, memory_ttl: Optional[int] = None) -> Optional[Dict[str, Any]]
Get data from cache with separate memory and disk TTLs.
Parameters:
key(str): Cache keymax_age(int): TTL for persisted (on-disk) entrymemory_ttl(int, optional): TTL for in-memory entry (defaults to max_age)
Returns: Cached data, or None if not found or stale
Example:
# Use memory cache for 60 seconds, disk cache for 1 hour
data = self.cache_manager.get_cached_data(
"api_response",
max_age=3600,
memory_ttl=60
)
get_cached_data_with_strategy(key: str, data_type: str = 'default') -> Optional[Dict[str, Any]]
Get data using data-type-specific cache strategy. Automatically selects appropriate TTL based on data type.
Parameters:
key(str): Cache keydata_type(str): Data type for strategy selection (e.g., 'weather', 'sports_live', 'stocks')
Returns: Cached data, or None if not found or stale
Example:
# Automatically uses appropriate cache duration for weather data
weather = self.cache_manager.get_cached_data_with_strategy(
"weather_current",
data_type="weather"
)
get_with_auto_strategy(key: str) -> Optional[Dict[str, Any]]
Get data with automatic strategy detection from cache key.
Parameters:
key(str): Cache key (strategy inferred from key name)
Returns: Cached data, or None if not found or stale
Example:
# Strategy automatically detected from key name
data = self.cache_manager.get_with_auto_strategy("nhl_live_scores")
get_background_cached_data(key: str, sport_key: Optional[str] = None) -> Optional[Dict[str, Any]]
Deprecated, removed in 3.8.0 — use
get(). See Deprecated APIs.
Get background service cached data with sport-specific intervals.
Parameters:
key(str): Cache keysport_key(str, optional): Sport identifier (e.g., 'nhl', 'nba') for live interval lookup
Returns: Cached data, or None if not found or stale
Example:
# Uses sport-specific live_update_interval from config
games = self.cache_manager.get_background_cached_data(
"nhl_games",
sport_key="nhl"
)
Strategy Methods
get_cache_strategy(data_type: str, sport_key: Optional[str] = None) -> Dict[str, Any]
Get cache strategy configuration for a data type.
Parameters:
data_type(str): Data type (e.g., 'weather', 'sports_live', 'stocks')sport_key(str, optional): Sport identifier for sport-specific strategies
Returns: Dictionary with strategy configuration (max_age, memory_ttl, etc.)
Example:
strategy = self.cache_manager.get_cache_strategy("sports_live", sport_key="nhl")
max_age = strategy['max_age'] # Get configured max age
get_sport_live_interval(sport_key: str) -> int
Deprecated, removed in 3.8.0. See Deprecated APIs.
Get the live_update_interval for a specific sport from config.
Parameters:
sport_key(str): Sport identifier (e.g., 'nhl', 'nba')
Returns: Live update interval in seconds
Example:
interval = self.cache_manager.get_sport_live_interval("nhl")
# Returns configured live_update_interval for NHL
get_data_type_from_key(key: str) -> str
Extract data type from cache key to determine appropriate cache strategy.
Parameters:
key(str): Cache key
Returns: Inferred data type string
get_sport_key_from_cache_key(key: str) -> Optional[str]
Deprecated, removed in 3.8.0. See Deprecated APIs.
Extract sport key from cache key for sport-specific strategies.
Parameters:
key(str): Cache key
Returns: Sport identifier, or None if not found
Utility Methods
clear_cache(key: Optional[str] = None) -> None
Clear cache for a specific key or all keys.
Parameters:
key(str, optional): Specific key to clear. IfNone, clears all cache.
Example:
# Clear specific key
self.cache_manager.clear_cache("weather_data")
# Clear all cache
self.cache_manager.clear_cache()
get_cache_dir() -> Optional[str]
Get the cache directory path.
Returns: Cache directory path string, or None if not available
list_cache_files() -> List[Dict[str, Any]]
List all cache files with metadata.
Returns: List of dictionaries with cache file information (key, age, size, path, etc.)
Example:
files = self.cache_manager.list_cache_files()
for file_info in files:
self.logger.info(f"Cache: {file_info['key']}, Age: {file_info['age_display']}")
Metrics Methods (deprecated)
get_cache_metrics() -> Dict[str, Any]
Deprecated, removed in 3.8.0. See Deprecated APIs.
Get cache performance metrics.
Returns: Dictionary with cache statistics (total_requests, cache_hit_rate, background_hit_rate, api_calls_saved, average_fetch_time, etc.)
Example:
metrics = self.cache_manager.get_cache_metrics()
self.logger.info(f"Cache hit rate: {metrics['cache_hit_rate']:.2%}")
get_memory_cache_stats() -> Dict[str, Any]
Deprecated, removed in 3.8.0. See Deprecated APIs.
Get memory cache statistics.
Returns: Dictionary with memory cache stats (size, max_size, etc.)
Plugin Manager
The Plugin Manager provides access to other plugins and plugin system information. Available as self.plugin_manager in plugins.
Methods
get_plugin(plugin_id: str) -> Optional[Any]
Get a plugin instance by ID.
Parameters:
plugin_id(str): Plugin identifier
Returns: Plugin instance, or None if not found
Example:
weather_plugin = self.plugin_manager.get_plugin("weather")
if weather_plugin:
# Access weather plugin data
pass
get_all_plugins() -> Dict[str, Any]
Get all loaded plugin instances.
Returns: Dictionary mapping plugin_id to plugin instance
Example:
all_plugins = self.plugin_manager.get_all_plugins()
for plugin_id, plugin in all_plugins.items():
self.logger.info(f"Plugin {plugin_id} is loaded")
get_enabled_plugins() -> List[str]
Deprecated, removed in 3.8.0 — check
enabledon the instances inplugin_manager.plugins. See Deprecated APIs.
Get list of enabled plugin IDs.
Returns: List of plugin identifier strings
get_plugin_info(plugin_id: str) -> Optional[Dict[str, Any]]
Get plugin information including manifest and runtime info.
Parameters:
plugin_id(str): Plugin identifier
Returns: Dictionary with plugin information, or None if not found
Example:
info = self.plugin_manager.get_plugin_info("weather")
if info:
self.logger.info(f"Plugin: {info['name']}, Version: {info.get('version')}")
get_all_plugin_info() -> List[Dict[str, Any]]
Get information for all plugins.
Returns: List of plugin information dictionaries
get_plugin_directory(plugin_id: str) -> Optional[str]
Get the directory path for a plugin.
Parameters:
plugin_id(str): Plugin identifier
Returns: Directory path string, or None if not found
get_plugin_display_modes(plugin_id: str) -> List[str]
Get list of display modes for a plugin.
Parameters:
plugin_id(str): Plugin identifier
Returns: List of display mode names
Example:
modes = self.plugin_manager.get_plugin_display_modes("football-scoreboard")
# Returns: ['nfl_live', 'nfl_recent', 'nfl_upcoming', ...]
Plugin Manifests
Access plugin manifests through self.plugin_manager.plugin_manifests:
# Get manifest for a plugin
manifest = self.plugin_manager.plugin_manifests.get(self.plugin_id, {})
# Access manifest fields
display_modes = manifest.get('display_modes', [])
version = manifest.get('version')
Inter-Plugin Communication
Plugins can communicate with each other through the Plugin Manager:
Example - Getting data from another plugin:
def update(self):
# Get weather plugin
weather_plugin = self.plugin_manager.get_plugin("weather")
if weather_plugin and hasattr(weather_plugin, 'current_temp'):
self.temp = weather_plugin.current_temp
Example - Checking if another plugin is enabled:
weather = self.plugin_manager.plugins.get("weather")
if weather is not None and weather.enabled:
pass
Best Practices
Caching
-
Use appropriate cache keys: Include plugin ID and data type in keys
cache_key = f"{self.plugin_id}_weather_current" -
Use cache strategies: Prefer
get_cached_data_with_strategy()for automatic TTL selectiondata = self.cache_manager.get_cached_data_with_strategy( f"{self.plugin_id}_data", data_type="weather" ) -
Handle cache misses: Always check for
Nonereturn valuescached = self.cache_manager.get(key, max_age=3600) if not cached: cached = self._fetch_from_api() self.cache_manager.set(key, cached)
Display Rendering
-
Always call update_display(): After drawing content, call
update_display()self.display_manager.draw_text("Hello", x=10, y=10) self.display_manager.update_display() # Required! -
Use clear() appropriately: Only clear when necessary (e.g.,
force_clear=True)def display(self, force_clear=False): if force_clear: self.display_manager.clear() # Draw content... self.display_manager.update_display() -
Handle scrolling state: If your plugin scrolls, use scrolling state methods, passing the frame hold
scroll_config.configure()returnedself.display_manager.set_scrolling_state(True, frame_hold=settings.frame_hold) # Scroll content... self.display_manager.set_scrolling_state(False)
Error Handling
-
Log errors appropriately: Use
self.loggerfor plugin-specific loggingtry: data = self._fetch_data() except Exception as e: self.logger.error(f"Failed to fetch data: {e}") return -
Handle missing data gracefully: Provide fallback displays when data is unavailable
if not self.data: self.display_manager.draw_text("No data available", x=10, y=16) self.display_manager.update_display() return
See Also
- BasePlugin Source - Base plugin implementation
- Display Manager Source - Display manager implementation
- Cache Manager Source - Cache manager implementation
- Plugin Manager Source - Plugin manager implementation
- Plugin Development Guide - Complete development guide
- Advanced Plugin Development - Advanced patterns and examples
Deprecated APIs
These still work but log a warning the first time they are called
(journalctl -u ledmatrix shows which one), and are removed in 3.8.0
(first announced for 3.7.0, which shipped with them still in place).
DEPRECATIONS_3.8.md is the usage scan behind that
decision: which of these the official plugins, the registry's third-party
plugins and core still call or override. Only methods that scan reports unused
are removed in 3.8.0; the rest stay until their callers migrate.
| Object | Methods | Instead |
|---|---|---|
cache_manager |
update_cache |
set() |
cache_manager |
get_background_cached_data, is_background_data_available |
get() |
cache_manager |
has_data_changed, setup_persistent_cache, get_sport_live_interval, get_sport_key_from_cache_key, record_cache_hit, record_cache_miss, record_fetch_time, get_cache_metrics, log_cache_metrics, get_memory_cache_stats |
no replacement |
display_manager |
draw_weather_icon, draw_sun, draw_cloud, draw_rain, draw_snow, draw_text_with_icons |
draw your own icons (the weather plugin ships WeatherIcons) |
display_manager |
get_scrolling_stats |
no replacement |
font_manager |
get_font_catalog, get_available_fonts |
read font_catalog |
font_manager |
set_override, remove_override, get_overrides, add_font, remove_font, validate_font, get_size_tokens, get_performance_stats, get_manager_fonts, get_detected_fonts, get_plugin_fonts, unregister_plugin_fonts |
no replacement |
plugin_manager |
get_enabled_plugins |
check enabled on the entries in plugin_manager.plugins |
Removed in 3.9.0
The Vegas APIs that described a fixed-width segment, which Vegas never
implemented. Vegas participation ('scroll', 'pause', 'exclude', see
Vegas scroll hooks) replaces them. Calling one of the
methods, or setting vegas_panel_count, logs a warning once per process.
No official plugin calls them; calendar, olympics and blackjack override
get_supported_vegas_modes(), which keeps working for the plugin itself.
| What | Instead |
|---|---|
BasePlugin.get_supported_vegas_modes() |
declare vegas_participation in the manifest |
BasePlugin.get_vegas_segment_width() and the vegas_panel_count config key |
nothing: a card's width comes from get_vegas_content() and vegas_width_pct |
VegasDisplayMode.SCROLL vs FIXED_SEGMENT (vegas_mode "scroll" vs "fixed") |
'scroll' participation; the two always behaved the same |