Files
LEDMatrix/docs/PLUGIN_API_REFERENCE.md
T
ChuckandClaude Opus 5.5 989eae9405 refactor(plugins): split PluginStoreManager into mixins (#659)
* refactor(plugins): split PluginStoreManager into mixins

src/plugin_system/store_manager.py (2,977 lines) keeps the class, its
shared state, locks, the uninstall registry, directory lookup and
uninstall; its methods are split by area into:
- store_registry.py (_RegistryMixin): registry, GitHub metadata, search,
  manifest validation
- store_install.py (_InstallMixin): install paths and dependencies
- store_update.py (_UpdateMixin): updates, rollback, local git state

Pure move: all 56 members are byte-identical (checked with ast) and the
assembled class has exactly the same attributes as before (checked at
runtime). PluginStoreManager is imported from store_manager.py as before.
Tests that patched shared modules (subprocess, requests, tempfile, shutil)
through store_manager now reach them through the module whose code they
exercise; a source-text contract test reads all store_*.py modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: annotate findings the split moved into new store modules

subprocess imports and a list-form git clone (no shell), and the config
template's placeholder token string -- existing code that Codacy reported
as new because it moved. Annotated with the repo's nosec/nosemgrep style.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: annotate the default-branch git clone the split moved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:00:45 -04:00

34 KiB
Raw Blame History

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.

Table of Contents


Manifest Required Fields

Three parts of core check manifest.json, each for a different set of fields:

Check Fields What happens when one is missing
JSON schema, schema/manifest_schema.json id, name, version, author, entry_point, class_name, compatible_versions Install from URL logs a warning (PluginStoreManager._validate_manifest_schema()); nothing is refused
Plugin Store install, src/plugin_system/store_install.py id, name, class_name, display_modes Install is refused. A registry install first tries to detect a missing class_name from the entry-point file
Plugin loader, src/plugin_system/plugin_loader.py class_name The plugin fails to load

Defaults and other uses:

  • entry_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).
  • 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.

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "author": "YourName",
  "entry_point": "manager.py",
  "class_name": "MyPlugin",
  "display_modes": ["my-plugin"],
  "compatible_versions": [">=2.0.0"]
}

BasePlugin

All plugins must inherit from BasePlugin and implement the required methods. The base class provides access to managers and common functionality.

Available Properties

self.plugin_id          # Plugin identifier (string)
self.config             # Plugin configuration dictionary
self.display_manager    # DisplayManager instance
self.cache_manager      # CacheManager instance
self.plugin_manager     # PluginManager instance
self.logger             # Plugin-specific logger
self.enabled            # Boolean enabled status

Required Methods

update() -> None

Fetch/update data for this plugin. Called on the plugin's update interval: the value get_update_interval() returns when it returns a number, otherwise the static interval: the update_interval in the plugin's manifest, else update_interval in the plugin's section of config.json, else 60 seconds (see get_update_interval() below).

Example:

def update(self):
    cache_key = f"{self.plugin_id}_data"
    cached = self.cache_manager.get(cache_key, max_age=3600)
    if cached:
        self.data = cached
        return
    
    self.data = self._fetch_from_api()
    self.cache_manager.set(cache_key, self.data)

display(force_clear: bool = False) -> None

Render this plugin's display. Called during display rotation or when explicitly requested.

Parameters:

  • force_clear (bool): If True, clear display before rendering

Example:

def display(self, force_clear=False):
    if force_clear:
        self.display_manager.clear()
    
    self.display_manager.draw_text(
        "Hello, World!",
        x=5, y=15,
        color=(255, 255, 255)
    )
    
    self.display_manager.update_display()

Optional Methods

validate_config() -> bool

Validate plugin configuration. Override to implement custom validation.

Returns: True if config is valid, False otherwise

has_live_content() -> bool

Check if plugin currently has live content. Override for live priority plugins.

Returns: True if plugin has live content

get_live_modes() -> List[str]

Get list of display modes to show during live priority takeover.

Returns: List of mode names

