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:
@@ -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")
|
||||
|
||||
@@ -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 =====
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user