Introduce Pattern Template entity as a reusable rectangle layout that Key Colors targets reference via pattern_template_id. This replaces inline rectangle storage with a shared template system. Backend: - New PatternTemplate data model, store (JSON persistence), CRUD API - KC targets now reference pattern_template_id instead of inline rectangles - ProcessorManager resolves pattern template at KC processing start - Picture source test endpoint supports capture_duration=0 for single frame - Delete protection: 409 when template is referenced by a KC target Frontend: - Pattern Templates section in Key Colors sub-tab with card UI - Visual canvas editor with drag-to-move, 8-point resize handles - Background capture from any picture source for visual alignment - Precise coordinate list synced bidirectionally with canvas - Resizable editor container, viewport-constrained modal - KC target editor uses pattern template dropdown instead of inline rects - Localization (en/ru) for all new UI elements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
160 lines
4.0 KiB
Python
160 lines
4.0 KiB
Python
"""Configuration management for LED Grab."""
|
|
|
|
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 = 90
|
|
min_fps: int = 10
|
|
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"
|
|
templates_file: str = "data/capture_templates.json"
|
|
postprocessing_templates_file: str = "data/postprocessing_templates.json"
|
|
picture_sources_file: str = "data/picture_sources.json"
|
|
picture_targets_file: str = "data/picture_targets.json"
|
|
pattern_templates_file: str = "data/pattern_templates.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
|