Add static color support, HAOS light entity, and real-time profile updates

- Add static_color capability to WLED and serial providers with native
  set_color() dispatch (WLED uses JSON API, serial uses idle client)
- Encapsulate device-specific logic in providers instead of device_type
  checks in ProcessorManager and API routes
- Add HAOS light entity for devices with brightness_control + static_color
  (Adalight/AmbiLED get light entity, WLED keeps number entity)
- Fix serial device brightness and turn-off: pass software_brightness
  through provider chain, clear device on color=null, re-send static
  color after brightness change
- Add global events WebSocket (events-ws.js) replacing per-tab WS,
  enabling real-time profile state updates on both dashboard and profiles tabs
- Fix profile activation: mark active when all targets already running,
  add asyncio.Lock to prevent concurrent evaluation races, skip process
  enumeration when no profile has conditions, trigger immediate evaluation
  on enable/create/update for instant target startup
- Add reliable server restart script (restart.ps1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 14:23:47 +03:00
parent 6388e0defa
commit bef28ece5c
21 changed files with 410 additions and 78 deletions

View File

@@ -597,18 +597,31 @@ class ProcessorManager:
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 the cached idle client."""
import numpy as np
"""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)
frame = np.full((ds.led_count, 3), color, dtype=np.uint8)
await client.send_pixels(frame)
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)
if not ds:
raise ValueError(f"Device {device_id} not found")
try:
await self._send_clear_pixels(device_id)
except Exception as e:
logger.error(f"Failed to clear device {device_id}: {e}")
async def _restore_device_idle_state(self, device_id: str) -> None:
"""Restore a device to its idle state when all targets stop.