cleanup() -> None

Clean up resources when plugin is unloaded. Override to close connections, stop threads, etc.

on_config_change(new_config: Dict[str, Any]) -> None

Called after 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:

def get_update_interval(self):
    # Fast while something is live, manifest default otherwise.
    if any(m.live_games for m in self._live_managers):
        return self.config.get("live_update_interval", 15)
    return None

Added in core 3.4.0; older cores never call it, so a plugin that relies on it should floor ledmatrix_min_version at 3.4.0.

get_display_duration() -> float

Get display duration for this plugin. Can be overridden for dynamic durations.

Returns: Duration in seconds

get_info() -> Dict[str, Any]

Return plugin info for display in web UI. Override to provide additional state information.

Dynamic-duration hooks

Plugins that render multi-step content (e.g. cycling through several games) can extend their display time until they've shown everything. To opt in, either set dynamic_duration.enabled: true in the plugin's config or override supports_dynamic_duration().

supports_dynamic_duration() -> bool

Return True if this plugin should use dynamic durations. Default reads config["dynamic_duration"]["enabled"].

get_dynamic_duration_cap() -> Optional[float]

Maximum number of seconds the controller will keep this plugin on screen in dynamic mode. Default reads config["dynamic_duration"]["max_duration_seconds"].

is_cycle_complete() -> bool

Override this to return True only after the plugin has rendered all of its content for the current rotation. Default returns True immediately, which means a single display() call counts as a full cycle.

reset_cycle_state() -> None

Called by the controller before each new dynamic-duration session. Reset internal counters/iterators here.

Live priority hooks

Live priority lets a plugin temporarily take over the rotation when it has urgent content (live games, breaking news). Enable by setting live_priority: true in the plugin's config and overriding has_live_content().

has_live_priority() -> bool

Whether live priority is enabled in config (default reads config["live_priority"]).

has_live_content() -> bool

Override to return True when the plugin currently has urgent content. Default returns False.

get_live_modes() -> List[str]

List of display modes to show during a live takeover. Default returns the plugin's display_modes from its manifest.

get_vegas_priority_weight() -> Optional[int]

How many slots per Vegas cycle this plugin should get. Default returns None, which defers to the core.

The Vegas ticker is otherwise a strict round robin — every plugin appears exactly once per cycle — so with a dozen plugins enabled a live score can be minutes stale by the time it comes round. A weight of N gives the plugin N slots per cycle, spread evenly through it rather than clumped.

You usually do not need this. When the hook returns None, the core already gives a plugin vegas_scroll.live_weight whenever has_live_priority() and has_live_content() are both true. Live sports get extra turns with no code at all.

Implement it only when the plugin knows something the core cannot. The motivating case is favorite teams — the core can see that a game is live, but not whose:

def get_vegas_priority_weight(self):
    if not (self.has_live_priority() and self.has_live_content()):
        return None                       # let the core decide
    vegas = self.global_config.get('display', {}).get('vegas_scroll', {})
    if self._favorite_is_live():
        return vegas.get('favorite_live_weight', 5)
    return vegas.get('live_weight', 3)

The weight is per plugin, not per game: a scoreboard showing four live games still occupies one slot at a time and rotates its own games within it. Values are clamped to 1–10 by the caller. An exception here is caught and logged, and the core then falls back to its own live-content check — so a plugin whose weight calculation is broken still gets live_weight for a game that really is live, rather than being demoted to 1.

Only consulted 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.

Vegas scroll hooks

Vegas mode shows multiple plugins as a single continuous scroll instead of rotating one at a time. Plugins control how their content appears via these hooks. See ADVANCED_FEATURES.md for the user side of Vegas mode.

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

display_manager.width    # Display width in pixels (int)
display_manager.height    # Display height in pixels (int)

Core Methods

clear() -> None

Clear the display completely. Creates a new black image.

Note: Does not call update_display() automatically. Call update_display() after drawing new content.

Example:

self.display_manager.clear()
# Draw new content...
self.display_manager.update_display()

