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:
166
server/src/wled_controller/main.py
Normal file
166
server/src/wled_controller/main.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""FastAPI application entry point."""
|
||||
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from wled_controller import __version__
|
||||
from wled_controller.api import router
|
||||
from wled_controller.api.routes import init_dependencies
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.core.processor_manager import ProcessorManager
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.utils import setup_logging, get_logger
|
||||
|
||||
# Initialize logging
|
||||
setup_logging()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Get configuration
|
||||
config = get_config()
|
||||
|
||||
# Initialize storage and processing
|
||||
device_store = DeviceStore(config.storage.devices_file)
|
||||
processor_manager = ProcessorManager()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager.
|
||||
|
||||
Handles startup and shutdown events.
|
||||
"""
|
||||
# Startup
|
||||
logger.info(f"Starting WLED Screen Controller v{__version__}")
|
||||
logger.info(f"Python version: {sys.version}")
|
||||
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
|
||||
|
||||
# Validate authentication configuration
|
||||
if not config.auth.api_keys:
|
||||
logger.error("=" * 70)
|
||||
logger.error("CRITICAL: No API keys configured!")
|
||||
logger.error("Authentication is REQUIRED for all API requests.")
|
||||
logger.error("Please add API keys to your configuration:")
|
||||
logger.error(" 1. Generate keys: openssl rand -hex 32")
|
||||
logger.error(" 2. Add to config/default_config.yaml under auth.api_keys")
|
||||
logger.error(" 3. Format: label: \"your-generated-key\"")
|
||||
logger.error("=" * 70)
|
||||
raise RuntimeError("No API keys configured - server cannot start without authentication")
|
||||
|
||||
# Log authentication status
|
||||
logger.info(f"API Authentication: ENFORCED ({len(config.auth.api_keys)} clients configured)")
|
||||
client_labels = ", ".join(config.auth.api_keys.keys())
|
||||
logger.info(f"Authorized clients: {client_labels}")
|
||||
logger.info("All API requests require valid Bearer token authentication")
|
||||
|
||||
# Initialize API dependencies
|
||||
init_dependencies(device_store, processor_manager)
|
||||
|
||||
# Load existing devices into processor manager
|
||||
devices = device_store.get_all_devices()
|
||||
for device in devices:
|
||||
try:
|
||||
processor_manager.add_device(
|
||||
device_id=device.id,
|
||||
device_url=device.url,
|
||||
led_count=device.led_count,
|
||||
settings=device.settings,
|
||||
calibration=device.calibration,
|
||||
)
|
||||
logger.info(f"Loaded device: {device.name} ({device.id})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load device {device.id}: {e}")
|
||||
|
||||
logger.info(f"Loaded {len(devices)} devices from storage")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
logger.info("Shutting down WLED Screen Controller")
|
||||
|
||||
# Stop all processing
|
||||
try:
|
||||
await processor_manager.stop_all()
|
||||
logger.info("Stopped all processors")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping processors: {e}")
|
||||
|
||||
# Create FastAPI application
|
||||
app = FastAPI(
|
||||
title="WLED Screen Controller",
|
||||
description="Control WLED devices based on screen content for ambient lighting",
|
||||
version=__version__,
|
||||
lifespan=lifespan,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
openapi_url="/openapi.json",
|
||||
)
|
||||
|
||||
# Configure CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=config.server.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include API routes
|
||||
app.include_router(router)
|
||||
|
||||
# Mount static files
|
||||
static_path = Path(__file__).parent / "static"
|
||||
if static_path.exists():
|
||||
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
|
||||
logger.info(f"Mounted static files from {static_path}")
|
||||
else:
|
||||
logger.warning(f"Static files directory not found: {static_path}")
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request, exc):
|
||||
"""Global exception handler for unhandled errors."""
|
||||
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"error": "InternalServerError",
|
||||
"message": "An unexpected error occurred",
|
||||
"detail": str(exc) if config.server.log_level == "DEBUG" else None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Serve the web UI dashboard."""
|
||||
static_path = Path(__file__).parent / "static" / "index.html"
|
||||
if static_path.exists():
|
||||
return FileResponse(static_path)
|
||||
|
||||
# Fallback to JSON if static files not found
|
||||
return {
|
||||
"name": "WLED Screen Controller",
|
||||
"version": __version__,
|
||||
"docs": "/docs",
|
||||
"health": "/health",
|
||||
"api": "/api/v1",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"wled_controller.main:app",
|
||||
host=config.server.host,
|
||||
port=config.server.port,
|
||||
log_level=config.server.log_level.lower(),
|
||||
reload=True,
|
||||
)
|
||||
Reference in New Issue
Block a user