Add static color for simple devices, change auto-shutdown to auto-restore

- Add `static_color` capability to Adalight provider with `set_color()` method
- Add `static_color` field to Device model, DeviceState, and API schemas
- Add GET/PUT `/devices/{id}/color` API endpoints
- Change auto-shutdown behavior: restore device to idle state instead of
  powering off (WLED uses snapshot/restore, Adalight sends static color
  or black frame)
- Rename `_auto_shutdown_device_if_idle` to `_restore_device_idle_state`
- Add inline color picker on device cards for devices with static_color
- Add auto_shutdown toggle to device settings modal
- Update labels from "Auto Shutdown" to "Auto Restore" (en + ru)
- Remove backward-compat KC aliases from ProcessorManager
- Align card action buttons to bottom with flex column layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 13:42:05 +03:00
parent fc779eef39
commit d6cf45c873
13 changed files with 349 additions and 48 deletions

View File

@@ -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"])

View File

@@ -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")