mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
* docs: add ARCHITECTURE and PERMISSIONS guides ARCHITECTURE.md maps the processes, the state the display and web services share through the cache, the display loop, the plugin system, the web UI and the update path, with links into the code and a where-to-start table. PERMISSIONS.md lists who owns what after install, both sudoers files (and why iptables is not granted), the polkit rule, and which scripts/fix_perms script to run as which user. Both are linked from the docs index, along with the MQTT bridge README and src/common/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: correct stale setup, service and troubleshooting claims - README: quick actions run systemctl on ledmatrix.service (run.py), not display_controller.py; use_short_date_format has no effect; the installer uses system pip with --break-system-packages, not a venv. - CONFIG_DEBUGGING: LEDMATRIX_DEBUG must be "true"; logs are in journald. - GETTING_STARTED, WEB_INTERFACE_GUIDE, TROUBLESHOOTING: enabling a plugin, plugin settings, brightness and Vegas settings apply without a restart; matrix hardware settings still need one. - TROUBLESHOOTING: install dependencies with sudo so the root service sees them; point permission problems at PERMISSIONS.md instead of a project-wide chown. - ADVANCED_FEATURES: real BackgroundDataService stats keys; Vegas hooks return VegasDisplayMode and None falls back to capture; cache files are 0660; fix_web_permissions.sh runs as the web user and does not touch sudoers. - STARLARK_APPS_GUIDE: only the linux-arm64 pixlet binary is downloaded. - HOW_TO_RUN_TESTS: test class examples that exist. - CLAUDE.md: PluginStoreManager, plugin_dirs.py, monorepo installs via the Trees API with ZIP fallback, requirements.txt is optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: mark deprecated plugin APIs and state manifest fields once Methods @deprecated("3.7.0") (the set pinned in test_deprecation.py) were shown as current API in the quick reference, API reference, advanced guide, development guide and FONT_MANAGER. Each is now marked deprecated with its replacement. FONT_MANAGER is rewritten around the current API; the override editor is gone and override methods are deprecated. Required manifest fields were stated three different ways. The API reference now has one section: the 7 schema-required fields, the 4 the store refuses without, class_name for the loader, and the 8 to set. The other guides link to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: document every src/common module and every widget - src/common/README.md covered 7 of 17 modules. It now has a table of all of them (purpose, whether plugins import it, release to floor on), a short entry each, and logging advice that matches the code. - SPORTS_UNIFICATION listed two shared modules and called sports_helpers the first; it now lists all six. - The widgets README lists all 28 registered widgets plus the support files, and absorbs the parts that only docs/widget-guide.md had (x-options.labels, x-advanced, x-display hidden, plugin-file-manager). docs/widget-guide.md is now a pointer to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(security): fix_web_permissions.sh re-hardens the root sudo helpers The script chowns the whole project to the web user. That included scripts/fix_perms/safe_plugin_rm.sh and safe_pip_install.sh -- the two helpers /etc/sudoers.d/ledmatrix_web lets the web user run as root -- so running it turned both into a root shell for whoever can edit them. It also re-grouped config_secrets.json away from ledmatrix. After the chown it now does what first_time_install.sh's Steps 11 and 11.1 do: helpers back to root:root 755, and config_secrets.json back to the web unit's User=:ledmatrix 640. Each step is non-fatal and prints the manual command if it fails. Also fixes what the script and its docs claimed: it never configured sudoers, its closing hint pointed at ./configure_web_sudo.sh (wrong path), and the README and ADVANCED_FEATURES.md said to run it with sudo, which it refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(security): validate and harden every sudoers drop-in the scripts write configure_wifi_permissions.sh copied its rules into /etc/sudoers.d/ledmatrix_wifi without `visudo -c`. A malformed drop-in makes sudo refuse every command for every user, which on a headless Pi leaves no way back in. It now checks first and leaves the installed file alone when the rules do not parse, as the other two writers do. (It already used mktemp, so that part of the review did not apply.) It also grants the two literal commands wifi_manager.py runs for NetworkManager's shared-mode dnsmasq drop-in -- `cp /tmp/ledmatrix-nm-dnsmasq.conf .../dnsmasq-shared.d/ledmatrix-captive.conf` and `rm -f` of that file. The directory's mkdir was granted, the file was not. Both are pinned in test_sudo_allowlist_covers_calls.py. configure_web_sudo.sh wrote its rules to /tmp/ledmatrix_web_sudoers_$$, a predictable name in a world-writable directory; it now uses mktemp with an EXIT trap, as first_time_install.sh does. It sets mode 440 on the installed file instead of leaving the temp file's mode, and finds visudo in /usr/sbin when that is not on the user's PATH, which skipped the check silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(install): escape the project path in the DNS-fix and MQTT unit renderers install_dns_fix.sh and install_mqtt_bridge.sh substituted __PROJECT_ROOT_DIR__ with the raw path, while the other three renderers go through sed_escape_replacement from lib_systemd_render.sh. A checkout under a path containing `&`, `\` or `|` rendered a corrupted unit from these two only. Both now source the helper and use it, and a test checks that every placeholder substitution in scripts/install uses an escaped value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(install): stop the installer scripts reporting things that are not true - first_time_install.sh printed "Password: ledmatrix123" for the setup access point. wifi_manager creates it as an open network ("No password" on the panel), so it now says so. - Step 10.1 printed "✓ WiFi management permissions configured" straight after its own failure message; install_wifi_monitor.sh printed "✓ Package installation completed" after a failed apt install. The tick now only follows success. - Step 7 printed "Web dependencies already installed ... in Step 5" in the one branch that runs because Step 5 did not install them, then created .web_deps_installed on that basis. It now warns and leaves the marker off so the next run retries, as the comment below it intends. - check_system_compatibility.sh called Debian 12 Bookworm "full compatibility confirmed" while first_time_install.sh refuses anything but Debian 13. Bookworm, older Debian and non-Debian systems are now errors. Its counters used ((X++)), which under `set -e` exits the script at the first warning or error (the expression is 0), so the check never reached its summary on any system with one. - configure_web_sudo.sh and configure_wifi_permissions.sh finished by testing `sudo -n test -f ...` and `sudo -n nmcli device status`, neither of which is granted, so they always reported a failure. They now ask `sudo -n -l` about commands the new rules do grant, which checks the rule without running anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(install): print the completion summary before rebooting With -y -- and so for every one-shot `curl | bash` install, which always passes -y -- first_time_install.sh ran `reboot` about 180 lines before its "Installation Complete / Web UI Access" summary. reboot returns at once, so the summary printed while the Pi was going down and the SSH session usually dropped before the web UI address could be read. The reboot block moves, unchanged, to the very end of the script. The interactive prompt now also follows the summary. Because the summary now runs before the -y reboot, its one command that could fail under `set -Eeuo pipefail` (the SSID lookup, when nmcli reports a connected device but no active network line) gets `|| true`; a missing SSID was already handled as "SSID unknown". one-shot-install.sh prints its "Next steps" after the installer returns, by which time the reboot is under way, so it now says so, and README's Quick Install mentions the automatic reboot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(scripts): correct wrong comments and messages, drop dead code No behaviour change except the output text noted below. - 2775 is setgid, not the sticky bit (first_time_install.sh Step 3.1, fix_plugin_permissions.sh), and root needs no "PWM hardware access" to plugin files. - The 777 comments in first_time_install.sh Step 3's fallback and fix_assets_permissions.sh said root needs it to write. Root ignores mode bits; the comments now say what 777 actually opens. The 777 itself is unchanged. - apt_remove ends in `|| true`, so Step 12's "Some packages could not be removed" branch could never run; it is gone and the helper stays non-fatal. - detect_web_service_user's comment named Step 8 for the web unit (install_service.sh installs it in Step 7.5) and now says which branch actually runs. - Step 5 described an "already installed" check that does not exist; the ACTUAL_USER comment described the re-exec backwards. - on_error printed a literal "\n" before "Common fixes:". - Dead code: one-shot-install.sh's uncalled fix_tmp_permissions, LEDMATRIX_ELEVATED=1 (never read) on the sudo re-exec, and configure_web_sudo.sh's unused PYTHON_PATH, which also made a missing python3 fatal for rules that never mention it. - start_display.sh / stop_display.sh said "for user: <you>"; the service runs as root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(fix_perms): fix_cache_permissions.sh uses setup_cache.sh's model There were two models for /var/cache/ledmatrix. setup_cache.sh (the installer's Step 2) and install_web_service.sh share it through the ledmatrix group: root:ledmatrix, 2775, files 660, which is also what DiskCache relies on to give files the directory's group. fix_cache_permissions.sh instead made it 777 and re-grouped it to the invoking user's group, undoing that. It now runs setup_cache.sh for /var/cache/ledmatrix and keeps its own handling of ~/.ledmatrix_cache. Dropped: /var/cache/ledmatrix/ placeholder_logos (nothing reads it) and the checks against the `daemon` user (no service runs as daemon). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: pin actions/checkout in the Claude workflows, drop template comments claude.yml and claude-code-review.yml used actions/checkout@v4 while test.yml and release-version-check.yml pin the v4.2.2 commit SHA; they now pin the same SHA. The commented-out starter-template settings (prompt, claude_args, paths, author filter) are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(scripts): index every script and list removal candidates New scripts/README.md gives one line per top-level script and scripts directory, marked keep, dev-only or diagnostic, and lists the eight scripts nothing in the repo refers to as candidates for removal (kept for now). The install, utils and dev READMEs now list the files they were missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: tighten two checks that mutation testing showed were too loose - The wifi sudoers check matched `visudo -c -f "$TEMP_SUDOERS"` in the error report too, so replacing the check with `if false` still passed. It now requires the command as the condition. - The summary test never had the setup access point up, so reinstating the bogus "Password: ledmatrix123" line went unnoticed. A case with hostapd active now checks the AP is described as open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(permissions): describe the repaired fix_perms scripts and new WiFi grants Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): docs-scripts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1089 lines
34 KiB
Markdown
1089 lines
34 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_manager.py`](../src/plugin_system/store_manager.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.
|
||
|
||
#### `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.7.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.7.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.7.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.7.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.7.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.7.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.7.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.7.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 in 3.6 but log a warning the first time they are called
|
||
(`journalctl -u ledmatrix` shows which one), and are **removed in 3.7.0**.
|
||
Nothing in core, the official plugins or the third-party plugins in the
|
||
registry calls them.
|
||
|
||
| 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` |
|
||
|