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:
305
server/tests/test_device_store.py
Normal file
305
server/tests/test_device_store.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user