Add Adalight serial LED device support with per-type discovery and capability-based UI

Implements the second device provider (Adalight) for Arduino-based serial LED controllers,
validating the LEDDeviceProvider abstraction. Adds serial port auto-discovery, per-type
discovery caching with lazy-load, capability-driven UI (brightness control, manual LED count,
standby), and serial port combobox in both Add Device and General Settings modals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 15:55:42 +03:00
parent 242718a9a9
commit 1612c04c90
13 changed files with 678 additions and 76 deletions

View File

@@ -83,7 +83,12 @@ async def create_device(
try:
result = await provider.validate_device(device_url)
led_count = result["led_count"]
led_count = result.get("led_count") or device_data.led_count
if not led_count or led_count < 1:
raise HTTPException(
status_code=422,
detail="LED count is required for this device type.",
)
except httpx.ConnectError:
raise HTTPException(
status_code=422,
@@ -146,18 +151,28 @@ async def discover_devices(
_auth: AuthRequired,
store: DeviceStore = Depends(get_device_store),
timeout: float = 3.0,
device_type: str | None = None,
):
"""Scan the local network for LED devices via all registered providers."""
"""Scan for LED devices. Optionally filter by device_type (e.g. wled, adalight)."""
import asyncio
import time
start = time.time()
capped_timeout = min(timeout, 10.0)
# Discover from all providers in parallel
providers = get_all_providers()
discover_tasks = [p.discover(timeout=capped_timeout) for p in providers.values()]
all_results = await asyncio.gather(*discover_tasks)
discovered = [d for batch in all_results for d in batch]
if device_type:
# Discover from a single provider
try:
provider = get_provider(device_type)
except ValueError:
raise HTTPException(status_code=400, detail=f"Unknown device type: {device_type}")
discovered = await provider.discover(timeout=capped_timeout)
else:
# Discover from all providers in parallel
providers = get_all_providers()
discover_tasks = [p.discover(timeout=capped_timeout) for p in providers.values()]
all_results = await asyncio.gather(*discover_tasks)
discovered = [d for batch in all_results for d in batch]
elapsed_ms = (time.time() - start) * 1000
existing_urls = {d.url.rstrip("/").lower() for d in store.get_all_devices()}
@@ -213,6 +228,7 @@ async def update_device(
name=update_data.name,
url=update_data.url,
enabled=update_data.enabled,
led_count=update_data.led_count,
)
# Sync connection info in processor manager
@@ -220,7 +236,7 @@ async def update_device(
manager.update_device_info(
device_id,
device_url=update_data.url,
led_count=None,
led_count=update_data.led_count,
)
except ValueError:
pass

View File

@@ -10,16 +10,18 @@ class DeviceCreate(BaseModel):
"""Request to create/attach an LED device."""
name: str = Field(description="Device name", min_length=1, max_length=100)
url: str = Field(description="Device URL (e.g., http://192.168.1.100)")
device_type: str = Field(default="wled", description="LED device type (e.g., wled)")
url: str = Field(description="Device URL (e.g., http://192.168.1.100 or COM3)")
device_type: str = Field(default="wled", description="LED device type (e.g., wled, adalight)")
led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (required for adalight)")
class DeviceUpdate(BaseModel):
"""Request to update device information."""
name: Optional[str] = Field(None, description="Device name", min_length=1, max_length=100)
url: Optional[str] = Field(None, description="WLED device URL")
url: Optional[str] = Field(None, description="Device URL or serial port")
enabled: Optional[bool] = Field(None, description="Whether device is enabled")
led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (for devices with manual_led_count capability)")
class Calibration(BaseModel):