Add capture template system with in-memory defaults and split device settings UI
Some checks failed
Validate / validate (push) Failing after 8s
Some checks failed
Validate / validate (push) Failing after 8s
- Generate default templates (MSS, DXcam, WGC) in memory from EngineRegistry at startup - Only persist user-created templates to JSON, skip defaults on load/save - Add capture_template_id to Device model and DeviceCreate schema - Remember last used template in localStorage, use it for new devices with fallback - Split Device Settings dialog into General Settings and Capture Settings - Add capture settings button (🎬) to device card - Separate default and custom templates with visual separator in Templates tab - Add capture engine integration to ProcessorManager - Add CLAUDE.md with git commit/push policy and server restart instructions - Add en/ru localization for all new UI elements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ class Device:
|
||||
enabled: bool = True,
|
||||
settings: Optional[ProcessingSettings] = None,
|
||||
calibration: Optional[CalibrationConfig] = None,
|
||||
capture_template_id: str = "tpl_mss_default",
|
||||
created_at: Optional[datetime] = None,
|
||||
updated_at: Optional[datetime] = None,
|
||||
):
|
||||
@@ -43,6 +44,7 @@ class Device:
|
||||
enabled: Whether device is enabled
|
||||
settings: Processing settings
|
||||
calibration: Calibration configuration
|
||||
capture_template_id: ID of assigned capture template
|
||||
created_at: Creation timestamp
|
||||
updated_at: Last update timestamp
|
||||
"""
|
||||
@@ -53,6 +55,7 @@ class Device:
|
||||
self.enabled = enabled
|
||||
self.settings = settings or ProcessingSettings()
|
||||
self.calibration = calibration or create_default_calibration(led_count)
|
||||
self.capture_template_id = capture_template_id
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
self.updated_at = updated_at or datetime.utcnow()
|
||||
|
||||
@@ -80,6 +83,7 @@ class Device:
|
||||
"state_check_interval": self.settings.state_check_interval,
|
||||
},
|
||||
"calibration": calibration_to_dict(self.calibration),
|
||||
"capture_template_id": self.capture_template_id,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
@@ -117,6 +121,12 @@ class Device:
|
||||
else create_default_calibration(data["led_count"])
|
||||
)
|
||||
|
||||
# Migration: assign default MSS template if no template set
|
||||
capture_template_id = data.get("capture_template_id")
|
||||
if not capture_template_id:
|
||||
capture_template_id = "tpl_mss_default"
|
||||
logger.info(f"Migrating device {data['id']} to default MSS template")
|
||||
|
||||
return cls(
|
||||
device_id=data["id"],
|
||||
name=data["name"],
|
||||
@@ -125,6 +135,7 @@ class Device:
|
||||
enabled=data.get("enabled", True),
|
||||
settings=settings,
|
||||
calibration=calibration,
|
||||
capture_template_id=capture_template_id,
|
||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||
)
|
||||
@@ -206,6 +217,7 @@ class DeviceStore:
|
||||
led_count: int,
|
||||
settings: Optional[ProcessingSettings] = None,
|
||||
calibration: Optional[CalibrationConfig] = None,
|
||||
capture_template_id: str = "tpl_mss_default",
|
||||
) -> Device:
|
||||
"""Create a new device.
|
||||
|
||||
@@ -215,6 +227,7 @@ class DeviceStore:
|
||||
led_count: Number of LEDs
|
||||
settings: Processing settings
|
||||
calibration: Calibration configuration
|
||||
capture_template_id: ID of assigned capture template
|
||||
|
||||
Returns:
|
||||
Created device
|
||||
@@ -233,6 +246,7 @@ class DeviceStore:
|
||||
led_count=led_count,
|
||||
settings=settings,
|
||||
calibration=calibration,
|
||||
capture_template_id=capture_template_id,
|
||||
)
|
||||
|
||||
# Store
|
||||
@@ -270,6 +284,7 @@ class DeviceStore:
|
||||
enabled: Optional[bool] = None,
|
||||
settings: Optional[ProcessingSettings] = None,
|
||||
calibration: Optional[CalibrationConfig] = None,
|
||||
capture_template_id: Optional[str] = None,
|
||||
) -> Device:
|
||||
"""Update device.
|
||||
|
||||
@@ -281,6 +296,7 @@ class DeviceStore:
|
||||
enabled: New enabled state (optional)
|
||||
settings: New settings (optional)
|
||||
calibration: New calibration (optional)
|
||||
capture_template_id: New capture template ID (optional)
|
||||
|
||||
Returns:
|
||||
Updated device
|
||||
@@ -313,6 +329,8 @@ class DeviceStore:
|
||||
f"does not match device LED count ({device.led_count})"
|
||||
)
|
||||
device.calibration = calibration
|
||||
if capture_template_id is not None:
|
||||
device.capture_template_id = capture_template_id
|
||||
|
||||
device.updated_at = datetime.utcnow()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from wled_controller.core.capture_engines.factory import EngineRegistry
|
||||
from wled_controller.storage.template import CaptureTemplate
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
@@ -13,7 +14,11 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class TemplateStore:
|
||||
"""Storage for capture templates."""
|
||||
"""Storage for capture templates.
|
||||
|
||||
Default templates for each available engine are created in memory at startup.
|
||||
Only user-created templates are persisted to the JSON file.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str):
|
||||
"""Initialize template store.
|
||||
@@ -23,13 +28,35 @@ class TemplateStore:
|
||||
"""
|
||||
self.file_path = Path(file_path)
|
||||
self._templates: Dict[str, CaptureTemplate] = {}
|
||||
self._ensure_defaults()
|
||||
self._load()
|
||||
|
||||
def _ensure_defaults(self) -> None:
|
||||
"""Create default templates in memory for all available engines."""
|
||||
available = EngineRegistry.get_available_engines()
|
||||
now = datetime.utcnow()
|
||||
|
||||
for engine_type in available:
|
||||
template_id = f"tpl_{engine_type}_default"
|
||||
engine_class = EngineRegistry.get_engine(engine_type)
|
||||
default_config = engine_class.get_default_config()
|
||||
|
||||
self._templates[template_id] = CaptureTemplate(
|
||||
id=template_id,
|
||||
name=engine_type.upper(),
|
||||
engine_type=engine_type,
|
||||
engine_config=default_config,
|
||||
is_default=True,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=f"Default {engine_type} capture template",
|
||||
)
|
||||
|
||||
logger.info(f"Created {len(available)} default templates in memory")
|
||||
|
||||
def _load(self) -> None:
|
||||
"""Load templates from file."""
|
||||
"""Load user-created templates from file."""
|
||||
if not self.file_path.exists():
|
||||
logger.warning(f"Templates file not found: {self.file_path}")
|
||||
self._save() # Create empty file
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -37,33 +64,42 @@ class TemplateStore:
|
||||
data = json.load(f)
|
||||
|
||||
templates_data = data.get("templates", {})
|
||||
loaded = 0
|
||||
for template_id, template_dict in templates_data.items():
|
||||
# Skip any default templates that may exist in old files
|
||||
if template_dict.get("is_default", False):
|
||||
continue
|
||||
try:
|
||||
template = CaptureTemplate.from_dict(template_dict)
|
||||
self._templates[template_id] = template
|
||||
loaded += 1
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to load template {template_id}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
logger.info(f"Loaded {len(self._templates)} templates from storage")
|
||||
logger.info(f"Template store initialized with {len(self._templates)} templates")
|
||||
if loaded > 0:
|
||||
logger.info(f"Loaded {loaded} user templates from storage")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load templates from {self.file_path}: {e}")
|
||||
raise
|
||||
|
||||
total = len(self._templates)
|
||||
logger.info(f"Template store initialized with {total} templates")
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Save templates to file."""
|
||||
"""Save only user-created templates to file."""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Convert templates to dict format
|
||||
# Only persist non-default templates
|
||||
templates_dict = {
|
||||
template_id: template.to_dict()
|
||||
for template_id, template in self._templates.items()
|
||||
if not template.is_default
|
||||
}
|
||||
|
||||
data = {
|
||||
|
||||
Reference in New Issue
Block a user