Remove idle color feature, simplify power to turn-off only, fix settings serial port bug

- Remove static/idle color from entire stack (storage, API, processing, UI, CSS, locales)
- Simplify device power button to turn-off only (send black frame, no toggle)
- Send black frame on serial port close (AdalightClient.close)
- Fix settings modal serial port dropdown showing WLED devices due to stale deviceType

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 04:04:28 +03:00
parent 1f6c913343
commit 8a0730d91b
12 changed files with 29 additions and 278 deletions

View File

@@ -100,7 +100,14 @@ class AdalightClient(LEDClient):
raise RuntimeError(f"Failed to open serial port {self._port}: {e}")
async def close(self) -> None:
"""Close the serial port."""
"""Send black frame and close the serial port."""
if self._connected and self._serial and self._serial.is_open and self._led_count > 0:
try:
black = np.zeros((self._led_count, 3), dtype=np.uint8)
frame = self._build_frame(black, brightness=255)
await asyncio.to_thread(self._serial.write, frame)
except Exception as e:
logger.debug(f"Failed to send black frame on close: {e}")
self._connected = False
if self._serial and self._serial.is_open:
try:

View File

@@ -2,10 +2,10 @@
Subclasses only need to override ``device_type`` and ``create_client()``.
All common serial-device logic (COM port validation, discovery, health
checks, power control via black frames, static colour) lives here.
checks, power control via black frames) lives here.
"""
from typing import List, Tuple
from typing import List
import numpy as np
@@ -28,8 +28,7 @@ class SerialDeviceProvider(LEDDeviceProvider):
# manual_led_count: user must specify LED count (can't auto-detect)
# power_control: can blank LEDs by sending all-black pixels
# brightness_control: software brightness (multiplies pixel values before sending)
# static_color: can send a solid colour frame
return {"manual_led_count", "power_control", "brightness_control", "static_color"}
return {"manual_led_count", "power_control", "brightness_control"}
async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
# Generic serial port health check — enumerate COM ports
@@ -116,31 +115,3 @@ class SerialDeviceProvider(LEDDeviceProvider):
finally:
await client.close()
async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None:
"""Send a solid color frame to the device.
Accepts optional kwargs:
client: An already-connected LEDClient (e.g. cached idle client).
brightness (int): Software brightness 0-255 (default 255).
led_count (int), baud_rate (int | None).
"""
led_count = kwargs.get("led_count", 0)
if led_count <= 0:
raise ValueError(f"led_count is required to send color frame to {self.device_type} device")
brightness = kwargs.get("brightness", 255)
frame = np.full((led_count, 3), color, dtype=np.uint8)
existing_client = kwargs.get("client")
if existing_client:
await existing_client.send_pixels(frame, brightness=brightness)
else:
baud_rate = kwargs.get("baud_rate")
client = self.create_client(url, led_count=led_count, baud_rate=baud_rate)
try:
await client.connect()
await client.send_pixels(frame, brightness=brightness)
finally:
await client.close()
logger.info(f"{self.device_type} set_color: sent solid {color} to {url}")

View File

@@ -47,8 +47,6 @@ class DeviceState:
hardware_brightness: Optional[int] = None
# Auto-restore: restore device to idle state when targets stop
auto_shutdown: bool = False
# Static idle color for devices without a rich editor (e.g. Adalight)
static_color: Optional[Tuple[int, int, int]] = None
# Calibration test mode (works independently of target processing)
test_mode_active: bool = False
test_mode_edges: Dict[str, Tuple[int, int, int]] = field(default_factory=dict)
@@ -159,7 +157,6 @@ class ProcessorManager:
baud_rate: Optional[int] = None,
software_brightness: int = 255,
auto_shutdown: bool = False,
static_color: Optional[Tuple[int, int, int]] = None,
):
"""Register a device for health monitoring."""
if device_id in self._devices:
@@ -173,7 +170,6 @@ class ProcessorManager:
baud_rate=baud_rate,
software_brightness=software_brightness,
auto_shutdown=auto_shutdown,
static_color=static_color,
)
self._devices[device_id] = state
@@ -634,22 +630,6 @@ class ProcessorManager:
return proc.device_id
return None
async def send_static_color(self, device_id: str, color: Tuple[int, int, int]) -> None:
"""Send a solid color to a device via its provider."""
ds = self._devices.get(device_id)
if not ds:
raise ValueError(f"Device {device_id} not found")
try:
provider = get_provider(ds.device_type)
client = await self._get_idle_client(device_id)
await provider.set_color(
ds.device_url, color,
led_count=ds.led_count, baud_rate=ds.baud_rate, client=client,
brightness=ds.software_brightness,
)
except Exception as e:
logger.error(f"Failed to send static color for {device_id}: {e}")
async def clear_device(self, device_id: str) -> None:
"""Clear LED output on a device (send black / power off)."""
ds = self._devices.get(device_id)
@@ -663,9 +643,8 @@ class ProcessorManager:
async def _restore_device_idle_state(self, device_id: str) -> None:
"""Restore a device to its idle state when all targets stop.
- If a static color is configured, send it.
- For WLED: do nothing — stop() already restored the snapshot.
- For other devices without static color: power off (black frame).
- For other devices: power off (send black frame).
"""
ds = self._devices.get(device_id)
if not ds or not ds.auto_shutdown:
@@ -675,14 +654,7 @@ class ProcessorManager:
return
try:
if ds.static_color is not None:
await self.send_static_color(device_id, ds.static_color)
logger.info(
f"Auto-restore: sent static color {ds.static_color} "
f"to {ds.device_type} device {device_id}"
)
elif ds.device_type != "wled":
# Non-WLED without static color: power off (send black frame)
if ds.device_type != "wled":
await self._send_clear_pixels(device_id)
logger.info(f"Auto-restore: powered off {ds.device_type} device {device_id}")
else: