Initial commit: WLED Screen Controller with FastAPI server and Home Assistant integration
Some checks failed
Validate / validate (push) Failing after 1m6s
Some checks failed
Validate / validate (push) Failing after 1m6s
This is a complete WLED ambient lighting controller that captures screen border pixels and sends them to WLED devices for immersive ambient lighting effects. ## Server Features: - FastAPI-based REST API with 17+ endpoints - Real-time screen capture with multi-monitor support - Advanced LED calibration system with visual GUI - API key authentication with labeled tokens - Per-device brightness control (0-100%) - Configurable FPS (1-60), border width, and color correction - Persistent device storage (JSON-based) - Comprehensive Web UI with dark/light themes - Docker support with docker-compose - Windows monitor name detection via WMI (shows "LG ULTRAWIDE" etc.) ## Web UI Features: - Device management (add, configure, remove WLED devices) - Real-time status monitoring with FPS metrics - Settings modal for device configuration - Visual calibration GUI with edge testing - Brightness slider per device - Display selection with friendly monitor names - Token-based authentication with login/logout - Responsive button layout ## Calibration System: - Support for any LED strip layout (clockwise/counterclockwise) - 4 starting position options (corners) - Per-edge LED count configuration - Visual preview with starting position indicator - Test buttons to light up individual edges - Smart LED ordering based on start position and direction ## Home Assistant Integration: - Custom HACS integration - Switch entities for processing control - Sensor entities for status and FPS - Select entities for display selection - Config flow for easy setup - Auto-discovery of devices from server ## Technical Stack: - Python 3.11+ - FastAPI + uvicorn - mss (screen capture) - httpx (async WLED client) - Pydantic (validation) - WMI (Windows monitor detection) - Structlog (logging) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
117
custom_components/wled_screen_controller/select.py
Normal file
117
custom_components/wled_screen_controller/select.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Select platform for WLED Screen Controller."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import WLEDScreenControllerCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up WLED Screen Controller select entities."""
|
||||
data = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: WLEDScreenControllerCoordinator = data["coordinator"]
|
||||
|
||||
entities = []
|
||||
if coordinator.data and "devices" in coordinator.data:
|
||||
for device_id, device_data in coordinator.data["devices"].items():
|
||||
device_info = device_data["info"]
|
||||
entities.append(
|
||||
WLEDScreenControllerDisplaySelect(
|
||||
coordinator, device_id, device_info, entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class WLEDScreenControllerDisplaySelect(CoordinatorEntity, SelectEntity):
|
||||
"""Display selection for WLED Screen Controller."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_icon = "mdi:monitor-multiple"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WLEDScreenControllerCoordinator,
|
||||
device_id: str,
|
||||
device_info: dict[str, Any],
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
"""Initialize the select."""
|
||||
super().__init__(coordinator)
|
||||
self._device_id = device_id
|
||||
self._device_info = device_info
|
||||
self._entry_id = entry_id
|
||||
|
||||
self._attr_unique_id = f"{device_id}_display"
|
||||
self._attr_name = "Display"
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict[str, Any]:
|
||||
"""Return device information."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device_id)},
|
||||
}
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return available display options."""
|
||||
if not self.coordinator.data or "displays" not in self.coordinator.data:
|
||||
return ["Display 0"]
|
||||
|
||||
displays = self.coordinator.data["displays"]
|
||||
return [f"Display {d['index']}" for d in displays]
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return current display."""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
device_data = self.coordinator.data["devices"].get(self._device_id)
|
||||
if not device_data or not device_data.get("state"):
|
||||
return None
|
||||
|
||||
display_index = device_data["state"].get("display_index", 0)
|
||||
return f"Display {display_index}"
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected display."""
|
||||
try:
|
||||
# Extract display index from option (e.g., "Display 1" -> 1)
|
||||
display_index = int(option.split()[-1])
|
||||
|
||||
# Get current settings
|
||||
device_data = self.coordinator.data["devices"].get(self._device_id)
|
||||
if not device_data:
|
||||
return
|
||||
|
||||
info = device_data["info"]
|
||||
settings = info.get("settings", {})
|
||||
|
||||
# Update settings with new display index
|
||||
updated_settings = {
|
||||
"display_index": display_index,
|
||||
"fps": settings.get("fps", 30),
|
||||
"border_width": settings.get("border_width", 10),
|
||||
}
|
||||
|
||||
await self.coordinator.update_settings(self._device_id, updated_settings)
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error("Failed to update display: %s", err)
|
||||
raise
|
||||
Reference in New Issue
Block a user