Rename picture-targets to output-targets across entire codebase

Rename all Python modules, classes, API endpoints, config keys, frontend
fetch URLs, and Home Assistant integration URLs from picture-targets to
output-targets. Store loads both new and legacy JSON keys for backward
compatibility with existing data files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 10:55:36 +03:00
parent 5b4813368b
commit 353a1c2d85
37 changed files with 243 additions and 244 deletions

View File

@@ -17,9 +17,7 @@ Priority: `P1` quick win · `P2` moderate · `P3` large effort
## Output Targets ## Output Targets
- [ ] `P1` **Rename `picture-targets` to `output-targets`** — Rename API endpoints and internal references for clarity - [x] `P1` **Rename `picture-targets` to `output-targets`** — Rename API endpoints and internal references for clarity
- Complexity: low — mechanical rename across routes, schemas, store, frontend fetch calls; no logic changes, but many files touched (~20+), needs care with stored JSON migration
- Impact: low-medium — improves API clarity for future integrations (OpenRGB, Art-Net)
- [x] `P2` **OpenRGB** — Control PC peripherals (keyboard, mouse, RAM, fans) as ambient targets - [x] `P2` **OpenRGB** — Control PC peripherals (keyboard, mouse, RAM, fans) as ambient targets
- [ ] `P2` **Art-Net / sACN (E1.31)** — Stage/theatrical lighting protocols, DMX controllers - [ ] `P2` **Art-Net / sACN (E1.31)** — Stage/theatrical lighting protocols, DMX controllers
- Complexity: medium — UDP-based protocols with well-documented specs; similar architecture to DDP client; needs DMX universe/channel mapping UI - Complexity: medium — UDP-based protocols with well-documented specs; similar architecture to DDP client; needs DMX universe/channel mapping UI

View File