update_display() -> None

Update the physical display using double buffering. Call this after drawing all content.

Example:

self.display_manager.draw_text("Hello", x=10, y=10)
self.display_manager.update_display()  # Actually show on display

Text Rendering

draw_text(text: str, x: int = None, y: int = None, color: tuple = (255, 255, 255), small_font: bool = False, font: ImageFont = None, centered: bool = False) -> None

Draw text on the canvas.

Parameters:

  • text (str): Text to 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:

# 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:

width = self.display_manager.get_text_width("Hello", self.display_manager.regular_font)
x = (self.display_manager.width - width) // 2  # Center text

get_font_height(font) -> int

Get the height of the given font for line spacing purposes.

Parameters:

  • font: Font object (ImageFont or freetype.Face)

Returns: Height in pixels

Example:

font_height = self.display_manager.get_font_height(self.display_manager.regular_font)
y = 10 + font_height  # Position next line

format_date_with_ordinal(dt: datetime) -> str

Format a datetime object into 'Mon Aug 30th' style with ordinal suffix.

Parameters:

  • dt: datetime object

Returns: Formatted date string

Example:

from datetime import datetime
date_str = self.display_manager.format_date_with_ordinal(datetime.now())
# Returns: "Jan 15th"

Image Rendering

The display manager doesn't provide a dedicated draw_image() method. Instead, plugins paste directly onto the underlying PIL Image (display_manager.image), then call update_display() to push the buffer to the matrix.

from PIL import Image

logo = Image.open("assets/logo.png").convert("RGB")
self.display_manager.image.paste(logo, (10, 10))
self.display_manager.update_display()

For transparency support, paste using a mask:

icon = Image.open("assets/icon.png").convert("RGBA")
self.display_manager.image.paste(icon, (5, 5), icon)
self.display_manager.update_display()

This is the canonical way to render arbitrary images.

Weather Icons (deprecated)

Deprecated, removed in 3.7.0 — draw your own icons (the weather plugin ships WeatherIcons). See Deprecated APIs.

  • draw_weather_icon(condition, x, y, size=16) — icon for a condition string such as "clear", "clouds", "rain", "snow", "storm"
  • draw_sun(x, y, size=16), draw_cloud(x, y, size=16, color=(200, 200, 200)), draw_rain(x, y, size=16), draw_snow(x, y, size=16)
  • draw_text_with_icons(text, icons=None, x=None, y=None, color=(255, 255, 255)) — text plus a list of (icon_type, x, y) icons; 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:

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:

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.

Get current scrolling statistics for debugging.

Returns: Dictionary with scrolling state information

Example:

stats = self.display_manager.get_scrolling_stats()
self.logger.debug(f"Scrolling: {stats['is_scrolling']}, Deferred: {stats['deferred_count']}")

Available Fonts

The Display Manager provides several pre-loaded fonts:

display_manager.regular_font      # Press Start 2P, size 8
display_manager.small_font        # Press Start 2P, size 8
display_manager.calendar_font     # 5x7 BDF font
display_manager.extra_small_font  # 4x6 TTF font, size 7 (6 snapped to its pixel grid)
display_manager.bdf_5x7_font     # Alias for calendar_font

Cache Manager

The Cache Manager handles data caching to reduce API calls and improve performance. Available as self.cache_manager in plugins.

Basic Methods

get(key: str, max_age: int = 300) -> Optional[Dict[str, Any]]

Get data from cache if it exists and is not stale.

Parameters:

  • key (str): Cache key
  • max_age (int): Maximum age in seconds (default: 300)

Returns: Cached data dictionary, or None if not found or stale

Example:

cached = self.cache_manager.get("weather_data", max_age=600)
if cached:
    return cached

set(key: str, data: Dict[str, Any], ttl: Optional[int] = None) -> None

Store data in cache with current timestamp.

Parameters:

  • key (str): Cache key
  • data (Dict): Data to cache
  • ttl (int, optional): Time-to-live in seconds (for compatibility)

