Initial commit: WLED Screen Controller with FastAPI server and Home Assistant integration
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:
2026-02-06 16:38:27 +03:00
commit d471a40234
57 changed files with 9726 additions and 0 deletions

View 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