@@ -61,7 +61,7 @@ async def validate_server(
headers = {"Authorization": f"Bearer {api_key}"} headers = {"Authorization": f"Bearer {api_key}"}
try: try:
async with session.get( async with session.get(
f"{server_url}/api/v1/picture-targets", f"{server_url}/api/v1/output-targets",
headers=headers, headers=headers,
timeout=timeout, timeout=timeout,
) as resp: ) as resp:

View File

@@ -146,9 +146,9 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
self.server_version = "unknown" self.server_version = "unknown"
async def _fetch_targets(self) -> list[dict[str, Any]]: async def _fetch_targets(self) -> list[dict[str, Any]]:
"""Fetch all picture targets.""" """Fetch all output targets."""
async with self.session.get( async with self.session.get(
f"{self.server_url}/api/v1/picture-targets", f"{self.server_url}/api/v1/output-targets",
headers=self._auth_headers, headers=self._auth_headers,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp: ) as resp:
@@ -159,7 +159,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
async def _fetch_target_state(self, target_id: str) -> dict[str, Any]: async def _fetch_target_state(self, target_id: str) -> dict[str, Any]:
"""Fetch target processing state.""" """Fetch target processing state."""
async with self.session.get( async with self.session.get(
f"{self.server_url}/api/v1/picture-targets/{target_id}/state", f"{self.server_url}/api/v1/output-targets/{target_id}/state",
headers=self._auth_headers, headers=self._auth_headers,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp: ) as resp:
@@ -169,7 +169,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
async def _fetch_target_metrics(self, target_id: str) -> dict[str, Any]: async def _fetch_target_metrics(self, target_id: str) -> dict[str, Any]:
"""Fetch target metrics.""" """Fetch target metrics."""
async with self.session.get( async with self.session.get(
f"{self.server_url}/api/v1/picture-targets/{target_id}/metrics", f"{self.server_url}/api/v1/output-targets/{target_id}/metrics",
headers=self._auth_headers, headers=self._auth_headers,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp: ) as resp:
@@ -277,7 +277,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
"""Set brightness for a Key Colors target (0-255 mapped to 0.0-1.0).""" """Set brightness for a Key Colors target (0-255 mapped to 0.0-1.0)."""
brightness_float = round(brightness / 255, 4) brightness_float = round(brightness / 255, 4)
async with self.session.put( async with self.session.put(
f"{self.server_url}/api/v1/picture-targets/{target_id}", f"{self.server_url}/api/v1/output-targets/{target_id}",
headers={**self._auth_headers, "Content-Type": "application/json"}, headers={**self._auth_headers, "Content-Type": "application/json"},
json={"key_colors_settings": {"brightness": brightness_float}}, json={"key_colors_settings": {"brightness": brightness_float}},
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
@@ -353,9 +353,9 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
await self.async_request_refresh() await self.async_request_refresh()
async def update_target(self, target_id: str, **kwargs: Any) -> None: async def update_target(self, target_id: str, **kwargs: Any) -> None:
"""Update a picture target's fields.""" """Update a output target's fields."""
async with self.session.put( async with self.session.put(
f"{self.server_url}/api/v1/picture-targets/{target_id}", f"{self.server_url}/api/v1/output-targets/{target_id}",
headers={**self._auth_headers, "Content-Type": "application/json"}, headers={**self._auth_headers, "Content-Type": "application/json"},
json=kwargs, json=kwargs,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
@@ -372,7 +372,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
async def start_processing(self, target_id: str) -> None: async def start_processing(self, target_id: str) -> None:
"""Start processing for a target.""" """Start processing for a target."""
async with self.session.post( async with self.session.post(
f"{self.server_url}/api/v1/picture-targets/{target_id}/start", f"{self.server_url}/api/v1/output-targets/{target_id}/start",
headers=self._auth_headers, headers=self._auth_headers,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp: ) as resp:
@@ -390,7 +390,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
async def stop_processing(self, target_id: str) -> None: async def stop_processing(self, target_id: str) -> None:
"""Stop processing for a target.""" """Stop processing for a target."""
async with self.session.post( async with self.session.post(
f"{self.server_url}/api/v1/picture-targets/{target_id}/stop", f"{self.server_url}/api/v1/output-targets/{target_id}/stop",
headers=self._auth_headers, headers=self._auth_headers,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp: ) as resp:

View File

@@ -40,7 +40,7 @@ class KeyColorsWebSocketManager:
ws_base = self._server_url.replace("http://", "ws://").replace( ws_base = self._server_url.replace("http://", "ws://").replace(
"https://", "wss://" "https://", "wss://"
) )
return f"{ws_base}/api/v1/picture-targets/{target_id}/ws?token={self._api_key}" return f"{ws_base}/api/v1/output-targets/{target_id}/ws?token={self._api_key}"
async def start_listening(self, target_id: str) -> None: async def start_listening(self, target_id: str) -> None:
"""Start WebSocket connection for a target.""" """Start WebSocket connection for a target."""

View File

@@ -17,7 +17,7 @@ storage:
templates_file: "data/capture_templates.json" templates_file: "data/capture_templates.json"
postprocessing_templates_file: "data/postprocessing_templates.json" postprocessing_templates_file: "data/postprocessing_templates.json"
picture_sources_file: "data/picture_sources.json" picture_sources_file: "data/picture_sources.json"
picture_targets_file: "data/picture_targets.json" output_targets_file: "data/output_targets.json"
pattern_templates_file: "data/pattern_templates.json" pattern_templates_file: "data/pattern_templates.json"
mqtt: mqtt:

View File

@@ -15,7 +15,7 @@ storage:
templates_file: "data/capture_templates.json" templates_file: "data/capture_templates.json"
postprocessing_templates_file: "data/postprocessing_templates.json" postprocessing_templates_file: "data/postprocessing_templates.json"
picture_sources_file: "data/picture_sources.json" picture_sources_file: "data/picture_sources.json"
picture_targets_file: "data/picture_targets.json" output_targets_file: "data/output_targets.json"
pattern_templates_file: "data/pattern_templates.json" pattern_templates_file: "data/pattern_templates.json"
logging: logging:

View File

@@ -8,7 +8,7 @@ from .routes.templates import router as templates_router
from .routes.postprocessing import router as postprocessing_router from .routes.postprocessing import router as postprocessing_router
from .routes.picture_sources import router as picture_sources_router from .routes.picture_sources import router as picture_sources_router
from .routes.pattern_templates import router as pattern_templates_router from .routes.pattern_templates import router as pattern_templates_router
from .routes.picture_targets import router as picture_targets_router from .routes.output_targets import router as output_targets_router
from .routes.color_strip_sources import router as color_strip_sources_router from .routes.color_strip_sources import router as color_strip_sources_router
from .routes.audio import router as audio_router from .routes.audio import router as audio_router
from .routes.audio_sources import router as audio_sources_router from .routes.audio_sources import router as audio_sources_router
@@ -31,7 +31,7 @@ router.include_router(audio_router)
router.include_router(audio_sources_router) router.include_router(audio_sources_router)
router.include_router(audio_templates_router) router.include_router(audio_templates_router)
router.include_router(value_sources_router) router.include_router(value_sources_router)
router.include_router(picture_targets_router) router.include_router(output_targets_router)
router.include_router(automations_router) router.include_router(automations_router)
router.include_router(scene_presets_router) router.include_router(scene_presets_router)
router.include_router(webhooks_router) router.include_router(webhooks_router)

View File

@@ -6,7 +6,7 @@ from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.pattern_template_store import PatternTemplateStore from wled_controller.storage.pattern_template_store import PatternTemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.storage.color_strip_store import ColorStripStore from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage.audio_source_store import AudioSourceStore from wled_controller.storage.audio_source_store import AudioSourceStore
from wled_controller.storage.audio_template_store import AudioTemplateStore from wled_controller.storage.audio_template_store import AudioTemplateStore
@@ -25,7 +25,7 @@ _template_store: TemplateStore | None = None
_pp_template_store: PostprocessingTemplateStore | None = None _pp_template_store: PostprocessingTemplateStore | None = None
_pattern_template_store: PatternTemplateStore | None = None _pattern_template_store: PatternTemplateStore | None = None
_picture_source_store: PictureSourceStore | None = None _picture_source_store: PictureSourceStore | None = None
_picture_target_store: PictureTargetStore | None = None _output_target_store: OutputTargetStore | None = None
_color_strip_store: ColorStripStore | None = None _color_strip_store: ColorStripStore | None = None
_audio_source_store: AudioSourceStore | None = None _audio_source_store: AudioSourceStore | None = None
_audio_template_store: AudioTemplateStore | None = None _audio_template_store: AudioTemplateStore | None = None
@@ -73,11 +73,11 @@ def get_picture_source_store() -> PictureSourceStore:
return _picture_source_store return _picture_source_store
def get_picture_target_store() -> PictureTargetStore: def get_output_target_store() -> OutputTargetStore:
"""Get picture target store dependency.""" """Get output target store dependency."""
if _picture_target_store is None: if _output_target_store is None:
raise RuntimeError("Picture target store not initialized") raise RuntimeError("Picture target store not initialized")
return _picture_target_store return _output_target_store
def get_color_strip_store() -> ColorStripStore: def get_color_strip_store() -> ColorStripStore:
@@ -164,7 +164,7 @@ def init_dependencies(
pp_template_store: PostprocessingTemplateStore | None = None, pp_template_store: PostprocessingTemplateStore | None = None,
pattern_template_store: PatternTemplateStore | None = None, pattern_template_store: PatternTemplateStore | None = None,
picture_source_store: PictureSourceStore | None = None, picture_source_store: PictureSourceStore | None = None,
picture_target_store: PictureTargetStore | None = None, output_target_store: OutputTargetStore | None = None,
color_strip_store: ColorStripStore | None = None, color_strip_store: ColorStripStore | None = None,
audio_source_store: AudioSourceStore | None = None, audio_source_store: AudioSourceStore | None = None,
audio_template_store: AudioTemplateStore | None = None, audio_template_store: AudioTemplateStore | None = None,
@@ -178,7 +178,7 @@ def init_dependencies(
): ):
"""Initialize global dependencies.""" """Initialize global dependencies."""
global _device_store, _template_store, _processor_manager global _device_store, _template_store, _processor_manager
global _pp_template_store, _pattern_template_store, _picture_source_store, _picture_target_store global _pp_template_store, _pattern_template_store, _picture_source_store, _output_target_store
global _color_strip_store, _audio_source_store, _audio_template_store global _color_strip_store, _audio_source_store, _audio_template_store
global _value_source_store, _automation_store, _scene_preset_store, _automation_engine, _auto_backup_engine global _value_source_store, _automation_store, _scene_preset_store, _automation_engine, _auto_backup_engine
global _sync_clock_store, _sync_clock_manager global _sync_clock_store, _sync_clock_manager
@@ -188,7 +188,7 @@ def init_dependencies(
_pp_template_store = pp_template_store _pp_template_store = pp_template_store
_pattern_template_store = pattern_template_store _pattern_template_store = pattern_template_store
_picture_source_store = picture_source_store _picture_source_store = picture_source_store
_picture_target_store = picture_target_store _output_target_store = output_target_store
_color_strip_store = color_strip_store _color_strip_store = color_strip_store
_audio_source_store = audio_source_store _audio_source_store = audio_source_store
_audio_template_store = audio_template_store _audio_template_store = audio_template_store

View File

@@ -9,7 +9,7 @@ from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import ( from wled_controller.api.dependencies import (
get_color_strip_store, get_color_strip_store,
get_picture_source_store, get_picture_source_store,
get_picture_target_store, get_output_target_store,
get_processor_manager, get_processor_manager,
) )
from wled_controller.api.schemas.color_strip_sources import ( from wled_controller.api.schemas.color_strip_sources import (
@@ -35,7 +35,7 @@ from wled_controller.storage.color_strip_source import ApiInputColorStripSource,
from wled_controller.storage.color_strip_store import ColorStripStore from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage.picture_source import ProcessedPictureSource, ScreenCapturePictureSource from wled_controller.storage.picture_source import ProcessedPictureSource, ScreenCapturePictureSource
from wled_controller.storage.picture_source_store import PictureSourceStore from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger from wled_controller.utils import get_logger
from wled_controller.config import get_config from wled_controller.config import get_config
@@ -295,7 +295,7 @@ async def delete_color_strip_source(
source_id: str, source_id: str,
_auth: AuthRequired, _auth: AuthRequired,
store: ColorStripStore = Depends(get_color_strip_store), store: ColorStripStore = Depends(get_color_strip_store),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
): ):
"""Delete a color strip source. Returns 409 if referenced by any LED target.""" """Delete a color strip source. Returns 409 if referenced by any LED target."""
try: try:

View File

@@ -13,7 +13,7 @@ from wled_controller.core.devices.led_client import (
) )
from wled_controller.api.dependencies import ( from wled_controller.api.dependencies import (
get_device_store, get_device_store,
get_picture_target_store, get_output_target_store,
get_processor_manager, get_processor_manager,
) )
from wled_controller.api.schemas.devices import ( from wled_controller.api.schemas.devices import (
@@ -29,7 +29,7 @@ from wled_controller.api.schemas.devices import (
) )
from wled_controller.core.processing.processor_manager import ProcessorManager from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore from wled_controller.storage import DeviceStore
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger from wled_controller.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -342,7 +342,7 @@ async def delete_device(
device_id: str, device_id: str,
_auth: AuthRequired, _auth: AuthRequired,
store: DeviceStore = Depends(get_device_store), store: DeviceStore = Depends(get_device_store),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Delete/detach a device. Returns 409 if referenced by a target.""" """Delete/detach a device. Returns 409 if referenced by a target."""

View File

@@ -1,4 +1,4 @@
"""Picture target routes: CRUD, processing control, settings, state, metrics.""" """Output target routes: CRUD, processing control, settings, state, metrics."""
import asyncio import asyncio
import base64 import base64
@@ -16,21 +16,21 @@ from wled_controller.api.dependencies import (
get_device_store, get_device_store,
get_pattern_template_store, get_pattern_template_store,
get_picture_source_store, get_picture_source_store,
get_picture_target_store, get_output_target_store,
get_pp_template_store, get_pp_template_store,
get_processor_manager, get_processor_manager,
get_template_store, get_template_store,
) )
from wled_controller.api.schemas.picture_targets import ( from wled_controller.api.schemas.output_targets import (
ExtractedColorResponse, ExtractedColorResponse,
KCTestRectangleResponse, KCTestRectangleResponse,
KCTestResponse, KCTestResponse,
KeyColorsResponse, KeyColorsResponse,
KeyColorsSettingsSchema, KeyColorsSettingsSchema,
PictureTargetCreate, OutputTargetCreate,
PictureTargetListResponse, OutputTargetListResponse,
PictureTargetResponse, OutputTargetResponse,
PictureTargetUpdate, OutputTargetUpdate,
TargetMetricsResponse, TargetMetricsResponse,
TargetProcessingState, TargetProcessingState,
) )
@@ -51,12 +51,12 @@ from wled_controller.storage.pattern_template_store import PatternTemplateStore
from wled_controller.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource from wled_controller.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
from wled_controller.storage.picture_source_store import PictureSourceStore from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.template_store import TemplateStore from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.wled_picture_target import WledPictureTarget from wled_controller.storage.wled_output_target import WledOutputTarget
from wled_controller.storage.key_colors_picture_target import ( from wled_controller.storage.key_colors_output_target import (
KeyColorsSettings, KeyColorsSettings,
KeyColorsPictureTarget, KeyColorsOutputTarget,
) )
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger from wled_controller.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -88,10 +88,10 @@ def _kc_schema_to_settings(schema: KeyColorsSettingsSchema) -> KeyColorsSettings
) )
def _target_to_response(target) -> PictureTargetResponse: def _target_to_response(target) -> OutputTargetResponse:
"""Convert a PictureTarget to PictureTargetResponse.""" """Convert an OutputTarget to OutputTargetResponse."""
if isinstance(target, WledPictureTarget): if isinstance(target, WledOutputTarget):
return PictureTargetResponse( return OutputTargetResponse(
id=target.id, id=target.id,
name=target.name, name=target.name,
target_type=target.target_type, target_type=target.target_type,
@@ -109,8 +109,8 @@ def _target_to_response(target) -> PictureTargetResponse:
created_at=target.created_at, created_at=target.created_at,
updated_at=target.updated_at, updated_at=target.updated_at,
) )
elif isinstance(target, KeyColorsPictureTarget): elif isinstance(target, KeyColorsOutputTarget):
return PictureTargetResponse( return OutputTargetResponse(
id=target.id, id=target.id,
name=target.name, name=target.name,
target_type=target.target_type, target_type=target.target_type,
@@ -122,7 +122,7 @@ def _target_to_response(target) -> PictureTargetResponse:
updated_at=target.updated_at, updated_at=target.updated_at,
) )
else: else:
return PictureTargetResponse( return OutputTargetResponse(
id=target.id, id=target.id,
name=target.name, name=target.name,
target_type=target.target_type, target_type=target.target_type,
@@ -135,15 +135,15 @@ def _target_to_response(target) -> PictureTargetResponse:
# ===== CRUD ENDPOINTS ===== # ===== CRUD ENDPOINTS =====
@router.post("/api/v1/picture-targets", response_model=PictureTargetResponse, tags=["Targets"], status_code=201) @router.post("/api/v1/output-targets", response_model=OutputTargetResponse, tags=["Targets"], status_code=201)
async def create_target( async def create_target(
data: PictureTargetCreate, data: OutputTargetCreate,
_auth: AuthRequired, _auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
device_store: DeviceStore = Depends(get_device_store), device_store: DeviceStore = Depends(get_device_store),
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Create a new picture target.""" """Create a new output target."""
try: try:
# Validate device exists if provided # Validate device exists if provided
if data.device_id: if data.device_id:
@@ -188,18 +188,18 @@ async def create_target(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/v1/picture-targets", response_model=PictureTargetListResponse, tags=["Targets"]) @router.get("/api/v1/output-targets", response_model=OutputTargetListResponse, tags=["Targets"])
async def list_targets( async def list_targets(
_auth: AuthRequired, _auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
): ):
"""List all picture targets.""" """List all output targets."""
targets = target_store.get_all_targets() targets = target_store.get_all_targets()
responses = [_target_to_response(t) for t in targets] responses = [_target_to_response(t) for t in targets]
return PictureTargetListResponse(targets=responses, count=len(responses)) return OutputTargetListResponse(targets=responses, count=len(responses))
@router.get("/api/v1/picture-targets/batch/states", tags=["Processing"]) @router.get("/api/v1/output-targets/batch/states", tags=["Processing"])
async def batch_target_states( async def batch_target_states(
_auth: AuthRequired, _auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
@@ -208,7 +208,7 @@ async def batch_target_states(
return {"states": manager.get_all_target_states()} return {"states": manager.get_all_target_states()}
@router.get("/api/v1/picture-targets/batch/metrics", tags=["Metrics"]) @router.get("/api/v1/output-targets/batch/metrics", tags=["Metrics"])
async def batch_target_metrics( async def batch_target_metrics(
_auth: AuthRequired, _auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
@@ -217,13 +217,13 @@ async def batch_target_metrics(
return {"metrics": manager.get_all_target_metrics()} return {"metrics": manager.get_all_target_metrics()}
@router.get("/api/v1/picture-targets/{target_id}", response_model=PictureTargetResponse, tags=["Targets"]) @router.get("/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"])
async def get_target( async def get_target(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
): ):
"""Get a picture target by ID.""" """Get a output target by ID."""
try: try:
target = target_store.get_target(target_id) target = target_store.get_target(target_id)
return _target_to_response(target) return _target_to_response(target)
@@ -231,16 +231,16 @@ async def get_target(
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
@router.put("/api/v1/picture-targets/{target_id}", response_model=PictureTargetResponse, tags=["Targets"]) @router.put("/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"])
async def update_target( async def update_target(
target_id: str, target_id: str,
data: PictureTargetUpdate, data: OutputTargetUpdate,
_auth: AuthRequired, _auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
device_store: DeviceStore = Depends(get_device_store), device_store: DeviceStore = Depends(get_device_store),
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Update a picture target.""" """Update a output target."""
try: try:
# Validate device exists if changing # Validate device exists if changing
if data.device_id is not None and data.device_id: if data.device_id is not None and data.device_id:
@@ -258,7 +258,7 @@ async def update_target(
except ValueError: except ValueError:
existing_target = None existing_target = None
if isinstance(existing_target, KeyColorsPictureTarget): if isinstance(existing_target, KeyColorsOutputTarget):
ex = existing_target.settings ex = existing_target.settings
merged = KeyColorsSettingsSchema( merged = KeyColorsSettingsSchema(
fps=incoming.get("fps", ex.fps), fps=incoming.get("fps", ex.fps),
@@ -325,14 +325,14 @@ async def update_target(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.delete("/api/v1/picture-targets/{target_id}", status_code=204, tags=["Targets"]) @router.delete("/api/v1/output-targets/{target_id}", status_code=204, tags=["Targets"])
async def delete_target( async def delete_target(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Delete a picture target. Stops processing first if active.""" """Delete a output target. Stops processing first if active."""
try: try:
# Stop processing if running # Stop processing if running
try: try:
@@ -360,14 +360,14 @@ async def delete_target(
# ===== PROCESSING CONTROL ENDPOINTS ===== # ===== PROCESSING CONTROL ENDPOINTS =====
@router.post("/api/v1/picture-targets/{target_id}/start", tags=["Processing"]) @router.post("/api/v1/output-targets/{target_id}/start", tags=["Processing"])
async def start_processing( async def start_processing(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Start processing for a picture target.""" """Start processing for a output target."""
try: try:
# Verify target exists in store # Verify target exists in store
target_store.get_target(target_id) target_store.get_target(target_id)
@@ -386,13 +386,13 @@ async def start_processing(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/v1/picture-targets/{target_id}/stop", tags=["Processing"]) @router.post("/api/v1/output-targets/{target_id}/stop", tags=["Processing"])
async def stop_processing( async def stop_processing(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Stop processing for a picture target.""" """Stop processing for a output target."""
try: try:
await manager.stop_processing(target_id) await manager.stop_processing(target_id)
@@ -408,7 +408,7 @@ async def stop_processing(
# ===== STATE & METRICS ENDPOINTS ===== # ===== STATE & METRICS ENDPOINTS =====
@router.get("/api/v1/picture-targets/{target_id}/state", response_model=TargetProcessingState, tags=["Processing"]) @router.get("/api/v1/output-targets/{target_id}/state", response_model=TargetProcessingState, tags=["Processing"])
async def get_target_state( async def get_target_state(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
@@ -426,7 +426,7 @@ async def get_target_state(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/v1/picture-targets/{target_id}/metrics", response_model=TargetMetricsResponse, tags=["Metrics"]) @router.get("/api/v1/output-targets/{target_id}/metrics", response_model=TargetMetricsResponse, tags=["Metrics"])
async def get_target_metrics( async def get_target_metrics(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
@@ -446,7 +446,7 @@ async def get_target_metrics(
# ===== KEY COLORS ENDPOINTS ===== # ===== KEY COLORS ENDPOINTS =====
@router.get("/api/v1/picture-targets/{target_id}/colors", response_model=KeyColorsResponse, tags=["Key Colors"]) @router.get("/api/v1/output-targets/{target_id}/colors", response_model=KeyColorsResponse, tags=["Key Colors"])
async def get_target_colors( async def get_target_colors(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
@@ -471,11 +471,11 @@ async def get_target_colors(
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
@router.post("/api/v1/picture-targets/{target_id}/test", response_model=KCTestResponse, tags=["Key Colors"]) @router.post("/api/v1/output-targets/{target_id}/test", response_model=KCTestResponse, tags=["Key Colors"])
async def test_kc_target( async def test_kc_target(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
source_store: PictureSourceStore = Depends(get_picture_source_store), source_store: PictureSourceStore = Depends(get_picture_source_store),
template_store: TemplateStore = Depends(get_template_store), template_store: TemplateStore = Depends(get_template_store),
pattern_store: PatternTemplateStore = Depends(get_pattern_template_store), pattern_store: PatternTemplateStore = Depends(get_pattern_template_store),
@@ -494,7 +494,7 @@ async def test_kc_target(
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
if not isinstance(target, KeyColorsPictureTarget): if not isinstance(target, KeyColorsOutputTarget):
raise HTTPException(status_code=400, detail="Target is not a key_colors target") raise HTTPException(status_code=400, detail="Target is not a key_colors target")
settings = target.settings settings = target.settings
@@ -670,7 +670,7 @@ async def test_kc_target(
logger.error(f"Error cleaning up test stream: {e}") logger.error(f"Error cleaning up test stream: {e}")
@router.websocket("/api/v1/picture-targets/{target_id}/ws") @router.websocket("/api/v1/output-targets/{target_id}/ws")
async def target_colors_ws( async def target_colors_ws(
websocket: WebSocket, websocket: WebSocket,
target_id: str, target_id: str,
@@ -710,7 +710,7 @@ async def target_colors_ws(
manager.remove_kc_ws_client(target_id, websocket) manager.remove_kc_ws_client(target_id, websocket)
@router.websocket("/api/v1/picture-targets/{target_id}/led-preview/ws") @router.websocket("/api/v1/output-targets/{target_id}/led-preview/ws")
async def led_preview_ws( async def led_preview_ws(
websocket: WebSocket, websocket: WebSocket,
target_id: str, target_id: str,
@@ -788,12 +788,12 @@ async def events_ws(
# ===== OVERLAY VISUALIZATION ===== # ===== OVERLAY VISUALIZATION =====
@router.post("/api/v1/picture-targets/{target_id}/overlay/start", tags=["Visualization"]) @router.post("/api/v1/output-targets/{target_id}/overlay/start", tags=["Visualization"])
async def start_target_overlay( async def start_target_overlay(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
color_strip_store: ColorStripStore = Depends(get_color_strip_store), color_strip_store: ColorStripStore = Depends(get_color_strip_store),
picture_source_store: PictureSourceStore = Depends(get_picture_source_store), picture_source_store: PictureSourceStore = Depends(get_picture_source_store),
): ):
@@ -815,7 +815,7 @@ async def start_target_overlay(
# can start even when processing is not currently running. # can start even when processing is not currently running.
calibration = None calibration = None
display_info = None display_info = None
if isinstance(target, WledPictureTarget) and target.color_strip_source_id: if isinstance(target, WledOutputTarget) and target.color_strip_source_id:
first_css_id = target.color_strip_source_id first_css_id = target.color_strip_source_id
if first_css_id: if first_css_id:
try: try:
@@ -844,7 +844,7 @@ async def start_target_overlay(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/v1/picture-targets/{target_id}/overlay/stop", tags=["Visualization"]) @router.post("/api/v1/output-targets/{target_id}/overlay/stop", tags=["Visualization"])
async def stop_target_overlay( async def stop_target_overlay(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,
@@ -862,7 +862,7 @@ async def stop_target_overlay(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/v1/picture-targets/{target_id}/overlay/status", tags=["Visualization"]) @router.get("/api/v1/output-targets/{target_id}/overlay/status", tags=["Visualization"])
async def get_overlay_status( async def get_overlay_status(
target_id: str, target_id: str,
_auth: AuthRequired, _auth: AuthRequired,

View File

@@ -5,7 +5,7 @@ from fastapi import APIRouter, HTTPException, Depends
from wled_controller.api.auth import AuthRequired from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import ( from wled_controller.api.dependencies import (
get_pattern_template_store, get_pattern_template_store,
get_picture_target_store, get_output_target_store,
) )
from wled_controller.api.schemas.pattern_templates import ( from wled_controller.api.schemas.pattern_templates import (
PatternTemplateCreate, PatternTemplateCreate,
@@ -13,10 +13,10 @@ from wled_controller.api.schemas.pattern_templates import (
PatternTemplateResponse, PatternTemplateResponse,
PatternTemplateUpdate, PatternTemplateUpdate,
) )
from wled_controller.api.schemas.picture_targets import KeyColorRectangleSchema from wled_controller.api.schemas.output_targets import KeyColorRectangleSchema
from wled_controller.storage.key_colors_picture_target import KeyColorRectangle from wled_controller.storage.key_colors_output_target import KeyColorRectangle
from wled_controller.storage.pattern_template_store import PatternTemplateStore from wled_controller.storage.pattern_template_store import PatternTemplateStore
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger from wled_controller.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -127,7 +127,7 @@ async def delete_pattern_template(
template_id: str, template_id: str,
_auth: AuthRequired, _auth: AuthRequired,
store: PatternTemplateStore = Depends(get_pattern_template_store), store: PatternTemplateStore = Depends(get_pattern_template_store),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
): ):
"""Delete a pattern template.""" """Delete a pattern template."""
try: try:

View File

@@ -13,7 +13,7 @@ from fastapi.responses import Response
from wled_controller.api.auth import AuthRequired from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import ( from wled_controller.api.dependencies import (
get_picture_source_store, get_picture_source_store,
get_picture_target_store, get_output_target_store,
get_pp_template_store, get_pp_template_store,
get_template_store, get_template_store,
) )
@@ -33,7 +33,7 @@ from wled_controller.api.schemas.picture_sources import (
) )
from wled_controller.core.capture_engines import EngineRegistry from wled_controller.core.capture_engines import EngineRegistry
from wled_controller.core.filters import FilterRegistry, ImagePool from wled_controller.core.filters import FilterRegistry, ImagePool
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.storage.template_store import TemplateStore from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore from wled_controller.storage.picture_source_store import PictureSourceStore
@@ -254,7 +254,7 @@ async def delete_picture_source(
stream_id: str, stream_id: str,
_auth: AuthRequired, _auth: AuthRequired,
store: PictureSourceStore = Depends(get_picture_source_store), store: PictureSourceStore = Depends(get_picture_source_store),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
): ):
"""Delete a picture source.""" """Delete a picture source."""
try: try:

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import ( from wled_controller.api.dependencies import (
get_picture_target_store, get_output_target_store,
get_processor_manager, get_processor_manager,
get_scene_preset_store, get_scene_preset_store,
) )
@@ -23,7 +23,7 @@ from wled_controller.core.scenes.scene_activator import (
apply_scene_state, apply_scene_state,
capture_current_snapshot, capture_current_snapshot,
) )
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.storage.scene_preset import ScenePreset from wled_controller.storage.scene_preset import ScenePreset
from wled_controller.storage.scene_preset_store import ScenePresetStore from wled_controller.storage.scene_preset_store import ScenePresetStore
from wled_controller.utils import get_logger from wled_controller.utils import get_logger
@@ -62,7 +62,7 @@ async def create_scene_preset(
data: ScenePresetCreate, data: ScenePresetCreate,
_auth: AuthRequired, _auth: AuthRequired,
store: ScenePresetStore = Depends(get_scene_preset_store), store: ScenePresetStore = Depends(get_scene_preset_store),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Capture current state as a new scene preset.""" """Capture current state as a new scene preset."""
@@ -176,7 +176,7 @@ async def recapture_scene_preset(
preset_id: str, preset_id: str,
_auth: AuthRequired, _auth: AuthRequired,
store: ScenePresetStore = Depends(get_scene_preset_store), store: ScenePresetStore = Depends(get_scene_preset_store),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Re-capture current state into an existing preset (updates snapshot).""" """Re-capture current state into an existing preset (updates snapshot)."""
@@ -214,7 +214,7 @@ async def activate_scene_preset(
preset_id: str, preset_id: str,
_auth: AuthRequired, _auth: AuthRequired,
store: ScenePresetStore = Depends(get_scene_preset_store), store: ScenePresetStore = Depends(get_scene_preset_store),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
manager: ProcessorManager = Depends(get_processor_manager), manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Activate a scene preset — restore the captured state.""" """Activate a scene preset — restore the captured state."""

View File

@@ -265,7 +265,7 @@ STORE_MAP = {
"capture_templates": "templates_file", "capture_templates": "templates_file",
"postprocessing_templates": "postprocessing_templates_file", "postprocessing_templates": "postprocessing_templates_file",
"picture_sources": "picture_sources_file", "picture_sources": "picture_sources_file",
"picture_targets": "picture_targets_file", "output_targets": "output_targets_file",
"pattern_templates": "pattern_templates_file", "pattern_templates": "pattern_templates_file",
"color_strip_sources": "color_strip_sources_file", "color_strip_sources": "color_strip_sources_file",
"audio_sources": "audio_sources_file", "audio_sources": "audio_sources_file",

View File

@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSock
from wled_controller.api.auth import AuthRequired from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import ( from wled_controller.api.dependencies import (
get_picture_target_store, get_output_target_store,
get_processor_manager, get_processor_manager,
get_value_source_store, get_value_source_store,
) )
@@ -21,7 +21,7 @@ from wled_controller.api.schemas.value_sources import (
) )
from wled_controller.storage.value_source import ValueSource from wled_controller.storage.value_source import ValueSource
from wled_controller.storage.value_source_store import ValueSourceStore from wled_controller.storage.value_source_store import ValueSourceStore
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.core.processing.processor_manager import ProcessorManager from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.utils import get_logger from wled_controller.utils import get_logger
@@ -157,14 +157,14 @@ async def delete_value_source(
source_id: str, source_id: str,
_auth: AuthRequired, _auth: AuthRequired,
store: ValueSourceStore = Depends(get_value_source_store), store: ValueSourceStore = Depends(get_value_source_store),
target_store: PictureTargetStore = Depends(get_picture_target_store), target_store: OutputTargetStore = Depends(get_output_target_store),
): ):
"""Delete a value source.""" """Delete a value source."""
try: try:
# Check if any targets reference this value source # Check if any targets reference this value source
from wled_controller.storage.wled_picture_target import WledPictureTarget from wled_controller.storage.wled_output_target import WledOutputTarget
for target in target_store.get_all_targets(): for target in target_store.get_all_targets():
if isinstance(target, WledPictureTarget): if isinstance(target, WledOutputTarget):
if getattr(target, "brightness_value_source_id", "") == source_id: if getattr(target, "brightness_value_source_id", "") == source_id:
raise ValueError( raise ValueError(
f"Cannot delete: referenced by target '{target.name}'" f"Cannot delete: referenced by target '{target.name}'"

View File

@@ -30,11 +30,11 @@ from .color_strip_sources import (
ColorStripSourceUpdate, ColorStripSourceUpdate,
CSSCalibrationTestRequest, CSSCalibrationTestRequest,
) )
from .picture_targets import ( from .output_targets import (
PictureTargetCreate, OutputTargetCreate,
PictureTargetListResponse, OutputTargetListResponse,
PictureTargetResponse, OutputTargetResponse,
PictureTargetUpdate, OutputTargetUpdate,
TargetMetricsResponse, TargetMetricsResponse,
TargetProcessingState, TargetProcessingState,
) )
@@ -100,10 +100,10 @@ __all__ = [
"ColorStripSourceResponse", "ColorStripSourceResponse",
"ColorStripSourceUpdate", "ColorStripSourceUpdate",
"CSSCalibrationTestRequest", "CSSCalibrationTestRequest",
"PictureTargetCreate", "OutputTargetCreate",
"PictureTargetListResponse", "OutputTargetListResponse",
"PictureTargetResponse", "OutputTargetResponse",
"PictureTargetUpdate", "OutputTargetUpdate",
"TargetMetricsResponse", "TargetMetricsResponse",
"TargetProcessingState", "TargetProcessingState",
"EngineInfo", "EngineInfo",

View File

@@ -1,4 +1,4 @@
"""Picture target schemas (CRUD, processing state, metrics).""" """Output target schemas (CRUD, processing state, metrics)."""
from datetime import datetime from datetime import datetime
from typing import Dict, Optional, List from typing import Dict, Optional, List
@@ -46,8 +46,8 @@ class KeyColorsResponse(BaseModel):
timestamp: Optional[datetime] = Field(None, description="Extraction timestamp") timestamp: Optional[datetime] = Field(None, description="Extraction timestamp")
class PictureTargetCreate(BaseModel): class OutputTargetCreate(BaseModel):
"""Request to create a picture target.""" """Request to create an output target."""
name: str = Field(description="Target name", min_length=1, max_length=100) name: str = Field(description="Target name", min_length=1, max_length=100)
target_type: str = Field(default="led", description="Target type (led, key_colors)") target_type: str = Field(default="led", description="Target type (led, key_colors)")
@@ -67,8 +67,8 @@ class PictureTargetCreate(BaseModel):
description: Optional[str] = Field(None, description="Optional description", max_length=500) description: Optional[str] = Field(None, description="Optional description", max_length=500)
class PictureTargetUpdate(BaseModel): class OutputTargetUpdate(BaseModel):
"""Request to update a picture target.""" """Request to update an output target."""
name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100) name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100)
# LED target fields # LED target fields
@@ -87,8 +87,8 @@ class PictureTargetUpdate(BaseModel):
description: Optional[str] = Field(None, description="Optional description", max_length=500) description: Optional[str] = Field(None, description="Optional description", max_length=500)
class PictureTargetResponse(BaseModel): class OutputTargetResponse(BaseModel):
"""Picture target response.""" """Output target response."""
id: str = Field(description="Target ID") id: str = Field(description="Target ID")
name: str = Field(description="Target name") name: str = Field(description="Target name")
@@ -111,15 +111,15 @@ class PictureTargetResponse(BaseModel):
updated_at: datetime = Field(description="Last update timestamp") updated_at: datetime = Field(description="Last update timestamp")
class PictureTargetListResponse(BaseModel): class OutputTargetListResponse(BaseModel):
"""List of picture targets response.""" """List of output targets response."""
targets: List[PictureTargetResponse] = Field(description="List of picture targets") targets: List[OutputTargetResponse] = Field(description="List of output targets")
count: int = Field(description="Number of targets") count: int = Field(description="Number of targets")
class TargetProcessingState(BaseModel): class TargetProcessingState(BaseModel):
"""Processing state for a picture target.""" """Processing state for an output target."""
target_id: str = Field(description="Target ID") target_id: str = Field(description="Target ID")
device_id: Optional[str] = Field(None, description="Device ID") device_id: Optional[str] = Field(None, description="Device ID")

View File

@@ -5,7 +5,7 @@ from typing import List, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from .picture_targets import KeyColorRectangleSchema from .output_targets import KeyColorRectangleSchema
class PatternTemplateCreate(BaseModel): class PatternTemplateCreate(BaseModel):

View File

@@ -31,7 +31,7 @@ class StorageConfig(BaseSettings):
templates_file: str = "data/capture_templates.json" templates_file: str = "data/capture_templates.json"
postprocessing_templates_file: str = "data/postprocessing_templates.json" postprocessing_templates_file: str = "data/postprocessing_templates.json"
picture_sources_file: str = "data/picture_sources.json" picture_sources_file: str = "data/picture_sources.json"
picture_targets_file: str = "data/picture_targets.json" output_targets_file: str = "data/output_targets.json"
pattern_templates_file: str = "data/pattern_templates.json" pattern_templates_file: str = "data/pattern_templates.json"
color_strip_sources_file: str = "data/color_strip_sources.json" color_strip_sources_file: str = "data/color_strip_sources.json"
audio_sources_file: str = "data/audio_sources.json" audio_sources_file: str = "data/audio_sources.json"

View File

@@ -1,7 +1,7 @@
"""Abstract base class for target processors. """Abstract base class for target processors.
A TargetProcessor encapsulates the processing loop and state for a single A TargetProcessor encapsulates the processing loop and state for a single
picture target. Concrete subclasses (WledTargetProcessor, KCTargetProcessor) output target. Concrete subclasses (WledTargetProcessor, KCTargetProcessor)
implement the target-specific capture→process→output pipeline. implement the target-specific capture→process→output pipeline.
ProcessorManager creates and owns TargetProcessor instances, delegating ProcessorManager creates and owns TargetProcessor instances, delegating

View File

@@ -6,7 +6,7 @@ These functions are used by both the scene-presets API route and the automation
from typing import List, Optional, Set, Tuple from typing import List, Optional, Set, Tuple
from wled_controller.core.processing.processor_manager import ProcessorManager from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.storage.scene_preset import ( from wled_controller.storage.scene_preset import (
ScenePreset, ScenePreset,
TargetSnapshot, TargetSnapshot,
@@ -17,7 +17,7 @@ logger = get_logger(__name__)
def capture_current_snapshot( def capture_current_snapshot(
target_store: PictureTargetStore, target_store: OutputTargetStore,
processor_manager: ProcessorManager, processor_manager: ProcessorManager,
target_ids: Optional[Set[str]] = None, target_ids: Optional[Set[str]] = None,
) -> List[TargetSnapshot]: ) -> List[TargetSnapshot]:
@@ -45,7 +45,7 @@ def capture_current_snapshot(
async def apply_scene_state( async def apply_scene_state(
preset: ScenePreset, preset: ScenePreset,
target_store: PictureTargetStore, target_store: OutputTargetStore,
processor_manager: ProcessorManager, processor_manager: ProcessorManager,
) -> Tuple[str, List[str]]: ) -> Tuple[str, List[str]]:
"""Apply a scene preset's state to the system. """Apply a scene preset's state to the system.

View File

@@ -21,7 +21,7 @@ from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.pattern_template_store import PatternTemplateStore from wled_controller.storage.pattern_template_store import PatternTemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.picture_target_store import PictureTargetStore from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.storage.color_strip_store import ColorStripStore from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage.audio_source_store import AudioSourceStore from wled_controller.storage.audio_source_store import AudioSourceStore
from wled_controller.storage.audio_template_store import AudioTemplateStore from wled_controller.storage.audio_template_store import AudioTemplateStore
@@ -50,7 +50,7 @@ device_store = DeviceStore(config.storage.devices_file)
template_store = TemplateStore(config.storage.templates_file) template_store = TemplateStore(config.storage.templates_file)
pp_template_store = PostprocessingTemplateStore(config.storage.postprocessing_templates_file) pp_template_store = PostprocessingTemplateStore(config.storage.postprocessing_templates_file)
picture_source_store = PictureSourceStore(config.storage.picture_sources_file) picture_source_store = PictureSourceStore(config.storage.picture_sources_file)
picture_target_store = PictureTargetStore(config.storage.picture_targets_file) output_target_store = OutputTargetStore(config.storage.output_targets_file)
pattern_template_store = PatternTemplateStore(config.storage.pattern_templates_file) pattern_template_store = PatternTemplateStore(config.storage.pattern_templates_file)
color_strip_store = ColorStripStore(config.storage.color_strip_sources_file) color_strip_store = ColorStripStore(config.storage.color_strip_sources_file)
audio_source_store = AudioSourceStore(config.storage.audio_sources_file) audio_source_store = AudioSourceStore(config.storage.audio_sources_file)
@@ -113,7 +113,7 @@ async def lifespan(app: FastAPI):
automation_store, processor_manager, automation_store, processor_manager,
mqtt_service=mqtt_service, mqtt_service=mqtt_service,
scene_preset_store=scene_preset_store, scene_preset_store=scene_preset_store,
target_store=picture_target_store, target_store=output_target_store,
device_store=device_store, device_store=device_store,
) )
@@ -131,7 +131,7 @@ async def lifespan(app: FastAPI):
pp_template_store=pp_template_store, pp_template_store=pp_template_store,
pattern_template_store=pattern_template_store, pattern_template_store=pattern_template_store,
picture_source_store=picture_source_store, picture_source_store=picture_source_store,
picture_target_store=picture_target_store, output_target_store=output_target_store,
color_strip_store=color_strip_store, color_strip_store=color_strip_store,
audio_source_store=audio_source_store, audio_source_store=audio_source_store,
audio_template_store=audio_template_store, audio_template_store=audio_template_store,
@@ -163,8 +163,8 @@ async def lifespan(app: FastAPI):
logger.info(f"Registered {len(devices)} devices for health monitoring") logger.info(f"Registered {len(devices)} devices for health monitoring")
# Register picture targets in processor manager # Register output targets in processor manager
targets = picture_target_store.get_all_targets() targets = output_target_store.get_all_targets()
registered_targets = 0 registered_targets = 0
for target in targets: for target in targets:
try: try:
@@ -174,7 +174,7 @@ async def lifespan(app: FastAPI):
except Exception as e: except Exception as e:
logger.error(f"Failed to register target {target.id}: {e}") logger.error(f"Failed to register target {target.id}: {e}")
logger.info(f"Registered {registered_targets} picture target(s)") logger.info(f"Registered {registered_targets} output target(s)")
# Start background health monitoring for all devices # Start background health monitoring for all devices
await processor_manager.start_health_monitoring() await processor_manager.start_health_monitoring()

View File

@@ -112,7 +112,7 @@ function _buildItems(results, states = {}) {
// Maps endpoint → response key that holds the array // Maps endpoint → response key that holds the array
const _responseKeys = [ const _responseKeys = [
['/devices', 'devices'], ['/devices', 'devices'],
['/picture-targets', 'targets'], ['/output-targets', 'targets'],
['/color-strip-sources', 'sources'], ['/color-strip-sources', 'sources'],
['/automations', 'automations'], ['/automations', 'automations'],
['/capture-templates', 'templates'], ['/capture-templates', 'templates'],
@@ -126,7 +126,7 @@ const _responseKeys = [
async function _fetchAllEntities() { async function _fetchAllEntities() {
const [statesData, ...results] = await Promise.all([ const [statesData, ...results] = await Promise.all([
fetchWithAuth('/picture-targets/batch/states', { retry: false, timeout: 5000 }) fetchWithAuth('/output-targets/batch/states', { retry: false, timeout: 5000 })
.then(r => r.ok ? r.json() : {}) .then(r => r.ok ? r.json() : {})
.then(data => data.states || {}) .then(data => data.states || {})
.catch(() => ({})), .catch(() => ({})),

View File

@@ -419,12 +419,12 @@ export async function loadDashboard(forceFullRender = false) {
try { try {
// Fire all requests in a single batch to avoid sequential RTTs // Fire all requests in a single batch to avoid sequential RTTs
const [targetsResp, automationsResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp, scenePresets, syncClocksResp] = await Promise.all([ const [targetsResp, automationsResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp, scenePresets, syncClocksResp] = await Promise.all([
fetchWithAuth('/picture-targets'), fetchWithAuth('/output-targets'),
fetchWithAuth('/automations').catch(() => null), fetchWithAuth('/automations').catch(() => null),
fetchWithAuth('/devices').catch(() => null), fetchWithAuth('/devices').catch(() => null),
fetchWithAuth('/color-strip-sources').catch(() => null), fetchWithAuth('/color-strip-sources').catch(() => null),
fetchWithAuth('/picture-targets/batch/states').catch(() => null), fetchWithAuth('/output-targets/batch/states').catch(() => null),
fetchWithAuth('/picture-targets/batch/metrics').catch(() => null), fetchWithAuth('/output-targets/batch/metrics').catch(() => null),
loadScenePresets(), loadScenePresets(),
fetchWithAuth('/sync-clocks').catch(() => null), fetchWithAuth('/sync-clocks').catch(() => null),
]); ]);
@@ -746,7 +746,7 @@ export async function dashboardToggleAutomation(automationId, enable) {
export async function dashboardStartTarget(targetId) { export async function dashboardStartTarget(targetId) {
try { try {
const response = await fetchWithAuth(`/picture-targets/${targetId}/start`, { const response = await fetchWithAuth(`/output-targets/${targetId}/start`, {
method: 'POST', method: 'POST',
}); });
if (response.ok) { if (response.ok) {
@@ -764,7 +764,7 @@ export async function dashboardStartTarget(targetId) {
export async function dashboardStopTarget(targetId) { export async function dashboardStopTarget(targetId) {
try { try {
const response = await fetchWithAuth(`/picture-targets/${targetId}/stop`, { const response = await fetchWithAuth(`/output-targets/${targetId}/stop`, {
method: 'POST', method: 'POST',
}); });
if (response.ok) { if (response.ok) {
@@ -783,15 +783,15 @@ export async function dashboardStopTarget(targetId) {
export async function dashboardStopAll() { export async function dashboardStopAll() {
try { try {
const [targetsResp, statesResp] = await Promise.all([ const [targetsResp, statesResp] = await Promise.all([
fetchWithAuth('/picture-targets'), fetchWithAuth('/output-targets'),
fetchWithAuth('/picture-targets/batch/states'), fetchWithAuth('/output-targets/batch/states'),
]); ]);
const data = await targetsResp.json(); const data = await targetsResp.json();
const statesData = statesResp.ok ? await statesResp.json() : { states: {} }; const statesData = statesResp.ok ? await statesResp.json() : { states: {} };
const states = statesData.states || {}; const states = statesData.states || {};
const running = (data.targets || []).filter(t => states[t.id]?.processing); const running = (data.targets || []).filter(t => states[t.id]?.processing);
await Promise.all(running.map(t => await Promise.all(running.map(t =>
fetchWithAuth(`/picture-targets/${t.id}/stop`, { method: 'POST' }).catch(() => {}) fetchWithAuth(`/output-targets/${t.id}/stop`, { method: 'POST' }).catch(() => {})
)); ));
loadDashboard(); loadDashboard();
} catch (error) { } catch (error) {

View File

@@ -300,7 +300,7 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
// ===== KEY COLORS TEST ===== // ===== KEY COLORS TEST =====
export async function fetchKCTest(targetId) { export async function fetchKCTest(targetId) {
const response = await fetch(`${API_BASE}/picture-targets/${targetId}/test`, { const response = await fetch(`${API_BASE}/output-targets/${targetId}/test`, {
method: 'POST', method: 'POST',
headers: getHeaders(), headers: getHeaders(),
}); });
@@ -539,7 +539,7 @@ export async function showKCEditor(targetId = null, cloneData = null) {
_ensurePatternEntitySelect(patTemplates); _ensurePatternEntitySelect(patTemplates);
if (targetId) { if (targetId) {
const resp = await fetch(`${API_BASE}/picture-targets/${targetId}`, { headers: getHeaders() }); const resp = await fetch(`${API_BASE}/output-targets/${targetId}`, { headers: getHeaders() });
if (!resp.ok) throw new Error('Failed to load target'); if (!resp.ok) throw new Error('Failed to load target');
const target = await resp.json(); const target = await resp.json();
const kcSettings = target.key_colors_settings || {}; const kcSettings = target.key_colors_settings || {};
@@ -653,13 +653,13 @@ export async function saveKCEditor() {
try { try {
let response; let response;
if (targetId) { if (targetId) {
response = await fetchWithAuth(`/picture-targets/${targetId}`, { response = await fetchWithAuth(`/output-targets/${targetId}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
} else { } else {
payload.target_type = 'key_colors'; payload.target_type = 'key_colors';
response = await fetchWithAuth('/picture-targets', { response = await fetchWithAuth('/output-targets', {
method: 'POST', method: 'POST',
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@@ -683,7 +683,7 @@ export async function saveKCEditor() {
export async function cloneKCTarget(targetId) { export async function cloneKCTarget(targetId) {
try { try {
const resp = await fetchWithAuth(`/picture-targets/${targetId}`); const resp = await fetchWithAuth(`/output-targets/${targetId}`);
if (!resp.ok) throw new Error('Failed to load target'); if (!resp.ok) throw new Error('Failed to load target');
const target = await resp.json(); const target = await resp.json();
showKCEditor(null, target); showKCEditor(null, target);
@@ -699,7 +699,7 @@ export async function deleteKCTarget(targetId) {
try { try {
disconnectKCWebSocket(targetId); disconnectKCWebSocket(targetId);
const response = await fetchWithAuth(`/picture-targets/${targetId}`, { const response = await fetchWithAuth(`/output-targets/${targetId}`, {
method: 'DELETE', method: 'DELETE',
}); });
if (response.ok) { if (response.ok) {
@@ -726,7 +726,7 @@ export function updateKCBrightnessLabel(targetId, value) {
export async function saveKCBrightness(targetId, value) { export async function saveKCBrightness(targetId, value) {
const brightness = parseInt(value) / 255; const brightness = parseInt(value) / 255;
try { try {
await fetch(`${API_BASE}/picture-targets/${targetId}`, { await fetch(`${API_BASE}/output-targets/${targetId}`, {
method: 'PUT', method: 'PUT',
headers: getHeaders(), headers: getHeaders(),
body: JSON.stringify({ key_colors_settings: { brightness } }), body: JSON.stringify({ key_colors_settings: { brightness } }),
@@ -747,7 +747,7 @@ export function connectKCWebSocket(targetId) {
if (!key) return; if (!key) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}${API_BASE}/picture-targets/${targetId}/ws?token=${encodeURIComponent(key)}`; const wsUrl = `${protocol}//${window.location.host}${API_BASE}/output-targets/${targetId}/ws?token=${encodeURIComponent(key)}`;
try { try {
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);

View File

@@ -128,7 +128,7 @@ export async function openScenePresetCapture() {
selectorGroup.style.display = ''; selectorGroup.style.display = '';
targetList.innerHTML = ''; targetList.innerHTML = '';
try { try {
const resp = await fetchWithAuth('/picture-targets'); const resp = await fetchWithAuth('/output-targets');
if (resp.ok) { if (resp.ok) {
const data = await resp.json(); const data = await resp.json();
_allTargets = data.targets || []; _allTargets = data.targets || [];
@@ -328,7 +328,7 @@ export async function cloneScenePreset(presetId) {
selectorGroup.style.display = ''; selectorGroup.style.display = '';
targetList.innerHTML = ''; targetList.innerHTML = '';
try { try {
const resp = await fetchWithAuth('/picture-targets'); const resp = await fetchWithAuth('/output-targets');
if (resp.ok) { if (resp.ok) {
const data = await resp.json(); const data = await resp.json();
_allTargets = data.targets || []; _allTargets = data.targets || [];

View File

@@ -337,7 +337,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
if (targetId) { if (targetId) {
// Editing existing target // Editing existing target
const resp = await fetch(`${API_BASE}/picture-targets/${targetId}`, { headers: getHeaders() }); const resp = await fetch(`${API_BASE}/output-targets/${targetId}`, { headers: getHeaders() });
if (!resp.ok) throw new Error('Failed to load target'); if (!resp.ok) throw new Error('Failed to load target');
const target = await resp.json(); const target = await resp.json();
@@ -478,13 +478,13 @@ export async function saveTargetEditor() {
try { try {
let response; let response;
if (targetId) { if (targetId) {
response = await fetchWithAuth(`/picture-targets/${targetId}`, { response = await fetchWithAuth(`/output-targets/${targetId}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
} else { } else {
payload.target_type = 'led'; payload.target_type = 'led';
response = await fetchWithAuth('/picture-targets', { response = await fetchWithAuth('/output-targets', {
method: 'POST', method: 'POST',
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@@ -550,7 +550,7 @@ export async function loadTargetsTab() {
// use DataCache for picture sources, audio sources, value sources, sync clocks // use DataCache for picture sources, audio sources, value sources, sync clocks
const [devicesResp, targetsResp, cssResp, patResp, psArr, valueSrcArr, asSrcArr] = await Promise.all([ const [devicesResp, targetsResp, cssResp, patResp, psArr, valueSrcArr, asSrcArr] = await Promise.all([
fetchWithAuth('/devices'), fetchWithAuth('/devices'),
fetchWithAuth('/picture-targets'), fetchWithAuth('/output-targets'),
fetchWithAuth('/color-strip-sources').catch(() => null), fetchWithAuth('/color-strip-sources').catch(() => null),
fetchWithAuth('/pattern-templates').catch(() => null), fetchWithAuth('/pattern-templates').catch(() => null),
streamsCache.fetch().catch(() => []), streamsCache.fetch().catch(() => []),
@@ -591,8 +591,8 @@ export async function loadTargetsTab() {
// Fetch all device states, target states, and target metrics in batch // Fetch all device states, target states, and target metrics in batch
const [batchDevStatesResp, batchTgtStatesResp, batchTgtMetricsResp] = await Promise.all([ const [batchDevStatesResp, batchTgtStatesResp, batchTgtMetricsResp] = await Promise.all([
fetchWithAuth('/devices/batch/states'), fetchWithAuth('/devices/batch/states'),
fetchWithAuth('/picture-targets/batch/states'), fetchWithAuth('/output-targets/batch/states'),
fetchWithAuth('/picture-targets/batch/metrics'), fetchWithAuth('/output-targets/batch/metrics'),
]); ]);
const allDeviceStates = batchDevStatesResp.ok ? (await batchDevStatesResp.json()).states : {}; const allDeviceStates = batchDevStatesResp.ok ? (await batchDevStatesResp.json()).states : {};
const allTargetStates = batchTgtStatesResp.ok ? (await batchTgtStatesResp.json()).states : {}; const allTargetStates = batchTgtStatesResp.ok ? (await batchTgtStatesResp.json()).states : {};
@@ -608,7 +608,7 @@ export async function loadTargetsTab() {
let latestColors = null; let latestColors = null;
if (target.target_type === 'key_colors' && state.processing) { if (target.target_type === 'key_colors' && state.processing) {
try { try {
const colorsResp = await fetch(`${API_BASE}/picture-targets/${target.id}/colors`, { headers: getHeaders() }); const colorsResp = await fetch(`${API_BASE}/output-targets/${target.id}/colors`, { headers: getHeaders() });
if (colorsResp.ok) latestColors = await colorsResp.json(); if (colorsResp.ok) latestColors = await colorsResp.json();
} catch {} } catch {}
} }
@@ -1046,7 +1046,7 @@ async function _targetAction(action) {
export async function startTargetProcessing(targetId) { export async function startTargetProcessing(targetId) {
await _targetAction(async () => { await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/start`, { const response = await fetchWithAuth(`/output-targets/${targetId}/start`, {
method: 'POST', method: 'POST',
}); });
if (response.ok) { if (response.ok) {
@@ -1060,7 +1060,7 @@ export async function startTargetProcessing(targetId) {
export async function stopTargetProcessing(targetId) { export async function stopTargetProcessing(targetId) {
await _targetAction(async () => { await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/stop`, { const response = await fetchWithAuth(`/output-targets/${targetId}/stop`, {
method: 'POST', method: 'POST',
}); });
if (response.ok) { if (response.ok) {
@@ -1083,8 +1083,8 @@ export async function stopAllKCTargets() {
async function _stopAllByType(targetType) { async function _stopAllByType(targetType) {
try { try {
const [targetsResp, statesResp] = await Promise.all([ const [targetsResp, statesResp] = await Promise.all([
fetchWithAuth('/picture-targets'), fetchWithAuth('/output-targets'),
fetchWithAuth('/picture-targets/batch/states'), fetchWithAuth('/output-targets/batch/states'),
]); ]);
const data = await targetsResp.json(); const data = await targetsResp.json();
const statesData = statesResp.ok ? await statesResp.json() : { states: {} }; const statesData = statesResp.ok ? await statesResp.json() : { states: {} };
@@ -1096,7 +1096,7 @@ async function _stopAllByType(targetType) {
return; return;
} }
await Promise.all(running.map(t => await Promise.all(running.map(t =>
fetchWithAuth(`/picture-targets/${t.id}/stop`, { method: 'POST' }).catch(() => {}) fetchWithAuth(`/output-targets/${t.id}/stop`, { method: 'POST' }).catch(() => {})
)); ));
showToast(t('targets.stop_all.stopped', { count: running.length }), 'success'); showToast(t('targets.stop_all.stopped', { count: running.length }), 'success');
loadTargetsTab(); loadTargetsTab();
@@ -1108,7 +1108,7 @@ async function _stopAllByType(targetType) {
export async function startTargetOverlay(targetId) { export async function startTargetOverlay(targetId) {
await _targetAction(async () => { await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/start`, { const response = await fetchWithAuth(`/output-targets/${targetId}/overlay/start`, {
method: 'POST', method: 'POST',
}); });
if (response.ok) { if (response.ok) {
@@ -1122,7 +1122,7 @@ export async function startTargetOverlay(targetId) {
export async function stopTargetOverlay(targetId) { export async function stopTargetOverlay(targetId) {
await _targetAction(async () => { await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/stop`, { const response = await fetchWithAuth(`/output-targets/${targetId}/overlay/stop`, {
method: 'POST', method: 'POST',
}); });
if (response.ok) { if (response.ok) {
@@ -1136,7 +1136,7 @@ export async function stopTargetOverlay(targetId) {
export async function cloneTarget(targetId) { export async function cloneTarget(targetId) {
try { try {
const resp = await fetch(`${API_BASE}/picture-targets/${targetId}`, { headers: getHeaders() }); const resp = await fetch(`${API_BASE}/output-targets/${targetId}`, { headers: getHeaders() });
if (!resp.ok) throw new Error('Failed to load target'); if (!resp.ok) throw new Error('Failed to load target');
const target = await resp.json(); const target = await resp.json();
showTargetEditor(null, target); showTargetEditor(null, target);
@@ -1151,7 +1151,7 @@ export async function deleteTarget(targetId) {
if (!confirmed) return; if (!confirmed) return;
await _targetAction(async () => { await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}`, { const response = await fetchWithAuth(`/output-targets/${targetId}`, {
method: 'DELETE', method: 'DELETE',
}); });
if (response.ok) { if (response.ok) {
@@ -1282,7 +1282,7 @@ function connectLedPreviewWS(targetId) {
if (!key) return; if (!key) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}${API_BASE}/picture-targets/${targetId}/led-preview/ws?token=${encodeURIComponent(key)}`; const wsUrl = `${protocol}//${window.location.host}${API_BASE}/output-targets/${targetId}/led-preview/ws?token=${encodeURIComponent(key)}`;
try { try {
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);

View File

@@ -7,7 +7,7 @@
* - Navigation: network-first with offline fallback * - Navigation: network-first with offline fallback
*/ */
const CACHE_NAME = 'ledgrab-v23'; const CACHE_NAME = 'ledgrab-v24';
// Only pre-cache static assets (no auth required). // Only pre-cache static assets (no auth required).
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page. // Do NOT pre-cache '/' — it requires API key auth and would cache an error page.

View File

@@ -16,7 +16,7 @@ class Device:
A device holds connection state and output settings. A device holds connection state and output settings.
Calibration, processing settings, and picture source assignments Calibration, processing settings, and picture source assignments
now live on ColorStripSource and WledPictureTarget respectively. now live on ColorStripSource and WledOutputTarget respectively.
""" """
def __init__( def __init__(

View File

@@ -1,10 +1,10 @@
"""Key colors picture target — extracts key colors from image rectangles.""" """Key colors output target — extracts key colors from image rectangles."""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from wled_controller.storage.picture_target import PictureTarget from wled_controller.storage.output_target import OutputTarget
@dataclass @dataclass
@@ -71,7 +71,7 @@ class KeyColorsSettings:
@dataclass @dataclass
class KeyColorsPictureTarget(PictureTarget): class KeyColorsOutputTarget(OutputTarget):
"""Key colors extractor target — extracts key colors from image rectangles.""" """Key colors extractor target — extracts key colors from image rectangles."""
picture_source_id: str = "" picture_source_id: str = ""
@@ -119,7 +119,7 @@ class KeyColorsPictureTarget(PictureTarget):
return d return d
@classmethod @classmethod
def from_dict(cls, data: dict) -> "KeyColorsPictureTarget": def from_dict(cls, data: dict) -> "KeyColorsOutputTarget":
settings_data = data.get("settings", {}) settings_data = data.get("settings", {})
settings = KeyColorsSettings.from_dict(settings_data) settings = KeyColorsSettings.from_dict(settings_data)

View File

@@ -1,4 +1,4 @@
"""Picture target base data model.""" """Output target base data model."""
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
@@ -6,8 +6,8 @@ from typing import Optional
@dataclass @dataclass
class PictureTarget: class OutputTarget:
"""Base class for picture targets.""" """Base class for output targets."""
id: str id: str
name: str name: str
@@ -50,13 +50,13 @@ class PictureTarget:
} }
@classmethod @classmethod
def from_dict(cls, data: dict) -> "PictureTarget": def from_dict(cls, data: dict) -> "OutputTarget":
"""Create from dictionary, dispatching to the correct subclass.""" """Create from dictionary, dispatching to the correct subclass."""
target_type = data.get("target_type", "led") target_type = data.get("target_type", "led")
if target_type == "led": if target_type == "led":
from wled_controller.storage.wled_picture_target import WledPictureTarget from wled_controller.storage.wled_output_target import WledOutputTarget
return WledPictureTarget.from_dict(data) return WledOutputTarget.from_dict(data)
if target_type == "key_colors": if target_type == "key_colors":
from wled_controller.storage.key_colors_picture_target import KeyColorsPictureTarget from wled_controller.storage.key_colors_output_target import KeyColorsOutputTarget
return KeyColorsPictureTarget.from_dict(data) return KeyColorsOutputTarget.from_dict(data)
raise ValueError(f"Unknown target type: {target_type}") raise ValueError(f"Unknown target type: {target_type}")

View File

@@ -1,4 +1,4 @@
"""Picture target storage using JSON files.""" """Output target storage using JSON files."""
import json import json
import uuid import uuid
@@ -6,11 +6,11 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from wled_controller.storage.picture_target import PictureTarget from wled_controller.storage.output_target import OutputTarget
from wled_controller.storage.wled_picture_target import WledPictureTarget from wled_controller.storage.wled_output_target import WledOutputTarget
from wled_controller.storage.key_colors_picture_target import ( from wled_controller.storage.key_colors_output_target import (
KeyColorsSettings, KeyColorsSettings,
KeyColorsPictureTarget, KeyColorsOutputTarget,
) )
from wled_controller.utils import atomic_write_json, get_logger from wled_controller.utils import atomic_write_json, get_logger
@@ -19,17 +19,17 @@ logger = get_logger(__name__)
DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds
class PictureTargetStore: class OutputTargetStore:
"""Persistent storage for picture targets.""" """Persistent storage for output targets."""
def __init__(self, file_path: str): def __init__(self, file_path: str):
"""Initialize picture target store. """Initialize output target store.
Args: Args:
file_path: Path to targets JSON file file_path: Path to targets JSON file
""" """
self.file_path = Path(file_path) self.file_path = Path(file_path)
self._targets: Dict[str, PictureTarget] = {} self._targets: Dict[str, OutputTarget] = {}
self._load() self._load()
def _load(self) -> None: def _load(self) -> None:
@@ -41,52 +41,53 @@ class PictureTargetStore:
with open(self.file_path, "r", encoding="utf-8") as f: with open(self.file_path, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
targets_data = data.get("picture_targets", {}) # Support both new "output_targets" and legacy "picture_targets" keys
targets_data = data.get("output_targets") or data.get("picture_targets", {})
loaded = 0 loaded = 0
for target_id, target_dict in targets_data.items(): for target_id, target_dict in targets_data.items():
try: try:
target = PictureTarget.from_dict(target_dict) target = OutputTarget.from_dict(target_dict)
self._targets[target_id] = target self._targets[target_id] = target
loaded += 1 loaded += 1
except Exception as e: except Exception as e:
logger.error(f"Failed to load picture target {target_id}: {e}", exc_info=True) logger.error(f"Failed to load output target {target_id}: {e}", exc_info=True)
if loaded > 0: if loaded > 0:
logger.info(f"Loaded {loaded} picture targets from storage") logger.info(f"Loaded {loaded} output targets from storage")
except Exception as e: except Exception as e:
logger.error(f"Failed to load picture targets from {self.file_path}: {e}") logger.error(f"Failed to load output targets from {self.file_path}: {e}")
raise raise
logger.info(f"Picture target store initialized with {len(self._targets)} targets") logger.info(f"Output target store initialized with {len(self._targets)} targets")
def _save(self) -> None: def _save(self) -> None:
"""Save all targets to file.""" """Save all targets to file."""
try: try:
data = { data = {
"version": "1.0.0", "version": "1.0.0",
"picture_targets": { "output_targets": {
target_id: target.to_dict() target_id: target.to_dict()
for target_id, target in self._targets.items() for target_id, target in self._targets.items()
}, },
} }
atomic_write_json(self.file_path, data) atomic_write_json(self.file_path, data)
except Exception as e: except Exception as e:
logger.error(f"Failed to save picture targets to {self.file_path}: {e}") logger.error(f"Failed to save output targets to {self.file_path}: {e}")
raise raise
def get_all_targets(self) -> List[PictureTarget]: def get_all_targets(self) -> List[OutputTarget]:
"""Get all picture targets.""" """Get all output targets."""
return list(self._targets.values()) return list(self._targets.values())
def get_target(self, target_id: str) -> PictureTarget: def get_target(self, target_id: str) -> OutputTarget:
"""Get target by ID. """Get target by ID.
Raises: Raises:
ValueError: If target not found ValueError: If target not found
""" """
if target_id not in self._targets: if target_id not in self._targets:
raise ValueError(f"Picture target not found: {target_id}") raise ValueError(f"Output target not found: {target_id}")
return self._targets[target_id] return self._targets[target_id]
def create_target( def create_target(
@@ -105,8 +106,8 @@ class PictureTargetStore:
key_colors_settings: Optional[KeyColorsSettings] = None, key_colors_settings: Optional[KeyColorsSettings] = None,
description: Optional[str] = None, description: Optional[str] = None,
picture_source_id: str = "", picture_source_id: str = "",
) -> PictureTarget: ) -> OutputTarget:
"""Create a new picture target. """Create a new output target.
Raises: Raises:
ValueError: If validation fails ValueError: If validation fails
@@ -117,13 +118,13 @@ class PictureTargetStore:
# Check for duplicate name # Check for duplicate name
for target in self._targets.values(): for target in self._targets.values():
if target.name == name: if target.name == name:
raise ValueError(f"Picture target with name '{name}' already exists") raise ValueError(f"Output target with name '{name}' already exists")
target_id = f"pt_{uuid.uuid4().hex[:8]}" target_id = f"pt_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow() now = datetime.utcnow()
if target_type == "led": if target_type == "led":
target: PictureTarget = WledPictureTarget( target: OutputTarget = WledOutputTarget(
id=target_id, id=target_id,
name=name, name=name,
target_type="led", target_type="led",
@@ -141,7 +142,7 @@ class PictureTargetStore:
updated_at=now, updated_at=now,
) )
elif target_type == "key_colors": elif target_type == "key_colors":
target = KeyColorsPictureTarget( target = KeyColorsOutputTarget(
id=target_id, id=target_id,
name=name, name=name,
target_type="key_colors", target_type="key_colors",
@@ -157,7 +158,7 @@ class PictureTargetStore:
self._targets[target_id] = target self._targets[target_id] = target
self._save() self._save()
logger.info(f"Created picture target: {name} ({target_id}, type={target_type})") logger.info(f"Created output target: {name} ({target_id}, type={target_type})")
return target return target
def update_target( def update_target(
@@ -175,14 +176,14 @@ class PictureTargetStore:
protocol: Optional[str] = None, protocol: Optional[str] = None,
key_colors_settings: Optional[KeyColorsSettings] = None, key_colors_settings: Optional[KeyColorsSettings] = None,
description: Optional[str] = None, description: Optional[str] = None,
) -> PictureTarget: ) -> OutputTarget:
"""Update a picture target. """Update an output target.
Raises: Raises:
ValueError: If target not found or validation fails ValueError: If target not found or validation fails
""" """
if target_id not in self._targets: if target_id not in self._targets:
raise ValueError(f"Picture target not found: {target_id}") raise ValueError(f"Output target not found: {target_id}")
target = self._targets[target_id] target = self._targets[target_id]
@@ -190,7 +191,7 @@ class PictureTargetStore:
# Check for duplicate name (exclude self) # Check for duplicate name (exclude self)
for other in self._targets.values(): for other in self._targets.values():
if other.id != target_id and other.name == name: if other.id != target_id and other.name == name:
raise ValueError(f"Picture target with name '{name}' already exists") raise ValueError(f"Output target with name '{name}' already exists")
target.update_fields( target.update_fields(
name=name, name=name,
@@ -210,42 +211,42 @@ class PictureTargetStore:
target.updated_at = datetime.utcnow() target.updated_at = datetime.utcnow()
self._save() self._save()
logger.info(f"Updated picture target: {target_id}") logger.info(f"Updated output target: {target_id}")
return target return target
def delete_target(self, target_id: str) -> None: def delete_target(self, target_id: str) -> None:
"""Delete a picture target. """Delete an output target.
Raises: Raises:
ValueError: If target not found ValueError: If target not found
""" """
if target_id not in self._targets: if target_id not in self._targets:
raise ValueError(f"Picture target not found: {target_id}") raise ValueError(f"Output target not found: {target_id}")
del self._targets[target_id] del self._targets[target_id]
self._save() self._save()
logger.info(f"Deleted picture target: {target_id}") logger.info(f"Deleted output target: {target_id}")
def get_targets_for_device(self, device_id: str) -> List[PictureTarget]: def get_targets_for_device(self, device_id: str) -> List[OutputTarget]:
"""Get all targets that reference a specific device.""" """Get all targets that reference a specific device."""
return [ return [
t for t in self._targets.values() t for t in self._targets.values()
if isinstance(t, WledPictureTarget) and t.device_id == device_id if isinstance(t, WledOutputTarget) and t.device_id == device_id
] ]
def get_targets_referencing_source(self, source_id: str) -> List[str]: def get_targets_referencing_source(self, source_id: str) -> List[str]:
"""Return names of KC targets that reference a picture source.""" """Return names of KC targets that reference a picture source."""
return [ return [
target.name for target in self._targets.values() target.name for target in self._targets.values()
if isinstance(target, KeyColorsPictureTarget) and target.picture_source_id == source_id if isinstance(target, KeyColorsOutputTarget) and target.picture_source_id == source_id
] ]
def get_targets_referencing_css(self, css_id: str) -> List[str]: def get_targets_referencing_css(self, css_id: str) -> List[str]:
"""Return names of LED targets that reference a color strip source.""" """Return names of LED targets that reference a color strip source."""
return [ return [
target.name for target in self._targets.values() target.name for target in self._targets.values()
if isinstance(target, WledPictureTarget) if isinstance(target, WledOutputTarget)
and target.color_strip_source_id == css_id and target.color_strip_source_id == css_id
] ]

View File

@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from wled_controller.storage.key_colors_picture_target import KeyColorRectangle from wled_controller.storage.key_colors_output_target import KeyColorRectangle
@dataclass @dataclass

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from wled_controller.storage.key_colors_picture_target import KeyColorRectangle from wled_controller.storage.key_colors_output_target import KeyColorRectangle
from wled_controller.storage.pattern_template import PatternTemplate from wled_controller.storage.pattern_template import PatternTemplate
from wled_controller.utils import atomic_write_json, get_logger from wled_controller.utils import atomic_write_json, get_logger
@@ -203,11 +203,11 @@ class PatternTemplateStore:
logger.info(f"Deleted pattern template: {template_id}") logger.info(f"Deleted pattern template: {template_id}")
def get_targets_referencing(self, template_id: str, picture_target_store) -> List[str]: def get_targets_referencing(self, template_id: str, output_target_store) -> List[str]:
"""Return names of KC targets that reference this template.""" """Return names of KC targets that reference this template."""
from wled_controller.storage.key_colors_picture_target import KeyColorsPictureTarget from wled_controller.storage.key_colors_output_target import KeyColorsOutputTarget
return [ return [
target.name for target in picture_target_store.get_all_targets() target.name for target in output_target_store.get_all_targets()
if isinstance(target, KeyColorsPictureTarget) and target.settings.pattern_template_id == template_id if isinstance(target, KeyColorsOutputTarget) and target.settings.pattern_template_id == template_id
] ]

View File

@@ -1,17 +1,17 @@
"""LED picture target — sends color strip sources to an LED device.""" """LED output target — sends color strip sources to an LED device."""
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from wled_controller.storage.picture_target import PictureTarget from wled_controller.storage.output_target import OutputTarget
DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds
@dataclass @dataclass
class WledPictureTarget(PictureTarget): class WledOutputTarget(OutputTarget):
"""LED picture target — pairs an LED device with a ColorStripSource.""" """LED output target — pairs an LED device with a ColorStripSource."""
device_id: str = "" device_id: str = ""
color_strip_source_id: str = "" color_strip_source_id: str = ""
@@ -104,7 +104,7 @@ class WledPictureTarget(PictureTarget):
return d return d
@classmethod @classmethod
def from_dict(cls, data: dict) -> "WledPictureTarget": def from_dict(cls, data: dict) -> "WledOutputTarget":
"""Create from dictionary.""" """Create from dictionary."""
return cls( return cls(
id=data["id"], id=data["id"],

View File

@@ -253,7 +253,7 @@ def test_get_target_metrics(processor_manager):
def test_target_type_detection(processor_manager): def test_target_type_detection(processor_manager):
"""Test target type detection via processor instances.""" """Test target type detection via processor instances."""
from wled_controller.storage.key_colors_picture_target import KeyColorsSettings from wled_controller.storage.key_colors_output_target import KeyColorsSettings
from wled_controller.core.processing.kc_target_processor import KCTargetProcessor from wled_controller.core.processing.kc_target_processor import KCTargetProcessor
from wled_controller.core.processing.wled_target_processor import WledTargetProcessor from wled_controller.core.processing.wled_target_processor import WledTargetProcessor