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
- [ ] `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] `P1` **Rename `picture-targets` to `output-targets`** — Rename API endpoints and internal references for clarity
- [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
- 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}"}
try:
async with session.get(
f"{server_url}/api/v1/picture-targets",
f"{server_url}/api/v1/output-targets",
headers=headers,
timeout=timeout,
) as resp:

View File

@@ -146,9 +146,9 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
self.server_version = "unknown"
async def _fetch_targets(self) -> list[dict[str, Any]]:
"""Fetch all picture targets."""
"""Fetch all output targets."""
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,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp:
@@ -159,7 +159,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
async def _fetch_target_state(self, target_id: str) -> dict[str, Any]:
"""Fetch target processing state."""
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,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp:
@@ -169,7 +169,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
async def _fetch_target_metrics(self, target_id: str) -> dict[str, Any]:
"""Fetch target metrics."""
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,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp:
@@ -277,7 +277,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
"""Set brightness for a Key Colors target (0-255 mapped to 0.0-1.0)."""
brightness_float = round(brightness / 255, 4)
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"},
json={"key_colors_settings": {"brightness": brightness_float}},
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
@@ -353,9 +353,9 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
await self.async_request_refresh()
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(
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"},
json=kwargs,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
@@ -372,7 +372,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
async def start_processing(self, target_id: str) -> None:
"""Start processing for a target."""
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,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp:
@@ -390,7 +390,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
async def stop_processing(self, target_id: str) -> None:
"""Stop processing for a target."""
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,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp:

View File

@@ -40,7 +40,7 @@ class KeyColorsWebSocketManager:
ws_base = self._server_url.replace("http://", "ws://").replace(
"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:
"""Start WebSocket connection for a target."""

View File

@@ -17,7 +17,7 @@ storage:
templates_file: "data/capture_templates.json"
postprocessing_templates_file: "data/postprocessing_templates.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"
mqtt:

View File

@@ -15,7 +15,7 @@ storage:
templates_file: "data/capture_templates.json"
postprocessing_templates_file: "data/postprocessing_templates.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"
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.picture_sources import router as picture_sources_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.audio import router as audio_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_templates_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(scene_presets_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.pattern_template_store import PatternTemplateStore
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.audio_source_store import AudioSourceStore
from wled_controller.storage.audio_template_store import AudioTemplateStore
@@ -25,7 +25,7 @@ _template_store: TemplateStore | None = None
_pp_template_store: PostprocessingTemplateStore | None = None
_pattern_template_store: PatternTemplateStore | 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
_audio_source_store: AudioSourceStore | None = None
_audio_template_store: AudioTemplateStore | None = None
@@ -73,11 +73,11 @@ def get_picture_source_store() -> PictureSourceStore:
return _picture_source_store
def get_picture_target_store() -> PictureTargetStore:
"""Get picture target store dependency."""
if _picture_target_store is None:
def get_output_target_store() -> OutputTargetStore:
"""Get output target store dependency."""
if _output_target_store is None:
raise RuntimeError("Picture target store not initialized")
return _picture_target_store
return _output_target_store
def get_color_strip_store() -> ColorStripStore:
@@ -164,7 +164,7 @@ def init_dependencies(
pp_template_store: PostprocessingTemplateStore | None = None,
pattern_template_store: PatternTemplateStore | 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,
audio_source_store: AudioSourceStore | None = None,
audio_template_store: AudioTemplateStore | None = None,
@@ -178,7 +178,7 @@ def init_dependencies(
):
"""Initialize global dependencies."""
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 _value_source_store, _automation_store, _scene_preset_store, _automation_engine, _auto_backup_engine
global _sync_clock_store, _sync_clock_manager
@@ -188,7 +188,7 @@ def init_dependencies(
_pp_template_store = pp_template_store
_pattern_template_store = pattern_template_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
_audio_source_store = audio_source_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 (
get_color_strip_store,
get_picture_source_store,
get_picture_target_store,
get_output_target_store,
get_processor_manager,
)
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.picture_source import ProcessedPictureSource, ScreenCapturePictureSource
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.config import get_config
@@ -295,7 +295,7 @@ async def delete_color_strip_source(
source_id: str,
_auth: AuthRequired,
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."""
try:

View File

@@ -13,7 +13,7 @@ from wled_controller.core.devices.led_client import (
)
from wled_controller.api.dependencies import (
get_device_store,
get_picture_target_store,
get_output_target_store,
get_processor_manager,
)
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.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
logger = get_logger(__name__)
@@ -342,7 +342,7 @@ async def delete_device(
device_id: str,
_auth: AuthRequired,
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),
):
"""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 base64
@@ -16,21 +16,21 @@ from wled_controller.api.dependencies import (
get_device_store,
get_pattern_template_store,
get_picture_source_store,
get_picture_target_store,
get_output_target_store,
get_pp_template_store,
get_processor_manager,
get_template_store,
)
from wled_controller.api.schemas.picture_targets import (
from wled_controller.api.schemas.output_targets import (
ExtractedColorResponse,
KCTestRectangleResponse,
KCTestResponse,
KeyColorsResponse,
KeyColorsSettingsSchema,
PictureTargetCreate,
PictureTargetListResponse,
PictureTargetResponse,
PictureTargetUpdate,
OutputTargetCreate,
OutputTargetListResponse,
OutputTargetResponse,
OutputTargetUpdate,
TargetMetricsResponse,
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_store import PictureSourceStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.wled_picture_target import WledPictureTarget
from wled_controller.storage.key_colors_picture_target import (
from wled_controller.storage.wled_output_target import WledOutputTarget
from wled_controller.storage.key_colors_output_target import (
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
logger = get_logger(__name__)
@@ -88,10 +88,10 @@ def _kc_schema_to_settings(schema: KeyColorsSettingsSchema) -> KeyColorsSettings
)
def _target_to_response(target) -> PictureTargetResponse:
"""Convert a PictureTarget to PictureTargetResponse."""
if isinstance(target, WledPictureTarget):
return PictureTargetResponse(
def _target_to_response(target) -> OutputTargetResponse:
"""Convert an OutputTarget to OutputTargetResponse."""
if isinstance(target, WledOutputTarget):
return OutputTargetResponse(
id=target.id,
name=target.name,
target_type=target.target_type,
@@ -109,8 +109,8 @@ def _target_to_response(target) -> PictureTargetResponse:
created_at=target.created_at,
updated_at=target.updated_at,
)
elif isinstance(target, KeyColorsPictureTarget):
return PictureTargetResponse(
elif isinstance(target, KeyColorsOutputTarget):
return OutputTargetResponse(
id=target.id,
name=target.name,
target_type=target.target_type,
@@ -122,7 +122,7 @@ def _target_to_response(target) -> PictureTargetResponse:
updated_at=target.updated_at,
)
else:
return PictureTargetResponse(
return OutputTargetResponse(
id=target.id,
name=target.name,
target_type=target.target_type,
@@ -135,15 +135,15 @@ def _target_to_response(target) -> PictureTargetResponse:
# ===== 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(
data: PictureTargetCreate,
data: OutputTargetCreate,
_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),
manager: ProcessorManager = Depends(get_processor_manager),
):
"""Create a new picture target."""
"""Create a new output target."""
try:
# Validate device exists if provided
if data.device_id:
@@ -188,18 +188,18 @@ async def create_target(
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(
_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()
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(
_auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager),
@@ -208,7 +208,7 @@ async def batch_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(
_auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager),
@@ -217,13 +217,13 @@ async def batch_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(
target_id: str,
_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:
target = target_store.get_target(target_id)
return _target_to_response(target)
@@ -231,16 +231,16 @@ async def get_target(
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(
target_id: str,
data: PictureTargetUpdate,
data: OutputTargetUpdate,
_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),
manager: ProcessorManager = Depends(get_processor_manager),
):
"""Update a picture target."""
"""Update a output target."""
try:
# Validate device exists if changing
if data.device_id is not None and data.device_id:
@@ -258,7 +258,7 @@ async def update_target(
except ValueError:
existing_target = None
if isinstance(existing_target, KeyColorsPictureTarget):
if isinstance(existing_target, KeyColorsOutputTarget):
ex = existing_target.settings
merged = KeyColorsSettingsSchema(
fps=incoming.get("fps", ex.fps),
@@ -325,14 +325,14 @@ async def update_target(
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(
target_id: str,
_auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store),
target_store: OutputTargetStore = Depends(get_output_target_store),
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:
# Stop processing if running
try:
@@ -360,14 +360,14 @@ async def delete_target(
# ===== 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(
target_id: str,
_auth: AuthRequired,
target_store: PictureTargetStore = Depends(get_picture_target_store),
target_store: OutputTargetStore = Depends(get_output_target_store),
manager: ProcessorManager = Depends(get_processor_manager),
):
"""Start processing for a picture target."""
"""Start processing for a output target."""
try:
# Verify target exists in store
target_store.get_target(target_id)
@@ -386,13 +386,13 @@ async def start_processing(
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(
target_id: str,
_auth: AuthRequired,
manager: ProcessorManager = Depends(get_processor_manager),
):
"""Stop processing for a picture target."""
"""Stop processing for a output target."""
try:
await manager.stop_processing(target_id)
@@ -408,7 +408,7 @@ async def stop_processing(
# ===== 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(
target_id: str,
_auth: AuthRequired,
@@ -426,7 +426,7 @@ async def get_target_state(
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(
target_id: str,
_auth: AuthRequired,
@@ -446,7 +446,7 @@ async def get_target_metrics(
# ===== 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(
target_id: str,
_auth: AuthRequired,
@@ -471,11 +471,11 @@ async def get_target_colors(
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(
target_id: str,
_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),
template_store: TemplateStore = Depends(get_template_store),
pattern_store: PatternTemplateStore = Depends(get_pattern_template_store),
@@ -494,7 +494,7 @@ async def test_kc_target(
except ValueError as 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")
settings = target.settings
@@ -670,7 +670,7 @@ async def test_kc_target(
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(
websocket: WebSocket,
target_id: str,
@@ -710,7 +710,7 @@ async def target_colors_ws(
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(
websocket: WebSocket,
target_id: str,
@@ -788,12 +788,12 @@ async def events_ws(
# ===== 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(
target_id: str,
_auth: AuthRequired,
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),
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.
calibration = 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
if first_css_id:
try:
@@ -844,7 +844,7 @@ async def start_target_overlay(
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(
target_id: str,
_auth: AuthRequired,
@@ -862,7 +862,7 @@ async def stop_target_overlay(
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(
target_id: str,
_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.dependencies import (
get_pattern_template_store,
get_picture_target_store,
get_output_target_store,
)
from wled_controller.api.schemas.pattern_templates import (
PatternTemplateCreate,
@@ -13,10 +13,10 @@ from wled_controller.api.schemas.pattern_templates import (
PatternTemplateResponse,
PatternTemplateUpdate,
)
from wled_controller.api.schemas.picture_targets import KeyColorRectangleSchema
from wled_controller.storage.key_colors_picture_target import KeyColorRectangle
from wled_controller.api.schemas.output_targets import KeyColorRectangleSchema
from wled_controller.storage.key_colors_output_target import KeyColorRectangle
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
logger = get_logger(__name__)
@@ -127,7 +127,7 @@ async def delete_pattern_template(
template_id: str,
_auth: AuthRequired,
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."""
try:

View File

@@ -13,7 +13,7 @@ from fastapi.responses import Response
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
get_picture_source_store,
get_picture_target_store,
get_output_target_store,
get_pp_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.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.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore
@@ -254,7 +254,7 @@ async def delete_picture_source(
stream_id: str,
_auth: AuthRequired,
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."""
try:

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
get_picture_target_store,
get_output_target_store,
get_processor_manager,
get_scene_preset_store,
)
@@ -23,7 +23,7 @@ from wled_controller.core.scenes.scene_activator import (
apply_scene_state,
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_store import ScenePresetStore
from wled_controller.utils import get_logger
@@ -62,7 +62,7 @@ async def create_scene_preset(
data: ScenePresetCreate,
_auth: AuthRequired,
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),
):
"""Capture current state as a new scene preset."""
@@ -176,7 +176,7 @@ async def recapture_scene_preset(
preset_id: str,
_auth: AuthRequired,
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),
):
"""Re-capture current state into an existing preset (updates snapshot)."""
@@ -214,7 +214,7 @@ async def activate_scene_preset(
preset_id: str,
_auth: AuthRequired,
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),
):
"""Activate a scene preset — restore the captured state."""

View File

@@ -265,7 +265,7 @@ STORE_MAP = {
"capture_templates": "templates_file",
"postprocessing_templates": "postprocessing_templates_file",
"picture_sources": "picture_sources_file",
"picture_targets": "picture_targets_file",
"output_targets": "output_targets_file",
"pattern_templates": "pattern_templates_file",
"color_strip_sources": "color_strip_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.dependencies import (
get_picture_target_store,
get_output_target_store,
get_processor_manager,
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_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.utils import get_logger
@@ -157,14 +157,14 @@ async def delete_value_source(
source_id: str,
_auth: AuthRequired,
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."""
try:
# 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():
if isinstance(target, WledPictureTarget):
if isinstance(target, WledOutputTarget):
if getattr(target, "brightness_value_source_id", "") == source_id:
raise ValueError(
f"Cannot delete: referenced by target '{target.name}'"

View File

@@ -30,11 +30,11 @@ from .color_strip_sources import (
ColorStripSourceUpdate,
CSSCalibrationTestRequest,
)
from .picture_targets import (
PictureTargetCreate,
PictureTargetListResponse,
PictureTargetResponse,
PictureTargetUpdate,
from .output_targets import (
OutputTargetCreate,
OutputTargetListResponse,
OutputTargetResponse,
OutputTargetUpdate,
TargetMetricsResponse,
TargetProcessingState,
)
@@ -100,10 +100,10 @@ __all__ = [
"ColorStripSourceResponse",
"ColorStripSourceUpdate",
"CSSCalibrationTestRequest",
"PictureTargetCreate",
"PictureTargetListResponse",
"PictureTargetResponse",
"PictureTargetUpdate",
"OutputTargetCreate",
"OutputTargetListResponse",
"OutputTargetResponse",
"OutputTargetUpdate",
"TargetMetricsResponse",
"TargetProcessingState",
"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 typing import Dict, Optional, List
@@ -46,8 +46,8 @@ class KeyColorsResponse(BaseModel):
timestamp: Optional[datetime] = Field(None, description="Extraction timestamp")
class PictureTargetCreate(BaseModel):
"""Request to create a picture target."""
class OutputTargetCreate(BaseModel):
"""Request to create an output target."""
name: str = Field(description="Target name", min_length=1, max_length=100)
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)
class PictureTargetUpdate(BaseModel):
"""Request to update a picture target."""
class OutputTargetUpdate(BaseModel):
"""Request to update an output target."""
name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100)
# LED target fields
@@ -87,8 +87,8 @@ class PictureTargetUpdate(BaseModel):
description: Optional[str] = Field(None, description="Optional description", max_length=500)
class PictureTargetResponse(BaseModel):
"""Picture target response."""
class OutputTargetResponse(BaseModel):
"""Output target response."""
id: str = Field(description="Target ID")
name: str = Field(description="Target name")
@@ -111,15 +111,15 @@ class PictureTargetResponse(BaseModel):
updated_at: datetime = Field(description="Last update timestamp")
class PictureTargetListResponse(BaseModel):
"""List of picture targets response."""
class OutputTargetListResponse(BaseModel):
"""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")
class TargetProcessingState(BaseModel):
"""Processing state for a picture target."""
"""Processing state for an output target."""
target_id: str = Field(description="Target 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 .picture_targets import KeyColorRectangleSchema
from .output_targets import KeyColorRectangleSchema
class PatternTemplateCreate(BaseModel):

View File

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

View File

@@ -1,7 +1,7 @@
"""Abstract base class for target processors.
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.
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 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 (
ScenePreset,
TargetSnapshot,
@@ -17,7 +17,7 @@ logger = get_logger(__name__)
def capture_current_snapshot(
target_store: PictureTargetStore,
target_store: OutputTargetStore,
processor_manager: ProcessorManager,
target_ids: Optional[Set[str]] = None,
) -> List[TargetSnapshot]:
@@ -45,7 +45,7 @@ def capture_current_snapshot(
async def apply_scene_state(
preset: ScenePreset,
target_store: PictureTargetStore,
target_store: OutputTargetStore,
processor_manager: ProcessorManager,
) -> Tuple[str, List[str]]:
"""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.pattern_template_store import PatternTemplateStore
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.audio_source_store import AudioSourceStore
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)
pp_template_store = PostprocessingTemplateStore(config.storage.postprocessing_templates_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)
color_strip_store = ColorStripStore(config.storage.color_strip_sources_file)
audio_source_store = AudioSourceStore(config.storage.audio_sources_file)
@@ -113,7 +113,7 @@ async def lifespan(app: FastAPI):
automation_store, processor_manager,
mqtt_service=mqtt_service,
scene_preset_store=scene_preset_store,
target_store=picture_target_store,
target_store=output_target_store,
device_store=device_store,
)
@@ -131,7 +131,7 @@ async def lifespan(app: FastAPI):
pp_template_store=pp_template_store,
pattern_template_store=pattern_template_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,
audio_source_store=audio_source_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")
# Register picture targets in processor manager
targets = picture_target_store.get_all_targets()
# Register output targets in processor manager
targets = output_target_store.get_all_targets()
registered_targets = 0
for target in targets:
try:
@@ -174,7 +174,7 @@ async def lifespan(app: FastAPI):
except Exception as 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
await processor_manager.start_health_monitoring()

View File

@@ -112,7 +112,7 @@ function _buildItems(results, states = {}) {
// Maps endpoint → response key that holds the array
const _responseKeys = [
['/devices', 'devices'],
['/picture-targets', 'targets'],
['/output-targets', 'targets'],
['/color-strip-sources', 'sources'],
['/automations', 'automations'],
['/capture-templates', 'templates'],
@@ -126,7 +126,7 @@ const _responseKeys = [
async function _fetchAllEntities() {
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(data => data.states || {})
.catch(() => ({})),

View File

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

View File

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

View File

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

View File

@@ -337,7 +337,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
if (targetId) {
// 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');
const target = await resp.json();
@@ -478,13 +478,13 @@ export async function saveTargetEditor() {
try {
let response;
if (targetId) {
response = await fetchWithAuth(`/picture-targets/${targetId}`, {
response = await fetchWithAuth(`/output-targets/${targetId}`, {
method: 'PUT',
body: JSON.stringify(payload),
});
} else {
payload.target_type = 'led';
response = await fetchWithAuth('/picture-targets', {
response = await fetchWithAuth('/output-targets', {
method: 'POST',
body: JSON.stringify(payload),
});
@@ -550,7 +550,7 @@ export async function loadTargetsTab() {
// use DataCache for picture sources, audio sources, value sources, sync clocks
const [devicesResp, targetsResp, cssResp, patResp, psArr, valueSrcArr, asSrcArr] = await Promise.all([
fetchWithAuth('/devices'),
fetchWithAuth('/picture-targets'),
fetchWithAuth('/output-targets'),
fetchWithAuth('/color-strip-sources').catch(() => null),
fetchWithAuth('/pattern-templates').catch(() => null),
streamsCache.fetch().catch(() => []),
@@ -591,8 +591,8 @@ export async function loadTargetsTab() {
// Fetch all device states, target states, and target metrics in batch
const [batchDevStatesResp, batchTgtStatesResp, batchTgtMetricsResp] = await Promise.all([
fetchWithAuth('/devices/batch/states'),
fetchWithAuth('/picture-targets/batch/states'),
fetchWithAuth('/picture-targets/batch/metrics'),
fetchWithAuth('/output-targets/batch/states'),
fetchWithAuth('/output-targets/batch/metrics'),
]);
const allDeviceStates = batchDevStatesResp.ok ? (await batchDevStatesResp.json()).states : {};
const allTargetStates = batchTgtStatesResp.ok ? (await batchTgtStatesResp.json()).states : {};
@@ -608,7 +608,7 @@ export async function loadTargetsTab() {
let latestColors = null;
if (target.target_type === 'key_colors' && state.processing) {
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();
} catch {}
}
@@ -1046,7 +1046,7 @@ async function _targetAction(action) {
export async function startTargetProcessing(targetId) {
await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/start`, {
const response = await fetchWithAuth(`/output-targets/${targetId}/start`, {
method: 'POST',
});
if (response.ok) {
@@ -1060,7 +1060,7 @@ export async function startTargetProcessing(targetId) {
export async function stopTargetProcessing(targetId) {
await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/stop`, {
const response = await fetchWithAuth(`/output-targets/${targetId}/stop`, {
method: 'POST',
});
if (response.ok) {
@@ -1083,8 +1083,8 @@ export async function stopAllKCTargets() {
async function _stopAllByType(targetType) {
try {
const [targetsResp, statesResp] = await Promise.all([
fetchWithAuth('/picture-targets'),
fetchWithAuth('/picture-targets/batch/states'),
fetchWithAuth('/output-targets'),
fetchWithAuth('/output-targets/batch/states'),
]);
const data = await targetsResp.json();
const statesData = statesResp.ok ? await statesResp.json() : { states: {} };
@@ -1096,7 +1096,7 @@ async function _stopAllByType(targetType) {
return;
}
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');
loadTargetsTab();
@@ -1108,7 +1108,7 @@ async function _stopAllByType(targetType) {
export async function startTargetOverlay(targetId) {
await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/start`, {
const response = await fetchWithAuth(`/output-targets/${targetId}/overlay/start`, {
method: 'POST',
});
if (response.ok) {
@@ -1122,7 +1122,7 @@ export async function startTargetOverlay(targetId) {
export async function stopTargetOverlay(targetId) {
await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}/overlay/stop`, {
const response = await fetchWithAuth(`/output-targets/${targetId}/overlay/stop`, {
method: 'POST',
});
if (response.ok) {
@@ -1136,7 +1136,7 @@ export async function stopTargetOverlay(targetId) {
export async function cloneTarget(targetId) {
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');
const target = await resp.json();
showTargetEditor(null, target);
@@ -1151,7 +1151,7 @@ export async function deleteTarget(targetId) {
if (!confirmed) return;
await _targetAction(async () => {
const response = await fetchWithAuth(`/picture-targets/${targetId}`, {
const response = await fetchWithAuth(`/output-targets/${targetId}`, {
method: 'DELETE',
});
if (response.ok) {
@@ -1282,7 +1282,7 @@ function connectLedPreviewWS(targetId) {
if (!key) return;
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 {
const ws = new WebSocket(wsUrl);

View File

@@ -7,7 +7,7 @@
* - Navigation: network-first with offline fallback
*/
const CACHE_NAME = 'ledgrab-v23';
const CACHE_NAME = 'ledgrab-v24';
// Only pre-cache static assets (no auth required).
// 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.
Calibration, processing settings, and picture source assignments
now live on ColorStripSource and WledPictureTarget respectively.
now live on ColorStripSource and WledOutputTarget respectively.
"""
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 datetime import datetime
from typing import List, Optional
from wled_controller.storage.picture_target import PictureTarget
from wled_controller.storage.output_target import OutputTarget
@dataclass
@@ -71,7 +71,7 @@ class KeyColorsSettings:
@dataclass
class KeyColorsPictureTarget(PictureTarget):
class KeyColorsOutputTarget(OutputTarget):
"""Key colors extractor target — extracts key colors from image rectangles."""
picture_source_id: str = ""
@@ -119,7 +119,7 @@ class KeyColorsPictureTarget(PictureTarget):
return d
@classmethod
def from_dict(cls, data: dict) -> "KeyColorsPictureTarget":
def from_dict(cls, data: dict) -> "KeyColorsOutputTarget":
settings_data = data.get("settings", {})
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 datetime import datetime
@@ -6,8 +6,8 @@ from typing import Optional
@dataclass
class PictureTarget:
"""Base class for picture targets."""
class OutputTarget:
"""Base class for output targets."""
id: str
name: str
@@ -50,13 +50,13 @@ class PictureTarget:
}
@classmethod
def from_dict(cls, data: dict) -> "PictureTarget":
def from_dict(cls, data: dict) -> "OutputTarget":
"""Create from dictionary, dispatching to the correct subclass."""
target_type = data.get("target_type", "led")
if target_type == "led":
from wled_controller.storage.wled_picture_target import WledPictureTarget
return WledPictureTarget.from_dict(data)
from wled_controller.storage.wled_output_target import WledOutputTarget
return WledOutputTarget.from_dict(data)
if target_type == "key_colors":
from wled_controller.storage.key_colors_picture_target import KeyColorsPictureTarget
return KeyColorsPictureTarget.from_dict(data)
from wled_controller.storage.key_colors_output_target import KeyColorsOutputTarget
return KeyColorsOutputTarget.from_dict(data)
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 uuid
@@ -6,11 +6,11 @@ from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from wled_controller.storage.picture_target import PictureTarget
from wled_controller.storage.wled_picture_target import WledPictureTarget
from wled_controller.storage.key_colors_picture_target import (
from wled_controller.storage.output_target import OutputTarget
from wled_controller.storage.wled_output_target import WledOutputTarget
from wled_controller.storage.key_colors_output_target import (
KeyColorsSettings,
KeyColorsPictureTarget,
KeyColorsOutputTarget,
)
from wled_controller.utils import atomic_write_json, get_logger
@@ -19,17 +19,17 @@ logger = get_logger(__name__)
DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds
class PictureTargetStore:
"""Persistent storage for picture targets."""
class OutputTargetStore:
"""Persistent storage for output targets."""
def __init__(self, file_path: str):
"""Initialize picture target store.
"""Initialize output target store.
Args:
file_path: Path to targets JSON file
"""
self.file_path = Path(file_path)
self._targets: Dict[str, PictureTarget] = {}
self._targets: Dict[str, OutputTarget] = {}
self._load()
def _load(self) -> None:
@@ -41,52 +41,53 @@ class PictureTargetStore:
with open(self.file_path, "r", encoding="utf-8") as 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
for target_id, target_dict in targets_data.items():
try:
target = PictureTarget.from_dict(target_dict)
target = OutputTarget.from_dict(target_dict)
self._targets[target_id] = target
loaded += 1
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:
logger.info(f"Loaded {loaded} picture targets from storage")
logger.info(f"Loaded {loaded} output targets from storage")
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
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:
"""Save all targets to file."""
try:
data = {
"version": "1.0.0",
"picture_targets": {
"output_targets": {
target_id: target.to_dict()
for target_id, target in self._targets.items()
},
}
atomic_write_json(self.file_path, data)
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
def get_all_targets(self) -> List[PictureTarget]:
"""Get all picture targets."""
def get_all_targets(self) -> List[OutputTarget]:
"""Get all output targets."""
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.
Raises:
ValueError: If target not found
"""
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]
def create_target(
@@ -105,8 +106,8 @@ class PictureTargetStore:
key_colors_settings: Optional[KeyColorsSettings] = None,
description: Optional[str] = None,
picture_source_id: str = "",
) -> PictureTarget:
"""Create a new picture target.
) -> OutputTarget:
"""Create a new output target.
Raises:
ValueError: If validation fails
@@ -117,13 +118,13 @@ class PictureTargetStore:
# Check for duplicate name
for target in self._targets.values():
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]}"
now = datetime.utcnow()
if target_type == "led":
target: PictureTarget = WledPictureTarget(
target: OutputTarget = WledOutputTarget(
id=target_id,
name=name,
target_type="led",
@@ -141,7 +142,7 @@ class PictureTargetStore:
updated_at=now,
)
elif target_type == "key_colors":
target = KeyColorsPictureTarget(
target = KeyColorsOutputTarget(
id=target_id,
name=name,
target_type="key_colors",
@@ -157,7 +158,7 @@ class PictureTargetStore:
self._targets[target_id] = target
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
def update_target(
@@ -175,14 +176,14 @@ class PictureTargetStore:
protocol: Optional[str] = None,
key_colors_settings: Optional[KeyColorsSettings] = None,
description: Optional[str] = None,
) -> PictureTarget:
"""Update a picture target.
) -> OutputTarget:
"""Update an output target.
Raises:
ValueError: If target not found or validation fails
"""
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]
@@ -190,7 +191,7 @@ class PictureTargetStore:
# Check for duplicate name (exclude self)
for other in self._targets.values():
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(
name=name,
@@ -210,42 +211,42 @@ class PictureTargetStore:
target.updated_at = datetime.utcnow()
self._save()
logger.info(f"Updated picture target: {target_id}")
logger.info(f"Updated output target: {target_id}")
return target
def delete_target(self, target_id: str) -> None:
"""Delete a picture target.
"""Delete an output target.
Raises:
ValueError: If target not found
"""
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]
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."""
return [
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]:
"""Return names of KC targets that reference a picture source."""
return [
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]:
"""Return names of LED targets that reference a color strip source."""
return [
target.name for target in self._targets.values()
if isinstance(target, WledPictureTarget)
if isinstance(target, WledOutputTarget)
and target.color_strip_source_id == css_id
]

View File

@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
from datetime import datetime
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

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from pathlib import Path
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.utils import atomic_write_json, get_logger
@@ -203,11 +203,11 @@ class PatternTemplateStore:
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."""
from wled_controller.storage.key_colors_picture_target import KeyColorsPictureTarget
from wled_controller.storage.key_colors_output_target import KeyColorsOutputTarget
return [
target.name for target in picture_target_store.get_all_targets()
if isinstance(target, KeyColorsPictureTarget) and target.settings.pattern_template_id == template_id
target.name for target in output_target_store.get_all_targets()
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 datetime import datetime
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
@dataclass
class WledPictureTarget(PictureTarget):
"""LED picture target — pairs an LED device with a ColorStripSource."""
class WledOutputTarget(OutputTarget):
"""LED output target — pairs an LED device with a ColorStripSource."""
device_id: str = ""
color_strip_source_id: str = ""
@@ -104,7 +104,7 @@ class WledPictureTarget(PictureTarget):
return d
@classmethod
def from_dict(cls, data: dict) -> "WledPictureTarget":
def from_dict(cls, data: dict) -> "WledOutputTarget":
"""Create from dictionary."""
return cls(
id=data["id"],

View File

@@ -253,7 +253,7 @@ def test_get_target_metrics(processor_manager):
def test_target_type_detection(processor_manager):
"""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.wled_target_processor import WledTargetProcessor