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:
205
custom_components/wled_screen_controller/sensor.py
Normal file
205
custom_components/wled_screen_controller/sensor.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""Sensor platform for WLED Screen Controller."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTime
|
||||
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 sensors."""
|
||||
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"]
|
||||
|
||||
# FPS sensor
|
||||
entities.append(
|
||||
WLEDScreenControllerFPSSensor(
|
||||
coordinator, device_id, device_info, entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
# Status sensor
|
||||
entities.append(
|
||||
WLEDScreenControllerStatusSensor(
|
||||
coordinator, device_id, device_info, entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
# Frames processed sensor
|
||||
entities.append(
|
||||
WLEDScreenControllerFramesSensor(
|
||||
coordinator, device_id, device_info, entry.entry_id
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class WLEDScreenControllerFPSSensor(CoordinatorEntity, SensorEntity):
|
||||
"""FPS sensor for WLED Screen Controller."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_native_unit_of_measurement = "FPS"
|
||||
_attr_icon = "mdi:speedometer"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WLEDScreenControllerCoordinator,
|
||||
device_id: str,
|
||||
device_info: dict[str, Any],
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
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}_fps"
|
||||
self._attr_name = "FPS"
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict[str, Any]:
|
||||
"""Return device information."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device_id)},
|
||||
}
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the FPS value."""
|
||||
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
|
||||
|
||||
return device_data["state"].get("fps_actual")
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return additional attributes."""
|
||||
if not self.coordinator.data:
|
||||
return {}
|
||||
|
||||
device_data = self.coordinator.data["devices"].get(self._device_id)
|
||||
if not device_data or not device_data.get("state"):
|
||||
return {}
|
||||
|
||||
return {
|
||||
"target_fps": device_data["state"].get("fps_target"),
|
||||
}
|
||||
|
||||
|
||||
class WLEDScreenControllerStatusSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Status sensor for WLED Screen Controller."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_icon = "mdi:information-outline"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WLEDScreenControllerCoordinator,
|
||||
device_id: str,
|
||||
device_info: dict[str, Any],
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
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}_status"
|
||||
self._attr_name = "Status"
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict[str, Any]:
|
||||
"""Return device information."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device_id)},
|
||||
}
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
"""Return the status."""
|
||||
if not self.coordinator.data:
|
||||
return "unknown"
|
||||
|
||||
device_data = self.coordinator.data["devices"].get(self._device_id)
|
||||
if not device_data:
|
||||
return "unavailable"
|
||||
|
||||
if device_data.get("state") and device_data["state"].get("processing"):
|
||||
return "processing"
|
||||
|
||||
return "idle"
|
||||
|
||||
|
||||
class WLEDScreenControllerFramesSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Frames processed sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
_attr_icon = "mdi:counter"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WLEDScreenControllerCoordinator,
|
||||
device_id: str,
|
||||
device_info: dict[str, Any],
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
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}_frames"
|
||||
self._attr_name = "Frames Processed"
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict[str, Any]:
|
||||
"""Return device information."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device_id)},
|
||||
}
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return frames processed."""
|
||||
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("metrics"):
|
||||
return None
|
||||
|
||||
return device_data["metrics"].get("frames_processed", 0)
|
||||
Reference in New Issue
Block a user