mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
* chore(deprecation): remove the 35 APIs deprecated for 3.8.0 The usage scan (docs/DEPRECATIONS_3.8.md, regenerated 2026-10-01 and committed here) finds no call or override of any of them in the 46 monorepo plugins or the 8 third-party plugins plugins.json lists; the only core callers were other deprecated methods removed alongside. - CacheManager: 13 methods, plus the private helpers only has_data_changed used (_has_*_changed, _is_market_open). - DisplayManager: 7 methods, plus WEATHER_COLORS and the private _draw_sun/_cloud/_rain/_snow/_storm helpers only the icon methods used. - FontManager: 14 methods, plus size_tokens, _save_overrides and _clear_plugin_font_cache. font_overrides and _load_overrides stay: resolve_font() still applies config/font_overrides.json. performance_stats stays: get_font() keeps it and tests read it. - PluginManager.get_enabled_plugins. test_deprecation.py pins only the two 3.9.0 markers now; the scanner tests run against a stand-in core instead of the real markers. The memory-tier tests read stats through log_memory_cache_stats() and the component, and the test of the removed _clear_plugin_font_cache goes. Docs drop the removed methods' reference entries; the Deprecated APIs table becomes "Removed in 3.8.0". CHANGELOG gains a Removed section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(deprecation): drop the test harness's copies of the removed icon methods VisualTestDisplayManager still drew weather icons that DisplayManager no longer has, so a plugin's visual tests could pass on calls that raise AttributeError on the real display. Its draw_sun/draw_cloud/draw_rain/ draw_snow/draw_weather_icon/draw_text_with_icons, WEATHER_COLORS and the private helpers go, with the tests that exercised them. The CHANGELOG's Deprecations entries no longer say nothing is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1156 lines
41 KiB
Markdown
1156 lines
41 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 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](ARCHITECTURE.md#web-and-display-processes-who-runs-plugins).
|
||
|
||
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:
|
||
|
||
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 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](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.
|
||
|
||
#### 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`:
|
||
|
||
```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](PLUGIN_CONFIG_CORE_PROPERTIES.md)).
|
||
Vegas resolves it in this order:
|
||
|
||
1. the user's `vegas_participation` config value;
|
||
2. the plugin's `get_vegas_participation()` — the default implementation
|
||
reads the manifest's `vegas_participation`, then derives a value from
|
||
the legacy hooks below;
|
||
3. derived from the legacy hooks: `get_vegas_display_mode()` returning
|
||
`VegasDisplayMode.STATIC` → `'pause'`; otherwise
|
||
`get_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.
|
||
|
||
```python
|
||
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.
|
||
|
||
```python
|
||
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 a `VegasDisplayMode` member (not a
|
||
string — the string `'static'` never paused anything). The default reads
|
||
the plugin's `vegas_mode` config value (`"scroll"`, `"fixed"` or
|
||
`"static"`), else maps content type `'multi'` to `SCROLL` and anything
|
||
else to `FIXED_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-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 `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.
|
||
|
||
### 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.
|
||
|
||
### 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")
|
||
```
|
||
|
||
### 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_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
|
||
|
||
### 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']}")
|
||
```
|
||
|
||
---
|
||
|
||
## 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_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
|
||
|
||
A deprecated method still works but logs a warning the first time it is
|
||
called (`journalctl -u ledmatrix` shows which one), until the release that
|
||
removes it. [DEPRECATIONS_3.8.md](DEPRECATIONS_3.8.md) is the usage scan
|
||
behind each removal: which of the deprecated methods the official plugins,
|
||
the registry's third-party plugins and core still call or override. Only
|
||
methods that scan reports unused are removed; the rest stay until their
|
||
callers migrate.
|
||
|
||
### Removed in 3.8.0
|
||
|
||
Deprecated in 3.5.0 with a warning on first call, and gone in 3.8.0:
|
||
the scan found no caller in any official or third-party plugin. Calling one
|
||
now raises `AttributeError`.
|
||
|
||
| 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](#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 |
|