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>
306 lines
8.0 KiB
Python
306 lines
8.0 KiB
Python
"""Tests for device storage."""
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from wled_controller.storage.device_store import Device, DeviceStore
|
|
from wled_controller.core.processor_manager import ProcessingSettings
|
|
from wled_controller.core.calibration import create_default_calibration
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_storage(tmp_path):
|
|
"""Provide temporary storage file."""
|
|
return tmp_path / "devices.json"
|
|
|
|
|
|
@pytest.fixture
|
|
def device_store(temp_storage):
|
|
"""Provide device store instance."""
|
|
return DeviceStore(temp_storage)
|
|
|
|
|
|
def test_device_creation():
|
|
"""Test creating a device."""
|
|
device = Device(
|
|
device_id="test_001",
|
|
name="Test Device",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
assert device.id == "test_001"
|
|
assert device.name == "Test Device"
|
|
assert device.url == "http://192.168.1.100"
|
|
assert device.led_count == 150
|
|
assert device.enabled is True
|
|
|
|
|
|
def test_device_to_dict():
|
|
"""Test converting device to dictionary."""
|
|
device = Device(
|
|
device_id="test_001",
|
|
name="Test Device",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
data = device.to_dict()
|
|
|
|
assert data["id"] == "test_001"
|
|
assert data["name"] == "Test Device"
|
|
assert data["url"] == "http://192.168.1.100"
|
|
assert data["led_count"] == 150
|
|
assert "settings" in data
|
|
assert "calibration" in data
|
|
|
|
|
|
def test_device_from_dict():
|
|
"""Test creating device from dictionary."""
|
|
data = {
|
|
"id": "test_001",
|
|
"name": "Test Device",
|
|
"url": "http://192.168.1.100",
|
|
"led_count": 150,
|
|
"enabled": True,
|
|
"settings": {
|
|
"display_index": 0,
|
|
"fps": 30,
|
|
"border_width": 10,
|
|
},
|
|
}
|
|
|
|
device = Device.from_dict(data)
|
|
|
|
assert device.id == "test_001"
|
|
assert device.name == "Test Device"
|
|
assert device.led_count == 150
|
|
|
|
|
|
def test_device_round_trip():
|
|
"""Test converting device to dict and back."""
|
|
original = Device(
|
|
device_id="test_001",
|
|
name="Test Device",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
data = original.to_dict()
|
|
restored = Device.from_dict(data)
|
|
|
|
assert restored.id == original.id
|
|
assert restored.name == original.name
|
|
assert restored.url == original.url
|
|
assert restored.led_count == original.led_count
|
|
|
|
|
|
def test_device_store_init(device_store):
|
|
"""Test device store initialization."""
|
|
assert device_store is not None
|
|
assert device_store.count() == 0
|
|
|
|
|
|
def test_create_device(device_store):
|
|
"""Test creating a device in store."""
|
|
device = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
assert device.id is not None
|
|
assert device.name == "Test WLED"
|
|
assert device_store.count() == 1
|
|
|
|
|
|
def test_get_device(device_store):
|
|
"""Test retrieving a device."""
|
|
created = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
retrieved = device_store.get_device(created.id)
|
|
|
|
assert retrieved is not None
|
|
assert retrieved.id == created.id
|
|
assert retrieved.name == "Test WLED"
|
|
|
|
|
|
def test_get_device_not_found(device_store):
|
|
"""Test retrieving non-existent device."""
|
|
device = device_store.get_device("nonexistent")
|
|
assert device is None
|
|
|
|
|
|
def test_get_all_devices(device_store):
|
|
"""Test getting all devices."""
|
|
device_store.create_device("Device 1", "http://192.168.1.100", 150)
|
|
device_store.create_device("Device 2", "http://192.168.1.101", 200)
|
|
|
|
devices = device_store.get_all_devices()
|
|
|
|
assert len(devices) == 2
|
|
assert any(d.name == "Device 1" for d in devices)
|
|
assert any(d.name == "Device 2" for d in devices)
|
|
|
|
|
|
def test_update_device(device_store):
|
|
"""Test updating a device."""
|
|
device = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
updated = device_store.update_device(
|
|
device.id,
|
|
name="Updated WLED",
|
|
enabled=False,
|
|
)
|
|
|
|
assert updated.name == "Updated WLED"
|
|
assert updated.enabled is False
|
|
|
|
|
|
def test_update_device_settings(device_store):
|
|
"""Test updating device settings."""
|
|
device = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
new_settings = ProcessingSettings(fps=60, border_width=20)
|
|
|
|
updated = device_store.update_device(
|
|
device.id,
|
|
settings=new_settings,
|
|
)
|
|
|
|
assert updated.settings.fps == 60
|
|
assert updated.settings.border_width == 20
|
|
|
|
|
|
def test_update_device_calibration(device_store):
|
|
"""Test updating device calibration."""
|
|
device = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
new_calibration = create_default_calibration(150)
|
|
|
|
updated = device_store.update_device(
|
|
device.id,
|
|
calibration=new_calibration,
|
|
)
|
|
|
|
assert updated.calibration is not None
|
|
|
|
|
|
def test_update_device_not_found(device_store):
|
|
"""Test updating non-existent device."""
|
|
with pytest.raises(ValueError, match="not found"):
|
|
device_store.update_device("nonexistent", name="New Name")
|
|
|
|
|
|
def test_delete_device(device_store):
|
|
"""Test deleting a device."""
|
|
device = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
device_store.delete_device(device.id)
|
|
|
|
assert device_store.count() == 0
|
|
assert device_store.get_device(device.id) is None
|
|
|
|
|
|
def test_delete_device_not_found(device_store):
|
|
"""Test deleting non-existent device."""
|
|
with pytest.raises(ValueError, match="not found"):
|
|
device_store.delete_device("nonexistent")
|
|
|
|
|
|
def test_device_exists(device_store):
|
|
"""Test checking if device exists."""
|
|
device = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
assert device_store.device_exists(device.id) is True
|
|
assert device_store.device_exists("nonexistent") is False
|
|
|
|
|
|
def test_persistence(temp_storage):
|
|
"""Test device persistence across store instances."""
|
|
# Create store and add device
|
|
store1 = DeviceStore(temp_storage)
|
|
device = store1.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
device_id = device.id
|
|
|
|
# Create new store instance (loads from file)
|
|
store2 = DeviceStore(temp_storage)
|
|
|
|
# Verify device persisted
|
|
loaded_device = store2.get_device(device_id)
|
|
assert loaded_device is not None
|
|
assert loaded_device.name == "Test WLED"
|
|
assert loaded_device.led_count == 150
|
|
|
|
|
|
def test_clear(device_store):
|
|
"""Test clearing all devices."""
|
|
device_store.create_device("Device 1", "http://192.168.1.100", 150)
|
|
device_store.create_device("Device 2", "http://192.168.1.101", 200)
|
|
|
|
assert device_store.count() == 2
|
|
|
|
device_store.clear()
|
|
|
|
assert device_store.count() == 0
|
|
|
|
|
|
def test_update_led_count_resets_calibration(device_store):
|
|
"""Test that updating LED count resets calibration."""
|
|
device = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
original_calibration = device.calibration
|
|
|
|
# Update LED count
|
|
updated = device_store.update_device(device.id, led_count=200)
|
|
|
|
# Calibration should be reset for new LED count
|
|
assert updated.calibration.get_total_leds() == 200
|
|
assert updated.calibration != original_calibration
|
|
|
|
|
|
def test_update_calibration_led_count_mismatch(device_store):
|
|
"""Test updating calibration with mismatched LED count fails."""
|
|
device = device_store.create_device(
|
|
name="Test WLED",
|
|
url="http://192.168.1.100",
|
|
led_count=150,
|
|
)
|
|
|
|
wrong_calibration = create_default_calibration(100)
|
|
|
|
with pytest.raises(ValueError, match="does not match"):
|
|
device_store.update_device(device.id, calibration=wrong_calibration)
|