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

@@ -348,11 +348,12 @@ async def get_device_brightness(
raise HTTPException(status_code=400, detail=f"Brightness control is not supported for {device.device_type} devices")
try:
if device.device_type == "adalight":
return {"brightness": device.software_brightness}
provider = get_provider(device.device_type)
bri = await provider.get_brightness(device.url)
return {"brightness": bri}
except NotImplementedError:
# Provider has no hardware brightness; use software brightness
return {"brightness": device.software_brightness}
except Exception as e:
logger.error(f"Failed to get brightness for {device_id}: {e}")
raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}")
@@ -378,16 +379,25 @@ async def set_device_brightness(
raise HTTPException(status_code=400, detail="brightness must be an integer 0-255")
try:
if device.device_type == "adalight":
try:
provider = get_provider(device.device_type)
await provider.set_brightness(device.url, bri)
except NotImplementedError:
# Provider has no hardware brightness; use software brightness
device.software_brightness = bri
device.updated_at = __import__("datetime").datetime.utcnow()
store.save()
# Update runtime state so the processing loop picks it up
if device_id in manager._devices:
manager._devices[device_id].software_brightness = bri
return {"brightness": bri}
provider = get_provider(device.device_type)
await provider.set_brightness(device.url, bri)
# If device is idle with a static color, re-send it at the new brightness
ds = manager._devices.get(device_id)
if ds and ds.static_color is not None and not manager.is_device_processing(device_id):
try:
await manager.send_static_color(device_id, ds.static_color)
except Exception:
pass
return {"brightness": bri}
except Exception as e:
logger.error(f"Failed to set brightness for {device_id}: {e}")
@@ -512,12 +522,15 @@ async def set_device_color(
if ds:
ds.static_color = color
# If device is idle, apply the color immediately
if color is not None and not manager.is_device_processing(device_id):
# If device is idle, apply the change immediately
if not manager.is_device_processing(device_id):
try:
await manager.send_static_color(device_id, color)
if color is not None:
await manager.send_static_color(device_id, color)
else:
await manager.clear_device(device_id)
except Exception as e:
logger.warning(f"Failed to apply static color immediately: {e}")
logger.warning(f"Failed to apply color change immediately: {e}")
return {"color": list(color) if color else None}