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:
598
server/src/wled_controller/api/routes.py
Normal file
598
server/src/wled_controller/api/routes.py
Normal file
@@ -0,0 +1,598 @@
|
||||
"""API routes and endpoints."""
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
|
||||
from wled_controller import __version__
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.schemas import (
|
||||
HealthResponse,
|
||||
VersionResponse,
|
||||
DisplayListResponse,
|
||||
DisplayInfo,
|
||||
DeviceCreate,
|
||||
DeviceUpdate,
|
||||
DeviceResponse,
|
||||
DeviceListResponse,
|
||||
ProcessingSettings as ProcessingSettingsSchema,
|
||||
Calibration as CalibrationSchema,
|
||||
ProcessingState,
|
||||
MetricsResponse,
|
||||
)
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.core.processor_manager import ProcessorManager, ProcessingSettings
|
||||
from wled_controller.core.calibration import (
|
||||
calibration_from_dict,
|
||||
calibration_to_dict,
|
||||
)
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.utils import get_logger, get_monitor_names
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Global instances (initialized in main.py)
|
||||
_device_store: DeviceStore | None = None
|
||||
_processor_manager: ProcessorManager | None = None
|
||||
|
||||
|
||||
def get_device_store() -> DeviceStore:
|
||||
"""Get device store dependency."""
|
||||
if _device_store is None:
|
||||
raise RuntimeError("Device store not initialized")
|
||||
return _device_store
|
||||
|
||||
|
||||
def get_processor_manager() -> ProcessorManager:
|
||||
"""Get processor manager dependency."""
|
||||
if _processor_manager is None:
|
||||
raise RuntimeError("Processor manager not initialized")
|
||||
return _processor_manager
|
||||
|
||||
|
||||
def init_dependencies(device_store: DeviceStore, processor_manager: ProcessorManager):
|
||||
"""Initialize global dependencies."""
|
||||
global _device_store, _processor_manager
|
||||
_device_store = device_store
|
||||
_processor_manager = processor_manager
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse, tags=["Health"])
|
||||
async def health_check():
|
||||
"""Check service health status.
|
||||
|
||||
Returns basic health information including status, version, and timestamp.
|
||||
"""
|
||||
logger.info("Health check requested")
|
||||
|
||||
return HealthResponse(
|
||||
status="healthy",
|
||||
timestamp=datetime.utcnow(),
|
||||
version=__version__,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/version", response_model=VersionResponse, tags=["Info"])
|
||||
async def get_version():
|
||||
"""Get version information.
|
||||
|
||||
Returns application version, Python version, and API version.
|
||||
"""
|
||||
logger.info("Version info requested")
|
||||
|
||||
return VersionResponse(
|
||||
version=__version__,
|
||||
python_version=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
api_version="v1",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/config/displays", response_model=DisplayListResponse, tags=["Config"])
|
||||
async def get_displays(_: AuthRequired):
|
||||
"""Get list of available displays.
|
||||
|
||||
Returns information about all available monitors/displays that can be captured.
|
||||
"""
|
||||
logger.info("Listing available displays")
|
||||
|
||||
try:
|
||||
# Import here to avoid issues if mss is not installed yet
|
||||
import mss
|
||||
|
||||
# Get friendly monitor names (Windows only, falls back to generic names)
|
||||
monitor_names = get_monitor_names()
|
||||
|
||||
with mss.mss() as sct:
|
||||
displays = []
|
||||
|
||||
# Skip the first monitor (it's the combined virtual screen on multi-monitor setups)
|
||||
for idx, monitor in enumerate(sct.monitors[1:], start=0):
|
||||
# Use friendly name from WMI if available, otherwise generic name
|
||||
friendly_name = monitor_names.get(idx, f"Display {idx}")
|
||||
|
||||
display_info = DisplayInfo(
|
||||
index=idx,
|
||||
name=friendly_name,
|
||||
width=monitor["width"],
|
||||
height=monitor["height"],
|
||||
x=monitor["left"],
|
||||
y=monitor["top"],
|
||||
is_primary=(idx == 0),
|
||||
)
|
||||
displays.append(display_info)
|
||||
|
||||
logger.info(f"Found {len(displays)} displays")
|
||||
|
||||
return DisplayListResponse(
|
||||
displays=displays,
|
||||
count=len(displays),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get displays: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to retrieve display information: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# ===== DEVICE MANAGEMENT ENDPOINTS =====
|
||||
|
||||
@router.post("/api/v1/devices", response_model=DeviceResponse, tags=["Devices"], status_code=201)
|
||||
async def create_device(
|
||||
device_data: DeviceCreate,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Create and attach a new WLED device."""
|
||||
try:
|
||||
logger.info(f"Creating device: {device_data.name}")
|
||||
|
||||
# Create device in storage
|
||||
device = store.create_device(
|
||||
name=device_data.name,
|
||||
url=device_data.url,
|
||||
led_count=device_data.led_count,
|
||||
)
|
||||
|
||||
# Add to processor manager
|
||||
manager.add_device(
|
||||
device_id=device.id,
|
||||
device_url=device.url,
|
||||
led_count=device.led_count,
|
||||
settings=device.settings,
|
||||
calibration=device.calibration,
|
||||
)
|
||||
|
||||
return DeviceResponse(
|
||||
id=device.id,
|
||||
name=device.name,
|
||||
url=device.url,
|
||||
led_count=device.led_count,
|
||||
enabled=device.enabled,
|
||||
status="disconnected",
|
||||
settings=ProcessingSettingsSchema(
|
||||
display_index=device.settings.display_index,
|
||||
fps=device.settings.fps,
|
||||
border_width=device.settings.border_width,
|
||||
brightness=device.settings.brightness,
|
||||
),
|
||||
calibration=CalibrationSchema(**calibration_to_dict(device.calibration)),
|
||||
created_at=device.created_at,
|
||||
updated_at=device.updated_at,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create device: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/v1/devices", response_model=DeviceListResponse, tags=["Devices"])
|
||||
async def list_devices(
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
):
|
||||
"""List all attached WLED devices."""
|
||||
try:
|
||||
devices = store.get_all_devices()
|
||||
|
||||
device_responses = [
|
||||
DeviceResponse(
|
||||
id=device.id,
|
||||
name=device.name,
|
||||
url=device.url,
|
||||
led_count=device.led_count,
|
||||
enabled=device.enabled,
|
||||
status="disconnected",
|
||||
settings=ProcessingSettingsSchema(
|
||||
display_index=device.settings.display_index,
|
||||
fps=device.settings.fps,
|
||||
border_width=device.settings.border_width,
|
||||
),
|
||||
calibration=CalibrationSchema(**calibration_to_dict(device.calibration)),
|
||||
created_at=device.created_at,
|
||||
updated_at=device.updated_at,
|
||||
)
|
||||
for device in devices
|
||||
]
|
||||
|
||||
return DeviceListResponse(devices=device_responses, count=len(device_responses))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list devices: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/v1/devices/{device_id}", response_model=DeviceResponse, tags=["Devices"])
|
||||
async def get_device(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Get device details by ID."""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
|
||||
# Determine status
|
||||
status = "connected" if manager.is_processing(device_id) else "disconnected"
|
||||
|
||||
return DeviceResponse(
|
||||
id=device.id,
|
||||
name=device.name,
|
||||
url=device.url,
|
||||
led_count=device.led_count,
|
||||
enabled=device.enabled,
|
||||
status=status,
|
||||
settings=ProcessingSettingsSchema(
|
||||
display_index=device.settings.display_index,
|
||||
fps=device.settings.fps,
|
||||
border_width=device.settings.border_width,
|
||||
),
|
||||
calibration=CalibrationSchema(**calibration_to_dict(device.calibration)),
|
||||
created_at=device.created_at,
|
||||
updated_at=device.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/v1/devices/{device_id}", response_model=DeviceResponse, tags=["Devices"])
|
||||
async def update_device(
|
||||
device_id: str,
|
||||
update_data: DeviceUpdate,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
):
|
||||
"""Update device information."""
|
||||
try:
|
||||
device = store.update_device(
|
||||
device_id=device_id,
|
||||
name=update_data.name,
|
||||
url=update_data.url,
|
||||
led_count=update_data.led_count,
|
||||
enabled=update_data.enabled,
|
||||
)
|
||||
|
||||
return DeviceResponse(
|
||||
id=device.id,
|
||||
name=device.name,
|
||||
url=device.url,
|
||||
led_count=device.led_count,
|
||||
enabled=device.enabled,
|
||||
status="disconnected",
|
||||
settings=ProcessingSettingsSchema(
|
||||
display_index=device.settings.display_index,
|
||||
fps=device.settings.fps,
|
||||
border_width=device.settings.border_width,
|
||||
brightness=device.settings.brightness,
|
||||
),
|
||||
calibration=CalibrationSchema(**calibration_to_dict(device.calibration)),
|
||||
created_at=device.created_at,
|
||||
updated_at=device.updated_at,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update device: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/api/v1/devices/{device_id}", status_code=204, tags=["Devices"])
|
||||
async def delete_device(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Delete/detach a device."""
|
||||
try:
|
||||
# Stop processing if running
|
||||
if manager.is_processing(device_id):
|
||||
await manager.stop_processing(device_id)
|
||||
|
||||
# Remove from manager
|
||||
manager.remove_device(device_id)
|
||||
|
||||
# Delete from storage
|
||||
store.delete_device(device_id)
|
||||
|
||||
logger.info(f"Deleted device {device_id}")
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete device: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ===== PROCESSING CONTROL ENDPOINTS =====
|
||||
|
||||
@router.post("/api/v1/devices/{device_id}/start", tags=["Processing"])
|
||||
async def start_processing(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Start screen processing for a device."""
|
||||
try:
|
||||
# Verify device exists
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
|
||||
await manager.start_processing(device_id)
|
||||
|
||||
logger.info(f"Started processing for device {device_id}")
|
||||
return {"status": "started", "device_id": device_id}
|
||||
|
||||
except RuntimeError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start processing: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/v1/devices/{device_id}/stop", tags=["Processing"])
|
||||
async def stop_processing(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Stop screen processing for a device."""
|
||||
try:
|
||||
await manager.stop_processing(device_id)
|
||||
|
||||
logger.info(f"Stopped processing for device {device_id}")
|
||||
return {"status": "stopped", "device_id": device_id}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stop processing: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/v1/devices/{device_id}/state", response_model=ProcessingState, tags=["Processing"])
|
||||
async def get_processing_state(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Get current processing state for a device."""
|
||||
try:
|
||||
state = manager.get_state(device_id)
|
||||
return ProcessingState(**state)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get state: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ===== SETTINGS ENDPOINTS =====
|
||||
|
||||
@router.get("/api/v1/devices/{device_id}/settings", response_model=ProcessingSettingsSchema, tags=["Settings"])
|
||||
async def get_settings(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
):
|
||||
"""Get processing settings for a device."""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
|
||||
return ProcessingSettingsSchema(
|
||||
display_index=device.settings.display_index,
|
||||
fps=device.settings.fps,
|
||||
border_width=device.settings.border_width,
|
||||
brightness=device.settings.brightness,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/v1/devices/{device_id}/settings", response_model=ProcessingSettingsSchema, tags=["Settings"])
|
||||
async def update_settings(
|
||||
device_id: str,
|
||||
settings: ProcessingSettingsSchema,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Update processing settings for a device."""
|
||||
try:
|
||||
# Create ProcessingSettings from schema
|
||||
new_settings = ProcessingSettings(
|
||||
display_index=settings.display_index,
|
||||
fps=settings.fps,
|
||||
border_width=settings.border_width,
|
||||
brightness=settings.color_correction.brightness if settings.color_correction else 1.0,
|
||||
gamma=settings.color_correction.gamma if settings.color_correction else 2.2,
|
||||
saturation=settings.color_correction.saturation if settings.color_correction else 1.0,
|
||||
)
|
||||
|
||||
# Update in storage
|
||||
device = store.update_device(device_id, settings=new_settings)
|
||||
|
||||
# Update in manager if device exists
|
||||
try:
|
||||
manager.update_settings(device_id, new_settings)
|
||||
except ValueError:
|
||||
# Device not in manager yet, that's ok
|
||||
pass
|
||||
|
||||
return ProcessingSettingsSchema(
|
||||
display_index=device.settings.display_index,
|
||||
fps=device.settings.fps,
|
||||
border_width=device.settings.border_width,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update settings: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ===== CALIBRATION ENDPOINTS =====
|
||||
|
||||
@router.get("/api/v1/devices/{device_id}/calibration", response_model=CalibrationSchema, tags=["Calibration"])
|
||||
async def get_calibration(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
):
|
||||
"""Get calibration configuration for a device."""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
|
||||
return CalibrationSchema(**calibration_to_dict(device.calibration))
|
||||
|
||||
|
||||
@router.put("/api/v1/devices/{device_id}/calibration", response_model=CalibrationSchema, tags=["Calibration"])
|
||||
async def update_calibration(
|
||||
device_id: str,
|
||||
calibration_data: CalibrationSchema,
|
||||
_auth: AuthRequired,
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Update calibration configuration for a device."""
|
||||
try:
|
||||
# Convert schema to CalibrationConfig
|
||||
calibration_dict = calibration_data.model_dump()
|
||||
calibration = calibration_from_dict(calibration_dict)
|
||||
|
||||
# Update in storage
|
||||
device = store.update_device(device_id, calibration=calibration)
|
||||
|
||||
# Update in manager if device exists
|
||||
try:
|
||||
manager.update_calibration(device_id, calibration)
|
||||
except ValueError:
|
||||
# Device not in manager yet, that's ok
|
||||
pass
|
||||
|
||||
return CalibrationSchema(**calibration_to_dict(device.calibration))
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update calibration: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/v1/devices/{device_id}/calibration/test", tags=["Calibration"])
|
||||
async def test_calibration(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
edge: str = "top",
|
||||
color: List[int] = [255, 0, 0],
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
):
|
||||
"""Test calibration by lighting up specific edge.
|
||||
|
||||
Useful for verifying LED positions match screen edges.
|
||||
"""
|
||||
try:
|
||||
# Get device
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
|
||||
# Find the segment for this edge
|
||||
segment = None
|
||||
for seg in device.calibration.segments:
|
||||
if seg.edge == edge:
|
||||
segment = seg
|
||||
break
|
||||
|
||||
if not segment:
|
||||
raise HTTPException(status_code=400, detail=f"No LEDs configured for {edge} edge")
|
||||
|
||||
# Create pixel array - all black except for the test edge
|
||||
pixels = [(0, 0, 0)] * device.led_count
|
||||
|
||||
# Light up the test edge
|
||||
r, g, b = color if len(color) == 3 else [255, 0, 0]
|
||||
for i in range(segment.led_start, segment.led_start + segment.led_count):
|
||||
if i < device.led_count:
|
||||
pixels[i] = (r, g, b)
|
||||
|
||||
# Send to WLED
|
||||
from wled_controller.core.wled_client import WLEDClient
|
||||
import asyncio
|
||||
|
||||
async with WLEDClient(device.url) as wled:
|
||||
# Light up the edge
|
||||
await wled.send_pixels(pixels)
|
||||
|
||||
# Wait 2 seconds
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Turn off
|
||||
pixels_off = [(0, 0, 0)] * device.led_count
|
||||
await wled.send_pixels(pixels_off)
|
||||
|
||||
logger.info(f"Calibration test completed for edge '{edge}' on device {device_id}")
|
||||
|
||||
return {
|
||||
"status": "test_completed",
|
||||
"device_id": device_id,
|
||||
"edge": edge,
|
||||
"led_count": segment.led_count,
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to test calibration: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ===== METRICS ENDPOINTS =====
|
||||
|
||||
@router.get("/api/v1/devices/{device_id}/metrics", response_model=MetricsResponse, tags=["Metrics"])
|
||||
async def get_metrics(
|
||||
device_id: str,
|
||||
_auth: AuthRequired,
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Get processing metrics for a device."""
|
||||
try:
|
||||
metrics = manager.get_metrics(device_id)
|
||||
return MetricsResponse(**metrics)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get metrics: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user