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

@@ -119,18 +119,28 @@ class SerialDeviceProvider(LEDDeviceProvider):
async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None:
"""Send a solid color frame to the device.
Requires kwargs: led_count (int), baud_rate (int | None).
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)
baud_rate = kwargs.get("baud_rate")
if led_count <= 0:
raise ValueError(f"led_count is required to send color frame to {self.device_type} device")
client = self.create_client(url, led_count=led_count, baud_rate=baud_rate)
try:
await client.connect()
frame = np.full((led_count, 3), color, dtype=np.uint8)
await client.send_pixels(frame, brightness=255)
logger.info(f"{self.device_type} set_color: sent solid {color} to {url}")
finally:
await client.close()
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

@@ -1,7 +1,7 @@
"""WLED device provider — consolidates all WLED-specific dispatch logic."""
import asyncio
from typing import List, Optional
from typing import List, Optional, Tuple
import httpx
from zeroconf import ServiceStateChange
@@ -30,7 +30,7 @@ class WLEDDeviceProvider(LEDDeviceProvider):
@property
def capabilities(self) -> set:
return {"brightness_control", "power_control", "standby_required"}
return {"brightness_control", "power_control", "standby_required", "static_color"}
def create_client(self, url: str, **kwargs) -> LEDClient:
from wled_controller.core.devices.wled_client import WLEDClient
@@ -186,3 +186,16 @@ class WLEDDeviceProvider(LEDDeviceProvider):
json={"on": on},
)
resp.raise_for_status()
async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None:
"""Set WLED to a solid color using the native segment color API."""
url = url.rstrip("/")
async with httpx.AsyncClient(timeout=5.0) as http_client:
resp = await http_client.post(
f"{url}/json/state",
json={
"on": True,
"seg": [{"col": [[color[0], color[1], color[2]]], "fx": 0}],
},
)
resp.raise_for_status()