Add CSPT entity, processed CSS source type, reverse filter, and UI improvements
- Add Color Strip Processing Template (CSPT) entity: reusable filter chains for 1D LED strip postprocessing (backend, storage, API, frontend CRUD) - Add "processed" color strip source type that wraps another CSS source and applies a CSPT filter chain (dataclass, stream, schema, modal, cards) - Add Reverse filter for strip LED order reversal - Add CSPT and processed CSS nodes/edges to visual graph editor - Add CSPT test preview WS endpoint with input source selection - Add device settings CSPT template selector (add + edit modals with hints) - Use icon grids for palette quantization preset selector in filter lists - Use EntitySelect for template references and test modal source selectors - Fix filters.css_filter_template.desc missing localization - Fix icon grid cell height inequality (grid-auto-rows: 1fr) - Rename "Processed" subtab to "Processing Templates" - Localize all new strings (en/ru/zh) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
"""Device routes: CRUD, health state, brightness, power, calibration, WS stream."""
|
||||
|
||||
import secrets
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
@@ -157,8 +155,7 @@ async def create_device(
|
||||
|
||||
# WS devices: auto-set URL to ws://{device_id}
|
||||
if device_type == "ws":
|
||||
store.update_device(device_id=device.id, url=f"ws://{device.id}")
|
||||
device = store.get_device(device.id)
|
||||
device = store.update_device(device.id, url=f"ws://{device.id}")
|
||||
|
||||
# Register in processor manager for health monitoring
|
||||
manager.add_device(
|
||||
@@ -309,9 +306,10 @@ async def get_device(
|
||||
store: DeviceStore = Depends(get_device_store),
|
||||
):
|
||||
"""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")
|
||||
try:
|
||||
device = store.get_device(device_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
return _device_to_response(device)
|
||||
|
||||
|
||||
@@ -430,9 +428,10 @@ async def get_device_state(
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Get device health/connection state."""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
try:
|
||||
device = store.get_device(device_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
try:
|
||||
state = manager.get_device_health_dict(device_id)
|
||||
@@ -450,9 +449,10 @@ async def ping_device(
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Force an immediate health check on a device."""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
try:
|
||||
device = store.get_device(device_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
try:
|
||||
state = await manager.force_device_health_check(device_id)
|
||||
@@ -477,9 +477,10 @@ async def get_device_brightness(
|
||||
frontend request — hitting the ESP32 over WiFi in the async event loop
|
||||
causes ~150 ms jitter in the processing loop.
|
||||
"""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
try:
|
||||
device = store.get_device(device_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
if "brightness_control" not in get_device_capabilities(device.device_type):
|
||||
raise HTTPException(status_code=400, detail=f"Brightness control is not supported for {device.device_type} devices")
|
||||
|
||||
@@ -512,9 +513,10 @@ async def set_device_brightness(
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Set brightness on the device."""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
try:
|
||||
device = store.get_device(device_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
if "brightness_control" not in get_device_capabilities(device.device_type):
|
||||
raise HTTPException(status_code=400, detail=f"Brightness control is not supported for {device.device_type} devices")
|
||||
|
||||
@@ -528,10 +530,7 @@ async def set_device_brightness(
|
||||
await provider.set_brightness(device.url, bri)
|
||||
except NotImplementedError:
|
||||
# Provider has no hardware brightness; use software brightness
|
||||
device.software_brightness = bri
|
||||
from datetime import datetime, timezone
|
||||
device.updated_at = datetime.now(timezone.utc)
|
||||
store.save()
|
||||
store.update_device(device_id=device_id, software_brightness=bri)
|
||||
ds = manager.find_device_state(device_id)
|
||||
if ds:
|
||||
ds.software_brightness = bri
|
||||
@@ -557,9 +556,10 @@ async def get_device_power(
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Get current power state from the device."""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
try:
|
||||
device = store.get_device(device_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
if "power_control" not in get_device_capabilities(device.device_type):
|
||||
raise HTTPException(status_code=400, detail=f"Power control is not supported for {device.device_type} devices")
|
||||
|
||||
@@ -586,9 +586,10 @@ async def set_device_power(
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Turn device on or off."""
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||
try:
|
||||
device = store.get_device(device_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
if "power_control" not in get_device_capabilities(device.device_type):
|
||||
raise HTTPException(status_code=400, detail=f"Power control is not supported for {device.device_type} devices")
|
||||
|
||||
@@ -628,23 +629,15 @@ async def device_ws_stream(
|
||||
Wire format: [brightness_byte][R G B R G B ...]
|
||||
Auth via ?token=<api_key>.
|
||||
"""
|
||||
from wled_controller.config import get_config
|
||||
|
||||
authenticated = False
|
||||
cfg = get_config()
|
||||
if token and cfg.auth.api_keys:
|
||||
for _label, api_key in cfg.auth.api_keys.items():
|
||||
if secrets.compare_digest(token, api_key):
|
||||
authenticated = True
|
||||
break
|
||||
|
||||
if not authenticated:
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
return
|
||||
|
||||
store = get_device_store()
|
||||
device = store.get_device(device_id)
|
||||
if not device:
|
||||
try:
|
||||
device = store.get_device(device_id)
|
||||
except ValueError:
|
||||
await websocket.close(code=4004, reason="Device not found")
|
||||
return
|
||||
if device.device_type != "ws":
|
||||
|
||||
Reference in New Issue
Block a user