From 0a3b0bc1cddfb3198967a38909d7ebc69d40a8d2 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Wed, 8 Oct 2025 20:48:17 -0400
Subject: [PATCH 001/736] docs: Add comprehensive plugin architecture
specification
- Complete technical specification for plugin-based architecture
- Plugin system design with base classes and managers
- GitHub-based store implementation (HACS-inspired)
- Web UI transformation plans with React components
- Migration strategy from v2.0 to v3.0
- Plugin developer guidelines and best practices
- Security considerations and testing standards
- Full implementation roadmap with timeline
- Example plugin (NHL scores) for reference
- Quick reference guide for developers
---
PLUGIN_ARCHITECTURE_SPEC.md | 2331 +++++++++++++++++++++++++++++++++++
PLUGIN_QUICK_REFERENCE.md | 286 +++++
2 files changed, 2617 insertions(+)
create mode 100644 PLUGIN_ARCHITECTURE_SPEC.md
create mode 100644 PLUGIN_QUICK_REFERENCE.md
diff --git a/PLUGIN_ARCHITECTURE_SPEC.md b/PLUGIN_ARCHITECTURE_SPEC.md
new file mode 100644
index 000000000..dfd4c225e
--- /dev/null
+++ b/PLUGIN_ARCHITECTURE_SPEC.md
@@ -0,0 +1,2331 @@
+# LEDMatrix Plugin Architecture Specification
+
+## Executive Summary
+
+This document outlines the transformation of the LEDMatrix project into a modular, plugin-based architecture that enables user-created displays. The goal is to create a flexible, extensible system similar to Home Assistant Community Store (HACS) where users can discover, install, and manage custom display managers from GitHub repositories.
+
+### Key Decisions
+
+1. **Gradual Migration**: Existing managers remain in core while new plugin infrastructure is built
+2. **Migration Required**: Breaking changes with migration tools provided
+3. **GitHub-Based Store**: Simple discovery system, packages served from GitHub repos
+4. **Plugin Location**: `./plugins/` directory in project root
+
+---
+
+## Table of Contents
+
+1. [Current Architecture Analysis](#current-architecture-analysis)
+2. [Plugin System Design](#plugin-system-design)
+3. [Plugin Store & Discovery](#plugin-store--discovery)
+4. [Web UI Transformation](#web-ui-transformation)
+5. [Migration Strategy](#migration-strategy)
+6. [Plugin Developer Guidelines](#plugin-developer-guidelines)
+7. [Technical Implementation Details](#technical-implementation-details)
+8. [Best Practices & Standards](#best-practices--standards)
+9. [Security Considerations](#security-considerations)
+10. [Implementation Roadmap](#implementation-roadmap)
+
+---
+
+## 1. Current Architecture Analysis
+
+### Current System Overview
+
+**Core Components:**
+- `display_controller.py`: Main orchestrator, hardcoded manager instantiation
+- `display_manager.py`: Handles LED matrix rendering
+- `config_manager.py`: Loads config from JSON files
+- `cache_manager.py`: Caching layer for API calls
+- `web_interface_v2.py`: Web UI with hardcoded manager references
+
+**Manager Pattern:**
+- All managers follow similar initialization: `__init__(config, display_manager, cache_manager)`
+- Common methods: `update()` for data fetching, `display()` for rendering
+- Located in `src/` with various naming conventions
+- Hardcoded imports in display_controller and web_interface
+
+**Configuration:**
+- Monolithic `config.json` with sections for each manager
+- Template-based updates via `config.template.json`
+- Secrets in separate `config_secrets.json`
+
+### Pain Points
+
+1. **Tight Coupling**: Display controller has hardcoded imports for ~40+ managers
+2. **Monolithic Config**: 650+ line config file, hard to navigate
+3. **No Extensibility**: Users can't add custom displays without modifying core
+4. **Update Conflicts**: Config template merges can fail with custom setups
+5. **Scaling Issues**: Adding new displays requires core code changes
+
+---
+
+## 2. Plugin System Design
+
+### Plugin Architecture
+
+```
+plugins/
+├── clock-simple/
+│ ├── manifest.json # Plugin metadata
+│ ├── manager.py # Main plugin class
+│ ├── requirements.txt # Python dependencies
+│ ├── assets/ # Plugin-specific assets
+│ │ └── fonts/
+│ ├── config_schema.json # JSON schema for validation
+│ └── README.md # Documentation
+│
+├── nhl-scoreboard/
+│ ├── manifest.json
+│ ├── manager.py
+│ ├── requirements.txt
+│ ├── assets/
+│ │ └── logos/
+│ └── README.md
+│
+└── weather-animated/
+ ├── manifest.json
+ ├── manager.py
+ ├── requirements.txt
+ ├── assets/
+ │ └── animations/
+ └── README.md
+```
+
+### Plugin Manifest Structure
+
+```json
+{
+ "id": "clock-simple",
+ "name": "Simple Clock",
+ "version": "1.0.0",
+ "author": "ChuckBuilds",
+ "description": "A simple clock display with date",
+ "homepage": "https://github.com/ChuckBuilds/ledmatrix-clock-simple",
+ "entry_point": "manager.py",
+ "class_name": "SimpleClock",
+ "category": "time",
+ "tags": ["clock", "time", "date"],
+ "compatible_versions": [">=2.0.0"],
+ "ledmatrix_version": "2.0.0",
+ "requires": {
+ "python": ">=3.9",
+ "display_size": {
+ "min_width": 64,
+ "min_height": 32
+ }
+ },
+ "config_schema": "config_schema.json",
+ "assets": {
+ "fonts": ["assets/fonts/clock.bdf"],
+ "images": []
+ },
+ "update_interval": 1,
+ "default_duration": 15,
+ "display_modes": ["clock"],
+ "api_requirements": []
+}
+```
+
+### Base Plugin Interface
+
+```python
+# src/plugin_system/base_plugin.py
+
+from abc import ABC, abstractmethod
+from typing import Dict, Any, Optional
+import logging
+
+class BasePlugin(ABC):
+ """
+ Base class that all plugins must inherit from.
+ Provides standard interface and helper methods.
+ """
+
+ def __init__(self, plugin_id: str, config: Dict[str, Any],
+ display_manager, cache_manager, plugin_manager):
+ """
+ Standard initialization for all plugins.
+
+ Args:
+ plugin_id: Unique identifier for this plugin instance
+ config: Plugin-specific configuration
+ display_manager: Shared display manager instance
+ cache_manager: Shared cache manager instance
+ plugin_manager: Reference to plugin manager for inter-plugin communication
+ """
+ self.plugin_id = plugin_id
+ self.config = config
+ self.display_manager = display_manager
+ self.cache_manager = cache_manager
+ self.plugin_manager = plugin_manager
+ self.logger = logging.getLogger(f"plugin.{plugin_id}")
+ self.enabled = config.get('enabled', True)
+
+ @abstractmethod
+ def update(self) -> None:
+ """
+ Fetch/update data for this plugin.
+ Called based on update_interval in manifest.
+ """
+ pass
+
+ @abstractmethod
+ def display(self, force_clear: bool = False) -> None:
+ """
+ Render this plugin's display.
+ Called during rotation or on-demand.
+
+ Args:
+ force_clear: If True, clear display before rendering
+ """
+ pass
+
+ def get_display_duration(self) -> float:
+ """
+ Get the display duration for this plugin instance.
+ Can be overridden based on dynamic content.
+
+ Returns:
+ Duration in seconds
+ """
+ return self.config.get('display_duration', 15.0)
+
+ def validate_config(self) -> bool:
+ """
+ Validate plugin configuration against schema.
+ Called during plugin loading.
+
+ Returns:
+ True if config is valid
+ """
+ # Implementation uses config_schema.json
+ return True
+
+ def cleanup(self) -> None:
+ """
+ Cleanup resources when plugin is unloaded.
+ Override if needed.
+ """
+ pass
+
+ def get_info(self) -> Dict[str, Any]:
+ """
+ Return plugin info for display in web UI.
+
+ Returns:
+ Dict with name, version, status, etc.
+ """
+ return {
+ 'id': self.plugin_id,
+ 'enabled': self.enabled,
+ 'config': self.config
+ }
+```
+
+### Plugin Manager
+
+```python
+# src/plugin_system/plugin_manager.py
+
+import os
+import json
+import importlib
+import sys
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+import logging
+
+class PluginManager:
+ """
+ Manages plugin discovery, loading, and lifecycle.
+ """
+
+ def __init__(self, plugins_dir: str = "plugins",
+ config_manager=None, display_manager=None, cache_manager=None):
+ self.plugins_dir = Path(plugins_dir)
+ self.config_manager = config_manager
+ self.display_manager = display_manager
+ self.cache_manager = cache_manager
+ self.logger = logging.getLogger(__name__)
+
+ # Active plugins
+ self.plugins: Dict[str, Any] = {}
+ self.plugin_manifests: Dict[str, Dict] = {}
+
+ # Ensure plugins directory exists
+ self.plugins_dir.mkdir(exist_ok=True)
+
+ def discover_plugins(self) -> List[str]:
+ """
+ Scan plugins directory for installed plugins.
+
+ Returns:
+ List of plugin IDs
+ """
+ discovered = []
+
+ if not self.plugins_dir.exists():
+ self.logger.warning(f"Plugins directory not found: {self.plugins_dir}")
+ return discovered
+
+ for item in self.plugins_dir.iterdir():
+ if not item.is_dir():
+ continue
+
+ manifest_path = item / "manifest.json"
+ if manifest_path.exists():
+ try:
+ with open(manifest_path, 'r') as f:
+ manifest = json.load(f)
+ plugin_id = manifest.get('id')
+ if plugin_id:
+ discovered.append(plugin_id)
+ self.plugin_manifests[plugin_id] = manifest
+ self.logger.info(f"Discovered plugin: {plugin_id}")
+ except Exception as e:
+ self.logger.error(f"Error reading manifest in {item}: {e}")
+
+ return discovered
+
+ def load_plugin(self, plugin_id: str) -> bool:
+ """
+ Load a plugin by ID.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if loaded successfully
+ """
+ if plugin_id in self.plugins:
+ self.logger.warning(f"Plugin {plugin_id} already loaded")
+ return True
+
+ manifest = self.plugin_manifests.get(plugin_id)
+ if not manifest:
+ self.logger.error(f"No manifest found for plugin: {plugin_id}")
+ return False
+
+ try:
+ # Add plugin directory to Python path
+ plugin_dir = self.plugins_dir / plugin_id
+ sys.path.insert(0, str(plugin_dir))
+
+ # Import the plugin module
+ entry_point = manifest.get('entry_point', 'manager.py')
+ module_name = entry_point.replace('.py', '')
+ module = importlib.import_module(module_name)
+
+ # Get the plugin class
+ class_name = manifest.get('class_name')
+ if not class_name:
+ self.logger.error(f"No class_name in manifest for {plugin_id}")
+ return False
+
+ plugin_class = getattr(module, class_name)
+
+ # Get plugin config
+ plugin_config = self.config_manager.load_config().get(plugin_id, {})
+
+ # Instantiate the plugin
+ plugin_instance = plugin_class(
+ plugin_id=plugin_id,
+ config=plugin_config,
+ display_manager=self.display_manager,
+ cache_manager=self.cache_manager,
+ plugin_manager=self
+ )
+
+ # Validate configuration
+ if not plugin_instance.validate_config():
+ self.logger.error(f"Config validation failed for {plugin_id}")
+ return False
+
+ self.plugins[plugin_id] = plugin_instance
+ self.logger.info(f"Loaded plugin: {plugin_id} v{manifest.get('version')}")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error loading plugin {plugin_id}: {e}", exc_info=True)
+ return False
+ finally:
+ # Clean up Python path
+ if str(plugin_dir) in sys.path:
+ sys.path.remove(str(plugin_dir))
+
+ def unload_plugin(self, plugin_id: str) -> bool:
+ """
+ Unload a plugin by ID.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if unloaded successfully
+ """
+ if plugin_id not in self.plugins:
+ self.logger.warning(f"Plugin {plugin_id} not loaded")
+ return False
+
+ try:
+ plugin = self.plugins[plugin_id]
+ plugin.cleanup()
+ del self.plugins[plugin_id]
+ self.logger.info(f"Unloaded plugin: {plugin_id}")
+ return True
+ except Exception as e:
+ self.logger.error(f"Error unloading plugin {plugin_id}: {e}")
+ return False
+
+ def reload_plugin(self, plugin_id: str) -> bool:
+ """
+ Reload a plugin (unload and load).
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if reloaded successfully
+ """
+ if plugin_id in self.plugins:
+ if not self.unload_plugin(plugin_id):
+ return False
+ return self.load_plugin(plugin_id)
+
+ def get_plugin(self, plugin_id: str) -> Optional[Any]:
+ """
+ Get a loaded plugin instance.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ Plugin instance or None
+ """
+ return self.plugins.get(plugin_id)
+
+ def get_all_plugins(self) -> Dict[str, Any]:
+ """
+ Get all loaded plugins.
+
+ Returns:
+ Dict of plugin_id: plugin_instance
+ """
+ return self.plugins
+
+ def get_enabled_plugins(self) -> List[str]:
+ """
+ Get list of enabled plugin IDs.
+
+ Returns:
+ List of plugin IDs
+ """
+ return [pid for pid, plugin in self.plugins.items() if plugin.enabled]
+```
+
+### Display Controller Integration
+
+```python
+# Modified src/display_controller.py
+
+class DisplayController:
+ def __init__(self):
+ # ... existing initialization ...
+
+ # Initialize plugin system
+ self.plugin_manager = PluginManager(
+ plugins_dir="plugins",
+ config_manager=self.config_manager,
+ display_manager=self.display_manager,
+ cache_manager=self.cache_manager
+ )
+
+ # Discover and load plugins
+ discovered = self.plugin_manager.discover_plugins()
+ logger.info(f"Discovered {len(discovered)} plugins")
+
+ for plugin_id in discovered:
+ if self.config.get(plugin_id, {}).get('enabled', False):
+ self.plugin_manager.load_plugin(plugin_id)
+
+ # Build available modes from plugins + legacy managers
+ self.available_modes = []
+
+ # Add legacy managers (existing code)
+ if self.clock: self.available_modes.append('clock')
+ # ... etc ...
+
+ # Add plugin modes
+ for plugin_id, plugin in self.plugin_manager.get_all_plugins().items():
+ if plugin.enabled:
+ manifest = self.plugin_manager.plugin_manifests.get(plugin_id, {})
+ display_modes = manifest.get('display_modes', [plugin_id])
+ self.available_modes.extend(display_modes)
+
+ def display_mode(self, mode: str, force_clear: bool = False):
+ """
+ Render a specific mode (legacy or plugin).
+ """
+ # Check if it's a plugin mode
+ for plugin_id, plugin in self.plugin_manager.get_all_plugins().items():
+ manifest = self.plugin_manager.plugin_manifests.get(plugin_id, {})
+ if mode in manifest.get('display_modes', []):
+ plugin.display(force_clear=force_clear)
+ return
+
+ # Fall back to legacy manager handling
+ if mode == 'clock' and self.clock:
+ self.clock.display_time(force_clear=force_clear)
+ # ... etc ...
+```
+
+---
+
+## 3. Plugin Store & Discovery
+
+### Store Architecture (HACS-inspired)
+
+The plugin store will be a simple GitHub-based discovery system where:
+
+1. **Central Registry**: A GitHub repo (`ChuckBuilds/ledmatrix-plugin-registry`) contains a JSON file listing approved plugins
+2. **Plugin Repos**: Individual GitHub repos contain plugin code
+3. **Installation**: Clone/download plugin repos directly to `./plugins/` directory
+4. **Updates**: Git pull or re-download from GitHub
+
+### Registry Structure
+
+```json
+// ledmatrix-plugin-registry/plugins.json
+{
+ "version": "1.0.0",
+ "plugins": [
+ {
+ "id": "clock-simple",
+ "name": "Simple Clock",
+ "description": "A simple clock display with date",
+ "author": "ChuckBuilds",
+ "category": "time",
+ "tags": ["clock", "time", "date"],
+ "repo": "https://github.com/ChuckBuilds/ledmatrix-clock-simple",
+ "branch": "main",
+ "versions": [
+ {
+ "version": "1.0.0",
+ "ledmatrix_min": "2.0.0",
+ "released": "2025-01-15",
+ "download_url": "https://github.com/ChuckBuilds/ledmatrix-clock-simple/archive/refs/tags/v1.0.0.zip"
+ }
+ ],
+ "stars": 45,
+ "downloads": 1234,
+ "last_updated": "2025-01-15",
+ "verified": true
+ },
+ {
+ "id": "weather-animated",
+ "name": "Animated Weather",
+ "description": "Weather display with animated icons",
+ "author": "SomeUser",
+ "category": "weather",
+ "tags": ["weather", "animated", "forecast"],
+ "repo": "https://github.com/SomeUser/ledmatrix-weather-animated",
+ "branch": "main",
+ "versions": [
+ {
+ "version": "2.1.0",
+ "ledmatrix_min": "2.0.0",
+ "released": "2025-01-10",
+ "download_url": "https://github.com/SomeUser/ledmatrix-weather-animated/archive/refs/tags/v2.1.0.zip"
+ }
+ ],
+ "stars": 89,
+ "downloads": 2341,
+ "last_updated": "2025-01-10",
+ "verified": true
+ }
+ ]
+}
+```
+
+### Plugin Store Manager
+
+```python
+# src/plugin_system/store_manager.py
+
+import requests
+import subprocess
+import shutil
+from pathlib import Path
+from typing import List, Dict, Optional
+import logging
+
+class PluginStoreManager:
+ """
+ Manages plugin discovery, installation, and updates from GitHub.
+ """
+
+ REGISTRY_URL = "https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugin-registry/main/plugins.json"
+
+ def __init__(self, plugins_dir: str = "plugins"):
+ self.plugins_dir = Path(plugins_dir)
+ self.logger = logging.getLogger(__name__)
+ self.registry_cache = None
+
+ def fetch_registry(self, force_refresh: bool = False) -> Dict:
+ """
+ Fetch the plugin registry from GitHub.
+
+ Args:
+ force_refresh: Force refresh even if cached
+
+ Returns:
+ Registry data
+ """
+ if self.registry_cache and not force_refresh:
+ return self.registry_cache
+
+ try:
+ response = requests.get(self.REGISTRY_URL, timeout=10)
+ response.raise_for_status()
+ self.registry_cache = response.json()
+ self.logger.info(f"Fetched registry with {len(self.registry_cache['plugins'])} plugins")
+ return self.registry_cache
+ except Exception as e:
+ self.logger.error(f"Error fetching registry: {e}")
+ return {"plugins": []}
+
+ def search_plugins(self, query: str = "", category: str = "", tags: List[str] = []) -> List[Dict]:
+ """
+ Search for plugins in the registry.
+
+ Args:
+ query: Search query string
+ category: Filter by category
+ tags: Filter by tags
+
+ Returns:
+ List of matching plugins
+ """
+ registry = self.fetch_registry()
+ plugins = registry.get('plugins', [])
+
+ results = []
+ for plugin in plugins:
+ # Category filter
+ if category and plugin.get('category') != category:
+ continue
+
+ # Tags filter
+ if tags and not any(tag in plugin.get('tags', []) for tag in tags):
+ continue
+
+ # Query search
+ if query:
+ query_lower = query.lower()
+ if not any([
+ query_lower in plugin.get('name', '').lower(),
+ query_lower in plugin.get('description', '').lower(),
+ query_lower in plugin.get('id', '').lower()
+ ]):
+ continue
+
+ results.append(plugin)
+
+ return results
+
+ def install_plugin(self, plugin_id: str, version: str = "latest") -> bool:
+ """
+ Install a plugin from GitHub.
+
+ Args:
+ plugin_id: Plugin identifier
+ version: Version to install (default: latest)
+
+ Returns:
+ True if installed successfully
+ """
+ registry = self.fetch_registry()
+ plugin_info = next((p for p in registry['plugins'] if p['id'] == plugin_id), None)
+
+ if not plugin_info:
+ self.logger.error(f"Plugin not found in registry: {plugin_id}")
+ return False
+
+ try:
+ # Get version info
+ if version == "latest":
+ version_info = plugin_info['versions'][0] # First is latest
+ else:
+ version_info = next((v for v in plugin_info['versions'] if v['version'] == version), None)
+ if not version_info:
+ self.logger.error(f"Version not found: {version}")
+ return False
+
+ # Get repo URL
+ repo_url = plugin_info['repo']
+
+ # Clone or download
+ plugin_path = self.plugins_dir / plugin_id
+
+ if plugin_path.exists():
+ self.logger.warning(f"Plugin directory already exists: {plugin_id}")
+ shutil.rmtree(plugin_path)
+
+ # Try git clone first
+ try:
+ subprocess.run(
+ ['git', 'clone', '--depth', '1', '--branch', version_info['version'],
+ repo_url, str(plugin_path)],
+ check=True,
+ capture_output=True
+ )
+ self.logger.info(f"Cloned plugin {plugin_id} v{version_info['version']}")
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ # Fall back to download
+ self.logger.info("Git not available, downloading zip...")
+ download_url = version_info['download_url']
+ response = requests.get(download_url, timeout=30)
+ response.raise_for_status()
+
+ # Extract zip (implementation needed)
+ # ...
+
+ # Install Python dependencies
+ requirements_file = plugin_path / "requirements.txt"
+ if requirements_file.exists():
+ subprocess.run(
+ ['pip3', 'install', '--break-system-packages', '-r', str(requirements_file)],
+ check=True
+ )
+ self.logger.info(f"Installed dependencies for {plugin_id}")
+
+ self.logger.info(f"Successfully installed plugin: {plugin_id}")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error installing plugin {plugin_id}: {e}")
+ return False
+
+ def uninstall_plugin(self, plugin_id: str) -> bool:
+ """
+ Uninstall a plugin.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if uninstalled successfully
+ """
+ plugin_path = self.plugins_dir / plugin_id
+
+ if not plugin_path.exists():
+ self.logger.warning(f"Plugin not found: {plugin_id}")
+ return False
+
+ try:
+ shutil.rmtree(plugin_path)
+ self.logger.info(f"Uninstalled plugin: {plugin_id}")
+ return True
+ except Exception as e:
+ self.logger.error(f"Error uninstalling plugin {plugin_id}: {e}")
+ return False
+
+ def update_plugin(self, plugin_id: str) -> bool:
+ """
+ Update a plugin to the latest version.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if updated successfully
+ """
+ plugin_path = self.plugins_dir / plugin_id
+
+ if not plugin_path.exists():
+ self.logger.error(f"Plugin not installed: {plugin_id}")
+ return False
+
+ try:
+ # Try git pull first
+ git_dir = plugin_path / ".git"
+ if git_dir.exists():
+ result = subprocess.run(
+ ['git', '-C', str(plugin_path), 'pull'],
+ capture_output=True,
+ text=True
+ )
+ if result.returncode == 0:
+ self.logger.info(f"Updated plugin {plugin_id} via git pull")
+ return True
+
+ # Fall back to re-download
+ self.logger.info(f"Re-downloading plugin {plugin_id}")
+ return self.install_plugin(plugin_id, version="latest")
+
+ except Exception as e:
+ self.logger.error(f"Error updating plugin {plugin_id}: {e}")
+ return False
+
+ def install_from_url(self, repo_url: str, plugin_id: str = None) -> bool:
+ """
+ Install a plugin directly from a GitHub URL (for custom/unlisted plugins).
+
+ Args:
+ repo_url: GitHub repository URL
+ plugin_id: Optional custom plugin ID (extracted from manifest if not provided)
+
+ Returns:
+ True if installed successfully
+ """
+ try:
+ # Clone to temporary location
+ temp_dir = self.plugins_dir / ".temp_install"
+ if temp_dir.exists():
+ shutil.rmtree(temp_dir)
+
+ subprocess.run(
+ ['git', 'clone', '--depth', '1', repo_url, str(temp_dir)],
+ check=True,
+ capture_output=True
+ )
+
+ # Read manifest to get plugin ID
+ manifest_path = temp_dir / "manifest.json"
+ if not manifest_path.exists():
+ self.logger.error("No manifest.json found in repository")
+ shutil.rmtree(temp_dir)
+ return False
+
+ with open(manifest_path, 'r') as f:
+ manifest = json.load(f)
+
+ plugin_id = plugin_id or manifest.get('id')
+ if not plugin_id:
+ self.logger.error("No plugin ID found in manifest")
+ shutil.rmtree(temp_dir)
+ return False
+
+ # Move to plugins directory
+ final_path = self.plugins_dir / plugin_id
+ if final_path.exists():
+ shutil.rmtree(final_path)
+
+ shutil.move(str(temp_dir), str(final_path))
+
+ # Install dependencies
+ requirements_file = final_path / "requirements.txt"
+ if requirements_file.exists():
+ subprocess.run(
+ ['pip3', 'install', '--break-system-packages', '-r', str(requirements_file)],
+ check=True
+ )
+
+ self.logger.info(f"Installed plugin from URL: {plugin_id}")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error installing from URL: {e}")
+ if temp_dir.exists():
+ shutil.rmtree(temp_dir)
+ return False
+```
+
+---
+
+## 4. Web UI Transformation
+
+### New Web UI Structure
+
+The web UI needs significant updates to support dynamic plugin management:
+
+**New Sections:**
+1. **Plugin Store** - Browse, search, install plugins
+2. **Plugin Manager** - View installed, enable/disable, configure
+3. **Display Rotation** - Drag-and-drop ordering of active displays
+4. **Plugin Settings** - Dynamic configuration UI generated from schemas
+
+### Plugin Store UI (React Component Structure)
+
+```javascript
+// New: templates/src/components/PluginStore.jsx
+
+import React, { useState, useEffect } from 'react';
+
+export default function PluginStore() {
+ const [plugins, setPlugins] = useState([]);
+ const [search, setSearch] = useState('');
+ const [category, setCategory] = useState('all');
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ fetchPlugins();
+ }, []);
+
+ const fetchPlugins = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch('/api/plugins/store/list');
+ const data = await response.json();
+ setPlugins(data.plugins);
+ } catch (error) {
+ console.error('Error fetching plugins:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const installPlugin = async (pluginId) => {
+ try {
+ const response = await fetch('/api/plugins/install', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ plugin_id: pluginId })
+ });
+
+ if (response.ok) {
+ alert('Plugin installed successfully!');
+ // Refresh plugin list
+ fetchPlugins();
+ }
+ } catch (error) {
+ console.error('Error installing plugin:', error);
+ }
+ };
+
+ const filteredPlugins = plugins.filter(plugin => {
+ const matchesSearch = search === '' ||
+ plugin.name.toLowerCase().includes(search.toLowerCase()) ||
+ plugin.description.toLowerCase().includes(search.toLowerCase());
+
+ const matchesCategory = category === 'all' || plugin.category === category;
+
+ return matchesSearch && matchesCategory;
+ });
+
+ return (
+
+
+
Plugin Store
+
+ setSearch(e.target.value)}
+ className="search-input"
+ />
+
+
+
+
+ {loading ? (
+
Loading plugins...
+ ) : (
+
+ {filteredPlugins.map(plugin => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+function PluginCard({ plugin, onInstall }) {
+ return (
+
+
+
{plugin.name}
+ {plugin.verified && ✓ Verified}
+
+
by {plugin.author}
+
{plugin.description}
+
+ ⭐ {plugin.stars}
+ 📥 {plugin.downloads}
+ {plugin.category}
+
+
+ {plugin.tags.map(tag => (
+ {tag}
+ ))}
+
+
+
+ );
+}
+```
+
+### Plugin Manager UI
+
+```javascript
+// New: templates/src/components/PluginManager.jsx
+
+import React, { useState, useEffect } from 'react';
+import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
+
+export default function PluginManager() {
+ const [installedPlugins, setInstalledPlugins] = useState([]);
+ const [rotationOrder, setRotationOrder] = useState([]);
+
+ useEffect(() => {
+ fetchInstalledPlugins();
+ }, []);
+
+ const fetchInstalledPlugins = async () => {
+ try {
+ const response = await fetch('/api/plugins/installed');
+ const data = await response.json();
+ setInstalledPlugins(data.plugins);
+ setRotationOrder(data.rotation_order || []);
+ } catch (error) {
+ console.error('Error fetching installed plugins:', error);
+ }
+ };
+
+ const togglePlugin = async (pluginId, enabled) => {
+ try {
+ await fetch('/api/plugins/toggle', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ plugin_id: pluginId, enabled })
+ });
+ fetchInstalledPlugins();
+ } catch (error) {
+ console.error('Error toggling plugin:', error);
+ }
+ };
+
+ const uninstallPlugin = async (pluginId) => {
+ if (!confirm(`Are you sure you want to uninstall ${pluginId}?`)) {
+ return;
+ }
+
+ try {
+ await fetch('/api/plugins/uninstall', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ plugin_id: pluginId })
+ });
+ fetchInstalledPlugins();
+ } catch (error) {
+ console.error('Error uninstalling plugin:', error);
+ }
+ };
+
+ const handleDragEnd = async (result) => {
+ if (!result.destination) return;
+
+ const newOrder = Array.from(rotationOrder);
+ const [removed] = newOrder.splice(result.source.index, 1);
+ newOrder.splice(result.destination.index, 0, removed);
+
+ setRotationOrder(newOrder);
+
+ try {
+ await fetch('/api/plugins/rotation-order', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ order: newOrder })
+ });
+ } catch (error) {
+ console.error('Error saving rotation order:', error);
+ }
+ };
+
+ return (
+
+
Installed Plugins
+
+
+ {installedPlugins.map(plugin => (
+
+
+
{plugin.name}
+
{plugin.description}
+
v{plugin.version}
+
+
+
+
+
+
+
+ ))}
+
+
+
Display Rotation Order
+
+
+ {(provided) => (
+
+ {rotationOrder.map((pluginId, index) => {
+ const plugin = installedPlugins.find(p => p.id === pluginId);
+ if (!plugin || !plugin.enabled) return null;
+
+ return (
+
+ {(provided) => (
+
+ ⋮⋮
+ {plugin.name}
+ {plugin.display_duration}s
+
+ )}
+
+ );
+ })}
+ {provided.placeholder}
+
+ )}
+
+
+
+ );
+}
+```
+
+### API Endpoints for Web UI
+
+```python
+# New endpoints in web_interface_v2.py
+
+@app.route('/api/plugins/store/list', methods=['GET'])
+def api_plugin_store_list():
+ """Get list of available plugins from store."""
+ try:
+ store_manager = PluginStoreManager()
+ registry = store_manager.fetch_registry()
+ return jsonify({
+ 'status': 'success',
+ 'plugins': registry.get('plugins', [])
+ })
+ except Exception as e:
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/install', methods=['POST'])
+def api_plugin_install():
+ """Install a plugin from the store."""
+ try:
+ data = request.get_json()
+ plugin_id = data.get('plugin_id')
+ version = data.get('version', 'latest')
+
+ store_manager = PluginStoreManager()
+ success = store_manager.install_plugin(plugin_id, version)
+
+ if success:
+ # Reload plugin manager to discover new plugin
+ global plugin_manager
+ plugin_manager.discover_plugins()
+
+ return jsonify({
+ 'status': 'success',
+ 'message': f'Plugin {plugin_id} installed successfully'
+ })
+ else:
+ return jsonify({
+ 'status': 'error',
+ 'message': f'Failed to install plugin {plugin_id}'
+ }), 500
+ except Exception as e:
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/installed', methods=['GET'])
+def api_plugins_installed():
+ """Get list of installed plugins."""
+ try:
+ global plugin_manager
+ plugins = []
+
+ for plugin_id, plugin in plugin_manager.get_all_plugins().items():
+ manifest = plugin_manager.plugin_manifests.get(plugin_id, {})
+ plugins.append({
+ 'id': plugin_id,
+ 'name': manifest.get('name', plugin_id),
+ 'version': manifest.get('version', ''),
+ 'description': manifest.get('description', ''),
+ 'author': manifest.get('author', ''),
+ 'enabled': plugin.enabled,
+ 'display_duration': plugin.get_display_duration()
+ })
+
+ # Get rotation order from config
+ config = config_manager.load_config()
+ rotation_order = config.get('display', {}).get('plugin_rotation_order', [])
+
+ return jsonify({
+ 'status': 'success',
+ 'plugins': plugins,
+ 'rotation_order': rotation_order
+ })
+ except Exception as e:
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/toggle', methods=['POST'])
+def api_plugin_toggle():
+ """Enable or disable a plugin."""
+ try:
+ data = request.get_json()
+ plugin_id = data.get('plugin_id')
+ enabled = data.get('enabled', True)
+
+ # Update config
+ config = config_manager.load_config()
+ if plugin_id not in config:
+ config[plugin_id] = {}
+ config[plugin_id]['enabled'] = enabled
+ config_manager.save_config(config)
+
+ # Reload plugin
+ global plugin_manager
+ if enabled:
+ plugin_manager.load_plugin(plugin_id)
+ else:
+ plugin_manager.unload_plugin(plugin_id)
+
+ return jsonify({
+ 'status': 'success',
+ 'message': f'Plugin {plugin_id} {"enabled" if enabled else "disabled"}'
+ })
+ except Exception as e:
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/uninstall', methods=['POST'])
+def api_plugin_uninstall():
+ """Uninstall a plugin."""
+ try:
+ data = request.get_json()
+ plugin_id = data.get('plugin_id')
+
+ # Unload first
+ global plugin_manager
+ plugin_manager.unload_plugin(plugin_id)
+
+ # Uninstall
+ store_manager = PluginStoreManager()
+ success = store_manager.uninstall_plugin(plugin_id)
+
+ if success:
+ return jsonify({
+ 'status': 'success',
+ 'message': f'Plugin {plugin_id} uninstalled successfully'
+ })
+ else:
+ return jsonify({
+ 'status': 'error',
+ 'message': f'Failed to uninstall plugin {plugin_id}'
+ }), 500
+ except Exception as e:
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/rotation-order', methods=['POST'])
+def api_plugin_rotation_order():
+ """Update plugin rotation order."""
+ try:
+ data = request.get_json()
+ order = data.get('order', [])
+
+ # Update config
+ config = config_manager.load_config()
+ if 'display' not in config:
+ config['display'] = {}
+ config['display']['plugin_rotation_order'] = order
+ config_manager.save_config(config)
+
+ return jsonify({
+ 'status': 'success',
+ 'message': 'Rotation order updated'
+ })
+ except Exception as e:
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/install-from-url', methods=['POST'])
+def api_plugin_install_from_url():
+ """Install a plugin from a custom GitHub URL."""
+ try:
+ data = request.get_json()
+ repo_url = data.get('repo_url')
+
+ if not repo_url:
+ return jsonify({
+ 'status': 'error',
+ 'message': 'repo_url is required'
+ }), 400
+
+ store_manager = PluginStoreManager()
+ success = store_manager.install_from_url(repo_url)
+
+ if success:
+ # Reload plugin manager
+ global plugin_manager
+ plugin_manager.discover_plugins()
+
+ return jsonify({
+ 'status': 'success',
+ 'message': 'Plugin installed from URL successfully'
+ })
+ else:
+ return jsonify({
+ 'status': 'error',
+ 'message': 'Failed to install plugin from URL'
+ }), 500
+ except Exception as e:
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+```
+
+---
+
+## 5. Migration Strategy
+
+### Phase 1: Core Plugin Infrastructure (v2.0.0)
+
+**Goal**: Build plugin system alongside existing managers
+
+**Changes**:
+1. Create `src/plugin_system/` module
+2. Implement `BasePlugin`, `PluginManager`, `PluginStoreManager`
+3. Add `plugins/` directory support
+4. Modify `display_controller.py` to load both legacy and plugins
+5. Update web UI to show plugin store tab
+
+**Backward Compatibility**: 100% - all existing managers still work
+
+### Phase 2: Example Plugins (v2.1.0)
+
+**Goal**: Create reference plugins and migration examples
+
+**Create Official Plugins**:
+1. `ledmatrix-clock-simple` - Simple clock (migrated from existing)
+2. `ledmatrix-weather-basic` - Basic weather display
+3. `ledmatrix-stocks-ticker` - Stock ticker
+4. `ledmatrix-nhl-scores` - NHL scoreboard
+
+**Changes**:
+- Document plugin creation process
+- Create plugin templates
+- Update wiki with plugin development guide
+
+**Backward Compatibility**: 100% - plugins are additive
+
+### Phase 3: Migration Tools (v2.2.0)
+
+**Goal**: Provide tools to migrate existing setups
+
+**Migration Script**:
+```python
+# scripts/migrate_to_plugins.py
+
+import json
+from pathlib import Path
+
+def migrate_config():
+ """
+ Migrate existing config.json to plugin-based format.
+ """
+ config_path = Path("config/config.json")
+ with open(config_path, 'r') as f:
+ config = json.load(f)
+
+ # Create migration plan
+ migration_map = {
+ 'clock': 'clock-simple',
+ 'weather': 'weather-basic',
+ 'stocks': 'stocks-ticker',
+ 'nhl_scoreboard': 'nhl-scores',
+ # ... etc
+ }
+
+ # Install recommended plugins
+ from src.plugin_system.store_manager import PluginStoreManager
+ store = PluginStoreManager()
+
+ for legacy_key, plugin_id in migration_map.items():
+ if config.get(legacy_key, {}).get('enabled', False):
+ print(f"Migrating {legacy_key} to plugin {plugin_id}")
+ store.install_plugin(plugin_id)
+
+ # Migrate config section
+ if legacy_key in config:
+ config[plugin_id] = config[legacy_key]
+
+ # Save migrated config
+ with open("config/config.json.migrated", 'w') as f:
+ json.dump(config, f, indent=2)
+
+ print("Migration complete! Review config.json.migrated")
+
+if __name__ == "__main__":
+ migrate_config()
+```
+
+**User Instructions**:
+```bash
+# 1. Backup existing config
+cp config/config.json config/config.json.backup
+
+# 2. Run migration script
+python3 scripts/migrate_to_plugins.py
+
+# 3. Review migrated config
+cat config/config.json.migrated
+
+# 4. Apply migration
+mv config/config.json.migrated config/config.json
+
+# 5. Restart service
+sudo systemctl restart ledmatrix
+```
+
+### Phase 4: Deprecation (v2.5.0)
+
+**Goal**: Mark legacy managers as deprecated
+
+**Changes**:
+- Add deprecation warnings to legacy managers
+- Update documentation to recommend plugins
+- Create migration guide in wiki
+
+**Backward Compatibility**: 95% - legacy still works but shows warnings
+
+### Phase 5: Plugin-Only (v3.0.0)
+
+**Goal**: Remove legacy managers from core
+
+**Breaking Changes**:
+- Remove hardcoded manager imports from `display_controller.py`
+- Remove legacy manager files from `src/`
+- Package legacy managers as official plugins
+- Update config template to plugin-based format
+
+**Migration Required**: Users must run migration script
+
+---
+
+## 6. Plugin Developer Guidelines
+
+### Creating a New Plugin
+
+#### Step 1: Plugin Structure
+
+```bash
+# Create plugin directory
+mkdir -p plugins/my-plugin
+cd plugins/my-plugin
+
+# Create required files
+touch manifest.json
+touch manager.py
+touch requirements.txt
+touch config_schema.json
+touch README.md
+```
+
+#### Step 2: Manifest
+
+```json
+{
+ "id": "my-plugin",
+ "name": "My Custom Display",
+ "version": "1.0.0",
+ "author": "YourName",
+ "description": "A custom display for LEDMatrix",
+ "homepage": "https://github.com/YourName/ledmatrix-my-plugin",
+ "entry_point": "manager.py",
+ "class_name": "MyPluginManager",
+ "category": "custom",
+ "tags": ["custom", "example"],
+ "compatible_versions": [">=2.0.0"],
+ "ledmatrix_version": "2.0.0",
+ "requires": {
+ "python": ">=3.9",
+ "display_size": {
+ "min_width": 64,
+ "min_height": 32
+ }
+ },
+ "config_schema": "config_schema.json",
+ "assets": {},
+ "update_interval": 60,
+ "default_duration": 15,
+ "display_modes": ["my-plugin"],
+ "api_requirements": []
+}
+```
+
+#### Step 3: Manager Implementation
+
+```python
+# manager.py
+
+from src.plugin_system.base_plugin import BasePlugin
+import time
+
+class MyPluginManager(BasePlugin):
+ """
+ Example plugin that displays custom content.
+ """
+
+ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
+ super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
+
+ # Plugin-specific initialization
+ self.message = config.get('message', 'Hello, World!')
+ self.color = tuple(config.get('color', [255, 255, 255]))
+ self.last_update = 0
+
+ def update(self):
+ """
+ Update plugin data.
+ Called based on update_interval in manifest.
+ """
+ # Fetch or update data here
+ self.last_update = time.time()
+ self.logger.info(f"Updated {self.plugin_id}")
+
+ def display(self, force_clear=False):
+ """
+ Render the plugin display.
+ """
+ if force_clear:
+ self.display_manager.clear()
+
+ # Get display dimensions
+ width = self.display_manager.width
+ height = self.display_manager.height
+
+ # Draw custom content
+ self.display_manager.draw_text(
+ self.message,
+ x=width // 2,
+ y=height // 2,
+ color=self.color,
+ centered=True
+ )
+
+ # Update the physical display
+ self.display_manager.update_display()
+
+ self.logger.debug(f"Displayed {self.plugin_id}")
+```
+
+#### Step 4: Configuration Schema
+
+```json
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "default": true,
+ "description": "Enable or disable this plugin"
+ },
+ "message": {
+ "type": "string",
+ "default": "Hello, World!",
+ "description": "Message to display"
+ },
+ "color": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "default": [255, 255, 255],
+ "description": "RGB color for text"
+ },
+ "display_duration": {
+ "type": "number",
+ "default": 15,
+ "minimum": 1,
+ "description": "How long to display in seconds"
+ }
+ },
+ "required": ["enabled"]
+}
+```
+
+#### Step 5: README
+
+```markdown
+# My Custom Display Plugin
+
+A custom display plugin for LEDMatrix.
+
+## Installation
+
+From the LEDMatrix web UI:
+1. Go to Plugin Store
+2. Search for "My Custom Display"
+3. Click Install
+
+Or install from command line:
+```bash
+cd /path/to/LEDMatrix
+python3 -c "from src.plugin_system.store_manager import PluginStoreManager; PluginStoreManager().install_plugin('my-plugin')"
+```
+
+## Configuration
+
+Add to `config/config.json`:
+
+```json
+{
+ "my-plugin": {
+ "enabled": true,
+ "message": "Hello, World!",
+ "color": [255, 255, 255],
+ "display_duration": 15
+ }
+}
+```
+
+## Options
+
+- `message` (string): Text to display
+- `color` (array): RGB color [R, G, B]
+- `display_duration` (number): Display time in seconds
+
+## License
+
+MIT
+```
+
+### Publishing a Plugin
+
+#### Step 1: Create GitHub Repository
+
+```bash
+# Initialize git
+git init
+git add .
+git commit -m "Initial commit"
+
+# Create on GitHub and push
+git remote add origin https://github.com/YourName/ledmatrix-my-plugin.git
+git push -u origin main
+```
+
+#### Step 2: Create Release
+
+```bash
+# Tag version
+git tag -a v1.0.0 -m "Version 1.0.0"
+git push origin v1.0.0
+```
+
+Create release on GitHub with:
+- Release notes
+- Installation instructions
+- Screenshots/GIFs
+
+#### Step 3: Submit to Registry
+
+Create pull request to `ChuckBuilds/ledmatrix-plugin-registry` adding your plugin:
+
+```json
+{
+ "id": "my-plugin",
+ "name": "My Custom Display",
+ "description": "A custom display for LEDMatrix",
+ "author": "YourName",
+ "category": "custom",
+ "tags": ["custom", "example"],
+ "repo": "https://github.com/YourName/ledmatrix-my-plugin",
+ "branch": "main",
+ "versions": [
+ {
+ "version": "1.0.0",
+ "ledmatrix_min": "2.0.0",
+ "released": "2025-01-15",
+ "download_url": "https://github.com/YourName/ledmatrix-my-plugin/archive/refs/tags/v1.0.0.zip"
+ }
+ ],
+ "verified": false
+}
+```
+
+---
+
+## 7. Technical Implementation Details
+
+### Configuration Management
+
+**Old Way** (monolithic):
+```json
+{
+ "clock": { "enabled": true },
+ "weather": { "enabled": true },
+ "nhl_scoreboard": { "enabled": true }
+}
+```
+
+**New Way** (plugin-based):
+```json
+{
+ "plugins": {
+ "clock-simple": { "enabled": true },
+ "weather-basic": { "enabled": true },
+ "nhl-scores": { "enabled": true }
+ },
+ "display": {
+ "plugin_rotation_order": [
+ "clock-simple",
+ "weather-basic",
+ "nhl-scores"
+ ]
+ }
+}
+```
+
+### Dependency Management
+
+Each plugin manages its own dependencies via `requirements.txt`:
+
+```txt
+# plugins/nhl-scores/requirements.txt
+requests>=2.28.0
+pytz>=2022.1
+```
+
+During installation:
+```python
+subprocess.run([
+ 'pip3', 'install',
+ '--break-system-packages',
+ '-r', 'plugins/nhl-scores/requirements.txt'
+])
+```
+
+### Asset Management
+
+Plugins can include their own assets:
+
+```
+plugins/nhl-scores/
+├── assets/
+│ ├── logos/
+│ │ ├── TB.png
+│ │ └── DAL.png
+│ └── fonts/
+│ └── sports.bdf
+```
+
+Access in plugin:
+```python
+def get_asset_path(self, relative_path):
+ """Get absolute path to plugin asset."""
+ plugin_dir = Path(__file__).parent
+ return plugin_dir / "assets" / relative_path
+
+# Usage
+logo_path = self.get_asset_path("logos/TB.png")
+```
+
+### Caching Integration
+
+Plugins use the shared cache manager:
+
+```python
+def update(self):
+ cache_key = f"{self.plugin_id}_data"
+
+ # Try to get cached data
+ cached = self.cache_manager.get(cache_key, max_age=3600)
+ if cached:
+ self.data = cached
+ return
+
+ # Fetch fresh data
+ self.data = self._fetch_from_api()
+
+ # Cache it
+ self.cache_manager.set(cache_key, self.data)
+```
+
+### Inter-Plugin Communication
+
+Plugins can communicate through the plugin manager:
+
+```python
+# In plugin A
+other_plugin = self.plugin_manager.get_plugin('plugin-b')
+if other_plugin:
+ data = other_plugin.get_shared_data()
+
+# In plugin B
+def get_shared_data(self):
+ return {'temperature': 72, 'conditions': 'sunny'}
+```
+
+### Error Handling
+
+Plugins should handle errors gracefully:
+
+```python
+def display(self, force_clear=False):
+ try:
+ # Plugin logic
+ self._render_content()
+ except Exception as e:
+ self.logger.error(f"Error in display: {e}", exc_info=True)
+ # Show error message on display
+ self.display_manager.clear()
+ self.display_manager.draw_text(
+ f"Error: {self.plugin_id}",
+ x=5, y=15,
+ color=(255, 0, 0)
+ )
+ self.display_manager.update_display()
+```
+
+---
+
+## 8. Best Practices & Standards
+
+### Plugin Best Practices
+
+1. **Follow BasePlugin Interface**: Always extend `BasePlugin` and implement required methods
+2. **Validate Configuration**: Use config schemas to validate user settings
+3. **Handle Errors Gracefully**: Never crash the entire system
+4. **Use Logging**: Log important events and errors
+5. **Cache Appropriately**: Use cache manager for API responses
+6. **Clean Up Resources**: Implement `cleanup()` for resource disposal
+7. **Document Everything**: Provide clear README and code comments
+8. **Test on Hardware**: Test on actual Raspberry Pi with LED matrix
+9. **Version Properly**: Use semantic versioning
+10. **Respect Resources**: Be mindful of CPU, memory, and API quotas
+
+### Coding Standards
+
+```python
+# Good: Clear, documented, error-handled
+class MyPlugin(BasePlugin):
+ """
+ Custom plugin that displays messages.
+
+ Configuration:
+ message (str): Message to display
+ color (tuple): RGB color tuple
+ """
+
+ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
+ super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
+ self.message = config.get('message', 'Default')
+ self.validate_color(config.get('color', (255, 255, 255)))
+
+ def validate_color(self, color):
+ """Validate color is proper RGB tuple."""
+ if not isinstance(color, (list, tuple)) or len(color) != 3:
+ raise ValueError("Color must be RGB tuple")
+ if not all(0 <= c <= 255 for c in color):
+ raise ValueError("Color values must be 0-255")
+ self.color = tuple(color)
+
+ def update(self):
+ """Update plugin data."""
+ try:
+ # Update logic
+ pass
+ except Exception as e:
+ self.logger.error(f"Update failed: {e}")
+
+ def display(self, force_clear=False):
+ """Display plugin content."""
+ try:
+ if force_clear:
+ self.display_manager.clear()
+
+ self.display_manager.draw_text(
+ self.message,
+ x=5, y=15,
+ color=self.color
+ )
+ self.display_manager.update_display()
+ except Exception as e:
+ self.logger.error(f"Display failed: {e}")
+```
+
+### Testing Guidelines
+
+```python
+# test/test_my_plugin.py
+
+import unittest
+from unittest.mock import Mock, MagicMock
+import sys
+sys.path.insert(0, 'plugins/my-plugin')
+from manager import MyPluginManager
+
+class TestMyPlugin(unittest.TestCase):
+ def setUp(self):
+ """Set up test fixtures."""
+ self.config = {
+ 'enabled': True,
+ 'message': 'Test',
+ 'color': [255, 0, 0]
+ }
+ self.display_manager = Mock()
+ self.cache_manager = Mock()
+ self.plugin_manager = Mock()
+
+ self.plugin = MyPluginManager(
+ plugin_id='my-plugin',
+ config=self.config,
+ display_manager=self.display_manager,
+ cache_manager=self.cache_manager,
+ plugin_manager=self.plugin_manager
+ )
+
+ def test_initialization(self):
+ """Test plugin initializes correctly."""
+ self.assertEqual(self.plugin.message, 'Test')
+ self.assertEqual(self.plugin.color, (255, 0, 0))
+
+ def test_display_calls_manager(self):
+ """Test display method calls display manager."""
+ self.plugin.display()
+ self.display_manager.draw_text.assert_called_once()
+ self.display_manager.update_display.assert_called_once()
+
+ def test_invalid_color_raises_error(self):
+ """Test invalid color configuration raises error."""
+ bad_config = {'color': [300, 0, 0]}
+ with self.assertRaises(ValueError):
+ MyPluginManager(
+ 'test', bad_config,
+ self.display_manager,
+ self.cache_manager,
+ self.plugin_manager
+ )
+
+if __name__ == '__main__':
+ unittest.main()
+```
+
+---
+
+## 9. Security Considerations
+
+### Plugin Verification
+
+**Verified Plugins**:
+- Reviewed by maintainers
+- Follow best practices
+- No known security issues
+- Marked with ✓ badge in store
+
+**Unverified Plugins**:
+- User-contributed
+- Not reviewed
+- Install at own risk
+- Show warning before installation
+
+### Code Review Process
+
+Before marking a plugin as verified:
+
+1. **Code Review**: Manual inspection of code
+2. **Dependency Audit**: Check all requirements
+3. **Permission Check**: Verify minimal permissions
+4. **API Key Safety**: Ensure no hardcoded secrets
+5. **Resource Usage**: Check for excessive CPU/memory use
+6. **Testing**: Test on actual hardware
+
+### Sandboxing Considerations
+
+Current implementation has NO sandboxing. Plugins run with same permissions as main process.
+
+**Future Enhancement** (v3.x):
+- Run plugins in separate processes
+- Limit file system access
+- Rate limit API calls
+- Monitor resource usage
+- Kill misbehaving plugins
+
+### User Guidelines
+
+**For Users**:
+1. Only install plugins from trusted sources
+2. Review plugin permissions before installing
+3. Check plugin ratings and reviews
+4. Keep plugins updated
+5. Report suspicious plugins
+
+**For Developers**:
+1. Never include hardcoded API keys
+2. Minimize required permissions
+3. Use secure API practices
+4. Validate all user inputs
+5. Handle errors gracefully
+
+---
+
+## 10. Implementation Roadmap
+
+### Timeline
+
+**Phase 1: Foundation (Weeks 1-3)**
+- Create plugin system infrastructure
+- Implement BasePlugin, PluginManager, StoreManager
+- Update display_controller for plugin support
+- Basic web UI for plugin management
+
+**Phase 2: Example Plugins (Weeks 4-5)**
+- Create 4-5 reference plugins
+- Migrate existing managers as examples
+- Write developer documentation
+- Create plugin templates
+
+**Phase 3: Store Integration (Weeks 6-7)**
+- Set up plugin registry repo
+- Implement store API
+- Build web UI for store
+- Add search and filtering
+
+**Phase 4: Migration Tools (Weeks 8-9)**
+- Create migration script
+- Test with existing installations
+- Write migration guide
+- Update documentation
+
+**Phase 5: Testing & Polish (Weeks 10-12)**
+- Comprehensive testing on Pi hardware
+- Bug fixes
+- Performance optimization
+- Documentation improvements
+
+**Phase 6: Release v2.0.0 (Week 13)**
+- Tag release
+- Publish documentation
+- Announce to community
+- Gather feedback
+
+### Success Metrics
+
+**Technical**:
+- 100% backward compatibility in v2.0
+- <100ms plugin load time
+- <5% performance overhead
+- Zero critical bugs in first month
+
+**User Adoption**:
+- 10+ community-created plugins in 3 months
+- 50%+ of users install at least one plugin
+- Positive feedback on ease of use
+
+**Developer Experience**:
+- Clear documentation
+- Responsive to plugin dev questions
+- Regular updates to plugin system
+
+---
+
+## Appendix A: File Structure Comparison
+
+### Before (v1.x)
+
+```
+LEDMatrix/
+├── src/
+│ ├── clock.py
+│ ├── weather_manager.py
+│ ├── stock_manager.py
+│ ├── nhl_managers.py
+│ ├── nba_managers.py
+│ ├── mlb_manager.py
+│ └── ... (40+ manager files)
+├── config/
+│ ├── config.json (650+ lines)
+│ └── config.template.json
+└── web_interface_v2.py (hardcoded imports)
+```
+
+### After (v2.0+)
+
+```
+LEDMatrix/
+├── src/
+│ ├── plugin_system/
+│ │ ├── __init__.py
+│ │ ├── base_plugin.py
+│ │ ├── plugin_manager.py
+│ │ └── store_manager.py
+│ ├── display_controller.py (plugin-aware)
+│ └── ... (core components only)
+├── plugins/
+│ ├── clock-simple/
+│ ├── weather-basic/
+│ ├── nhl-scores/
+│ └── ... (user-installed plugins)
+├── config/
+│ └── config.json (minimal core config)
+└── web_interface_v2.py (dynamic plugin loading)
+```
+
+---
+
+## Appendix B: Example Plugin: NHL Scoreboard
+
+Complete example of migrating NHL scoreboard to plugin:
+
+**Directory Structure**:
+```
+plugins/nhl-scores/
+├── manifest.json
+├── manager.py
+├── requirements.txt
+├── config_schema.json
+├── assets/
+│ └── logos/
+│ ├── TB.png
+│ └── ... (NHL team logos)
+└── README.md
+```
+
+**manifest.json**:
+```json
+{
+ "id": "nhl-scores",
+ "name": "NHL Scoreboard",
+ "version": "1.0.0",
+ "author": "ChuckBuilds",
+ "description": "Display NHL game scores and schedules",
+ "homepage": "https://github.com/ChuckBuilds/ledmatrix-nhl-scores",
+ "entry_point": "manager.py",
+ "class_name": "NHLScoresPlugin",
+ "category": "sports",
+ "tags": ["nhl", "hockey", "sports", "scores"],
+ "compatible_versions": [">=2.0.0"],
+ "requires": {
+ "python": ">=3.9",
+ "display_size": {
+ "min_width": 64,
+ "min_height": 32
+ }
+ },
+ "config_schema": "config_schema.json",
+ "assets": {
+ "logos": "assets/logos/"
+ },
+ "update_interval": 60,
+ "default_duration": 30,
+ "display_modes": ["nhl_live", "nhl_recent", "nhl_upcoming"],
+ "api_requirements": ["ESPN API"]
+}
+```
+
+**requirements.txt**:
+```txt
+requests>=2.28.0
+pytz>=2022.1
+```
+
+**manager.py** (abbreviated):
+```python
+from src.plugin_system.base_plugin import BasePlugin
+import requests
+from datetime import datetime
+from pathlib import Path
+
+class NHLScoresPlugin(BasePlugin):
+ """NHL Scoreboard plugin for LEDMatrix."""
+
+ ESPN_NHL_URL = "https://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard"
+
+ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
+ super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
+
+ self.favorite_teams = config.get('favorite_teams', [])
+ self.show_favorite_only = config.get('show_favorite_teams_only', True)
+ self.games = []
+
+ def update(self):
+ """Fetch NHL games from ESPN API."""
+ cache_key = f"{self.plugin_id}_games"
+
+ # Try cache first
+ cached = self.cache_manager.get(cache_key, max_age=60)
+ if cached:
+ self.games = cached
+ self.logger.debug("Using cached NHL data")
+ return
+
+ try:
+ response = requests.get(self.ESPN_NHL_URL, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+
+ self.games = self._process_games(data.get('events', []))
+
+ # Cache the results
+ self.cache_manager.set(cache_key, self.games)
+
+ self.logger.info(f"Fetched {len(self.games)} NHL games")
+ except Exception as e:
+ self.logger.error(f"Error fetching NHL data: {e}")
+
+ def _process_games(self, events):
+ """Process raw ESPN data into game objects."""
+ games = []
+ for event in events:
+ # Extract game info
+ # ... (implementation)
+ pass
+ return games
+
+ def display(self, force_clear=False):
+ """Display NHL scores."""
+ if force_clear:
+ self.display_manager.clear()
+
+ if not self.games:
+ self._show_no_games()
+ return
+
+ # Show first game (or cycle through)
+ game = self.games[0]
+ self._display_game(game)
+
+ self.display_manager.update_display()
+
+ def _display_game(self, game):
+ """Render a single game."""
+ # Load team logos
+ away_logo = self._get_logo(game['away_team'])
+ home_logo = self._get_logo(game['home_team'])
+
+ # Draw logos and scores
+ # ... (implementation)
+
+ def _get_logo(self, team_abbr):
+ """Get team logo from assets."""
+ logo_path = Path(__file__).parent / "assets" / "logos" / f"{team_abbr}.png"
+ if logo_path.exists():
+ return logo_path
+ return None
+
+ def _show_no_games(self):
+ """Show 'no games' message."""
+ self.display_manager.draw_text(
+ "No NHL games",
+ x=5, y=15,
+ color=(255, 255, 255)
+ )
+```
+
+---
+
+## Conclusion
+
+This specification outlines a comprehensive transformation of the LEDMatrix project into a modular, extensible platform. The plugin architecture enables:
+
+- **User Extensibility**: Anyone can create custom displays
+- **Easy Distribution**: GitHub-based store for discovery and installation
+- **Backward Compatibility**: Gradual migration path for existing users
+- **Community Growth**: Lower barrier to contribution
+- **Better Maintenance**: Smaller core, cleaner codebase
+
+The gradual migration approach ensures existing users aren't disrupted while new users benefit from the improved architecture.
+
+**Next Steps**:
+1. Review and refine this specification
+2. Begin Phase 1 implementation
+3. Create prototype plugins for testing
+4. Gather community feedback
+5. Iterate and improve
+
+---
+
+**Document Version**: 1.0.0
+**Last Updated**: 2025-01-09
+**Author**: AI Assistant (Claude)
+**Status**: Draft for Review
+
diff --git a/PLUGIN_QUICK_REFERENCE.md b/PLUGIN_QUICK_REFERENCE.md
new file mode 100644
index 000000000..47489cf7e
--- /dev/null
+++ b/PLUGIN_QUICK_REFERENCE.md
@@ -0,0 +1,286 @@
+# LEDMatrix Plugin Architecture - Quick Reference
+
+## Overview
+
+Transform LEDMatrix into a modular, plugin-based system where users can create, share, and install custom displays via a GitHub-based store (similar to HACS for Home Assistant).
+
+## Key Decisions
+
+✅ **Gradual Migration**: Existing managers stay, plugins added alongside
+✅ **Migration Required**: Breaking changes in v3.0, tools provided
+✅ **GitHub Store**: Simple discovery, packages from repos
+✅ **Plugin Location**: `./plugins/` directory
+
+## File Structure
+
+```
+LEDMatrix/
+├── src/
+│ └── plugin_system/
+│ ├── base_plugin.py # Plugin interface
+│ ├── plugin_manager.py # Load/unload plugins
+│ └── store_manager.py # Install from GitHub
+├── plugins/
+│ ├── clock-simple/
+│ │ ├── manifest.json # Metadata
+│ │ ├── manager.py # Main plugin class
+│ │ ├── requirements.txt # Dependencies
+│ │ ├── config_schema.json # Validation
+│ │ └── README.md
+│ └── nhl-scores/
+│ └── ... (same structure)
+└── config/config.json # Plugin configs
+```
+
+## Creating a Plugin
+
+### 1. Minimal Plugin Structure
+
+**manifest.json**:
+```json
+{
+ "id": "my-plugin",
+ "name": "My Display",
+ "version": "1.0.0",
+ "author": "YourName",
+ "entry_point": "manager.py",
+ "class_name": "MyPlugin",
+ "category": "custom"
+}
+```
+
+**manager.py**:
+```python
+from src.plugin_system.base_plugin import BasePlugin
+
+class MyPlugin(BasePlugin):
+ def update(self):
+ # Fetch data
+ pass
+
+ def display(self, force_clear=False):
+ # Render to display
+ self.display_manager.draw_text("Hello!", x=5, y=15)
+ self.display_manager.update_display()
+```
+
+### 2. Configuration
+
+**config_schema.json**:
+```json
+{
+ "type": "object",
+ "properties": {
+ "enabled": {"type": "boolean", "default": true},
+ "message": {"type": "string", "default": "Hello"}
+ }
+}
+```
+
+**User's config.json**:
+```json
+{
+ "my-plugin": {
+ "enabled": true,
+ "message": "Custom text",
+ "display_duration": 15
+ }
+}
+```
+
+### 3. Publishing
+
+```bash
+# Create repo
+git init
+git add .
+git commit -m "Initial commit"
+git remote add origin https://github.com/YourName/ledmatrix-my-plugin
+git push -u origin main
+
+# Tag release
+git tag v1.0.0
+git push origin v1.0.0
+
+# Submit to registry (PR to ChuckBuilds/ledmatrix-plugin-registry)
+```
+
+## Using Plugins
+
+### Web UI
+
+1. **Browse Store**: Plugin Store tab → Search/filter
+2. **Install**: Click "Install" button
+3. **Configure**: Plugin Manager → Click ⚙️ Configure
+4. **Enable/Disable**: Toggle switch
+5. **Reorder**: Drag and drop in rotation list
+
+### API
+
+```python
+# Install plugin
+POST /api/plugins/install
+{"plugin_id": "my-plugin"}
+
+# Install from custom URL
+POST /api/plugins/install-from-url
+{"repo_url": "https://github.com/User/plugin"}
+
+# List installed
+GET /api/plugins/installed
+
+# Toggle
+POST /api/plugins/toggle
+{"plugin_id": "my-plugin", "enabled": true}
+```
+
+### Command Line
+
+```python
+from src.plugin_system.store_manager import PluginStoreManager
+
+store = PluginStoreManager()
+
+# Install
+store.install_plugin('nhl-scores')
+
+# Install from URL
+store.install_from_url('https://github.com/User/plugin')
+
+# Update
+store.update_plugin('nhl-scores')
+
+# Uninstall
+store.uninstall_plugin('nhl-scores')
+```
+
+## Migration Path
+
+### Phase 1: v2.0.0 (Plugin Infrastructure)
+- Plugin system alongside existing managers
+- 100% backward compatible
+- Web UI shows plugin store
+
+### Phase 2: v2.1.0 (Example Plugins)
+- Reference plugins created
+- Migration examples
+- Developer docs
+
+### Phase 3: v2.2.0 (Migration Tools)
+- Auto-migration script
+- Config converter
+- Testing tools
+
+### Phase 4: v2.5.0 (Deprecation)
+- Warnings on legacy managers
+- Migration guide
+- 95% backward compatible
+
+### Phase 5: v3.0.0 (Plugin-Only)
+- Legacy managers removed from core
+- Packaged as official plugins
+- **Breaking change - migration required**
+
+## Quick Migration
+
+```bash
+# 1. Backup
+cp config/config.json config/config.json.backup
+
+# 2. Run migration
+python3 scripts/migrate_to_plugins.py
+
+# 3. Review
+cat config/config.json.migrated
+
+# 4. Apply
+mv config/config.json.migrated config/config.json
+
+# 5. Restart
+sudo systemctl restart ledmatrix
+```
+
+## Plugin Registry Structure
+
+**ChuckBuilds/ledmatrix-plugin-registry/plugins.json**:
+```json
+{
+ "plugins": [
+ {
+ "id": "clock-simple",
+ "name": "Simple Clock",
+ "author": "ChuckBuilds",
+ "category": "time",
+ "repo": "https://github.com/ChuckBuilds/ledmatrix-clock-simple",
+ "versions": [
+ {
+ "version": "1.0.0",
+ "ledmatrix_min": "2.0.0",
+ "download_url": "https://github.com/.../v1.0.0.zip"
+ }
+ ],
+ "verified": true
+ }
+ ]
+}
+```
+
+## Benefits
+
+### For Users
+- ✅ Install only what you need
+- ✅ Easy discovery of new displays
+- ✅ Simple updates
+- ✅ Community-created content
+
+### For Developers
+- ✅ Lower barrier to contribute
+- ✅ No need to fork core repo
+- ✅ Faster iteration
+- ✅ Clear plugin API
+
+### For Maintainers
+- ✅ Smaller core codebase
+- ✅ Less merge conflicts
+- ✅ Community handles custom displays
+- ✅ Easier to review changes
+
+## What's Missing?
+
+This specification covers the technical architecture. Additional considerations:
+
+1. **Sandboxing**: Current design has no isolation (future enhancement)
+2. **Resource Limits**: No CPU/memory limits per plugin (future)
+3. **Plugin Ratings**: Registry needs rating/review system
+4. **Auto-Updates**: Manual update only (could add auto-update)
+5. **Dependency Conflicts**: No automatic resolution
+6. **Version Pinning**: Limited version constraint checking
+7. **Plugin Testing**: No automated testing framework
+8. **Marketplace**: No paid plugins (all free/open source)
+
+## Next Steps
+
+1. ✅ Review this specification
+2. Start Phase 1 implementation
+3. Create first 3-4 example plugins
+4. Set up plugin registry repo
+5. Build web UI components
+6. Test on Pi hardware
+7. Release v2.0.0 alpha
+
+## Questions to Resolve
+
+Before implementing, consider:
+
+1. Should we support plugin dependencies (plugin A requires plugin B)?
+2. How to handle breaking changes in core display_manager API?
+3. Should plugins be able to add new web UI pages?
+4. What about plugins that need hardware beyond LED matrix?
+5. How to prevent malicious plugins?
+6. Should there be plugin quotas (max API calls, etc.)?
+7. How to handle plugin conflicts (two clocks competing)?
+
+---
+
+**See PLUGIN_ARCHITECTURE_SPEC.md for full details**
+
From 1cd2980a8170ad50ecc851bb272688e45423efbb Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Wed, 8 Oct 2025 21:16:24 -0400
Subject: [PATCH 002/736] docs: Add base classes and code reuse section to
plugin spec
- Detailed explanation of base class philosophy
- Sports and Hockey plugin base class implementations
- API versioning and compatibility checking
- Examples of using base classes in plugins
- Migration strategy for existing base classes
- When to use vs not use base classes
- Clear benefits and trade-offs documented
---
PLUGIN_ARCHITECTURE_SPEC.md | 514 ++++++++++++++++++++++++++++++++++++
1 file changed, 514 insertions(+)
diff --git a/PLUGIN_ARCHITECTURE_SPEC.md b/PLUGIN_ARCHITECTURE_SPEC.md
index dfd4c225e..ccfb1c895 100644
--- a/PLUGIN_ARCHITECTURE_SPEC.md
+++ b/PLUGIN_ARCHITECTURE_SPEC.md
@@ -480,6 +480,520 @@ class DisplayController:
# ... etc ...
```
+### Base Classes and Code Reuse
+
+#### Philosophy: Core Provides Stable Plugin API
+
+The core LEDMatrix provides stable base classes and utilities for common plugin types. This approach balances code reuse with plugin independence.
+
+#### Plugin API Base Classes
+
+```
+src/
+├── plugin_system/
+│ ├── base_plugin.py # Core plugin interface (required)
+│ └── base_classes/ # Optional base classes for common use cases
+│ ├── __init__.py
+│ ├── sports_plugin.py # Generic sports displays
+│ ├── hockey_plugin.py # Hockey-specific features
+│ ├── basketball_plugin.py # Basketball-specific features
+│ ├── baseball_plugin.py # Baseball-specific features
+│ ├── football_plugin.py # Football-specific features
+│ └── display_helpers.py # Common rendering utilities
+```
+
+#### Sports Plugin Base Class
+
+```python
+# src/plugin_system/base_classes/sports_plugin.py
+
+from src.plugin_system.base_plugin import BasePlugin
+from typing import List, Dict, Any, Optional
+import requests
+
+class SportsPlugin(BasePlugin):
+ """
+ Base class for sports-related plugins.
+
+ API Version: 1.0.0
+ Stability: Stable - maintains backward compatibility
+
+ Provides common functionality:
+ - Favorite team filtering
+ - ESPN API integration
+ - Standard game data structures
+ - Common rendering methods
+ """
+
+ API_VERSION = "1.0.0"
+
+ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
+ super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
+
+ # Standard sports plugin configuration
+ self.favorite_teams = config.get('favorite_teams', [])
+ self.show_favorite_only = config.get('show_favorite_teams_only', True)
+ self.show_odds = config.get('show_odds', True)
+ self.show_records = config.get('show_records', True)
+ self.logo_dir = config.get('logo_dir', 'assets/sports/logos')
+
+ def filter_by_favorites(self, games: List[Dict]) -> List[Dict]:
+ """
+ Filter games to show only favorite teams.
+
+ Args:
+ games: List of game dictionaries
+
+ Returns:
+ Filtered list of games
+ """
+ if not self.show_favorite_only or not self.favorite_teams:
+ return games
+
+ return [g for g in games if self._is_favorite_game(g)]
+
+ def _is_favorite_game(self, game: Dict) -> bool:
+ """Check if game involves a favorite team."""
+ home_team = game.get('home_team', '')
+ away_team = game.get('away_team', '')
+ return home_team in self.favorite_teams or away_team in self.favorite_teams
+
+ def fetch_espn_data(self, sport: str, endpoint: str = "scoreboard",
+ params: Dict = None) -> Optional[Dict]:
+ """
+ Fetch data from ESPN API.
+
+ Args:
+ sport: Sport identifier (e.g., 'hockey/nhl', 'basketball/nba')
+ endpoint: API endpoint (default: 'scoreboard')
+ params: Query parameters
+
+ Returns:
+ API response data or None on error
+ """
+ url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{endpoint}"
+ cache_key = f"espn_{sport}_{endpoint}"
+
+ # Try cache first
+ cached = self.cache_manager.get(cache_key, max_age=60)
+ if cached:
+ return cached
+
+ try:
+ response = requests.get(url, params=params, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+
+ # Cache the response
+ self.cache_manager.set(cache_key, data)
+
+ return data
+ except Exception as e:
+ self.logger.error(f"Error fetching ESPN data: {e}")
+ return None
+
+ def render_team_logo(self, team_abbr: str, x: int, y: int, size: int = 16):
+ """
+ Render a team logo at specified position.
+
+ Args:
+ team_abbr: Team abbreviation
+ x, y: Position on display
+ size: Logo size in pixels
+ """
+ from pathlib import Path
+ from PIL import Image
+
+ # Try plugin assets first
+ logo_path = Path(self.plugin_id) / "assets" / "logos" / f"{team_abbr}.png"
+
+ # Fall back to core assets
+ if not logo_path.exists():
+ logo_path = Path(self.logo_dir) / f"{team_abbr}.png"
+
+ if logo_path.exists():
+ try:
+ logo = Image.open(logo_path)
+ logo = logo.resize((size, size), Image.LANCZOS)
+ self.display_manager.image.paste(logo, (x, y))
+ except Exception as e:
+ self.logger.error(f"Error rendering logo for {team_abbr}: {e}")
+
+ def render_score(self, away_team: str, away_score: int,
+ home_team: str, home_score: int,
+ x: int, y: int):
+ """
+ Render a game score in standard format.
+
+ Args:
+ away_team, away_score: Away team info
+ home_team, home_score: Home team info
+ x, y: Position on display
+ """
+ # Render away team
+ self.render_team_logo(away_team, x, y)
+ self.display_manager.draw_text(
+ f"{away_score}",
+ x=x + 20, y=y + 4,
+ color=(255, 255, 255)
+ )
+
+ # Render home team
+ self.render_team_logo(home_team, x + 40, y)
+ self.display_manager.draw_text(
+ f"{home_score}",
+ x=x + 60, y=y + 4,
+ color=(255, 255, 255)
+ )
+```
+
+#### Hockey Plugin Base Class
+
+```python
+# src/plugin_system/base_classes/hockey_plugin.py
+
+from src.plugin_system.base_classes.sports_plugin import SportsPlugin
+from typing import Dict, List, Optional
+
+class HockeyPlugin(SportsPlugin):
+ """
+ Base class for hockey plugins (NHL, NCAA Hockey, etc).
+
+ API Version: 1.0.0
+ Provides hockey-specific features:
+ - Period handling
+ - Power play indicators
+ - Shots on goal display
+ """
+
+ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
+ super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
+
+ # Hockey-specific config
+ self.show_shots = config.get('show_shots_on_goal', True)
+ self.show_power_play = config.get('show_power_play', True)
+
+ def fetch_hockey_games(self, league: str = "nhl") -> List[Dict]:
+ """
+ Fetch hockey games from ESPN.
+
+ Args:
+ league: League identifier (nhl, college-hockey)
+
+ Returns:
+ List of standardized game dictionaries
+ """
+ sport = f"hockey/{league}"
+ data = self.fetch_espn_data(sport)
+
+ if not data:
+ return []
+
+ return self._parse_hockey_games(data.get('events', []))
+
+ def _parse_hockey_games(self, events: List[Dict]) -> List[Dict]:
+ """
+ Parse ESPN hockey events into standardized format.
+
+ Returns:
+ List of dicts with keys: id, home_team, away_team, home_score,
+ away_score, period, clock, status, power_play, shots
+ """
+ games = []
+
+ for event in events:
+ try:
+ competition = event['competitions'][0]
+
+ game = {
+ 'id': event['id'],
+ 'home_team': competition['competitors'][0]['team']['abbreviation'],
+ 'away_team': competition['competitors'][1]['team']['abbreviation'],
+ 'home_score': int(competition['competitors'][0]['score']),
+ 'away_score': int(competition['competitors'][1]['score']),
+ 'status': competition['status']['type']['state'],
+ 'period': competition.get('period', 0),
+ 'clock': competition.get('displayClock', ''),
+ 'power_play': self._extract_power_play(competition),
+ 'shots': self._extract_shots(competition)
+ }
+
+ games.append(game)
+ except (KeyError, IndexError, ValueError) as e:
+ self.logger.error(f"Error parsing hockey game: {e}")
+ continue
+
+ return games
+
+ def render_hockey_game(self, game: Dict, x: int = 0, y: int = 0):
+ """
+ Render a hockey game in standard format.
+
+ Args:
+ game: Game dictionary (from _parse_hockey_games)
+ x, y: Position on display
+ """
+ # Render score
+ self.render_score(
+ game['away_team'], game['away_score'],
+ game['home_team'], game['home_score'],
+ x, y
+ )
+
+ # Render period and clock
+ if game['status'] == 'in':
+ period_text = f"P{game['period']} {game['clock']}"
+ self.display_manager.draw_text(
+ period_text,
+ x=x, y=y + 20,
+ color=(255, 255, 0)
+ )
+
+ # Render power play indicator
+ if self.show_power_play and game.get('power_play'):
+ self.display_manager.draw_text(
+ "PP",
+ x=x + 80, y=y + 20,
+ color=(255, 0, 0)
+ )
+
+ # Render shots
+ if self.show_shots and game.get('shots'):
+ shots_text = f"SOG: {game['shots']['away']}-{game['shots']['home']}"
+ self.display_manager.draw_text(
+ shots_text,
+ x=x, y=y + 28,
+ color=(200, 200, 200),
+ small_font=True
+ )
+
+ def _extract_power_play(self, competition: Dict) -> Optional[str]:
+ """Extract power play information from competition data."""
+ # Implementation details...
+ return None
+
+ def _extract_shots(self, competition: Dict) -> Optional[Dict]:
+ """Extract shots on goal from competition data."""
+ # Implementation details...
+ return None
+```
+
+#### Using Base Classes in Plugins
+
+**Example: NHL Scores Plugin**
+
+```python
+# plugins/nhl-scores/manager.py
+
+from src.plugin_system.base_classes.hockey_plugin import HockeyPlugin
+
+class NHLScoresPlugin(HockeyPlugin):
+ """
+ NHL Scores plugin using stable hockey base class.
+
+ Inherits all hockey functionality, just needs to implement
+ update() and display() for NHL-specific behavior.
+ """
+
+ def update(self):
+ """Fetch NHL games using inherited method."""
+ self.games = self.fetch_hockey_games(league="nhl")
+
+ # Filter to favorites
+ if self.show_favorite_only:
+ self.games = self.filter_by_favorites(self.games)
+
+ self.logger.info(f"Fetched {len(self.games)} NHL games")
+
+ def display(self, force_clear=False):
+ """Display NHL games using inherited rendering."""
+ if force_clear:
+ self.display_manager.clear()
+
+ if not self.games:
+ self._show_no_games()
+ return
+
+ # Show first game using inherited method
+ self.render_hockey_game(self.games[0], x=0, y=5)
+
+ self.display_manager.update_display()
+
+ def _show_no_games(self):
+ """Show no games message."""
+ self.display_manager.draw_text(
+ "No NHL games",
+ x=5, y=15,
+ color=(255, 255, 255)
+ )
+```
+
+**Example: Custom Hockey Plugin (NCAA Hockey)**
+
+```python
+# plugins/ncaa-hockey/manager.py
+
+from src.plugin_system.base_classes.hockey_plugin import HockeyPlugin
+
+class NCAAHockeyPlugin(HockeyPlugin):
+ """
+ NCAA Hockey plugin - different league, same base class.
+ """
+
+ def update(self):
+ """Fetch NCAA hockey games."""
+ self.games = self.fetch_hockey_games(league="college-hockey")
+ self.games = self.filter_by_favorites(self.games)
+
+ def display(self, force_clear=False):
+ """Display using inherited hockey rendering."""
+ if force_clear:
+ self.display_manager.clear()
+
+ if self.games:
+ # Use inherited rendering method
+ self.render_hockey_game(self.games[0], x=0, y=5)
+
+ self.display_manager.update_display()
+```
+
+#### API Versioning and Compatibility
+
+**Manifest declares required API version:**
+
+```json
+{
+ "id": "nhl-scores",
+ "plugin_api_version": "1.0.0",
+ "compatible_versions": [">=2.0.0"]
+}
+```
+
+**Plugin Manager checks compatibility:**
+
+```python
+# In plugin_manager.py
+
+def load_plugin(self, plugin_id: str) -> bool:
+ manifest = self.plugin_manifests.get(plugin_id)
+
+ # Check API compatibility
+ required_api = manifest.get('plugin_api_version', '1.0.0')
+
+ from src.plugin_system.base_classes.sports_plugin import SportsPlugin
+ current_api = SportsPlugin.API_VERSION
+
+ if not self._is_api_compatible(required_api, current_api):
+ self.logger.error(
+ f"Plugin {plugin_id} requires API {required_api}, "
+ f"but {current_api} is available. Please update plugin or core."
+ )
+ return False
+
+ # Continue loading...
+ return True
+
+def _is_api_compatible(self, required: str, current: str) -> bool:
+ """
+ Check if required API version is compatible with current.
+ Uses semantic versioning: MAJOR.MINOR.PATCH
+
+ - Same major version = compatible
+ - Different major version = incompatible (breaking changes)
+ """
+ req_major = int(required.split('.')[0])
+ cur_major = int(current.split('.')[0])
+
+ return req_major == cur_major
+```
+
+#### Handling API Changes
+
+**Non-Breaking Changes (Minor/Patch versions):**
+
+```python
+# v1.0.0 -> v1.1.0 (new optional parameter)
+class HockeyPlugin:
+ def render_hockey_game(self, game, x=0, y=0, show_penalties=False):
+ # Added optional parameter, old code still works
+ pass
+```
+
+**Breaking Changes (Major version):**
+
+```python
+# v1.x.x
+class HockeyPlugin:
+ def render_hockey_game(self, game, x=0, y=0):
+ pass
+
+# v2.0.0 (breaking change)
+class HockeyPlugin:
+ API_VERSION = "2.0.0"
+
+ def render_hockey_game(self, game, position=(0, 0), style="default"):
+ # Changed signature - plugins need updates
+ pass
+```
+
+Plugins requiring v1.x would fail to load with v2.0.0 core, prompting user to update.
+
+#### Benefits of This Approach
+
+1. **No Code Duplication**: Plugins import from core
+2. **Consistent Behavior**: All hockey plugins render the same way
+3. **Easy Updates**: Bug fixes in base classes benefit all plugins
+4. **Smaller Plugins**: No need to bundle common code
+5. **Clear API Contract**: Versioned, stable interface
+6. **Flexibility**: Plugins can override any method
+
+#### When NOT to Use Base Classes
+
+Plugins should implement BasePlugin directly when:
+
+- Creating completely custom displays (no common patterns)
+- Needing full control over every aspect
+- Prototyping new display types
+- External data sources (not ESPN)
+
+Example:
+```python
+# plugins/custom-animation/manager.py
+
+from src.plugin_system.base_plugin import BasePlugin
+
+class CustomAnimationPlugin(BasePlugin):
+ """Fully custom plugin - doesn't need sports base classes."""
+
+ def update(self):
+ # Custom data fetching
+ pass
+
+ def display(self, force_clear=False):
+ # Custom rendering
+ pass
+```
+
+#### Migration Strategy for Existing Base Classes
+
+**Current base classes** (`src/base_classes/`):
+- `sports.py`
+- `hockey.py`
+- `basketball.py`
+- etc.
+
+**Phase 1**: Create new plugin-specific base classes
+- Keep old ones for backward compatibility
+- New base classes in `src/plugin_system/base_classes/`
+
+**Phase 2**: Migrate existing managers
+- Legacy managers still use old base classes
+- New plugins use new base classes
+
+**Phase 3**: Deprecate old base classes (v3.0)
+- Remove old `src/base_classes/`
+- All code uses plugin system base classes
+
---
## 3. Plugin Store & Discovery
From 318c1faafc5c8f891c074956288ca08a02fa5235 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Wed, 8 Oct 2025 21:19:05 -0400
Subject: [PATCH 003/736] organize files, remove wiki, test folder, docs folder
---
LEDMatrix.wiki | 1 -
docs/AP_TOP_25_DYNAMIC_TEAMS.md | 260 ++++
.../AP_TOP_25_IMPLEMENTATION_SUMMARY.md | 0
docs/BACKGROUND_SERVICE_GUIDE.md | 314 ++++
.../BACKGROUND_SERVICE_README.md | 0
docs/CACHE_STRATEGY.md | 173 +++
docs/CONFIGURATION_REFERENCE.md | 517 +++++++
docs/CUSTOM_FEEDS_GUIDE.md | 245 ++++
docs/DYNAMIC_DURATION_GUIDE.md | 177 +++
.../DYNAMIC_DURATION_STOCKS_IMPLEMENTATION.md | 189 +++
docs/GRACEFUL_UPDATES.md | 146 ++
docs/Home.md | 84 ++
docs/INSTALLATION_GUIDE.md | 350 +++++
docs/MANAGER_GUIDE_COMPREHENSIVE.md | 1285 +++++++++++++++++
docs/MILB_TROUBLESHOOTING.md | 178 +++
docs/NEWS_MANAGER_README.md | 245 ++++
docs/TEAM_ABBREVIATIONS_AND_LEAGUE_SLUGS.md | 442 ++++++
docs/WEB_INTERFACE_INSTALLATION.md | 379 +++++
docs/WEB_INTERFACE_V2_ENHANCED_SUMMARY.md | 156 ++
docs/WEB_UI_COMPLETE_GUIDE.md | 798 ++++++++++
docs/WIKI_ARCHITECTURE.md | 587 ++++++++
docs/WIKI_CONFIGURATION.md | 654 +++++++++
docs/WIKI_DISPLAY_MANAGERS.md | 501 +++++++
docs/WIKI_HOME.md | 96 ++
docs/WIKI_QUICK_START.md | 407 ++++++
docs/WIKI_TROUBLESHOOTING.md | 516 +++++++
docs/cache_management.md | 231 +++
docs/dynamic_duration.md | 243 ++++
.../clear_nhl_cache.py | 0
.../test_config_loading.py | 0
.../test_config_simple.py | 0
.../test_config_validation.py | 0
.../test_static_image.py | 0
.../test_static_image_simple.py | 0
34 files changed, 9173 insertions(+), 1 deletion(-)
delete mode 160000 LEDMatrix.wiki
create mode 100644 docs/AP_TOP_25_DYNAMIC_TEAMS.md
rename AP_TOP_25_IMPLEMENTATION_SUMMARY.md => docs/AP_TOP_25_IMPLEMENTATION_SUMMARY.md (100%)
create mode 100644 docs/BACKGROUND_SERVICE_GUIDE.md
rename BACKGROUND_SERVICE_README.md => docs/BACKGROUND_SERVICE_README.md (100%)
create mode 100644 docs/CACHE_STRATEGY.md
create mode 100644 docs/CONFIGURATION_REFERENCE.md
create mode 100644 docs/CUSTOM_FEEDS_GUIDE.md
create mode 100644 docs/DYNAMIC_DURATION_GUIDE.md
create mode 100644 docs/DYNAMIC_DURATION_STOCKS_IMPLEMENTATION.md
create mode 100644 docs/GRACEFUL_UPDATES.md
create mode 100644 docs/Home.md
create mode 100644 docs/INSTALLATION_GUIDE.md
create mode 100644 docs/MANAGER_GUIDE_COMPREHENSIVE.md
create mode 100644 docs/MILB_TROUBLESHOOTING.md
create mode 100644 docs/NEWS_MANAGER_README.md
create mode 100644 docs/TEAM_ABBREVIATIONS_AND_LEAGUE_SLUGS.md
create mode 100644 docs/WEB_INTERFACE_INSTALLATION.md
create mode 100644 docs/WEB_INTERFACE_V2_ENHANCED_SUMMARY.md
create mode 100644 docs/WEB_UI_COMPLETE_GUIDE.md
create mode 100644 docs/WIKI_ARCHITECTURE.md
create mode 100644 docs/WIKI_CONFIGURATION.md
create mode 100644 docs/WIKI_DISPLAY_MANAGERS.md
create mode 100644 docs/WIKI_HOME.md
create mode 100644 docs/WIKI_QUICK_START.md
create mode 100644 docs/WIKI_TROUBLESHOOTING.md
create mode 100644 docs/cache_management.md
create mode 100644 docs/dynamic_duration.md
rename clear_nhl_cache.py => scripts/clear_nhl_cache.py (100%)
rename test_config_loading.py => test/test_config_loading.py (100%)
rename test_config_simple.py => test/test_config_simple.py (100%)
rename test_config_validation.py => test/test_config_validation.py (100%)
rename test_static_image.py => test/test_static_image.py (100%)
rename test_static_image_simple.py => test/test_static_image_simple.py (100%)
diff --git a/LEDMatrix.wiki b/LEDMatrix.wiki
deleted file mode 160000
index fbd8d89a1..000000000
--- a/LEDMatrix.wiki
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit fbd8d89a186e5757d1785737b0ee4c03ad442dbf
diff --git a/docs/AP_TOP_25_DYNAMIC_TEAMS.md b/docs/AP_TOP_25_DYNAMIC_TEAMS.md
new file mode 100644
index 000000000..ef8e3b1a1
--- /dev/null
+++ b/docs/AP_TOP_25_DYNAMIC_TEAMS.md
@@ -0,0 +1,260 @@
+# AP Top 25 Dynamic Teams Feature
+
+## Overview
+
+The AP Top 25 Dynamic Teams feature allows you to automatically follow the current AP Top 25 ranked teams in NCAA Football without manually updating your configuration each week. This feature dynamically resolves special team names like `"AP_TOP_25"` into the actual team abbreviations that are currently ranked in the AP Top 25.
+
+## How It Works
+
+When you add `"AP_TOP_25"` to your `favorite_teams` list, the system:
+
+1. **Fetches Current Rankings**: Automatically retrieves the latest AP Top 25 rankings from ESPN API
+2. **Resolves Dynamic Names**: Converts `"AP_TOP_25"` into the actual team abbreviations (e.g., `["UGA", "MICH", "OSU", ...]`)
+3. **Updates Automatically**: Rankings are cached for 1 hour and automatically refresh
+4. **Filters Games**: Only shows games involving the current AP Top 25 teams
+
+## Supported Dynamic Team Names
+
+| Dynamic Team Name | Description | Teams Returned |
+|------------------|-------------|----------------|
+| `"AP_TOP_25"` | Current AP Top 25 teams | All 25 ranked teams |
+| `"AP_TOP_10"` | Current AP Top 10 teams | Top 10 ranked teams |
+| `"AP_TOP_5"` | Current AP Top 5 teams | Top 5 ranked teams |
+
+## Configuration Examples
+
+### Basic AP Top 25 Configuration
+
+```json
+{
+ "ncaa_fb_scoreboard": {
+ "enabled": true,
+ "show_favorite_teams_only": true,
+ "favorite_teams": [
+ "AP_TOP_25"
+ ]
+ }
+}
+```
+
+### Mixed Regular and Dynamic Teams
+
+```json
+{
+ "ncaa_fb_scoreboard": {
+ "enabled": true,
+ "show_favorite_teams_only": true,
+ "favorite_teams": [
+ "UGA",
+ "AUB",
+ "AP_TOP_25"
+ ]
+ }
+}
+```
+
+### Top 10 Teams Only
+
+```json
+{
+ "ncaa_fb_scoreboard": {
+ "enabled": true,
+ "show_favorite_teams_only": true,
+ "favorite_teams": [
+ "AP_TOP_10"
+ ]
+ }
+}
+```
+
+## Technical Details
+
+### Caching Strategy
+
+- **Cache Duration**: Rankings are cached for 1 hour to reduce API calls
+- **Automatic Refresh**: Cache automatically expires and refreshes
+- **Manual Clear**: Cache can be cleared programmatically if needed
+
+### API Integration
+
+- **Data Source**: ESPN College Football Rankings API
+- **Update Frequency**: Rankings update weekly (typically Tuesday)
+- **Fallback**: If rankings unavailable, dynamic teams resolve to empty list
+
+### Performance Impact
+
+- **Minimal Overhead**: Only fetches rankings when dynamic teams are used
+- **Efficient Caching**: 1-hour cache reduces API calls
+- **Background Updates**: Rankings fetched in background, doesn't block display
+
+## Usage Examples
+
+### Example 1: Follow All Top 25 Teams
+
+```json
+{
+ "ncaa_fb_scoreboard": {
+ "enabled": true,
+ "show_favorite_teams_only": true,
+ "favorite_teams": ["AP_TOP_25"],
+ "display_modes": {
+ "ncaa_fb_live": true,
+ "ncaa_fb_recent": true,
+ "ncaa_fb_upcoming": true
+ }
+ }
+}
+```
+
+**Result**: Shows all live, recent, and upcoming games for the current AP Top 25 teams.
+
+### Example 2: Follow Your Team + Top 25
+
+```json
+{
+ "ncaa_fb_scoreboard": {
+ "enabled": true,
+ "show_favorite_teams_only": true,
+ "favorite_teams": [
+ "UGA", // Your favorite team
+ "AP_TOP_25" // Plus all top 25 teams
+ ]
+ }
+}
+```
+
+**Result**: Shows games for UGA plus all current AP Top 25 teams.
+
+### Example 3: Top 10 Teams Only
+
+```json
+{
+ "ncaa_fb_scoreboard": {
+ "enabled": true,
+ "show_favorite_teams_only": true,
+ "favorite_teams": ["AP_TOP_10"]
+ }
+}
+```
+
+**Result**: Shows games only for the current AP Top 10 teams.
+
+## Integration with Other Features
+
+### Odds Ticker
+
+The odds ticker automatically respects dynamic team resolution:
+
+```json
+{
+ "odds_ticker": {
+ "enabled": true,
+ "enabled_leagues": ["ncaa_fb"],
+ "show_favorite_teams_only": true
+ },
+ "ncaa_fb_scoreboard": {
+ "favorite_teams": ["AP_TOP_25"]
+ }
+}
+```
+
+### Leaderboard
+
+The leaderboard will show current AP Top 25 teams when using dynamic teams.
+
+### Background Service
+
+Dynamic teams work seamlessly with the background service for efficient data fetching.
+
+## Troubleshooting
+
+### Common Issues
+
+1. **No Games Showing**
+ - Check if `"show_favorite_teams_only": true` is set
+ - Verify `"enabled": true` for the sport
+ - Check logs for ranking fetch errors
+
+2. **Rankings Not Updating**
+ - Rankings update weekly (typically Tuesday)
+ - Check ESPN API availability
+ - Clear cache if needed
+
+3. **Too Many Games**
+ - Use `"AP_TOP_10"` or `"AP_TOP_5"` instead of `"AP_TOP_25"`
+ - Adjust `"recent_games_to_show"` and `"upcoming_games_to_show"`
+
+### Debug Information
+
+Check the logs for dynamic team resolution:
+
+```
+INFO: Resolved dynamic teams: ['AP_TOP_25'] -> ['UGA', 'MICH', 'OSU', ...]
+INFO: Favorite teams: ['UGA', 'MICH', 'OSU', ...]
+```
+
+### Cache Management
+
+To force refresh rankings:
+
+```python
+from src.dynamic_team_resolver import DynamicTeamResolver
+resolver = DynamicTeamResolver()
+resolver.clear_cache()
+```
+
+## Best Practices
+
+1. **Use Sparingly**: AP_TOP_25 can generate many games - consider AP_TOP_10 or AP_TOP_5
+2. **Combine with Regular Teams**: Mix dynamic teams with your specific favorites
+3. **Monitor Performance**: Check logs for API call frequency
+4. **Seasonal Usage**: Most useful during college football season
+
+## Future Enhancements
+
+Potential future dynamic team types:
+
+- `"PLAYOFF_TEAMS"`: Teams in playoff contention
+- `"CONFERENCE_LEADERS"`: Conference leaders
+- `"HEISMAN_CANDIDATES"`: Teams with Heisman candidates
+- `"RIVALRY_GAMES"`: Traditional rivalry matchups
+
+## API Reference
+
+### DynamicTeamResolver Class
+
+```python
+from src.dynamic_team_resolver import DynamicTeamResolver
+
+# Initialize resolver
+resolver = DynamicTeamResolver()
+
+# Resolve dynamic teams
+teams = resolver.resolve_teams(["UGA", "AP_TOP_25"], 'ncaa_fb')
+
+# Check if team is dynamic
+is_dynamic = resolver.is_dynamic_team("AP_TOP_25")
+
+# Get available dynamic teams
+available = resolver.get_available_dynamic_teams()
+
+# Clear cache
+resolver.clear_cache()
+```
+
+### Convenience Function
+
+```python
+from src.dynamic_team_resolver import resolve_dynamic_teams
+
+# Simple resolution
+teams = resolve_dynamic_teams(["UGA", "AP_TOP_25"], 'ncaa_fb')
+```
+
+## Changelog
+
+- **v1.0.0**: Initial implementation of AP Top 25 dynamic teams
+- Added support for AP_TOP_25, AP_TOP_10, AP_TOP_5
+- Integrated with existing favorite teams system
+- Added caching and error handling
+- Added comprehensive documentation
diff --git a/AP_TOP_25_IMPLEMENTATION_SUMMARY.md b/docs/AP_TOP_25_IMPLEMENTATION_SUMMARY.md
similarity index 100%
rename from AP_TOP_25_IMPLEMENTATION_SUMMARY.md
rename to docs/AP_TOP_25_IMPLEMENTATION_SUMMARY.md
diff --git a/docs/BACKGROUND_SERVICE_GUIDE.md b/docs/BACKGROUND_SERVICE_GUIDE.md
new file mode 100644
index 000000000..85a233e17
--- /dev/null
+++ b/docs/BACKGROUND_SERVICE_GUIDE.md
@@ -0,0 +1,314 @@
+# Background Service Guide
+
+## Overview
+
+The Background Service is a high-performance threading system that prevents the main display loop from blocking during data fetching operations. This feature significantly improves responsiveness and user experience by offloading heavy API calls to background threads.
+
+## How It Works
+
+### Core Architecture
+
+The Background Service uses a **ThreadPoolExecutor** to manage concurrent data fetching operations:
+
+```
+┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
+│ Main Display │ │ Background │ │ API Endpoints │
+│ Loop │◄──►│ Service │◄──►│ (ESPN, etc.) │
+│ │ │ │ │ │
+│ • Non-blocking │ │ • Thread Pool │ │ • Season Data │
+│ • Responsive │ │ • Request Queue │ │ • Live Scores │
+│ • Fast Updates │ │ • Caching │ │ • Standings │
+└─────────────────┘ └──────────────────┘ └─────────────────┘
+```
+
+### Key Components
+
+#### 1. BackgroundDataService Class
+- **Location**: `src/background_data_service.py`
+- **Purpose**: Manages the thread pool and request queuing
+- **Features**:
+ - Configurable worker threads (default: 3)
+ - Request timeout handling (default: 30 seconds)
+ - Automatic retry logic (default: 3 attempts)
+ - Result caching and validation
+
+#### 2. Sport Manager Integration
+Each sport manager (NFL, NBA, NHL, etc.) includes:
+- Background service initialization
+- Request tracking and management
+- Graceful fallback to synchronous fetching
+- Comprehensive logging
+
+#### 3. Configuration System
+- Per-sport configuration options
+- Enable/disable functionality
+- Customizable worker counts and timeouts
+- Priority-based request handling
+
+## Configuration
+
+### Basic Setup
+
+Add background service configuration to any sport manager in `config.json`:
+
+```json
+{
+ "nfl_scoreboard": {
+ "enabled": true,
+ "background_service": {
+ "enabled": true,
+ "max_workers": 3,
+ "request_timeout": 30,
+ "max_retries": 3,
+ "priority": 2
+ }
+ }
+}
+```
+
+### Configuration Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enabled` | boolean | `true` | Enable/disable background service |
+| `max_workers` | integer | `3` | Number of background threads |
+| `request_timeout` | integer | `30` | Timeout in seconds for API requests |
+| `max_retries` | integer | `3` | Number of retry attempts on failure |
+| `priority` | integer | `2` | Request priority (1=highest, 5=lowest) |
+
+### Supported Sport Managers
+
+All major sport managers support background service:
+
+- **NFL Manager** - Professional football
+- **NCAAFB Manager** - College football
+- **NBA Manager** - Professional basketball
+- **NHL Manager** - Professional hockey
+- **MLB Manager** - Professional baseball
+- **MiLB Manager** - Minor league baseball
+- **Soccer Manager** - Multiple soccer leagues
+- **Leaderboard Manager** - Multi-sport standings
+- **Odds Ticker Manager** - Live betting odds
+
+## How Data Fetching Works
+
+### Traditional (Synchronous) Approach
+```
+1. Display loop requests data
+2. API call blocks for 5-10 seconds
+3. Display freezes during fetch
+4. Data returned and displayed
+5. Repeat process
+```
+
+### Background Service Approach
+```
+1. Display loop requests data
+2. Check cache for existing data
+3. Return cached data immediately (if available)
+4. Submit background fetch request
+5. Background thread fetches fresh data
+6. Cache updated for next request
+7. Display remains responsive throughout
+```
+
+### Request Flow
+
+```mermaid
+graph TD
+ A[Sport Manager Update] --> B{Cache Available?}
+ B -->|Yes| C[Return Cached Data]
+ B -->|No| D[Return Partial Data]
+ D --> E[Submit Background Request]
+ E --> F[Background Thread Pool]
+ F --> G[API Call with Retry Logic]
+ G --> H[Data Validation]
+ H --> I[Update Cache]
+ I --> J[Callback Notification]
+ C --> K[Display Update]
+ J --> K
+```
+
+## Performance Benefits
+
+### Before Background Service
+- **Display blocking**: 5-10 seconds during data fetch
+- **Poor user experience**: Frozen display during updates
+- **Limited responsiveness**: Only one API call at a time
+- **Cache misses**: No fallback for failed requests
+
+### After Background Service
+- **Non-blocking**: Display remains responsive
+- **Immediate response**: Cached data returned instantly
+- **Concurrent fetching**: Multiple API calls simultaneously
+- **Graceful degradation**: Fallback to partial data
+- **Better caching**: Intelligent cache management
+
+## Error Handling
+
+### Robust Error Management
+The background service includes comprehensive error handling:
+
+1. **API Failures**: Automatic retry with exponential backoff
+2. **Timeout Handling**: Configurable request timeouts
+3. **Data Validation**: Ensures API responses are properly formatted
+4. **Fallback Mechanisms**: Graceful degradation when services fail
+5. **Logging**: Detailed logging for debugging and monitoring
+
+### Error Recovery
+```python
+# Example error handling in background service
+try:
+ response = requests.get(url, timeout=timeout)
+ data = response.json()
+
+ # Validate data structure
+ if not isinstance(data, dict) or 'events' not in data:
+ raise ValueError("Invalid API response format")
+
+except requests.Timeout:
+ logger.warning(f"Request timeout for {url}")
+ # Retry with exponential backoff
+
+except requests.RequestException as e:
+ logger.error(f"Request failed: {e}")
+ # Fall back to cached data
+```
+
+## Monitoring and Debugging
+
+### Logging
+The background service provides detailed logging:
+
+```
+13:22:13.614 - INFO:src.background_data_service:[NFL] Background service enabled with 3 workers
+13:22:13.615 - INFO:src.background_data_service:[NFL] Submitting background fetch request
+13:22:13.616 - INFO:src.background_data_service:[NFL] Background fetch completed successfully
+13:22:13.617 - INFO:src.background_data_service:[NFL] Cache updated with fresh data
+```
+
+### Performance Metrics
+Monitor these key metrics:
+- **Request completion time**: How long API calls take
+- **Cache hit rate**: Percentage of requests served from cache
+- **Error rate**: Frequency of failed requests
+- **Worker utilization**: How efficiently threads are used
+
+## Best Practices
+
+### Configuration Recommendations
+
+1. **Worker Count**: Start with 3 workers, adjust based on system performance
+2. **Timeout Settings**: Use 30 seconds for most APIs, adjust for slow endpoints
+3. **Retry Logic**: 3 retries with exponential backoff works well
+4. **Priority Levels**: Use priority 1 for live games, priority 3 for historical data
+
+### Performance Optimization
+
+1. **Enable for All Sports**: Use background service for all sport managers
+2. **Monitor Cache Usage**: Ensure cache is being utilized effectively
+3. **Adjust Workers**: Increase workers for systems with good network performance
+4. **Use Appropriate Timeouts**: Balance between responsiveness and reliability
+
+### Troubleshooting
+
+#### Common Issues
+
+1. **High Memory Usage**: Reduce `max_workers` if system is memory-constrained
+2. **Slow Performance**: Increase `max_workers` for better concurrency
+3. **API Rate Limits**: Increase `request_timeout` and reduce `max_workers`
+4. **Cache Issues**: Check cache directory permissions and disk space
+
+#### Debug Mode
+Enable debug logging to troubleshoot issues:
+
+```json
+{
+ "logging": {
+ "level": "DEBUG"
+ }
+}
+```
+
+## Advanced Features
+
+### Priority-Based Queuing
+Requests can be prioritized based on importance:
+- **Priority 1**: Live games (highest priority)
+- **Priority 2**: Recent games
+- **Priority 3**: Upcoming games
+- **Priority 4**: Historical data
+- **Priority 5**: Standings and statistics
+
+### Dynamic Worker Scaling
+The system can automatically adjust worker count based on:
+- System load
+- Network performance
+- API response times
+- Error rates
+
+### Intelligent Caching
+Advanced caching strategies:
+- **TTL-based expiration**: Data expires after configurable time
+- **Smart invalidation**: Cache invalidated when new data available
+- **Partial data caching**: Store incomplete data for immediate display
+- **Compression**: Compress cached data to save disk space
+
+## Migration Guide
+
+### Enabling Background Service
+
+1. **Update Configuration**: Add background service config to sport managers
+2. **Test Individual Sports**: Enable one sport at a time
+3. **Monitor Performance**: Check logs and system performance
+4. **Enable All Sports**: Once tested, enable for all sport managers
+
+### Example Migration
+
+```json
+// Before
+{
+ "nfl_scoreboard": {
+ "enabled": true,
+ "update_interval_seconds": 3600
+ }
+}
+
+// After
+{
+ "nfl_scoreboard": {
+ "enabled": true,
+ "update_interval_seconds": 3600,
+ "background_service": {
+ "enabled": true,
+ "max_workers": 3,
+ "request_timeout": 30,
+ "max_retries": 3,
+ "priority": 2
+ }
+ }
+}
+```
+
+## Future Enhancements
+
+### Planned Features
+- **Priority-based request queuing**: Intelligent request prioritization
+- **Dynamic worker scaling**: Automatic worker count adjustment
+- **Performance analytics dashboard**: Real-time performance monitoring
+- **Advanced caching strategies**: More sophisticated cache management
+- **API rate limit handling**: Intelligent rate limit management
+
+### Contributing
+To contribute to the background service:
+1. Fork the repository
+2. Create a feature branch
+3. Implement your changes
+4. Add tests and documentation
+5. Submit a pull request
+
+## Conclusion
+
+The Background Service represents a significant improvement in LEDMatrix performance and user experience. By offloading data fetching to background threads, the display remains responsive while ensuring fresh data is always available.
+
+For questions or issues, please refer to the troubleshooting section or create an issue in the GitHub repository.
diff --git a/BACKGROUND_SERVICE_README.md b/docs/BACKGROUND_SERVICE_README.md
similarity index 100%
rename from BACKGROUND_SERVICE_README.md
rename to docs/BACKGROUND_SERVICE_README.md
diff --git a/docs/CACHE_STRATEGY.md b/docs/CACHE_STRATEGY.md
new file mode 100644
index 000000000..ff8b4a50f
--- /dev/null
+++ b/docs/CACHE_STRATEGY.md
@@ -0,0 +1,173 @@
+# LEDMatrix Cache Strategy Analysis
+
+## Current Implementation
+
+Your LEDMatrix system uses a sophisticated multi-tier caching strategy that balances data freshness with API efficiency.
+
+### Cache Duration Categories
+
+#### 1. **Ultra Time-Sensitive Data (15-60 seconds)**
+- **Live Sports Scores**: Now respects sport-specific `live_update_interval` configuration
+ - Soccer live data: Uses `soccer_scoreboard.live_update_interval` (default: 60 seconds)
+ - NFL live data: Uses `nfl_scoreboard.live_update_interval` (default: 60 seconds)
+ - NHL live data: Uses `nhl_scoreboard.live_update_interval` (default: 60 seconds)
+ - NBA live data: Uses `nba_scoreboard.live_update_interval` (default: 60 seconds)
+ - MLB live data: Uses `mlb.live_update_interval` (default: 60 seconds)
+ - NCAA sports: Use respective `live_update_interval` configurations (default: 60 seconds)
+- **Current Weather**: 5 minutes (300 seconds)
+
+#### 2. **Market Data (5-10 minutes)**
+- **Stocks**: 10 minutes (600 seconds) - market hours aware
+- **Crypto**: 5 minutes (300 seconds) - 24/7 trading
+- **Stock News**: 1 hour (3600 seconds)
+
+#### 3. **Sports Data (5 minutes to 24 hours)**
+- **Recent Games**: 5 minutes (300 seconds)
+- **Upcoming Games**: 1 hour (3600 seconds)
+- **Season Schedules**: 24 hours (86400 seconds)
+- **Team Information**: 1 week (604800 seconds)
+
+#### 4. **Static Data (1 week to 30 days)**
+- **Team Logos**: 30 days (2592000 seconds)
+- **Configuration Data**: 1 week (604800 seconds)
+
+### Smart Cache Invalidation
+
+Beyond time limits, the system uses content-based invalidation:
+
+```python
+def has_data_changed(self, data_type: str, new_data: Dict[str, Any]) -> bool:
+ """Check if data has changed from cached version."""
+```
+
+- **Weather**: Compares temperature and conditions
+- **Stocks**: Compares prices (only during market hours)
+- **Sports**: Compares scores, game status, inning details
+- **News**: Compares headlines and article IDs
+
+### Market-Aware Caching
+
+For stocks, the system extends cache duration during off-hours:
+
+```python
+def _is_market_open(self) -> bool:
+ """Check if the US stock market is currently open."""
+ # Only invalidates cache during market hours
+```
+
+## Enhanced Cache Strategy
+
+### Sport-Specific Live Update Intervals
+
+The cache manager now automatically respects the `live_update_interval` configuration for each sport:
+
+```python
+def get_sport_live_interval(self, sport_key: str) -> int:
+ """Get the live_update_interval for a specific sport from config."""
+ config = self.config_manager.get_config()
+ sport_config = config.get(f"{sport_key}_scoreboard", {})
+ return sport_config.get("live_update_interval", 30)
+```
+
+### Automatic Sport Detection
+
+The cache manager automatically detects the sport from cache keys:
+
+```python
+def get_sport_key_from_cache_key(self, key: str) -> Optional[str]:
+ """Extract sport key from cache key to determine appropriate live_update_interval."""
+ # Maps cache key patterns to sport keys
+ sport_patterns = {
+ 'nfl': ['nfl', 'football'],
+ 'nba': ['nba', 'basketball'],
+ 'mlb': ['mlb', 'baseball'],
+ 'nhl': ['nhl', 'hockey'],
+ 'soccer': ['soccer', 'football'],
+ # ... etc
+ }
+```
+
+### Configuration Examples
+
+**Current Configuration (config/config.json):**
+```json
+{
+ "nfl_scoreboard": {
+ "live_update_interval": 30,
+ "enabled": true
+ },
+ "soccer_scoreboard": {
+ "live_update_interval": 30,
+ "enabled": false
+ },
+ "mlb": {
+ "live_update_interval": 30,
+ "enabled": true
+ }
+}
+```
+
+**Cache Behavior:**
+- NFL live data: 30-second cache (from config)
+- Soccer live data: 30-second cache (from config)
+- MLB live data: 30-second cache (from config)
+
+### Fallback Strategy
+
+If configuration is unavailable, the system uses sport-specific defaults:
+
+```python
+default_intervals = {
+ 'soccer': 60, # Soccer default
+ 'nfl': 60, # NFL default
+ 'nhl': 60, # NHL default
+ 'nba': 60, # NBA default
+ 'mlb': 60, # MLB default
+ 'milb': 60, # Minor league default
+ 'ncaa_fb': 60, # College football default
+ 'ncaa_baseball': 60, # College baseball default
+ 'ncaam_basketball': 60, # College basketball default
+}
+```
+
+## Usage Examples
+
+### Automatic Sport Detection
+```python
+# Cache manager automatically detects NFL and uses nfl_scoreboard.live_update_interval
+cached_data = cache_manager.get_with_auto_strategy("nfl_live_20241201")
+
+# Cache manager automatically detects soccer and uses soccer_scoreboard.live_update_interval
+cached_data = cache_manager.get_with_auto_strategy("soccer_live_20241201")
+```
+
+### Manual Sport Specification
+```python
+# Explicitly specify sport for custom cache keys
+cached_data = cache_manager.get_cached_data_with_strategy("custom_live_key", "sports_live")
+```
+
+## Benefits
+
+1. **Configuration-Driven**: Cache respects your sport-specific settings
+2. **Automatic Detection**: No manual cache duration management needed
+3. **Sport-Optimized**: Each sport uses its appropriate update interval
+4. **Backward Compatible**: Existing code continues to work
+5. **Flexible**: Easy to adjust intervals per sport in config
+
+## Migration
+
+The enhanced cache manager is backward compatible. Existing code will automatically benefit from sport-specific intervals without any changes needed.
+
+To customize intervals for specific sports, simply update the `live_update_interval` in your `config/config.json`:
+
+```json
+{
+ "nfl_scoreboard": {
+ "live_update_interval": 15 // More aggressive for NFL
+ },
+ "mlb": {
+ "live_update_interval": 45 // Slower pace for MLB
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/CONFIGURATION_REFERENCE.md b/docs/CONFIGURATION_REFERENCE.md
new file mode 100644
index 000000000..b7493475d
--- /dev/null
+++ b/docs/CONFIGURATION_REFERENCE.md
@@ -0,0 +1,517 @@
+# Configuration Reference Guide
+
+This guide provides a complete cross-reference between configuration options and their corresponding managers, helping you understand exactly what each setting does and where to find detailed documentation.
+
+## Quick Configuration Lookup
+
+### Core System Settings
+
+| Configuration Section | Manager | Documentation | Purpose |
+|----------------------|---------|---------------|---------|
+| `clock` | Clock Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-clock-manager) | Time display |
+| `display` | Display Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-display-manager) | Hardware control |
+| `schedule` | Display Controller | [Web UI Guide](WEB_UI_COMPLETE_GUIDE.md#-schedule-tab) | Display timing |
+| `timezone` | System-wide | [Configuration Guide](WIKI_CONFIGURATION.md) | Global timezone |
+| `location` | System-wide | [Configuration Guide](WIKI_CONFIGURATION.md) | Geographic location |
+
+### Weather & Environment
+
+| Configuration Section | Manager | Documentation | Purpose |
+|----------------------|---------|---------------|---------|
+| `weather` | Weather Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-weather-manager) | Weather display |
+
+### Financial Data
+
+| Configuration Section | Manager | Documentation | Purpose |
+|----------------------|---------|---------------|---------|
+| `stocks` | Stock Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-stock-manager) | Stock ticker |
+| `crypto` | Stock Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-stock-manager) | Cryptocurrency |
+| `stock_news` | Stock News Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-stock-news-manager) | Financial news |
+
+### Sports Leagues
+
+| Configuration Section | Manager | Documentation | Purpose |
+|----------------------|---------|---------------|---------|
+| `nhl_scoreboard` | NHL Managers | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-nhl-managers) | NHL games |
+| `nba_scoreboard` | NBA Managers | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-nba-managers) | NBA games |
+| `mlb` | MLB Managers | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-mlb-managers) | MLB games |
+| `nfl_scoreboard` | NFL Managers | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-nfl-managers) | NFL games |
+| `soccer_scoreboard` | Soccer Managers | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-soccer-managers) | Soccer matches |
+| `ncaa_fb_scoreboard` | NCAA FB Managers | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-ncaa-football-managers) | College football |
+| `ncaa_baseball_scoreboard` | NCAA Baseball Managers | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-ncaa-baseball-managers) | College baseball |
+| `ncaam_basketball_scoreboard` | NCAA Basketball Managers | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-ncaa-basketball-managers) | College basketball |
+| `milb` | MiLB Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-milb-manager) | Minor league baseball |
+
+### Content & Media
+
+| Configuration Section | Manager | Documentation | Purpose |
+|----------------------|---------|---------------|---------|
+| `music` | Music Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-music-manager) | Music display |
+| `youtube` | YouTube Display | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-youtube-display) | YouTube stats |
+| `text_display` | Text Display | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-text-display) | Custom messages |
+| `calendar` | Calendar Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-calendar-manager) | Calendar events |
+| `news_manager` | News Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-news-manager) | RSS news feeds |
+| `of_the_day` | Of The Day Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-of-the-day-manager) | Daily content |
+
+### Utilities & Analysis
+
+| Configuration Section | Manager | Documentation | Purpose |
+|----------------------|---------|---------------|---------|
+| `odds_ticker` | Odds Ticker Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-odds-ticker-manager) | Betting odds |
+| `leaderboard` | Leaderboard Manager | [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md#-leaderboard-manager) | League standings |
+
+---
+
+## Configuration by Feature Type
+
+### 🕐 Time & Scheduling
+
+**Clock Display**:
+```json
+{
+ "clock": {
+ "enabled": true,
+ "format": "%I:%M %p",
+ "update_interval": 1
+ }
+}
+```
+📖 **Documentation**: [Clock Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-clock-manager)
+
+**Display Schedule**:
+```json
+{
+ "schedule": {
+ "enabled": true,
+ "start_time": "07:00",
+ "end_time": "23:00"
+ }
+}
+```
+📖 **Documentation**: [Web UI Schedule Tab](WEB_UI_COMPLETE_GUIDE.md#-schedule-tab)
+
+**Global Timezone**:
+```json
+{
+ "timezone": "America/Chicago"
+}
+```
+
+### 🖥️ Display Hardware
+
+**Hardware Configuration**:
+```json
+{
+ "display": {
+ "hardware": {
+ "rows": 32,
+ "cols": 64,
+ "chain_length": 2,
+ "brightness": 95,
+ "hardware_mapping": "adafruit-hat-pwm"
+ },
+ "runtime": {
+ "gpio_slowdown": 3
+ },
+ "display_durations": {
+ "clock": 15,
+ "weather": 30,
+ "stocks": 30
+ }
+ }
+}
+```
+📖 **Documentation**: [Display Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-display-manager)
+
+### 🌤️ Weather & Location
+
+**Weather Display**:
+```json
+{
+ "weather": {
+ "enabled": true,
+ "api_key": "your_openweathermap_api_key",
+ "update_interval": 1800,
+ "units": "imperial",
+ "show_feels_like": true,
+ "show_humidity": true,
+ "show_wind": true,
+ "show_uv_index": true
+ }
+}
+```
+📖 **Documentation**: [Weather Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-weather-manager)
+
+**Location Settings**:
+```json
+{
+ "location": {
+ "city": "Dallas",
+ "state": "Texas",
+ "country": "US"
+ }
+}
+```
+
+### 💰 Financial Data
+
+**Stock Ticker**:
+```json
+{
+ "stocks": {
+ "enabled": true,
+ "symbols": ["AAPL", "MSFT", "GOOGL", "TSLA"],
+ "update_interval": 600,
+ "scroll_speed": 1,
+ "dynamic_duration": true
+ }
+}
+```
+📖 **Documentation**: [Stock Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-stock-manager)
+
+**Cryptocurrency**:
+```json
+{
+ "crypto": {
+ "enabled": true,
+ "symbols": ["BTC-USD", "ETH-USD", "ADA-USD"],
+ "update_interval": 300
+ }
+}
+```
+📖 **Documentation**: [Stock Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-stock-manager)
+
+**Financial News**:
+```json
+{
+ "stock_news": {
+ "enabled": true,
+ "scroll_speed": 1,
+ "max_headlines_per_symbol": 1,
+ "dynamic_duration": true
+ }
+}
+```
+📖 **Documentation**: [Stock News Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-stock-news-manager)
+
+### 🏈 Sports Configuration
+
+**NHL Hockey**:
+```json
+{
+ "nhl_scoreboard": {
+ "enabled": true,
+ "favorite_teams": ["TB", "FLA"],
+ "show_odds": true,
+ "show_records": true,
+ "live_priority": true,
+ "live_update_interval": 60
+ }
+}
+```
+📖 **Documentation**: [NHL Managers](MANAGER_GUIDE_COMPREHENSIVE.md#-nhl-managers)
+
+**NBA Basketball**:
+```json
+{
+ "nba_scoreboard": {
+ "enabled": true,
+ "favorite_teams": ["MIA", "LAL"],
+ "show_odds": true,
+ "show_records": true,
+ "live_priority": true
+ }
+}
+```
+📖 **Documentation**: [NBA Managers](MANAGER_GUIDE_COMPREHENSIVE.md#-nba-managers)
+
+**MLB Baseball**:
+```json
+{
+ "mlb": {
+ "enabled": true,
+ "favorite_teams": ["TB", "NYY"],
+ "show_odds": true,
+ "live_priority": true
+ }
+}
+```
+📖 **Documentation**: [MLB Managers](MANAGER_GUIDE_COMPREHENSIVE.md#-mlb-managers)
+
+**NFL Football**:
+```json
+{
+ "nfl_scoreboard": {
+ "enabled": true,
+ "favorite_teams": ["TB", "MIA"],
+ "show_odds": true,
+ "show_records": true,
+ "live_priority": true
+ }
+}
+```
+📖 **Documentation**: [NFL Managers](MANAGER_GUIDE_COMPREHENSIVE.md#-nfl-managers)
+
+**Soccer**:
+```json
+{
+ "soccer_scoreboard": {
+ "enabled": true,
+ "favorite_teams": ["Real Madrid", "Barcelona"],
+ "target_leagues": ["uefa.champions", "eng.1", "esp.1"],
+ "show_odds": true,
+ "live_priority": true
+ }
+}
+```
+📖 **Documentation**: [Soccer Managers](MANAGER_GUIDE_COMPREHENSIVE.md#-soccer-managers)
+
+### 🎵 Music & Entertainment
+
+**Music Display**:
+```json
+{
+ "music": {
+ "enabled": true,
+ "preferred_source": "spotify",
+ "POLLING_INTERVAL_SECONDS": 2,
+ "spotify": {
+ "client_id": "your_spotify_client_id",
+ "client_secret": "your_spotify_client_secret"
+ }
+ }
+}
+```
+📖 **Documentation**: [Music Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-music-manager)
+
+**YouTube Stats**:
+```json
+{
+ "youtube": {
+ "enabled": true,
+ "api_key": "your_youtube_api_key",
+ "channels": [
+ {
+ "name": "Channel Name",
+ "channel_id": "UCxxxxxxxxxx",
+ "display_name": "Custom Name"
+ }
+ ]
+ }
+}
+```
+📖 **Documentation**: [YouTube Display](MANAGER_GUIDE_COMPREHENSIVE.md#-youtube-display)
+
+### 📰 News & Information
+
+**News Manager**:
+```json
+{
+ "news_manager": {
+ "enabled": true,
+ "enabled_feeds": ["NFL", "NBA", "TOP SPORTS"],
+ "custom_feeds": {
+ "TECH NEWS": "https://feeds.feedburner.com/TechCrunch"
+ },
+ "headlines_per_feed": 2,
+ "scroll_speed": 2,
+ "dynamic_duration": true
+ }
+}
+```
+📖 **Documentation**: [News Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-news-manager)
+
+**Text Display**:
+```json
+{
+ "text_display": {
+ "enabled": true,
+ "messages": ["Welcome to LEDMatrix!", "Custom message"],
+ "scroll_speed": 2,
+ "text_color": [255, 255, 255]
+ }
+}
+```
+📖 **Documentation**: [Text Display](MANAGER_GUIDE_COMPREHENSIVE.md#-text-display)
+
+**Calendar Events**:
+```json
+{
+ "calendar": {
+ "enabled": true,
+ "calendars": ["primary", "birthdays"],
+ "max_events": 3,
+ "update_interval": 300
+ }
+}
+```
+📖 **Documentation**: [Calendar Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-calendar-manager)
+
+### 🎯 Utilities & Analysis
+
+**Odds Ticker**:
+```json
+{
+ "odds_ticker": {
+ "enabled": true,
+ "enabled_leagues": ["nfl", "nba", "mlb"],
+ "show_favorite_teams_only": false,
+ "scroll_speed": 2,
+ "dynamic_duration": true
+ }
+}
+```
+📖 **Documentation**: [Odds Ticker Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-odds-ticker-manager)
+
+**Leaderboards**:
+```json
+{
+ "leaderboard": {
+ "enabled": true,
+ "enabled_sports": {
+ "nfl": {
+ "enabled": true,
+ "top_teams": 10
+ }
+ },
+ "scroll_speed": 1,
+ "dynamic_duration": true
+ }
+}
+```
+📖 **Documentation**: [Leaderboard Manager](MANAGER_GUIDE_COMPREHENSIVE.md#-leaderboard-manager)
+
+---
+
+## Web Interface Configuration
+
+The web interface provides GUI controls for all configuration options:
+
+| Configuration Area | Web UI Tab | Documentation |
+|-------------------|------------|---------------|
+| System monitoring | Overview | [Web UI Overview](WEB_UI_COMPLETE_GUIDE.md#-overview-tab) |
+| Display timing | Schedule | [Web UI Schedule](WEB_UI_COMPLETE_GUIDE.md#-schedule-tab) |
+| Hardware settings | Display | [Web UI Display](WEB_UI_COMPLETE_GUIDE.md#-display-tab) |
+| Sports leagues | Sports | [Web UI Sports](WEB_UI_COMPLETE_GUIDE.md#-sports-tab) |
+| Weather service | Weather | [Web UI Weather](WEB_UI_COMPLETE_GUIDE.md#-weather-tab) |
+| Financial data | Stocks | [Web UI Stocks](WEB_UI_COMPLETE_GUIDE.md#-stocks-tab) |
+| Additional features | Features | [Web UI Features](WEB_UI_COMPLETE_GUIDE.md#-features-tab) |
+| Music integration | Music | [Web UI Music](WEB_UI_COMPLETE_GUIDE.md#-music-tab) |
+| Calendar events | Calendar | [Web UI Calendar](WEB_UI_COMPLETE_GUIDE.md#-calendar-tab) |
+| News feeds | News | [Web UI News](WEB_UI_COMPLETE_GUIDE.md#-news-tab) |
+| API keys | API Keys | [Web UI API Keys](WEB_UI_COMPLETE_GUIDE.md#-api-keys-tab) |
+| Direct JSON editing | Raw JSON | [Web UI Raw JSON](WEB_UI_COMPLETE_GUIDE.md#-raw-json-tab) |
+
+---
+
+## Configuration File Locations
+
+### Main Configuration
+- **File**: `config/config.json`
+- **Purpose**: All non-sensitive settings
+- **Documentation**: [Configuration Guide](WIKI_CONFIGURATION.md)
+
+### Secrets Configuration
+- **File**: `config/config_secrets.json`
+- **Purpose**: API keys and sensitive credentials
+- **Documentation**: [Configuration Guide](WIKI_CONFIGURATION.md)
+
+### Template Files
+- **Main Template**: `config/config.template.json`
+- **Secrets Template**: `config/config_secrets.template.json`
+- **Purpose**: Default configuration examples
+
+---
+
+## Common Configuration Patterns
+
+### Enable/Disable Pattern
+Most managers use this pattern:
+```json
+{
+ "manager_name": {
+ "enabled": true,
+ "other_options": "..."
+ }
+}
+```
+
+### Update Interval Pattern
+Data-fetching managers use:
+```json
+{
+ "manager_name": {
+ "update_interval": 600,
+ "live_update_interval": 60
+ }
+}
+```
+
+### Scrolling Display Pattern
+Text-based displays use:
+```json
+{
+ "manager_name": {
+ "scroll_speed": 2,
+ "scroll_delay": 0.02,
+ "dynamic_duration": true
+ }
+}
+```
+
+### Favorite Teams Pattern
+Sports managers use:
+```json
+{
+ "sport_scoreboard": {
+ "favorite_teams": ["TEAM1", "TEAM2"],
+ "live_priority": true
+ }
+}
+```
+
+---
+
+## Configuration Validation
+
+### JSON Validation
+- Use the [Raw JSON Tab](WEB_UI_COMPLETE_GUIDE.md#-raw-json-tab) for real-time validation
+- Check syntax highlighting and error indicators
+- Use the Format button for proper indentation
+
+### Manager-Specific Validation
+- Each manager validates its own configuration section
+- Invalid configurations are logged with detailed error messages
+- Fallback to default values when possible
+
+### Web Interface Validation
+- Client-side validation in web forms
+- Server-side validation on submission
+- Real-time feedback and error messages
+
+---
+
+## Troubleshooting Configuration
+
+### Common Issues
+1. **JSON Syntax Errors**: Use Raw JSON tab for validation
+2. **Missing API Keys**: Check API Keys tab and secrets file
+3. **Invalid Team Names**: Refer to [Team Abbreviations Guide](TEAM_ABBREVIATIONS_AND_LEAGUE_SLUGS.md)
+4. **Permission Errors**: Run permission fix scripts
+5. **Service Not Starting**: Check configuration syntax and required fields
+
+### Debug Tools
+- **Web UI Logs Tab**: View real-time logs
+- **Raw JSON Tab**: Validate configuration syntax
+- **System Actions**: Restart services with new configuration
+- **Cache Management**: Clear cache when configuration changes
+
+### Getting Help
+- **Troubleshooting Guide**: [General Troubleshooting](WIKI_TROUBLESHOOTING.md)
+- **Configuration Examples**: [Configuration Guide](WIKI_CONFIGURATION.md)
+- **Manager Documentation**: [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md)
+- **Web Interface Help**: [Complete Web UI Guide](WEB_UI_COMPLETE_GUIDE.md)
+
+---
+
+This reference guide connects every configuration option to its corresponding manager and documentation, making it easy to understand and configure your LEDMatrix system.
diff --git a/docs/CUSTOM_FEEDS_GUIDE.md b/docs/CUSTOM_FEEDS_GUIDE.md
new file mode 100644
index 000000000..aca7a007d
--- /dev/null
+++ b/docs/CUSTOM_FEEDS_GUIDE.md
@@ -0,0 +1,245 @@
+# Adding Custom RSS Feeds & Sports - Complete Guide
+
+This guide shows you **3 different ways** to add custom RSS feeds like F1, MotoGP, or any personal feeds to your news manager.
+
+## Quick Examples
+
+### F1 Racing Feeds
+```bash
+# BBC F1 (Recommended - works well)
+python3 add_custom_feed_example.py add "BBC F1" "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
+
+# Motorsport.com F1
+python3 add_custom_feed_example.py add "Motorsport F1" "https://www.motorsport.com/rss/f1/news/"
+
+# Formula1.com Official
+python3 add_custom_feed_example.py add "F1 Official" "https://www.formula1.com/en/latest/all.xml"
+```
+
+### Other Sports
+```bash
+# MotoGP
+python3 add_custom_feed_example.py add "MotoGP" "https://www.motogp.com/en/rss/news"
+
+# Tennis
+python3 add_custom_feed_example.py add "Tennis" "https://www.atptour.com/en/rss/news"
+
+# Golf
+python3 add_custom_feed_example.py add "Golf" "https://www.pgatour.com/news.rss"
+
+# Soccer/Football
+python3 add_custom_feed_example.py add "ESPN Soccer" "https://www.espn.com/espn/rss/soccer/news"
+```
+
+### Personal/Blog Feeds
+```bash
+# Personal blog
+python3 add_custom_feed_example.py add "My Blog" "https://myblog.com/rss.xml"
+
+# Tech news
+python3 add_custom_feed_example.py add "TechCrunch" "https://techcrunch.com/feed/"
+
+# Local news
+python3 add_custom_feed_example.py add "Local News" "https://localnews.com/rss"
+```
+
+---
+
+## Method 1: Command Line (Easiest)
+
+### Add a Feed
+```bash
+python3 add_custom_feed_example.py add "FEED_NAME" "RSS_URL"
+```
+
+### List All Feeds
+```bash
+python3 add_custom_feed_example.py list
+```
+
+### Remove a Feed
+```bash
+python3 add_custom_feed_example.py remove "FEED_NAME"
+```
+
+### Example: Adding F1
+```bash
+# Step 1: Check current feeds
+python3 add_custom_feed_example.py list
+
+# Step 2: Add BBC F1 feed
+python3 add_custom_feed_example.py add "BBC F1" "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
+
+# Step 3: Verify it was added
+python3 add_custom_feed_example.py list
+```
+
+---
+
+## Method 2: Web Interface
+
+1. **Open Web Interface**: Go to `http://your-display-ip:5001`
+2. **Navigate to News Tab**: Click the "News Manager" tab
+3. **Add Custom Feed**:
+ - Enter feed name in "Feed Name" field (e.g., "BBC F1")
+ - Enter RSS URL in "RSS Feed URL" field
+ - Click "Add Feed" button
+4. **Enable the Feed**: Check the checkbox next to your new feed
+5. **Save Settings**: Click "Save News Settings"
+
+---
+
+## Method 3: Direct Config Edit
+
+Edit `config/config.json` directly:
+
+```json
+{
+ "news_manager": {
+ "enabled": true,
+ "enabled_feeds": ["NFL", "NCAA FB", "BBC F1"],
+ "custom_feeds": {
+ "BBC F1": "http://feeds.bbci.co.uk/sport/formula1/rss.xml",
+ "Motorsport F1": "https://www.motorsport.com/rss/f1/news/",
+ "My Blog": "https://myblog.com/rss.xml"
+ },
+ "headlines_per_feed": 2
+ }
+}
+```
+
+---
+
+## Finding RSS Feeds
+
+### Popular Sports RSS Feeds
+
+| Sport | Source | RSS URL |
+|-------|--------|---------|
+| **F1** | BBC Sport | `http://feeds.bbci.co.uk/sport/formula1/rss.xml` |
+| **F1** | Motorsport.com | `https://www.motorsport.com/rss/f1/news/` |
+| **MotoGP** | Official | `https://www.motogp.com/en/rss/news` |
+| **Tennis** | ATP Tour | `https://www.atptour.com/en/rss/news` |
+| **Golf** | PGA Tour | `https://www.pgatour.com/news.rss` |
+| **Soccer** | ESPN | `https://www.espn.com/espn/rss/soccer/news` |
+| **Boxing** | ESPN | `https://www.espn.com/espn/rss/boxing/news` |
+| **UFC/MMA** | ESPN | `https://www.espn.com/espn/rss/mma/news` |
+
+### How to Find RSS Feeds
+1. **Look for RSS icons** on websites
+2. **Check `/rss`, `/feed`, or `/rss.xml`** paths
+3. **Use RSS discovery tools** like RSS Feed Finder
+4. **Check site footers** for RSS links
+
+### Testing RSS Feeds
+```bash
+# Test if a feed works before adding it
+python3 -c "
+import feedparser
+import requests
+url = 'YOUR_RSS_URL_HERE'
+try:
+ response = requests.get(url, timeout=10)
+ feed = feedparser.parse(response.content)
+ print(f'SUCCESS: Feed works! Title: {feed.feed.get(\"title\", \"N/A\")}')
+ print(f'{len(feed.entries)} articles found')
+ if feed.entries:
+ print(f'Latest: {feed.entries[0].title}')
+except Exception as e:
+ print(f'ERROR: {e}')
+"
+```
+
+---
+
+## Advanced Configuration
+
+### Controlling Feed Behavior
+
+```json
+{
+ "news_manager": {
+ "headlines_per_feed": 3, // Headlines from each feed
+ "scroll_speed": 2, // Pixels per frame
+ "scroll_delay": 0.02, // Seconds between updates
+ "rotation_enabled": true, // Rotate content to avoid repetition
+ "rotation_threshold": 3, // Cycles before rotating
+ "update_interval": 300 // Seconds between feed updates
+ }
+}
+```
+
+### Feed Priority
+Feeds are displayed in the order they appear in `enabled_feeds`:
+```json
+"enabled_feeds": ["NFL", "BBC F1", "NCAA FB"] // NFL first, then F1, then NCAA
+```
+
+### Custom Display Names
+You can use any display name for feeds:
+```bash
+python3 add_custom_feed_example.py add "Formula 1" "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
+python3 add_custom_feed_example.py add "Basketball News" "https://www.espn.com/espn/rss/nba/news"
+```
+
+---
+
+## Troubleshooting
+
+### Feed Not Working?
+1. **Test the RSS URL** using the testing command above
+2. **Check for HTTPS vs HTTP** - some feeds require secure connections
+3. **Verify the feed format** - must be valid RSS or Atom
+4. **Check rate limiting** - some sites block frequent requests
+
+### Common Issues
+- **403 Forbidden**: Site blocks automated requests (try different feed)
+- **SSL Errors**: Use HTTP instead of HTTPS if available
+- **No Content**: Feed might be empty or incorrectly formatted
+- **Slow Loading**: Increase timeout in news manager settings
+
+### Feed Alternatives
+If one feed doesn't work, try alternatives:
+- **ESPN feeds** sometimes have access restrictions
+- **BBC feeds** are generally reliable
+- **Official sport websites** often have RSS feeds
+- **News aggregators** like Google News have topic-specific feeds
+
+---
+
+## Real-World Example: Complete F1 Setup
+
+```bash
+# 1. List current setup
+python3 add_custom_feed_example.py list
+
+# 2. Add multiple F1 sources for better coverage
+python3 add_custom_feed_example.py add "BBC F1" "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
+python3 add_custom_feed_example.py add "Motorsport F1" "https://www.motorsport.com/rss/f1/news/"
+
+# 3. Add other racing series
+python3 add_custom_feed_example.py add "MotoGP" "https://www.motogp.com/en/rss/news"
+
+# 4. Verify all feeds work
+python3 simple_news_test.py
+
+# 5. Check final configuration
+python3 add_custom_feed_example.py list
+```
+
+Result: Your display will now rotate between NFL, NCAA FB, BBC F1, Motorsport F1, and MotoGP headlines!
+
+---
+
+## Pro Tips
+
+1. **Start Small**: Add one feed at a time and test it
+2. **Mix Sources**: Use multiple sources for the same sport for better coverage
+3. **Monitor Performance**: Too many feeds can slow down updates
+4. **Use Descriptive Names**: "BBC F1" is better than just "F1"
+5. **Test Regularly**: RSS feeds can change or break over time
+6. **Backup Config**: Save your `config.json` before making changes
+
+---
+
+**Need help?** The news manager is designed to be flexible and user-friendly. Start with the command line method - it's the easiest way to get started!
\ No newline at end of file
diff --git a/docs/DYNAMIC_DURATION_GUIDE.md b/docs/DYNAMIC_DURATION_GUIDE.md
new file mode 100644
index 000000000..9e04a1981
--- /dev/null
+++ b/docs/DYNAMIC_DURATION_GUIDE.md
@@ -0,0 +1,177 @@
+# Dynamic Duration Feature - Complete Guide
+
+The news manager now includes intelligent **dynamic duration calculation** that automatically determines the exact time needed to display all your selected headlines without cutting off mid-scroll.
+
+## How It Works
+
+### Automatic Calculation
+The system calculates the perfect display duration by:
+
+1. **Measuring Text Width**: Calculates the exact pixel width of all headlines combined
+2. **Computing Scroll Distance**: Determines how far text needs to scroll (display width + text width)
+3. **Calculating Time**: Uses scroll speed and delay to compute exact timing
+4. **Adding Buffer**: Includes configurable buffer time for smooth transitions
+5. **Applying Limits**: Ensures duration stays within your min/max preferences
+
+### Real-World Example
+With current settings (4 feeds, 2 headlines each):
+- **Total Headlines**: 8 headlines per cycle
+- **Estimated Duration**: 57 seconds
+- **Cycles per Hour**: ~63 cycles
+- **Result**: Perfect timing, no cut-offs
+
+## Configuration Options
+
+### Core Settings
+```json
+{
+ "news_manager": {
+ "dynamic_duration": true, // Enable/disable feature
+ "min_duration": 30, // Minimum display time (seconds)
+ "max_duration": 300, // Maximum display time (seconds)
+ "duration_buffer": 0.1, // Buffer time (10% extra)
+ "headlines_per_feed": 2, // Headlines from each feed
+ "scroll_speed": 2, // Pixels per frame
+ "scroll_delay": 0.02 // Seconds per frame
+ }
+}
+```
+
+### Duration Scenarios
+
+| Scenario | Headlines | Est. Duration | Cycles/Hour |
+|----------|-----------|---------------|-------------|
+| **Light** | 4 headlines | 30s (min) | 120 |
+| **Medium** | 6 headlines | 30s (min) | 120 |
+| **Current** | 8 headlines | 57s | 63 |
+| **Heavy** | 12 headlines | 85s | 42 |
+| **Maximum** | 20+ headlines | 300s (max) | 12 |
+
+## Benefits
+
+### Perfect Timing
+- **No Cut-offs**: Headlines never cut off mid-sentence
+- **Complete Cycles**: Always shows full rotation of all selected content
+- **Smooth Transitions**: Buffer time prevents jarring switches
+
+### Intelligent Scaling
+- **Adapts to Content**: More feeds = longer duration automatically
+- **User Control**: Set your preferred min/max limits
+- **Flexible**: Works with any combination of feeds and headlines
+
+### Predictable Behavior
+- **Consistent Experience**: Same content always takes same time
+- **Reliable Cycling**: Know exactly when content will repeat
+- **Configurable**: Adjust to your viewing preferences
+
+## Usage Examples
+
+### Command Line Testing
+```bash
+# Test dynamic duration calculations
+python3 test_dynamic_duration.py
+
+# Check current status
+python3 test_dynamic_duration.py status
+```
+
+### Configuration Changes
+```bash
+# Add more feeds (increases duration)
+python3 add_custom_feed_example.py add "Tennis" "https://www.atptour.com/en/rss/news"
+
+# Check new duration
+python3 test_dynamic_duration.py status
+```
+
+### Web Interface
+1. Go to `http://display-ip:5001`
+2. Click "News Manager" tab
+3. Adjust "Duration Settings":
+ - **Min Duration**: Shortest acceptable cycle time
+ - **Max Duration**: Longest acceptable cycle time
+ - **Buffer**: Extra time for smooth transitions
+
+## Advanced Configuration
+
+### Fine-Tuning Duration
+```json
+{
+ "min_duration": 45, // Increase for longer minimum cycles
+ "max_duration": 180, // Decrease for shorter maximum cycles
+ "duration_buffer": 0.15 // Increase buffer for more transition time
+}
+```
+
+### Scroll Speed Impact
+```json
+{
+ "scroll_speed": 3, // Faster scroll = shorter duration
+ "scroll_delay": 0.015 // Less delay = shorter duration
+}
+```
+
+### Content Control
+```json
+{
+ "headlines_per_feed": 3, // More headlines = longer duration
+ "enabled_feeds": [ // More feeds = longer duration
+ "NFL", "NBA", "MLB", "NHL", "BBC F1", "Tennis"
+ ]
+}
+```
+
+## Troubleshooting
+
+### Duration Too Short
+- **Increase** `min_duration`
+- **Add** more feeds or headlines per feed
+- **Decrease** `scroll_speed`
+
+### Duration Too Long
+- **Decrease** `max_duration`
+- **Remove** some feeds
+- **Reduce** `headlines_per_feed`
+- **Increase** `scroll_speed`
+
+### Jerky Transitions
+- **Increase** `duration_buffer`
+- **Adjust** `scroll_delay`
+
+## Disable Dynamic Duration
+
+To use fixed timing instead:
+```json
+{
+ "dynamic_duration": false,
+ "fixed_duration": 60 // Fixed 60-second cycles
+}
+```
+
+## Technical Details
+
+### Calculation Formula
+```
+total_scroll_distance = display_width + text_width
+frames_needed = total_scroll_distance / scroll_speed
+base_time = frames_needed * scroll_delay
+buffer_time = base_time * duration_buffer
+final_duration = base_time + buffer_time (within min/max limits)
+```
+
+### Display Integration
+The display controller automatically:
+1. Calls `news_manager.get_dynamic_duration()`
+2. Uses returned value for display timing
+3. Switches to next mode after exact calculated time
+4. Logs duration decisions for debugging
+
+## Best Practices
+
+1. **Start Conservative**: Use default settings initially
+2. **Test Changes**: Use test script to preview duration changes
+3. **Monitor Performance**: Watch for smooth transitions
+4. **Adjust Gradually**: Make small changes to settings
+5. **Consider Viewing**: Match duration to your typical viewing patterns
+
+The dynamic duration feature ensures your news ticker always displays complete, perfectly-timed content cycles regardless of how many feeds or headlines you configure!
\ No newline at end of file
diff --git a/docs/DYNAMIC_DURATION_STOCKS_IMPLEMENTATION.md b/docs/DYNAMIC_DURATION_STOCKS_IMPLEMENTATION.md
new file mode 100644
index 000000000..fc34cbba2
--- /dev/null
+++ b/docs/DYNAMIC_DURATION_STOCKS_IMPLEMENTATION.md
@@ -0,0 +1,189 @@
+# Dynamic Duration Implementation for Stocks and Stock News
+
+## Overview
+
+This document describes the implementation of dynamic duration functionality for the `stock_manager` and `stock_news_manager` classes, following the same pattern as the existing `news_manager`.
+
+## What Was Implemented
+
+### 1. Configuration Updates
+
+Added dynamic duration settings to both `stocks` and `stock_news` sections in `config/config.json`:
+
+```json
+"stocks": {
+ "enabled": true,
+ "update_interval": 600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "toggle_chart": true,
+ "dynamic_duration": true,
+ "min_duration": 30,
+ "max_duration": 300,
+ "duration_buffer": 0.1,
+ "symbols": [...],
+ "display_format": "{symbol}: ${price} ({change}%)"
+},
+"stock_news": {
+ "enabled": true,
+ "update_interval": 3600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "max_headlines_per_symbol": 1,
+ "headlines_per_rotation": 2,
+ "dynamic_duration": true,
+ "min_duration": 30,
+ "max_duration": 300,
+ "duration_buffer": 0.1
+}
+```
+
+### 2. Stock Manager Updates (`src/stock_manager.py`)
+
+#### Added Dynamic Duration Properties
+```python
+# Dynamic duration settings
+self.dynamic_duration_enabled = self.stocks_config.get('dynamic_duration', True)
+self.min_duration = self.stocks_config.get('min_duration', 30)
+self.max_duration = self.stocks_config.get('max_duration', 300)
+self.duration_buffer = self.stocks_config.get('duration_buffer', 0.1)
+self.dynamic_duration = 60 # Default duration in seconds
+self.total_scroll_width = 0 # Track total width for dynamic duration calculation
+```
+
+#### Added `calculate_dynamic_duration()` Method
+This method calculates the exact time needed to display all stocks based on:
+- Total scroll width of the content
+- Display width
+- Scroll speed and delay settings
+- Configurable buffer time
+- Min/max duration limits
+
+#### Added `get_dynamic_duration()` Method
+Returns the calculated dynamic duration for use by the display controller.
+
+#### Updated `display_stocks()` Method
+The method now calculates and stores the total scroll width and calls `calculate_dynamic_duration()` when creating the scrolling image.
+
+### 3. Stock News Manager Updates (`src/stock_news_manager.py`)
+
+#### Added Dynamic Duration Properties
+```python
+# Dynamic duration settings
+self.dynamic_duration_enabled = self.stock_news_config.get('dynamic_duration', True)
+self.min_duration = self.stock_news_config.get('min_duration', 30)
+self.max_duration = self.stock_news_config.get('max_duration', 300)
+self.duration_buffer = self.stock_news_config.get('duration_buffer', 0.1)
+self.dynamic_duration = 60 # Default duration in seconds
+self.total_scroll_width = 0 # Track total width for dynamic duration calculation
+```
+
+#### Added `calculate_dynamic_duration()` Method
+Similar to the stock manager, calculates duration based on content width and scroll settings.
+
+#### Added `get_dynamic_duration()` Method
+Returns the calculated dynamic duration for use by the display controller.
+
+#### Updated `display_news()` Method
+The method now calculates and stores the total scroll width and calls `calculate_dynamic_duration()` when creating the scrolling image.
+
+### 4. Display Controller Updates (`src/display_controller.py`)
+
+#### Updated `get_current_duration()` Method
+Added dynamic duration handling for both `stocks` and `stock_news` modes:
+
+```python
+# Handle dynamic duration for stocks
+if mode_key == 'stocks' and self.stocks:
+ try:
+ dynamic_duration = self.stocks.get_dynamic_duration()
+ # Only log if duration has changed or we haven't logged this duration yet
+ if not hasattr(self, '_last_logged_duration') or self._last_logged_duration != dynamic_duration:
+ logger.info(f"Using dynamic duration for stocks: {dynamic_duration} seconds")
+ self._last_logged_duration = dynamic_duration
+ return dynamic_duration
+ except Exception as e:
+ logger.error(f"Error getting dynamic duration for stocks: {e}")
+ # Fall back to configured duration
+ return self.display_durations.get(mode_key, 60)
+
+# Handle dynamic duration for stock_news
+if mode_key == 'stock_news' and self.news:
+ try:
+ dynamic_duration = self.news.get_dynamic_duration()
+ # Only log if duration has changed or we haven't logged this duration yet
+ if not hasattr(self, '_last_logged_duration') or self._last_logged_duration != dynamic_duration:
+ logger.info(f"Using dynamic duration for stock_news: {dynamic_duration} seconds")
+ self._last_logged_duration = dynamic_duration
+ return dynamic_duration
+ except Exception as e:
+ logger.error(f"Error getting dynamic duration for stock_news: {e}")
+ # Fall back to configured duration
+ return self.display_durations.get(mode_key, 60)
+```
+
+## How It Works
+
+### Dynamic Duration Calculation
+
+The dynamic duration is calculated using the following formula:
+
+1. **Total Scroll Distance**: `display_width + total_scroll_width`
+2. **Frames Needed**: `total_scroll_distance / scroll_speed`
+3. **Base Time**: `frames_needed * scroll_delay`
+4. **Buffer Time**: `base_time * duration_buffer`
+5. **Final Duration**: `int(base_time + buffer_time)`
+
+The final duration is then clamped between `min_duration` and `max_duration`.
+
+### Integration with Display Controller
+
+1. When the display controller needs to determine how long to show a particular mode, it calls `get_current_duration()`
+2. For `stocks` and `stock_news` modes, it calls the respective manager's `get_dynamic_duration()` method
+3. The manager returns the calculated duration based on the current content width
+4. The display controller uses this duration to determine how long to display the content
+
+### Benefits
+
+1. **Consistent Display Time**: Content is displayed for an appropriate amount of time based on its length
+2. **Configurable**: Users can adjust min/max durations and buffer percentages
+3. **Fallback Support**: If dynamic duration fails, it falls back to configured fixed durations
+4. **Performance**: Duration is calculated once when content is created, not on every frame
+
+## Configuration Options
+
+### Dynamic Duration Settings
+
+- **`dynamic_duration`**: Enable/disable dynamic duration calculation (default: `true`)
+- **`min_duration`**: Minimum display duration in seconds (default: `30`)
+- **`max_duration`**: Maximum display duration in seconds (default: `300`)
+- **`duration_buffer`**: Buffer percentage to add for smooth cycling (default: `0.1` = 10%)
+
+### Example Configuration
+
+```json
+{
+ "dynamic_duration": true,
+ "min_duration": 20,
+ "max_duration": 180,
+ "duration_buffer": 0.15
+}
+```
+
+This would:
+- Enable dynamic duration
+- Set minimum display time to 20 seconds
+- Set maximum display time to 3 minutes
+- Add 15% buffer time for smooth cycling
+
+## Testing
+
+The implementation has been tested to ensure:
+- Configuration is properly loaded
+- Dynamic duration calculation works correctly
+- Display controller integration is functional
+- Fallback behavior works when dynamic duration is disabled
+
+## Compatibility
+
+This implementation follows the exact same pattern as the existing `news_manager` dynamic duration functionality, ensuring consistency across the codebase and making it easy to maintain and extend.
diff --git a/docs/GRACEFUL_UPDATES.md b/docs/GRACEFUL_UPDATES.md
new file mode 100644
index 000000000..ec16d06f9
--- /dev/null
+++ b/docs/GRACEFUL_UPDATES.md
@@ -0,0 +1,146 @@
+# Graceful Update System
+
+The LED Matrix project now includes a graceful update system that prevents lag during scrolling displays by deferring updates until the display is not actively scrolling.
+
+## Overview
+
+When displays like the odds ticker, stock ticker, or news ticker are actively scrolling, performing API updates or data fetching can cause visual lag or stuttering. The graceful update system solves this by:
+
+1. **Tracking scrolling state** - The system monitors when displays are actively scrolling
+2. **Deferring updates** - Updates that might cause lag are deferred during scrolling periods
+3. **Processing when safe** - Deferred updates are processed when scrolling stops or during non-scrolling periods
+4. **Priority-based execution** - Updates are executed in priority order when processed
+
+## How It Works
+
+### Scrolling State Tracking
+
+The `DisplayManager` class now includes scrolling state tracking:
+
+```python
+# Signal when scrolling starts
+display_manager.set_scrolling_state(True)
+
+# Signal when scrolling stops
+display_manager.set_scrolling_state(False)
+
+# Check if currently scrolling
+if display_manager.is_currently_scrolling():
+ # Defer updates
+ pass
+```
+
+### Deferred Updates
+
+Updates can be deferred using the `defer_update` method:
+
+```python
+# Defer an update with priority
+display_manager.defer_update(
+ lambda: self._perform_update(),
+ priority=1 # Lower numbers = higher priority
+)
+```
+
+### Automatic Processing
+
+Deferred updates are automatically processed when:
+- A display signals it's not scrolling
+- The main loop processes updates during non-scrolling periods
+- The inactivity threshold is reached (default: 2 seconds)
+
+## Implementation Details
+
+### Display Manager Changes
+
+The `DisplayManager` class now includes:
+
+- `set_scrolling_state(is_scrolling)` - Signal scrolling state changes
+- `is_currently_scrolling()` - Check if display is currently scrolling
+- `defer_update(update_func, priority)` - Defer an update function
+- `process_deferred_updates()` - Process all pending deferred updates
+- `get_scrolling_stats()` - Get current scrolling statistics
+
+### Manager Updates
+
+The following managers have been updated to use the graceful update system:
+
+#### Odds Ticker Manager
+- Defers API updates during scrolling
+- Signals scrolling state during display
+- Processes deferred updates when not scrolling
+
+#### Stock Manager
+- Defers stock data updates during scrolling
+- Always signals scrolling state (continuous scrolling)
+- Priority 2 for stock updates
+
+#### Stock News Manager
+- Defers news data updates during scrolling
+- Signals scrolling state during display
+- Priority 2 for news updates
+
+### Display Controller Changes
+
+The main display controller now:
+- Checks scrolling state before updating modules
+- Defers scrolling-sensitive updates during scrolling periods
+- Processes deferred updates in the main loop
+- Continues non-scrolling-sensitive updates normally
+
+## Configuration
+
+The system uses these default settings:
+
+- **Inactivity threshold**: 2.0 seconds
+- **Update priorities**:
+ - Priority 1: Odds ticker updates
+ - Priority 2: Stock and news updates
+ - Priority 3+: Other updates
+
+## Benefits
+
+1. **Smoother Scrolling** - No more lag during ticker scrolling
+2. **Better User Experience** - Displays remain responsive during updates
+3. **Efficient Resource Usage** - Updates happen when the system is idle
+4. **Priority-Based** - Important updates are processed first
+5. **Automatic** - No manual intervention required
+
+## Testing
+
+You can test the graceful update system using the provided test script:
+
+```bash
+python test_graceful_updates.py
+```
+
+This script demonstrates:
+- Deferring updates during scrolling
+- Processing updates when not scrolling
+- Priority-based execution
+- Inactivity threshold behavior
+
+## Debugging
+
+To debug the graceful update system, enable debug logging:
+
+```python
+import logging
+logging.getLogger('src.display_manager').setLevel(logging.DEBUG)
+```
+
+The system will log:
+- When scrolling state changes
+- When updates are deferred
+- When deferred updates are processed
+- Current scrolling statistics
+
+## Future Enhancements
+
+Potential improvements to the system:
+
+1. **Configurable thresholds** - Allow users to adjust inactivity thresholds
+2. **More granular priorities** - Add more priority levels for different update types
+3. **Update batching** - Group similar updates to reduce processing overhead
+4. **Performance metrics** - Track and report update deferral statistics
+5. **Web interface integration** - Show deferred update status in the web UI
diff --git a/docs/Home.md b/docs/Home.md
new file mode 100644
index 000000000..14a2b882e
--- /dev/null
+++ b/docs/Home.md
@@ -0,0 +1,84 @@
+## LEDMatrix Wiki
+
+Welcome to the comprehensive documentation for the LEDMatrix system! This wiki will help you install, configure, and customize your LED matrix display.
+
+## 🚀 Getting Started
+
+### 📋 [Quick Start Guide](WIKI_QUICK_START.md)
+Get your LEDMatrix up and running in minutes with this step-by-step guide.
+
+### 🔧 [Installation Guide](INSTALLATION_GUIDE.md)
+Detailed installation instructions for new users and advanced configurations.
+
+## 📖 Core Documentation
+
+### 🏗️ [System Architecture](WIKI_ARCHITECTURE.md)
+Understand how the LEDMatrix system is organized and how all components work together.
+
+### ⚙️ [Configuration Guide](WIKI_CONFIGURATION.md)
+Complete guide to configuring all aspects of your LEDMatrix system.
+
+### 🎯 [Display Managers](WIKI_DISPLAY_MANAGERS.md)
+Detailed documentation for each display manager and their configuration options.
+
+### 🔧 [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md)
+Comprehensive documentation covering every manager, all configuration options, and detailed explanations of how everything works.
+
+## 🌐 Web Interface
+- [Web Interface Installation](WEB_INTERFACE_INSTALLATION.md)
+- [🖥️ Complete Web UI Guide](WEB_UI_COMPLETE_GUIDE.md)
+- [Web Interface V2 Features](WEB_INTERFACE_V2_ENHANCED_SUMMARY.md)
+
+## 🎨 Features & Content
+- [📰 Sports News Manager](NEWS_MANAGER_README.md)
+- [📡 Custom RSS Feeds](CUSTOM_FEEDS_GUIDE.md)
+- [⏱️ Dynamic Duration Overview](dynamic_duration.md)
+- [📖 Dynamic Duration Guide](DYNAMIC_DURATION_GUIDE.md)
+- [📈 Dynamic Duration (Stocks)](DYNAMIC_DURATION_STOCKS_IMPLEMENTATION.md)
+
+## 🗄️ Performance & Caching
+- [Cache Strategy](CACHE_STRATEGY.md)
+- [Cache Management](cache_management.md)
+- [Graceful Updates](GRACEFUL_UPDATES.md)
+
+## 🧩 Troubleshooting
+- [General Troubleshooting](WIKI_TROUBLESHOOTING.md)
+- [MiLB Troubleshooting](MILB_TROUBLESHOOTING.md)
+
+## 📚 Reference
+- [📋 Configuration Reference](CONFIGURATION_REFERENCE.md)
+- [Team Abbreviations & League Slugs](TEAM_ABBREVIATIONS_AND_LEAGUE_SLUGS.md)
+
+---
+
+## About LEDMatrix
+
+LEDMatrix is a comprehensive LED matrix display system that provides real-time information display capabilities for various data sources. The system is highly configurable and supports multiple display modes that can be enabled or disabled based on user preferences.
+
+### Key Features
+- **Modular Design**: Each feature is a separate manager that can be enabled/disabled
+- **Real-time Updates**: Live data from APIs with intelligent caching
+- **Multiple Sports**: NHL, NBA, MLB, NFL, NCAA, Soccer, and more
+- **Financial Data**: Stock ticker, crypto prices, and financial news
+- **Weather**: Current conditions, hourly and daily forecasts
+- **Music**: Spotify and YouTube Music integration
+- **Custom Content**: Text display, YouTube stats, and more
+- **Scheduling**: Configurable display rotation and timing
+- **Caching**: Intelligent caching to reduce API calls
+
+### System Requirements
+- Raspberry Pi 3B+ or 4 (NOT Pi 5)
+- Adafruit RGB Matrix Bonnet/HAT
+- 2x LED Matrix panels (64x32)
+- 5V 4A DC Power Supply
+- Internet connection for API access
+
+### Quick Links
+- [YouTube Setup Video](https://www.youtube.com/watch?v=_HaqfJy1Y54)
+- [Project Website](https://www.chuck-builds.com/led-matrix/)
+- [GitHub Repository](https://github.com/ChuckBuilds/LEDMatrix)
+- [Discord Community](https://discord.com/invite/uW36dVAtcT)
+
+---
+
+*This wiki is designed to help you get the most out of your LEDMatrix system. Each page contains detailed information, configuration examples, and troubleshooting tips.*
\ No newline at end of file
diff --git a/docs/INSTALLATION_GUIDE.md b/docs/INSTALLATION_GUIDE.md
new file mode 100644
index 000000000..743307d22
--- /dev/null
+++ b/docs/INSTALLATION_GUIDE.md
@@ -0,0 +1,350 @@
+# LED Matrix Installation Guide
+
+## 🚀 Quick Start (Recommended for First-Time Installation)
+
+The easiest way to get your LEDMatrix up and running is to use the automated installer:
+
+### Step 1: Connect to Your Raspberry Pi
+```bash
+# SSH into your Raspberry Pi
+ssh ledpi@ledpi
+# (replace with your actual username@hostname)
+```
+
+### Step 2: Update System
+```bash
+sudo apt update && sudo apt upgrade -y
+sudo apt install -y git python3-pip cython3 build-essential python3-dev python3-pillow scons
+```
+
+### Step 3: Clone Repository
+```bash
+git clone https://github.com/ChuckBuilds/LEDMatrix.git
+cd LEDMatrix
+```
+
+### Step 4: Run First-Time Installation ⭐
+```bash
+chmod +x first_time_install.sh
+sudo ./first_time_install.sh
+```
+
+**This single script handles everything you need for a new installation!**
+
+---
+
+## 📋 Manual Installation (Advanced Users)
+
+If you prefer to install components individually, follow these steps:
+
+4. Install dependencies:
+```bash
+sudo pip3 install --break-system-packages -r requirements.txt
+```
+--break-system-packages allows us to install without a virtual environment
+
+
+5. Install rpi-rgb-led-matrix dependencies:
+```bash
+cd rpi-rgb-led-matrix-master
+```
+```bash
+sudo make build-python PYTHON=$(which python3)
+```
+```bash
+cd bindings/python
+sudo python3 setup.py install
+```
+Test it with:
+```bash
+python3 -c 'from rgbmatrix import RGBMatrix, RGBMatrixOptions; print("Success!")'
+```
+
+## Important: Sound Module Configuration
+
+1. Remove unnecessary services that might interfere with the LED matrix:
+```bash
+sudo apt-get remove bluez bluez-firmware pi-bluetooth triggerhappy pigpio
+```
+
+2. Blacklist the sound module:
+```bash
+cat <=5.9.0` - System monitoring
+- Updated Flask and related packages for better compatibility
+
+### File Structure
+```
+├── web_interface_v2.py # Enhanced backend with all features
+├── templates/index_v2.html # Complete frontend with all tabs
+├── requirements_web_v2.txt # Updated dependencies
+├── start_web_v2.py # Startup script (unchanged)
+└── WEB_INTERFACE_V2_ENHANCED_SUMMARY.md # This summary
+```
+
+### Key Features Preserved from Original
+- All configuration options from the original web interface
+- JSON linter with validation and formatting
+- System actions (start/stop service, reboot, git pull)
+- API key management
+- News manager functionality
+- Sports configuration
+- Display duration settings
+- All form validation and error handling
+
+### New Features Added
+- CPU utilization monitoring
+- Enhanced display preview (8x scaling, 20fps)
+- Complete LED Matrix hardware configuration
+- Improved responsive design
+- Better error handling and user feedback
+- Real-time system stats updates
+- Enhanced JSON editor with validation
+- Visual status indicators throughout
+
+## Usage
+
+1. **Start the Enhanced Interface**:
+ ```bash
+ python3 start_web_v2.py
+ ```
+
+2. **Access the Interface**:
+ Open browser to `http://your-pi-ip:5001`
+
+3. **Configure LED Matrix**:
+ - Go to "Display" tab for hardware settings
+ - Use "Schedule" tab for timing
+ - Configure services in respective tabs
+
+4. **Monitor System**:
+ - "Overview" tab shows real-time stats
+ - CPU, memory, disk, and temperature monitoring
+
+5. **Edit Configurations**:
+ - Use individual tabs for specific settings
+ - "Raw JSON" tab for direct configuration editing
+ - Real-time validation and error feedback
+
+## Benefits
+
+1. **Complete Control**: Every LED Matrix configuration option is now accessible
+2. **Better Monitoring**: Real-time system performance monitoring
+3. **Improved Usability**: Modern, responsive interface with better UX
+4. **Enhanced Preview**: Better display preview with higher resolution
+5. **Comprehensive Management**: All features in one unified interface
+6. **Backward Compatibility**: All original features preserved and enhanced
+
+The enhanced web interface provides a complete, professional-grade management system for LED Matrix displays while maintaining ease of use and reliability.
\ No newline at end of file
diff --git a/docs/WEB_UI_COMPLETE_GUIDE.md b/docs/WEB_UI_COMPLETE_GUIDE.md
new file mode 100644
index 000000000..a7d2592a3
--- /dev/null
+++ b/docs/WEB_UI_COMPLETE_GUIDE.md
@@ -0,0 +1,798 @@
+# Complete Web UI Guide
+
+The LEDMatrix Web Interface V2 provides a comprehensive, modern web-based control panel for managing your LED matrix display. This guide covers every feature, tab, and configuration option available in the web interface.
+
+## Overview
+
+The web interface runs on port 5001 and provides real-time control, monitoring, and configuration of your LEDMatrix system. It features a tabbed interface with different sections for various aspects of system management.
+
+### Accessing the Web Interface
+
+```
+http://your-pi-ip:5001
+```
+
+### Auto-Start Configuration
+
+To automatically start the web interface on boot, set this in your config:
+
+```json
+{
+ "web_display_autostart": true
+}
+```
+
+---
+
+## Interface Layout
+
+The web interface uses a modern tabbed layout with the following main sections:
+
+1. **Overview** - System monitoring and status
+2. **Schedule** - Display timing control
+3. **Display** - Hardware configuration
+4. **Sports** - Sports leagues settings
+5. **Weather** - Weather service configuration
+6. **Stocks** - Financial data settings
+7. **Features** - Additional display features
+8. **Music** - Music integration settings
+9. **Calendar** - Google Calendar integration
+10. **News** - RSS news feeds management
+11. **API Keys** - Secure API key management
+12. **Editor** - Visual display editor
+13. **Actions** - System control actions
+14. **Raw JSON** - Direct configuration editing
+15. **Logs** - System logs viewer
+
+---
+
+## Tab Details
+
+### 🏠 Overview Tab
+
+**Purpose**: Real-time system monitoring and status display.
+
+**Features**:
+- **System Statistics**:
+ - CPU utilization percentage (real-time)
+ - Memory usage with available/total display
+ - Disk usage percentage
+ - CPU temperature monitoring
+ - System uptime
+ - Service status (running/stopped)
+
+- **Display Preview**:
+ - Live preview of LED matrix display (8x scaling)
+ - 20fps update rate for smooth viewing
+ - Fallback display when no data available
+ - Enhanced border and styling
+
+- **Quick Status**:
+ - Current display mode
+ - Active managers
+ - Connection status
+ - WebSocket connectivity indicator
+
+**Auto-Refresh**: Updates every 2 seconds for real-time monitoring.
+
+---
+
+### ⏰ Schedule Tab
+
+**Purpose**: Configure when the display is active.
+
+**Configuration Options**:
+
+```json
+{
+ "schedule": {
+ "enabled": true,
+ "start_time": "07:00",
+ "end_time": "23:00"
+ }
+}
+```
+
+**Features**:
+- **Enable/Disable Scheduling**: Toggle automatic display scheduling
+- **Start Time**: Time to turn display on (24-hour format)
+- **End Time**: Time to turn display off (24-hour format)
+- **Timezone Awareness**: Uses system timezone
+- **Immediate Apply**: Changes take effect immediately
+
+**Form Controls**:
+- Checkbox to enable/disable scheduling
+- Time pickers for start and end times
+- Save button with async submission
+- Success/error notifications
+
+---
+
+### 🖥️ Display Tab
+
+**Purpose**: Complete LED matrix hardware configuration.
+
+**Hardware Settings**:
+
+```json
+{
+ "display": {
+ "hardware": {
+ "rows": 32,
+ "cols": 64,
+ "chain_length": 2,
+ "parallel": 1,
+ "brightness": 95,
+ "hardware_mapping": "adafruit-hat-pwm",
+ "scan_mode": 0,
+ "pwm_bits": 9,
+ "pwm_dither_bits": 1,
+ "pwm_lsb_nanoseconds": 130,
+ "disable_hardware_pulsing": false,
+ "inverse_colors": false,
+ "show_refresh_rate": false,
+ "limit_refresh_rate_hz": 120
+ },
+ "runtime": {
+ "gpio_slowdown": 3
+ }
+ }
+}
+```
+
+**Configuration Options**:
+
+**Physical Configuration**:
+- **Rows**: LED matrix height (typically 32)
+- **Columns**: LED matrix width (typically 64)
+- **Chain Length**: Number of panels connected in series
+- **Parallel**: Number of parallel chains
+
+**Display Quality**:
+- **Brightness**: Display brightness (0-100) with real-time slider
+- **PWM Bits**: Color depth (8-11, higher = better colors)
+- **PWM Dither Bits**: Smoothing for gradients
+- **PWM LSB Nanoseconds**: Timing precision
+
+**Hardware Interface**:
+- **Hardware Mapping**: HAT/Bonnet configuration
+ - `adafruit-hat-pwm` - With jumper mod (recommended)
+ - `adafruit-hat` - Without jumper mod
+ - `regular` - Direct GPIO
+ - `pi1` - Raspberry Pi 1 compatibility
+- **GPIO Slowdown**: Timing adjustment for different Pi models
+- **Scan Mode**: Panel scanning method
+
+**Advanced Options**:
+- **Disable Hardware Pulsing**: Software PWM override
+- **Inverse Colors**: Color inversion
+- **Show Refresh Rate**: Display refresh rate on screen
+- **Limit Refresh Rate**: Maximum refresh rate (Hz)
+
+**Form Features**:
+- Real-time brightness slider with immediate preview
+- Dropdown selectors for hardware mapping
+- Number inputs with validation
+- Checkbox controls for boolean options
+- Tooltips explaining each setting
+
+---
+
+### 🏈 Sports Tab
+
+**Purpose**: Configure sports leagues and display options.
+
+**Supported Sports**:
+- NFL (National Football League)
+- NBA (National Basketball Association)
+- MLB (Major League Baseball)
+- NHL (National Hockey League)
+- NCAA Football
+- NCAA Basketball
+- NCAA Baseball
+- MiLB (Minor League Baseball)
+- Soccer (Multiple leagues)
+
+**Configuration Per Sport**:
+
+```json
+{
+ "nfl_scoreboard": {
+ "enabled": true,
+ "favorite_teams": ["TB", "MIA"],
+ "show_odds": true,
+ "show_records": true,
+ "live_priority": true,
+ "test_mode": false
+ }
+}
+```
+
+**Common Options**:
+- **Enable/Disable**: Toggle each sport individually
+- **Favorite Teams**: List of team abbreviations to prioritize
+- **Show Odds**: Display betting odds for games
+- **Show Records**: Display team win-loss records
+- **Live Priority**: Prioritize live games in rotation
+- **Test Mode**: Use test data instead of live API
+
+**Features**:
+- Individual sport configuration sections
+- Team selection with dropdown menus
+- Checkbox controls for display options
+- Real-time form validation
+- Bulk enable/disable options
+
+---
+
+### 🌤️ Weather Tab
+
+**Purpose**: Configure weather display and data sources.
+
+**Configuration Options**:
+
+```json
+{
+ "weather": {
+ "enabled": true,
+ "api_key": "your_openweathermap_api_key",
+ "update_interval": 1800,
+ "units": "imperial",
+ "show_feels_like": true,
+ "show_humidity": true,
+ "show_wind": true,
+ "show_uv_index": true
+ }
+}
+```
+
+**Settings**:
+- **Enable Weather**: Toggle weather display
+- **API Key**: OpenWeatherMap API key (secure input)
+- **Update Interval**: Data refresh frequency (seconds)
+- **Units**: Temperature units (imperial/metric/kelvin)
+- **Display Options**:
+ - Show "feels like" temperature
+ - Show humidity percentage
+ - Show wind speed and direction
+ - Show UV index with color coding
+
+**Location Settings**:
+- Uses location from main configuration
+- Automatic timezone handling
+- Multiple display modes (current/hourly/daily)
+
+**Form Features**:
+- Secure API key input (password field)
+- Unit selection dropdown
+- Update interval slider
+- Checkbox controls for display options
+- API key validation
+
+---
+
+### 💰 Stocks Tab
+
+**Purpose**: Configure stock ticker, cryptocurrency, and financial news.
+
+**Stock Configuration**:
+
+```json
+{
+ "stocks": {
+ "enabled": true,
+ "symbols": ["AAPL", "MSFT", "GOOGL", "TSLA"],
+ "update_interval": 600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "toggle_chart": false,
+ "dynamic_duration": true
+ }
+}
+```
+
+**Cryptocurrency Configuration**:
+
+```json
+{
+ "crypto": {
+ "enabled": true,
+ "symbols": ["BTC-USD", "ETH-USD", "ADA-USD"],
+ "update_interval": 300
+ }
+}
+```
+
+**Stock Options**:
+- **Enable Stocks**: Toggle stock ticker display
+- **Symbols**: List of stock symbols to display
+- **Update Interval**: Data refresh frequency
+- **Scroll Speed**: Ticker scrolling speed (1-5)
+- **Scroll Delay**: Delay between scroll steps
+- **Toggle Chart**: Show mini price charts
+- **Dynamic Duration**: Auto-adjust display time
+
+**Crypto Options**:
+- **Enable Crypto**: Toggle cryptocurrency display
+- **Symbols**: Crypto symbols (format: SYMBOL-USD)
+- **Update Interval**: Crypto data refresh rate
+
+**Features**:
+- Symbol input with validation
+- Real-time price change indicators
+- Scrolling ticker configuration
+- Market hours awareness
+- Logo display for supported symbols
+
+---
+
+### 🎯 Features Tab
+
+**Purpose**: Configure additional display features and utilities.
+
+**Available Features**:
+
+**Clock Configuration**:
+```json
+{
+ "clock": {
+ "enabled": true,
+ "format": "%I:%M %p",
+ "update_interval": 1
+ }
+}
+```
+
+**Text Display Configuration**:
+```json
+{
+ "text_display": {
+ "enabled": true,
+ "messages": ["Welcome to LEDMatrix!", "Custom message"],
+ "scroll_speed": 2,
+ "text_color": [255, 255, 255]
+ }
+}
+```
+
+**YouTube Display Configuration**:
+```json
+{
+ "youtube": {
+ "enabled": true,
+ "api_key": "your_youtube_api_key",
+ "channels": [
+ {
+ "name": "Channel Name",
+ "channel_id": "UCxxxxxxxxxx",
+ "display_name": "Custom Name"
+ }
+ ]
+ }
+}
+```
+
+**"Of The Day" Configuration**:
+```json
+{
+ "of_the_day": {
+ "enabled": true,
+ "sources": ["word_of_the_day", "bible_verse"],
+ "rotation_enabled": true
+ }
+}
+```
+
+**Feature Controls**:
+- Enable/disable toggles for each feature
+- Configuration forms for each feature
+- Real-time preview where applicable
+- Input validation and error handling
+
+---
+
+### 🎵 Music Tab
+
+**Purpose**: Configure music display integration with Spotify and YouTube Music.
+
+**Configuration Options**:
+
+```json
+{
+ "music": {
+ "enabled": true,
+ "preferred_source": "spotify",
+ "POLLING_INTERVAL_SECONDS": 2,
+ "spotify": {
+ "client_id": "your_spotify_client_id",
+ "client_secret": "your_spotify_client_secret",
+ "redirect_uri": "http://localhost:8888/callback"
+ },
+ "ytm": {
+ "enabled": true
+ }
+ }
+}
+```
+
+**Settings**:
+- **Enable Music**: Toggle music display
+- **Preferred Source**: Choose primary music source
+ - Spotify
+ - YouTube Music
+- **Polling Interval**: Update frequency for track info
+- **Spotify Configuration**:
+ - Client ID (from Spotify Developer Dashboard)
+ - Client Secret (secure input)
+ - Redirect URI for OAuth
+- **YouTube Music Configuration**:
+ - Enable/disable YTM integration
+
+**Features**:
+- Currently playing track display
+- Artist and album information
+- Album artwork display
+- Progress bar
+- Play/pause status
+- Automatic source switching
+- Authentication status indicators
+
+---
+
+### 📅 Calendar Tab
+
+**Purpose**: Configure Google Calendar integration for event display.
+
+**Configuration Options**:
+
+```json
+{
+ "calendar": {
+ "enabled": true,
+ "credentials_file": "credentials.json",
+ "token_file": "token.pickle",
+ "calendars": ["primary", "birthdays"],
+ "max_events": 3,
+ "update_interval": 300
+ }
+}
+```
+
+**Settings**:
+- **Enable Calendar**: Toggle calendar display
+- **Credentials File**: Google API credentials file path
+- **Token File**: OAuth token storage file
+- **Calendars**: List of calendar names to display
+- **Max Events**: Maximum number of events to show
+- **Update Interval**: Event refresh frequency
+
+**Features**:
+- Multiple calendar support
+- Upcoming events display
+- All-day event handling
+- Timezone-aware event times
+- Google OAuth integration
+- Authentication status display
+
+---
+
+### 📰 News Tab
+
+**Purpose**: Manage RSS news feeds and display configuration.
+
+**Configuration Options**:
+
+```json
+{
+ "news_manager": {
+ "enabled": true,
+ "enabled_feeds": ["NFL", "NBA", "TOP SPORTS"],
+ "custom_feeds": {
+ "TECH NEWS": "https://feeds.feedburner.com/TechCrunch"
+ },
+ "headlines_per_feed": 2,
+ "scroll_speed": 2,
+ "update_interval": 300,
+ "dynamic_duration": true
+ }
+}
+```
+
+**Built-in Feeds**:
+- MLB (ESPN MLB News)
+- NFL (ESPN NFL News)
+- NCAA FB (ESPN College Football)
+- NHL (ESPN NHL News)
+- NBA (ESPN NBA News)
+- TOP SPORTS (ESPN Top Sports)
+- BIG10 (ESPN Big Ten Blog)
+- NCAA (ESPN NCAA News)
+
+**Features**:
+- **Feed Management**:
+ - Enable/disable built-in feeds
+ - Add custom RSS feeds
+ - Remove custom feeds
+ - Feed URL validation
+
+- **Display Configuration**:
+ - Headlines per feed
+ - Scroll speed adjustment
+ - Update interval setting
+ - Dynamic duration control
+
+- **Custom Feeds**:
+ - Add custom RSS feed URLs
+ - Custom feed name assignment
+ - Real-time feed validation
+ - Delete custom feeds
+
+**Form Controls**:
+- Checkboxes for built-in feeds
+- Text inputs for custom feed names and URLs
+- Add/remove buttons for custom feeds
+- Sliders for numeric settings
+- Real-time validation feedback
+
+---
+
+### 🔑 API Keys Tab
+
+**Purpose**: Secure management of API keys for various services.
+
+**Supported Services**:
+- **Weather**: OpenWeatherMap API key
+- **YouTube**: YouTube Data API v3 key
+- **Spotify**: Client ID and Client Secret
+- **Sports**: ESPN API keys (if required)
+- **News**: RSS feed API keys (if required)
+
+**Security Features**:
+- Password-type input fields for sensitive data
+- Masked display of existing keys
+- Secure transmission to server
+- No client-side storage of keys
+- Server-side encryption
+
+**Key Management**:
+- Add new API keys
+- Update existing keys
+- Remove unused keys
+- Test key validity
+- Status indicators for each service
+
+**Form Features**:
+- Service-specific input sections
+- Secure input fields
+- Save/update buttons
+- Validation feedback
+- Help text for obtaining keys
+
+---
+
+### 🎨 Editor Tab
+
+**Purpose**: Visual display editor for creating custom layouts and content.
+
+**Features** (Planned/In Development):
+- Visual layout designer
+- Drag-and-drop interface
+- Real-time preview
+- Custom content creation
+- Layout templates
+- Color picker
+- Font selection
+- Animation controls
+
+**Current Implementation**:
+- Placeholder for future visual editor
+- Link to configuration documentation
+- Manual layout guidance
+
+---
+
+### ⚡ Actions Tab
+
+**Purpose**: System control and maintenance actions.
+
+**Available Actions**:
+
+**Service Control**:
+- **Start Display**: Start the LED matrix display service
+- **Stop Display**: Stop the display service gracefully
+- **Restart Display**: Restart the display service
+- **Service Status**: Check current service status
+
+**System Control**:
+- **Reboot System**: Safely reboot the Raspberry Pi
+- **Shutdown System**: Safely shutdown the system
+- **Restart Web Interface**: Restart the web interface
+
+**Maintenance**:
+- **Git Pull**: Update LEDMatrix from repository
+- **Clear Cache**: Clear all cached data
+- **Reset Configuration**: Reset to default configuration
+- **Backup Configuration**: Create configuration backup
+
+**Features**:
+- Confirmation dialogs for destructive actions
+- Real-time action feedback
+- Progress indicators
+- Error handling and reporting
+- Safe shutdown procedures
+
+**Safety Features**:
+- Confirmation prompts for system actions
+- Graceful service stopping
+- Cache cleanup before restarts
+- Configuration backup before resets
+
+---
+
+### 📝 Raw JSON Tab
+
+**Purpose**: Direct JSON configuration editing with advanced features.
+
+**Features**:
+
+**JSON Editor**:
+- Syntax highlighting
+- Line numbers
+- Monospace font
+- Auto-indentation
+- Bracket matching
+
+**Validation**:
+- Real-time JSON syntax validation
+- Color-coded status indicators:
+ - Green: Valid JSON
+ - Red: Invalid JSON
+ - Yellow: Warning/Incomplete
+- Detailed error messages with line numbers
+- Error highlighting
+
+**Tools**:
+- **Format JSON**: Automatic formatting and indentation
+- **Validate**: Manual validation trigger
+- **Save**: Save configuration changes
+- **Reset**: Restore from last saved version
+
+**Status Display**:
+- Current validation status
+- Error count and details
+- Character count
+- Line count
+
+**Advanced Features**:
+- Undo/redo functionality
+- Find and replace
+- Configuration backup before changes
+- Automatic save prompts
+- Conflict detection
+
+---
+
+### 📋 Logs Tab
+
+**Purpose**: View system logs and troubleshooting information.
+
+**Log Sources**:
+- **System Logs**: General system messages
+- **Service Logs**: LED matrix service logs
+- **Web Interface Logs**: Web UI operation logs
+- **Error Logs**: Error and exception logs
+- **API Logs**: External API call logs
+
+**Features**:
+- **Real-time Updates**: Auto-refresh log display
+- **Log Filtering**: Filter by log level or source
+- **Search**: Search through log entries
+- **Download**: Download logs for offline analysis
+- **Clear**: Clear log display (not files)
+
+**Log Levels**:
+- DEBUG: Detailed diagnostic information
+- INFO: General information messages
+- WARNING: Warning messages
+- ERROR: Error messages
+- CRITICAL: Critical error messages
+
+**Controls**:
+- Refresh button for manual updates
+- Auto-refresh toggle
+- Log level filter dropdown
+- Search input box
+- Clear display button
+- Download logs button
+
+---
+
+## Advanced Features
+
+### WebSocket Integration
+
+The web interface uses WebSocket connections for real-time updates:
+
+- **Live Preview**: Real-time display preview updates
+- **System Monitoring**: Live CPU, memory, and temperature data
+- **Status Updates**: Real-time service status changes
+- **Notifications**: Instant feedback for user actions
+
+### Responsive Design
+
+The interface adapts to different screen sizes:
+
+- **Desktop**: Full tabbed interface with sidebar
+- **Tablet**: Responsive grid layout
+- **Mobile**: Stacked layout with collapsible tabs
+- **Touch-Friendly**: Large buttons and touch targets
+
+### Error Handling
+
+Comprehensive error handling throughout:
+
+- **Form Validation**: Client-side and server-side validation
+- **Network Errors**: Graceful handling of connection issues
+- **API Failures**: Fallback displays and retry mechanisms
+- **Configuration Errors**: Detailed error messages and recovery options
+
+### Performance Optimization
+
+- **Lazy Loading**: Tabs load content on demand
+- **Caching**: Client-side caching of configuration data
+- **Compression**: Gzip compression for faster loading
+- **Minification**: Optimized CSS and JavaScript
+
+---
+
+## Usage Tips
+
+### Getting Started
+1. Access the web interface at `http://your-pi-ip:5001`
+2. Start with the Overview tab to check system status
+3. Configure basic settings in Display and Schedule tabs
+4. Add API keys in the API Keys tab
+5. Enable desired features in their respective tabs
+
+### Best Practices
+- **Regular Monitoring**: Check the Overview tab regularly
+- **Configuration Backup**: Use Raw JSON tab to backup configuration
+- **Gradual Changes**: Make incremental configuration changes
+- **Test Mode**: Use test modes when available for new configurations
+- **Log Review**: Check logs when troubleshooting issues
+
+### Troubleshooting
+- **Connection Issues**: Check WebSocket status indicator
+- **Configuration Problems**: Use Raw JSON tab for validation
+- **Service Issues**: Use Actions tab to restart services
+- **Performance Issues**: Monitor CPU and memory in Overview tab
+
+---
+
+## API Reference
+
+The web interface exposes several API endpoints for programmatic access:
+
+### Configuration Endpoints
+- `GET /api/config` - Get current configuration
+- `POST /api/config` - Update configuration
+- `GET /api/config/validate` - Validate configuration
+
+### System Endpoints
+- `GET /api/system/status` - Get system status
+- `POST /api/system/action` - Execute system actions
+- `GET /api/system/logs` - Get system logs
+
+### Display Endpoints
+- `GET /api/display/preview` - Get display preview image
+- `POST /api/display/control` - Control display state
+- `GET /api/display/modes` - Get available display modes
+
+### Service Endpoints
+- `GET /api/service/status` - Get service status
+- `POST /api/service/control` - Control services
+- `GET /api/service/logs` - Get service logs
+
+---
+
+The LEDMatrix Web Interface V2 provides complete control over your LED matrix display system through an intuitive, modern web interface. Every aspect of the system can be monitored, configured, and controlled remotely through any web browser.
diff --git a/docs/WIKI_ARCHITECTURE.md b/docs/WIKI_ARCHITECTURE.md
new file mode 100644
index 000000000..511c88066
--- /dev/null
+++ b/docs/WIKI_ARCHITECTURE.md
@@ -0,0 +1,587 @@
+# System Architecture
+
+The LEDMatrix system is built with a modular, extensible architecture that separates concerns and allows for easy maintenance and extension. This guide explains how all components work together.
+
+## System Overview
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ LEDMatrix System │
+├─────────────────────────────────────────────────────────────┤
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ Display │ │ Display │ │ Display │ │
+│ │ Controller │ │ Manager │ │ Managers │ │
+│ │ │ │ │ │ │ │
+│ │ • Main Loop │ │ • Hardware │ │ • Weather │ │
+│ │ • Rotation │ │ • Rendering │ │ • Stocks │ │
+│ │ • Scheduling│ │ • Fonts │ │ • Sports │ │
+│ │ • Live Mode │ │ • Graphics │ │ • Music │ │
+│ └─────────────┘ └─────────────┘ └─────────────┘ │
+├─────────────────────────────────────────────────────────────┤
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ Config │ │ Cache │ │ Web │ │
+│ │ Manager │ │ Manager │ │ Interface │ │
+│ │ │ │ │ │ │ │
+│ │ • Settings │ │ • Data │ │ • Control │ │
+│ │ • Validation│ │ • Persistence│ │ • Status │ │
+│ │ • Loading │ │ • Fallbacks │ │ • Settings │ │
+│ └─────────────┘ └─────────────┘ └─────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+## Core Components
+
+### 1. Display Controller (`src/display_controller.py`)
+
+**Purpose**: Main orchestrator that manages the entire display system.
+
+**Responsibilities**:
+- Initialize all display managers
+- Control display rotation and timing
+- Handle live game priority
+- Manage system scheduling
+- Coordinate data updates
+- Handle error recovery
+
+**Key Methods**:
+```python
+class DisplayController:
+ def __init__(self):
+ # Initialize all managers and configuration
+
+ def run(self):
+ # Main display loop
+
+ def _update_modules(self):
+ # Update all enabled modules
+
+ def _check_live_games(self):
+ # Check for live games and prioritize
+
+ def _rotate_team_games(self, sport):
+ # Rotate through team games
+```
+
+**Data Flow**:
+1. Load configuration
+2. Initialize display managers
+3. Start main loop
+4. Check for live games
+5. Rotate through enabled displays
+6. Handle scheduling and timing
+
+### 2. Display Manager (`src/display_manager.py`)
+
+**Purpose**: Low-level hardware interface and graphics rendering.
+
+**Responsibilities**:
+- Initialize RGB LED matrix hardware
+- Handle font loading and management
+- Provide drawing primitives
+- Manage display buffers
+- Handle hardware configuration
+- Provide text rendering utilities
+
+**Key Features**:
+```python
+class DisplayManager:
+ def __init__(self, config):
+ # Initialize hardware and fonts
+
+ def draw_text(self, text, x, y, color, font):
+ # Draw text on display
+
+ def update_display(self):
+ # Update physical display
+
+ def clear(self):
+ # Clear display
+
+ def draw_weather_icon(self, condition, x, y, size):
+ # Draw weather icons
+```
+
+**Hardware Interface**:
+- RGB Matrix library integration
+- GPIO pin management
+- PWM timing control
+- Double buffering for smooth updates
+- Font rendering (TTF and BDF)
+
+### 3. Configuration Manager (`src/config_manager.py`)
+
+**Purpose**: Load, validate, and manage system configuration.
+
+**Responsibilities**:
+- Load JSON configuration files
+- Validate configuration syntax
+- Provide default values
+- Handle configuration updates
+- Manage secrets and API keys
+
+**Configuration Sources**:
+```python
+class ConfigManager:
+ def load_config(self):
+ # Load main config.json
+
+ def load_secrets(self):
+ # Load config_secrets.json
+
+ def validate_config(self):
+ # Validate configuration
+
+ def get_defaults(self):
+ # Provide default values
+```
+
+**Configuration Structure**:
+- Main settings in `config/config.json`
+- API keys in `config/config_secrets.json`
+- Validation and error handling
+- Default value fallbacks
+
+### 4. Cache Manager (`src/cache_manager.py`)
+
+**Purpose**: Intelligent data caching to reduce API calls and improve performance.
+
+**Responsibilities**:
+- Store API responses
+- Manage cache expiration
+- Handle cache persistence
+- Provide fallback data
+- Optimize storage usage
+
+**Cache Strategy**:
+```python
+class CacheManager:
+ def get(self, key):
+ # Retrieve cached data
+
+ def set(self, key, data, ttl):
+ # Store data with expiration
+
+ def is_valid(self, key):
+ # Check if cache is still valid
+
+ def clear_expired(self):
+ # Remove expired cache entries
+```
+
+**Cache Locations** (in order of preference):
+1. `~/.ledmatrix_cache/` (user's home directory)
+2. `/var/cache/ledmatrix/` (system cache directory)
+3. `/tmp/ledmatrix_cache/` (temporary directory)
+
+## Display Manager Architecture
+
+### Manager Interface
+
+All display managers follow a consistent interface:
+
+```python
+class BaseManager:
+ def __init__(self, config, display_manager):
+ self.config = config
+ self.display_manager = display_manager
+ self.cache_manager = CacheManager()
+
+ def update_data(self):
+ """Fetch and process new data"""
+ pass
+
+ def display(self, force_clear=False):
+ """Render content to display"""
+ pass
+
+ def is_enabled(self):
+ """Check if manager is enabled"""
+ return self.config.get('enabled', False)
+
+ def get_duration(self):
+ """Get display duration"""
+ return self.config.get('duration', 30)
+```
+
+### Data Flow Pattern
+
+Each manager follows this pattern:
+
+1. **Initialization**: Load configuration and setup
+2. **Data Fetching**: Retrieve data from APIs or local sources
+3. **Caching**: Store data using CacheManager
+4. **Processing**: Transform raw data into display format
+5. **Rendering**: Use DisplayManager to show content
+6. **Cleanup**: Return control to main controller
+
+### Error Handling
+
+- **API Failures**: Fall back to cached data
+- **Network Issues**: Use last known good data
+- **Invalid Data**: Filter out bad entries
+- **Hardware Errors**: Graceful degradation
+- **Configuration Errors**: Use safe defaults
+
+## Sports Manager Architecture
+
+### Sports Manager Pattern
+
+Each sport follows a three-manager pattern:
+
+```python
+# Live games (currently playing)
+class NHLLiveManager(BaseSportsManager):
+ def fetch_games(self):
+ # Get currently playing games
+
+ def display_games(self):
+ # Show live scores and status
+
+# Recent games (completed)
+class NHLRecentManager(BaseSportsManager):
+ def fetch_games(self):
+ # Get recently completed games
+
+ def display_games(self):
+ # Show final scores
+
+# Upcoming games (scheduled)
+class NHLUpcomingManager(BaseSportsManager):
+ def fetch_games(self):
+ # Get scheduled games
+
+ def display_games(self):
+ # Show game times and matchups
+```
+
+### Base Sports Manager
+
+Common functionality shared by all sports:
+
+```python
+class BaseSportsManager:
+ def __init__(self, config, display_manager):
+ # Common initialization
+
+ def fetch_espn_data(self, sport, endpoint):
+ # Fetch from ESPN API
+
+ def process_game_data(self, games):
+ # Process raw game data
+
+ def display_game(self, game):
+ # Display individual game
+
+ def get_team_logo(self, team_abbr):
+ # Load team logo
+
+ def format_score(self, score):
+ # Format score display
+```
+
+### ESPN API Integration
+
+All sports use ESPN's API for data:
+
+```python
+def fetch_espn_data(self, sport, endpoint):
+ url = f"http://site.api.espn.com/apis/site/v2/sports/{sport}/{endpoint}"
+ response = requests.get(url)
+ return response.json()
+```
+
+**Supported Sports**:
+- NHL (hockey)
+- NBA (basketball)
+- MLB (baseball)
+- NFL (football)
+- NCAA Football
+- NCAA Basketball
+- NCAA Baseball
+- Soccer (multiple leagues)
+- MiLB (minor league baseball)
+
+## Financial Data Architecture
+
+### Stock Manager
+
+```python
+class StockManager:
+ def __init__(self, config, display_manager):
+ # Initialize stock and crypto settings
+
+ def fetch_stock_data(self, symbol):
+ # Fetch from Yahoo Finance
+
+ def fetch_crypto_data(self, symbol):
+ # Fetch crypto data
+
+ def display_stocks(self):
+ # Show stock ticker
+
+ def display_crypto(self):
+ # Show crypto prices
+```
+
+### Stock News Manager
+
+```python
+class StockNewsManager:
+ def __init__(self, config, display_manager):
+ # Initialize news settings
+
+ def fetch_news(self, symbols):
+ # Fetch financial news
+
+ def display_news(self):
+ # Show news headlines
+```
+
+## Weather Architecture
+
+### Weather Manager
+
+```python
+class WeatherManager:
+ def __init__(self, config, display_manager):
+ # Initialize weather settings
+
+ def fetch_weather(self):
+ # Fetch from OpenWeatherMap
+
+ def display_current_weather(self):
+ # Show current conditions
+
+ def display_hourly_forecast(self):
+ # Show hourly forecast
+
+ def display_daily_forecast(self):
+ # Show daily forecast
+```
+
+### Weather Icons
+
+```python
+class WeatherIcons:
+ def __init__(self):
+ # Load weather icon definitions
+
+ def get_icon(self, condition):
+ # Get icon for weather condition
+
+ def draw_icon(self, condition, x, y, size):
+ # Draw weather icon
+```
+
+## Music Architecture
+
+### Music Manager
+
+```python
+class MusicManager:
+ def __init__(self, display_manager, config):
+ # Initialize music settings
+
+ def start_polling(self):
+ # Start background polling
+
+ def update_music_display(self):
+ # Update music information
+
+ def display_spotify(self):
+ # Display Spotify info
+
+ def display_ytm(self):
+ # Display YouTube Music info
+```
+
+### Spotify Client
+
+```python
+class SpotifyClient:
+ def __init__(self, config):
+ # Initialize Spotify API
+
+ def authenticate(self):
+ # Handle OAuth authentication
+
+ def get_current_track(self):
+ # Get currently playing track
+```
+
+### YouTube Music Client
+
+```python
+class YTMClient:
+ def __init__(self, config):
+ # Initialize YTM companion server
+
+ def get_current_track(self):
+ # Get current track from YTMD
+```
+
+## Web Interface Architecture
+
+### Web Interface
+
+```python
+class WebInterface:
+ def __init__(self, config):
+ # Initialize Flask app
+
+ def start_server(self):
+ # Start web server
+
+ def get_status(self):
+ # Get system status
+
+ def control_display(self, action):
+ # Control display actions
+```
+
+**Features**:
+- System status monitoring
+- Display control (start/stop)
+- Configuration management
+- Service management
+- Real-time status updates
+
+## Service Architecture
+
+### Systemd Service
+
+```ini
+[Unit]
+Description=LEDMatrix Display Service
+After=network.target
+
+[Service]
+Type=simple
+User=root
+WorkingDirectory=/home/ledpi/LEDMatrix
+ExecStart=/usr/bin/python3 display_controller.py
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+**Service Features**:
+- Automatic startup
+- Crash recovery
+- Log management
+- Resource monitoring
+
+## Data Flow Architecture
+
+### 1. Configuration Loading
+
+```
+config.json → ConfigManager → DisplayController → Display Managers
+```
+
+### 2. Data Fetching
+
+```
+API Sources → CacheManager → Display Managers → Display Manager
+```
+
+### 3. Display Rendering
+
+```
+Display Managers → Display Manager → RGB Matrix → LED Display
+```
+
+### 4. User Control
+
+```
+Web Interface → Display Controller → Display Managers
+```
+
+## Performance Architecture
+
+### Caching Strategy
+
+1. **API Response Caching**: Store API responses with TTL
+2. **Processed Data Caching**: Cache processed display data
+3. **Font Caching**: Cache loaded fonts
+4. **Image Caching**: Cache team logos and icons
+
+### Resource Management
+
+1. **Memory Usage**: Monitor and optimize memory usage
+2. **CPU Usage**: Minimize processing overhead
+3. **Network Usage**: Optimize API calls
+4. **Storage Usage**: Manage cache storage
+
+### Error Recovery
+
+1. **API Failures**: Use cached data
+2. **Network Issues**: Retry with exponential backoff
+3. **Hardware Errors**: Graceful degradation
+4. **Configuration Errors**: Use safe defaults
+
+## Extension Architecture
+
+### Adding New Display Managers
+
+1. **Create Manager Class**: Extend base manager pattern
+2. **Add Configuration**: Add to config.json
+3. **Register in Controller**: Add to DisplayController
+4. **Add Assets**: Include logos, icons, fonts
+5. **Test Integration**: Verify with main system
+
+### Example New Manager
+
+```python
+class CustomManager(BaseManager):
+ def __init__(self, config, display_manager):
+ super().__init__(config, display_manager)
+
+ def update_data(self):
+ # Fetch custom data
+
+ def display(self, force_clear=False):
+ # Display custom content
+```
+
+## Security Architecture
+
+### API Key Management
+
+1. **Separate Secrets**: Store in config_secrets.json
+2. **Environment Variables**: Support for env vars
+3. **Access Control**: Restrict file permissions
+4. **Key Rotation**: Support for key updates
+
+### Network Security
+
+1. **HTTPS Only**: Use secure API endpoints
+2. **Rate Limiting**: Respect API limits
+3. **Error Handling**: Don't expose sensitive data
+4. **Logging**: Secure log management
+
+## Monitoring Architecture
+
+### Logging System
+
+```python
+import logging
+
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(levelname)s:%(name)s:%(message)s'
+)
+```
+
+### Health Monitoring
+
+1. **API Health**: Monitor API availability
+2. **Display Health**: Monitor display functionality
+3. **Cache Health**: Monitor cache performance
+4. **System Health**: Monitor system resources
+
+---
+
+*This architecture provides a solid foundation for the LEDMatrix system while maintaining flexibility for future enhancements and customizations.*
\ No newline at end of file
diff --git a/docs/WIKI_CONFIGURATION.md b/docs/WIKI_CONFIGURATION.md
new file mode 100644
index 000000000..c5febc662
--- /dev/null
+++ b/docs/WIKI_CONFIGURATION.md
@@ -0,0 +1,654 @@
+# Configuration Guide
+
+The LEDMatrix system is configured through JSON files that control every aspect of the display. This guide covers all configuration options and their effects.
+
+## Configuration Files
+
+### Main Configuration (`config/config.json`)
+Contains all non-sensitive settings for the system.
+
+### Secrets Configuration (`config/config_secrets.json`)
+Contains API keys and sensitive credentials.
+
+## System Configuration
+
+### Display Hardware Settings
+
+```json
+{
+ "display": {
+ "hardware": {
+ "rows": 32,
+ "cols": 64,
+ "chain_length": 2,
+ "parallel": 1,
+ "brightness": 95,
+ "hardware_mapping": "adafruit-hat-pwm",
+ "scan_mode": 0,
+ "pwm_bits": 9,
+ "pwm_dither_bits": 1,
+ "pwm_lsb_nanoseconds": 130,
+ "disable_hardware_pulsing": false,
+ "inverse_colors": false,
+ "show_refresh_rate": false,
+ "limit_refresh_rate_hz": 120
+ },
+ "runtime": {
+ "gpio_slowdown": 3
+ }
+ }
+}
+```
+
+**Hardware Settings Explained**:
+- **`rows`/`cols`**: Physical LED matrix dimensions (32x64 for 2 panels)
+- **`chain_length`**: Number of LED panels connected (2 for 128x32 total)
+- **`parallel`**: Number of parallel chains (usually 1)
+- **`brightness`**: Display brightness (0-100)
+- **`hardware_mapping`**:
+ - `"adafruit-hat-pwm"`: With jumper mod (recommended)
+ - `"adafruit-hat"`: Without jumper mod
+- **`pwm_bits`**: Color depth (8-11, higher = better colors)
+- **`gpio_slowdown`**: Timing adjustment (3 for Pi 3, 4 for Pi 4)
+
+### Display Durations
+
+```json
+{
+ "display": {
+ "display_durations": {
+ "clock": 15,
+ "weather": 30,
+ "stocks": 30,
+ "hourly_forecast": 30,
+ "daily_forecast": 30,
+ "stock_news": 20,
+ "odds_ticker": 60,
+ "nhl_live": 30,
+ "nhl_recent": 30,
+ "nhl_upcoming": 30,
+ "nba_live": 30,
+ "nba_recent": 30,
+ "nba_upcoming": 30,
+ "nfl_live": 30,
+ "nfl_recent": 30,
+ "nfl_upcoming": 30,
+ "ncaa_fb_live": 30,
+ "ncaa_fb_recent": 30,
+ "ncaa_fb_upcoming": 30,
+ "ncaa_baseball_live": 30,
+ "ncaa_baseball_recent": 30,
+ "ncaa_baseball_upcoming": 30,
+ "calendar": 30,
+ "youtube": 30,
+ "mlb_live": 30,
+ "mlb_recent": 30,
+ "mlb_upcoming": 30,
+ "milb_live": 30,
+ "milb_recent": 30,
+ "milb_upcoming": 30,
+ "text_display": 10,
+ "soccer_live": 30,
+ "soccer_recent": 30,
+ "soccer_upcoming": 30,
+ "ncaam_basketball_live": 30,
+ "ncaam_basketball_recent": 30,
+ "ncaam_basketball_upcoming": 30,
+ "music": 30,
+ "of_the_day": 40
+ }
+ }
+}
+```
+
+**Duration Settings**:
+- Each value controls how long (in seconds) that display mode shows
+- Higher values = more time for that content
+- Total rotation time = sum of all enabled durations
+
+### System Settings
+
+```json
+{
+ "web_display_autostart": true,
+ "schedule": {
+ "enabled": true,
+ "start_time": "07:00",
+ "end_time": "23:00"
+ },
+ "timezone": "America/Chicago",
+ "location": {
+ "city": "Dallas",
+ "state": "Texas",
+ "country": "US"
+ }
+}
+```
+
+**System Settings Explained**:
+- **`web_display_autostart`**: Start web interface automatically
+- **`schedule`**: Control when display is active
+- **`timezone`**: System timezone for accurate times
+- **`location`**: Default location for weather and other location-based services
+
+## Display Manager Configurations
+
+### Clock Configuration
+
+```json
+{
+ "clock": {
+ "enabled": false,
+ "format": "%I:%M %p",
+ "update_interval": 1
+ }
+}
+```
+
+**Clock Settings**:
+- **`enabled`**: Enable/disable clock display
+- **`format`**: Time format string (Python strftime)
+- **`update_interval`**: Update frequency in seconds
+
+**Common Time Formats**:
+- `"%I:%M %p"` → `12:34 PM`
+- `"%H:%M"` → `14:34`
+- `"%I:%M:%S %p"` → `12:34:56 PM`
+
+### Weather Configuration
+
+```json
+{
+ "weather": {
+ "enabled": false,
+ "update_interval": 1800,
+ "units": "imperial",
+ "display_format": "{temp}°F\n{condition}"
+ }
+}
+```
+
+**Weather Settings**:
+- **`enabled`**: Enable/disable weather display
+- **`update_interval`**: Update frequency in seconds (1800 = 30 minutes)
+- **`units`**: `"imperial"` (Fahrenheit) or `"metric"` (Celsius)
+- **`display_format`**: Custom format string for weather display
+
+**Weather Display Modes**:
+- Current weather with icon
+- Hourly forecast (next 24 hours)
+- Daily forecast (next 7 days)
+
+### Stocks Configuration
+
+```json
+{
+ "stocks": {
+ "enabled": false,
+ "update_interval": 600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "toggle_chart": false,
+ "symbols": ["ASTS", "SCHD", "INTC", "NVDA", "T", "VOO", "SMCI"]
+ },
+ "crypto": {
+ "enabled": false,
+ "update_interval": 600,
+ "symbols": ["BTC-USD", "ETH-USD"]
+ }
+}
+```
+
+**Stock Settings**:
+- **`enabled`**: Enable/disable stock display
+- **`update_interval`**: Update frequency in seconds (600 = 10 minutes)
+- **`scroll_speed`**: Pixels per scroll update
+- **`scroll_delay`**: Delay between scroll updates
+- **`toggle_chart`**: Show/hide mini price charts
+- **`symbols`**: Array of stock symbols to display
+
+**Crypto Settings**:
+- **`enabled`**: Enable/disable crypto display
+- **`symbols`**: Array of crypto symbols (use `-USD` suffix)
+
+### Stock News Configuration
+
+```json
+{
+ "stock_news": {
+ "enabled": false,
+ "update_interval": 3600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "max_headlines_per_symbol": 1,
+ "headlines_per_rotation": 2
+ }
+}
+```
+
+**News Settings**:
+- **`enabled`**: Enable/disable news display
+- **`update_interval`**: Update frequency in seconds
+- **`max_headlines_per_symbol`**: Max headlines per stock
+- **`headlines_per_rotation`**: Headlines shown per rotation
+
+### Music Configuration
+
+```json
+{
+ "music": {
+ "enabled": true,
+ "preferred_source": "ytm",
+ "YTM_COMPANION_URL": "http://192.168.86.12:9863",
+ "POLLING_INTERVAL_SECONDS": 1
+ }
+}
+```
+
+**Music Settings**:
+- **`enabled`**: Enable/disable music display
+- **`preferred_source`**: `"spotify"` or `"ytm"`
+- **`YTM_COMPANION_URL`**: YouTube Music companion server URL
+- **`POLLING_INTERVAL_SECONDS`**: How often to check for updates
+
+### Calendar Configuration
+
+```json
+{
+ "calendar": {
+ "enabled": false,
+ "credentials_file": "credentials.json",
+ "token_file": "token.pickle",
+ "update_interval": 3600,
+ "max_events": 3,
+ "calendars": ["birthdays"]
+ }
+}
+```
+
+**Calendar Settings**:
+- **`enabled`**: Enable/disable calendar display
+- **`credentials_file`**: Google API credentials file
+- **`token_file`**: Authentication token file
+- **`update_interval`**: Update frequency in seconds
+- **`max_events`**: Maximum events to display
+- **`calendars`**: Array of calendar IDs to monitor
+
+## Sports Configurations
+
+### Common Sports Settings
+
+All sports managers share these common settings:
+
+```json
+{
+ "nhl_scoreboard": {
+ "enabled": false,
+ "live_priority": true,
+ "live_game_duration": 20,
+ "show_odds": true,
+ "test_mode": false,
+ "update_interval_seconds": 3600,
+ "live_update_interval": 30,
+ "recent_update_interval": 3600,
+ "upcoming_update_interval": 3600,
+ "show_favorite_teams_only": true,
+ "favorite_teams": ["TB"],
+ "logo_dir": "assets/sports/nhl_logos",
+ "show_records": true,
+ "display_modes": {
+ "nhl_live": true,
+ "nhl_recent": true,
+ "nhl_upcoming": true
+ }
+ }
+}
+```
+
+**Common Sports Settings**:
+- **`enabled`**: Enable/disable this sport
+- **`live_priority`**: Give live games priority over other content
+- **`live_game_duration`**: How long to show live games
+- **`show_odds`**: Display betting odds (where available)
+- **`test_mode`**: Use test data instead of live API
+- **`update_interval_seconds`**: How often to fetch new data
+- **`live_update_interval`**: How often to update live games
+- **`show_favorite_teams_only`**: Only show games for favorite teams
+- **`favorite_teams`**: Array of team abbreviations
+- **`logo_dir`**: Directory containing team logos
+- **`show_records`**: Display team win/loss records
+- **`display_modes`**: Enable/disable specific display modes
+
+### Football-Specific Settings
+
+NFL and NCAA Football use game-based fetching:
+
+```json
+{
+ "nfl_scoreboard": {
+ "enabled": false,
+ "recent_games_to_show": 0,
+ "upcoming_games_to_show": 2,
+ "favorite_teams": ["TB", "DAL"]
+ }
+}
+```
+
+**Football Settings**:
+- **`recent_games_to_show`**: Number of recent games to display
+- **`upcoming_games_to_show`**: Number of upcoming games to display
+
+### Soccer Configuration
+
+```json
+{
+ "soccer_scoreboard": {
+ "enabled": false,
+ "recent_game_hours": 168,
+ "favorite_teams": ["LIV"],
+ "leagues": ["eng.1", "esp.1", "ger.1", "ita.1", "fra.1", "uefa.champions", "usa.1"]
+ }
+}
+```
+
+**Soccer Settings**:
+- **`recent_game_hours`**: Hours back to show recent games
+- **`leagues`**: Array of league codes to monitor
+
+## Odds Ticker Configuration
+
+```json
+{
+ "odds_ticker": {
+ "enabled": false,
+ "show_favorite_teams_only": true,
+ "games_per_favorite_team": 1,
+ "max_games_per_league": 5,
+ "show_odds_only": false,
+ "sort_order": "soonest",
+ "enabled_leagues": ["nfl", "mlb", "ncaa_fb", "milb"],
+ "update_interval": 3600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "loop": true,
+ "future_fetch_days": 50,
+ "show_channel_logos": true
+ }
+}
+```
+
+**Odds Ticker Settings**:
+- **`enabled`**: Enable/disable odds ticker
+- **`show_favorite_teams_only`**: Only show odds for favorite teams
+- **`games_per_favorite_team`**: Games per team to show
+- **`max_games_per_league`**: Maximum games per league
+- **`enabled_leagues`**: Leagues to include in ticker
+- **`sort_order`**: `"soonest"` or `"latest"`
+- **`future_fetch_days`**: Days ahead to fetch games
+- **`show_channel_logos`**: Display broadcast network logos
+
+## Custom Display Configurations
+
+### Text Display
+
+```json
+{
+ "text_display": {
+ "enabled": false,
+ "text": "Subscribe to ChuckBuilds",
+ "font_path": "assets/fonts/press-start-2p.ttf",
+ "font_size": 8,
+ "scroll": true,
+ "scroll_speed": 40,
+ "text_color": [255, 0, 0],
+ "background_color": [0, 0, 0],
+ "scroll_gap_width": 32
+ }
+}
+```
+
+**Text Display Settings**:
+- **`enabled`**: Enable/disable text display
+- **`text`**: Text to display
+- **`font_path`**: Path to TTF font file
+- **`font_size`**: Font size in pixels
+- **`scroll`**: Enable/disable scrolling
+- **`scroll_speed`**: Scroll speed in pixels
+- **`text_color`**: RGB color for text
+- **`background_color`**: RGB color for background
+- **`scroll_gap_width`**: Gap between text repetitions
+
+### YouTube Display
+
+```json
+{
+ "youtube": {
+ "enabled": false,
+ "update_interval": 3600
+ }
+}
+```
+
+**YouTube Settings**:
+- **`enabled`**: Enable/disable YouTube stats
+- **`update_interval`**: Update frequency in seconds
+
+### Of The Day Display
+
+```json
+{
+ "of_the_day": {
+ "enabled": true,
+ "display_rotate_interval": 20,
+ "update_interval": 3600,
+ "subtitle_rotate_interval": 10,
+ "category_order": ["word_of_the_day", "slovenian_word_of_the_day", "bible_verse_of_the_day"],
+ "categories": {
+ "word_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/word_of_the_day.json",
+ "display_name": "Word of the Day"
+ },
+ "slovenian_word_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/slovenian_word_of_the_day.json",
+ "display_name": "Slovenian Word of the Day"
+ },
+ "bible_verse_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/bible_verse_of_the_day.json",
+ "display_name": "Bible Verse of the Day"
+ }
+ }
+ }
+}
+```
+
+**Of The Day Settings**:
+- **`enabled`**: Enable/disable of the day display
+- **`display_rotate_interval`**: How long to show each category
+- **`update_interval`**: Update frequency in seconds
+- **`subtitle_rotate_interval`**: How long to show subtitles
+- **`category_order`**: Order of categories to display
+- **`categories`**: Configuration for each category
+
+## API Configuration (config_secrets.json)
+
+### Weather API
+
+```json
+{
+ "weather": {
+ "api_key": "your_openweathermap_api_key"
+ }
+}
+```
+
+### YouTube API
+
+```json
+{
+ "youtube": {
+ "api_key": "your_youtube_api_key",
+ "channel_id": "your_channel_id"
+ }
+}
+```
+
+### Music APIs
+
+```json
+{
+ "music": {
+ "SPOTIFY_CLIENT_ID": "your_spotify_client_id",
+ "SPOTIFY_CLIENT_SECRET": "your_spotify_client_secret",
+ "SPOTIFY_REDIRECT_URI": "http://127.0.0.1:8888/callback"
+ }
+}
+```
+
+## Configuration Best Practices
+
+### Performance Optimization
+
+1. **Update Intervals**: Balance between fresh data and API limits
+ - Weather: 1800 seconds (30 minutes)
+ - Stocks: 600 seconds (10 minutes)
+ - Sports: 3600 seconds (1 hour)
+ - Music: 1 second (real-time)
+
+2. **Display Durations**: Balance content visibility
+ - Live sports: 20-30 seconds
+ - Weather: 30 seconds
+ - Stocks: 30-60 seconds
+ - Clock: 15 seconds
+
+3. **Favorite Teams**: Reduce API calls by focusing on specific teams
+
+### Caching Strategy
+
+```json
+{
+ "cache_settings": {
+ "persistent_cache": true,
+ "cache_directory": "/var/cache/ledmatrix",
+ "fallback_cache": "/tmp/ledmatrix_cache"
+ }
+}
+```
+
+### Error Handling
+
+- Failed API calls use cached data
+- Network timeouts are handled gracefully
+- Invalid data is filtered out
+- Logging provides debugging information
+
+## Configuration Validation
+
+### Required Settings
+
+1. **Hardware Configuration**: Must match your physical setup
+2. **API Keys**: Required for enabled services
+3. **Location**: Required for weather and timezone
+4. **Team Abbreviations**: Must match official team codes
+
+### Optional Settings
+
+1. **Display Durations**: Defaults provided if missing
+2. **Update Intervals**: Defaults provided if missing
+3. **Favorite Teams**: Can be empty for all teams
+4. **Custom Text**: Can be any string
+
+## Configuration Examples
+
+### Minimal Configuration
+
+```json
+{
+ "display": {
+ "hardware": {
+ "rows": 32,
+ "cols": 64,
+ "chain_length": 2,
+ "brightness": 90,
+ "hardware_mapping": "adafruit-hat-pwm"
+ }
+ },
+ "clock": {
+ "enabled": true
+ },
+ "weather": {
+ "enabled": true
+ }
+}
+```
+
+### Full Sports Configuration
+
+```json
+{
+ "nhl_scoreboard": {
+ "enabled": true,
+ "favorite_teams": ["TB", "DAL"],
+ "show_favorite_teams_only": true
+ },
+ "nba_scoreboard": {
+ "enabled": true,
+ "favorite_teams": ["DAL"],
+ "show_favorite_teams_only": true
+ },
+ "nfl_scoreboard": {
+ "enabled": true,
+ "favorite_teams": ["TB", "DAL"],
+ "show_favorite_teams_only": true
+ },
+ "odds_ticker": {
+ "enabled": true,
+ "enabled_leagues": ["nfl", "nba", "mlb"]
+ }
+}
+```
+
+### Financial Focus Configuration
+
+```json
+{
+ "stocks": {
+ "enabled": true,
+ "symbols": ["AAPL", "MSFT", "GOOGL", "TSLA", "NVDA"],
+ "update_interval": 300
+ },
+ "crypto": {
+ "enabled": true,
+ "symbols": ["BTC-USD", "ETH-USD", "ADA-USD"]
+ },
+ "stock_news": {
+ "enabled": true,
+ "update_interval": 1800
+ }
+}
+```
+
+## Troubleshooting Configuration
+
+### Common Issues
+
+1. **No Display**: Check hardware configuration
+2. **No Data**: Verify API keys and network
+3. **Wrong Times**: Check timezone setting
+4. **Performance Issues**: Reduce update frequencies
+
+### Validation Commands
+
+```bash
+# Validate JSON syntax
+python3 -m json.tool config/config.json
+
+# Check configuration loading
+python3 -c "from src.config_manager import ConfigManager; c = ConfigManager(); print('Config valid')"
+```
+
+---
+
+*For detailed information about specific display managers, see the [Display Managers](WIKI_DISPLAY_MANAGERS.md) page.*
\ No newline at end of file
diff --git a/docs/WIKI_DISPLAY_MANAGERS.md b/docs/WIKI_DISPLAY_MANAGERS.md
new file mode 100644
index 000000000..5c6a05a4e
--- /dev/null
+++ b/docs/WIKI_DISPLAY_MANAGERS.md
@@ -0,0 +1,501 @@
+# Display Managers Guide
+
+The LEDMatrix system uses a modular architecture where each feature is implemented as a separate "Display Manager". This guide covers all available display managers and their configuration options.
+
+## Overview
+
+Each display manager is responsible for:
+1. **Data Fetching**: Retrieving data from APIs or local sources
+2. **Data Processing**: Transforming raw data into displayable format
+3. **Display Rendering**: Creating visual content for the LED matrix
+4. **Caching**: Storing data to reduce API calls
+5. **Configuration**: Managing settings and preferences
+
+## Core Display Managers
+
+### 🕐 Clock Manager (`src/clock.py`)
+**Purpose**: Displays current time in various formats
+
+**Configuration**:
+```json
+{
+ "clock": {
+ "enabled": true,
+ "format": "%I:%M %p",
+ "update_interval": 1
+ }
+}
+```
+
+**Features**:
+- Real-time clock display
+- Configurable time format
+- Automatic timezone handling
+- Minimal resource usage
+
+**Display Format**: `12:34 PM`
+
+---
+
+### 🌤️ Weather Manager (`src/weather_manager.py`)
+**Purpose**: Displays current weather, hourly forecasts, and daily forecasts
+
+**Configuration**:
+```json
+{
+ "weather": {
+ "enabled": true,
+ "update_interval": 1800,
+ "units": "imperial",
+ "display_format": "{temp}°F\n{condition}"
+ }
+}
+```
+
+**Features**:
+- Current weather conditions
+- Hourly forecast (next 24 hours)
+- Daily forecast (next 7 days)
+- Weather icons and animations
+- UV index display
+- Wind speed and direction
+- Humidity and pressure data
+
+**Display Modes**:
+- Current weather with icon
+- Hourly forecast with temperature trend
+- Daily forecast with high/low temps
+
+---
+
+### 💰 Stock Manager (`src/stock_manager.py`)
+**Purpose**: Displays stock prices, crypto prices, and financial data
+
+**Configuration**:
+```json
+{
+ "stocks": {
+ "enabled": true,
+ "update_interval": 600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "toggle_chart": false,
+ "symbols": ["AAPL", "MSFT", "GOOGL", "TSLA"]
+ },
+ "crypto": {
+ "enabled": true,
+ "update_interval": 600,
+ "symbols": ["BTC-USD", "ETH-USD"]
+ }
+}
+```
+
+**Features**:
+- Real-time stock prices
+- Cryptocurrency prices
+- Price change indicators (green/red)
+- Percentage change display
+- Optional mini charts
+- Scrolling ticker format
+- Company/crypto logos
+
+**Data Sources**:
+- Yahoo Finance API for stocks
+- Yahoo Finance API for crypto
+- Automatic market hours detection
+
+---
+
+### 📰 Stock News Manager (`src/stock_news_manager.py`)
+**Purpose**: Displays financial news headlines for configured stocks
+
+**Configuration**:
+```json
+{
+ "stock_news": {
+ "enabled": true,
+ "update_interval": 3600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "max_headlines_per_symbol": 1,
+ "headlines_per_rotation": 2
+ }
+}
+```
+
+**Features**:
+- Financial news headlines
+- Stock-specific news filtering
+- Scrolling text display
+- Configurable headline limits
+- Automatic rotation
+
+---
+
+### 🎵 Music Manager (`src/music_manager.py`)
+**Purpose**: Displays currently playing music from Spotify or YouTube Music
+
+**Configuration**:
+```json
+{
+ "music": {
+ "enabled": true,
+ "preferred_source": "ytm",
+ "YTM_COMPANION_URL": "http://192.168.86.12:9863",
+ "POLLING_INTERVAL_SECONDS": 1
+ }
+}
+```
+
+**Features**:
+- Spotify integration
+- YouTube Music integration
+- Album art display
+- Song title and artist
+- Playback status
+- Real-time updates
+
+**Supported Sources**:
+- Spotify (requires API credentials)
+- YouTube Music (requires YTMD companion server)
+
+---
+
+### 📅 Calendar Manager (`src/calendar_manager.py`)
+**Purpose**: Displays upcoming Google Calendar events
+
+**Configuration**:
+```json
+{
+ "calendar": {
+ "enabled": true,
+ "credentials_file": "credentials.json",
+ "token_file": "token.pickle",
+ "update_interval": 3600,
+ "max_events": 3,
+ "calendars": ["birthdays"]
+ }
+}
+```
+
+**Features**:
+- Google Calendar integration
+- Event date and time display
+- Event title (wrapped to fit display)
+- Multiple calendar support
+- Configurable event limits
+
+---
+
+### 🏈 Sports Managers
+
+The system includes separate managers for each sports league:
+
+#### NHL Managers (`src/nhl_managers.py`)
+- **NHLLiveManager**: Currently playing games
+- **NHLRecentManager**: Completed games (last 48 hours)
+- **NHLUpcomingManager**: Scheduled games
+
+#### NBA Managers (`src/nba_managers.py`)
+- **NBALiveManager**: Currently playing games
+- **NBARecentManager**: Completed games
+- **NBAUpcomingManager**: Scheduled games
+
+#### MLB Managers (`src/mlb_manager.py`)
+- **MLBLiveManager**: Currently playing games
+- **MLBRecentManager**: Completed games
+- **MLBUpcomingManager**: Scheduled games
+
+#### NFL Managers (`src/nfl_managers.py`)
+- **NFLLiveManager**: Currently playing games
+- **NFLRecentManager**: Completed games
+- **NFLUpcomingManager**: Scheduled games
+
+#### NCAA Managers
+- **NCAA Football** (`src/ncaa_fb_managers.py`)
+- **NCAA Baseball** (`src/ncaa_baseball_managers.py`)
+- **NCAA Basketball** (`src/ncaam_basketball_managers.py`)
+
+#### Soccer Managers (`src/soccer_managers.py`)
+- **SoccerLiveManager**: Currently playing games
+- **SoccerRecentManager**: Completed games
+- **SoccerUpcomingManager**: Scheduled games
+
+#### MiLB Managers (`src/milb_manager.py`)
+- **MiLBLiveManager**: Currently playing games
+- **MiLBRecentManager**: Completed games
+- **MiLBUpcomingManager**: Scheduled games
+
+**Common Sports Configuration**:
+```json
+{
+ "nhl_scoreboard": {
+ "enabled": true,
+ "live_priority": true,
+ "live_game_duration": 20,
+ "show_odds": true,
+ "test_mode": false,
+ "update_interval_seconds": 3600,
+ "live_update_interval": 30,
+ "show_favorite_teams_only": true,
+ "favorite_teams": ["TB"],
+ "logo_dir": "assets/sports/nhl_logos",
+ "show_records": true,
+ "display_modes": {
+ "nhl_live": true,
+ "nhl_recent": true,
+ "nhl_upcoming": true
+ }
+ }
+}
+```
+
+**Sports Features**:
+- Live game scores and status
+- Team logos and records
+- Game times and venues
+- Odds integration (where available)
+- Favorite team filtering
+- Automatic game switching
+- ESPN API integration
+
+---
+
+### 🎲 Odds Ticker Manager (`src/odds_ticker_manager.py`)
+**Purpose**: Displays betting odds for upcoming sports games
+
+**Configuration**:
+```json
+{
+ "odds_ticker": {
+ "enabled": true,
+ "show_favorite_teams_only": true,
+ "games_per_favorite_team": 1,
+ "max_games_per_league": 5,
+ "show_odds_only": false,
+ "sort_order": "soonest",
+ "enabled_leagues": ["nfl", "mlb", "ncaa_fb", "milb"],
+ "update_interval": 3600,
+ "scroll_speed": 1,
+ "scroll_delay": 0.01,
+ "loop": true,
+ "future_fetch_days": 50,
+ "show_channel_logos": true
+ }
+}
+```
+
+**Features**:
+- Multi-league support (NFL, NBA, MLB, NCAA)
+- Spread, money line, and over/under odds
+- Team logos display
+- Scrolling text format
+- Game time display
+- ESPN API integration
+
+---
+
+### 🎨 Custom Display Managers
+
+#### Text Display Manager (`src/text_display.py`)
+**Purpose**: Displays custom text messages
+
+**Configuration**:
+```json
+{
+ "text_display": {
+ "enabled": true,
+ "text": "Subscribe to ChuckBuilds",
+ "font_path": "assets/fonts/press-start-2p.ttf",
+ "font_size": 8,
+ "scroll": true,
+ "scroll_speed": 40,
+ "text_color": [255, 0, 0],
+ "background_color": [0, 0, 0],
+ "scroll_gap_width": 32
+ }
+}
+```
+
+**Features**:
+- Custom text messages
+- Configurable fonts and colors
+- Scrolling text support
+- Static text display
+- Background color options
+
+#### YouTube Display Manager (`src/youtube_display.py`)
+**Purpose**: Displays YouTube channel statistics
+
+**Configuration**:
+```json
+{
+ "youtube": {
+ "enabled": true,
+ "update_interval": 3600
+ }
+}
+```
+
+**Features**:
+- Subscriber count display
+- Video count display
+- View count display
+- YouTube API integration
+
+#### Of The Day Manager (`src/of_the_day_manager.py`)
+**Purpose**: Displays various "of the day" content
+
+**Configuration**:
+```json
+{
+ "of_the_day": {
+ "enabled": true,
+ "display_rotate_interval": 20,
+ "update_interval": 3600,
+ "subtitle_rotate_interval": 10,
+ "category_order": ["word_of_the_day", "slovenian_word_of_the_day", "bible_verse_of_the_day"],
+ "categories": {
+ "word_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/word_of_the_day.json",
+ "display_name": "Word of the Day"
+ },
+ "slovenian_word_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/slovenian_word_of_the_day.json",
+ "display_name": "Slovenian Word of the Day"
+ },
+ "bible_verse_of_the_day": {
+ "enabled": true,
+ "data_file": "of_the_day/bible_verse_of_the_day.json",
+ "display_name": "Bible Verse of the Day"
+ }
+ }
+ }
+}
+```
+
+**Features**:
+- Word of the day
+- Slovenian word of the day
+- Bible verse of the day
+- Rotating display categories
+- Local JSON data files
+
+---
+
+## Display Manager Architecture
+
+### Common Interface
+All display managers follow a consistent interface:
+
+```python
+class DisplayManager:
+ def __init__(self, config, display_manager):
+ # Initialize with configuration and display manager
+
+ def update_data(self):
+ # Fetch and process new data
+
+ def display(self, force_clear=False):
+ # Render content to the display
+
+ def is_enabled(self):
+ # Check if manager is enabled
+```
+
+### Data Flow
+1. **Configuration**: Manager reads settings from `config.json`
+2. **Data Fetching**: Retrieves data from APIs or local sources
+3. **Caching**: Stores data using `CacheManager`
+4. **Processing**: Transforms data into display format
+5. **Rendering**: Uses `DisplayManager` to show content
+6. **Rotation**: Returns to main display controller
+
+### Error Handling
+- API failures fall back to cached data
+- Network timeouts are handled gracefully
+- Invalid data is filtered out
+- Logging provides debugging information
+
+## Configuration Best Practices
+
+### Enable/Disable Managers
+```json
+{
+ "weather": {
+ "enabled": true // Set to false to disable
+ }
+}
+```
+
+### Set Display Durations
+```json
+{
+ "display": {
+ "display_durations": {
+ "weather": 30, // 30 seconds
+ "stocks": 60, // 1 minute
+ "nhl_live": 20 // 20 seconds
+ }
+ }
+}
+```
+
+### Configure Update Intervals
+```json
+{
+ "weather": {
+ "update_interval": 1800 // Update every 30 minutes
+ }
+}
+```
+
+### Set Favorite Teams
+```json
+{
+ "nhl_scoreboard": {
+ "show_favorite_teams_only": true,
+ "favorite_teams": ["TB", "DAL"]
+ }
+}
+```
+
+## Performance Considerations
+
+### API Rate Limits
+- Weather: 1000 calls/day (OpenWeatherMap)
+- Stocks: 2000 calls/hour (Yahoo Finance)
+- Sports: ESPN API (no documented limits)
+- Music: Spotify/YouTube Music APIs
+
+### Caching Strategy
+- Data cached based on `update_interval`
+- Cache persists across restarts
+- Failed API calls use cached data
+- Automatic cache invalidation
+
+### Resource Usage
+- Each manager runs independently
+- Disabled managers use no resources
+- Memory usage scales with enabled features
+- CPU usage minimal during idle periods
+
+## Troubleshooting Display Managers
+
+### Common Issues
+1. **No Data Displayed**: Check API keys and network connectivity
+2. **Outdated Data**: Verify update intervals and cache settings
+3. **Display Errors**: Check font files and display configuration
+4. **Performance Issues**: Reduce update frequency or disable unused managers
+
+### Debugging
+- Enable logging for specific managers
+- Check cache directory for data files
+- Verify API credentials in `config_secrets.json`
+- Test individual managers in isolation
+
+---
+
+*For detailed technical information about each display manager, see the [Display Manager Details](WIKI_DISPLAY_MANAGER_DETAILS.md) page.*
\ No newline at end of file
diff --git a/docs/WIKI_HOME.md b/docs/WIKI_HOME.md
new file mode 100644
index 000000000..f32f32258
--- /dev/null
+++ b/docs/WIKI_HOME.md
@@ -0,0 +1,96 @@
+# LEDMatrix Wiki
+
+Welcome to the LEDMatrix Wiki! This comprehensive documentation will help you understand, configure, and customize your LED matrix display system.
+
+## 🏠 [Home](WIKI_HOME.md) - You are here
+The main wiki page with overview and navigation.
+
+## 📋 [Quick Start Guide](WIKI_QUICK_START.md)
+Get your LEDMatrix up and running in minutes with this step-by-step guide.
+
+## 🏗️ [System Architecture](WIKI_ARCHITECTURE.md)
+Understand how the LEDMatrix system is organized and how all components work together.
+
+## ⚙️ [Configuration Guide](WIKI_CONFIGURATION.md)
+Complete guide to configuring all aspects of your LEDMatrix system.
+
+## 🎯 [Display Managers](WIKI_DISPLAY_MANAGERS.md)
+Detailed documentation for each display manager and their configuration options.
+
+## 🔧 [Complete Manager Guide](MANAGER_GUIDE_COMPREHENSIVE.md)
+Comprehensive documentation covering every manager, all configuration options, and detailed explanations of how everything works.
+
+## 🌐 Web Interface
+- [Web Interface Installation](WEB_INTERFACE_INSTALLATION.md)
+- [🖥️ Complete Web UI Guide](WEB_UI_COMPLETE_GUIDE.md)
+- [Web Interface V2 Enhancements](WEB_INTERFACE_V2_ENHANCED_SUMMARY.md)
+
+## 📰 News & Feeds
+- [Sports News Manager](NEWS_MANAGER_README.md)
+- [Add Custom RSS Feeds](CUSTOM_FEEDS_GUIDE.md)
+
+## ⏱️ Dynamic Duration
+- [Feature Overview](dynamic_duration.md)
+- [Implementation Guide](DYNAMIC_DURATION_GUIDE.md)
+- [Stocks Implementation Details](DYNAMIC_DURATION_STOCKS_IMPLEMENTATION.md)
+
+## 🗄️ Caching & Performance
+- [Cache Strategy](CACHE_STRATEGY.md)
+- [Cache Management](cache_management.md)
+- [⚡ Background Service Guide](BACKGROUND_SERVICE_GUIDE.md) - **NEW!** High-performance threading system
+
+## 🧩 Troubleshooting
+- [General Troubleshooting](WIKI_TROUBLESHOOTING.md)
+- [MiLB Troubleshooting](MILB_TROUBLESHOOTING.md)
+
+## 🚀 Install & Quick Start
+- [Installation Guide](INSTALLATION_GUIDE.md)
+- [Quick Start](WIKI_QUICK_START.md)
+
+---
+
+## Quick Navigation
+
+### Core Features
+- [Display Managers](WIKI_DISPLAY_MANAGERS.md) - All display modules
+- [Configuration](WIKI_CONFIGURATION.md) - Complete config guide
+- [Installation Guide](INSTALLATION_GUIDE.md) - Hardware setup and installation
+
+### Technical
+- [Architecture](WIKI_ARCHITECTURE.md) - System design
+- [Cache Strategy](CACHE_STRATEGY.md) - Caching implementation
+- [Cache Management](cache_management.md) - Cache utilities
+
+---
+
+## About LEDMatrix
+
+LEDMatrix is a comprehensive LED matrix display system that provides real-time information display capabilities for various data sources. The system is highly configurable and supports multiple display modes that can be enabled or disabled based on user preferences.
+
+### Key Features
+- **Modular Design**: Each feature is a separate manager that can be enabled/disabled
+- **Real-time Updates**: Live data from APIs with intelligent caching
+- **Multiple Sports**: NHL, NBA, MLB, NFL, NCAA, Soccer, and more
+- **Financial Data**: Stock ticker, crypto prices, and financial news
+- **Weather**: Current conditions, hourly and daily forecasts
+- **Music**: Spotify and YouTube Music integration
+- **Custom Content**: Text display, YouTube stats, and more
+- **Scheduling**: Configurable display rotation and timing
+- **Caching**: Intelligent caching to reduce API calls
+
+### System Requirements
+- Raspberry Pi 3B+ or 4 (NOT Pi 5)
+- Adafruit RGB Matrix Bonnet/HAT
+- 2x LED Matrix panels (64x32)
+- 5V 4A DC Power Supply
+- Internet connection for API access
+
+### Quick Links
+- [YouTube Setup Video](https://www.youtube.com/watch?v=_HaqfJy1Y54)
+- [Project Website](https://www.chuck-builds.com/led-matrix/)
+- [GitHub Repository](https://github.com/ChuckBuilds/LEDMatrix)
+- [Discord Community](https://discord.com/invite/uW36dVAtcT)
+
+---
+
+*This wiki is designed to help you get the most out of your LEDMatrix system. Each page contains detailed information, configuration examples, and troubleshooting tips.*
\ No newline at end of file
diff --git a/docs/WIKI_QUICK_START.md b/docs/WIKI_QUICK_START.md
new file mode 100644
index 000000000..0dc869f10
--- /dev/null
+++ b/docs/WIKI_QUICK_START.md
@@ -0,0 +1,407 @@
+# Quick Start Guide
+
+Get your LEDMatrix system up and running in minutes! This guide covers the essential steps to get your display working.
+
+## Fast Path (Recommended)
+
+If this is a brand new install, you can run the all-in-one installer and then use the web UI:
+
+```bash
+chmod +x first_time_install.sh
+sudo ./first_time_install.sh
+```
+
+Then open the web UI at:
+
+```
+http://your-pi-ip:5001
+```
+
+The steps below document the manual process for advanced users.
+
+## Prerequisites
+
+### Hardware Requirements
+- Raspberry Pi 3B+ or 4 (NOT Pi 5)
+- Adafruit RGB Matrix Bonnet/HAT
+- 2x LED Matrix panels (64x32 each)
+- 5V 4A DC Power Supply
+- Micro SD card (8GB or larger)
+
+### Software Requirements
+- Internet connection
+- SSH access to Raspberry Pi
+- Basic command line knowledge
+
+## Step 1: Prepare Raspberry Pi
+
+### 1.1 Create Raspberry Pi Image
+1. Download [Raspberry Pi Imager](https://www.raspberrypi.com/software/)
+2. Choose your Raspberry Pi model
+3. Select "Raspbian OS Lite (64-bit)"
+4. Choose your micro SD card
+5. Click "Next" then "Edit Settings"
+
+### 1.2 Configure OS Settings
+1. **General Tab**:
+ - Set hostname: `ledpi`
+ - Enable SSH
+ - Set username and password
+ - Configure WiFi
+
+2. **Services Tab**:
+ - Enable SSH
+ - Use password authentication
+
+3. Click "Save" and write the image
+
+### 1.3 Boot and Connect
+1. Insert SD card into Raspberry Pi
+2. Power on and wait for boot
+3. Connect via SSH:
+ ```bash
+ ssh ledpi@ledpi
+ ```
+
+## Step 2: Install LEDMatrix
+
+### 2.1 Update System
+```bash
+sudo apt update && sudo apt upgrade -y
+sudo apt install -y git python3-pip cython3 build-essential python3-dev python3-pillow scons
+```
+
+### 2.2 Clone Repository
+```bash
+git clone https://github.com/ChuckBuilds/LEDMatrix.git
+cd LEDMatrix
+```
+
+ ### 2.3 First-time installation (recommended)
+
+```bash
+chmod +x first_time_install.sh
+sudo ./first_time_install.sh
+```
+
+
+### Or manually
+
+### 2.3 Install Dependencies
+```bash
+sudo pip3 install --break-system-packages -r requirements.txt
+```
+
+### 2.4 Install RGB Matrix Library
+```bash
+cd rpi-rgb-led-matrix-master
+sudo make build-python PYTHON=$(which python3)
+cd bindings/python
+sudo python3 setup.py install
+```
+
+### 2.5 Test Installation
+```bash
+python3 -c 'from rgbmatrix import RGBMatrix, RGBMatrixOptions; print("Success!")'
+```
+
+## Step 3: Configure Hardware
+
+### 3.1 Remove Audio Services
+```bash
+sudo apt-get remove bluez bluez-firmware pi-bluetooth triggerhappy pigpio
+```
+
+### 3.2 Blacklist Sound Module
+```bash
+cat <> /var/log/ledmatrix_cache.log
+```
+
+### Conditional Cache Clearing
+
+```bash
+#!/bin/bash
+# Only clear cache if it's older than 24 hours
+
+# Check cache age and clear if needed
+# (This would require additional logic to check cache timestamps)
+```
+
+## Related Documentation
+
+- [Configuration Guide](../config/README.md)
+- [Troubleshooting Guide](troubleshooting.md)
+- [API Integration](api_integration.md)
+- [Display Controller](display_controller.md)
diff --git a/docs/dynamic_duration.md b/docs/dynamic_duration.md
new file mode 100644
index 000000000..dc5ff3e65
--- /dev/null
+++ b/docs/dynamic_duration.md
@@ -0,0 +1,243 @@
+# Dynamic Duration Implementation
+
+## Overview
+
+Dynamic Duration is a feature that calculates the exact time needed to display scrolling content (like news headlines or stock tickers) based on the content's length, scroll speed, and display characteristics, rather than using a fixed duration. This ensures optimal viewing time for users while maintaining smooth content flow.
+
+## How It Works
+
+The dynamic duration calculation considers several factors:
+
+1. **Content Width**: The total width of the text/image content to be displayed
+2. **Display Width**: The width of the LED matrix display
+3. **Scroll Speed**: How many pixels the content moves per frame
+4. **Scroll Delay**: Time between each frame update
+5. **Buffer Time**: Additional time added for smooth cycling (configurable percentage)
+
+### Calculation Formula
+
+```
+Total Scroll Distance = Display Width + Content Width
+Frames Needed = Total Scroll Distance / Scroll Speed
+Base Time = Frames Needed × Scroll Delay
+Buffer Time = Base Time × Duration Buffer
+Calculated Duration = Base Time + Buffer Time
+```
+
+The final duration is then capped between the configured minimum and maximum values.
+
+## Configuration
+
+Add the following settings to your `config/config.json` file:
+
+### For Stocks (`stocks` section)
+```json
+{
+ "stocks": {
+ "dynamic_duration": true,
+ "min_duration": 30,
+ "max_duration": 300,
+ "duration_buffer": 0.1,
+ // ... other existing settings
+ }
+}
+```
+
+### For Stock News (`stock_news` section)
+```json
+{
+ "stock_news": {
+ "dynamic_duration": true,
+ "min_duration": 30,
+ "max_duration": 300,
+ "duration_buffer": 0.1,
+ // ... other existing settings
+ }
+}
+```
+
+### For Odds Ticker (`odds_ticker` section)
+```json
+{
+ "odds_ticker": {
+ "dynamic_duration": true,
+ "min_duration": 30,
+ "max_duration": 300,
+ "duration_buffer": 0.1,
+ // ... other existing settings
+ }
+}
+```
+
+### Configuration Options
+
+- **`dynamic_duration`** (boolean): Enable/disable dynamic duration calculation
+- **`min_duration`** (seconds): Minimum display time regardless of content length
+- **`max_duration`** (seconds): Maximum display time to prevent excessive delays
+- **`duration_buffer`** (decimal): Additional time as a percentage of calculated time (e.g., 0.1 = 10% extra)
+
+## Implementation Details
+
+### StockManager Updates
+
+The `StockManager` class has been enhanced with dynamic duration capabilities:
+
+```python
+# In __init__ method
+self.dynamic_duration_enabled = self.stocks_config.get('dynamic_duration', True)
+self.min_duration = self.stocks_config.get('min_duration', 30)
+self.max_duration = self.stocks_config.get('max_duration', 300)
+self.duration_buffer = self.stocks_config.get('duration_buffer', 0.1)
+self.dynamic_duration = 60 # Default duration in seconds
+self.total_scroll_width = 0 # Track total width for calculation
+```
+
+#### New Methods
+
+**`calculate_dynamic_duration()`**
+- Calculates the exact time needed to display all stock information
+- Considers display width, content width, scroll speed, and delays
+- Applies min/max duration limits
+- Includes detailed debug logging
+
+**`get_dynamic_duration()`**
+- Returns the calculated dynamic duration for external use
+- Used by the DisplayController to determine display timing
+
+### StockNewsManager Updates
+
+Similar enhancements have been applied to the `StockNewsManager`:
+
+```python
+# In __init__ method
+self.dynamic_duration_enabled = self.stock_news_config.get('dynamic_duration', True)
+self.min_duration = self.stock_news_config.get('min_duration', 30)
+self.max_duration = self.stock_news_config.get('max_duration', 300)
+self.duration_buffer = self.stock_news_config.get('duration_buffer', 0.1)
+self.dynamic_duration = 60 # Default duration in seconds
+self.total_scroll_width = 0 # Track total width for calculation
+```
+
+#### New Methods
+
+**`calculate_dynamic_duration()`**
+- Calculates display time for news headlines
+- Uses the same logic as StockManager but with stock news configuration
+- Handles text width calculation from cached images
+
+**`get_dynamic_duration()`**
+- Returns the calculated duration for news display
+
+### OddsTickerManager Updates
+
+The `OddsTickerManager` class has been enhanced with dynamic duration capabilities:
+
+```python
+# In __init__ method
+self.dynamic_duration_enabled = self.odds_ticker_config.get('dynamic_duration', True)
+self.min_duration = self.odds_ticker_config.get('min_duration', 30)
+self.max_duration = self.odds_ticker_config.get('max_duration', 300)
+self.duration_buffer = self.odds_ticker_config.get('duration_buffer', 0.1)
+self.dynamic_duration = 60 # Default duration in seconds
+self.total_scroll_width = 0 # Track total width for calculation
+```
+
+#### New Methods
+
+**`calculate_dynamic_duration()`**
+- Calculates display time for odds ticker content
+- Uses the same logic as other managers but with odds ticker configuration
+- Handles width calculation from the composite ticker image
+
+**`get_dynamic_duration()`**
+- Returns the calculated duration for odds ticker display
+
+### DisplayController Integration
+
+The `DisplayController` has been updated to use dynamic durations:
+
+```python
+# In get_current_duration() method
+# Handle dynamic duration for stocks
+if mode_key == 'stocks' and self.stocks:
+ try:
+ dynamic_duration = self.stocks.get_dynamic_duration()
+ logger.info(f"Using dynamic duration for stocks: {dynamic_duration} seconds")
+ return dynamic_duration
+ except Exception as e:
+ logger.error(f"Error getting dynamic duration for stocks: {e}")
+ return self.display_durations.get(mode_key, 60)
+
+# Handle dynamic duration for stock_news
+if mode_key == 'stock_news' and self.news:
+ try:
+ dynamic_duration = self.news.get_dynamic_duration()
+ logger.info(f"Using dynamic duration for stock_news: {dynamic_duration} seconds")
+ return dynamic_duration
+ except Exception as e:
+ logger.error(f"Error getting dynamic duration for stock_news: {e}")
+ return self.display_durations.get(mode_key, 60)
+
+# Handle dynamic duration for odds_ticker
+if mode_key == 'odds_ticker' and self.odds_ticker:
+ try:
+ dynamic_duration = self.odds_ticker.get_dynamic_duration()
+ logger.info(f"Using dynamic duration for odds_ticker: {dynamic_duration} seconds")
+ return dynamic_duration
+ except Exception as e:
+ logger.error(f"Error getting dynamic duration for odds_ticker: {e}")
+ return self.display_durations.get(mode_key, 60)
+
+## Benefits
+
+1. **Optimal Viewing Time**: Content is displayed for exactly the right amount of time
+2. **Smooth Transitions**: Buffer time ensures smooth cycling between content
+3. **Configurable Limits**: Min/max durations prevent too short or too long displays
+4. **Consistent Experience**: All scrolling content uses the same timing logic
+5. **Debug Visibility**: Detailed logging helps troubleshoot timing issues
+
+## Testing
+
+The implementation includes comprehensive logging to verify calculations:
+
+```
+Stock dynamic duration calculation:
+ Display width: 128px
+ Text width: 450px
+ Total scroll distance: 578px
+ Frames needed: 578.0
+ Base time: 5.78s
+ Buffer time: 0.58s (10%)
+ Calculated duration: 6s
+ Final duration: 30s (capped to minimum)
+```
+
+## Troubleshooting
+
+### Duration Always at Minimum
+If your calculated duration is always capped at the minimum value, check:
+- Scroll speed settings (higher speed = shorter duration)
+- Scroll delay settings (lower delay = shorter duration)
+- Content width calculation
+- Display width configuration
+
+### Duration Too Long
+If content displays for too long:
+- Reduce the `duration_buffer` percentage
+- Increase `scroll_speed` or decrease `scroll_delay`
+- Lower the `max_duration` limit
+
+### Dynamic Duration Not Working
+If dynamic duration isn't being used:
+- Verify `dynamic_duration: true` in configuration
+- Check that the manager instances are properly initialized
+- Review error logs for calculation failures
+
+## Related Files
+
+- `config/config.json` - Configuration settings
+- `src/stock_manager.py` - Stock display with dynamic duration
+- `src/stock_news_manager.py` - Stock news with dynamic duration
+- `src/odds_ticker_manager.py` - Odds ticker with dynamic duration
+- `src/display_controller.py` - Integration and duration management
+- `src/news_manager.py` - Original implementation reference
diff --git a/clear_nhl_cache.py b/scripts/clear_nhl_cache.py
similarity index 100%
rename from clear_nhl_cache.py
rename to scripts/clear_nhl_cache.py
diff --git a/test_config_loading.py b/test/test_config_loading.py
similarity index 100%
rename from test_config_loading.py
rename to test/test_config_loading.py
diff --git a/test_config_simple.py b/test/test_config_simple.py
similarity index 100%
rename from test_config_simple.py
rename to test/test_config_simple.py
diff --git a/test_config_validation.py b/test/test_config_validation.py
similarity index 100%
rename from test_config_validation.py
rename to test/test_config_validation.py
diff --git a/test_static_image.py b/test/test_static_image.py
similarity index 100%
rename from test_static_image.py
rename to test/test_static_image.py
diff --git a/test_static_image_simple.py b/test/test_static_image_simple.py
similarity index 100%
rename from test_static_image_simple.py
rename to test/test_static_image_simple.py
From 6a059a4f4c8b2667ad0286f0322727eeee8c9bb0 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Wed, 8 Oct 2025 21:37:12 -0400
Subject: [PATCH 004/736] first stab with a plugin system
---
docs/PLUGIN_PHASE_1_SUMMARY.md | 226 +++++++++++++
plugins/README.md | 63 ++++
src/display_controller.py | 61 ++++
src/plugin_system/__init__.py | 22 ++
src/plugin_system/base_plugin.py | 210 ++++++++++++
src/plugin_system/plugin_manager.py | 356 +++++++++++++++++++
src/plugin_system/store_manager.py | 506 ++++++++++++++++++++++++++++
web_interface_v2.py | 233 +++++++++++++
8 files changed, 1677 insertions(+)
create mode 100644 docs/PLUGIN_PHASE_1_SUMMARY.md
create mode 100644 plugins/README.md
create mode 100644 src/plugin_system/__init__.py
create mode 100644 src/plugin_system/base_plugin.py
create mode 100644 src/plugin_system/plugin_manager.py
create mode 100644 src/plugin_system/store_manager.py
diff --git a/docs/PLUGIN_PHASE_1_SUMMARY.md b/docs/PLUGIN_PHASE_1_SUMMARY.md
new file mode 100644
index 000000000..86bf3e52d
--- /dev/null
+++ b/docs/PLUGIN_PHASE_1_SUMMARY.md
@@ -0,0 +1,226 @@
+# Plugin Architecture - Phase 1 Implementation Summary
+
+## Overview
+
+Phase 1 of the Plugin Architecture has been successfully implemented. This phase focuses on building the foundation for the plugin system without breaking any existing functionality.
+
+## What Was Implemented
+
+### 1. Plugin System Core (`src/plugin_system/`)
+
+Created the core plugin infrastructure with three main components:
+
+#### `base_plugin.py` - BasePlugin Abstract Class
+- Provides the standard interface that all plugins must implement
+- Required abstract methods: `update()` and `display()`
+- Helper methods: `get_display_duration()`, `validate_config()`, `cleanup()`, `get_info()`
+- Lifecycle hooks: `on_enable()`, `on_disable()`
+- Built-in logging, configuration management, and cache integration
+
+#### `plugin_manager.py` - PluginManager
+- Discovers plugins in the `plugins/` directory
+- Loads and unloads plugins dynamically
+- Manages plugin lifecycle and state
+- Provides access to loaded plugins
+- Handles manifest validation and module importing
+
+#### `store_manager.py` - PluginStoreManager
+- Fetches plugin registry from GitHub
+- Installs plugins from the registry or custom GitHub URLs
+- Uninstalls and updates plugins
+- Manages plugin dependencies via `requirements.txt`
+- Supports both git clone and ZIP download methods
+
+### 2. Plugins Directory (`plugins/`)
+
+- Created `plugins/` directory in project root
+- Added comprehensive README.md with usage instructions
+- Plugins are automatically discovered if they have a valid `manifest.json`
+
+### 3. Display Controller Integration
+
+Modified `src/display_controller.py` to support plugins alongside legacy managers:
+
+- **Initialization** (lines 388-427):
+ - Initializes PluginManager after all legacy managers
+ - Discovers available plugins
+ - Loads enabled plugins from configuration
+ - Adds plugin display modes to rotation
+ - Gracefully handles missing plugin system (backward compatible)
+
+- **Update Integration** (lines 878-885):
+ - Calls `update()` method on all enabled plugins
+ - Integrated into existing `_update_modules()` cycle
+ - Error handling to prevent plugin failures from crashing system
+
+- **Display Integration** (lines 1407-1417):
+ - Checks if current mode belongs to a plugin
+ - Uses existing display infrastructure (no special handling needed)
+ - Plugins work with standard `display(force_clear)` interface
+
+### 4. Web Interface API Endpoints
+
+Added comprehensive API endpoints to `web_interface_v2.py` (lines 1652-1883):
+
+- **`GET /api/plugins/store/list`** - Browse available plugins in registry
+- **`GET /api/plugins/installed`** - List installed plugins with status
+- **`POST /api/plugins/install`** - Install plugin from registry
+- **`POST /api/plugins/uninstall`** - Uninstall plugin
+- **`POST /api/plugins/toggle`** - Enable/disable plugin
+- **`POST /api/plugins/update`** - Update plugin to latest version
+- **`POST /api/plugins/install-from-url`** - Install from custom GitHub URL
+
+## Key Design Decisions
+
+### 1. Non-Breaking Integration
+- Plugin system is **optional** and **additive**
+- All existing managers continue to work unchanged
+- System gracefully handles missing plugin system via try/except
+- Plugins and legacy managers coexist in the same rotation
+
+### 2. Simple Manifest-Based Discovery
+- Plugins must have `manifest.json` to be recognized
+- Manifest defines plugin metadata, entry point, requirements
+- No complex registration or installation scripts needed
+
+### 3. Standard Interface
+- All plugins inherit from `BasePlugin`
+- Consistent initialization: `__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)`
+- Standard methods: `update()` and `display(force_clear)`
+- Plugins can override helper methods for custom behavior
+
+### 4. Shared Resources
+- Plugins use existing `display_manager` for rendering
+- Plugins use existing `cache_manager` for data caching
+- Plugins access configuration from `config.json`
+- No separate resource management needed
+
+## Testing Status
+
+✅ **No Linter Errors**: All Python files pass linting checks
+✅ **Backward Compatible**: Existing functionality unchanged
+✅ **Graceful Degradation**: System works if plugin system unavailable
+
+## Configuration Example
+
+To enable a plugin, add it to `config/config.json`:
+
+```json
+{
+ "my-plugin": {
+ "enabled": true,
+ "display_duration": 15,
+ "custom_option": "value"
+ }
+}
+```
+
+## API Usage Examples
+
+### List Available Plugins from Store
+```bash
+curl http://localhost:5001/api/plugins/store/list
+```
+
+### Install a Plugin
+```bash
+curl -X POST http://localhost:5001/api/plugins/install \
+ -H "Content-Type: application/json" \
+ -d '{"plugin_id": "clock-simple", "version": "latest"}'
+```
+
+### List Installed Plugins
+```bash
+curl http://localhost:5001/api/plugins/installed
+```
+
+### Enable/Disable a Plugin
+```bash
+curl -X POST http://localhost:5001/api/plugins/toggle \
+ -H "Content-Type: application/json" \
+ -d '{"plugin_id": "my-plugin", "enabled": true}'
+```
+
+### Install from Custom URL
+```bash
+curl -X POST http://localhost:5001/api/plugins/install-from-url \
+ -H "Content-Type: application/json" \
+ -d '{"repo_url": "https://github.com/user/ledmatrix-plugin"}'
+```
+
+## What's Next (Future Phases)
+
+### Phase 2: Example Plugins (Weeks 4-5)
+- Create 4-5 reference plugins
+- Migrate existing managers as examples
+- Write plugin developer documentation
+- Create plugin templates
+
+### Phase 3: Store Integration (Weeks 6-7)
+- Set up plugin registry repository
+- Build web UI for plugin store
+- Add search and filtering
+
+### Phase 4: Migration Tools (Weeks 8-9)
+- Create migration script for existing setups
+- Test with existing installations
+- Write migration guide
+
+## File Changes Summary
+
+### New Files Created:
+- `src/plugin_system/__init__.py`
+- `src/plugin_system/base_plugin.py`
+- `src/plugin_system/plugin_manager.py`
+- `src/plugin_system/store_manager.py`
+- `plugins/README.md`
+- `docs/PLUGIN_PHASE_1_SUMMARY.md`
+
+### Modified Files:
+- `src/display_controller.py` - Added plugin system initialization and integration
+- `web_interface_v2.py` - Added plugin API endpoints
+
+### Total Lines Added: ~1,100 lines of new code
+### Breaking Changes: **None** ✅
+
+## Testing Recommendations
+
+1. **Basic Functionality Test**:
+ - Start the display controller
+ - Verify no errors in logs related to plugin system
+ - Confirm existing displays still work
+
+2. **Plugin Discovery Test**:
+ - Create a simple test plugin in `plugins/` directory
+ - Verify it's discovered in logs
+ - Check API endpoint returns it
+
+3. **Web API Test**:
+ - Access plugin endpoints via curl/Postman
+ - Verify proper error handling
+ - Test enable/disable functionality
+
+## Notes
+
+- Plugin system initializes even if no plugins are installed
+- Failed plugin loads don't crash the system
+- Plugins are loaded at startup, not hot-reloaded (restart required)
+- All plugin operations logged for debugging
+- API endpoints return helpful error messages
+
+## Support
+
+For questions or issues with the plugin system:
+1. Check logs for detailed error messages
+2. Verify plugin manifest.json is valid JSON
+3. Ensure plugin follows BasePlugin interface
+4. Check that required dependencies are installed
+
+---
+
+**Implementation Date**: October 9, 2025
+**Phase**: 1 of 6 (Foundation)
+**Status**: ✅ Complete
+**Breaking Changes**: None
+**Backward Compatible**: Yes
+
diff --git a/plugins/README.md b/plugins/README.md
new file mode 100644
index 000000000..56c3149b0
--- /dev/null
+++ b/plugins/README.md
@@ -0,0 +1,63 @@
+# Plugins Directory
+
+This directory contains installed LEDMatrix plugins.
+
+## Structure
+
+Each plugin is in its own subdirectory:
+
+```
+plugins/
+├── plugin-name/
+│ ├── manifest.json # Plugin metadata
+│ ├── manager.py # Plugin implementation
+│ ├── requirements.txt # Python dependencies (optional)
+│ ├── config_schema.json # Configuration schema (optional)
+│ ├── assets/ # Plugin assets (optional)
+│ └── README.md # Plugin documentation
+```
+
+## Installing Plugins
+
+### Via Web UI (Recommended)
+1. Navigate to the Plugin Store in the web interface
+2. Browse or search for plugins
+3. Click "Install" on the desired plugin
+
+### Via Command Line
+```bash
+# Install from registry
+python3 -c "from src.plugin_system.store_manager import PluginStoreManager; PluginStoreManager().install_plugin('plugin-id')"
+
+# Install from GitHub URL
+python3 -c "from src.plugin_system.store_manager import PluginStoreManager; PluginStoreManager().install_from_url('https://github.com/user/repo')"
+```
+
+## Creating Plugins
+
+See the [Plugin Developer Guide](../docs/PLUGIN_DEVELOPER_GUIDE.md) for information on creating your own plugins.
+
+## Plugin Discovery
+
+Plugins in this directory are automatically discovered when the LEDMatrix system starts. A plugin must have a valid `manifest.json` file to be recognized.
+
+## Configuration
+
+Plugin configuration is stored in `config/config.json` under a key matching the plugin ID:
+
+```json
+{
+ "plugin-name": {
+ "enabled": true,
+ "display_duration": 15,
+ "custom_option": "value"
+ }
+}
+```
+
+## Support
+
+For issues with specific plugins, contact the plugin author via their GitHub repository.
+
+For issues with the plugin system itself, see the [main project repository](https://github.com/ChuckBuilds/LEDMatrix).
+
diff --git a/src/display_controller.py b/src/display_controller.py
index 04abe2822..064606786 100644
--- a/src/display_controller.py
+++ b/src/display_controller.py
@@ -385,6 +385,47 @@ def __init__(self):
# Add live modes to rotation if live_priority is False and there are live games
self._update_live_modes_in_rotation()
+ # Initialize Plugin System (Phase 1: Foundation)
+ plugin_time = time.time()
+ self.plugin_manager = None
+ try:
+ from src.plugin_system import PluginManager
+ self.plugin_manager = PluginManager(
+ plugins_dir="plugins",
+ config_manager=self.config_manager,
+ display_manager=self.display_manager,
+ cache_manager=self.cache_manager
+ )
+
+ # Discover plugins
+ discovered_plugins = self.plugin_manager.discover_plugins()
+ logger.info(f"Discovered {len(discovered_plugins)} plugin(s)")
+
+ # Load enabled plugins
+ for plugin_id in discovered_plugins:
+ plugin_config = self.config.get(plugin_id, {})
+ if plugin_config.get('enabled', False):
+ if self.plugin_manager.load_plugin(plugin_id):
+ logger.info(f"Loaded plugin: {plugin_id}")
+
+ # Add plugin display modes to available_modes
+ manifest = self.plugin_manager.plugin_manifests.get(plugin_id, {})
+ display_modes = manifest.get('display_modes', [plugin_id])
+ for mode in display_modes:
+ if mode not in self.available_modes:
+ self.available_modes.append(mode)
+ logger.info(f"Added plugin mode to rotation: {mode}")
+ else:
+ logger.error(f"Failed to load plugin: {plugin_id}")
+
+ logger.info(f"Plugin system initialized in {time.time() - plugin_time:.3f} seconds")
+ except ImportError as e:
+ logger.warning(f"Plugin system not available: {e}")
+ self.plugin_manager = None
+ except Exception as e:
+ logger.error(f"Error initializing plugin system: {e}", exc_info=True)
+ self.plugin_manager = None
+
# Set initial display to first available mode (clock)
self.current_mode_index = 0
self.current_display_mode = "none"
@@ -833,6 +874,15 @@ def _update_modules(self):
if self.ncaaw_hockey_live: self.ncaaw_hockey_live.update()
if self.ncaaw_hockey_recent: self.ncaaw_hockey_recent.update()
if self.ncaaw_hockey_upcoming: self.ncaaw_hockey_upcoming.update()
+
+ # Update plugin managers if plugin system is available
+ if self.plugin_manager:
+ for plugin_id, plugin in self.plugin_manager.get_all_plugins().items():
+ if plugin.enabled:
+ try:
+ plugin.update()
+ except Exception as e:
+ logger.error(f"Error updating plugin {plugin_id}: {e}", exc_info=True)
def _check_live_games(self) -> tuple:
"""
@@ -1363,6 +1413,17 @@ def run(self):
manager_to_display = self.milb_live
elif self.current_display_mode == 'soccer_live' and self.soccer_live:
manager_to_display = self.soccer_live
+ # Check if this is a plugin mode
+ elif self.plugin_manager:
+ # Try to find a plugin that handles this display mode
+ for plugin_id, plugin in self.plugin_manager.get_all_plugins().items():
+ if not plugin.enabled:
+ continue
+ manifest = self.plugin_manager.plugin_manifests.get(plugin_id, {})
+ plugin_modes = manifest.get('display_modes', [plugin_id])
+ if self.current_display_mode in plugin_modes:
+ manager_to_display = plugin
+ break
# --- Perform Display Update ---
try:
diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py
new file mode 100644
index 000000000..4c3954daf
--- /dev/null
+++ b/src/plugin_system/__init__.py
@@ -0,0 +1,22 @@
+"""
+LEDMatrix Plugin System
+
+This module provides the core plugin infrastructure for the LEDMatrix project.
+It enables dynamic loading, management, and discovery of display plugins.
+
+API Version: 1.0.0
+"""
+
+__version__ = "1.0.0"
+__api_version__ = "1.0.0"
+
+from .base_plugin import BasePlugin
+from .plugin_manager import PluginManager
+from .store_manager import PluginStoreManager
+
+__all__ = [
+ 'BasePlugin',
+ 'PluginManager',
+ 'PluginStoreManager',
+]
+
diff --git a/src/plugin_system/base_plugin.py b/src/plugin_system/base_plugin.py
new file mode 100644
index 000000000..e35c0edd4
--- /dev/null
+++ b/src/plugin_system/base_plugin.py
@@ -0,0 +1,210 @@
+"""
+Base Plugin Interface
+
+All LEDMatrix plugins must inherit from BasePlugin and implement
+the required abstract methods: update() and display().
+
+API Version: 1.0.0
+Stability: Stable - maintains backward compatibility
+"""
+
+from abc import ABC, abstractmethod
+from typing import Dict, Any, Optional
+import logging
+
+
+class BasePlugin(ABC):
+ """
+ Base class that all plugins must inherit from.
+ Provides standard interface and helper methods.
+
+ This is the core plugin interface that all plugins must implement.
+ Provides common functionality for logging, configuration, and
+ integration with the LEDMatrix core system.
+ """
+
+ API_VERSION = "1.0.0"
+
+ def __init__(self, plugin_id: str, config: Dict[str, Any],
+ display_manager, cache_manager, plugin_manager):
+ """
+ Standard initialization for all plugins.
+
+ Args:
+ plugin_id: Unique identifier for this plugin instance
+ config: Plugin-specific configuration dictionary
+ display_manager: Shared display manager instance for rendering
+ cache_manager: Shared cache manager instance for data persistence
+ plugin_manager: Reference to plugin manager for inter-plugin communication
+ """
+ self.plugin_id = plugin_id
+ self.config = config
+ self.display_manager = display_manager
+ self.cache_manager = cache_manager
+ self.plugin_manager = plugin_manager
+ self.logger = logging.getLogger(f"plugin.{plugin_id}")
+ self.enabled = config.get('enabled', True)
+
+ self.logger.info(f"Initialized plugin: {plugin_id}")
+
+ @abstractmethod
+ def update(self) -> None:
+ """
+ Fetch/update data for this plugin.
+
+ This method is called based on update_interval specified in the
+ plugin's manifest. It should fetch any necessary data from APIs,
+ databases, or other sources and prepare it for display.
+
+ Use the cache_manager for caching API responses to avoid
+ excessive requests.
+
+ 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)
+ """
+ pass
+
+ @abstractmethod
+ def display(self, force_clear: bool = False) -> None:
+ """
+ Render this plugin's display.
+
+ This method is called during the display rotation or when the plugin
+ is explicitly requested to render. It should use the display_manager
+ to draw content on the LED matrix.
+
+ Args:
+ force_clear: 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()
+ """
+ pass
+
+ def get_display_duration(self) -> float:
+ """
+ Get the display duration for this plugin instance.
+
+ Can be overridden by plugins to provide dynamic durations based
+ on content (e.g., longer duration for more complex displays).
+
+ Returns:
+ Duration in seconds to display this plugin's content
+ """
+ return self.config.get('display_duration', 15.0)
+
+ def validate_config(self) -> bool:
+ """
+ Validate plugin configuration against schema.
+
+ Called during plugin loading to ensure configuration is valid.
+ Override this method to implement custom validation logic.
+
+ Returns:
+ True if config is valid, False otherwise
+
+ Example:
+ def validate_config(self):
+ required_fields = ['api_key', 'city']
+ for field in required_fields:
+ if field not in self.config:
+ self.logger.error(f"Missing required field: {field}")
+ return False
+ return True
+ """
+ # Basic validation - check that enabled is a boolean if present
+ if 'enabled' in self.config:
+ if not isinstance(self.config['enabled'], bool):
+ self.logger.error("'enabled' must be a boolean")
+ return False
+
+ # Check display_duration if present
+ if 'display_duration' in self.config:
+ duration = self.config['display_duration']
+ if not isinstance(duration, (int, float)) or duration <= 0:
+ self.logger.error("'display_duration' must be a positive number")
+ return False
+
+ return True
+
+ def cleanup(self) -> None:
+ """
+ Cleanup resources when plugin is unloaded.
+
+ Override this method to clean up any resources (e.g., close
+ file handles, terminate threads, close network connections).
+
+ This method is called when the plugin is unloaded or when the
+ system is shutting down.
+
+ Example:
+ def cleanup(self):
+ if hasattr(self, 'api_client'):
+ self.api_client.close()
+ if hasattr(self, 'worker_thread'):
+ self.worker_thread.stop()
+ """
+ self.logger.info(f"Cleaning up plugin: {self.plugin_id}")
+
+ def get_info(self) -> Dict[str, Any]:
+ """
+ Return plugin info for display in web UI.
+
+ Override this method to provide additional information about
+ the plugin's current state.
+
+ Returns:
+ Dict with plugin information including id, enabled status, and config
+
+ Example:
+ def get_info(self):
+ info = super().get_info()
+ info['games_count'] = len(self.games)
+ info['last_update'] = self.last_update_time
+ return info
+ """
+ return {
+ 'id': self.plugin_id,
+ 'enabled': self.enabled,
+ 'config': self.config,
+ 'api_version': self.API_VERSION
+ }
+
+ def on_enable(self) -> None:
+ """
+ Called when plugin is enabled.
+
+ Override this method to perform any actions needed when the
+ plugin is enabled (e.g., start background tasks, open connections).
+ """
+ self.enabled = True
+ self.logger.info(f"Plugin enabled: {self.plugin_id}")
+
+ def on_disable(self) -> None:
+ """
+ Called when plugin is disabled.
+
+ Override this method to perform any actions needed when the
+ plugin is disabled (e.g., stop background tasks, close connections).
+ """
+ self.enabled = False
+ self.logger.info(f"Plugin disabled: {self.plugin_id}")
+
diff --git a/src/plugin_system/plugin_manager.py b/src/plugin_system/plugin_manager.py
new file mode 100644
index 000000000..9f59ff128
--- /dev/null
+++ b/src/plugin_system/plugin_manager.py
@@ -0,0 +1,356 @@
+"""
+Plugin Manager
+
+Manages plugin discovery, loading, and lifecycle for the LEDMatrix system.
+Handles dynamic plugin loading from the plugins/ directory.
+
+API Version: 1.0.0
+"""
+
+import os
+import json
+import importlib
+import importlib.util
+import sys
+from pathlib import Path
+from typing import Dict, List, Optional, Any
+import logging
+
+
+class PluginManager:
+ """
+ Manages plugin discovery, loading, and lifecycle.
+
+ The PluginManager is responsible for:
+ - Discovering plugins in the plugins/ directory
+ - Loading plugin modules and instantiating plugin classes
+ - Managing plugin lifecycle (load, unload, reload)
+ - Providing access to loaded plugins
+ - Maintaining plugin manifests
+ """
+
+ def __init__(self, plugins_dir: str = "plugins",
+ config_manager=None, display_manager=None, cache_manager=None):
+ """
+ Initialize the Plugin Manager.
+
+ Args:
+ plugins_dir: Path to the plugins directory
+ config_manager: Configuration manager instance
+ display_manager: Display manager instance
+ cache_manager: Cache manager instance
+ """
+ self.plugins_dir = Path(plugins_dir)
+ self.config_manager = config_manager
+ self.display_manager = display_manager
+ self.cache_manager = cache_manager
+ self.logger = logging.getLogger(__name__)
+
+ # Active plugins
+ self.plugins: Dict[str, Any] = {}
+ self.plugin_manifests: Dict[str, Dict] = {}
+ self.plugin_modules: Dict[str, Any] = {}
+
+ # Ensure plugins directory exists
+ self.plugins_dir.mkdir(exist_ok=True)
+ self.logger.info(f"Plugin Manager initialized with plugins directory: {self.plugins_dir}")
+
+ def discover_plugins(self) -> List[str]:
+ """
+ Scan plugins directory for installed plugins.
+
+ A valid plugin directory must contain a manifest.json file.
+
+ Returns:
+ List of plugin IDs that were discovered
+ """
+ discovered = []
+
+ if not self.plugins_dir.exists():
+ self.logger.warning(f"Plugins directory not found: {self.plugins_dir}")
+ return discovered
+
+ for item in self.plugins_dir.iterdir():
+ if not item.is_dir():
+ continue
+
+ # Skip hidden directories and temp directories
+ if item.name.startswith('.') or item.name.startswith('_'):
+ continue
+
+ manifest_path = item / "manifest.json"
+ if manifest_path.exists():
+ try:
+ with open(manifest_path, 'r', encoding='utf-8') as f:
+ manifest = json.load(f)
+
+ plugin_id = manifest.get('id')
+ if not plugin_id:
+ self.logger.error(f"No 'id' field in manifest: {manifest_path}")
+ continue
+
+ if plugin_id != item.name:
+ self.logger.warning(
+ f"Plugin ID '{plugin_id}' doesn't match directory name '{item.name}'"
+ )
+
+ discovered.append(plugin_id)
+ self.plugin_manifests[plugin_id] = manifest
+ self.logger.info(f"Discovered plugin: {plugin_id} v{manifest.get('version', '?')}")
+
+ except json.JSONDecodeError as e:
+ self.logger.error(f"Invalid JSON in manifest {manifest_path}: {e}")
+ except Exception as e:
+ self.logger.error(f"Error reading manifest in {item}: {e}")
+
+ self.logger.info(f"Discovered {len(discovered)} plugin(s)")
+ return discovered
+
+ def load_plugin(self, plugin_id: str) -> bool:
+ """
+ Load a plugin by ID.
+
+ This method:
+ 1. Checks if plugin is already loaded
+ 2. Validates the manifest exists
+ 3. Imports the plugin module
+ 4. Instantiates the plugin class
+ 5. Validates the plugin configuration
+ 6. Stores the plugin instance
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if loaded successfully, False otherwise
+ """
+ if plugin_id in self.plugins:
+ self.logger.warning(f"Plugin {plugin_id} already loaded")
+ return True
+
+ manifest = self.plugin_manifests.get(plugin_id)
+ if not manifest:
+ self.logger.error(f"No manifest found for plugin: {plugin_id}")
+ return False
+
+ try:
+ # Get plugin directory
+ plugin_dir = self.plugins_dir / plugin_id
+ if not plugin_dir.exists():
+ self.logger.error(f"Plugin directory not found: {plugin_dir}")
+ return False
+
+ # Get entry point
+ entry_point = manifest.get('entry_point', 'manager.py')
+ entry_file = plugin_dir / entry_point
+
+ if not entry_file.exists():
+ self.logger.error(f"Entry point not found: {entry_file}")
+ return False
+
+ # Import the plugin module
+ module_name = f"plugin_{plugin_id.replace('-', '_')}"
+ spec = importlib.util.spec_from_file_location(module_name, entry_file)
+
+ if spec is None or spec.loader is None:
+ self.logger.error(f"Could not create module spec for {entry_file}")
+ return False
+
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+
+ self.plugin_modules[plugin_id] = module
+
+ # Get the plugin class
+ class_name = manifest.get('class_name')
+ if not class_name:
+ self.logger.error(f"No class_name in manifest for {plugin_id}")
+ return False
+
+ if not hasattr(module, class_name):
+ self.logger.error(f"Class {class_name} not found in {entry_file}")
+ return False
+
+ plugin_class = getattr(module, class_name)
+
+ # Get plugin config
+ if self.config_manager:
+ config = self.config_manager.load_config().get(plugin_id, {})
+ else:
+ config = {}
+
+ # Instantiate the plugin
+ plugin_instance = plugin_class(
+ plugin_id=plugin_id,
+ config=config,
+ display_manager=self.display_manager,
+ cache_manager=self.cache_manager,
+ plugin_manager=self
+ )
+
+ # Validate configuration
+ if not plugin_instance.validate_config():
+ self.logger.error(f"Config validation failed for {plugin_id}")
+ return False
+
+ # Store the plugin
+ self.plugins[plugin_id] = plugin_instance
+ self.logger.info(f"Loaded plugin: {plugin_id} v{manifest.get('version', '?')}")
+
+ # Call on_enable if plugin is enabled
+ if plugin_instance.enabled:
+ plugin_instance.on_enable()
+
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error loading plugin {plugin_id}: {e}", exc_info=True)
+ return False
+
+ def unload_plugin(self, plugin_id: str) -> bool:
+ """
+ Unload a plugin by ID.
+
+ This method:
+ 1. Calls the plugin's cleanup() method
+ 2. Removes the plugin from active plugins
+ 3. Removes the plugin module from sys.modules
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if unloaded successfully, False otherwise
+ """
+ if plugin_id not in self.plugins:
+ self.logger.warning(f"Plugin {plugin_id} not loaded")
+ return False
+
+ try:
+ plugin = self.plugins[plugin_id]
+
+ # Call cleanup
+ plugin.cleanup()
+
+ # Call on_disable
+ plugin.on_disable()
+
+ # Remove from active plugins
+ del self.plugins[plugin_id]
+
+ # Remove module from sys.modules if present
+ module_name = f"plugin_{plugin_id.replace('-', '_')}"
+ if module_name in sys.modules:
+ del sys.modules[module_name]
+
+ # Remove from plugin_modules
+ if plugin_id in self.plugin_modules:
+ del self.plugin_modules[plugin_id]
+
+ self.logger.info(f"Unloaded plugin: {plugin_id}")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error unloading plugin {plugin_id}: {e}", exc_info=True)
+ return False
+
+ def reload_plugin(self, plugin_id: str) -> bool:
+ """
+ Reload a plugin (unload and load).
+
+ Useful for development and when plugin files have been updated.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if reloaded successfully, False otherwise
+ """
+ self.logger.info(f"Reloading plugin: {plugin_id}")
+
+ if plugin_id in self.plugins:
+ if not self.unload_plugin(plugin_id):
+ return False
+
+ # Re-discover to get updated manifest
+ manifest_path = self.plugins_dir / plugin_id / "manifest.json"
+ if manifest_path.exists():
+ try:
+ with open(manifest_path, 'r', encoding='utf-8') as f:
+ self.plugin_manifests[plugin_id] = json.load(f)
+ except Exception as e:
+ self.logger.error(f"Error reading manifest: {e}")
+ return False
+
+ return self.load_plugin(plugin_id)
+
+ def get_plugin(self, plugin_id: str) -> Optional[Any]:
+ """
+ Get a loaded plugin instance by ID.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ Plugin instance or None if not loaded
+ """
+ return self.plugins.get(plugin_id)
+
+ def get_all_plugins(self) -> Dict[str, Any]:
+ """
+ Get all loaded plugins.
+
+ Returns:
+ Dict of plugin_id: plugin_instance
+ """
+ return self.plugins.copy()
+
+ def get_enabled_plugins(self) -> List[str]:
+ """
+ Get list of enabled plugin IDs.
+
+ Returns:
+ List of plugin IDs that are currently enabled
+ """
+ return [pid for pid, plugin in self.plugins.items() if plugin.enabled]
+
+ def get_plugin_info(self, plugin_id: str) -> Optional[Dict[str, Any]]:
+ """
+ Get information about a plugin (manifest + runtime info).
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ Dict with plugin information or None if not found
+ """
+ manifest = self.plugin_manifests.get(plugin_id)
+ if not manifest:
+ return None
+
+ info = manifest.copy()
+
+ # Add runtime information if plugin is loaded
+ plugin = self.plugins.get(plugin_id)
+ if plugin:
+ info['loaded'] = True
+ info['runtime_info'] = plugin.get_info()
+ else:
+ info['loaded'] = False
+
+ return info
+
+ def get_all_plugin_info(self) -> List[Dict[str, Any]]:
+ """
+ Get information about all discovered plugins.
+
+ Returns:
+ List of plugin information dicts
+ """
+ return [
+ self.get_plugin_info(plugin_id)
+ for plugin_id in self.plugin_manifests.keys()
+ if self.get_plugin_info(plugin_id) is not None
+ ]
+
diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py
new file mode 100644
index 000000000..fc93c92df
--- /dev/null
+++ b/src/plugin_system/store_manager.py
@@ -0,0 +1,506 @@
+"""
+Plugin Store Manager
+
+Manages plugin discovery, installation, and updates from GitHub repositories.
+Provides HACS-like functionality for plugin management.
+
+API Version: 1.0.0
+"""
+
+import requests
+import subprocess
+import shutil
+import zipfile
+import io
+import json
+from pathlib import Path
+from typing import List, Dict, Optional
+import logging
+
+
+class PluginStoreManager:
+ """
+ Manages plugin discovery, installation, and updates from GitHub.
+
+ The store manager provides:
+ - Plugin registry fetching and searching
+ - Plugin installation from GitHub
+ - Plugin uninstallation
+ - Plugin updates
+ - Direct installation from GitHub URLs
+ """
+
+ REGISTRY_URL = "https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugin-registry/main/plugins.json"
+
+ def __init__(self, plugins_dir: str = "plugins"):
+ """
+ Initialize the Plugin Store Manager.
+
+ Args:
+ plugins_dir: Path to the plugins directory
+ """
+ self.plugins_dir = Path(plugins_dir)
+ self.logger = logging.getLogger(__name__)
+ self.registry_cache = None
+
+ # Ensure plugins directory exists
+ self.plugins_dir.mkdir(exist_ok=True)
+ self.logger.info(f"Plugin Store Manager initialized with plugins directory: {self.plugins_dir}")
+
+ def fetch_registry(self, force_refresh: bool = False) -> Dict:
+ """
+ Fetch the plugin registry from GitHub.
+
+ The registry is cached in memory to avoid repeated network requests.
+
+ Args:
+ force_refresh: Force refresh even if cached
+
+ Returns:
+ Registry data dict with 'version' and 'plugins' keys
+ """
+ if self.registry_cache and not force_refresh:
+ self.logger.debug("Using cached registry")
+ return self.registry_cache
+
+ try:
+ self.logger.info("Fetching plugin registry from GitHub...")
+ response = requests.get(self.REGISTRY_URL, timeout=10)
+ response.raise_for_status()
+ self.registry_cache = response.json()
+
+ plugin_count = len(self.registry_cache.get('plugins', []))
+ self.logger.info(f"Fetched registry with {plugin_count} plugin(s)")
+
+ return self.registry_cache
+
+ except requests.exceptions.RequestException as e:
+ self.logger.error(f"Error fetching registry: {e}")
+ # Return empty registry on error
+ return {"version": "0.0.0", "plugins": []}
+ except json.JSONDecodeError as e:
+ self.logger.error(f"Error parsing registry JSON: {e}")
+ return {"version": "0.0.0", "plugins": []}
+
+ def search_plugins(self, query: str = "", category: str = "",
+ tags: List[str] = None) -> List[Dict]:
+ """
+ Search for plugins in the registry.
+
+ Args:
+ query: Search query string (searches name, description, id)
+ category: Filter by category (e.g., 'sports', 'weather', 'time')
+ tags: Filter by tags (matches if any tag is present)
+
+ Returns:
+ List of matching plugin dicts
+ """
+ registry = self.fetch_registry()
+ plugins = registry.get('plugins', [])
+
+ if tags is None:
+ tags = []
+
+ results = []
+ for plugin in plugins:
+ # Category filter
+ if category and plugin.get('category') != category:
+ continue
+
+ # Tags filter (match if any tag is present)
+ if tags and not any(tag in plugin.get('tags', []) for tag in tags):
+ continue
+
+ # Query search
+ if query:
+ query_lower = query.lower()
+ searchable_text = ' '.join([
+ plugin.get('name', ''),
+ plugin.get('description', ''),
+ plugin.get('id', ''),
+ plugin.get('author', '')
+ ]).lower()
+
+ if query_lower not in searchable_text:
+ continue
+
+ results.append(plugin)
+
+ self.logger.debug(f"Search returned {len(results)} result(s)")
+ return results
+
+ def install_plugin(self, plugin_id: str, version: str = "latest") -> bool:
+ """
+ Install a plugin from GitHub.
+
+ This method:
+ 1. Looks up the plugin in the registry
+ 2. Downloads the plugin files from GitHub
+ 3. Extracts to the plugins directory
+ 4. Installs Python dependencies if requirements.txt exists
+
+ Args:
+ plugin_id: Plugin identifier
+ version: Version to install (default: latest)
+
+ Returns:
+ True if installed successfully, False otherwise
+ """
+ registry = self.fetch_registry()
+ plugin_info = next((p for p in registry['plugins'] if p['id'] == plugin_id), None)
+
+ if not plugin_info:
+ self.logger.error(f"Plugin not found in registry: {plugin_id}")
+ return False
+
+ try:
+ # Get version info
+ if version == "latest":
+ version_info = plugin_info['versions'][0] # First is latest
+ else:
+ version_info = next(
+ (v for v in plugin_info['versions'] if v['version'] == version),
+ None
+ )
+ if not version_info:
+ self.logger.error(f"Version not found: {version}")
+ return False
+
+ self.logger.info(f"Installing plugin {plugin_id} v{version_info['version']}...")
+
+ # Check if plugin directory already exists
+ plugin_path = self.plugins_dir / plugin_id
+ if plugin_path.exists():
+ self.logger.warning(f"Plugin directory already exists: {plugin_id}. Removing...")
+ shutil.rmtree(plugin_path)
+
+ # Try git clone first (more efficient)
+ repo_url = plugin_info['repo']
+ tag = version_info.get('version')
+
+ if self._install_via_git(repo_url, plugin_path, tag):
+ self.logger.info(f"Cloned plugin {plugin_id} via git")
+ else:
+ # Fall back to downloading zip
+ download_url = version_info.get('download_url')
+ if not download_url:
+ self.logger.error(f"No download_url found for {plugin_id}")
+ return False
+
+ if not self._install_via_download(download_url, plugin_path, plugin_id):
+ return False
+
+ # Verify manifest exists
+ manifest_path = plugin_path / "manifest.json"
+ if not manifest_path.exists():
+ self.logger.error(f"No manifest.json found after installation: {plugin_id}")
+ shutil.rmtree(plugin_path)
+ return False
+
+ # Install Python dependencies
+ self._install_dependencies(plugin_path)
+
+ self.logger.info(f"Successfully installed plugin: {plugin_id}")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error installing plugin {plugin_id}: {e}", exc_info=True)
+ # Cleanup on failure
+ if plugin_path.exists():
+ shutil.rmtree(plugin_path)
+ return False
+
+ def _install_via_git(self, repo_url: str, target_path: Path, tag: str = None) -> bool:
+ """
+ Install plugin via git clone.
+
+ Args:
+ repo_url: GitHub repository URL
+ target_path: Target installation path
+ tag: Git tag/version to checkout
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ cmd = ['git', 'clone', '--depth', '1']
+ if tag:
+ cmd.extend(['--branch', f"v{tag}"])
+ cmd.extend([repo_url, str(target_path)])
+
+ result = subprocess.run(
+ cmd,
+ check=True,
+ capture_output=True,
+ text=True
+ )
+
+ # Remove .git directory to save space
+ git_dir = target_path / ".git"
+ if git_dir.exists():
+ shutil.rmtree(git_dir)
+
+ return True
+
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
+ self.logger.debug(f"Git clone failed: {e}")
+ return False
+
+ def _install_via_download(self, download_url: str, target_path: Path,
+ plugin_id: str) -> bool:
+ """
+ Install plugin via ZIP download.
+
+ Args:
+ download_url: URL to download ZIP file
+ target_path: Target installation path
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if successful, False otherwise
+ """
+ try:
+ self.logger.info(f"Downloading plugin from {download_url}...")
+ response = requests.get(download_url, timeout=30)
+ response.raise_for_status()
+
+ # Extract ZIP
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zip_file:
+ # GitHub archives create a root directory like "repo-name-tag/"
+ # We need to extract and move contents
+ temp_dir = self.plugins_dir / f".temp_{plugin_id}"
+ if temp_dir.exists():
+ shutil.rmtree(temp_dir)
+
+ zip_file.extractall(temp_dir)
+
+ # Find the root directory (should be only one)
+ root_dirs = [d for d in temp_dir.iterdir() if d.is_dir()]
+ if len(root_dirs) != 1:
+ self.logger.error(f"Unexpected ZIP structure: {len(root_dirs)} root directories")
+ shutil.rmtree(temp_dir)
+ return False
+
+ # Move contents to target path
+ shutil.move(str(root_dirs[0]), str(target_path))
+
+ # Cleanup temp directory
+ shutil.rmtree(temp_dir)
+
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error downloading plugin: {e}")
+ return False
+
+ def _install_dependencies(self, plugin_path: Path) -> bool:
+ """
+ Install Python dependencies from requirements.txt.
+
+ Args:
+ plugin_path: Path to plugin directory
+
+ Returns:
+ True if successful or no requirements, False on error
+ """
+ requirements_file = plugin_path / "requirements.txt"
+ if not requirements_file.exists():
+ self.logger.debug("No requirements.txt found, skipping dependency installation")
+ return True
+
+ try:
+ self.logger.info("Installing plugin dependencies...")
+ result = subprocess.run(
+ ['pip3', 'install', '--break-system-packages', '-r', str(requirements_file)],
+ check=True,
+ capture_output=True,
+ text=True
+ )
+ self.logger.info("Dependencies installed successfully")
+ return True
+
+ except subprocess.CalledProcessError as e:
+ self.logger.error(f"Error installing dependencies: {e.stderr}")
+ # Don't fail installation if dependencies fail
+ # User can manually install them
+ return True
+ except FileNotFoundError:
+ self.logger.warning("pip3 not found, skipping dependency installation")
+ return True
+
+ def uninstall_plugin(self, plugin_id: str) -> bool:
+ """
+ Uninstall a plugin.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if uninstalled successfully, False otherwise
+ """
+ plugin_path = self.plugins_dir / plugin_id
+
+ if not plugin_path.exists():
+ self.logger.warning(f"Plugin not found: {plugin_id}")
+ return False
+
+ try:
+ self.logger.info(f"Uninstalling plugin: {plugin_id}")
+ shutil.rmtree(plugin_path)
+ self.logger.info(f"Uninstalled plugin: {plugin_id}")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error uninstalling plugin {plugin_id}: {e}")
+ return False
+
+ def update_plugin(self, plugin_id: str) -> bool:
+ """
+ Update a plugin to the latest version.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ True if updated successfully, False otherwise
+ """
+ plugin_path = self.plugins_dir / plugin_id
+
+ if not plugin_path.exists():
+ self.logger.error(f"Plugin not installed: {plugin_id}")
+ return False
+
+ try:
+ # Try git pull first
+ git_dir = plugin_path / ".git"
+ if git_dir.exists():
+ self.logger.info(f"Updating plugin {plugin_id} via git pull...")
+ result = subprocess.run(
+ ['git', '-C', str(plugin_path), 'pull'],
+ capture_output=True,
+ text=True
+ )
+ if result.returncode == 0:
+ self.logger.info(f"Updated plugin {plugin_id} via git pull")
+ # Reinstall dependencies in case they changed
+ self._install_dependencies(plugin_path)
+ return True
+
+ # Fall back to re-download
+ self.logger.info(f"Re-downloading plugin {plugin_id} for update...")
+ return self.install_plugin(plugin_id, version="latest")
+
+ except Exception as e:
+ self.logger.error(f"Error updating plugin {plugin_id}: {e}")
+ return False
+
+ def install_from_url(self, repo_url: str, plugin_id: str = None) -> bool:
+ """
+ Install a plugin directly from a GitHub URL (for custom/unlisted plugins).
+
+ This allows users to install plugins not in the official registry.
+
+ Args:
+ repo_url: GitHub repository URL
+ plugin_id: Optional custom plugin ID (extracted from manifest if not provided)
+
+ Returns:
+ True if installed successfully, False otherwise
+ """
+ try:
+ self.logger.info(f"Installing plugin from URL: {repo_url}")
+
+ # Clone to temporary location
+ temp_dir = self.plugins_dir / ".temp_install"
+ if temp_dir.exists():
+ shutil.rmtree(temp_dir)
+
+ # Try git clone
+ if not self._install_via_git(repo_url, temp_dir):
+ self.logger.error("Git clone failed")
+ return False
+
+ # Read manifest to get plugin ID
+ manifest_path = temp_dir / "manifest.json"
+ if not manifest_path.exists():
+ self.logger.error("No manifest.json found in repository")
+ shutil.rmtree(temp_dir)
+ return False
+
+ with open(manifest_path, 'r', encoding='utf-8') as f:
+ manifest = json.load(f)
+
+ plugin_id = plugin_id or manifest.get('id')
+ if not plugin_id:
+ self.logger.error("No plugin ID found in manifest")
+ shutil.rmtree(temp_dir)
+ return False
+
+ # Move to plugins directory
+ final_path = self.plugins_dir / plugin_id
+ if final_path.exists():
+ self.logger.warning(f"Plugin {plugin_id} already exists, removing...")
+ shutil.rmtree(final_path)
+
+ shutil.move(str(temp_dir), str(final_path))
+
+ # Install dependencies
+ self._install_dependencies(final_path)
+
+ self.logger.info(f"Installed plugin from URL: {plugin_id}")
+ return True
+
+ except Exception as e:
+ self.logger.error(f"Error installing from URL: {e}", exc_info=True)
+ # Cleanup
+ if temp_dir.exists():
+ shutil.rmtree(temp_dir)
+ return False
+
+ def check_for_updates(self, plugin_id: str) -> Optional[Dict]:
+ """
+ Check if an update is available for a plugin.
+
+ Args:
+ plugin_id: Plugin identifier
+
+ Returns:
+ Dict with update info if available, None otherwise
+ """
+ plugin_path = self.plugins_dir / plugin_id
+ if not plugin_path.exists():
+ return None
+
+ # Read current version from manifest
+ manifest_path = plugin_path / "manifest.json"
+ if not manifest_path.exists():
+ return None
+
+ try:
+ with open(manifest_path, 'r', encoding='utf-8') as f:
+ current_manifest = json.load(f)
+ current_version = current_manifest.get('version', '0.0.0')
+
+ # Get latest version from registry
+ registry = self.fetch_registry()
+ plugin_info = next((p for p in registry['plugins'] if p['id'] == plugin_id), None)
+
+ if not plugin_info:
+ return None
+
+ latest_version = plugin_info['versions'][0]['version']
+
+ if latest_version != current_version:
+ return {
+ 'plugin_id': plugin_id,
+ 'current_version': current_version,
+ 'latest_version': latest_version,
+ 'update_available': True
+ }
+
+ return None
+
+ except Exception as e:
+ self.logger.error(f"Error checking for updates: {e}")
+ return None
+
diff --git a/web_interface_v2.py b/web_interface_v2.py
index 9e8cfebf5..544da8ddf 100644
--- a/web_interface_v2.py
+++ b/web_interface_v2.py
@@ -1649,6 +1649,239 @@ def get_custom_layouts():
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
+# ===== Plugin System API Endpoints (Phase 1: Foundation) =====
+
+@app.route('/api/plugins/store/list', methods=['GET'])
+def api_plugin_store_list():
+ """Get list of available plugins from store registry."""
+ try:
+ from src.plugin_system import PluginStoreManager
+ store_manager = PluginStoreManager()
+ registry = store_manager.fetch_registry()
+ return jsonify({
+ 'status': 'success',
+ 'plugins': registry.get('plugins', [])
+ })
+ except Exception as e:
+ logger.error(f"Error fetching plugin store list: {e}", exc_info=True)
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/installed', methods=['GET'])
+def api_plugins_installed():
+ """Get list of installed and discovered plugins."""
+ try:
+ from src.plugin_system import PluginManager
+ plugin_manager = PluginManager(
+ plugins_dir="plugins",
+ config_manager=config_manager,
+ display_manager=display_manager,
+ cache_manager=cache_manager
+ )
+
+ # Discover all plugins
+ discovered = plugin_manager.discover_plugins()
+
+ plugins = []
+ for plugin_id in discovered:
+ info = plugin_manager.get_plugin_info(plugin_id)
+ if info:
+ # Add configuration from config.json
+ plugin_config = config_manager.load_config().get(plugin_id, {})
+ info['config'] = plugin_config
+ info['enabled'] = plugin_config.get('enabled', False)
+ plugins.append(info)
+
+ return jsonify({
+ 'status': 'success',
+ 'plugins': plugins
+ })
+ except Exception as e:
+ logger.error(f"Error fetching installed plugins: {e}", exc_info=True)
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/install', methods=['POST'])
+def api_plugin_install():
+ """Install a plugin from the store registry."""
+ try:
+ data = request.get_json()
+ plugin_id = data.get('plugin_id')
+ version = data.get('version', 'latest')
+
+ if not plugin_id:
+ return jsonify({
+ 'status': 'error',
+ 'message': 'plugin_id is required'
+ }), 400
+
+ from src.plugin_system import PluginStoreManager
+ store_manager = PluginStoreManager()
+ success = store_manager.install_plugin(plugin_id, version)
+
+ if success:
+ return jsonify({
+ 'status': 'success',
+ 'message': f'Plugin {plugin_id} installed successfully. Restart display to activate.'
+ })
+ else:
+ return jsonify({
+ 'status': 'error',
+ 'message': f'Failed to install plugin {plugin_id}'
+ }), 500
+ except Exception as e:
+ logger.error(f"Error installing plugin: {e}", exc_info=True)
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/uninstall', methods=['POST'])
+def api_plugin_uninstall():
+ """Uninstall a plugin."""
+ try:
+ data = request.get_json()
+ plugin_id = data.get('plugin_id')
+
+ if not plugin_id:
+ return jsonify({
+ 'status': 'error',
+ 'message': 'plugin_id is required'
+ }), 400
+
+ from src.plugin_system import PluginStoreManager
+ store_manager = PluginStoreManager()
+ success = store_manager.uninstall_plugin(plugin_id)
+
+ if success:
+ # Also remove from config
+ config = config_manager.load_config()
+ if plugin_id in config:
+ del config[plugin_id]
+ config_manager.save_config(config)
+
+ return jsonify({
+ 'status': 'success',
+ 'message': f'Plugin {plugin_id} uninstalled successfully. Restart display to apply changes.'
+ })
+ else:
+ return jsonify({
+ 'status': 'error',
+ 'message': f'Failed to uninstall plugin {plugin_id}'
+ }), 500
+ except Exception as e:
+ logger.error(f"Error uninstalling plugin: {e}", exc_info=True)
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/toggle', methods=['POST'])
+def api_plugin_toggle():
+ """Enable or disable a plugin."""
+ try:
+ data = request.get_json()
+ plugin_id = data.get('plugin_id')
+ enabled = data.get('enabled', True)
+
+ if not plugin_id:
+ return jsonify({
+ 'status': 'error',
+ 'message': 'plugin_id is required'
+ }), 400
+
+ # Update config
+ config = config_manager.load_config()
+ if plugin_id not in config:
+ config[plugin_id] = {}
+ config[plugin_id]['enabled'] = enabled
+ config_manager.save_config(config)
+
+ return jsonify({
+ 'status': 'success',
+ 'message': f'Plugin {plugin_id} {"enabled" if enabled else "disabled"}. Restart display to apply changes.'
+ })
+ except Exception as e:
+ logger.error(f"Error toggling plugin: {e}", exc_info=True)
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/update', methods=['POST'])
+def api_plugin_update():
+ """Update a plugin to the latest version."""
+ try:
+ data = request.get_json()
+ plugin_id = data.get('plugin_id')
+
+ if not plugin_id:
+ return jsonify({
+ 'status': 'error',
+ 'message': 'plugin_id is required'
+ }), 400
+
+ from src.plugin_system import PluginStoreManager
+ store_manager = PluginStoreManager()
+ success = store_manager.update_plugin(plugin_id)
+
+ if success:
+ return jsonify({
+ 'status': 'success',
+ 'message': f'Plugin {plugin_id} updated successfully. Restart display to apply changes.'
+ })
+ else:
+ return jsonify({
+ 'status': 'error',
+ 'message': f'Failed to update plugin {plugin_id}'
+ }), 500
+ except Exception as e:
+ logger.error(f"Error updating plugin: {e}", exc_info=True)
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+@app.route('/api/plugins/install-from-url', methods=['POST'])
+def api_plugin_install_from_url():
+ """Install a plugin directly from a GitHub URL."""
+ try:
+ data = request.get_json()
+ repo_url = data.get('repo_url')
+
+ if not repo_url:
+ return jsonify({
+ 'status': 'error',
+ 'message': 'repo_url is required'
+ }), 400
+
+ from src.plugin_system import PluginStoreManager
+ store_manager = PluginStoreManager()
+ success = store_manager.install_from_url(repo_url)
+
+ if success:
+ return jsonify({
+ 'status': 'success',
+ 'message': 'Plugin installed from URL successfully. Restart display to activate.'
+ })
+ else:
+ return jsonify({
+ 'status': 'error',
+ 'message': 'Failed to install plugin from URL'
+ }), 500
+ except Exception as e:
+ logger.error(f"Error installing plugin from URL: {e}", exc_info=True)
+ return jsonify({
+ 'status': 'error',
+ 'message': str(e)
+ }), 500
+
+# ===== End Plugin System API Endpoints =====
+
@socketio.on('connect')
def handle_connect():
"""Handle client connection."""
From 3a40c26ec12e4273e558d532ac9d247163c80cd0 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Wed, 8 Oct 2025 21:47:51 -0400
Subject: [PATCH 005/736] test plugin
---
plugins/hello-world/QUICK_START.md | 131 ++++++++++
plugins/hello-world/README.md | 186 ++++++++++++++
plugins/hello-world/config_schema.json | 59 +++++
plugins/hello-world/example_config.json | 12 +
plugins/hello-world/manager.py | 187 ++++++++++++++
plugins/hello-world/manifest.json | 28 +++
test/test_plugin_system.py | 320 ++++++++++++++++++++++++
7 files changed, 923 insertions(+)
create mode 100644 plugins/hello-world/QUICK_START.md
create mode 100644 plugins/hello-world/README.md
create mode 100644 plugins/hello-world/config_schema.json
create mode 100644 plugins/hello-world/example_config.json
create mode 100644 plugins/hello-world/manager.py
create mode 100644 plugins/hello-world/manifest.json
create mode 100644 test/test_plugin_system.py
diff --git a/plugins/hello-world/QUICK_START.md b/plugins/hello-world/QUICK_START.md
new file mode 100644
index 000000000..0fc447b77
--- /dev/null
+++ b/plugins/hello-world/QUICK_START.md
@@ -0,0 +1,131 @@
+# Hello World Plugin - Quick Start Guide
+
+## 🚀 Enable the Plugin
+
+Add this to your `config/config.json`:
+
+```json
+{
+ "hello-world": {
+ "enabled": true,
+ "message": "Hello, World!",
+ "show_time": true,
+ "color": [255, 255, 255],
+ "time_color": [0, 255, 255],
+ "display_duration": 10
+ }
+}
+```
+
+## ✅ Test Results
+
+All plugin system tests passed:
+- ✅ Plugin Discovery
+- ✅ Plugin Loading
+- ✅ Manifest Validation
+- ✅ BasePlugin Interface
+- ✅ Store Manager
+
+## 📋 Verify Plugin is Working
+
+### 1. Check Plugin Discovery (Windows)
+```bash
+python test/test_plugin_system.py
+```
+
+### 2. Check on Raspberry Pi
+```bash
+# SSH into your Pi
+ssh pi@your-pi-ip
+
+# Check if plugin is discovered
+sudo journalctl -u ledmatrix -n 50 | grep "hello-world"
+
+# Should see:
+# Discovered plugin: hello-world v1.0.0
+# Loaded plugin: hello-world
+```
+
+### 3. Via Web API
+```bash
+# List installed plugins
+curl http://localhost:5001/api/plugins/installed
+
+# Enable the plugin
+curl -X POST http://localhost:5001/api/plugins/toggle \
+ -H "Content-Type: application/json" \
+ -d '{"plugin_id": "hello-world", "enabled": true}'
+```
+
+## 🎨 Customization Examples
+
+### Lightning Theme
+```json
+{
+ "hello-world": {
+ "enabled": true,
+ "message": "Go Bolts!",
+ "color": [0, 128, 255],
+ "time_color": [255, 255, 255],
+ "display_duration": 15
+ }
+}
+```
+
+### RGB Rainbow
+```json
+{
+ "hello-world": {
+ "enabled": true,
+ "message": "RGB Test",
+ "color": [255, 0, 255],
+ "show_time": false,
+ "display_duration": 5
+ }
+}
+```
+
+## 🔧 Troubleshooting
+
+### Plugin Not Showing
+1. Check `enabled: true` in config
+2. Restart the display service
+3. Check logs for errors
+
+### Configuration Errors
+- Ensure all colors are [R, G, B] arrays
+- Values must be 0-255
+- `display_duration` must be a positive number
+
+## 📂 Plugin Files
+
+```
+plugins/hello-world/
+├── manifest.json # Plugin metadata
+├── manager.py # Plugin code
+├── config_schema.json # Configuration schema
+├── example_config.json # Example configuration
+├── README.md # Full documentation
+└── QUICK_START.md # This file
+```
+
+## 🎯 What This Demonstrates
+
+- ✅ Plugin discovery and loading
+- ✅ Configuration validation
+- ✅ Display rendering
+- ✅ Error handling
+- ✅ BasePlugin interface
+- ✅ Integration with display rotation
+
+## 📚 Next Steps
+
+- Modify the message to personalize it
+- Change colors to match your team
+- Adjust display_duration for timing
+- Use this as a template for your own plugins!
+
+---
+
+**Need Help?** Check the main [README.md](README.md) or [Plugin System Documentation](../../docs/PLUGIN_PHASE_1_SUMMARY.md)
+
diff --git a/plugins/hello-world/README.md b/plugins/hello-world/README.md
new file mode 100644
index 000000000..b5af69b49
--- /dev/null
+++ b/plugins/hello-world/README.md
@@ -0,0 +1,186 @@
+# Hello World Plugin
+
+A simple test plugin for the LEDMatrix plugin system. Displays a customizable greeting message with optional time display.
+
+## Purpose
+
+This plugin serves as:
+- **Test plugin** for validating the plugin system works correctly
+- **Example plugin** for developers creating their own plugins
+- **Simple demonstration** of the BasePlugin interface
+
+## Features
+
+- ✅ Customizable greeting message
+- ✅ Optional time display
+- ✅ Configurable text colors
+- ✅ Proper error handling
+- ✅ Configuration validation
+
+## Installation
+
+This plugin is included as a test plugin. To enable it:
+
+1. Edit `config/config.json` and add:
+
+```json
+{
+ "hello-world": {
+ "enabled": true,
+ "message": "Hello, World!",
+ "show_time": true,
+ "color": [255, 255, 255],
+ "time_color": [0, 255, 255],
+ "display_duration": 10
+ }
+}
+```
+
+2. Restart the display:
+
+```bash
+sudo systemctl restart ledmatrix
+```
+
+## Configuration Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enabled` | boolean | `true` | Enable/disable the plugin |
+| `message` | string | `"Hello, World!"` | The greeting message to display |
+| `show_time` | boolean | `true` | Show current time below message |
+| `color` | array | `[255, 255, 255]` | RGB color for message (white) |
+| `time_color` | array | `[0, 255, 255]` | RGB color for time (cyan) |
+| `display_duration` | number | `10` | Display time in seconds |
+
+## Examples
+
+### Minimal Configuration
+```json
+{
+ "hello-world": {
+ "enabled": true
+ }
+}
+```
+
+### Custom Message
+```json
+{
+ "hello-world": {
+ "enabled": true,
+ "message": "Go Lightning!",
+ "color": [0, 128, 255],
+ "display_duration": 15
+ }
+}
+```
+
+### Message Only (No Time)
+```json
+{
+ "hello-world": {
+ "enabled": true,
+ "message": "LED Matrix",
+ "show_time": false,
+ "color": [255, 0, 255]
+ }
+}
+```
+
+## Testing the Plugin
+
+### 1. Check Plugin Discovery
+
+After adding the configuration, check the logs:
+
+```bash
+sudo journalctl -u ledmatrix -f | grep hello-world
+```
+
+You should see:
+```
+Discovered plugin: hello-world v1.0.0
+Loaded plugin: hello-world
+Hello World plugin initialized with message: 'Hello, World!'
+```
+
+### 2. Test via Web API
+
+Check if the plugin is installed:
+```bash
+curl http://localhost:5001/api/plugins/installed | jq '.plugins[] | select(.id=="hello-world")'
+```
+
+### 3. Watch It Display
+
+The plugin will appear in the normal display rotation based on your `display_duration` setting.
+
+## Development Notes
+
+This plugin demonstrates:
+
+### BasePlugin Interface
+```python
+class HelloWorldPlugin(BasePlugin):
+ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
+ super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
+ # Initialize your plugin
+
+ def update(self):
+ # Fetch/update data
+ pass
+
+ def display(self, force_clear=False):
+ # Render to display
+ pass
+```
+
+### Configuration Validation
+```python
+def validate_config(self):
+ # Validate configuration values
+ return True
+```
+
+### Error Handling
+```python
+try:
+ # Plugin logic
+except Exception as e:
+ self.logger.error(f"Error: {e}", exc_info=True)
+```
+
+## Troubleshooting
+
+### Plugin Not Loading
+- Check that `manifest.json` is valid JSON
+- Verify `enabled: true` in config.json
+- Check logs for error messages
+- Ensure Python path is correct
+
+### Display Issues
+- Verify display_manager is initialized
+- Check that colors are valid RGB arrays
+- Ensure message isn't too long for display
+
+### Configuration Errors
+- Validate JSON syntax in config.json
+- Check that all color arrays have 3 values (RGB)
+- Ensure display_duration is a positive number
+
+## License
+
+MIT License - Same as LEDMatrix project
+
+## Contributing
+
+This is a reference plugin included with LEDMatrix. Feel free to use it as a template for your own plugins!
+
+## Support
+
+For plugin system questions, see:
+- [Plugin Architecture Spec](../../PLUGIN_ARCHITECTURE_SPEC.md)
+- [Plugin Phase 1 Summary](../../docs/PLUGIN_PHASE_1_SUMMARY.md)
+- [Main README](../../README.md)
+
diff --git a/plugins/hello-world/config_schema.json b/plugins/hello-world/config_schema.json
new file mode 100644
index 000000000..fb9537f29
--- /dev/null
+++ b/plugins/hello-world/config_schema.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "title": "Hello World Plugin Configuration",
+ "description": "Configuration schema for the Hello World test plugin",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "default": true,
+ "description": "Enable or disable this plugin"
+ },
+ "message": {
+ "type": "string",
+ "default": "Hello, World!",
+ "minLength": 1,
+ "maxLength": 50,
+ "description": "The greeting message to display"
+ },
+ "show_time": {
+ "type": "boolean",
+ "default": true,
+ "description": "Show the current time below the message"
+ },
+ "color": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "default": [255, 255, 255],
+ "description": "RGB color for the message text [R, G, B]"
+ },
+ "time_color": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "default": [0, 255, 255],
+ "description": "RGB color for the time text [R, G, B]"
+ },
+ "display_duration": {
+ "type": "number",
+ "default": 10,
+ "minimum": 1,
+ "maximum": 300,
+ "description": "How long to display in seconds"
+ }
+ },
+ "required": ["enabled"],
+ "additionalProperties": false
+}
+
diff --git a/plugins/hello-world/example_config.json b/plugins/hello-world/example_config.json
new file mode 100644
index 000000000..3e4c16461
--- /dev/null
+++ b/plugins/hello-world/example_config.json
@@ -0,0 +1,12 @@
+{
+ "_comment": "Add this to your config/config.json to enable the Hello World plugin",
+ "hello-world": {
+ "enabled": true,
+ "message": "Hello, World!",
+ "show_time": true,
+ "color": [255, 255, 255],
+ "time_color": [0, 255, 255],
+ "display_duration": 10
+ }
+}
+
diff --git a/plugins/hello-world/manager.py b/plugins/hello-world/manager.py
new file mode 100644
index 000000000..1f5415432
--- /dev/null
+++ b/plugins/hello-world/manager.py
@@ -0,0 +1,187 @@
+"""
+Hello World Plugin
+
+A simple test plugin that displays a customizable greeting message
+on the LED matrix. Used to demonstrate and test the plugin system.
+"""
+
+from src.plugin_system.base_plugin import BasePlugin
+import time
+from datetime import datetime
+
+
+class HelloWorldPlugin(BasePlugin):
+ """
+ Simple Hello World plugin for LEDMatrix.
+
+ Displays a customizable greeting message with the current time.
+ Demonstrates basic plugin functionality.
+ """
+
+ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
+ """Initialize the Hello World plugin."""
+ super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
+
+ # Plugin-specific configuration
+ self.message = config.get('message', 'Hello, World!')
+ self.show_time = config.get('show_time', True)
+ self.color = tuple(config.get('color', [255, 255, 255]))
+ self.time_color = tuple(config.get('time_color', [0, 255, 255]))
+
+ # State
+ self.last_update = None
+ self.current_time_str = ""
+
+ self.logger.info(f"Hello World plugin initialized with message: '{self.message}'")
+
+ def update(self):
+ """
+ Update plugin data.
+
+ For this simple plugin, we just update the current time string.
+ In a real plugin, this would fetch data from APIs, databases, etc.
+ """
+ try:
+ self.last_update = time.time()
+
+ if self.show_time:
+ now = datetime.now()
+ self.current_time_str = now.strftime("%I:%M %p")
+ self.logger.debug(f"Updated time: {self.current_time_str}")
+
+ # Log update occasionally to avoid spam
+ if not hasattr(self, '_last_log_time') or time.time() - self._last_log_time > 60:
+ self.logger.info(f"Plugin updated successfully")
+ self._last_log_time = time.time()
+
+ except Exception as e:
+ self.logger.error(f"Error during update: {e}", exc_info=True)
+
+ def display(self, force_clear=False):
+ """
+ Render the plugin display.
+
+ Displays the configured message and optionally the current time.
+ """
+ try:
+ # Clear display if requested
+ if force_clear:
+ self.display_manager.clear()
+
+ # Get display dimensions
+ width = self.display_manager.width
+ height = self.display_manager.height
+
+ # Calculate positions for centered text
+ if self.show_time:
+ # Display message at top, time at bottom
+ message_y = height // 3
+ time_y = (2 * height) // 3
+
+ # Draw the greeting message
+ self.display_manager.draw_text(
+ self.message,
+ x=width // 2,
+ y=message_y,
+ color=self.color,
+ font_name='6x9.bdf'
+ )
+
+ # Draw the current time
+ if self.current_time_str:
+ self.display_manager.draw_text(
+ self.current_time_str,
+ x=width // 2,
+ y=time_y,
+ color=self.time_color,
+ font_name='6x9.bdf'
+ )
+ else:
+ # Display message centered
+ self.display_manager.draw_text(
+ self.message,
+ x=width // 2,
+ y=height // 2,
+ color=self.color,
+ font_name='6x9.bdf'
+ )
+
+ # Update the physical display
+ self.display_manager.update_display()
+
+ # Log display occasionally to reduce spam
+ if not hasattr(self, '_last_display_log') or time.time() - self._last_display_log > 30:
+ self.logger.debug(f"Display rendered: '{self.message}'")
+ self._last_display_log = time.time()
+
+ except Exception as e:
+ self.logger.error(f"Error during display: {e}", exc_info=True)
+ # Show error message on display
+ try:
+ self.display_manager.clear()
+ self.display_manager.draw_text(
+ "Error!",
+ x=width // 2,
+ y=height // 2,
+ color=(255, 0, 0),
+ font_name='6x9.bdf'
+ )
+ self.display_manager.update_display()
+ except:
+ pass # If we can't even show error, just log it
+
+ def validate_config(self):
+ """
+ Validate plugin configuration.
+
+ Ensures the configuration values are valid.
+ """
+ # Call parent validation
+ if not super().validate_config():
+ return False
+
+ # Validate message
+ if 'message' in self.config:
+ if not isinstance(self.config['message'], str):
+ self.logger.error("'message' must be a string")
+ return False
+ if len(self.config['message']) > 50:
+ self.logger.warning("'message' is very long, may not fit on display")
+
+ # Validate colors
+ for color_key in ['color', 'time_color']:
+ if color_key in self.config:
+ color = self.config[color_key]
+ if not isinstance(color, (list, tuple)) or len(color) != 3:
+ self.logger.error(f"'{color_key}' must be an RGB array [R, G, B]")
+ return False
+ if not all(isinstance(c, int) and 0 <= c <= 255 for c in color):
+ self.logger.error(f"'{color_key}' values must be integers 0-255")
+ return False
+
+ # Validate show_time
+ if 'show_time' in self.config:
+ if not isinstance(self.config['show_time'], bool):
+ self.logger.error("'show_time' must be a boolean")
+ return False
+
+ self.logger.info("Configuration validated successfully")
+ return True
+
+ def get_info(self):
+ """
+ Return plugin information for web UI.
+ """
+ info = super().get_info()
+ info['message'] = self.message
+ info['show_time'] = self.show_time
+ info['last_update'] = self.last_update
+ return info
+
+ def cleanup(self):
+ """
+ Cleanup resources when plugin is unloaded.
+ """
+ self.logger.info("Cleaning up Hello World plugin")
+ super().cleanup()
+
diff --git a/plugins/hello-world/manifest.json b/plugins/hello-world/manifest.json
new file mode 100644
index 000000000..a31acd07b
--- /dev/null
+++ b/plugins/hello-world/manifest.json
@@ -0,0 +1,28 @@
+{
+ "id": "hello-world",
+ "name": "Hello World",
+ "version": "1.0.0",
+ "author": "LEDMatrix Team",
+ "description": "A simple test plugin that displays a customizable message",
+ "homepage": "https://github.com/ChuckBuilds/LEDMatrix",
+ "entry_point": "manager.py",
+ "class_name": "HelloWorldPlugin",
+ "category": "demo",
+ "tags": ["demo", "test", "simple"],
+ "compatible_versions": [">=2.0.0"],
+ "ledmatrix_version": "2.0.0",
+ "requires": {
+ "python": ">=3.9",
+ "display_size": {
+ "min_width": 64,
+ "min_height": 32
+ }
+ },
+ "config_schema": "config_schema.json",
+ "assets": {},
+ "update_interval": 60,
+ "default_duration": 10,
+ "display_modes": ["hello-world"],
+ "api_requirements": []
+}
+
diff --git a/test/test_plugin_system.py b/test/test_plugin_system.py
new file mode 100644
index 000000000..2c98b8219
--- /dev/null
+++ b/test/test_plugin_system.py
@@ -0,0 +1,320 @@
+#!/usr/bin/env python3
+"""
+Test script for Plugin System validation.
+
+This script tests the plugin system functionality without needing
+the actual LED matrix hardware.
+"""
+
+import sys
+import os
+import json
+from pathlib import Path
+
+# Add project root to path
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from src.plugin_system import PluginManager, PluginStoreManager, BasePlugin
+from unittest.mock import Mock
+
+
+def print_header(text):
+ """Print a formatted header."""
+ print("\n" + "=" * 60)
+ print(f" {text}")
+ print("=" * 60)
+
+
+def test_plugin_discovery():
+ """Test plugin discovery functionality."""
+ print_header("Test 1: Plugin Discovery")
+
+ try:
+ # Create mock managers
+ mock_config_manager = Mock()
+ mock_config_manager.load_config.return_value = {}
+ mock_display_manager = Mock()
+ mock_display_manager.width = 128
+ mock_display_manager.height = 64
+ mock_cache_manager = Mock()
+
+ # Initialize plugin manager
+ plugin_manager = PluginManager(
+ plugins_dir="plugins",
+ config_manager=mock_config_manager,
+ display_manager=mock_display_manager,
+ cache_manager=mock_cache_manager
+ )
+
+ # Discover plugins
+ discovered = plugin_manager.discover_plugins()
+
+ print(f"[OK] Discovered {len(discovered)} plugin(s): {', '.join(discovered)}")
+
+ # Show plugin manifests
+ for plugin_id in discovered:
+ manifest = plugin_manager.plugin_manifests.get(plugin_id, {})
+ print(f"\n Plugin: {manifest.get('name', plugin_id)}")
+ print(f" ID: {plugin_id}")
+ print(f" Version: {manifest.get('version', 'unknown')}")
+ print(f" Author: {manifest.get('author', 'unknown')}")
+ print(f" Description: {manifest.get('description', 'N/A')}")
+
+ return True
+ except Exception as e:
+ print(f"[FAIL] Error: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+def test_plugin_loading():
+ """Test plugin loading functionality."""
+ print_header("Test 2: Plugin Loading")
+
+ try:
+ # Create mock managers
+ mock_config_manager = Mock()
+ mock_config_manager.load_config.return_value = {
+ "hello-world": {
+ "enabled": True,
+ "message": "Test Message",
+ "show_time": True,
+ "color": [255, 0, 0],
+ "display_duration": 10
+ }
+ }
+ mock_display_manager = Mock()
+ mock_display_manager.width = 128
+ mock_display_manager.height = 64
+ mock_cache_manager = Mock()
+
+ # Initialize plugin manager
+ plugin_manager = PluginManager(
+ plugins_dir="plugins",
+ config_manager=mock_config_manager,
+ display_manager=mock_display_manager,
+ cache_manager=mock_cache_manager
+ )
+
+ # Discover and load plugins
+ discovered = plugin_manager.discover_plugins()
+
+ if "hello-world" not in discovered:
+ print("[WARN] Hello World plugin not found. Skipping load test.")
+ return True
+
+ # Load the hello-world plugin
+ success = plugin_manager.load_plugin("hello-world")
+
+ if success:
+ print("[OK] Hello World plugin loaded successfully")
+
+ # Get plugin instance
+ plugin = plugin_manager.get_plugin("hello-world")
+ if plugin:
+ print(f" Plugin instance: {type(plugin).__name__}")
+ print(f" Plugin ID: {plugin.plugin_id}")
+ print(f" Enabled: {plugin.enabled}")
+ print(f" Message: {plugin.message}")
+
+ # Test update method
+ print("\n Testing update() method...")
+ plugin.update()
+ print(" [OK] Update completed")
+
+ # Test display method (won't actually display without real hardware)
+ print("\n Testing display() method...")
+ plugin.display(force_clear=True)
+ print(" [OK] Display completed (mock)")
+
+ # Test get_info method
+ info = plugin.get_info()
+ print(f"\n Plugin info: {json.dumps(info, indent=2)}")
+
+ return True
+ else:
+ print("[FAIL] Failed to load Hello World plugin")
+ return False
+
+ except Exception as e:
+ print(f"[FAIL] Error: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+def test_plugin_manifest_validation():
+ """Test manifest validation."""
+ print_header("Test 3: Manifest Validation")
+
+ try:
+ manifest_path = Path("plugins/hello-world/manifest.json")
+
+ if not manifest_path.exists():
+ print("[WARN] Hello World manifest not found")
+ return True
+
+ with open(manifest_path, 'r') as f:
+ manifest = json.load(f)
+
+ # Check required fields
+ required_fields = ['id', 'name', 'version', 'entry_point', 'class_name']
+ missing = [field for field in required_fields if field not in manifest]
+
+ if missing:
+ print(f"[FAIL] Missing required fields: {', '.join(missing)}")
+ return False
+
+ print("[OK] All required fields present")
+
+ # Validate field types
+ if not isinstance(manifest.get('id'), str):
+ print("[FAIL] 'id' must be a string")
+ return False
+
+ if not isinstance(manifest.get('version'), str):
+ print("[FAIL] 'version' must be a string")
+ return False
+
+ print("[OK] Field types valid")
+
+ # Check entry point exists
+ entry_point = Path("plugins/hello-world") / manifest['entry_point']
+ if not entry_point.exists():
+ print(f"[FAIL] Entry point not found: {entry_point}")
+ return False
+
+ print(f"[OK] Entry point exists: {manifest['entry_point']}")
+
+ return True
+
+ except Exception as e:
+ print(f"[FAIL] Error: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+def test_base_plugin_interface():
+ """Test BasePlugin interface."""
+ print_header("Test 4: BasePlugin Interface")
+
+ try:
+ # Import hello-world plugin
+ sys.path.insert(0, "plugins/hello-world")
+ from manager import HelloWorldPlugin
+
+ # Create mock managers
+ mock_config = {
+ "enabled": True,
+ "message": "Test",
+ "display_duration": 10
+ }
+ mock_display_manager = Mock()
+ mock_display_manager.width = 128
+ mock_display_manager.height = 64
+ mock_cache_manager = Mock()
+ mock_plugin_manager = Mock()
+
+ # Instantiate plugin
+ plugin = HelloWorldPlugin(
+ plugin_id="hello-world",
+ config=mock_config,
+ display_manager=mock_display_manager,
+ cache_manager=mock_cache_manager,
+ plugin_manager=mock_plugin_manager
+ )
+
+ # Check that it's a BasePlugin
+ if not isinstance(plugin, BasePlugin):
+ print("[FAIL] Plugin does not inherit from BasePlugin")
+ return False
+
+ print("[OK] Plugin inherits from BasePlugin")
+
+ # Check required methods exist
+ required_methods = ['update', 'display', 'validate_config', 'cleanup', 'get_info']
+ missing_methods = [m for m in required_methods if not hasattr(plugin, m)]
+
+ if missing_methods:
+ print(f"[FAIL] Missing methods: {', '.join(missing_methods)}")
+ return False
+
+ print("[OK] All required methods present")
+
+ # Test validate_config
+ if not plugin.validate_config():
+ print("[FAIL] Configuration validation failed")
+ return False
+
+ print("[OK] Configuration validation passed")
+
+ return True
+
+ except Exception as e:
+ print(f"[FAIL] Error: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+def test_store_manager():
+ """Test PluginStoreManager basic functionality."""
+ print_header("Test 5: Plugin Store Manager")
+
+ try:
+ store_manager = PluginStoreManager(plugins_dir="plugins")
+
+ print("[OK] Store manager initialized")
+
+ # Note: We don't actually fetch from network in tests
+ # Just verify the manager was created successfully
+ print(" Store manager ready for plugin installation")
+
+ return True
+
+ except Exception as e:
+ print(f"[FAIL] Error: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+def main():
+ """Run all tests."""
+ print("\n" + "=" * 60)
+ print(" LEDMatrix Plugin System Test Suite")
+ print("=" * 60)
+
+ results = {
+ "Plugin Discovery": test_plugin_discovery(),
+ "Plugin Loading": test_plugin_loading(),
+ "Manifest Validation": test_plugin_manifest_validation(),
+ "BasePlugin Interface": test_base_plugin_interface(),
+ "Store Manager": test_store_manager()
+ }
+
+ # Print summary
+ print_header("Test Results Summary")
+
+ passed = sum(1 for result in results.values() if result)
+ total = len(results)
+
+ for test_name, result in results.items():
+ status = "[PASS]" if result else "[FAIL]"
+ print(f" {status} - {test_name}")
+
+ print(f"\n Total: {passed}/{total} tests passed")
+
+ if passed == total:
+ print("\n All tests passed!")
+ return 0
+ else:
+ print(f"\n WARNING: {total - passed} test(s) failed")
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
+
From 0d96a9fe97086af439e776c2f4f5ae76db3c225e Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 13:47:54 -0400
Subject: [PATCH 006/736] fix python import
---
ledmatrix.service | 2 +-
run.py | 6 ++++++
src/__init__.py | 8 ++++++++
3 files changed, 15 insertions(+), 1 deletion(-)
create mode 100644 src/__init__.py
diff --git a/ledmatrix.service b/ledmatrix.service
index 2f7a9fabd..c471a1691 100644
--- a/ledmatrix.service
+++ b/ledmatrix.service
@@ -6,7 +6,7 @@ After=network.target
Type=simple
User=root
WorkingDirectory=/home/ledpi/LEDMatrix
-ExecStart=/usr/bin/python3 /home/ledpi/LEDMatrix/display_controller.py
+ExecStart=/usr/bin/python3 /home/ledpi/LEDMatrix/run.py
Restart=on-failure
RestartSec=10
StandardOutput=syslog
diff --git a/run.py b/run.py
index 6bb2dd9be..b8d475476 100755
--- a/run.py
+++ b/run.py
@@ -1,6 +1,12 @@
#!/usr/bin/env python3
import logging
import sys
+import os
+
+# Add project directory to Python path
+project_dir = os.path.dirname(os.path.abspath(__file__))
+if project_dir not in sys.path:
+ sys.path.insert(0, project_dir)
# Configure logging before importing any other modules
logging.basicConfig(
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 000000000..1bcb2cb4c
--- /dev/null
+++ b/src/__init__.py
@@ -0,0 +1,8 @@
+"""
+LEDMatrix Display System
+
+Core source package for the LED Matrix Display project.
+"""
+
+__version__ = "1.0.0"
+
From 1db9fd80d9d70b3f33baa66c2dcff4c4bbe6b639 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 13:55:00 -0400
Subject: [PATCH 007/736] debug script
---
test_plugin_import.py | 73 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 73 insertions(+)
create mode 100644 test_plugin_import.py
diff --git a/test_plugin_import.py b/test_plugin_import.py
new file mode 100644
index 000000000..f520f2487
--- /dev/null
+++ b/test_plugin_import.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""
+Diagnostic script to test plugin system import
+Run this on the Raspberry Pi to diagnose import issues
+"""
+
+import sys
+import os
+from pathlib import Path
+
+print("=== Plugin System Import Diagnostic ===\n")
+
+# Get project directory
+project_dir = os.path.dirname(os.path.abspath(__file__))
+print(f"Project directory: {project_dir}")
+
+# Add to path
+if project_dir not in sys.path:
+ sys.path.insert(0, project_dir)
+ print(f"Added {project_dir} to sys.path")
+
+print(f"\nPython path:")
+for i, path in enumerate(sys.path[:5]):
+ print(f" {i}: {path}")
+
+# Check if src/__init__.py exists
+src_init = Path(project_dir) / "src" / "__init__.py"
+print(f"\nsrc/__init__.py exists: {src_init.exists()}")
+if src_init.exists():
+ print(f" Path: {src_init}")
+ print(f" Size: {src_init.stat().st_size} bytes")
+
+# Check if src/plugin_system/__init__.py exists
+plugin_init = Path(project_dir) / "src" / "plugin_system" / "__init__.py"
+print(f"\nsrc/plugin_system/__init__.py exists: {plugin_init.exists()}")
+if plugin_init.exists():
+ print(f" Path: {plugin_init}")
+ print(f" Size: {plugin_init.stat().st_size} bytes")
+
+# Try importing src
+print("\n=== Testing imports ===")
+try:
+ import src
+ print("✓ Successfully imported 'src'")
+ print(f" src.__file__ = {src.__file__}")
+except Exception as e:
+ print(f"✗ Failed to import 'src': {e}")
+ sys.exit(1)
+
+# Try importing src.plugin_system
+try:
+ from src import plugin_system
+ print("✓ Successfully imported 'src.plugin_system'")
+ print(f" plugin_system.__file__ = {plugin_system.__file__}")
+except Exception as e:
+ print(f"✗ Failed to import 'src.plugin_system': {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+# Try importing PluginManager
+try:
+ from src.plugin_system import PluginManager
+ print("✓ Successfully imported 'PluginManager'")
+ print(f" PluginManager class: {PluginManager}")
+except Exception as e:
+ print(f"✗ Failed to import 'PluginManager': {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+print("\n=== All imports successful! ===")
+
From d2ef7213c1cbdc6f808aba32d7d42d17ce9c22ac Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 13:58:05 -0400
Subject: [PATCH 008/736] debug: Add diagnostic output to run.py for systemd
troubleshooting
---
run.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/run.py b/run.py
index b8d475476..dd1293454 100755
--- a/run.py
+++ b/run.py
@@ -8,6 +8,12 @@
if project_dir not in sys.path:
sys.path.insert(0, project_dir)
+# Debug: Print Python path (will show in systemd logs)
+print(f"DEBUG: Project directory: {project_dir}", flush=True)
+print(f"DEBUG: Python path[0]: {sys.path[0]}", flush=True)
+print(f"DEBUG: src/__init__.py exists: {os.path.exists(os.path.join(project_dir, 'src', '__init__.py'))}", flush=True)
+print(f"DEBUG: src/plugin_system/__init__.py exists: {os.path.exists(os.path.join(project_dir, 'src', 'plugin_system', '__init__.py'))}", flush=True)
+
# Configure logging before importing any other modules
logging.basicConfig(
level=logging.DEBUG,
From 6e1434c893fbd845003fa009f83d316b39c02367 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 14:01:35 -0400
Subject: [PATCH 009/736] debug: Add detailed traceback for plugin import
failure
---
src/display_controller.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/display_controller.py b/src/display_controller.py
index 064606786..11c076ef6 100644
--- a/src/display_controller.py
+++ b/src/display_controller.py
@@ -386,10 +386,13 @@ def __init__(self):
self._update_live_modes_in_rotation()
# Initialize Plugin System (Phase 1: Foundation)
+ import traceback
plugin_time = time.time()
self.plugin_manager = None
try:
+ logger.info("Attempting to import plugin system...")
from src.plugin_system import PluginManager
+ logger.info("Plugin system imported successfully")
self.plugin_manager = PluginManager(
plugins_dir="plugins",
config_manager=self.config_manager,
@@ -421,6 +424,7 @@ def __init__(self):
logger.info(f"Plugin system initialized in {time.time() - plugin_time:.3f} seconds")
except ImportError as e:
logger.warning(f"Plugin system not available: {e}")
+ logger.warning(f"Full traceback:\n{traceback.format_exc()}")
self.plugin_manager = None
except Exception as e:
logger.error(f"Error initializing plugin system: {e}", exc_info=True)
From f45cfebaad02127449b6e00b23009447cff8fa47 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 14:13:21 -0400
Subject: [PATCH 010/736] verification script
---
run.py | 22 +++++-
src/display_controller.py | 4 +-
test_import_from_controller.py | 47 +++++++++++++
verify_plugin_system.py | 123 +++++++++++++++++++++++++++++++++
4 files changed, 193 insertions(+), 3 deletions(-)
create mode 100644 test_import_from_controller.py
create mode 100644 verify_plugin_system.py
diff --git a/run.py b/run.py
index dd1293454..452e82c46 100755
--- a/run.py
+++ b/run.py
@@ -8,11 +8,31 @@
if project_dir not in sys.path:
sys.path.insert(0, project_dir)
-# Debug: Print Python path (will show in systemd logs)
+# Debug: Print Python path and file checks (will show in systemd logs)
print(f"DEBUG: Project directory: {project_dir}", flush=True)
print(f"DEBUG: Python path[0]: {sys.path[0]}", flush=True)
+print(f"DEBUG: Current working directory: {os.getcwd()}", flush=True)
print(f"DEBUG: src/__init__.py exists: {os.path.exists(os.path.join(project_dir, 'src', '__init__.py'))}", flush=True)
print(f"DEBUG: src/plugin_system/__init__.py exists: {os.path.exists(os.path.join(project_dir, 'src', 'plugin_system', '__init__.py'))}", flush=True)
+print(f"DEBUG: src/plugin_system directory exists: {os.path.exists(os.path.join(project_dir, 'src', 'plugin_system'))}", flush=True)
+
+# Additional debugging for plugin system
+try:
+ import sys
+ plugin_system_path = os.path.join(project_dir, 'src', 'plugin_system')
+ if plugin_system_path not in sys.path:
+ sys.path.insert(0, plugin_system_path)
+ print(f"DEBUG: Added plugin_system path to sys.path: {plugin_system_path}", flush=True)
+
+ # Try to import the plugin system directly to get better error info
+ print("DEBUG: Attempting to import src.plugin_system...", flush=True)
+ from src.plugin_system import PluginManager
+ print("DEBUG: Plugin system import successful", flush=True)
+except ImportError as e:
+ print(f"DEBUG: Plugin system import failed: {e}", flush=True)
+ print(f"DEBUG: Import error details: {type(e).__name__}", flush=True)
+except Exception as e:
+ print(f"DEBUG: Unexpected error during plugin system import: {e}", flush=True)
# Configure logging before importing any other modules
logging.basicConfig(
diff --git a/src/display_controller.py b/src/display_controller.py
index 11c076ef6..af193e104 100644
--- a/src/display_controller.py
+++ b/src/display_controller.py
@@ -399,11 +399,11 @@ def __init__(self):
display_manager=self.display_manager,
cache_manager=self.cache_manager
)
-
+
# Discover plugins
discovered_plugins = self.plugin_manager.discover_plugins()
logger.info(f"Discovered {len(discovered_plugins)} plugin(s)")
-
+
# Load enabled plugins
for plugin_id in discovered_plugins:
plugin_config = self.config.get(plugin_id, {})
diff --git a/test_import_from_controller.py b/test_import_from_controller.py
new file mode 100644
index 000000000..501fb24f6
--- /dev/null
+++ b/test_import_from_controller.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""
+Test if we can import plugin_system from display_controller context
+"""
+import sys
+import os
+
+# Add project directory to Python path
+project_dir = os.path.dirname(os.path.abspath(__file__))
+if project_dir not in sys.path:
+ sys.path.insert(0, project_dir)
+
+print(f"Project dir: {project_dir}")
+print(f"sys.path[0]: {sys.path[0]}")
+
+# Test 1: Import src
+print("\n=== Test 1: Import src ===")
+import src
+print(f"✓ src imported: {src}")
+
+# Test 2: Import src.plugin_system
+print("\n=== Test 2: Import src.plugin_system ===")
+import src.plugin_system
+print(f"✓ src.plugin_system imported: {src.plugin_system}")
+
+# Test 3: Try the same import style as display_controller
+print("\n=== Test 3: Import like display_controller ===")
+try:
+ # This is EXACTLY how display_controller.py does it
+ from src.plugin_system import PluginManager
+ print(f"✓ PluginManager imported: {PluginManager}")
+except ImportError as e:
+ print(f"✗ Failed: {e}")
+ import traceback
+ traceback.print_exc()
+
+# Test 4: Now import display_controller and see if IT can import plugin_system
+print("\n=== Test 4: Check display_controller's ability to import ===")
+print("sys.path before importing display_controller:")
+for i, p in enumerate(sys.path[:3]):
+ print(f" {i}: {p}")
+
+from src import display_controller
+print(f"✓ display_controller imported")
+
+print("\nAll tests completed!")
+
diff --git a/verify_plugin_system.py b/verify_plugin_system.py
new file mode 100644
index 000000000..cf7d10a60
--- /dev/null
+++ b/verify_plugin_system.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+"""
+Plugin System Verification Script
+
+This script verifies that the plugin system is properly installed and functional.
+Run this script on the Raspberry Pi to diagnose plugin system issues.
+"""
+
+import os
+import sys
+import importlib.util
+
+def check_file_exists(filepath):
+ """Check if a file exists and return details."""
+ exists = os.path.exists(filepath)
+ if exists:
+ size = os.path.getsize(filepath)
+ return f"✓ EXISTS ({size} bytes)"
+ else:
+ return "✗ MISSING"
+
+def check_directory_exists(dirpath):
+ """Check if a directory exists and return details."""
+ exists = os.path.exists(dirpath)
+ if exists:
+ return "✓ EXISTS"
+ else:
+ return "✗ MISSING"
+
+def test_import(module_name, filepath):
+ """Test importing a module and return result."""
+ try:
+ spec = importlib.util.spec_from_file_location(module_name, filepath)
+ if spec is None:
+ return f"✗ Could not create module spec for {filepath}"
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return f"✓ Successfully imported {module_name}"
+ except Exception as e:
+ return f"✗ Import failed: {e}"
+
+def main():
+ """Main verification function."""
+ print("==========================================")
+ print("Plugin System Verification")
+ print("==========================================")
+ print()
+
+ # Get project directory
+ project_dir = os.path.dirname(os.path.abspath(__file__))
+ print(f"Project directory: {project_dir}")
+ print(f"Current working directory: {os.getcwd()}")
+ print(f"Python path: {sys.path[:3]}...")
+ print()
+
+ # Check key files
+ print("=== File Existence Checks ===")
+ files_to_check = [
+ ("src/__init__.py", os.path.join(project_dir, "src", "__init__.py")),
+ ("src/plugin_system/__init__.py", os.path.join(project_dir, "src", "plugin_system", "__init__.py")),
+ ("src/plugin_system/base_plugin.py", os.path.join(project_dir, "src", "plugin_system", "base_plugin.py")),
+ ("src/plugin_system/plugin_manager.py", os.path.join(project_dir, "src", "plugin_system", "plugin_manager.py")),
+ ("plugins/hello-world/manifest.json", os.path.join(project_dir, "plugins", "hello-world", "manifest.json")),
+ ("plugins/hello-world/manager.py", os.path.join(project_dir, "plugins", "hello-world", "manager.py")),
+ ]
+
+ for name, path in files_to_check:
+ print(f"{name"30"} {check_file_exists(path)}")
+ print()
+
+ # Check directories
+ print("=== Directory Checks ===")
+ dirs_to_check = [
+ ("src/", os.path.join(project_dir, "src")),
+ ("src/plugin_system/", os.path.join(project_dir, "src", "plugin_system")),
+ ("plugins/", os.path.join(project_dir, "plugins")),
+ ("plugins/hello-world/", os.path.join(project_dir, "plugins", "hello-world")),
+ ]
+
+ for name, path in dirs_to_check:
+ print(f"{name"25"} {check_directory_exists(path)}")
+ print()
+
+ # Test imports
+ print("=== Import Tests ===")
+ import_tests = [
+ ("src.plugin_system", os.path.join(project_dir, "src", "plugin_system", "__init__.py")),
+ ("src.plugin_system.base_plugin", os.path.join(project_dir, "src", "plugin_system", "base_plugin.py")),
+ ("src.plugin_system.plugin_manager", os.path.join(project_dir, "src", "plugin_system", "plugin_manager.py")),
+ ]
+
+ for module_name, filepath in import_tests:
+ result = test_import(module_name, filepath)
+ print(f"{module_name"35"} {result}")
+ print()
+
+ # Test plugin loading
+ print("=== Plugin Loading Test ===")
+ try:
+ sys.path.insert(0, project_dir)
+ from src.plugin_system import PluginManager
+ print("✓ PluginManager import successful")
+
+ # Try to create a PluginManager instance
+ pm = PluginManager(plugins_dir=os.path.join(project_dir, "plugins"))
+ discovered = pm.discover_plugins()
+ print(f"✓ Plugin discovery successful: {len(discovered)} plugins found")
+
+ for plugin_id in discovered:
+ print(f" - {plugin_id}")
+
+ except Exception as e:
+ print(f"✗ Plugin loading failed: {e}")
+ import traceback
+ print(f" Traceback: {traceback.format_exc()}")
+ print()
+
+ print("=== Verification Complete ===")
+ print("If any checks failed, ensure all files are properly deployed to the Pi.")
+ print("You may need to run: git pull && git submodule update --init --recursive")
+
+if __name__ == "__main__":
+ main()
From 4c5eac2fed7223b82d8e76e156ac00b6c8d9c4e4 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 14:14:25 -0400
Subject: [PATCH 011/736] verification script
---
verify_plugin_system.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/verify_plugin_system.py b/verify_plugin_system.py
index cf7d10a60..9950f386a 100644
--- a/verify_plugin_system.py
+++ b/verify_plugin_system.py
@@ -65,7 +65,7 @@ def main():
]
for name, path in files_to_check:
- print(f"{name"30"} {check_file_exists(path)}")
+ print(f"{name:30} {check_file_exists(path)}")
print()
# Check directories
@@ -78,7 +78,7 @@ def main():
]
for name, path in dirs_to_check:
- print(f"{name"25"} {check_directory_exists(path)}")
+ print(f"{name:25} {check_directory_exists(path)}")
print()
# Test imports
@@ -91,7 +91,7 @@ def main():
for module_name, filepath in import_tests:
result = test_import(module_name, filepath)
- print(f"{module_name"35"} {result}")
+ print(f"{module_name:35} {result}")
print()
# Test plugin loading
From 7fd4b8d7a0cffd7cec3e7d09efc86062dc0ec960 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 14:32:22 -0400
Subject: [PATCH 012/736] fix: Use absolute path for plugins directory in
PluginManager
- Use os.path.join(os.getcwd(), 'plugins') instead of relative 'plugins' path
- Add os import to display_controller.py
- This should fix the permission error when loading plugins in systemd service
---
src/display_controller.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/display_controller.py b/src/display_controller.py
index af193e104..ad872d517 100644
--- a/src/display_controller.py
+++ b/src/display_controller.py
@@ -1,6 +1,7 @@
import time
import logging
import sys
+import os
from typing import Dict, Any, List
from datetime import datetime, time as time_obj
@@ -394,7 +395,7 @@ def __init__(self):
from src.plugin_system import PluginManager
logger.info("Plugin system imported successfully")
self.plugin_manager = PluginManager(
- plugins_dir="plugins",
+ plugins_dir=os.path.join(os.getcwd(), "plugins"),
config_manager=self.config_manager,
display_manager=self.display_manager,
cache_manager=self.cache_manager
From 350b83d7a15fb3c6717098980a81f24060eb52b1 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 14:37:19 -0400
Subject: [PATCH 013/736] fix(hello-world): Resolve font_name parameter error
in draw_text calls
- Load 6x9.bdf font as freetype.Face object in plugin initialization
- Replace font_name parameter with font parameter in all draw_text calls
- Add proper error handling for font loading failures
- Plugin now uses BDF font instead of non-existent font_name parameter
---
plugins/hello-world/manager.py | 36 ++++++++++++++++++++++++++--------
1 file changed, 28 insertions(+), 8 deletions(-)
diff --git a/plugins/hello-world/manager.py b/plugins/hello-world/manager.py
index 1f5415432..010616088 100644
--- a/plugins/hello-world/manager.py
+++ b/plugins/hello-world/manager.py
@@ -8,6 +8,8 @@
from src.plugin_system.base_plugin import BasePlugin
import time
from datetime import datetime
+import freetype
+import os
class HelloWorldPlugin(BasePlugin):
@@ -21,19 +23,37 @@ class HelloWorldPlugin(BasePlugin):
def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
"""Initialize the Hello World plugin."""
super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
-
+
# Plugin-specific configuration
self.message = config.get('message', 'Hello, World!')
self.show_time = config.get('show_time', True)
self.color = tuple(config.get('color', [255, 255, 255]))
self.time_color = tuple(config.get('time_color', [0, 255, 255]))
-
+
+ # Load the 6x9 BDF font
+ self._load_font()
+
# State
self.last_update = None
self.current_time_str = ""
-
+
self.logger.info(f"Hello World plugin initialized with message: '{self.message}'")
-
+
+ def _load_font(self):
+ """Load the 6x9 BDF font for text rendering."""
+ try:
+ font_path = "assets/fonts/6x9.bdf"
+ if not os.path.exists(font_path):
+ self.logger.error(f"Font file not found: {font_path}")
+ self.bdf_font = None
+ return
+
+ self.bdf_font = freetype.Face(font_path)
+ self.logger.info(f"6x9 BDF font loaded successfully from {font_path}")
+ except Exception as e:
+ self.logger.error(f"Failed to load 6x9 BDF font: {e}")
+ self.bdf_font = None
+
def update(self):
"""
Update plugin data.
@@ -84,7 +104,7 @@ def display(self, force_clear=False):
x=width // 2,
y=message_y,
color=self.color,
- font_name='6x9.bdf'
+ font=self.bdf_font
)
# Draw the current time
@@ -94,7 +114,7 @@ def display(self, force_clear=False):
x=width // 2,
y=time_y,
color=self.time_color,
- font_name='6x9.bdf'
+ font=self.bdf_font
)
else:
# Display message centered
@@ -103,7 +123,7 @@ def display(self, force_clear=False):
x=width // 2,
y=height // 2,
color=self.color,
- font_name='6x9.bdf'
+ font=self.bdf_font
)
# Update the physical display
@@ -124,7 +144,7 @@ def display(self, force_clear=False):
x=width // 2,
y=height // 2,
color=(255, 0, 0),
- font_name='6x9.bdf'
+ font=self.bdf_font
)
self.display_manager.update_display()
except:
From 00b56c402c9e58b82914827734f9cf5e19d83bc7 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 14:39:06 -0400
Subject: [PATCH 014/736] fix font error
---
fix_plugin_permissions.sh | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 fix_plugin_permissions.sh
diff --git a/fix_plugin_permissions.sh b/fix_plugin_permissions.sh
new file mode 100644
index 000000000..b2a2c6edb
--- /dev/null
+++ b/fix_plugin_permissions.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+# Fix permissions for the plugins directory
+
+echo "Fixing permissions for plugins directory..."
+
+# Get the directory where the script is located
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# Set ownership to ledpi user and group
+echo "Setting ownership to ledpi:ledpi..."
+sudo chown -R ledpi:ledpi "$SCRIPT_DIR/plugins"
+
+# Set directory permissions (755: rwxr-xr-x)
+echo "Setting directory permissions to 755..."
+find "$SCRIPT_DIR/plugins" -type d -exec sudo chmod 755 {} \;
+
+# Set file permissions (644: rw-r--r--)
+echo "Setting file permissions to 644..."
+find "$SCRIPT_DIR/plugins" -type f -exec sudo chmod 644 {} \;
+
+echo "Plugin permissions fixed successfully!"
+echo ""
+echo "Directory structure:"
+ls -la "$SCRIPT_DIR/plugins"
+echo ""
+echo "Hello-world plugin:"
+ls -la "$SCRIPT_DIR/plugins/hello-world"
+
From 1d4062d2d9b560595e97e1509c7f3c1fd03e3467 Mon Sep 17 00:00:00 2001
From: Chuck <33324927+ChuckBuilds@users.noreply.github.com>
Date: Thu, 9 Oct 2025 15:08:17 -0400
Subject: [PATCH 015/736] feat(plugins): Add comprehensive plugin management
system
- Add clock-simple plugin as complete working example
- Implement plugin management web interface with tabs for installed plugins and store
- Add CSS styling for plugin cards and management interface
- Implement JavaScript functions for plugin operations (install, uninstall, toggle, configure)
- Update plugin manager with automatic dependency installation
- Add graceful handling for missing optional dependencies
- Integrate plugin configuration with existing config.json system
Features:
- Plugin discovery and loading from plugins/ directory
- Web UI for managing installed plugins (enable/disable/configure)
- Plugin store interface for browsing and installing community plugins
- Configuration inheritance from config.json with validation
- Real-time plugin status updates and notifications
- Responsive design that matches existing web interface
---
plugins/clock-simple/README.md | 153 ++++++++
plugins/clock-simple/config_schema.json | 96 +++++
plugins/clock-simple/manager.py | 269 ++++++++++++++
plugins/clock-simple/manifest.json | 27 ++
plugins/clock-simple/requirements.txt | 1 +
plugins/hello-world/manager.py | 11 +-
plugins/hello-world/requirements.txt | 1 +
src/plugin_system/__init__.py | 12 +-
src/plugin_system/plugin_manager.py | 47 ++-
templates/index_v2.html | 463 ++++++++++++++++++++++++
web_interface_v2.py | 15 +-
11 files changed, 1083 insertions(+), 12 deletions(-)
create mode 100644 plugins/clock-simple/README.md
create mode 100644 plugins/clock-simple/config_schema.json
create mode 100644 plugins/clock-simple/manager.py
create mode 100644 plugins/clock-simple/manifest.json
create mode 100644 plugins/clock-simple/requirements.txt
create mode 100644 plugins/hello-world/requirements.txt
diff --git a/plugins/clock-simple/README.md b/plugins/clock-simple/README.md
new file mode 100644
index 000000000..b37ddfe92
--- /dev/null
+++ b/plugins/clock-simple/README.md
@@ -0,0 +1,153 @@
+# Simple Clock Plugin
+
+A simple, customizable clock display plugin for LEDMatrix that shows the current time and date.
+
+## Features
+
+- **Time Display**: Shows current time in 12-hour or 24-hour format
+- **Date Display**: Optional date display with multiple format options
+- **Timezone Support**: Configurable timezone for accurate time display
+- **Color Customization**: Customizable colors for time, date, and AM/PM indicator
+- **Position Control**: Configurable display position
+
+## Installation
+
+### From Plugin Store (Recommended)
+
+1. Open the LEDMatrix web interface
+2. Navigate to the Plugin Store tab
+3. Search for "Simple Clock" or browse the "time" category
+4. Click "Install"
+
+### Manual Installation
+
+1. Copy this plugin directory to your `plugins/` folder
+2. Restart LEDMatrix
+3. Enable the plugin in the web interface
+
+## Configuration
+
+Add the following to your `config/config.json`:
+
+```json
+{
+ "clock-simple": {
+ "enabled": true,
+ "timezone": "America/New_York",
+ "time_format": "12h",
+ "show_seconds": false,
+ "show_date": true,
+ "date_format": "MM/DD/YYYY",
+ "time_color": [255, 255, 255],
+ "date_color": [255, 128, 64],
+ "ampm_color": [255, 255, 128],
+ "display_duration": 15,
+ "position": {
+ "x": 0,
+ "y": 0
+ }
+ }
+}
+```
+
+### Configuration Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `enabled` | boolean | `true` | Enable or disable the plugin |
+| `timezone` | string | `"UTC"` | Timezone for display (e.g., `"America/New_York"`) |
+| `time_format` | string | `"12h"` | Time format: `"12h"` or `"24h"` |
+| `show_seconds` | boolean | `false` | Show seconds in time display |
+| `show_date` | boolean | `true` | Show date below the time |
+| `date_format` | string | `"MM/DD/YYYY"` | Date format: `"MM/DD/YYYY"`, `"DD/MM/YYYY"`, or `"YYYY-MM-DD"` |
+| `time_color` | array | `[255, 255, 255]` | RGB color for time display |
+| `date_color` | array | `[255, 128, 64]` | RGB color for date display |
+| `ampm_color` | array | `[255, 255, 128]` | RGB color for AM/PM indicator |
+| `display_duration` | number | `15` | Display duration in seconds |
+| `position.x` | integer | `0` | X position for display |
+| `position.y` | integer | `0` | Y position for display |
+
+### Timezone Examples
+
+- `"America/New_York"` - Eastern Time
+- `"America/Chicago"` - Central Time
+- `"America/Denver"` - Mountain Time
+- `"America/Los_Angeles"` - Pacific Time
+- `"Europe/London"` - GMT/BST
+- `"Asia/Tokyo"` - Japan Standard Time
+- `"Australia/Sydney"` - Australian Eastern Time
+
+## Usage
+
+Once installed and configured:
+
+1. The plugin will automatically update every second (based on `update_interval` in manifest)
+2. The display will show during rotation according to your configured `display_duration`
+3. The time updates in real-time based on your configured timezone
+
+## Troubleshooting
+
+### Common Issues
+
+**Time shows wrong timezone:**
+- Verify the `timezone` setting in your configuration
+- Check that the timezone string is valid (see timezone examples above)
+
+**Colors not displaying correctly:**
+- Ensure RGB values are between 0-255
+- Check that your display supports the chosen colors
+
+**Plugin not appearing in rotation:**
+- Verify `enabled` is set to `true`
+- Check that the plugin loaded successfully in the web interface
+- Ensure `display_duration` is greater than 0
+
+### Debug Logging
+
+Enable debug logging to troubleshoot issues:
+
+```json
+{
+ "logging": {
+ "level": "DEBUG",
+ "file": "/path/to/ledmatrix.log"
+ }
+}
+```
+
+## Development
+
+### Plugin Structure
+
+```
+plugins/clock-simple/
+├── manifest.json # Plugin metadata and requirements
+├── manager.py # Main plugin class
+├── config_schema.json # Configuration validation schema
+└── README.md # This file
+```
+
+### Testing
+
+Test the plugin by running:
+
+```bash
+cd /path/to/LEDMatrix
+python3 -c "
+from src.plugin_system.plugin_manager import PluginManager
+pm = PluginManager()
+pm.discover_plugins()
+pm.load_plugin('clock-simple')
+plugin = pm.get_plugin('clock-simple')
+plugin.update()
+plugin.display()
+"
+```
+
+## License
+
+MIT License - feel free to modify and distribute.
+
+## Contributing
+
+Found a bug or want to add features? Please create an issue or submit a pull request on the [LEDMatrix GitHub repository](https://github.com/ChuckBuilds/LEDMatrix).
diff --git a/plugins/clock-simple/config_schema.json b/plugins/clock-simple/config_schema.json
new file mode 100644
index 000000000..09b85ec4e
--- /dev/null
+++ b/plugins/clock-simple/config_schema.json
@@ -0,0 +1,96 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "default": true,
+ "description": "Enable or disable the clock plugin"
+ },
+ "timezone": {
+ "type": "string",
+ "default": "UTC",
+ "description": "Timezone for the clock display (e.g., 'America/New_York', 'Europe/London')"
+ },
+ "time_format": {
+ "type": "string",
+ "enum": ["12h", "24h"],
+ "default": "12h",
+ "description": "Time format to display"
+ },
+ "show_seconds": {
+ "type": "boolean",
+ "default": false,
+ "description": "Show seconds in the time display"
+ },
+ "show_date": {
+ "type": "boolean",
+ "default": true,
+ "description": "Show date below the time"
+ },
+ "date_format": {
+ "type": "string",
+ "enum": ["MM/DD/YYYY", "DD/MM/YYYY", "YYYY-MM-DD"],
+ "default": "MM/DD/YYYY",
+ "description": "Date format to display"
+ },
+ "time_color": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "default": [255, 255, 255],
+ "description": "RGB color for the time display"
+ },
+ "date_color": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "default": [255, 128, 64],
+ "description": "RGB color for the date display"
+ },
+ "ampm_color": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "default": [255, 255, 128],
+ "description": "RGB color for AM/PM indicator"
+ },
+ "display_duration": {
+ "type": "number",
+ "default": 15,
+ "minimum": 1,
+ "description": "How long to display the clock in seconds"
+ },
+ "position": {
+ "type": "object",
+ "properties": {
+ "x": {
+ "type": "integer",
+ "default": 0,
+ "description": "X position for clock display"
+ },
+ "y": {
+ "type": "integer",
+ "default": 0,
+ "description": "Y position for clock display"
+ }
+ }
+ }
+ },
+ "required": ["enabled"]
+}
diff --git a/plugins/clock-simple/manager.py b/plugins/clock-simple/manager.py
new file mode 100644
index 000000000..a7f3711ed
--- /dev/null
+++ b/plugins/clock-simple/manager.py
@@ -0,0 +1,269 @@
+"""
+Simple Clock Plugin for LEDMatrix
+
+Displays current time and date with customizable formatting and colors.
+Migrated from the original clock.py manager as a plugin example.
+
+API Version: 1.0.0
+"""
+
+import time
+import logging
+from datetime import datetime
+from typing import Dict, Any, Tuple
+from src.plugin_system.base_plugin import BasePlugin
+
+try:
+ import pytz
+except ImportError:
+ pytz = None
+
+
+class SimpleClock(BasePlugin):
+ """
+ Simple clock plugin that displays current time and date.
+
+ Configuration options:
+ timezone (str): Timezone for display (default: UTC)
+ time_format (str): 12h or 24h format (default: 12h)
+ show_seconds (bool): Show seconds in time (default: False)
+ show_date (bool): Show date below time (default: True)
+ date_format (str): Date format (default: MM/DD/YYYY)
+ time_color (list): RGB color for time [R, G, B] (default: [255, 255, 255])
+ date_color (list): RGB color for date [R, G, B] (default: [255, 128, 64])
+ ampm_color (list): RGB color for AM/PM [R, G, B] (default: [255, 255, 128])
+ position (dict): X,Y position for display (default: 0,0)
+ """
+
+ def __init__(self, plugin_id: str, config: Dict[str, Any],
+ display_manager, cache_manager, plugin_manager):
+ """Initialize the clock plugin."""
+ super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
+
+ # Clock-specific configuration
+ self.timezone_str = config.get('timezone', 'UTC')
+ self.time_format = config.get('time_format', '12h')
+ self.show_seconds = config.get('show_seconds', False)
+ self.show_date = config.get('show_date', True)
+ self.date_format = config.get('date_format', 'MM/DD/YYYY')
+
+ # Colors
+ self.time_color = tuple(config.get('time_color', [255, 255, 255]))
+ self.date_color = tuple(config.get('date_color', [255, 128, 64]))
+ self.ampm_color = tuple(config.get('ampm_color', [255, 255, 128]))
+
+ # Position
+ position = config.get('position', {'x': 0, 'y': 0})
+ self.pos_x = position.get('x', 0)
+ self.pos_y = position.get('y', 0)
+
+ # Get timezone
+ self.timezone = self._get_timezone()
+
+ # Track last display for optimization
+ self.last_time_str = None
+ self.last_date_str = None
+
+ self.logger.info(f"Clock plugin initialized for timezone: {self.timezone_str}")
+
+ def _get_timezone(self):
+ """Get timezone from configuration."""
+ if pytz is None:
+ self.logger.warning("pytz not available, using UTC timezone only")
+ return None
+
+ try:
+ return pytz.timezone(self.timezone_str)
+ except Exception:
+ self.logger.warning(
+ f"Invalid timezone '{self.timezone_str}'. Falling back to UTC. "
+ "Valid timezones can be found at: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
+ )
+ return pytz.utc
+
+ def _format_time_12h(self, dt: datetime) -> Tuple[str, str]:
+ """Format time in 12-hour format."""
+ time_str = dt.strftime("%I:%M")
+ if self.show_seconds:
+ time_str += dt.strftime(":%S")
+
+ # Remove leading zero from hour
+ if time_str.startswith("0"):
+ time_str = time_str[1:]
+
+ ampm = dt.strftime("%p")
+ return time_str, ampm
+
+ def _format_time_24h(self, dt: datetime) -> str:
+ """Format time in 24-hour format."""
+ time_str = dt.strftime("%H:%M")
+ if self.show_seconds:
+ time_str += dt.strftime(":%S")
+ return time_str
+
+ def _format_date(self, dt: datetime) -> str:
+ """Format date according to configured format."""
+ if self.date_format == "MM/DD/YYYY":
+ return dt.strftime("%m/%d/%Y")
+ elif self.date_format == "DD/MM/YYYY":
+ return dt.strftime("%d/%m/%Y")
+ elif self.date_format == "YYYY-MM-DD":
+ return dt.strftime("%Y-%m-%d")
+ else:
+ return dt.strftime("%m/%d/%Y") # fallback
+
+ def update(self) -> None:
+ """
+ Update clock data.
+
+ For a clock, we don't need to fetch external data, but we can
+ prepare the current time for display optimization.
+ """
+ try:
+ # Get current time
+ if pytz and self.timezone:
+ # Use timezone-aware datetime
+ utc_now = datetime.now(pytz.utc)
+ local_time = utc_now.astimezone(self.timezone)
+ else:
+ # Use local system time (no timezone conversion)
+ local_time = datetime.now()
+
+ if self.time_format == "12h":
+ self.current_time, self.current_ampm = self._format_time_12h(local_time)
+ else:
+ self.current_time = self._format_time_24h(local_time)
+
+ if self.show_date:
+ self.current_date = self._format_date(local_time)
+
+ self.last_update = time.time()
+ self.logger.debug(f"Updated clock: {self.current_time} {self.current_ampm if self.time_format == '12h' else ''}")
+
+ except Exception as e:
+ self.logger.error(f"Error updating clock: {e}")
+
+ def display(self, force_clear: bool = False) -> None:
+ """
+ Display the clock.
+
+ Args:
+ force_clear: If True, clear display before rendering
+ """
+ try:
+ if force_clear:
+ self.display_manager.clear()
+
+ # Get display dimensions
+ width = self.display_manager.width
+ height = self.display_manager.height
+
+ # Center the clock display
+ center_x = width // 2
+ center_y = height // 2
+
+ # Display time (centered)
+ self.display_manager.draw_text(
+ self.current_time,
+ x=center_x,
+ y=center_y - 8,
+ color=self.time_color,
+ centered=True
+ )
+
+ # Display AM/PM indicator (12h format only)
+ if self.time_format == "12h" and hasattr(self, 'current_ampm'):
+ self.display_manager.draw_text(
+ self.current_ampm,
+ x=center_x + 40, # Position to the right of time
+ y=center_y - 8,
+ color=self.ampm_color,
+ centered=False
+ )
+
+ # Display date (below time, if enabled)
+ if self.show_date and hasattr(self, 'current_date'):
+ self.display_manager.draw_text(
+ self.current_date,
+ x=center_x,
+ y=center_y + 8,
+ color=self.date_color,
+ centered=True
+ )
+
+ # Update the physical display
+ self.display_manager.update_display()
+
+ self.logger.debug(f"Displayed clock: {self.current_time}")
+
+ except Exception as e:
+ self.logger.error(f"Error displaying clock: {e}")
+ # Show error message on display
+ try:
+ self.display_manager.clear()
+ self.display_manager.draw_text(
+ "Clock Error",
+ x=5, y=15,
+ color=(255, 0, 0)
+ )
+ self.display_manager.update_display()
+ except:
+ pass # If display fails, don't crash
+
+ def get_display_duration(self) -> float:
+ """Get display duration from config."""
+ return self.config.get('display_duration', 15.0)
+
+ def validate_config(self) -> bool:
+ """Validate plugin configuration."""
+ # Call parent validation first
+ if not super().validate_config():
+ return False
+
+ # Validate timezone
+ if pytz is not None:
+ try:
+ pytz.timezone(self.timezone_str)
+ except Exception:
+ self.logger.error(f"Invalid timezone: {self.timezone_str}")
+ return False
+ else:
+ self.logger.warning("pytz not available, timezone validation skipped")
+
+ # Validate time format
+ if self.time_format not in ["12h", "24h"]:
+ self.logger.error(f"Invalid time format: {self.time_format}")
+ return False
+
+ # Validate date format
+ if self.date_format not in ["MM/DD/YYYY", "DD/MM/YYYY", "YYYY-MM-DD"]:
+ self.logger.error(f"Invalid date format: {self.date_format}")
+ return False
+
+ # Validate colors
+ for color_name, color_value in [
+ ("time_color", self.time_color),
+ ("date_color", self.date_color),
+ ("ampm_color", self.ampm_color)
+ ]:
+ if not isinstance(color_value, tuple) or len(color_value) != 3:
+ self.logger.error(f"Invalid {color_name}: must be RGB tuple")
+ return False
+ if not all(0 <= c <= 255 for c in color_value):
+ self.logger.error(f"Invalid {color_name}: values must be 0-255")
+ return False
+
+ return True
+
+ def get_info(self) -> Dict[str, Any]:
+ """Return plugin info for web UI."""
+ info = super().get_info()
+ info.update({
+ 'current_time': getattr(self, 'current_time', None),
+ 'timezone': self.timezone_str,
+ 'time_format': self.time_format,
+ 'show_seconds': self.show_seconds,
+ 'show_date': self.show_date,
+ 'date_format': self.date_format
+ })
+ return info
diff --git a/plugins/clock-simple/manifest.json b/plugins/clock-simple/manifest.json
new file mode 100644
index 000000000..2f35c6d51
--- /dev/null
+++ b/plugins/clock-simple/manifest.json
@@ -0,0 +1,27 @@
+{
+ "id": "clock-simple",
+ "name": "Simple Clock",
+ "version": "1.0.0",
+ "author": "ChuckBuilds",
+ "description": "A simple clock display with current time and date",
+ "homepage": "https://github.com/ChuckBuilds/ledmatrix-clock-simple",
+ "entry_point": "manager.py",
+ "class_name": "SimpleClock",
+ "category": "time",
+ "tags": ["clock", "time", "date"],
+ "compatible_versions": [">=2.0.0"],
+ "ledmatrix_version": "2.0.0",
+ "requires": {
+ "python": ">=3.9",
+ "display_size": {
+ "min_width": 64,
+ "min_height": 32
+ }
+ },
+ "config_schema": "config_schema.json",
+ "assets": {},
+ "update_interval": 1,
+ "default_duration": 15,
+ "display_modes": ["clock"],
+ "api_requirements": []
+}
diff --git a/plugins/clock-simple/requirements.txt b/plugins/clock-simple/requirements.txt
new file mode 100644
index 000000000..5f8ad248b
--- /dev/null
+++ b/plugins/clock-simple/requirements.txt
@@ -0,0 +1 @@
+pytz>=2022.1
diff --git a/plugins/hello-world/manager.py b/plugins/hello-world/manager.py
index 010616088..806e6c3ff 100644
--- a/plugins/hello-world/manager.py
+++ b/plugins/hello-world/manager.py
@@ -8,9 +8,13 @@
from src.plugin_system.base_plugin import BasePlugin
import time
from datetime import datetime
-import freetype
import os
+try:
+ import freetype
+except ImportError:
+ freetype = None
+
class HelloWorldPlugin(BasePlugin):
"""
@@ -41,6 +45,11 @@ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_man
def _load_font(self):
"""Load the 6x9 BDF font for text rendering."""
+ if freetype is None:
+ self.logger.warning("freetype not available, font rendering disabled")
+ self.bdf_font = None
+ return
+
try:
font_path = "assets/fonts/6x9.bdf"
if not os.path.exists(font_path):
diff --git a/plugins/hello-world/requirements.txt b/plugins/hello-world/requirements.txt
new file mode 100644
index 000000000..26be2fe45
--- /dev/null
+++ b/plugins/hello-world/requirements.txt
@@ -0,0 +1 @@
+freetype-py>=2.4.0
diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py
index 4c3954daf..e2772a167 100644
--- a/src/plugin_system/__init__.py
+++ b/src/plugin_system/__init__.py
@@ -12,11 +12,19 @@
from .base_plugin import BasePlugin
from .plugin_manager import PluginManager
-from .store_manager import PluginStoreManager
+
+# Import store_manager only when needed to avoid dependency issues
+def get_store_manager():
+ """Get PluginStoreManager, importing only when needed."""
+ try:
+ from .store_manager import PluginStoreManager
+ return PluginStoreManager
+ except ImportError as e:
+ raise ImportError("PluginStoreManager requires additional dependencies. Install requests: pip install requests") from e
__all__ = [
'BasePlugin',
'PluginManager',
- 'PluginStoreManager',
+ 'get_store_manager',
]
diff --git a/src/plugin_system/plugin_manager.py b/src/plugin_system/plugin_manager.py
index 9f59ff128..0dbc4adce 100644
--- a/src/plugin_system/plugin_manager.py
+++ b/src/plugin_system/plugin_manager.py
@@ -105,7 +105,41 @@ def discover_plugins(self) -> List[str]:
self.logger.info(f"Discovered {len(discovered)} plugin(s)")
return discovered
-
+
+ def _install_plugin_dependencies(self, requirements_file: Path) -> bool:
+ """
+ Install Python dependencies for a plugin.
+
+ Args:
+ requirements_file: Path to requirements.txt file
+
+ Returns:
+ True if successful or no dependencies needed, False on error
+ """
+ try:
+ import subprocess
+
+ self.logger.info(f"Installing dependencies for plugin from {requirements_file}")
+
+ result = subprocess.run(
+ ['pip3', 'install', '--break-system-packages', '-r', str(requirements_file)],
+ check=True,
+ capture_output=True,
+ text=True
+ )
+
+ self.logger.info(f"Successfully installed dependencies for {requirements_file}")
+ return True
+
+ except subprocess.CalledProcessError as e:
+ self.logger.error(f"Error installing dependencies: {e.stderr}")
+ # Don't fail plugin loading if dependencies fail to install
+ # User can manually install them
+ return True
+ except FileNotFoundError:
+ self.logger.warning("pip3 not found, skipping dependency installation")
+ return True
+
def load_plugin(self, plugin_id: str) -> bool:
"""
Load a plugin by ID.
@@ -147,15 +181,20 @@ def load_plugin(self, plugin_id: str) -> bool:
if not entry_file.exists():
self.logger.error(f"Entry point not found: {entry_file}")
return False
-
+
+ # Install plugin dependencies if requirements.txt exists
+ requirements_file = plugin_dir / "requirements.txt"
+ if requirements_file.exists():
+ self._install_plugin_dependencies(requirements_file)
+
# Import the plugin module
module_name = f"plugin_{plugin_id.replace('-', '_')}"
spec = importlib.util.spec_from_file_location(module_name, entry_file)
-
+
if spec is None or spec.loader is None:
self.logger.error(f"Could not create module spec for {entry_file}")
return False
-
+
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
diff --git a/templates/index_v2.html b/templates/index_v2.html
index c67a6be12..c00d97cad 100644
--- a/templates/index_v2.html
+++ b/templates/index_v2.html
@@ -730,6 +730,116 @@
background: rgba(231, 76, 60, 0.9);
color: white;
}
+
+ /* Plugin Management Styles */
+ .plugins-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+ margin-top: 20px;
+ }
+
+ .plugin-card {
+ background: var(--card-background);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ padding: 20px;
+ box-shadow: var(--shadow);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ }
+
+ .plugin-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
+ }
+
+ .plugin-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+ }
+
+ .plugin-header h4 {
+ margin: 0;
+ color: var(--primary-color);
+ font-size: 1.1rem;
+ }
+
+ .plugin-meta {
+ display: flex;
+ gap: 15px;
+ margin-bottom: 10px;
+ font-size: 0.85rem;
+ color: #666;
+ }
+
+ .plugin-description {
+ color: #555;
+ margin-bottom: 15px;
+ line-height: 1.4;
+ }
+
+ .plugin-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+ margin-bottom: 15px;
+ }
+
+ .tag {
+ background: rgba(52, 152, 219, 0.1);
+ color: var(--secondary-color);
+ padding: 3px 8px;
+ border-radius: 12px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ }
+
+ .plugin-controls {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+
+ .plugin-controls button {
+ flex: 1;
+ min-width: 100px;
+ }
+
+ .plugin-config-section {
+ margin-top: 20px;
+ padding-top: 20px;
+ border-top: 1px solid var(--border-color);
+ }
+
+ .config-row {
+ display: flex;
+ gap: 15px;
+ margin-bottom: 15px;
+ align-items: center;
+ }
+
+ .config-row label {
+ min-width: 120px;
+ font-weight: 500;
+ }
+
+ .config-row input,
+ .config-row select,
+ .config-row textarea {
+ flex: 1;
+ padding: 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ }
+
+ .store-controls {
+ background: rgba(52, 152, 219, 0.05);
+ padding: 15px;
+ border-radius: var(--border-radius);
+ margin-bottom: 20px;
+ }
@@ -911,6 +1021,12 @@ Live Display Preview
+
+
@@ -2108,6 +2224,58 @@ System Logs
+
+
+
+
+
Installed Plugins
+
Manage your installed plugins. Enable/disable plugins and configure their settings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Plugin Store
+
Browse and install plugins from the community store.
+
+
+
+
+
+
+
+
+
+
+
Loading plugin store...
+
+
+
@@ -3844,6 +4012,301 @@
}
showNotification('Sports configuration saved', 'success');
}
+ // ===== Plugin Management Functions =====
+
+ async function refreshPlugins() {
+ try {
+ showNotification('Refreshing plugins...', 'info');
+ const response = await fetch('/api/plugins/installed');
+ if (response.ok) {
+ const data = await response.json();
+ renderInstalledPlugins(data.plugins);
+ showNotification('Plugins refreshed successfully', 'success');
+ } else {
+ showNotification('Failed to refresh plugins', 'error');
+ }
+ } catch (error) {
+ showNotification('Error refreshing plugins: ' + error.message, 'error');
+ }
+ }
+
+ function renderInstalledPlugins(plugins) {
+ const container = document.getElementById('installed-plugins');
+ if (!plugins || plugins.length === 0) {
+ container.innerHTML = '
No plugins installed
';
+ return;
+ }
+
+ container.innerHTML = plugins.map(plugin => `
+
+
+
+ ${plugin.author}
+ v${plugin.version}
+ ${plugin.category}
+
+
${plugin.description}
+
+ ${plugin.tags ? plugin.tags.map(tag => `${tag}`).join('') : ''}
+
+
+
+
+
+
+ ${plugin.config ? `
+
+
Configuration
+ ${Object.entries(plugin.config).map(([key, value]) => `
+
+
+
+
+ `).join('')}
+
+ ` : ''}
+
+ `).join('');
+ }
+
+ async function togglePlugin(pluginId, enabled) {
+ try {
+ const response = await fetch('/api/plugins/toggle', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ plugin_id: pluginId, enabled })
+ });
+
+ if (response.ok) {
+ showNotification(`Plugin ${pluginId} ${enabled ? 'enabled' : 'disabled'}`, 'success');
+ refreshPlugins();
+ } else {
+ showNotification(`Failed to ${enabled ? 'enable' : 'disable'} plugin`, 'error');
+ }
+ } catch (error) {
+ showNotification('Error toggling plugin: ' + error.message, 'error');
+ }
+ }
+
+ async function configurePlugin(pluginId) {
+ // For now, just show a placeholder. In a full implementation,
+ // this would open a modal or navigate to a configuration page
+ showNotification(`Configuration for ${pluginId} - Feature coming soon!`, 'info');
+ }
+
+ async function updatePlugin(pluginId) {
+ try {
+ showNotification(`Updating ${pluginId}...`, 'info');
+ const response = await fetch('/api/plugins/update', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ plugin_id: pluginId })
+ });
+
+ if (response.ok) {
+ showNotification(`Plugin ${pluginId} updated successfully`, 'success');
+ refreshPlugins();
+ } else {
+ showNotification(`Failed to update plugin ${pluginId}`, 'error');
+ }
+ } catch (error) {
+ showNotification('Error updating plugin: ' + error.message, 'error');
+ }
+ }
+
+ async function uninstallPlugin(pluginId) {
+ if (!confirm(`Are you sure you want to uninstall ${pluginId}?`)) {
+ return;
+ }
+
+ try {
+ showNotification(`Uninstalling ${pluginId}...`, 'info');
+ const response = await fetch('/api/plugins/uninstall', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ plugin_id: pluginId })
+ });
+
+ if (response.ok) {
+ showNotification(`Plugin ${pluginId} uninstalled successfully`, 'success');
+ refreshPlugins();
+ } else {
+ showNotification(`Failed to uninstall plugin ${pluginId}`, 'error');
+ }
+ } catch (error) {
+ showNotification('Error uninstalling plugin: ' + error.message, 'error');
+ }
+ }
+
+ async function updatePluginConfig(pluginId, key, value) {
+ try {
+ // For now, just show a notification. In a full implementation,
+ // this would save the configuration change
+ showNotification(`Configuration updated for ${pluginId}.${key}`, 'success');
+ } catch (error) {
+ showNotification('Error updating plugin config: ' + error.message, 'error');
+ }
+ }
+
+ async function searchPlugins() {
+ try {
+ const query = document.getElementById('plugin-search').value;
+ const category = document.getElementById('plugin-category').value;
+
+ showNotification('Searching plugin store...', 'info');
+
+ let url = '/api/plugins/store/list';
+ const params = new URLSearchParams();
+ if (query) params.append('query', query);
+ if (category) params.append('category', category);
+
+ if (params.toString()) {
+ url += '?' + params.toString();
+ }
+
+ const response = await fetch(url);
+ if (response.ok) {
+ const data = await response.json();
+ renderPluginStore(data.plugins);
+ showNotification(`Found ${data.plugins.length} plugins`, 'success');
+ } else {
+ showNotification('Failed to search plugin store', 'error');
+ }
+ } catch (error) {
+ showNotification('Error searching plugins: ' + error.message, 'error');
+ }
+ }
+
+ function renderPluginStore(plugins) {
+ const container = document.getElementById('plugin-store-content');
+ if (!plugins || plugins.length === 0) {
+ container.innerHTML = 'No plugins found
';
+ return;
+ }
+
+ container.innerHTML = plugins.map(plugin => `
+
+
+
+ ${plugin.author}
+ ${plugin.category}
+ ${plugin.stars || 0}
+ ${plugin.downloads || 0}
+
+
${plugin.description}
+
+ ${plugin.tags ? plugin.tags.map(tag => `${tag}`).join('') : ''}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ async function installPlugin(pluginId) {
+ try {
+ showNotification(`Installing ${pluginId}...`, 'info');
+ const response = await fetch('/api/plugins/install', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ plugin_id: pluginId })
+ });
+
+ if (response.ok) {
+ showNotification(`Plugin ${pluginId} installed successfully!`, 'success');
+ searchPlugins(); // Refresh the store
+ // Also refresh installed plugins to show the new plugin
+ setTimeout(refreshPlugins, 1000);
+ } else {
+ const error = await response.json();
+ showNotification(`Failed to install plugin: ${error.message}`, 'error');
+ }
+ } catch (error) {
+ showNotification('Error installing plugin: ' + error.message, 'error');
+ }
+ }
+
+ async function restartDisplay() {
+ try {
+ showNotification('Restarting display service...', 'info');
+ const response = await fetch('/api/actions/restart_display', {
+ method: 'POST'
+ });
+
+ if (response.ok) {
+ showNotification('Display service restarted successfully', 'success');
+ } else {
+ showNotification('Failed to restart display service', 'error');
+ }
+ } catch (error) {
+ showNotification('Error restarting display: ' + error.message, 'error');
+ }
+ }
+
+ // Initialize plugin management when tabs are shown
+ document.addEventListener('DOMContentLoaded', function() {
+ // Load plugins when the plugins tab is first shown
+ const observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
+ const target = mutation.target;
+ if (target.id === 'plugins' && target.classList.contains('active')) {
+ refreshPlugins();
+ observer.disconnect();
+ }
+ }
+ });
+ });
+
+ const pluginsTab = document.getElementById('plugins');
+ if (pluginsTab) {
+ observer.observe(pluginsTab, { attributes: true });
+ }
+
+ // Load plugin store when the store tab is first shown
+ const storeObserver = new MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
+ const target = mutation.target;
+ if (target.id === 'plugin-store' && target.classList.contains('active')) {
+ searchPlugins();
+ storeObserver.disconnect();
+ }
+ }
+ });
+ });
+
+ const storeTab = document.getElementById('plugin-store');
+ if (storeTab) {
+ storeObserver.observe(storeTab, { attributes: true });
+ }
+ });