Add profile conditions, scene presets, MQTT integration, and Scenes tab
Feature 1 — Profile Conditions: time-of-day, system idle (Win32 GetLastInputInfo), and display state (GUID_CONSOLE_DISPLAY_STATE) condition types for automatic profile activation. Feature 2 — Scene Presets: snapshot/restore system that captures target running states, device brightness, and profile enables. Server-side capture with 5-step activation order. Dedicated Scenes tab with CardSection-based card grid, command palette integration, and dashboard quick-activate section. Feature 3 — MQTT Integration: MQTTService singleton with aiomqtt, MQTTLEDClient device provider for pixel output, MQTT profile condition type with topic/payload matching, and frontend support for MQTT device type and condition editor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -282,5 +282,8 @@ def _register_builtin_providers():
|
||||
from wled_controller.core.devices.mock_provider import MockDeviceProvider
|
||||
register_provider(MockDeviceProvider())
|
||||
|
||||
from wled_controller.core.devices.mqtt_provider import MQTTDeviceProvider
|
||||
register_provider(MQTTDeviceProvider())
|
||||
|
||||
|
||||
_register_builtin_providers()
|
||||
|
||||
98
server/src/wled_controller/core/devices/mqtt_client.py
Normal file
98
server/src/wled_controller/core/devices/mqtt_client.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""MQTT LED client — publishes pixel data to an MQTT topic."""
|
||||
|
||||
import json
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.devices.led_client import DeviceHealth, LEDClient
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Singleton reference — injected from main.py at startup
|
||||
_mqtt_service = None
|
||||
|
||||
|
||||
def set_mqtt_service(service) -> None:
|
||||
global _mqtt_service
|
||||
_mqtt_service = service
|
||||
|
||||
|
||||
def get_mqtt_service():
|
||||
return _mqtt_service
|
||||
|
||||
|
||||
def parse_mqtt_url(url: str) -> str:
|
||||
"""Extract topic from an mqtt:// URL.
|
||||
|
||||
Format: mqtt://topic/path (broker connection is global via config)
|
||||
"""
|
||||
if url.startswith("mqtt://"):
|
||||
return url[7:]
|
||||
return url
|
||||
|
||||
|
||||
class MQTTLEDClient(LEDClient):
|
||||
"""Publishes JSON pixel data to an MQTT topic via the shared service."""
|
||||
|
||||
def __init__(self, url: str, led_count: int = 0, **kwargs):
|
||||
self._topic = parse_mqtt_url(url)
|
||||
self._led_count = led_count
|
||||
self._connected = False
|
||||
|
||||
async def connect(self) -> bool:
|
||||
svc = _mqtt_service
|
||||
if svc is None or not svc.is_enabled:
|
||||
raise ConnectionError("MQTT service not available")
|
||||
if not svc.is_connected:
|
||||
raise ConnectionError("MQTT service not connected to broker")
|
||||
self._connected = True
|
||||
return True
|
||||
|
||||
async def close(self) -> None:
|
||||
self._connected = False
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._connected and _mqtt_service is not None and _mqtt_service.is_connected
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
svc = _mqtt_service
|
||||
if svc is None or not svc.is_connected:
|
||||
return False
|
||||
|
||||
if isinstance(pixels, np.ndarray):
|
||||
pixel_list = pixels.tolist()
|
||||
else:
|
||||
pixel_list = list(pixels)
|
||||
|
||||
payload = json.dumps({
|
||||
"pixels": pixel_list,
|
||||
"brightness": brightness,
|
||||
"led_count": len(pixel_list),
|
||||
})
|
||||
|
||||
await svc.publish(self._topic, payload, retain=False, qos=0)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def check_health(
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health=None,
|
||||
) -> DeviceHealth:
|
||||
from datetime import datetime
|
||||
svc = _mqtt_service
|
||||
if svc is None or not svc.is_enabled:
|
||||
return DeviceHealth(online=False, error="MQTT disabled", last_checked=datetime.utcnow())
|
||||
return DeviceHealth(
|
||||
online=svc.is_connected,
|
||||
last_checked=datetime.utcnow(),
|
||||
error=None if svc.is_connected else "MQTT broker disconnected",
|
||||
)
|
||||
51
server/src/wled_controller/core/devices/mqtt_provider.py
Normal file
51
server/src/wled_controller/core/devices/mqtt_provider.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""MQTT device provider — factory, validation, health checks."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from wled_controller.core.devices.led_client import (
|
||||
DeviceHealth,
|
||||
DiscoveredDevice,
|
||||
LEDClient,
|
||||
LEDDeviceProvider,
|
||||
)
|
||||
from wled_controller.core.devices.mqtt_client import (
|
||||
MQTTLEDClient,
|
||||
get_mqtt_service,
|
||||
parse_mqtt_url,
|
||||
)
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MQTTDeviceProvider(LEDDeviceProvider):
|
||||
"""Provider for MQTT-based LED devices."""
|
||||
|
||||
@property
|
||||
def device_type(self) -> str:
|
||||
return "mqtt"
|
||||
|
||||
@property
|
||||
def capabilities(self) -> set:
|
||||
return {"manual_led_count"}
|
||||
|
||||
def create_client(self, url: str, **kwargs) -> LEDClient:
|
||||
return MQTTLEDClient(url, **kwargs)
|
||||
|
||||
async def check_health(
|
||||
self, url: str, http_client, prev_health=None,
|
||||
) -> DeviceHealth:
|
||||
return await MQTTLEDClient.check_health(url, http_client, prev_health)
|
||||
|
||||
async def validate_device(self, url: str) -> dict:
|
||||
"""Validate MQTT device URL (topic path)."""
|
||||
topic = parse_mqtt_url(url)
|
||||
if not topic or topic == "/":
|
||||
raise ValueError("MQTT topic cannot be empty")
|
||||
# Can't auto-detect LED count — require manual entry
|
||||
return {}
|
||||
|
||||
async def discover(self, timeout: float = 3.0) -> List[DiscoveredDevice]:
|
||||
# MQTT devices are not auto-discoverable
|
||||
return []
|
||||
Reference in New Issue
Block a user