mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
Moves the 35 @deprecated markers from 3.7.0 (already shipped with them in place) to 3.8.0, and adds scripts/plugin_api_usage.py plus the generated docs/DEPRECATIONS_3.8.md: who still calls or overrides each deprecated method across core, the monorepo and third-party plugins. Removes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1098 lines
35 KiB
Markdown
1098 lines
35 KiB
Markdown
# 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 `BasePlugin` also exposes `self.layout`,
|
||
> `self.draw_fit(text, region)` and `self.draw_image(img, region, ...)` —
|
||
> the recommended way to render text and images that scale to any panel
|
||
> size. See [ADAPTIVE_LAYOUT.md](ADAPTIVE_LAYOUT.md).
|
||
|
||
## Table of Contents
|
||
|
||
- [Manifest Required Fields](#manifest-required-fields)
|
||
- [BasePlugin](#baseplugin)
|
||
- [Display Manager](#display-manager)
|
||
- [Cache Manager](#cache-manager)
|
||
- [Plugin Manager](#plugin-manager)
|
||
- [Deprecated APIs](#deprecated-apis)
|
||
|
||
---
|
||
|
||
## 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`](../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`](../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`](../src/plugin_system/plugin_loader.py) | `class_name` | The plugin fails to load |
|
||
|
||
Defaults and other uses:
|
||
|
||
- `entry_point` defaults to `manager.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()` in
|
||
[`src/plugin_system/compatibility.py`](../src/plugin_system/compatibility.py)).
|
||
- `version` is compared with the registry's `latest_version` to decide
|
||
whether an update is available.
|
||
- If `display_modes` is 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.
|
||
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```python
|
||
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()`](#get_update_interval---optionalfloat) below).
|
||
|
||
**Example**:
|
||
```python
|
||
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**:
|
||
```python
|
||
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 plugin configuration is updated via web API.
|
||
|
||
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 plugin is enabled.
|
||
|
||
#### `on_disable() -> None`
|
||
|
||
Called when plugin is disabled.
|
||
|
||
#### `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:
|
||
|
||
1. It calls `get_update_interval()`. A number wins over everything below.
|
||
Values under `PluginManager.MIN_DYNAMIC_UPDATE_INTERVAL` (5 seconds) are
|
||
raised to it.
|
||
2. If the hook returns `None`, raises, or returns something that isn't a
|
||
finite number (a `bool`, a string, NaN, infinity), it is ignored and the
|
||
static interval applies: the manifest's `update_interval`, else
|
||
`update_interval` in the plugin's section of `config.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**:
|
||
```python
|
||
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*:
|
||
|
||
```python
|
||
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 when the user has set `vegas_scroll.live_in_ticker`. With the
|
||
default (`false`) live content preempts Vegas entirely and there is no ticker
|
||
to be weighted within. See
|
||
[ADVANCED_FEATURES.md](ADVANCED_FEATURES.md#live-content-in-the-ticker).
|
||
|
||
### 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](ADVANCED_FEATURES.md) for the user
|
||
side of Vegas mode.
|
||
|
||
#### `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_content_type() -> str`
|
||
|
||
`'multi'`, `'static'`, or `'none'`. Affects how Vegas mode treats the
|
||
plugin. Default `'static'`.
|
||
|
||
#### `get_vegas_display_mode() -> VegasDisplayMode`
|
||
|
||
Returns one of `VegasDisplayMode.SCROLL`, `FIXED_SEGMENT`, or `STATIC`.
|
||
Read from `config["vegas_mode"]` or override directly.
|
||
|
||
#### `get_supported_vegas_modes() -> List[VegasDisplayMode]`
|
||
|
||
The set of Vegas modes this plugin can render. Used by the UI to populate
|
||
the mode selector for this plugin.
|
||
|
||
#### `get_vegas_segment_width() -> Optional[int]`
|
||
|
||
For `FIXED_SEGMENT` plugins, the number of *panels* the segment
|
||
occupies in the scroll (pixel width = panels × `single_panel_width`,
|
||
from `display.hardware.cols`). `None` uses the default of 1 panel.
|
||
|
||
> The full source for `BasePlugin` lives in
|
||
> `src/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
|
||
|
||
```python
|
||
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**:
|
||
```python
|
||
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**:
|
||
```python
|
||
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 display
|
||
- `x` (int, optional): X position. If `None`, text is centered horizontally. If `centered=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 True
|
||
- `font` (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**:
|
||
```python
|
||
# 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 measure
|
||
- `font`: Font object (ImageFont or freetype.Face)
|
||
|
||
**Returns**: Width in pixels
|
||
|
||
**Example**:
|
||
```python
|
||
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**:
|
||
```python
|
||
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**:
|
||
```python
|
||
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.
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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](#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; calls `update_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 otherwise
|
||
- `frame_hold` (int, default 1): how many panel refreshes each pushed frame is
|
||
held for (clamped to 1-255; ignored when `is_scrolling` is False, which
|
||
resets it to 1). Pass the `frame_hold` of the settings
|
||
`src.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**:
|
||
```python
|
||
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 scrolling
|
||
- `priority` (int): Priority level (lower numbers = higher priority, default: 0)
|
||
|
||
**Example**:
|
||
```python
|
||
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](#deprecated-apis).
|
||
|
||
Get current scrolling statistics for debugging.
|
||
|
||
**Returns**: Dictionary with scrolling state information
|
||
|
||
**Example**:
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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 key
|
||
- `max_age` (int): Maximum age in seconds (default: 300)
|
||
|
||
**Returns**: Cached data dictionary, or `None` if not found or stale
|
||
|
||
**Example**:
|
||
```python
|
||
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 key
|
||
- `data` (Dict): Data to cache
|
||
- `ttl` (int, optional): Time-to-live in seconds (for compatibility)
|
||
|
||
**Example**:
|
||
```python
|
||
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**:
|
||
```python
|
||
# 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 key
|
||
- `max_age` (int): TTL for persisted (on-disk) entry
|
||
- `memory_ttl` (int, optional): TTL for in-memory entry (defaults to max_age)
|
||
|
||
**Returns**: Cached data, or `None` if not found or stale
|
||
|
||
**Example**:
|
||
```python
|
||
# 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 key
|
||
- `data_type` (str): Data type for strategy selection (e.g., 'weather', 'sports_live', 'stocks')
|
||
|
||
**Returns**: Cached data, or `None` if not found or stale
|
||
|
||
**Example**:
|
||
```python
|
||
# 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**:
|
||
```python
|
||
# 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](#deprecated-apis).
|
||
|
||
Get background service cached data with sport-specific intervals.
|
||
|
||
**Parameters**:
|
||
- `key` (str): Cache key
|
||
- `sport_key` (str, optional): Sport identifier (e.g., 'nhl', 'nba') for live interval lookup
|
||
|
||
**Returns**: Cached data, or `None` if not found or stale
|
||
|
||
**Example**:
|
||
```python
|
||
# 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**:
|
||
```python
|
||
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](#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**:
|
||
```python
|
||
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](#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. If `None`, clears all cache.
|
||
|
||
**Example**:
|
||
```python
|
||
# 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**:
|
||
```python
|
||
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](#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**:
|
||
```python
|
||
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](#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**:
|
||
```python
|
||
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**:
|
||
```python
|
||
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 `enabled` on the instances in `plugin_manager.plugins`. See [Deprecated APIs](#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**:
|
||
```python
|
||
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**:
|
||
```python
|
||
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`:
|
||
|
||
```python
|
||
# 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**:
|
||
```python
|
||
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**:
|
||
```python
|
||
weather = self.plugin_manager.plugins.get("weather")
|
||
if weather is not None and weather.enabled:
|
||
pass
|
||
```
|
||
|
||
---
|
||
|
||
## Best Practices
|
||
|
||
### Caching
|
||
|
||
1. **Use appropriate cache keys**: Include plugin ID and data type in keys
|
||
```python
|
||
cache_key = f"{self.plugin_id}_weather_current"
|
||
```
|
||
|
||
2. **Use cache strategies**: Prefer `get_cached_data_with_strategy()` for automatic TTL selection
|
||
```python
|
||
data = self.cache_manager.get_cached_data_with_strategy(
|
||
f"{self.plugin_id}_data",
|
||
data_type="weather"
|
||
)
|
||
```
|
||
|
||
3. **Handle cache misses**: Always check for `None` return values
|
||
```python
|
||
cached = self.cache_manager.get(key, max_age=3600)
|
||
if not cached:
|
||
cached = self._fetch_from_api()
|
||
self.cache_manager.set(key, cached)
|
||
```
|
||
|
||
### Display Rendering
|
||
|
||
1. **Always call update_display()**: After drawing content, call `update_display()`
|
||
```python
|
||
self.display_manager.draw_text("Hello", x=10, y=10)
|
||
self.display_manager.update_display() # Required!
|
||
```
|
||
|
||
2. **Use clear() appropriately**: Only clear when necessary (e.g., `force_clear=True`)
|
||
```python
|
||
def display(self, force_clear=False):
|
||
if force_clear:
|
||
self.display_manager.clear()
|
||
# Draw content...
|
||
self.display_manager.update_display()
|
||
```
|
||
|
||
3. **Handle scrolling state**: If your plugin scrolls, use scrolling state methods,
|
||
passing the frame hold `scroll_config.configure()` returned
|
||
```python
|
||
self.display_manager.set_scrolling_state(True, frame_hold=settings.frame_hold)
|
||
# Scroll content...
|
||
self.display_manager.set_scrolling_state(False)
|
||
```
|
||
|
||
### Error Handling
|
||
|
||
1. **Log errors appropriately**: Use `self.logger` for plugin-specific logging
|
||
```python
|
||
try:
|
||
data = self._fetch_data()
|
||
except Exception as e:
|
||
self.logger.error(f"Failed to fetch data: {e}")
|
||
return
|
||
```
|
||
|
||
2. **Handle missing data gracefully**: Provide fallback displays when data is unavailable
|
||
```python
|
||
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](../src/plugin_system/base_plugin.py) - Base plugin implementation
|
||
- [Display Manager Source](../src/display_manager.py) - Display manager implementation
|
||
- [Cache Manager Source](../src/cache_manager.py) - Cache manager implementation
|
||
- [Plugin Manager Source](../src/plugin_system/plugin_manager.py) - Plugin manager implementation
|
||
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md) - Complete development guide
|
||
- [Advanced Plugin Development](ADVANCED_PLUGIN_DEVELOPMENT.md) - 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](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` |
|
||
|