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,214 @@
# WLED Screen Controller - Home Assistant Integration
Native Home Assistant integration for WLED Screen Controller with full HACS support.
## Overview
This integration connects Home Assistant to the WLED Screen Controller server, providing:
- 🎛️ **Switch Entities** - Turn processing on/off per device
- 📊 **Sensor Entities** - Monitor FPS, status, and frame count
- 🖥️ **Select Entities** - Choose which display to capture
- 🔄 **Auto-Discovery** - Devices appear automatically
- 📦 **HACS Compatible** - Install directly from HACS
- ⚙️ **Config Flow** - Easy setup through UI
## Installation
### Method 1: HACS (Recommended)
1. **Install HACS** if you haven't already:
- Visit https://hacs.xyz/docs/setup/download
2. **Add Custom Repository:**
- Open HACS in Home Assistant
- Click the menu (⋮) → Custom repositories
- Add URL: `https://github.com/yourusername/wled-screen-controller`
- Category: **Integration**
- Click **Add**
3. **Install Integration:**
- In HACS, search for "WLED Screen Controller"
- Click **Download**
- Restart Home Assistant
4. **Configure:**
- Go to Settings → Devices & Services
- Click **+ Add Integration**
- Search for "WLED Screen Controller"
- Enter your server URL (e.g., `http://192.168.1.100:8080`)
- Click **Submit**
### Method 2: Manual Installation
1. **Download:**
```bash
cd /config # Your Home Assistant config directory
mkdir -p custom_components
```
2. **Copy Files:**
Copy the entire `custom_components/wled_screen_controller` folder to your Home Assistant `custom_components/` directory.
3. **Restart Home Assistant**
4. **Configure:**
- Settings → Devices & Services → Add Integration
- Search for "WLED Screen Controller"
## Configuration
### Initial Setup
When adding the integration, you'll be prompted for:
- **Name**: Friendly name for the integration (default: "WLED Screen Controller")
- **Server URL**: URL of your WLED Screen Controller server (e.g., `http://192.168.1.100:8080`)
The integration will automatically:
- Verify connection to the server
- Discover all configured WLED devices
- Create entities for each device
### Entities Created
For each WLED device, the following entities are created:
#### Switch Entities
**`switch.{device_name}_processing`**
- Controls processing on/off for the device
- Attributes:
- `device_id`: Internal device ID
- `fps_target`: Target FPS
- `fps_actual`: Current FPS
- `display_index`: Active display
- `frames_processed`: Total frames
- `errors_count`: Error count
- `uptime_seconds`: Processing uptime
#### Sensor Entities
**`sensor.{device_name}_fps`**
- Current FPS value
- Unit: FPS
- Attributes:
- `target_fps`: Target FPS setting
**`sensor.{device_name}_status`**
- Processing status
- States: `processing`, `idle`, `unavailable`, `unknown`
**`sensor.{device_name}_frames_processed`**
- Total frames processed counter
- Continuously increasing while processing
#### Select Entities
**`select.{device_name}_display`**
- Select which display to capture
- Options: `Display 0`, `Display 1`, etc.
- Changes take effect immediately
## Usage Examples
### Basic Automation
Turn on processing when TV turns on:
```yaml
automation:
- alias: "Auto Start WLED with TV"
trigger:
- platform: state
entity_id: media_player.living_room_tv
to: "on"
action:
- service: switch.turn_on
target:
entity_id: switch.living_room_wled_processing
- alias: "Auto Stop WLED with TV"
trigger:
- platform: state
entity_id: media_player.living_room_tv
to: "off"
action:
- service: switch.turn_off
target:
entity_id: switch.living_room_wled_processing
```
### Lovelace UI Examples
#### Simple Card
```yaml
type: entities
title: WLED Screen Controller
entities:
- entity: switch.living_room_wled_processing
- entity: sensor.living_room_wled_fps
- entity: sensor.living_room_wled_status
- entity: select.living_room_wled_display
```
#### Advanced Card
```yaml
type: vertical-stack
cards:
- type: entity
entity: switch.living_room_wled_processing
name: Ambient Lighting
icon: mdi:television-ambient-light
- type: conditional
conditions:
- entity: switch.living_room_wled_processing
state: "on"
card:
type: entities
entities:
- entity: sensor.living_room_wled_fps
name: Current FPS
- entity: sensor.living_room_wled_frames_processed
name: Frames Processed
- entity: select.living_room_wled_display
name: Display Selection
```
## Troubleshooting
### Integration Not Appearing
1. Check HACS installation
2. Clear browser cache
3. Restart Home Assistant
4. Check logs: Settings → System → Logs
### Connection Errors
1. Verify server is running:
```bash
curl http://YOUR_SERVER_IP:8080/health
```
2. Check firewall settings
3. Ensure Home Assistant can reach server
4. Try http:// not https://
### Entities Not Updating
1. Check coordinator logs
2. Verify server has devices
3. Restart integration
## Support
- 📖 [Full Documentation](../../INSTALLATION.md)
- 🐛 [Report Issues](https://github.com/yourusername/wled-screen-controller/issues)
## License
MIT License - see [../../LICENSE](../../LICENSE)

View File

@@ -0,0 +1,100 @@
"""The WLED Screen Controller integration."""
from __future__ import annotations
import logging
from datetime import timedelta
from urllib.parse import urlparse
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, CONF_SERVER_URL, DEFAULT_SCAN_INTERVAL
from .coordinator import WLEDScreenControllerCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.SWITCH,
Platform.SENSOR,
Platform.SELECT,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up WLED Screen Controller from a config entry."""
server_url = entry.data[CONF_SERVER_URL]
server_name = entry.data.get(CONF_NAME, "WLED Screen Controller")
session = async_get_clientsession(hass)
coordinator = WLEDScreenControllerCoordinator(
hass,
session,
server_url,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
# Fetch initial data
await coordinator.async_config_entry_first_refresh()
# Create hub device (the server PC)
device_registry = dr.async_get(hass)
# Parse URL for hub identifier
parsed_url = urlparse(server_url)
hub_identifier = f"{parsed_url.hostname}:{parsed_url.port or (443 if parsed_url.scheme == 'https' else 80)}"
hub_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, hub_identifier)},
name=server_name,
manufacturer="WLED Screen Controller",
model="Server",
sw_version=coordinator.server_version,
configuration_url=server_url,
)
# Create device entries for each WLED device
if coordinator.data and "devices" in coordinator.data:
for device_id, device_data in coordinator.data["devices"].items():
device_info = device_data["info"]
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, device_id)},
name=device_info["name"],
manufacturer="WLED",
model="Screen Ambient Lighting",
sw_version=f"{device_info.get('led_count', 0)} LEDs",
via_device=(DOMAIN, hub_identifier), # Link to hub
configuration_url=device_info.get("url"),
)
# Store coordinator and hub info
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
"coordinator": coordinator,
"hub_device_id": hub_device.id,
}
# Setup platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)

View File

@@ -0,0 +1,111 @@
"""Config flow for WLED Screen Controller integration."""
from __future__ import annotations
import logging
from typing import Any
from urllib.parse import urlparse, urlunparse
import aiohttp
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, CONF_SERVER_URL, DEFAULT_TIMEOUT
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME, default="WLED Screen Controller"): str,
vol.Required(CONF_SERVER_URL, default="http://localhost:8080"): str,
}
)
def normalize_url(url: str) -> str:
"""Normalize URL to ensure port is an integer."""
parsed = urlparse(url)
# If port is specified, ensure it's an integer
if parsed.port is not None:
# Reconstruct URL with integer port
netloc = parsed.hostname or "localhost"
port = int(parsed.port) # Cast to int to avoid float
if port != (443 if parsed.scheme == "https" else 80):
netloc = f"{netloc}:{port}"
parsed = parsed._replace(netloc=netloc)
return urlunparse(parsed)
async def validate_server_connection(
hass: HomeAssistant, server_url: str
) -> dict[str, Any]:
"""Validate the server URL by checking the health endpoint."""
session = async_get_clientsession(hass)
try:
async with session.get(
f"{server_url}/health",
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
if response.status == 200:
data = await response.json()
return {
"version": data.get("version", "unknown"),
"status": data.get("status", "unknown"),
}
raise ConnectionError(f"Server returned status {response.status}")
except aiohttp.ClientError as err:
raise ConnectionError(f"Cannot connect to server: {err}")
except Exception as err:
raise ConnectionError(f"Unexpected error: {err}")
class WLEDScreenControllerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for WLED Screen Controller."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
server_url = normalize_url(user_input[CONF_SERVER_URL].rstrip("/"))
try:
info = await validate_server_connection(self.hass, server_url)
# Set unique ID based on server URL
await self.async_set_unique_id(server_url)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_NAME],
data={
CONF_SERVER_URL: server_url,
"version": info["version"],
},
)
except ConnectionError as err:
_LOGGER.error("Connection error: %s", err)
errors["base"] = "cannot_connect"
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception: %s", err)
errors["base"] = "unknown"
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)

View File

@@ -0,0 +1,23 @@
"""Constants for the WLED Screen Controller integration."""
DOMAIN = "wled_screen_controller"
# Configuration
CONF_SERVER_URL = "server_url"
# Default values
DEFAULT_SCAN_INTERVAL = 10 # seconds
DEFAULT_TIMEOUT = 10 # seconds
# Attributes
ATTR_DEVICE_ID = "device_id"
ATTR_FPS_ACTUAL = "fps_actual"
ATTR_FPS_TARGET = "fps_target"
ATTR_DISPLAY_INDEX = "display_index"
ATTR_FRAMES_PROCESSED = "frames_processed"
ATTR_ERRORS_COUNT = "errors_count"
ATTR_UPTIME = "uptime_seconds"
# Services
SERVICE_START_PROCESSING = "start_processing"
SERVICE_STOP_PROCESSING = "stop_processing"

View File

@@ -0,0 +1,179 @@
"""Data update coordinator for WLED Screen Controller."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
from typing import Any
import aiohttp
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, DEFAULT_TIMEOUT
_LOGGER = logging.getLogger(__name__)
class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
"""Class to manage fetching WLED Screen Controller data."""
def __init__(
self,
hass: HomeAssistant,
session: aiohttp.ClientSession,
server_url: str,
update_interval: timedelta,
) -> None:
"""Initialize the coordinator."""
self.server_url = server_url
self.session = session
self.server_version = "unknown"
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=update_interval,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from API."""
try:
async with asyncio.timeout(DEFAULT_TIMEOUT):
# Fetch server version on first update
if self.server_version == "unknown":
await self._fetch_server_version()
# Fetch devices list
devices = await self._fetch_devices()
# Fetch state for each device
devices_data = {}
for device in devices:
device_id = device["id"]
try:
state = await self._fetch_device_state(device_id)
metrics = await self._fetch_device_metrics(device_id)
devices_data[device_id] = {
"info": device,
"state": state,
"metrics": metrics,
}
except Exception as err:
_LOGGER.warning(
"Failed to fetch data for device %s: %s", device_id, err
)
# Include device info even if state fetch fails
devices_data[device_id] = {
"info": device,
"state": None,
"metrics": None,
}
# Fetch available displays
displays = await self._fetch_displays()
return {
"devices": devices_data,
"displays": displays,
}
except asyncio.TimeoutError as err:
raise UpdateFailed(f"Timeout fetching data: {err}") from err
except aiohttp.ClientError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
async def _fetch_server_version(self) -> None:
"""Fetch server version from health endpoint."""
try:
async with self.session.get(
f"{self.server_url}/health",
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
response.raise_for_status()
data = await response.json()
self.server_version = data.get("version", "unknown")
except Exception as err:
_LOGGER.warning("Failed to fetch server version: %s", err)
self.server_version = "unknown"
async def _fetch_devices(self) -> list[dict[str, Any]]:
"""Fetch devices list."""
async with self.session.get(
f"{self.server_url}/api/v1/devices",
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
response.raise_for_status()
data = await response.json()
return data.get("devices", [])
async def _fetch_device_state(self, device_id: str) -> dict[str, Any]:
"""Fetch device processing state."""
async with self.session.get(
f"{self.server_url}/api/v1/devices/{device_id}/state",
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
response.raise_for_status()
return await response.json()
async def _fetch_device_metrics(self, device_id: str) -> dict[str, Any]:
"""Fetch device metrics."""
async with self.session.get(
f"{self.server_url}/api/v1/devices/{device_id}/metrics",
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
response.raise_for_status()
return await response.json()
async def _fetch_displays(self) -> list[dict[str, Any]]:
"""Fetch available displays."""
try:
async with self.session.get(
f"{self.server_url}/api/v1/config/displays",
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
response.raise_for_status()
data = await response.json()
return data.get("displays", [])
except Exception as err:
_LOGGER.warning("Failed to fetch displays: %s", err)
return []
async def start_processing(self, device_id: str) -> None:
"""Start processing for a device."""
async with self.session.post(
f"{self.server_url}/api/v1/devices/{device_id}/start",
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
response.raise_for_status()
# Refresh data immediately
await self.async_request_refresh()
async def stop_processing(self, device_id: str) -> None:
"""Stop processing for a device."""
async with self.session.post(
f"{self.server_url}/api/v1/devices/{device_id}/stop",
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
response.raise_for_status()
# Refresh data immediately
await self.async_request_refresh()
async def update_settings(
self, device_id: str, settings: dict[str, Any]
) -> None:
"""Update device settings."""
async with self.session.put(
f"{self.server_url}/api/v1/devices/{device_id}/settings",
json=settings,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as response:
response.raise_for_status()
# Refresh data immediately
await self.async_request_refresh()

View File

@@ -0,0 +1,12 @@
{
"domain": "wled_screen_controller",
"name": "WLED Screen Controller",
"codeowners": ["@alexeidolgolyov"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/yourusername/wled-screen-controller",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/yourusername/wled-screen-controller/issues",
"requirements": ["aiohttp>=3.9.0"],
"version": "0.1.0"
}

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

View 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)

View File

@@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"title": "Set up WLED Screen Controller",
"description": "Enter the URL of your WLED Screen Controller server",
"data": {
"name": "Name",
"server_url": "Server URL"
}
}
},
"error": {
"cannot_connect": "Failed to connect to server. Check the URL and ensure the server is running.",
"unknown": "Unexpected error occurred"
},
"abort": {
"already_configured": "This server is already configured"
}
}
}

View File

@@ -0,0 +1,133 @@
"""Switch platform for WLED Screen Controller."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.switch import SwitchEntity
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, ATTR_DEVICE_ID
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 switches."""
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():
entities.append(
WLEDScreenControllerSwitch(
coordinator, device_id, device_data["info"], entry.entry_id
)
)
async_add_entities(entities)
class WLEDScreenControllerSwitch(CoordinatorEntity, SwitchEntity):
"""Representation of a WLED Screen Controller processing switch."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: WLEDScreenControllerCoordinator,
device_id: str,
device_info: dict[str, Any],
entry_id: str,
) -> None:
"""Initialize the switch."""
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}_processing"
self._attr_name = "Processing"
self._attr_icon = "mdi:television-ambient-light"
@property
def device_info(self) -> dict[str, Any]:
"""Return device information."""
return {
"identifiers": {(DOMAIN, self._device_id)},
}
@property
def is_on(self) -> bool:
"""Return true if processing is active."""
if not self.coordinator.data:
return False
device_data = self.coordinator.data["devices"].get(self._device_id)
if not device_data or not device_data.get("state"):
return False
return device_data["state"].get("processing", False)
@property
def available(self) -> bool:
"""Return if entity is available."""
if not self.coordinator.data:
return False
device_data = self.coordinator.data["devices"].get(self._device_id)
return device_data is not None
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
if not self.coordinator.data:
return {}
device_data = self.coordinator.data["devices"].get(self._device_id)
if not device_data:
return {}
state = device_data.get("state", {})
metrics = device_data.get("metrics", {})
attrs = {
ATTR_DEVICE_ID: self._device_id,
}
if state:
attrs["fps_target"] = state.get("fps_target")
attrs["fps_actual"] = state.get("fps_actual")
attrs["display_index"] = state.get("display_index")
if metrics:
attrs["frames_processed"] = metrics.get("frames_processed")
attrs["errors_count"] = metrics.get("errors_count")
attrs["uptime_seconds"] = metrics.get("uptime_seconds")
return attrs
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on processing."""
try:
await self.coordinator.start_processing(self._device_id)
except Exception as err:
_LOGGER.error("Failed to start processing: %s", err)
raise
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off processing."""
try:
await self.coordinator.stop_processing(self._device_id)
except Exception as err:
_LOGGER.error("Failed to stop processing: %s", err)
raise

View File

@@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"title": "Set up WLED Screen Controller",
"description": "Enter the URL of your WLED Screen Controller server",
"data": {
"name": "Name",
"server_url": "Server URL"
}
}
},
"error": {
"cannot_connect": "Failed to connect to server. Check the URL and ensure the server is running.",
"unknown": "Unexpected error occurred"
},
"abort": {
"already_configured": "This server is already configured"
}
}
}