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>
254 lines
6.6 KiB
Python
254 lines
6.6 KiB
Python
"""Tests for WLED client."""
|
|
|
|
import pytest
|
|
import respx
|
|
from httpx import Response
|
|
|
|
from wled_controller.core.wled_client import WLEDClient, WLEDInfo
|
|
|
|
|
|
@pytest.fixture
|
|
def wled_url():
|
|
"""Provide test WLED device URL."""
|
|
return "http://192.168.1.100"
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_wled_info():
|
|
"""Provide mock WLED info response."""
|
|
return {
|
|
"name": "Test WLED",
|
|
"ver": "0.14.0",
|
|
"leds": {"count": 150},
|
|
"brand": "WLED",
|
|
"product": "FOSS",
|
|
"mac": "AA:BB:CC:DD:EE:FF",
|
|
"ip": "192.168.1.100",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_wled_state():
|
|
"""Provide mock WLED state response."""
|
|
return {
|
|
"on": True,
|
|
"bri": 255,
|
|
"seg": [{"id": 0, "on": True}],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_wled_client_connect(wled_url, mock_wled_info):
|
|
"""Test connecting to WLED device."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
|
|
client = WLEDClient(wled_url)
|
|
success = await client.connect()
|
|
|
|
assert success is True
|
|
assert client.is_connected is True
|
|
|
|
await client.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_wled_client_connect_failure(wled_url):
|
|
"""Test connection failure handling."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(500, text="Internal Server Error")
|
|
)
|
|
|
|
client = WLEDClient(wled_url, retry_attempts=1)
|
|
|
|
with pytest.raises(RuntimeError):
|
|
await client.connect()
|
|
|
|
assert client.is_connected is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_get_info(wled_url, mock_wled_info):
|
|
"""Test getting device info."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
|
|
async with WLEDClient(wled_url) as client:
|
|
info = await client.get_info()
|
|
|
|
assert isinstance(info, WLEDInfo)
|
|
assert info.name == "Test WLED"
|
|
assert info.version == "0.14.0"
|
|
assert info.led_count == 150
|
|
assert info.mac == "AA:BB:CC:DD:EE:FF"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_get_state(wled_url, mock_wled_info, mock_wled_state):
|
|
"""Test getting device state."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
respx.get(f"{wled_url}/json/state").mock(
|
|
return_value=Response(200, json=mock_wled_state)
|
|
)
|
|
|
|
async with WLEDClient(wled_url) as client:
|
|
state = await client.get_state()
|
|
|
|
assert state["on"] is True
|
|
assert state["bri"] == 255
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_send_pixels(wled_url, mock_wled_info):
|
|
"""Test sending pixel data."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
respx.post(f"{wled_url}/json/state").mock(
|
|
return_value=Response(200, json={"success": True})
|
|
)
|
|
|
|
async with WLEDClient(wled_url) as client:
|
|
pixels = [
|
|
(255, 0, 0), # Red
|
|
(0, 255, 0), # Green
|
|
(0, 0, 255), # Blue
|
|
]
|
|
|
|
success = await client.send_pixels(pixels, brightness=200)
|
|
assert success is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_send_pixels_invalid_values(wled_url, mock_wled_info):
|
|
"""Test sending invalid pixel values."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
|
|
async with WLEDClient(wled_url) as client:
|
|
# Invalid RGB value
|
|
with pytest.raises(ValueError):
|
|
await client.send_pixels([(300, 0, 0)])
|
|
|
|
# Invalid brightness
|
|
with pytest.raises(ValueError):
|
|
await client.send_pixels([(255, 0, 0)], brightness=300)
|
|
|
|
# Empty pixels list
|
|
with pytest.raises(ValueError):
|
|
await client.send_pixels([])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_set_power(wled_url, mock_wled_info):
|
|
"""Test turning device on/off."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
respx.post(f"{wled_url}/json/state").mock(
|
|
return_value=Response(200, json={"success": True})
|
|
)
|
|
|
|
async with WLEDClient(wled_url) as client:
|
|
# Turn on
|
|
success = await client.set_power(True)
|
|
assert success is True
|
|
|
|
# Turn off
|
|
success = await client.set_power(False)
|
|
assert success is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_set_brightness(wled_url, mock_wled_info):
|
|
"""Test setting brightness."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
respx.post(f"{wled_url}/json/state").mock(
|
|
return_value=Response(200, json={"success": True})
|
|
)
|
|
|
|
async with WLEDClient(wled_url) as client:
|
|
success = await client.set_brightness(128)
|
|
assert success is True
|
|
|
|
# Invalid brightness
|
|
with pytest.raises(ValueError):
|
|
await client.set_brightness(300)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_test_connection(wled_url, mock_wled_info):
|
|
"""Test connection testing."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
|
|
async with WLEDClient(wled_url) as client:
|
|
success = await client.test_connection()
|
|
assert success is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_retry_logic(wled_url, mock_wled_info):
|
|
"""Test retry logic on failures."""
|
|
# Mock to fail twice, then succeed
|
|
call_count = 0
|
|
|
|
def mock_response(request):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count < 3:
|
|
return Response(500, text="Error")
|
|
return Response(200, json=mock_wled_info)
|
|
|
|
respx.get(f"{wled_url}/json/info").mock(side_effect=mock_response)
|
|
|
|
client = WLEDClient(wled_url, retry_attempts=3, retry_delay=0.1)
|
|
success = await client.connect()
|
|
|
|
assert success is True
|
|
assert call_count == 3 # Failed 2 times, succeeded on 3rd
|
|
|
|
await client.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_context_manager(wled_url, mock_wled_info):
|
|
"""Test async context manager usage."""
|
|
respx.get(f"{wled_url}/json/info").mock(
|
|
return_value=Response(200, json=mock_wled_info)
|
|
)
|
|
|
|
async with WLEDClient(wled_url) as client:
|
|
assert client.is_connected is True
|
|
|
|
# Client should be closed after context
|
|
assert client.is_connected is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_without_connection(wled_url):
|
|
"""Test making request without connecting first."""
|
|
client = WLEDClient(wled_url)
|
|
|
|
with pytest.raises(RuntimeError):
|
|
await client.get_state()
|