Add LEDDeviceProvider abstraction and standby capability flag
Consolidate all device-type-specific logic into LEDDeviceProvider ABC with provider registry. WLEDDeviceProvider handles client creation, health checks, validation, mDNS discovery, and brightness control. Routes now delegate to providers instead of using if/else type checks. Add standby_required capability and expose device capabilities in API. Target editor conditionally shows standby interval based on selected device's capabilities. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,11 @@ import httpx
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.core.led_client import get_device_capabilities
|
||||
from wled_controller.core.led_client import (
|
||||
get_all_providers,
|
||||
get_device_capabilities,
|
||||
get_provider,
|
||||
)
|
||||
from wled_controller.api.dependencies import (
|
||||
get_device_store,
|
||||
get_picture_target_store,
|
||||
@@ -45,6 +49,7 @@ def _device_to_response(device) -> DeviceResponse:
|
||||
device_type=device.device_type,
|
||||
led_count=device.led_count,
|
||||
enabled=device.enabled,
|
||||
capabilities=sorted(get_device_capabilities(device.device_type)),
|
||||
calibration=CalibrationSchema(**calibration_to_dict(device.calibration)),
|
||||
created_at=device.created_at,
|
||||
updated_at=device.updated_at,
|
||||
@@ -66,53 +71,44 @@ async def create_device(
|
||||
logger.info(f"Creating {device_type} device: {device_data.name}")
|
||||
|
||||
device_url = device_data.url.rstrip("/")
|
||||
wled_led_count = 0
|
||||
|
||||
if device_type == "wled":
|
||||
# Validate WLED device is reachable before adding
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
response = await client.get(f"{device_url}/json/info")
|
||||
response.raise_for_status()
|
||||
wled_info = response.json()
|
||||
wled_led_count = wled_info.get("leds", {}).get("count")
|
||||
if not wled_led_count or wled_led_count < 1:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"WLED device at {device_url} reported invalid LED count: {wled_led_count}"
|
||||
)
|
||||
logger.info(
|
||||
f"WLED device reachable: {wled_info.get('name', 'Unknown')} "
|
||||
f"v{wled_info.get('ver', '?')} ({wled_led_count} LEDs)"
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Cannot reach WLED device at {device_url}. Check the URL and ensure the device is powered on."
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Connection to {device_url} timed out. Check network connectivity."
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Failed to connect to WLED device at {device_url}: {e}"
|
||||
)
|
||||
else:
|
||||
# Validate via provider
|
||||
try:
|
||||
provider = get_provider(device_type)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unsupported device type: {device_type}"
|
||||
)
|
||||
|
||||
try:
|
||||
result = await provider.validate_device(device_url)
|
||||
led_count = result["led_count"]
|
||||
except httpx.ConnectError:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Cannot reach {device_type} device at {device_url}. Check the URL and ensure the device is powered on."
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Connection to {device_url} timed out. Check network connectivity."
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Failed to connect to {device_type} device at {device_url}: {e}"
|
||||
)
|
||||
|
||||
# Create device in storage
|
||||
device = store.create_device(
|
||||
name=device_data.name,
|
||||
url=device_data.url,
|
||||
led_count=wled_led_count,
|
||||
led_count=led_count,
|
||||
device_type=device_type,
|
||||
)
|
||||
|
||||
@@ -151,12 +147,17 @@ async def discover_devices(
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
timeout: float = 3.0,
|
||||
):
|
||||
"""Scan the local network for WLED devices via mDNS."""
|
||||
"""Scan the local network for LED devices via all registered providers."""
|
||||
import asyncio
|
||||
import time
|
||||
from wled_controller.core.discovery import discover_wled_devices
|
||||
|
||||
start = time.time()
|
||||
discovered = await discover_wled_devices(timeout=min(timeout, 10.0))
|
||||
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]
|
||||
elapsed_ms = (time.time() - start) * 1000
|
||||
|
||||
existing_urls = {d.url.rstrip("/").lower() for d in store.get_all_devices()}
|
||||
@@ -310,15 +311,12 @@ async def get_device_brightness(
|
||||
raise HTTPException(status_code=400, detail=f"Brightness control is not supported for {device.device_type} devices")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as http_client:
|
||||
resp = await http_client.get(f"{device.url}/json/state")
|
||||
resp.raise_for_status()
|
||||
state = resp.json()
|
||||
bri = state.get("bri", 255)
|
||||
return {"brightness": bri}
|
||||
provider = get_provider(device.device_type)
|
||||
bri = await provider.get_brightness(device.url)
|
||||
return {"brightness": bri}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get WLED brightness for {device_id}: {e}")
|
||||
raise HTTPException(status_code=502, detail=f"Failed to reach WLED device: {e}")
|
||||
logger.error(f"Failed to get brightness for {device_id}: {e}")
|
||||
raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}")
|
||||
|
||||
|
||||
@router.put("/api/v1/devices/{device_id}/brightness", tags=["Settings"])
|
||||
@@ -340,16 +338,12 @@ async def set_device_brightness(
|
||||
raise HTTPException(status_code=400, detail="brightness must be an integer 0-255")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as http_client:
|
||||
resp = await http_client.post(
|
||||
f"{device.url}/json/state",
|
||||
json={"bri": bri},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return {"brightness": bri}
|
||||
provider = get_provider(device.device_type)
|
||||
await provider.set_brightness(device.url, bri)
|
||||
return {"brightness": bri}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set WLED brightness for {device_id}: {e}")
|
||||
raise HTTPException(status_code=502, detail=f"Failed to reach WLED device: {e}")
|
||||
logger.error(f"Failed to set brightness for {device_id}: {e}")
|
||||
raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}")
|
||||
|
||||
|
||||
# ===== CALIBRATION ENDPOINTS =====
|
||||
|
||||
@@ -85,6 +85,7 @@ class DeviceResponse(BaseModel):
|
||||
device_type: str = Field(default="wled", description="LED device type")
|
||||
led_count: int = Field(description="Total number of LEDs")
|
||||
enabled: bool = Field(description="Whether device is enabled")
|
||||
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")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
Reference in New Issue
Block a user