Initial commit: WLED Screen Controller with FastAPI server and Home Assistant integration
Some checks failed
Validate / validate (push) Failing after 1m6s
Some checks failed
Validate / validate (push) Failing after 1m6s
This is a complete WLED ambient lighting controller that captures screen border pixels and sends them to WLED devices for immersive ambient lighting effects. ## Server Features: - FastAPI-based REST API with 17+ endpoints - Real-time screen capture with multi-monitor support - Advanced LED calibration system with visual GUI - API key authentication with labeled tokens - Per-device brightness control (0-100%) - Configurable FPS (1-60), border width, and color correction - Persistent device storage (JSON-based) - Comprehensive Web UI with dark/light themes - Docker support with docker-compose - Windows monitor name detection via WMI (shows "LG ULTRAWIDE" etc.) ## Web UI Features: - Device management (add, configure, remove WLED devices) - Real-time status monitoring with FPS metrics - Settings modal for device configuration - Visual calibration GUI with edge testing - Brightness slider per device - Display selection with friendly monitor names - Token-based authentication with login/logout - Responsive button layout ## Calibration System: - Support for any LED strip layout (clockwise/counterclockwise) - 4 starting position options (corners) - Per-edge LED count configuration - Visual preview with starting position indicator - Test buttons to light up individual edges - Smart LED ordering based on start position and direction ## Home Assistant Integration: - Custom HACS integration - Switch entities for processing control - Sensor entities for status and FPS - Select entities for display selection - Config flow for easy setup - Auto-discovery of devices from server ## Technical Stack: - Python 3.11+ - FastAPI + uvicorn - mss (screen capture) - httpx (async WLED client) - Pydantic (validation) - WMI (Windows monitor detection) - Structlog (logging) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
154
server/src/wled_controller/config.py
Normal file
154
server/src/wled_controller/config.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Configuration management for WLED Screen Controller."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Literal
|
||||
|
||||
import yaml
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class ServerConfig(BaseSettings):
|
||||
"""Server configuration."""
|
||||
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8080
|
||||
log_level: str = "INFO"
|
||||
cors_origins: List[str] = ["*"]
|
||||
|
||||
|
||||
class AuthConfig(BaseSettings):
|
||||
"""Authentication configuration."""
|
||||
|
||||
api_keys: dict[str, str] = {} # label: key mapping (required for security)
|
||||
|
||||
|
||||
class ProcessingConfig(BaseSettings):
|
||||
"""Processing configuration."""
|
||||
|
||||
default_fps: int = 30
|
||||
max_fps: int = 60
|
||||
min_fps: int = 1
|
||||
border_width: int = 10
|
||||
interpolation_mode: Literal["average", "median", "dominant"] = "average"
|
||||
|
||||
|
||||
class ScreenCaptureConfig(BaseSettings):
|
||||
"""Screen capture configuration."""
|
||||
|
||||
buffer_size: int = 2
|
||||
|
||||
|
||||
class WLEDConfig(BaseSettings):
|
||||
"""WLED client configuration."""
|
||||
|
||||
timeout: int = 5
|
||||
retry_attempts: int = 3
|
||||
retry_delay: int = 1
|
||||
protocol: Literal["http", "https"] = "http"
|
||||
max_brightness: int = 255
|
||||
|
||||
|
||||
class StorageConfig(BaseSettings):
|
||||
"""Storage configuration."""
|
||||
|
||||
devices_file: str = "data/devices.json"
|
||||
|
||||
|
||||
class LoggingConfig(BaseSettings):
|
||||
"""Logging configuration."""
|
||||
|
||||
format: Literal["json", "text"] = "json"
|
||||
file: str = "logs/wled_controller.log"
|
||||
max_size_mb: int = 100
|
||||
backup_count: int = 5
|
||||
|
||||
|
||||
class Config(BaseSettings):
|
||||
"""Main application configuration."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="WLED_",
|
||||
env_nested_delimiter="__",
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
server: ServerConfig = Field(default_factory=ServerConfig)
|
||||
auth: AuthConfig = Field(default_factory=AuthConfig)
|
||||
processing: ProcessingConfig = Field(default_factory=ProcessingConfig)
|
||||
screen_capture: ScreenCaptureConfig = Field(default_factory=ScreenCaptureConfig)
|
||||
wled: WLEDConfig = Field(default_factory=WLEDConfig)
|
||||
storage: StorageConfig = Field(default_factory=StorageConfig)
|
||||
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, config_path: str | Path) -> "Config":
|
||||
"""Load configuration from YAML file.
|
||||
|
||||
Args:
|
||||
config_path: Path to YAML configuration file
|
||||
|
||||
Returns:
|
||||
Config instance
|
||||
"""
|
||||
config_path = Path(config_path)
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"Configuration file not found: {config_path}")
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config_data = yaml.safe_load(f)
|
||||
|
||||
return cls(**config_data)
|
||||
|
||||
@classmethod
|
||||
def load(cls) -> "Config":
|
||||
"""Load configuration from default locations.
|
||||
|
||||
Tries to load from:
|
||||
1. Environment variable WLED_CONFIG_PATH
|
||||
2. ./config/default_config.yaml
|
||||
3. Default values
|
||||
|
||||
Returns:
|
||||
Config instance
|
||||
"""
|
||||
config_path = os.getenv("WLED_CONFIG_PATH")
|
||||
|
||||
if config_path:
|
||||
return cls.from_yaml(config_path)
|
||||
|
||||
# Try default location
|
||||
default_path = Path("config/default_config.yaml")
|
||||
if default_path.exists():
|
||||
return cls.from_yaml(default_path)
|
||||
|
||||
# Use defaults
|
||||
return cls()
|
||||
|
||||
|
||||
# Global configuration instance
|
||||
config: Config | None = None
|
||||
|
||||
|
||||
def get_config() -> Config:
|
||||
"""Get global configuration instance.
|
||||
|
||||
Returns:
|
||||
Config instance
|
||||
"""
|
||||
global config
|
||||
if config is None:
|
||||
config = Config.load()
|
||||
return config
|
||||
|
||||
|
||||
def reload_config() -> Config:
|
||||
"""Reload configuration from file.
|
||||
|
||||
Returns:
|
||||
New Config instance
|
||||
"""
|
||||
global config
|
||||
config = Config.load()
|
||||
return config
|
||||
Reference in New Issue
Block a user