Example:

self.cache_manager.set("weather_data", {
    "temp": 72,
    "condition": "sunny"
})

clear_cache(key: Optional[str] = None) -> None

Remove a specific cache entry, or all cache entries when called without arguments.

Parameters:

  • key (str, optional): Cache key to delete. If omitted, every cached entry (memory + disk) is cleared.

Example:

# Drop one stale entry
self.cache_manager.clear_cache("weather_data")

# Nuke everything (rare — typically only used by maintenance tooling)
self.cache_manager.clear_cache()

Advanced Methods

get_cached_data(key: str, max_age: int = 300, memory_ttl: Optional[int] = None) -> Optional[Dict[str, Any]]

Get data from cache with separate memory and disk TTLs.

Parameters:

  • key (str): Cache 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:

# 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:

# Automatically uses appropriate cache duration for weather data
weather = self.cache_manager.get_cached_data_with_strategy(
    "weather_current",
    data_type="weather"
)

get_with_auto_strategy(key: str) -> Optional[Dict[str, Any]]

Get data with automatic strategy detection from cache key.

Parameters:

  • key (str): Cache key (strategy inferred from key name)

Returns: Cached data, or None if not found or stale

Example:

# Strategy automatically detected from key name
data = self.cache_manager.get_with_auto_strategy("nhl_live_scores")

get_background_cached_data(key: str, sport_key: Optional[str] = None) -> Optional[Dict[str, Any]]

Deprecated, removed in 3.7.0 — use get(). See 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:

# Uses sport-specific live_update_interval from config
games = self.cache_manager.get_background_cached_data(
    "nhl_games",
    sport_key="nhl"
)

Strategy Methods

get_cache_strategy(data_type: str, sport_key: Optional[str] = None) -> Dict[str, Any]

Get cache strategy configuration for a data type.

Parameters:

  • data_type (str): Data type (e.g., 'weather', 'sports_live', 'stocks')
  • sport_key (str, optional): Sport identifier for sport-specific strategies

Returns: Dictionary with strategy configuration (max_age, memory_ttl, etc.)

Example:

strategy = self.cache_manager.get_cache_strategy("sports_live", sport_key="nhl")
max_age = strategy['max_age']  # Get configured max age

get_sport_live_interval(sport_key: str) -> int

Deprecated, removed in 3.7.0. See Deprecated APIs.

Get the live_update_interval for a specific sport from config.

Parameters:

  • sport_key (str): Sport identifier (e.g., 'nhl', 'nba')

Returns: Live update interval in seconds

Example:

interval = self.cache_manager.get_sport_live_interval("nhl")
# Returns configured live_update_interval for NHL

get_data_type_from_key(key: str) -> str

Extract data type from cache key to determine appropriate cache strategy.

Parameters:

  • key (str): Cache key

Returns: Inferred data type string

get_sport_key_from_cache_key(key: str) -> Optional[str]

Deprecated, removed in 3.7.0. See Deprecated APIs.

Extract sport key from cache key for sport-specific strategies.

Parameters:

  • key (str): Cache key

Returns: Sport identifier, or None if not found

Utility Methods

clear_cache(key: Optional[str] = None) -> None

Clear cache for a specific key or all keys.

Parameters:

  • key (str, optional): Specific key to clear. If None, clears all cache.

Example:

# Clear specific key
self.cache_manager.clear_cache("weather_data")

# Clear all cache
self.cache_manager.clear_cache()

get_cache_dir() -> Optional[str]

Get the cache directory path.

Returns: Cache directory path string, or None if not available

list_cache_files() -> List[Dict[str, Any]]

List all cache files with metadata.

Returns: List of dictionaries with cache file information (key, age, size, path, etc.)

Example:

files = self.cache_manager.list_cache_files()
for file_info in files:
    self.logger.info(f"Cache: {file_info['key']}, Age: {file_info['age_display']}")

Metrics Methods (deprecated)

get_cache_metrics() -> Dict[str, Any]

Deprecated, removed in 3.7.0. See Deprecated APIs.

