diff --git a/server/src/wled_controller/api/routes/devices.py b/server/src/wled_controller/api/routes/devices.py index 1e13d93..3e12183 100644 --- a/server/src/wled_controller/api/routes/devices.py +++ b/server/src/wled_controller/api/routes/devices.py @@ -25,6 +25,7 @@ from wled_controller.api.schemas.devices import ( DeviceUpdate, DiscoveredDeviceResponse, DiscoverDevicesResponse, + StaticColorUpdate, ) from wled_controller.core.capture.calibration import ( calibration_from_dict, @@ -51,6 +52,7 @@ def _device_to_response(device) -> DeviceResponse: enabled=device.enabled, baud_rate=device.baud_rate, auto_shutdown=device.auto_shutdown, + static_color=list(device.static_color) if device.static_color else None, capabilities=sorted(get_device_capabilities(device.device_type)), calibration=CalibrationSchema(**calibration_to_dict(device.calibration)), created_at=device.created_at, @@ -446,6 +448,66 @@ async def set_device_power( raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}") +# ===== STATIC COLOR ENDPOINTS ===== + + +@router.get("/api/v1/devices/{device_id}/color", tags=["Settings"]) +async def get_device_color( + device_id: str, + _auth: AuthRequired, + store: DeviceStore = Depends(get_device_store), +): + """Get the static idle color for a device.""" + device = store.get_device(device_id) + if not device: + raise HTTPException(status_code=404, detail=f"Device {device_id} not found") + if "static_color" not in get_device_capabilities(device.device_type): + raise HTTPException(status_code=400, detail="Static color is not supported for this device type") + return {"color": list(device.static_color) if device.static_color else None} + + +@router.put("/api/v1/devices/{device_id}/color", tags=["Settings"]) +async def set_device_color( + device_id: str, + body: StaticColorUpdate, + _auth: AuthRequired, + store: DeviceStore = Depends(get_device_store), + manager: ProcessorManager = Depends(get_processor_manager), +): + """Set or clear the static idle color for a device.""" + device = store.get_device(device_id) + if not device: + raise HTTPException(status_code=404, detail=f"Device {device_id} not found") + if "static_color" not in get_device_capabilities(device.device_type): + raise HTTPException(status_code=400, detail="Static color is not supported for this device type") + + color = None + if body.color is not None: + if len(body.color) != 3 or not all(isinstance(c, int) and 0 <= c <= 255 for c in body.color): + raise HTTPException(status_code=400, detail="color must be [R, G, B] with values 0-255") + color = tuple(body.color) + + store.set_static_color(device_id, color) + + # Update runtime state + ds = manager._devices.get(device_id) + if ds: + ds.static_color = color + + # If device is idle, apply the color immediately + if color is not None and not manager.is_device_processing(device_id): + try: + provider = get_provider(device.device_type) + await provider.set_color( + device.url, color, + led_count=device.led_count, baud_rate=device.baud_rate, + ) + except Exception as e: + logger.warning(f"Failed to apply static color immediately: {e}") + + return {"color": list(color) if color else None} + + # ===== CALIBRATION ENDPOINTS ===== @router.get("/api/v1/devices/{device_id}/calibration", response_model=CalibrationSchema, tags=["Calibration"]) diff --git a/server/src/wled_controller/api/schemas/devices.py b/server/src/wled_controller/api/schemas/devices.py index de2ae88..fe626ad 100644 --- a/server/src/wled_controller/api/schemas/devices.py +++ b/server/src/wled_controller/api/schemas/devices.py @@ -28,6 +28,15 @@ class DeviceUpdate(BaseModel): auto_shutdown: Optional[bool] = Field(None, description="Turn off device when server stops") +class StaticColorUpdate(BaseModel): + """Request to set or clear the static idle color.""" + + color: Optional[List[int]] = Field( + None, + description="RGB color [R, G, B] with values 0-255, or null to clear", + ) + + class Calibration(BaseModel): """Calibration configuration for pixel-to-LED mapping.""" @@ -92,7 +101,8 @@ class DeviceResponse(BaseModel): led_count: int = Field(description="Total number of LEDs") enabled: bool = Field(description="Whether device is enabled") baud_rate: Optional[int] = Field(None, description="Serial baud rate") - auto_shutdown: bool = Field(default=False, description="Turn off device when server stops") + auto_shutdown: bool = Field(default=False, description="Restore device to idle state when targets stop") + static_color: Optional[List[int]] = Field(None, description="Static idle color [R, G, B]") capabilities: List[str] = Field(default_factory=list, description="Device type capabilities") calibration: Optional[Calibration] = Field(None, description="Calibration configuration") created_at: datetime = Field(description="Creation timestamp") diff --git a/server/src/wled_controller/core/devices/adalight_provider.py b/server/src/wled_controller/core/devices/adalight_provider.py index 85e269f..9964515 100644 --- a/server/src/wled_controller/core/devices/adalight_provider.py +++ b/server/src/wled_controller/core/devices/adalight_provider.py @@ -1,6 +1,6 @@ """Adalight device provider — serial LED controller support.""" -from typing import List +from typing import List, Tuple import numpy as np @@ -27,7 +27,7 @@ class AdalightDeviceProvider(LEDDeviceProvider): # manual_led_count: user must specify LED count (can't auto-detect) # power_control: can blank LEDs by sending all-black pixels # brightness_control: software brightness (multiplies pixel values before sending) - return {"manual_led_count", "power_control", "brightness_control"} + return {"manual_led_count", "power_control", "brightness_control", "static_color"} def create_client(self, url: str, **kwargs) -> LEDClient: from wled_controller.core.devices.adalight_client import AdalightClient @@ -123,3 +123,24 @@ class AdalightDeviceProvider(LEDDeviceProvider): logger.info(f"Adalight power off: sent black frame to {url}") finally: await client.close() + + async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None: + """Send a solid color frame to the Adalight device. + + Requires kwargs: led_count (int), baud_rate (int | None). + """ + led_count = kwargs.get("led_count", 0) + baud_rate = kwargs.get("baud_rate") + if led_count <= 0: + raise ValueError("led_count is required to send color frame to Adalight device") + + from wled_controller.core.devices.adalight_client import AdalightClient + + client = AdalightClient(url, led_count=led_count, baud_rate=baud_rate) + try: + await client.connect() + frame = np.full((led_count, 3), color, dtype=np.uint8) + await client.send_pixels(frame, brightness=255) + logger.info(f"Adalight set_color: sent solid {color} to {url}") + finally: + await client.close() diff --git a/server/src/wled_controller/core/devices/led_client.py b/server/src/wled_controller/core/devices/led_client.py index ecbe5c6..33695fa 100644 --- a/server/src/wled_controller/core/devices/led_client.py +++ b/server/src/wled_controller/core/devices/led_client.py @@ -210,6 +210,10 @@ class LEDDeviceProvider(ABC): """Set device power state. Override if capabilities include power_control.""" raise NotImplementedError + async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None: + """Set all LEDs to a solid color. Override if capabilities include static_color.""" + raise NotImplementedError + # ===== PROVIDER REGISTRY ===== diff --git a/server/src/wled_controller/core/processing/processor_manager.py b/server/src/wled_controller/core/processing/processor_manager.py index 161ca39..5790d8f 100644 --- a/server/src/wled_controller/core/processing/processor_manager.py +++ b/server/src/wled_controller/core/processing/processor_manager.py @@ -48,8 +48,10 @@ class DeviceState: health_task: Optional[asyncio.Task] = None # Software brightness for devices without hardware brightness (e.g. Adalight) software_brightness: int = 255 - # Auto-shutdown: turn off device when server stops + # Auto-restore: restore device to idle state when targets stop auto_shutdown: bool = False + # Static idle color for devices without a rich editor (e.g. Adalight) + static_color: Optional[Tuple[int, int, int]] = None # Calibration test mode (works independently of target processing) test_mode_active: bool = False test_mode_edges: Dict[str, Tuple[int, int, int]] = field(default_factory=dict) @@ -151,6 +153,7 @@ class ProcessorManager: baud_rate: Optional[int] = None, software_brightness: int = 255, auto_shutdown: bool = False, + static_color: Optional[Tuple[int, int, int]] = None, ): """Register a device for health monitoring.""" if device_id in self._devices: @@ -168,6 +171,7 @@ class ProcessorManager: baud_rate=baud_rate, software_brightness=software_brightness, auto_shutdown=auto_shutdown, + static_color=static_color, ) self._devices[device_id] = state @@ -378,10 +382,19 @@ class ProcessorManager: await proc.start() async def stop_processing(self, target_id: str): - """Stop processing for a target (any type).""" + """Stop processing for a target (any type). + + For WLED targets, if the associated device has auto_shutdown enabled + and no other targets are still actively processing on it, the device + is restored to its idle state (static color or pre-streaming snapshot). + """ proc = self._get_processor(target_id) await proc.stop() + # Auto-shutdown device if applicable + if isinstance(proc, WledTargetProcessor): + await self._restore_device_idle_state(proc.device_id) + def get_target_state(self, target_id: str) -> dict: """Get current processing state for a target (any type). @@ -430,29 +443,6 @@ class ProcessorManager: return proc.target_id return None - # Backward-compat aliases for KC-specific operations - def update_kc_target_settings(self, target_id: str, settings) -> None: - self.update_target_settings(target_id, settings) - - def update_kc_target_source(self, target_id: str, picture_source_id: str) -> None: - self.update_target_source(target_id, picture_source_id) - - async def start_kc_processing(self, target_id: str) -> None: - await self.start_processing(target_id) - - async def stop_kc_processing(self, target_id: str) -> None: - await self.stop_processing(target_id) - - def get_kc_target_state(self, target_id: str) -> dict: - return self.get_target_state(target_id) - - def get_kc_target_metrics(self, target_id: str) -> dict: - return self.get_target_metrics(target_id) - - def is_kc_target(self, target_id: str) -> bool: - """Check if a target ID belongs to a KC target.""" - return isinstance(self._processors.get(target_id), KCTargetProcessor) - # ===== OVERLAY VISUALIZATION (delegates to processor) ===== async def start_overlay(self, target_id: str, target_name: str = None) -> None: @@ -564,6 +554,45 @@ class ProcessorManager: return proc.device_id return None + async def _restore_device_idle_state(self, device_id: str) -> None: + """Restore a device to its idle state when all targets stop. + + - If a static color is configured, send it. + - For WLED: do nothing — stop() already restored the snapshot. + - For other devices without static color: power off (black frame). + """ + ds = self._devices.get(device_id) + if not ds or not ds.auto_shutdown: + return + + if self.is_device_processing(device_id): + return + + try: + provider = get_provider(ds.device_type) + + if ds.static_color is not None: + await provider.set_color( + ds.device_url, ds.static_color, + led_count=ds.led_count, baud_rate=ds.baud_rate, + ) + logger.info( + f"Auto-restore: sent static color {ds.static_color} " + f"to {ds.device_type} device {device_id}" + ) + elif ds.device_type != "wled": + # Non-WLED without static color: power off (send black frame) + await provider.set_power( + ds.device_url, False, + led_count=ds.led_count, baud_rate=ds.baud_rate, + ) + logger.info(f"Auto-restore: powered off {ds.device_type} device {device_id}") + else: + # WLED: stop() already called restore_device_state() via snapshot + logger.info(f"Auto-restore: WLED device {device_id} restored by snapshot") + except Exception as e: + logger.error(f"Auto-restore failed for device {device_id}: {e}") + # ===== LIFECYCLE ===== async def stop_all(self): @@ -578,19 +607,9 @@ class ProcessorManager: except Exception as e: logger.error(f"Error stopping target {target_id}: {e}") - # Auto-shutdown devices that have the flag enabled - for device_id, ds in self._devices.items(): - if not ds.auto_shutdown: - continue - try: - provider = get_provider(ds.device_type) - await provider.set_power( - ds.device_url, False, - led_count=ds.led_count, baud_rate=ds.baud_rate, - ) - logger.info(f"Auto-shutdown: powered off {ds.device_type} device {device_id}") - except Exception as e: - logger.error(f"Auto-shutdown failed for device {device_id}: {e}") + # Restore idle state for devices that have auto-restore enabled + for device_id in self._devices: + await self._restore_device_idle_state(device_id) # Safety net: release any remaining managed live streams self._live_stream_manager.release_all() diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py index fde4722..f13d858 100644 --- a/server/src/wled_controller/main.py +++ b/server/src/wled_controller/main.py @@ -160,6 +160,7 @@ async def lifespan(app: FastAPI): baud_rate=device.baud_rate, software_brightness=device.software_brightness, auto_shutdown=device.auto_shutdown, + static_color=device.static_color, ) logger.info(f"Registered device: {device.name} ({device.id})") except Exception as e: diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index dcaa812..940b67d 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -667,6 +667,7 @@ function createDeviceCard(device) { ${ledCount ? `💡 ${ledCount}` : ''} ${state.device_led_type ? `🔌 ${state.device_led_type.replace(/ RGBW$/, '')}` : ''} ${state.device_rgbw ? '' : ''} + ${(device.capabilities || []).includes('static_color') ? `` : ''} ${(device.capabilities || []).includes('brightness_control') ? `
@@ -814,6 +815,9 @@ async function showSettings(deviceId) { baudRateGroup.style.display = 'none'; } + // Populate auto shutdown toggle + document.getElementById('settings-auto-shutdown').checked = !!device.auto_shutdown; + // Snapshot initial values for dirty checking settingsInitialValues = { name: device.name, @@ -823,6 +827,7 @@ async function showSettings(deviceId) { device_type: device.device_type, capabilities: caps, state_check_interval: '30', + auto_shutdown: !!device.auto_shutdown, }; // Show modal @@ -855,6 +860,7 @@ function isSettingsDirty() { document.getElementById('settings-device-name').value !== settingsInitialValues.name || _getSettingsUrl() !== settingsInitialValues.url || document.getElementById('settings-health-interval').value !== settingsInitialValues.state_check_interval || + document.getElementById('settings-auto-shutdown').checked !== settingsInitialValues.auto_shutdown || ledCountDirty ); } @@ -890,8 +896,8 @@ async function saveDeviceSettings() { } try { - // Update device info (name, url, optionally led_count, baud_rate) - const body = { name, url }; + // Update device info (name, url, auto_shutdown, optionally led_count, baud_rate) + const body = { name, url, auto_shutdown: document.getElementById('settings-auto-shutdown').checked }; const ledCountInput = document.getElementById('settings-led-count'); if ((settingsInitialValues.capabilities || []).includes('manual_led_count') && ledCountInput.value) { body.led_count = parseInt(ledCountInput.value, 10); @@ -973,6 +979,56 @@ async function fetchDeviceBrightness(deviceId) { } } +// Static color helpers +function rgbToHex(r, g, b) { + return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join(''); +} + +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null; +} + +async function saveDeviceStaticColor(deviceId, hexValue) { + const rgb = hexToRgb(hexValue); + try { + await fetch(`${API_BASE}/devices/${deviceId}/color`, { + method: 'PUT', + headers: { ...getHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ color: rgb }) + }); + // Show clear button + const wrap = document.querySelector(`[data-color-wrap="${deviceId}"]`); + if (wrap) { + const clearBtn = wrap.querySelector('.btn-clear-color'); + if (clearBtn) clearBtn.style.display = ''; + } + } catch (err) { + console.error('Failed to set static color:', err); + showToast('Failed to set static color', 'error'); + } +} + +async function clearDeviceStaticColor(deviceId) { + try { + await fetch(`${API_BASE}/devices/${deviceId}/color`, { + method: 'PUT', + headers: { ...getHeaders(), 'Content-Type': 'application/json' }, + body: JSON.stringify({ color: null }) + }); + // Reset picker to black and hide clear button + const picker = document.querySelector(`[data-device-color="${deviceId}"]`); + if (picker) picker.value = '#000000'; + const wrap = document.querySelector(`[data-color-wrap="${deviceId}"]`); + if (wrap) { + const clearBtn = wrap.querySelector('.btn-clear-color'); + if (clearBtn) clearBtn.style.display = 'none'; + } + } catch (err) { + console.error('Failed to clear static color:', err); + } +} + // Add device modal let _discoveryScanRunning = false; let _discoveryCache = {}; // { deviceType: [...devices] } — per-type discovery cache diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html index 24b735b..a309c37 100644 --- a/server/src/wled_controller/static/index.html +++ b/server/src/wled_controller/static/index.html @@ -297,6 +297,18 @@
+
+
+ + +
+ + +
+ diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 37b29ae..b1b51eb 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -158,6 +158,9 @@ "device.health.online": "Online", "device.health.offline": "Offline", "device.health.checking": "Checking...", + "device.static_color": "Idle Color", + "device.static_color.hint": "Color shown when device is idle", + "device.static_color.clear": "Clear idle color", "device.tutorial.start": "Start tutorial", "device.tip.metadata": "Device info (LED count, type, color channels) is auto-detected from the device", "device.tip.brightness": "Slide to adjust device brightness", @@ -184,6 +187,8 @@ "settings.button.cancel": "Cancel", "settings.health_interval": "Health Check Interval (s):", "settings.health_interval.hint": "How often to check the device status (5-600 seconds)", + "settings.auto_shutdown": "Auto Restore:", + "settings.auto_shutdown.hint": "Restore device to idle state when targets stop or server shuts down", "settings.button.save": "Save Changes", "settings.saved": "Settings saved successfully", "settings.failed": "Failed to save settings", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 461921d..9fe9e3c 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -158,6 +158,9 @@ "device.health.online": "Онлайн", "device.health.offline": "Недоступен", "device.health.checking": "Проверка...", + "device.static_color": "Цвет ожидания", + "device.static_color.hint": "Цвет, когда устройство в режиме ожидания", + "device.static_color.clear": "Очистить цвет ожидания", "device.tutorial.start": "Начать обучение", "device.tip.metadata": "Информация об устройстве (кол-во LED, тип, цветовые каналы) определяется автоматически", "device.tip.brightness": "Перетащите для регулировки яркости", @@ -184,6 +187,8 @@ "settings.button.cancel": "Отмена", "settings.health_interval": "Интервал Проверки (с):", "settings.health_interval.hint": "Как часто проверять статус устройства (5-600 секунд)", + "settings.auto_shutdown": "Авто-восстановление:", + "settings.auto_shutdown.hint": "Восстанавливать устройство в режим ожидания при остановке целей или сервера", "settings.button.save": "Сохранить Изменения", "settings.saved": "Настройки успешно сохранены", "settings.failed": "Не удалось сохранить настройки", diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css index a301ab4..6d22092 100644 --- a/server/src/wled_controller/static/style.css +++ b/server/src/wled_controller/static/style.css @@ -231,6 +231,8 @@ section { padding: 12px 20px 20px; position: relative; transition: transform 0.2s, box-shadow 0.2s; + display: flex; + flex-direction: column; } .card:hover { @@ -739,6 +741,40 @@ section { pointer-events: none; } +/* Static color picker — inline in card-subtitle */ +.static-color-control { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.static-color-picker { + width: 22px; + height: 18px; + padding: 0; + border: 1px solid var(--border-color); + border-radius: 3px; + cursor: pointer; + background: none; + vertical-align: middle; +} + +.btn-clear-color { + background: none; + border: none; + color: #777; + font-size: 0.75rem; + cursor: pointer; + padding: 0 2px; + line-height: 1; + border-radius: 3px; + transition: color 0.2s; +} + +.btn-clear-color:hover { + color: var(--danger-color); +} + .section-header { display: flex; align-items: center; @@ -779,6 +815,54 @@ ul.section-tip li { margin-bottom: 15px; } +.settings-toggle-group { + display: flex; + flex-direction: column; +} + +.settings-toggle { + position: relative; + display: inline-block; + width: 34px; + height: 18px; + cursor: pointer; + margin-top: 4px; +} + +.settings-toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.settings-toggle-slider { + position: absolute; + inset: 0; + background: var(--border-color); + border-radius: 9px; + transition: background 0.2s; +} + +.settings-toggle-slider::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + transition: transform 0.2s; +} + +.settings-toggle input:checked + .settings-toggle-slider { + background: var(--primary-color); +} + +.settings-toggle input:checked + .settings-toggle-slider::after { + transform: translateX(16px); +} + label { display: block; margin-bottom: 5px; diff --git a/server/src/wled_controller/storage/device_store.py b/server/src/wled_controller/storage/device_store.py index 1530707..ead3717 100644 --- a/server/src/wled_controller/storage/device_store.py +++ b/server/src/wled_controller/storage/device_store.py @@ -4,7 +4,7 @@ import json import uuid from datetime import datetime from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple from wled_controller.core.capture.calibration import ( CalibrationConfig, @@ -35,6 +35,7 @@ class Device: baud_rate: Optional[int] = None, software_brightness: int = 255, auto_shutdown: bool = False, + static_color: Optional[Tuple[int, int, int]] = None, calibration: Optional[CalibrationConfig] = None, created_at: Optional[datetime] = None, updated_at: Optional[datetime] = None, @@ -48,6 +49,7 @@ class Device: self.baud_rate = baud_rate self.software_brightness = software_brightness self.auto_shutdown = auto_shutdown + self.static_color = static_color self.calibration = calibration or create_default_calibration(led_count) self.created_at = created_at or datetime.utcnow() self.updated_at = updated_at or datetime.utcnow() @@ -71,6 +73,8 @@ class Device: d["software_brightness"] = self.software_brightness if self.auto_shutdown: d["auto_shutdown"] = True + if self.static_color is not None: + d["static_color"] = list(self.static_color) return d @classmethod @@ -87,6 +91,9 @@ class Device: else create_default_calibration(data["led_count"]) ) + static_color_raw = data.get("static_color") + static_color = tuple(static_color_raw) if static_color_raw else None + return cls( device_id=data["id"], name=data["name"], @@ -97,6 +104,7 @@ class Device: baud_rate=data.get("baud_rate"), software_brightness=data.get("software_brightness", 255), auto_shutdown=data.get("auto_shutdown", False), + static_color=static_color, calibration=calibration, created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())), updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())), @@ -256,6 +264,18 @@ class DeviceStore: logger.info(f"Updated device {device_id}") return device + def set_static_color( + self, device_id: str, color: Optional[Tuple[int, int, int]] + ) -> "Device": + """Set or clear the static idle color for a device.""" + device = self._devices.get(device_id) + if not device: + raise ValueError(f"Device {device_id} not found") + device.static_color = color + device.updated_at = datetime.utcnow() + self.save() + return device + def delete_device(self, device_id: str): """Delete device.""" if device_id not in self._devices: diff --git a/server/tests/test_processor_manager.py b/server/tests/test_processor_manager.py index f064b07..31d4076 100644 --- a/server/tests/test_processor_manager.py +++ b/server/tests/test_processor_manager.py @@ -251,9 +251,11 @@ def test_get_target_metrics(processor_manager): assert metrics["errors_count"] == 0 -def test_is_kc_target(processor_manager): - """Test KC target type detection.""" +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.core.processing.kc_target_processor import KCTargetProcessor + from wled_controller.core.processing.wled_target_processor import WledTargetProcessor processor_manager.add_device( device_id="test_device", @@ -272,8 +274,8 @@ def test_is_kc_target(processor_manager): settings=KeyColorsSettings(), ) - assert processor_manager.is_kc_target("kc_target") is True - assert processor_manager.is_kc_target("wled_target") is False + assert isinstance(processor_manager._processors["kc_target"], KCTargetProcessor) + assert isinstance(processor_manager._processors["wled_target"], WledTargetProcessor) @pytest.mark.asyncio