Add power toggle button to LED device cards

WLED: native on/off via JSON API. Adalight: sends all-black frame
to blank LEDs (uses existing client if target is running, otherwise
opens temporary serial connection). Toggle button placed next to
delete button in card top-right corner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 19:18:39 +03:00
parent f4503d36b4
commit cc91ccd75a
9 changed files with 193 additions and 4 deletions

View File

@@ -24,7 +24,8 @@ class AdalightDeviceProvider(LEDDeviceProvider):
def capabilities(self) -> set:
# No hardware brightness control, no standby required
# manual_led_count: user must specify LED count (can't auto-detect)
return {"manual_led_count"}
# power_control: can blank LEDs by sending all-black pixels
return {"manual_led_count", "power_control"}
def create_client(self, url: str, **kwargs) -> LEDClient:
from wled_controller.core.adalight_client import AdalightClient
@@ -92,3 +93,12 @@ class AdalightDeviceProvider(LEDDeviceProvider):
except Exception as e:
logger.error(f"Serial port discovery failed: {e}")
return []
async def get_power(self, url: str) -> bool:
# Adalight has no hardware power query; assume on
return True
async def set_power(self, url: str, on: bool) -> None:
# Adalight power control is handled at the API layer via processor manager
# because it needs access to the active serial client or device info.
raise NotImplementedError("Use API-level set_power for Adalight")

View File

@@ -200,6 +200,14 @@ class LEDDeviceProvider(ABC):
"""Set device brightness (0-255). Override if capabilities include brightness_control."""
raise NotImplementedError
async def get_power(self, url: str) -> bool:
"""Get device power state. Override if capabilities include power_control."""
raise NotImplementedError
async def set_power(self, url: str, on: bool) -> None:
"""Set device power state. Override if capabilities include power_control."""
raise NotImplementedError
# ===== PROVIDER REGISTRY =====

View File

@@ -382,6 +382,35 @@ class ProcessorManager:
logger.info(f"Updated calibration for device {device_id}")
async def send_black_frame(self, device_id: str) -> None:
"""Send an all-black frame to an Adalight device to blank its LEDs.
Uses the existing client from a running target if available,
otherwise opens a temporary serial connection.
"""
if device_id not in self._devices:
raise ValueError(f"Device {device_id} not found")
ds = self._devices[device_id]
black = np.zeros((ds.led_count, 3), dtype=np.uint8)
# Try to use existing client from a running target
for ts in self._targets.values():
if ts.device_id == device_id and ts.is_running and ts.led_client:
await ts.led_client.send_pixels(black, brightness=255)
return
# No running target — open a temporary connection
client = create_led_client(
ds.device_type, ds.device_url,
led_count=ds.led_count, baud_rate=ds.baud_rate,
)
try:
await client.connect()
await client.send_pixels(black, brightness=255)
finally:
await client.close()
def get_device_state(self, device_id: str) -> DeviceState:
"""Get device state (for health/calibration info)."""
if device_id not in self._devices:

View File

@@ -30,7 +30,7 @@ class WLEDDeviceProvider(LEDDeviceProvider):
@property
def capabilities(self) -> set:
return {"brightness_control", "standby_required"}
return {"brightness_control", "power_control", "standby_required"}
def create_client(self, url: str, **kwargs) -> LEDClient:
from wled_controller.core.wled_client import WLEDClient
@@ -170,3 +170,19 @@ class WLEDDeviceProvider(LEDDeviceProvider):
json={"bri": brightness},
)
resp.raise_for_status()
async def get_power(self, url: str) -> bool:
url = url.rstrip("/")
async with httpx.AsyncClient(timeout=5.0) as http_client:
resp = await http_client.get(f"{url}/json/state")
resp.raise_for_status()
return resp.json().get("on", False)
async def set_power(self, url: str, on: bool) -> None:
url = url.rstrip("/")
async with httpx.AsyncClient(timeout=5.0) as http_client:
resp = await http_client.post(
f"{url}/json/state",
json={"on": on},
)
resp.raise_for_status()