Get cache performance metrics.

Returns: Dictionary with cache statistics (total_requests, cache_hit_rate, background_hit_rate, api_calls_saved, average_fetch_time, etc.)

Example:

metrics = self.cache_manager.get_cache_metrics()
self.logger.info(f"Cache hit rate: {metrics['cache_hit_rate']:.2%}")

get_memory_cache_stats() -> Dict[str, Any]

Deprecated, removed in 3.7.0. See Deprecated APIs.

Get memory cache statistics.

Returns: Dictionary with memory cache stats (size, max_size, etc.)


Plugin Manager

The Plugin Manager provides access to other plugins and plugin system information. Available as self.plugin_manager in plugins.

Methods

get_plugin(plugin_id: str) -> Optional[Any]

Get a plugin instance by ID.

Parameters:

  • plugin_id (str): Plugin identifier

Returns: Plugin instance, or None if not found

Example:

weather_plugin = self.plugin_manager.get_plugin("weather")
if weather_plugin:
    # Access weather plugin data
    pass

get_all_plugins() -> Dict[str, Any]

Get all loaded plugin instances.

Returns: Dictionary mapping plugin_id to plugin instance

Example:

all_plugins = self.plugin_manager.get_all_plugins()
for plugin_id, plugin in all_plugins.items():
    self.logger.info(f"Plugin {plugin_id} is loaded")

get_enabled_plugins() -> List[str]

Deprecated, removed in 3.7.0 — check enabled on the instances in plugin_manager.plugins. See Deprecated APIs.

Get list of enabled plugin IDs.

Returns: List of plugin identifier strings

get_plugin_info(plugin_id: str) -> Optional[Dict[str, Any]]

Get plugin information including manifest and runtime info.

Parameters:

  • plugin_id (str): Plugin identifier

Returns: Dictionary with plugin information, or None if not found

Example:

info = self.plugin_manager.get_plugin_info("weather")
if info:
    self.logger.info(f"Plugin: {info['name']}, Version: {info.get('version')}")

get_all_plugin_info() -> List[Dict[str, Any]]

Get information for all plugins.

Returns: List of plugin information dictionaries

get_plugin_directory(plugin_id: str) -> Optional[str]

Get the directory path for a plugin.

Parameters:

  • plugin_id (str): Plugin identifier

Returns: Directory path string, or None if not found

get_plugin_display_modes(plugin_id: str) -> List[str]

Get list of display modes for a plugin.

Parameters:

  • plugin_id (str): Plugin identifier

Returns: List of display mode names

Example:

modes = self.plugin_manager.get_plugin_display_modes("football-scoreboard")
# Returns: ['nfl_live', 'nfl_recent', 'nfl_upcoming', ...]

Plugin Manifests

Access plugin manifests through self.plugin_manager.plugin_manifests:

# Get manifest for a plugin
manifest = self.plugin_manager.plugin_manifests.get(self.plugin_id, {})

# Access manifest fields
display_modes = manifest.get('display_modes', [])
version = manifest.get('version')

Inter-Plugin Communication

Plugins can communicate with each other through the Plugin Manager:

Example - Getting data from another plugin:

def update(self):
    # Get weather plugin
    weather_plugin = self.plugin_manager.get_plugin("weather")
    if weather_plugin and hasattr(weather_plugin, 'current_temp'):
        self.temp = weather_plugin.current_temp

Example - Checking if another plugin is enabled:

weather = self.plugin_manager.plugins.get("weather")
if weather is not None and weather.enabled:
    pass

Best Practices

Caching

  1. Use appropriate cache keys: Include plugin ID and data type in keys

    cache_key = f"{self.plugin_id}_weather_current"
    
  2. Use cache strategies: Prefer get_cached_data_with_strategy() for automatic TTL selection

    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

    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()

    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)

    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

    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

    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

    if not self.data:
        self.display_manager.draw_text("No data available", x=10, y=16)
        self.display_manager.update_display()
        return
    

See Also


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