refactor: rename project to LedGrab, split HA integration into separate repo
Lint & Test / test (push) Successful in 1m56s

- Rename Python package: wled_controller -> ledgrab
- Rename env var prefix: WLED_ -> LEDGRAB_ (with auto-migration for old vars)
- Rename localStorage key: wled_api_key -> ledgrab_api_key (with migration)
- Rename HA integration domain: wled_screen_controller -> ledgrab
- Update all imports, build scripts, Docker, installer, config, docs
- Remove HA integration (moved to ledgrab-haos-integration repo)
- Remove hacs.json (belongs in HA repo now)
- Add startup warning for users with old WLED_ env vars
- All tests pass (715/715), ruff clean, tsc clean, frontend builds
This commit is contained in:
2026-04-12 22:45:28 +03:00
parent 38f73badbf
commit 02cd9d519c
548 changed files with 3502 additions and 5180 deletions
+22 -22
View File
@@ -1,41 +1,41 @@
# WLED Screen Controller — Environment Variables
# LedGrab — Environment Variables
# Copy this file to .env and adjust values as needed.
# All variables use the WLED_ prefix with __ (double underscore) as the nesting delimiter.
# All variables use the LEDGRAB_ prefix with __ (double underscore) as the nesting delimiter.
# ── Server ──────────────────────────────────────────────
# WLED_SERVER__HOST=0.0.0.0 # Listen address (default: 0.0.0.0)
# WLED_SERVER__PORT=8080 # Listen port (default: 8080)
# WLED_SERVER__LOG_LEVEL=INFO # Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)
# WLED_SERVER__CORS_ORIGINS=["*"] # JSON array of allowed CORS origins
# LEDGRAB_SERVER__HOST=0.0.0.0 # Listen address (default: 0.0.0.0)
# LEDGRAB_SERVER__PORT=8080 # Listen port (default: 8080)
# LEDGRAB_SERVER__LOG_LEVEL=INFO # Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)
# LEDGRAB_SERVER__CORS_ORIGINS=["*"] # JSON array of allowed CORS origins
# ── Authentication ──────────────────────────────────────
# API keys are required. Format: JSON object {"label": "key"}.
# WLED_AUTH__API_KEYS={"dev": "development-key-change-in-production"}
# LEDGRAB_AUTH__API_KEYS={"dev": "development-key-change-in-production"}
# ── Storage ────────────────────────────────────────────
# All data is stored in a single SQLite database.
# WLED_STORAGE__DATABASE_FILE=data/ledgrab.db
# LEDGRAB_STORAGE__DATABASE_FILE=data/ledgrab.db
# ── MQTT (optional) ────────────────────────────────────
# WLED_MQTT__ENABLED=false
# WLED_MQTT__BROKER_HOST=localhost
# WLED_MQTT__BROKER_PORT=1883
# WLED_MQTT__USERNAME=
# WLED_MQTT__PASSWORD=
# WLED_MQTT__CLIENT_ID=ledgrab
# WLED_MQTT__BASE_TOPIC=ledgrab
# LEDGRAB_MQTT__ENABLED=false
# LEDGRAB_MQTT__BROKER_HOST=localhost
# LEDGRAB_MQTT__BROKER_PORT=1883
# LEDGRAB_MQTT__USERNAME=
# LEDGRAB_MQTT__PASSWORD=
# LEDGRAB_MQTT__CLIENT_ID=ledgrab
# LEDGRAB_MQTT__BASE_TOPIC=ledgrab
# ── Logging ─────────────────────────────────────────────
# WLED_LOGGING__FORMAT=json # json or text (default: json)
# WLED_LOGGING__FILE=logs/wled_controller.log
# WLED_LOGGING__MAX_SIZE_MB=100
# WLED_LOGGING__BACKUP_COUNT=5
# LEDGRAB_LOGGING__FORMAT=json # json or text (default: json)
# LEDGRAB_LOGGING__FILE=logs/wled_controller.log
# LEDGRAB_LOGGING__MAX_SIZE_MB=100
# LEDGRAB_LOGGING__BACKUP_COUNT=5
# ── Demo mode ───────────────────────────────────────────
# WLED_DEMO=false # Enable demo mode (uses data/demo/ directory)
# LEDGRAB_DEMO=false # Enable demo mode (uses data/demo/ directory)
# ── Config file override ───────────────────────────────
# WLED_CONFIG_PATH= # Absolute path to a YAML config file (overrides all above)
# LEDGRAB_CONFIG_PATH= # Absolute path to a YAML config file (overrides all above)
# ── Docker Compose extras (not part of WLED_ prefix) ───
# ── Docker Compose extras (not part of LEDGRAB_ prefix) ───
# DISPLAY=:0 # X11 display for Linux screen capture
+10 -10
View File
@@ -1,15 +1,15 @@
# Claude Instructions for WLED Screen Controller Server
# Claude Instructions for LedGrab Server
## Project Structure
- `src/wled_controller/main.py` — FastAPI application entry point
- `src/wled_controller/api/routes/` — REST API endpoints (one file per entity)
- `src/wled_controller/api/schemas/` — Pydantic request/response models (one file per entity)
- `src/wled_controller/core/` — Core business logic (capture, devices, audio, processing, automations)
- `src/wled_controller/storage/` — Data models (dataclasses) and JSON persistence stores
- `src/wled_controller/utils/` — Utility functions (logging, monitor detection, SSRF validation, sound playback)
- `src/wled_controller/static/` — Frontend files (TypeScript, CSS, locales)
- `src/wled_controller/templates/` — Jinja2 HTML templates
- `src/ledgrab/main.py` — FastAPI application entry point
- `src/ledgrab/api/routes/` — REST API endpoints (one file per entity)
- `src/ledgrab/api/schemas/` — Pydantic request/response models (one file per entity)
- `src/ledgrab/core/` — Core business logic (capture, devices, audio, processing, automations)
- `src/ledgrab/storage/` — Data models (dataclasses) and JSON persistence stores
- `src/ledgrab/utils/` — Utility functions (logging, monitor detection, SSRF validation, sound playback)
- `src/ledgrab/static/` — Frontend files (TypeScript, CSS, locales)
- `src/ledgrab/templates/` — Jinja2 HTML templates
- `config/` — Configuration files (YAML)
- `data/` — Runtime data (JSON stores, persisted state)
@@ -22,7 +22,7 @@ Each entity follows: dataclass model (`storage/`) + JSON store (`storage/*_store
Server uses API key authentication via Bearer token in `Authorization` header.
- Config: `config/default_config.yaml` under `auth.api_keys`
- Env var: `WLED_AUTH__API_KEYS`
- Env var: `LEDGRAB_AUTH__API_KEYS`
- When `api_keys` is empty (default), auth is disabled — all endpoints are open
- To enable auth, add key entries (e.g. `dev: "your-secret-key"`)
+7 -7
View File
@@ -4,7 +4,7 @@ WORKDIR /build
COPY package.json package-lock.json* ./
RUN npm ci --ignore-scripts
COPY esbuild.mjs tsconfig.json ./
COPY src/wled_controller/static/ ./src/wled_controller/static/
COPY src/ledgrab/static/ ./src/ledgrab/static/
RUN npm run build
## Stage 2: Python application
@@ -16,8 +16,8 @@ LABEL maintainer="Alexei Dolgolyov <dolgolyov.alexei@gmail.com>"
LABEL org.opencontainers.image.title="LED Grab"
LABEL org.opencontainers.image.description="Ambient lighting system that captures screen content and drives LED strips in real time"
LABEL org.opencontainers.image.version="${APP_VERSION}"
LABEL org.opencontainers.image.url="https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
LABEL org.opencontainers.image.source="https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
LABEL org.opencontainers.image.url="https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab"
LABEL org.opencontainers.image.source="https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab"
LABEL org.opencontainers.image.licenses="MIT"
WORKDIR /app
@@ -37,16 +37,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# The real source is copied afterward, keeping the dep layer cached.
COPY pyproject.toml .
RUN sed -i "s/^version = .*/version = \"${APP_VERSION}\"/" pyproject.toml \
&& mkdir -p src/wled_controller && touch src/wled_controller/__init__.py \
&& mkdir -p src/ledgrab && touch src/ledgrab/__init__.py \
&& pip install --no-cache-dir ".[notifications]" \
&& rm -rf src/wled_controller
&& rm -rf src/ledgrab
# Copy source code and config (invalidates cache only when source changes)
COPY src/ ./src/
COPY config/ ./config/
# Copy built frontend bundle from stage 1
COPY --from=frontend /build/src/wled_controller/static/dist/ ./src/wled_controller/static/dist/
COPY --from=frontend /build/src/ledgrab/static/dist/ ./src/ledgrab/static/dist/
# Create non-root user for security
RUN groupadd --gid 1000 ledgrab \
@@ -67,4 +67,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
ENV PYTHONPATH=/app/src
# Run the application
CMD ["uvicorn", "wled_controller.main:app", "--host", "0.0.0.0", "--port", "8080"]
CMD ["uvicorn", "ledgrab.main:app", "--host", "0.0.0.0", "--port", "8080"]
+11 -11
View File
@@ -1,4 +1,4 @@
# WLED Screen Controller - Server
# LedGrab - Server
High-performance FastAPI server that captures screen content and controls WLED devices for ambient lighting.
@@ -47,7 +47,7 @@ export PYTHONPATH=$(pwd)/src # Linux/Mac
set PYTHONPATH=%CD%\src # Windows
# Run server
uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080
uvicorn ledgrab.main:app --host 0.0.0.0 --port 8080
```
## Installation
@@ -85,20 +85,20 @@ storage:
logging:
format: "json"
file: "logs/wled_controller.log"
file: "logs/ledgrab.log"
```
### Environment Variables
```bash
# Server configuration
export WLED_SERVER__HOST="0.0.0.0"
export WLED_SERVER__PORT=8080
export WLED_SERVER__LOG_LEVEL="INFO"
export LEDGRAB_SERVER__HOST="0.0.0.0"
export LEDGRAB_SERVER__PORT=8080
export LEDGRAB_SERVER__LOG_LEVEL="INFO"
# Processing configuration
export WLED_PROCESSING__DEFAULT_FPS=30
export WLED_PROCESSING__BORDER_WIDTH=10
export LEDGRAB_PROCESSING__DEFAULT_FPS=30
export LEDGRAB_PROCESSING__BORDER_WIDTH=10
# WLED configuration
export WLED_WLED__TIMEOUT=5
@@ -147,7 +147,7 @@ curl http://localhost:8080/api/v1/devices/{device_id}/state
pytest
# Run with coverage
pytest --cov=wled_controller --cov-report=html
pytest --cov=ledgrab --cov-report=html
# Run specific test
pytest tests/test_screen_capture.py -v
@@ -158,7 +158,7 @@ pytest tests/test_screen_capture.py -v
### Project Structure
```
src/wled_controller/
src/ledgrab/
├── main.py # FastAPI application
├── config.py # Configuration
├── api/ # API routes
@@ -188,4 +188,4 @@ MIT - see [../LICENSE](../LICENSE)
## Support
- 📖 [Full Documentation](../docs/)
- 🐛 [Issues](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/issues)
- 🐛 [Issues](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/issues)
+1 -1
View File
@@ -28,6 +28,6 @@ mqtt:
logging:
format: "json" # json or text
file: "logs/wled_controller.log"
file: "logs/ledgrab.log"
max_size_mb: 100
backup_count: 5
+2 -2
View File
@@ -1,5 +1,5 @@
# Demo mode configuration
# Loaded automatically when WLED_DEMO=true is set.
# Loaded automatically when LEDGRAB_DEMO=true is set.
# Uses isolated data directory (data/demo/) and a pre-configured API key
# so the demo works out of the box with zero setup.
@@ -26,6 +26,6 @@ mqtt:
logging:
format: "text"
file: "logs/wled_controller.log"
file: "logs/ledgrab.log"
max_size_mb: 100
backup_count: 5
+1 -1
View File
@@ -15,6 +15,6 @@ storage:
logging:
format: "text"
file: "logs/wled_test.log"
file: "logs/ledgrab_test.log"
max_size_mb: 10
backup_count: 2
+17 -17
View File
@@ -1,14 +1,14 @@
services:
wled-controller:
ledgrab:
build:
context: .
dockerfile: Dockerfile
image: ledgrab:latest
container_name: wled-screen-controller
container_name: ledgrab
restart: unless-stopped
ports:
- "${WLED_PORT:-8080}:8080"
- "${LEDGRAB_PORT:-8080}:8080"
volumes:
# Persist device data and configuration across restarts
@@ -22,37 +22,37 @@ services:
environment:
## Server
# Bind address and port (usually no need to change)
- WLED_SERVER__HOST=0.0.0.0
- WLED_SERVER__PORT=8080
- WLED_SERVER__LOG_LEVEL=INFO
- LEDGRAB_SERVER__HOST=0.0.0.0
- LEDGRAB_SERVER__PORT=8080
- LEDGRAB_SERVER__LOG_LEVEL=INFO
# CORS origins — add your LAN IP for remote access, e.g.:
# WLED_SERVER__CORS_ORIGINS=["http://localhost:8080","http://192.168.1.100:8080"]
# LEDGRAB_SERVER__CORS_ORIGINS=["http://localhost:8080","http://192.168.1.100:8080"]
## Auth
# Override the default API key (STRONGLY recommended for production):
# WLED_AUTH__API_KEYS__main=your-secure-key-here
# LEDGRAB_AUTH__API_KEYS__main=your-secure-key-here
# Generate a key: openssl rand -hex 32
## Display (Linux X11 only)
- DISPLAY=${DISPLAY:-:0}
## Processing defaults
#- WLED_PROCESSING__DEFAULT_FPS=30
#- WLED_PROCESSING__BORDER_WIDTH=10
#- LEDGRAB_PROCESSING__DEFAULT_FPS=30
#- LEDGRAB_PROCESSING__BORDER_WIDTH=10
## MQTT (optional — for Home Assistant auto-discovery)
#- WLED_MQTT__ENABLED=true
#- WLED_MQTT__BROKER_HOST=192.168.1.2
#- WLED_MQTT__BROKER_PORT=1883
#- WLED_MQTT__USERNAME=
#- WLED_MQTT__PASSWORD=
#- LEDGRAB_MQTT__ENABLED=true
#- LEDGRAB_MQTT__BROKER_HOST=192.168.1.2
#- LEDGRAB_MQTT__BROKER_PORT=1883
#- LEDGRAB_MQTT__USERNAME=
#- LEDGRAB_MQTT__PASSWORD=
# Uncomment for Linux screen capture (requires host network for X11 access)
# network_mode: host
networks:
- wled-network
- ledgrab-network
networks:
wled-network:
ledgrab-network:
driver: bridge
+4 -4
View File
@@ -1,6 +1,6 @@
# API Authentication Guide
WLED Screen Controller **requires** API key authentication for all API endpoints. This ensures your server is secure and all access is properly authenticated and audited.
LedGrab **requires** API key authentication for all API endpoints. This ensures your server is secure and all access is properly authenticated and audited.
## Configuration
@@ -66,7 +66,7 @@ curl -H "Authorization: Bearer your-api-key-here" \
The integration will prompt for an API key during setup if authentication is enabled. You can also configure it in `configuration.yaml`:
```yaml
wled_screen_controller:
ledgrab:
server_url: "http://192.168.1.100:8080"
api_key: "your-api-key-here" # Optional, only if auth is enabled
```
@@ -168,8 +168,8 @@ export WLED_API_KEY_2="$(openssl rand -hex 32)"
services:
wled-controller:
environment:
- WLED_AUTH__ENABLED=true
- WLED_AUTH__API_KEYS__0=your-key-here
- LEDGRAB_AUTH__ENABLED=true
- LEDGRAB_AUTH__API_KEYS__0=your-key-here
```
Or use Docker secrets for better security.
+1 -1
View File
@@ -1,6 +1,6 @@
import * as esbuild from 'esbuild';
const srcDir = 'src/wled_controller/static';
const srcDir = 'src/ledgrab/static';
const outDir = `${srcDir}/dist`;
const watch = process.argv.includes('--watch');
+6 -6
View File
@@ -3,7 +3,7 @@ requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "wled-screen-controller"
name = "ledgrab"
version = "0.3.0"
description = "Ambient lighting system that captures screen content and drives LED strips in real time"
authors = [
@@ -83,10 +83,10 @@ perf = [
]
[project.urls]
Homepage = "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
Repository = "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
Documentation = "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/src/branch/master/INSTALLATION.md"
Issues = "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/issues"
Homepage = "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab"
Repository = "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab"
Documentation = "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/src/branch/master/INSTALLATION.md"
Issues = "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/issues"
[tool.setuptools]
package-dir = {"" = "src"}
@@ -97,7 +97,7 @@ where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-v --cov=wled_controller --cov-report=html --cov-report=term"
addopts = "-v --cov=ledgrab --cov-report=html --cov-report=term"
[tool.black]
line-length = 100
+8 -8
View File
@@ -1,8 +1,8 @@
# Restart the WLED Screen Controller server
# Restart the LedGrab server
# Uses graceful shutdown first (lets the server persist data to disk),
# then force-kills as a fallback.
$serverRoot = 'c:\Users\Alexei\Documents\wled-screen-controller\server'
$serverRoot = $PSScriptRoot
# Read API key from config for authenticated shutdown request
$configPath = Join-Path $serverRoot 'config\default_config.yaml'
@@ -20,7 +20,7 @@ if (Test-Path $configPath) {
# Find running server processes
$procs = Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
Where-Object { $_.CommandLine -like '*wled_controller*' -and $_.CommandLine -notlike '*demo*' -and $_.CommandLine -notlike '*vscode*' -and $_.CommandLine -notlike '*isort*' }
Where-Object { $_.CommandLine -like '*ledgrab*' -and $_.CommandLine -notlike '*demo*' -and $_.CommandLine -notlike '*vscode*' -and $_.CommandLine -notlike '*isort*' }
if ($procs) {
# Step 1: Request graceful shutdown via API (triggers lifespan shutdown + store save)
@@ -46,7 +46,7 @@ if ($procs) {
Start-Sleep -Seconds 1
$waited++
$still = Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
Where-Object { $_.CommandLine -like '*wled_controller*' -and $_.CommandLine -notlike '*demo*' -and $_.CommandLine -notlike '*vscode*' -and $_.CommandLine -notlike '*isort*' }
Where-Object { $_.CommandLine -like '*ledgrab*' -and $_.CommandLine -notlike '*demo*' -and $_.CommandLine -notlike '*vscode*' -and $_.CommandLine -notlike '*isort*' }
if (-not $still) {
Write-Host " Server exited cleanly after ${waited}s"
break
@@ -54,7 +54,7 @@ if ($procs) {
}
# Step 3: Force-kill stragglers
$still = Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
Where-Object { $_.CommandLine -like '*wled_controller*' -and $_.CommandLine -notlike '*demo*' -and $_.CommandLine -notlike '*vscode*' -and $_.CommandLine -notlike '*isort*' }
Where-Object { $_.CommandLine -like '*ledgrab*' -and $_.CommandLine -notlike '*demo*' -and $_.CommandLine -notlike '*vscode*' -and $_.CommandLine -notlike '*isort*' }
if ($still) {
Write-Host " Force-killing remaining processes..."
foreach ($p in $still) {
@@ -85,13 +85,13 @@ if ($regUser) {
# Start server detached (set WLED_RESTART=1 to skip browser open)
Write-Host "Starting server..."
$env:WLED_RESTART = "1"
$env:LEDGRAB_RESTART = "1"
$pythonExe = (Get-Command python -ErrorAction SilentlyContinue).Source
if (-not $pythonExe) {
# Fallback to known install location
$pythonExe = "$env:LOCALAPPDATA\Programs\Python\Python313\python.exe"
}
Start-Process -FilePath $pythonExe -ArgumentList '-m', 'wled_controller' `
Start-Process -FilePath $pythonExe -ArgumentList '-m', 'ledgrab' `
-WorkingDirectory $serverRoot `
-WindowStyle Hidden
@@ -99,7 +99,7 @@ Start-Sleep -Seconds 3
# Verify it's running
$check = Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
Where-Object { $_.CommandLine -like '*wled_controller*' -and $_.CommandLine -notlike '*demo*' -and $_.CommandLine -notlike '*vscode*' -and $_.CommandLine -notlike '*isort*' }
Where-Object { $_.CommandLine -like '*ledgrab*' -and $_.CommandLine -notlike '*demo*' -and $_.CommandLine -notlike '*vscode*' -and $_.CommandLine -notlike '*isort*' }
if ($check) {
Write-Host "Server started (PID $($check[0].ProcessId))"
} else {
+5 -5
View File
@@ -1,25 +1,25 @@
#!/usr/bin/env bash
# Restart the WLED Screen Controller server (Linux/macOS)
# Restart the LedGrab server (Linux/macOS)
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Stop any running instance
PIDS=$(pgrep -f 'wled_controller\.main' 2>/dev/null || true)
PIDS=$(pgrep -f 'ledgrab\.main' 2>/dev/null || true)
if [ -n "$PIDS" ]; then
echo "Stopping server (PID $PIDS)..."
pkill -f 'wled_controller\.main' 2>/dev/null || true
pkill -f 'ledgrab\.main' 2>/dev/null || true
sleep 2
fi
# Start server detached
echo "Starting server..."
cd "$SCRIPT_DIR"
nohup python -m wled_controller.main > /dev/null 2>&1 &
nohup python -m ledgrab.main > /dev/null 2>&1 &
sleep 3
# Verify it's running
NEW_PID=$(pgrep -f 'wled_controller\.main' 2>/dev/null || true)
NEW_PID=$(pgrep -f 'ledgrab\.main' 2>/dev/null || true)
if [ -n "$NEW_PID" ]; then
echo "Server started (PID $NEW_PID)"
else
+3 -3
View File
@@ -1,8 +1,8 @@
@echo off
REM WLED Screen Controller Restart Script
REM LedGrab Restart Script
REM This script restarts the WLED screen controller server
echo Restarting WLED Screen Controller...
echo Restarting LedGrab...
echo.
REM Stop the server first
@@ -18,7 +18,7 @@ cd /d "%~dp0\.."
REM Start the server
echo.
echo [2/2] Starting server...
python -m wled_controller
python -m ledgrab
REM If the server exits, pause to show any error messages
pause
+3 -3
View File
@@ -8,13 +8,13 @@ WshShell.CurrentDirectory = appRoot
' Set env vars for the child process (inherited via WshShell.Run)
Set procEnv = WshShell.Environment("Process")
procEnv("PYTHONPATH") = appRoot & "\app\src"
procEnv("WLED_CONFIG_PATH") = appRoot & "\app\config\default_config.yaml"
procEnv("LEDGRAB_CONFIG_PATH") = appRoot & "\app\config\default_config.yaml"
' Use embedded python.exe (NOT pythonw.exe) with WindowStyle=0.
' Same pattern as the Media Server sibling app.
embeddedPython = appRoot & "\python\python.exe"
If fso.FileExists(embeddedPython) Then
WshShell.Run """" & embeddedPython & """ -m wled_controller", 0, False
WshShell.Run """" & embeddedPython & """ -m ledgrab", 0, False
Else
WshShell.Run "python -m wled_controller", 0, False
WshShell.Run "python -m ledgrab", 0, False
End If
+1 -1
View File
@@ -2,6 +2,6 @@ Set WshShell = CreateObject("WScript.Shell")
Set FSO = CreateObject("Scripting.FileSystemObject")
' Get parent folder of scripts folder (server root)
WshShell.CurrentDirectory = FSO.GetParentFolderName(FSO.GetParentFolderName(WScript.ScriptFullName))
WshShell.Run "python -m wled_controller", 0, False
WshShell.Run "python -m ledgrab", 0, False
Set FSO = Nothing
Set WshShell = Nothing
+3 -3
View File
@@ -1,15 +1,15 @@
@echo off
REM WLED Screen Controller Startup Script
REM LedGrab Startup Script
REM This script starts the WLED screen controller server
echo Starting WLED Screen Controller...
echo Starting LedGrab...
echo.
REM Change to the server directory (parent of scripts folder)
cd /d "%~dp0\.."
REM Start the server
python -m wled_controller
python -m ledgrab
REM If the server exits, pause to show any error messages
pause
+5 -5
View File
@@ -1,13 +1,13 @@
@echo off
REM WLED Screen Controller Stop Script
REM LedGrab Stop Script
REM This script stops the running WLED screen controller server
echo Stopping WLED Screen Controller...
echo Stopping LedGrab...
echo.
REM Find and kill Python processes running wled_controller.main
REM Find and kill Python processes running ledgrab.main
for /f "tokens=2" %%i in ('tasklist /FI "IMAGENAME eq python.exe" /FO LIST ^| findstr /B "PID:"') do (
wmic process where "ProcessId=%%i" get CommandLine 2>nul | findstr /C:"wled_controller.main" >nul
wmic process where "ProcessId=%%i" get CommandLine 2>nul | findstr /C:"ledgrab.main" >nul
if not errorlevel 1 (
taskkill /PID %%i /F
echo WLED controller process (PID %%i) terminated.
@@ -15,5 +15,5 @@ for /f "tokens=2" %%i in ('tasklist /FI "IMAGENAME eq python.exe" /FO LIST ^| fi
)
echo.
echo Done! WLED Screen Controller stopped.
echo Done! LedGrab stopped.
pause
@@ -3,7 +3,7 @@
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version("wled-screen-controller")
__version__ = version("ledgrab")
except PackageNotFoundError:
# Running from source without pip install (e.g. dev, embedded Python)
__version__ = "0.0.0-dev"
@@ -13,6 +13,6 @@ __email__ = "dolgolyov.alexei@gmail.com"
# ─── Project links ───────────────────────────────────────────
GITEA_BASE_URL = "https://git.dolgolyov-family.by"
GITEA_REPO = "alexei.dolgolyov/wled-screen-controller-mixed"
GITEA_REPO = "alexei.dolgolyov/ledgrab"
REPO_URL = f"{GITEA_BASE_URL}/{GITEA_REPO}"
DONATE_URL = "" # TODO: set once donation platform is chosen
@@ -1,4 +1,4 @@
"""Entry point for ``python -m wled_controller``.
"""Entry point for ``python -m ledgrab``.
Starts the uvicorn server and, on Windows when *pystray* is installed,
shows a system-tray icon with **Show UI** / **Exit** actions.
@@ -36,10 +36,10 @@ _fix_embedded_tcl_paths()
import uvicorn # noqa: E402
from wled_controller.config import get_config # noqa: E402
from wled_controller.server_ref import set_server, set_tray # noqa: E402
from wled_controller.tray import PYSTRAY_AVAILABLE, TrayManager # noqa: E402
from wled_controller.utils import setup_logging, get_logger # noqa: E402
from ledgrab.config import get_config # noqa: E402
from ledgrab.server_ref import set_server, set_tray # noqa: E402
from ledgrab.tray import PYSTRAY_AVAILABLE, TrayManager # noqa: E402
from ledgrab.utils import setup_logging, get_logger # noqa: E402
setup_logging()
logger = get_logger(__name__)
@@ -62,7 +62,7 @@ def _open_browser(port: int, delay: float = 2.0) -> None:
def _is_restart() -> bool:
"""Detect if this is a restart (vs first launch)."""
return os.environ.get("WLED_RESTART", "") == "1"
return os.environ.get("LEDGRAB_RESTART", "") == "1"
def _check_port(host: str, port: int) -> None:
@@ -81,7 +81,7 @@ def main() -> None:
_check_port(config.server.host, config.server.port)
uv_config = uvicorn.Config(
"wled_controller.main:app",
"ledgrab.main:app",
host=config.server.host,
port=config.server.port,
log_level=config.server.log_level.lower(),
@@ -133,10 +133,10 @@ def _request_shutdown(server: uvicorn.Server) -> None:
def _force_tray() -> bool:
"""Allow forcing tray on non-Windows via WLED_TRAY=1."""
"""Allow forcing tray on non-Windows via LEDGRAB_TRAY=1."""
import os
return os.environ.get("WLED_TRAY", "").strip() in ("1", "true", "yes")
return os.environ.get("LEDGRAB_TRAY", "").strip() in ("1", "true", "yes")
if __name__ == "__main__":
@@ -6,8 +6,8 @@ from typing import Annotated
from fastapi import Depends, HTTPException, Security, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from wled_controller.config import get_config
from wled_controller.utils import get_logger
from ledgrab.config import get_config
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -6,39 +6,39 @@ All getter function signatures remain unchanged for FastAPI Depends() compatibil
from typing import Any, Dict, TypeVar
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage.database import Database
from wled_controller.storage import DeviceStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage.audio_source_store import AudioSourceStore
from wled_controller.storage.audio_template_store import AudioTemplateStore
from wled_controller.storage.value_source_store import ValueSourceStore
from wled_controller.storage.automation_store import AutomationStore
from wled_controller.storage.scene_preset_store import ScenePresetStore
from wled_controller.storage.sync_clock_store import SyncClockStore
from wled_controller.storage.color_strip_processing_template_store import (
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.storage.database import Database
from ledgrab.storage import DeviceStore
from ledgrab.storage.template_store import TemplateStore
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
from ledgrab.storage.picture_source_store import PictureSourceStore
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.storage.color_strip_store import ColorStripStore
from ledgrab.storage.audio_source_store import AudioSourceStore
from ledgrab.storage.audio_template_store import AudioTemplateStore
from ledgrab.storage.value_source_store import ValueSourceStore
from ledgrab.storage.automation_store import AutomationStore
from ledgrab.storage.scene_preset_store import ScenePresetStore
from ledgrab.storage.sync_clock_store import SyncClockStore
from ledgrab.storage.color_strip_processing_template_store import (
ColorStripProcessingTemplateStore,
)
from wled_controller.storage.gradient_store import GradientStore
from wled_controller.storage.weather_source_store import WeatherSourceStore
from wled_controller.storage.asset_store import AssetStore
from wled_controller.core.automations.automation_engine import AutomationEngine
from wled_controller.core.weather.weather_manager import WeatherManager
from wled_controller.core.backup.auto_backup import AutoBackupEngine
from wled_controller.core.processing.sync_clock_manager import SyncClockManager
from wled_controller.core.update.update_service import UpdateService
from wled_controller.storage.home_assistant_store import HomeAssistantStore
from wled_controller.core.home_assistant.ha_manager import HomeAssistantManager
from wled_controller.storage.game_integration_store import GameIntegrationStore
from wled_controller.core.game_integration.event_bus import GameEventBus
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
from wled_controller.storage.pattern_template_store import PatternTemplateStore
from ledgrab.storage.gradient_store import GradientStore
from ledgrab.storage.weather_source_store import WeatherSourceStore
from ledgrab.storage.asset_store import AssetStore
from ledgrab.core.automations.automation_engine import AutomationEngine
from ledgrab.core.weather.weather_manager import WeatherManager
from ledgrab.core.backup.auto_backup import AutoBackupEngine
from ledgrab.core.processing.sync_clock_manager import SyncClockManager
from ledgrab.core.update.update_service import UpdateService
from ledgrab.storage.home_assistant_store import HomeAssistantStore
from ledgrab.core.home_assistant.ha_manager import HomeAssistantManager
from ledgrab.storage.game_integration_store import GameIntegrationStore
from ledgrab.core.game_integration.event_bus import GameEventBus
from ledgrab.storage.mqtt_source_store import MQTTSourceStore
from ledgrab.core.mqtt.mqtt_manager import MQTTManager
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
from ledgrab.storage.pattern_template_store import PatternTemplateStore
T = TypeVar("T")
@@ -8,9 +8,9 @@ from typing import Callable, Optional
import numpy as np
from starlette.websockets import WebSocket
from wled_controller.core.filters import FilterRegistry, ImagePool
from wled_controller.utils import get_logger
from wled_controller.utils.image_codec import (
from ledgrab.core.filters import FilterRegistry, ImagePool
from ledgrab.utils import get_logger
from ledgrab.utils.image_codec import (
encode_jpeg,
encode_jpeg_data_uri,
resize_down,
@@ -31,7 +31,8 @@ def authenticate_ws_token(token: str) -> bool:
Delegates to the canonical implementation in auth module.
"""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
return verify_ws_token(token)
@@ -160,14 +161,16 @@ async def stream_capture_test(
thumb_uri = _encode_jpeg(thumb, PREVIEW_JPEG_QUALITY)
fps = fc / elapsed if elapsed > 0 else 0
avg_ms = (tc / fc * 1000) if fc > 0 else 0
await websocket.send_json({
"type": "frame",
"thumbnail": thumb_uri,
"frame_count": fc,
"elapsed_s": round(elapsed, 2),
"fps": round(fps, 1),
"avg_capture_ms": round(avg_ms, 1),
})
await websocket.send_json(
{
"type": "frame",
"thumbnail": thumb_uri,
"frame_count": fc,
"elapsed_s": round(elapsed, 2),
"fps": round(fps, 1),
"avg_capture_ms": round(avg_ms, 1),
}
)
# Wait for capture thread to fully finish
await capture_future
@@ -199,17 +202,19 @@ async def stream_capture_test(
thumb = _make_thumbnail(final_frame, FINAL_THUMBNAIL_WIDTH)
thumb_uri = _encode_jpeg(thumb, 85)
await websocket.send_json({
"type": "result",
"full_image": full_uri,
"thumbnail": thumb_uri,
"width": w,
"height": h,
"frame_count": fc,
"elapsed_s": round(elapsed, 2),
"fps": round(fps, 1),
"avg_capture_ms": round(avg_ms, 1),
})
await websocket.send_json(
{
"type": "result",
"full_image": full_uri,
"thumbnail": thumb_uri,
"width": w,
"height": h,
"frame_count": fc,
"elapsed_s": round(elapsed, 2),
"fps": round(fps, 1),
"avg_capture_ms": round(avg_ms, 1),
}
)
except Exception as e:
# WebSocket disconnect or send error — signal capture thread to stop
@@ -5,17 +5,17 @@ from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
from fastapi.responses import FileResponse
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import fire_entity_event, get_asset_store
from wled_controller.api.schemas.assets import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import fire_entity_event, get_asset_store
from ledgrab.api.schemas.assets import (
AssetListResponse,
AssetResponse,
AssetUpdate,
)
from wled_controller.config import get_config
from wled_controller.storage.asset_store import AssetStore
from wled_controller.storage.base_store import EntityNotFoundError
from wled_controller.utils import get_logger
from ledgrab.config import get_config
from ledgrab.storage.asset_store import AssetStore
from ledgrab.storage.base_store import EntityNotFoundError
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -103,7 +103,9 @@ async def upload_asset(
if not data:
raise HTTPException(status_code=400, detail="Empty file")
display_name = name or Path(file.filename or "unnamed").stem.replace("_", " ").replace("-", " ").title()
display_name = (
name or Path(file.filename or "unnamed").stem.replace("_", " ").replace("-", " ").title()
)
try:
asset = store.create_asset(
@@ -4,8 +4,8 @@ import asyncio
from fastapi import APIRouter
from wled_controller.api.auth import AuthRequired
from wled_controller.core.audio.audio_capture import AudioCaptureManager
from ledgrab.api.auth import AuthRequired
from ledgrab.core.audio.audio_capture import AudioCaptureManager
router = APIRouter()
@@ -2,15 +2,15 @@
from fastapi import APIRouter, Depends
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import get_audio_processing_template_store
from wled_controller.api.schemas.filters import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import get_audio_processing_template_store
from ledgrab.api.schemas.filters import (
FilterOptionDefSchema,
FilterTypeListResponse,
FilterTypeResponse,
)
from wled_controller.core.audio.filters import AudioFilterRegistry
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
from ledgrab.core.audio.filters import AudioFilterRegistry
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
router = APIRouter()
@@ -2,24 +2,24 @@
from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_audio_processing_template_store,
get_audio_source_store,
get_processor_manager,
)
from wled_controller.api.schemas.audio_processing import (
from ledgrab.api.schemas.audio_processing import (
AudioProcessingTemplateCreate,
AudioProcessingTemplateListResponse,
AudioProcessingTemplateResponse,
AudioProcessingTemplateUpdate,
)
from wled_controller.api.schemas.filters import FilterInstanceSchema
from wled_controller.core.filters.filter_instance import FilterInstance
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
from wled_controller.storage.base_store import EntityNotFoundError
from wled_controller.utils import get_logger
from ledgrab.api.schemas.filters import FilterInstanceSchema
from ledgrab.core.filters.filter_instance import FilterInstance
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
from ledgrab.storage.base_store import EntityNotFoundError
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -6,8 +6,8 @@ from typing import Annotated, Optional
from fastapi import APIRouter, Body, Depends, HTTPException, Query
from starlette.websockets import WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_audio_processing_template_store,
get_audio_source_store,
@@ -15,7 +15,7 @@ from wled_controller.api.dependencies import (
get_color_strip_store,
get_processor_manager,
)
from wled_controller.api.schemas.audio_sources import (
from ledgrab.api.schemas.audio_sources import (
AudioSourceCreate,
AudioSourceListResponse,
AudioSourceResponse,
@@ -23,15 +23,15 @@ from wled_controller.api.schemas.audio_sources import (
CaptureAudioSourceResponse,
ProcessedAudioSourceResponse,
)
from wled_controller.storage.audio_source import (
from ledgrab.storage.audio_source import (
AudioSource,
CaptureAudioSource,
ProcessedAudioSource,
)
from wled_controller.storage.audio_source_store import AudioSourceStore
from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.storage.audio_source_store import AudioSourceStore
from ledgrab.storage.color_strip_store import ColorStripStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -178,7 +178,7 @@ async def delete_audio_source(
"""Delete an audio source."""
try:
# Check if any CSS entities reference this audio source
from wled_controller.storage.color_strip_source import AudioColorStripSource
from ledgrab.storage.color_strip_source import AudioColorStripSource
for css in css_store.get_all_sources():
if (
@@ -215,8 +215,8 @@ async def test_audio_source_ws(
analysis before sending, so the WebSocket output matches what running
streams see.
"""
from wled_controller.api.auth import verify_ws_token
from wled_controller.core.audio.filters.pipeline import build_pipeline_from_template_ids
from ledgrab.api.auth import verify_ws_token
from ledgrab.core.audio.filters.pipeline import build_pipeline_from_template_ids
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -4,9 +4,14 @@ import asyncio
from fastapi import APIRouter, HTTPException, Depends, Query
from starlette.websockets import WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import fire_entity_event, get_audio_template_store, get_audio_source_store, get_processor_manager
from wled_controller.api.schemas.audio_templates import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_audio_template_store,
get_audio_source_store,
get_processor_manager,
)
from ledgrab.api.schemas.audio_templates import (
AudioEngineInfo,
AudioEngineListResponse,
AudioTemplateCreate,
@@ -14,11 +19,11 @@ from wled_controller.api.schemas.audio_templates import (
AudioTemplateResponse,
AudioTemplateUpdate,
)
from wled_controller.core.audio.factory import AudioEngineRegistry
from wled_controller.storage.audio_template_store import AudioTemplateStore
from wled_controller.storage.audio_source_store import AudioSourceStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.core.audio.factory import AudioEngineRegistry
from ledgrab.storage.audio_template_store import AudioTemplateStore
from ledgrab.storage.audio_source_store import AudioSourceStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -27,7 +32,10 @@ router = APIRouter()
# ===== AUDIO TEMPLATE ENDPOINTS =====
@router.get("/api/v1/audio-templates", response_model=AudioTemplateListResponse, tags=["Audio Templates"])
@router.get(
"/api/v1/audio-templates", response_model=AudioTemplateListResponse, tags=["Audio Templates"]
)
async def list_audio_templates(
_auth: AuthRequired,
store: AudioTemplateStore = Depends(get_audio_template_store),
@@ -37,10 +45,14 @@ async def list_audio_templates(
templates = store.get_all_templates()
responses = [
AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, tags=t.tags,
id=t.id,
name=t.name,
engine_type=t.engine_type,
engine_config=t.engine_config,
tags=t.tags,
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
updated_at=t.updated_at,
description=t.description,
)
for t in templates
]
@@ -50,7 +62,12 @@ async def list_audio_templates(
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/api/v1/audio-templates", response_model=AudioTemplateResponse, tags=["Audio Templates"], status_code=201)
@router.post(
"/api/v1/audio-templates",
response_model=AudioTemplateResponse,
tags=["Audio Templates"],
status_code=201,
)
async def create_audio_template(
data: AudioTemplateCreate,
_auth: AuthRequired,
@@ -59,16 +76,22 @@ async def create_audio_template(
"""Create a new audio capture template."""
try:
template = store.create_template(
name=data.name, engine_type=data.engine_type,
engine_config=data.engine_config, description=data.description,
name=data.name,
engine_type=data.engine_type,
engine_config=data.engine_config,
description=data.description,
tags=data.tags,
)
fire_entity_event("audio_template", "created", template.id)
return AudioTemplateResponse(
id=template.id, name=template.name, engine_type=template.engine_type,
engine_config=template.engine_config, tags=template.tags,
id=template.id,
name=template.name,
engine_type=template.engine_type,
engine_config=template.engine_config,
tags=template.tags,
created_at=template.created_at,
updated_at=template.updated_at, description=template.description,
updated_at=template.updated_at,
description=template.description,
)
except EntityNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
@@ -80,7 +103,11 @@ async def create_audio_template(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/api/v1/audio-templates/{template_id}", response_model=AudioTemplateResponse, tags=["Audio Templates"])
@router.get(
"/api/v1/audio-templates/{template_id}",
response_model=AudioTemplateResponse,
tags=["Audio Templates"],
)
async def get_audio_template(
template_id: str,
_auth: AuthRequired,
@@ -92,14 +119,22 @@ async def get_audio_template(
except ValueError:
raise HTTPException(status_code=404, detail=f"Audio template {template_id} not found")
return AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, tags=t.tags,
id=t.id,
name=t.name,
engine_type=t.engine_type,
engine_config=t.engine_config,
tags=t.tags,
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
updated_at=t.updated_at,
description=t.description,
)
@router.put("/api/v1/audio-templates/{template_id}", response_model=AudioTemplateResponse, tags=["Audio Templates"])
@router.put(
"/api/v1/audio-templates/{template_id}",
response_model=AudioTemplateResponse,
tags=["Audio Templates"],
)
async def update_audio_template(
template_id: str,
data: AudioTemplateUpdate,
@@ -109,16 +144,23 @@ async def update_audio_template(
"""Update an audio template."""
try:
t = store.update_template(
template_id=template_id, name=data.name,
engine_type=data.engine_type, engine_config=data.engine_config,
description=data.description, tags=data.tags,
template_id=template_id,
name=data.name,
engine_type=data.engine_type,
engine_config=data.engine_config,
description=data.description,
tags=data.tags,
)
fire_entity_event("audio_template", "updated", template_id)
return AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, tags=t.tags,
id=t.id,
name=t.name,
engine_type=t.engine_type,
engine_config=t.engine_config,
tags=t.tags,
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
updated_at=t.updated_at,
description=t.description,
)
except EntityNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
@@ -155,7 +197,10 @@ async def delete_audio_template(
# ===== AUDIO ENGINE ENDPOINTS =====
@router.get("/api/v1/audio-engines", response_model=AudioEngineListResponse, tags=["Audio Templates"])
@router.get(
"/api/v1/audio-engines", response_model=AudioEngineListResponse, tags=["Audio Templates"]
)
async def list_audio_engines(_auth: AuthRequired):
"""List all registered audio capture engines."""
try:
@@ -195,7 +240,8 @@ async def test_audio_template_ws(
Auth via ?token=<api_key>. Device specified via ?device_index=N&is_loopback=0|1.
Streams AudioAnalysis snapshots as JSON at ~20 Hz.
"""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
return
@@ -214,13 +260,17 @@ async def test_audio_template_ws(
loopback = is_loopback != 0
try:
stream = audio_mgr.acquire(device_index, loopback, template.engine_type, template.engine_config)
stream = audio_mgr.acquire(
device_index, loopback, template.engine_type, template.engine_config
)
except RuntimeError as e:
await websocket.close(code=4003, reason=str(e))
return
await websocket.accept()
logger.info(f"Audio template test WS connected: template={template_id} device={device_index} loopback={loopback}")
logger.info(
f"Audio template test WS connected: template={template_id} device={device_index} loopback={loopback}"
)
last_ts = 0.0
try:
@@ -228,13 +278,15 @@ async def test_audio_template_ws(
analysis = stream.get_latest_analysis()
if analysis is not None and analysis.timestamp != last_ts:
last_ts = analysis.timestamp
await websocket.send_json({
"spectrum": analysis.spectrum.tolist(),
"rms": round(analysis.rms, 4),
"peak": round(analysis.peak, 4),
"beat": analysis.beat,
"beat_intensity": round(analysis.beat_intensity, 4),
})
await websocket.send_json(
{
"spectrum": analysis.spectrum.tolist(),
"rms": round(analysis.rms, 4),
"peak": round(analysis.peak, 4),
"beat": analysis.beat,
"beat_intensity": round(analysis.beat_intensity, 4),
}
)
await asyncio.sleep(0.05)
except WebSocketDisconnect:
logger.debug("Audio template test WebSocket disconnected")
@@ -4,22 +4,22 @@ import secrets
from fastapi import APIRouter, Depends, HTTPException, Request
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_automation_engine,
get_automation_store,
get_scene_preset_store,
)
from wled_controller.api.schemas.automations import (
from ledgrab.api.schemas.automations import (
AutomationCreate,
AutomationListResponse,
AutomationResponse,
AutomationUpdate,
RuleSchema,
)
from wled_controller.core.automations.automation_engine import AutomationEngine
from wled_controller.storage.automation import (
from ledgrab.core.automations.automation_engine import AutomationEngine
from ledgrab.storage.automation import (
ApplicationRule,
DisplayStateRule,
HomeAssistantRule,
@@ -30,10 +30,10 @@ from wled_controller.storage.automation import (
TimeOfDayRule,
WebhookRule,
)
from wled_controller.storage.automation_store import AutomationStore
from wled_controller.storage.scene_preset_store import ScenePresetStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.storage.automation_store import AutomationStore
from ledgrab.storage.scene_preset_store import ScenePresetStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
router = APIRouter()
@@ -97,7 +97,7 @@ def _automation_to_response(
for r in automation.rules:
if isinstance(r, WebhookRule) and r.token:
# Prefer configured external URL, fall back to request base URL
from wled_controller.api.routes.system import load_external_url
from ledgrab.api.routes.system import load_external_url
ext = load_external_url()
if ext:
@@ -15,20 +15,20 @@ from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import get_asset_store, get_auto_backup_engine, get_database
from wled_controller.api.schemas.system import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import get_asset_store, get_auto_backup_engine, get_database
from ledgrab.api.schemas.system import (
AutoBackupSettings,
AutoBackupStatusResponse,
BackupFileInfo,
BackupListResponse,
RestoreResponse,
)
from wled_controller.config import get_config
from wled_controller.core.backup.auto_backup import AutoBackupEngine
from wled_controller.storage.asset_store import AssetStore
from wled_controller.storage.database import Database, freeze_writes
from wled_controller.utils import get_logger
from ledgrab.config import get_config
from ledgrab.core.backup.auto_backup import AutoBackupEngine
from ledgrab.storage.asset_store import AssetStore
from ledgrab.storage.database import Database, freeze_writes
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -42,11 +42,17 @@ def _schedule_restart() -> None:
def _restart():
import time
time.sleep(1)
if sys.platform == "win32":
subprocess.Popen(
["powershell", "-ExecutionPolicy", "Bypass", "-File",
str(_SERVER_DIR / "restart.ps1")],
[
"powershell",
"-ExecutionPolicy",
"Bypass",
"-File",
str(_SERVER_DIR / "restart.ps1"),
],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
)
else:
@@ -71,6 +77,7 @@ def backup_config(
):
"""Download a full backup as a .zip containing the database and asset files."""
import tempfile
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
tmp_path = Path(tmp.name)
@@ -95,6 +102,7 @@ def backup_config(
zip_buffer.seek(0)
from datetime import datetime, timezone
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
filename = f"ledgrab-backup-{timestamp}.zip"
@@ -129,7 +137,9 @@ async def restore_config(
is_sqlite = raw[:16].startswith(b"SQLite format 3")
if not is_zip and not is_sqlite:
raise HTTPException(status_code=400, detail="Not a valid backup file (expected .zip or .db)")
raise HTTPException(
status_code=400, detail="Not a valid backup file (expected .zip or .db)"
)
if is_zip:
# Extract DB and assets from ZIP
@@ -160,6 +170,7 @@ async def restore_config(
tmp_path = Path(tmp.name)
try:
def _restore():
db.restore_from(tmp_path)
@@ -181,7 +192,8 @@ async def restore_config(
@router.post("/api/v1/system/restart", tags=["System"])
def restart_server(_: AuthRequired):
"""Schedule a server restart and return immediately."""
from wled_controller.server_ref import _broadcast_restarting
from ledgrab.server_ref import _broadcast_restarting
_broadcast_restarting()
_schedule_restart()
return {"status": "restarting"}
@@ -190,7 +202,8 @@ def restart_server(_: AuthRequired):
@router.post("/api/v1/system/shutdown", tags=["System"])
def shutdown_server(_: AuthRequired):
"""Gracefully shut down the server."""
from wled_controller.server_ref import request_shutdown
from ledgrab.server_ref import request_shutdown
request_shutdown()
return {"status": "shutting_down"}
@@ -6,27 +6,27 @@ import uuid as _uuid
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_color_strip_store,
get_cspt_store,
get_device_store,
get_processor_manager,
)
from wled_controller.api.schemas.filters import FilterInstanceSchema
from wled_controller.api.schemas.color_strip_processing import (
from ledgrab.api.schemas.filters import FilterInstanceSchema
from ledgrab.api.schemas.color_strip_processing import (
ColorStripProcessingTemplateCreate,
ColorStripProcessingTemplateListResponse,
ColorStripProcessingTemplateResponse,
ColorStripProcessingTemplateUpdate,
)
from wled_controller.core.filters import FilterInstance
from wled_controller.storage.color_strip_processing_template_store import ColorStripProcessingTemplateStore
from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage import DeviceStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.core.filters import FilterInstance
from ledgrab.storage.color_strip_processing_template_store import ColorStripProcessingTemplateStore
from ledgrab.storage.color_strip_store import ColorStripStore
from ledgrab.storage import DeviceStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -46,7 +46,11 @@ def _cspt_to_response(t) -> ColorStripProcessingTemplateResponse:
)
@router.get("/api/v1/color-strip-processing-templates", response_model=ColorStripProcessingTemplateListResponse, tags=["Color Strip Processing"])
@router.get(
"/api/v1/color-strip-processing-templates",
response_model=ColorStripProcessingTemplateListResponse,
tags=["Color Strip Processing"],
)
async def list_cspt(
_auth: AuthRequired,
store: ColorStripProcessingTemplateStore = Depends(get_cspt_store),
@@ -61,7 +65,12 @@ async def list_cspt(
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/api/v1/color-strip-processing-templates", response_model=ColorStripProcessingTemplateResponse, tags=["Color Strip Processing"], status_code=201)
@router.post(
"/api/v1/color-strip-processing-templates",
response_model=ColorStripProcessingTemplateResponse,
tags=["Color Strip Processing"],
status_code=201,
)
async def create_cspt(
data: ColorStripProcessingTemplateCreate,
_auth: AuthRequired,
@@ -88,7 +97,11 @@ async def create_cspt(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/api/v1/color-strip-processing-templates/{template_id}", response_model=ColorStripProcessingTemplateResponse, tags=["Color Strip Processing"])
@router.get(
"/api/v1/color-strip-processing-templates/{template_id}",
response_model=ColorStripProcessingTemplateResponse,
tags=["Color Strip Processing"],
)
async def get_cspt(
template_id: str,
_auth: AuthRequired,
@@ -99,10 +112,16 @@ async def get_cspt(
template = store.get_template(template_id)
return _cspt_to_response(template)
except ValueError:
raise HTTPException(status_code=404, detail=f"Color strip processing template {template_id} not found")
raise HTTPException(
status_code=404, detail=f"Color strip processing template {template_id} not found"
)
@router.put("/api/v1/color-strip-processing-templates/{template_id}", response_model=ColorStripProcessingTemplateResponse, tags=["Color Strip Processing"])
@router.put(
"/api/v1/color-strip-processing-templates/{template_id}",
response_model=ColorStripProcessingTemplateResponse,
tags=["Color Strip Processing"],
)
async def update_cspt(
template_id: str,
data: ColorStripProcessingTemplateUpdate,
@@ -111,7 +130,11 @@ async def update_cspt(
):
"""Update a color strip processing template."""
try:
filters = [FilterInstance(f.filter_id, f.options) for f in data.filters] if data.filters is not None else None
filters = (
[FilterInstance(f.filter_id, f.options) for f in data.filters]
if data.filters is not None
else None
)
template = store.update_template(
template_id=template_id,
name=data.name,
@@ -131,7 +154,11 @@ async def update_cspt(
raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/api/v1/color-strip-processing-templates/{template_id}", status_code=204, tags=["Color Strip Processing"])
@router.delete(
"/api/v1/color-strip-processing-templates/{template_id}",
status_code=204,
tags=["Color Strip Processing"],
)
async def delete_cspt(
template_id: str,
_auth: AuthRequired,
@@ -147,7 +174,7 @@ async def delete_cspt(
raise HTTPException(
status_code=409,
detail=f"Cannot delete: template is referenced by: {names}. "
"Please reassign before deleting.",
"Please reassign before deleting.",
)
store.delete_template(template_id)
fire_entity_event("cspt", "deleted", template_id)
@@ -165,6 +192,7 @@ async def delete_cspt(
# ── Test / Preview WebSocket ──────────────────────────────────────────
@router.websocket("/api/v1/color-strip-processing-templates/{template_id}/test/ws")
async def test_cspt_ws(
websocket: WebSocket,
@@ -179,9 +207,9 @@ async def test_cspt_ws(
Takes an input CSS source, applies the CSPT filter chain, and streams
the processed RGB frames. Auth via ``?token=<api_key>``.
"""
from wled_controller.api.auth import verify_ws_token
from wled_controller.core.filters import FilterRegistry
from wled_controller.core.processing.processor_manager import ProcessorManager
from ledgrab.api.auth import verify_ws_token
from ledgrab.core.filters import FilterRegistry
from ledgrab.core.processing.processor_manager import ProcessorManager
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -9,8 +9,8 @@ from typing import Annotated
import numpy as np
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_color_strip_store,
get_device_store,
@@ -20,7 +20,7 @@ from wled_controller.api.dependencies import (
get_processor_manager,
get_template_store,
)
from wled_controller.api.schemas.color_strip_sources import (
from ledgrab.api.schemas.color_strip_sources import (
ApiInputCSSResponse,
AudioCSSResponse,
CandlelightCSSResponse,
@@ -47,17 +47,17 @@ from wled_controller.api.schemas.color_strip_sources import (
StaticCSSResponse,
WeatherCSSResponse,
)
from wled_controller.api.schemas.devices import (
from ledgrab.api.schemas.devices import (
Calibration as CalibrationSchema,
CalibrationTestModeResponse,
)
from wled_controller.core.capture.calibration import (
from ledgrab.core.capture.calibration import (
calibration_from_dict,
calibration_to_dict,
)
from wled_controller.core.capture.screen_capture import get_available_displays
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage.color_strip_source import (
from ledgrab.core.capture.screen_capture import get_available_displays
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.storage.color_strip_source import (
AdvancedPictureColorStripSource,
ApiInputColorStripSource,
AudioColorStripSource,
@@ -76,17 +76,17 @@ from wled_controller.storage.color_strip_source import (
StaticColorStripSource,
WeatherColorStripSource,
)
from wled_controller.storage import DeviceStore
from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.picture_source import (
from ledgrab.storage import DeviceStore
from ledgrab.storage.color_strip_store import ColorStripStore
from ledgrab.storage.template_store import TemplateStore
from ledgrab.storage.picture_source import (
ProcessedPictureSource,
ScreenCapturePictureSource,
)
from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.storage.picture_source_store import PictureSourceStore
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -371,7 +371,7 @@ async def create_color_strip_source(
if data.source_type == "composite" and kwargs.get("layers"):
child_ids = [ly.get("source_id", "") for ly in kwargs["layers"] if ly.get("source_id")]
# No parent_id yet (new source), just check depth
from wled_controller.storage.color_strip_store import MAX_COMPOSITE_DEPTH
from ledgrab.storage.color_strip_store import MAX_COMPOSITE_DEPTH
for cid in child_ids:
depth = store.get_nesting_depth(cid)
@@ -524,19 +524,19 @@ async def test_key_colors_source(
pp_template_store=Depends(get_pp_template_store),
):
"""Test a key_colors source: capture a frame, extract colors from each rectangle."""
from wled_controller.storage.color_strip_source import KeyColorsColorStripSource
from wled_controller.core.capture.screen_capture import (
from ledgrab.storage.color_strip_source import KeyColorsColorStripSource
from ledgrab.core.capture.screen_capture import (
calculate_average_color,
calculate_dominant_color,
calculate_median_color,
)
from wled_controller.core.capture_engines import EngineRegistry
from wled_controller.core.filters import FilterRegistry, ImagePool
from wled_controller.storage.picture_source import (
from ledgrab.core.capture_engines import EngineRegistry
from ledgrab.core.filters import FilterRegistry, ImagePool
from ledgrab.storage.picture_source import (
ScreenCapturePictureSource,
StaticImagePictureSource,
)
from wled_controller.utils.image_codec import encode_jpeg_data_uri
from ledgrab.utils.image_codec import encode_jpeg_data_uri
stream = None
try:
@@ -553,10 +553,10 @@ async def test_key_colors_source(
chain = source_store.resolve_stream_chain(source.picture_source_id)
raw_stream = chain["raw_stream"]
from wled_controller.utils.image_codec import load_image_file
from ledgrab.utils.image_codec import load_image_file
if isinstance(raw_stream, StaticImagePictureSource):
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
from ledgrab.api.dependencies import get_asset_store as _get_asset_store
asset_store = _get_asset_store()
image_path = (
@@ -681,15 +681,15 @@ async def test_key_colors_ws(
"""WebSocket for real-time key_colors test preview with frame + rectangle overlay."""
import json as ws_json
import time as ws_time
from wled_controller.api.auth import verify_ws_token
from wled_controller.storage.color_strip_source import KeyColorsColorStripSource
from wled_controller.core.capture.screen_capture import (
from ledgrab.api.auth import verify_ws_token
from ledgrab.storage.color_strip_source import KeyColorsColorStripSource
from ledgrab.core.capture.screen_capture import (
calculate_average_color,
calculate_dominant_color,
calculate_median_color,
)
from wled_controller.storage.picture_source import ScreenCapturePictureSource
from wled_controller.utils.image_codec import encode_jpeg_data_uri, resize_down
from ledgrab.storage.picture_source import ScreenCapturePictureSource
from ledgrab.utils.image_codec import encode_jpeg_data_uri, resize_down
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -1095,7 +1095,7 @@ async def notify_source(
@router.get("/api/v1/color-strip-sources/os-notifications/history", tags=["Color Strip Sources"])
async def os_notification_history(_auth: AuthRequired):
"""Return recent OS notification capture history (newest first)."""
from wled_controller.core.processing.os_notification_listener import (
from ledgrab.core.processing.os_notification_listener import (
get_os_notification_listener,
)
@@ -1139,7 +1139,7 @@ async def preview_color_strip_ws(
Subsequent text messages are treated as config updates: if the source_type
changed the old stream is replaced; otherwise ``update_source()`` is used.
"""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -1168,7 +1168,7 @@ async def preview_color_strip_ws(
def _build_source(config: dict):
"""Build a ColorStripSource from a raw config dict, injecting synthetic id/name."""
from wled_controller.storage.color_strip_source import ColorStripSource
from ledgrab.storage.color_strip_source import ColorStripSource
config.setdefault("id", "__preview__")
config.setdefault("name", "__preview__")
@@ -1176,7 +1176,7 @@ async def preview_color_strip_ws(
def _create_stream(source):
"""Instantiate and start the appropriate stream class for *source*."""
from wled_controller.core.processing.color_strip_stream_manager import _SIMPLE_STREAM_MAP
from ledgrab.core.processing.color_strip_stream_manager import _SIMPLE_STREAM_MAP
stream_cls = _SIMPLE_STREAM_MAP.get(source.source_type)
if not stream_cls:
@@ -1185,7 +1185,7 @@ async def preview_color_strip_ws(
# Inject gradient store for palette resolution
if hasattr(s, "set_gradient_store"):
try:
from wled_controller.api.dependencies import get_gradient_store
from ledgrab.api.dependencies import get_gradient_store
s.set_gradient_store(get_gradient_store())
except Exception:
@@ -1283,7 +1283,7 @@ async def preview_color_strip_ws(
# Handle "fire" command for notification streams
if new_config.get("action") == "fire":
from wled_controller.core.processing.notification_stream import (
from ledgrab.core.processing.notification_stream import (
NotificationColorStripStream,
)
@@ -1349,7 +1349,7 @@ async def css_api_input_ws(
Auth via ?token=<api_key>. Accepts JSON frames ({"colors": [[R,G,B], ...]})
or binary frames (raw RGBRGB... bytes, 3 bytes per LED).
"""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -1391,7 +1391,7 @@ async def css_api_input_ws(
if "segments" in data:
# Segment-based path — validate and push
try:
from wled_controller.api.schemas.color_strip_sources import SegmentPayload
from ledgrab.api.schemas.color_strip_sources import SegmentPayload
seg_dicts = [SegmentPayload(**s).model_dump() for s in data["segments"]]
except Exception as e:
@@ -1460,7 +1460,7 @@ async def test_color_strip_ws(
First message is JSON metadata (source_type, led_count, calibration segments).
Subsequent messages are binary RGB frames (``led_count * 3`` bytes).
"""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -1506,9 +1506,9 @@ async def test_color_strip_ws(
logger.info(f"CSS test WebSocket connected for {source_id} (fps={fps})")
try:
from wled_controller.core.processing.composite_stream import CompositeColorStripStream
from ledgrab.core.processing.composite_stream import CompositeColorStripStream
from wled_controller.core.processing.api_input_stream import ApiInputColorStripStream
from ledgrab.core.processing.api_input_stream import ApiInputColorStripStream
is_api_input = isinstance(stream, ApiInputColorStripStream)
_last_push_gen = 0 # track api_input push generation to skip unchanged frames
@@ -1643,7 +1643,7 @@ async def test_color_strip_ws(
try:
frame = _frame_live.get_latest_frame()
if frame is not None and frame.image is not None:
from wled_controller.utils.image_codec import encode_jpeg
from ledgrab.utils.image_codec import encode_jpeg
import cv2 as _cv2
img = frame.image
@@ -3,19 +3,19 @@
import httpx
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.core.devices.led_client import (
from ledgrab.api.auth import AuthRequired
from ledgrab.core.devices.led_client import (
get_all_providers,
get_device_capabilities,
get_provider,
)
from wled_controller.api.dependencies import (
from ledgrab.api.dependencies import (
fire_entity_event,
get_device_store,
get_output_target_store,
get_processor_manager,
)
from wled_controller.api.schemas.devices import (
from ledgrab.api.schemas.devices import (
BrightnessRequest,
DeviceCreate,
DeviceListResponse,
@@ -28,10 +28,10 @@ from wled_controller.api.schemas.devices import (
OpenRGBZonesResponse,
PowerRequest,
)
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.storage import DeviceStore
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -300,14 +300,14 @@ async def get_openrgb_zones(
"""List available zones on an OpenRGB device."""
import asyncio
from wled_controller.core.devices.openrgb_client import parse_openrgb_url
from ledgrab.core.devices.openrgb_client import parse_openrgb_url
host, port, device_index, _zones = parse_openrgb_url(url)
def _fetch_zones():
from openrgb import OpenRGBClient
client = OpenRGBClient(host, port, name="WLED Controller (zones)")
client = OpenRGBClient(host, port, name="LedGrab (zones)")
try:
devices = client.devices
if device_index >= len(devices):
@@ -742,7 +742,7 @@ async def device_ws_stream(
Wire format: [brightness_byte][R G B R G B ...]
Auth via ?token=<api_key>.
"""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -760,7 +760,7 @@ async def device_ws_stream(
await websocket.accept()
from wled_controller.core.devices.ws_client import get_ws_broadcaster
from ledgrab.core.devices.ws_client import get_ws_broadcaster
broadcaster = get_ws_broadcaster()
broadcaster.add_client(device_id, websocket)
@@ -10,14 +10,14 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_database,
get_game_integration_store,
get_game_event_bus,
)
from wled_controller.api.schemas.game_integration import (
from ledgrab.api.schemas.game_integration import (
AdapterInfoResponse,
AdapterListResponse,
ApplyPresetRequest,
@@ -34,13 +34,13 @@ from wled_controller.api.schemas.game_integration import (
PresetListResponse,
RecentEventsResponse,
)
from wled_controller.core.game_integration.adapter_registry import AdapterRegistry
from wled_controller.core.game_integration.event_bus import GameEventBus
from wled_controller.core.game_integration.events import GameEvent
from wled_controller.storage.base_store import EntityNotFoundError
from wled_controller.storage.game_integration import EventMapping
from wled_controller.storage.game_integration_store import GameIntegrationStore
from wled_controller.utils import get_logger
from ledgrab.core.game_integration.adapter_registry import AdapterRegistry
from ledgrab.core.game_integration.event_bus import GameEventBus
from ledgrab.core.game_integration.events import GameEvent
from ledgrab.storage.base_store import EntityNotFoundError
from ledgrab.storage.game_integration import EventMapping
from ledgrab.storage.game_integration_store import GameIntegrationStore
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -135,7 +135,7 @@ def _cleanup_state(integration_id: str) -> None:
def _config_to_response(config: Any) -> GameIntegrationResponse:
"""Convert a GameIntegrationConfig to its API response."""
from wled_controller.api.schemas.game_integration import EventMappingSchema
from ledgrab.api.schemas.game_integration import EventMappingSchema
return GameIntegrationResponse(
id=config.id,
@@ -171,7 +171,7 @@ def _config_to_response(config: Any) -> GameIntegrationResponse:
)
async def list_presets(_auth: AuthRequired):
"""List all available built-in effect presets."""
from wled_controller.core.game_integration.presets import get_all_presets
from ledgrab.core.game_integration.presets import get_all_presets
presets = get_all_presets()
responses = [
@@ -554,7 +554,7 @@ async def apply_preset(
If replace=true, replaces all existing mappings.
If replace=false (default), appends preset mappings to existing ones.
"""
from wled_controller.core.game_integration.presets import get_preset
from ledgrab.core.game_integration.presets import get_preset
try:
config = store.get_integration(integration_id)
@@ -619,7 +619,7 @@ async def auto_setup_integration(
)
# Determine server URL
from wled_controller.api.routes.system_settings import load_external_url
from ledgrab.api.routes.system_settings import load_external_url
db = get_database()
server_url = load_external_url(db)
@@ -2,23 +2,23 @@
from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_color_strip_store,
get_gradient_store,
)
from wled_controller.api.schemas.gradients import (
from ledgrab.api.schemas.gradients import (
GradientCreate,
GradientListResponse,
GradientResponse,
GradientUpdate,
)
from wled_controller.storage.gradient import Gradient
from wled_controller.storage.gradient_store import GradientStore
from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage.base_store import EntityNotFoundError
from wled_controller.utils import get_logger
from ledgrab.storage.gradient import Gradient
from ledgrab.storage.gradient_store import GradientStore
from ledgrab.storage.color_strip_store import ColorStripStore
from ledgrab.storage.base_store import EntityNotFoundError
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -51,7 +51,9 @@ async def list_gradients(
)
@router.post("/api/v1/gradients", response_model=GradientResponse, status_code=201, tags=["Gradients"])
@router.post(
"/api/v1/gradients", response_model=GradientResponse, status_code=201, tags=["Gradients"]
)
async def create_gradient(
data: GradientCreate,
_auth: AuthRequired,
@@ -109,7 +111,12 @@ async def update_gradient(
raise HTTPException(status_code=status, detail=str(e))
@router.post("/api/v1/gradients/{gradient_id}/clone", response_model=GradientResponse, status_code=201, tags=["Gradients"])
@router.post(
"/api/v1/gradients/{gradient_id}/clone",
response_model=GradientResponse,
status_code=201,
tags=["Gradients"],
)
async def clone_gradient(
gradient_id: str,
_auth: AuthRequired,
@@ -143,9 +150,7 @@ async def delete_gradient(
# Check references
for source in css_store.get_all_sources():
if getattr(source, "gradient_id", None) == gradient_id:
raise ValueError(
f"Cannot delete: referenced by color strip source '{source.name}'"
)
raise ValueError(f"Cannot delete: referenced by color strip source '{source.name}'")
store.delete_gradient(gradient_id)
fire_entity_event("gradient", "deleted", gradient_id)
except (ValueError, EntityNotFoundError) as e:
@@ -5,13 +5,13 @@ import json
from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_ha_manager,
get_ha_store,
)
from wled_controller.api.schemas.home_assistant import (
from ledgrab.api.schemas.home_assistant import (
HomeAssistantConnectionStatus,
HomeAssistantEntityListResponse,
HomeAssistantEntityResponse,
@@ -22,12 +22,12 @@ from wled_controller.api.schemas.home_assistant import (
HomeAssistantStatusResponse,
HomeAssistantTestResponse,
)
from wled_controller.core.home_assistant.ha_manager import HomeAssistantManager
from wled_controller.core.home_assistant.ha_runtime import HARuntime
from wled_controller.storage.base_store import EntityNotFoundError
from wled_controller.storage.home_assistant_source import HomeAssistantSource
from wled_controller.storage.home_assistant_store import HomeAssistantStore
from wled_controller.utils import get_logger
from ledgrab.core.home_assistant.ha_manager import HomeAssistantManager
from ledgrab.core.home_assistant.ha_runtime import HARuntime
from ledgrab.storage.base_store import EntityNotFoundError
from ledgrab.storage.home_assistant_source import HomeAssistantSource
from ledgrab.storage.home_assistant_store import HomeAssistantStore
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -5,13 +5,13 @@ import asyncio
import aiomqtt
from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_mqtt_manager,
get_mqtt_store,
)
from wled_controller.api.schemas.mqtt import (
from ledgrab.api.schemas.mqtt import (
MQTTConnectionStatus,
MQTTSourceCreate,
MQTTSourceListResponse,
@@ -20,11 +20,11 @@ from wled_controller.api.schemas.mqtt import (
MQTTStatusResponse,
MQTTTestResponse,
)
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
from wled_controller.storage.base_store import EntityNotFoundError
from wled_controller.storage.mqtt_source import MQTTSource
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
from wled_controller.utils import get_logger
from ledgrab.core.mqtt.mqtt_manager import MQTTManager
from ledgrab.storage.base_store import EntityNotFoundError
from ledgrab.storage.mqtt_source import MQTTSource
from ledgrab.storage.mqtt_source_store import MQTTSourceStore
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -5,14 +5,14 @@ from typing import Annotated
from fastapi import APIRouter, Body, HTTPException, Depends
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_device_store,
get_output_target_store,
get_processor_manager,
)
from wled_controller.api.schemas.output_targets import (
from ledgrab.api.schemas.output_targets import (
HALightMappingSchema,
HALightOutputTargetResponse,
LedOutputTargetResponse,
@@ -21,17 +21,17 @@ from wled_controller.api.schemas.output_targets import (
OutputTargetResponse,
OutputTargetUpdate,
)
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.bindable import BindableFloat
from wled_controller.storage.wled_output_target import WledOutputTarget
from wled_controller.storage.ha_light_output_target import (
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.storage import DeviceStore
from ledgrab.storage.bindable import BindableFloat
from ledgrab.storage.wled_output_target import WledOutputTarget
from ledgrab.storage.ha_light_output_target import (
HALightMapping,
HALightOutputTarget,
)
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -5,30 +5,30 @@ Extracted from output_targets.py to keep files under 800 lines.
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
get_color_strip_store,
get_output_target_store,
get_picture_source_store,
get_processor_manager,
)
from wled_controller.api.schemas.output_targets import (
from ledgrab.api.schemas.output_targets import (
BulkTargetRequest,
BulkTargetResponse,
TargetMetricsResponse,
TargetProcessingState,
)
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.core.capture.screen_capture import get_available_displays
from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.storage.color_strip_source import (
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.core.capture.screen_capture import get_available_displays
from ledgrab.storage.color_strip_store import ColorStripStore
from ledgrab.storage.color_strip_source import (
AdvancedPictureColorStripSource,
PictureColorStripSource,
)
from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.wled_output_target import WledOutputTarget
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger
from ledgrab.storage.picture_source_store import PictureSourceStore
from ledgrab.storage.wled_output_target import WledOutputTarget
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -208,7 +208,7 @@ async def events_ws(
token: str = Query(""),
):
"""WebSocket for real-time state change events. Auth via ?token=<api_key>."""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -272,7 +272,7 @@ async def start_target_overlay(
):
calibration = css.calibration
# Resolve the display this CSS is capturing
from wled_controller.api.routes.color_strip_sources import (
from ledgrab.api.routes.color_strip_sources import (
_resolve_display_index,
)
@@ -348,7 +348,7 @@ async def ha_light_colors_ws(
Streams: {"type": "colors_update", "colors": {entity_id: {r,g,b,hex}, ...}}
at the target's update_rate.
"""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -390,7 +390,7 @@ async def led_preview_ws(
token: str = Query(""),
):
"""WebSocket for real-time LED strip preview. Sends binary RGB frames. Auth via ?token=<api_key>."""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -2,24 +2,24 @@
from fastapi import APIRouter, HTTPException, Depends
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_pattern_template_store,
get_output_target_store,
)
from wled_controller.api.schemas.pattern_templates import (
from ledgrab.api.schemas.pattern_templates import (
PatternTemplateCreate,
PatternTemplateListResponse,
PatternTemplateResponse,
PatternTemplateUpdate,
)
from wled_controller.api.schemas.output_targets import KeyColorRectangleSchema
from wled_controller.storage.pattern_template import KeyColorRectangle
from wled_controller.storage.pattern_template_store import PatternTemplateStore
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.api.schemas.output_targets import KeyColorRectangleSchema
from ledgrab.storage.pattern_template import KeyColorRectangle
from ledgrab.storage.pattern_template_store import PatternTemplateStore
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -9,20 +9,20 @@ import numpy as np
from fastapi import APIRouter, Body, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import Response
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_picture_source_store,
get_output_target_store,
get_pp_template_store,
get_template_store,
)
from wled_controller.api.schemas.common import (
from ledgrab.api.schemas.common import (
CaptureImage,
PerformanceMetrics,
TemplateTestResponse,
)
from wled_controller.api.schemas.picture_sources import (
from ledgrab.api.schemas.picture_sources import (
ImageValidateRequest,
ImageValidateResponse,
PictureSourceCreate,
@@ -35,20 +35,20 @@ from wled_controller.api.schemas.picture_sources import (
StaticImagePictureSourceResponse,
VideoPictureSourceResponse,
)
from wled_controller.core.capture_engines import EngineRegistry
from wled_controller.core.filters import FilterRegistry, ImagePool
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.picture_source import (
from ledgrab.core.capture_engines import EngineRegistry
from ledgrab.core.filters import FilterRegistry, ImagePool
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.storage.template_store import TemplateStore
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
from ledgrab.storage.picture_source_store import PictureSourceStore
from ledgrab.storage.picture_source import (
ProcessedPictureSource,
ScreenCapturePictureSource,
StaticImagePictureSource,
VideoCaptureSource,
)
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -142,7 +142,7 @@ async def validate_image(
"""Validate an image source (URL or file path) and return a preview thumbnail."""
try:
from wled_controller.utils.safe_source import validate_image_path, validate_image_url
from ledgrab.utils.safe_source import validate_image_path, validate_image_url
source = data.image_source.strip()
if not source:
@@ -161,7 +161,7 @@ async def validate_image(
img_bytes = path
def _process_image(src):
from wled_controller.utils.image_codec import (
from ledgrab.utils.image_codec import (
encode_jpeg_data_uri,
load_image_bytes,
load_image_file,
@@ -198,7 +198,7 @@ async def get_full_image(
):
"""Serve the full-resolution image for lightbox preview."""
from wled_controller.utils.safe_source import validate_image_path, validate_image_url
from ledgrab.utils.safe_source import validate_image_path, validate_image_url
try:
if source.startswith(("http://", "https://")):
@@ -214,7 +214,7 @@ async def get_full_image(
img_bytes = path
def _encode_full(src):
from wled_controller.utils.image_codec import (
from ledgrab.utils.image_codec import (
encode_jpeg,
load_image_bytes,
load_image_file,
@@ -375,9 +375,9 @@ async def get_video_thumbnail(
store: PictureSourceStore = Depends(get_picture_source_store),
):
"""Get a thumbnail for a video picture source (first frame)."""
from wled_controller.core.processing.video_stream import extract_thumbnail
from wled_controller.storage.picture_source import VideoCaptureSource
from wled_controller.utils.image_codec import encode_jpeg_data_uri, resize_down
from ledgrab.core.processing.video_stream import extract_thumbnail
from ledgrab.storage.picture_source import VideoCaptureSource
from ledgrab.utils.image_codec import encode_jpeg_data_uri, resize_down
try:
source = store.get_stream(stream_id)
@@ -385,7 +385,7 @@ async def get_video_thumbnail(
raise HTTPException(status_code=400, detail="Not a video source")
# Resolve video asset to file path
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
from ledgrab.api.dependencies import get_asset_store as _get_asset_store
asset_store = _get_asset_store()
video_path = (
@@ -449,8 +449,8 @@ async def test_picture_source(
if isinstance(raw_stream, StaticImagePictureSource):
# Static image stream: load image from asset
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
from wled_controller.utils.image_codec import load_image_file
from ledgrab.api.dependencies import get_asset_store as _get_asset_store
from ledgrab.utils.image_codec import load_image_file
asset_store = _get_asset_store()
image_path = (
@@ -531,7 +531,7 @@ async def test_picture_source(
image = last_frame.image
# Create thumbnail + encode (CPU-bound — run in thread)
from wled_controller.utils.image_codec import (
from ledgrab.utils.image_codec import (
encode_jpeg_data_uri,
thumbnail as make_thumbnail,
)
@@ -628,11 +628,11 @@ async def test_picture_source_ws(
preview_width: int = Query(0),
):
"""WebSocket for picture source test with intermediate frame previews."""
from wled_controller.api.routes._preview_helpers import (
from ledgrab.api.routes._preview_helpers import (
authenticate_ws_token,
stream_capture_test,
)
from wled_controller.api.dependencies import (
from ledgrab.api.dependencies import (
get_picture_source_store as _get_ps_store,
get_template_store as _get_t_store,
get_pp_template_store as _get_pp_store,
@@ -662,8 +662,8 @@ async def test_picture_source_ws(
# Video sources: use VideoCaptureLiveStream for test preview
if isinstance(raw_stream, VideoCaptureSource):
from wled_controller.core.processing.video_stream import VideoCaptureLiveStream
from wled_controller.api.dependencies import get_asset_store as _get_asset_store2
from ledgrab.core.processing.video_stream import VideoCaptureLiveStream
from ledgrab.api.dependencies import get_asset_store as _get_asset_store2
asset_store = _get_asset_store2()
video_path = (
@@ -690,7 +690,7 @@ async def test_picture_source_ws(
def _encode_video_frame(image, pw):
"""Encode numpy RGB image as JPEG base64 data URI."""
from wled_controller.utils.image_codec import encode_jpeg_data_uri, resize_down
from ledgrab.utils.image_codec import encode_jpeg_data_uri, resize_down
if pw:
image = resize_down(image, pw)
@@ -5,34 +5,34 @@ import time
import numpy as np
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_picture_source_store,
get_pp_template_store,
get_template_store,
)
from wled_controller.api.schemas.common import (
from ledgrab.api.schemas.common import (
CaptureImage,
PerformanceMetrics,
TemplateTestResponse,
)
from wled_controller.api.schemas.filters import FilterInstanceSchema
from wled_controller.api.schemas.postprocessing import (
from ledgrab.api.schemas.filters import FilterInstanceSchema
from ledgrab.api.schemas.postprocessing import (
PostprocessingTemplateCreate,
PostprocessingTemplateListResponse,
PostprocessingTemplateResponse,
PostprocessingTemplateUpdate,
PPTemplateTestRequest,
)
from wled_controller.core.capture_engines import EngineRegistry
from wled_controller.core.filters import FilterRegistry, FilterInstance, ImagePool
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.core.capture_engines import EngineRegistry
from ledgrab.core.filters import FilterRegistry, FilterInstance, ImagePool
from ledgrab.storage.template_store import TemplateStore
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
from ledgrab.storage.picture_source_store import PictureSourceStore
from ledgrab.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -52,7 +52,11 @@ def _pp_template_to_response(t) -> PostprocessingTemplateResponse:
)
@router.get("/api/v1/postprocessing-templates", response_model=PostprocessingTemplateListResponse, tags=["Postprocessing Templates"])
@router.get(
"/api/v1/postprocessing-templates",
response_model=PostprocessingTemplateListResponse,
tags=["Postprocessing Templates"],
)
async def list_pp_templates(
_auth: AuthRequired,
store: PostprocessingTemplateStore = Depends(get_pp_template_store),
@@ -63,7 +67,12 @@ async def list_pp_templates(
return PostprocessingTemplateListResponse(templates=responses, count=len(responses))
@router.post("/api/v1/postprocessing-templates", response_model=PostprocessingTemplateResponse, tags=["Postprocessing Templates"], status_code=201)
@router.post(
"/api/v1/postprocessing-templates",
response_model=PostprocessingTemplateResponse,
tags=["Postprocessing Templates"],
status_code=201,
)
async def create_pp_template(
data: PostprocessingTemplateCreate,
_auth: AuthRequired,
@@ -90,7 +99,11 @@ async def create_pp_template(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/api/v1/postprocessing-templates/{template_id}", response_model=PostprocessingTemplateResponse, tags=["Postprocessing Templates"])
@router.get(
"/api/v1/postprocessing-templates/{template_id}",
response_model=PostprocessingTemplateResponse,
tags=["Postprocessing Templates"],
)
async def get_pp_template(
template_id: str,
_auth: AuthRequired,
@@ -101,10 +114,16 @@ async def get_pp_template(
template = store.get_template(template_id)
return _pp_template_to_response(template)
except ValueError:
raise HTTPException(status_code=404, detail=f"Postprocessing template {template_id} not found")
raise HTTPException(
status_code=404, detail=f"Postprocessing template {template_id} not found"
)
@router.put("/api/v1/postprocessing-templates/{template_id}", response_model=PostprocessingTemplateResponse, tags=["Postprocessing Templates"])
@router.put(
"/api/v1/postprocessing-templates/{template_id}",
response_model=PostprocessingTemplateResponse,
tags=["Postprocessing Templates"],
)
async def update_pp_template(
template_id: str,
data: PostprocessingTemplateUpdate,
@@ -113,7 +132,11 @@ async def update_pp_template(
):
"""Update a postprocessing template."""
try:
filters = [FilterInstance(f.filter_id, f.options) for f in data.filters] if data.filters is not None else None
filters = (
[FilterInstance(f.filter_id, f.options) for f in data.filters]
if data.filters is not None
else None
)
template = store.update_template(
template_id=template_id,
name=data.name,
@@ -133,7 +156,11 @@ async def update_pp_template(
raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/api/v1/postprocessing-templates/{template_id}", status_code=204, tags=["Postprocessing Templates"])
@router.delete(
"/api/v1/postprocessing-templates/{template_id}",
status_code=204,
tags=["Postprocessing Templates"],
)
async def delete_pp_template(
template_id: str,
_auth: AuthRequired,
@@ -149,7 +176,7 @@ async def delete_pp_template(
raise HTTPException(
status_code=409,
detail=f"Cannot delete postprocessing template: it is referenced by picture source(s): {names}. "
"Please reassign those streams before deleting.",
"Please reassign those streams before deleting.",
)
store.delete_template(template_id)
fire_entity_event("pp_template", "deleted", template_id)
@@ -165,7 +192,11 @@ async def delete_pp_template(
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/api/v1/postprocessing-templates/{template_id}/test", response_model=TemplateTestResponse, tags=["Postprocessing Templates"])
@router.post(
"/api/v1/postprocessing-templates/{template_id}/test",
response_model=TemplateTestResponse,
tags=["Postprocessing Templates"],
)
async def test_pp_template(
template_id: str,
test_request: PPTemplateTestRequest,
@@ -194,7 +225,7 @@ async def test_pp_template(
raw_stream = chain["raw_stream"]
from wled_controller.utils.image_codec import (
from ledgrab.utils.image_codec import (
encode_jpeg_data_uri,
load_image_file,
thumbnail as make_thumbnail,
@@ -202,10 +233,14 @@ async def test_pp_template(
if isinstance(raw_stream, StaticImagePictureSource):
# Static image: load from asset
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
from ledgrab.api.dependencies import get_asset_store as _get_asset_store
asset_store = _get_asset_store()
image_path = asset_store.get_file_path(raw_stream.image_asset_id) if raw_stream.image_asset_id else None
image_path = (
asset_store.get_file_path(raw_stream.image_asset_id)
if raw_stream.image_asset_id
else None
)
if not image_path:
raise HTTPException(status_code=400, detail="Image asset not found or missing file")
@@ -238,7 +273,9 @@ async def test_pp_template(
)
stream.initialize()
logger.info(f"Starting {test_request.capture_duration}s PP template test for {template_id} using stream {test_request.source_stream_id}")
logger.info(
f"Starting {test_request.capture_duration}s PP template test for {template_id} using stream {test_request.source_stream_id}"
)
frame_count = 0
total_capture_time = 0.0
@@ -346,11 +383,11 @@ async def test_pp_template_ws(
preview_width: int = Query(0),
):
"""WebSocket for PP template test with intermediate frame previews."""
from wled_controller.api.routes._preview_helpers import (
from ledgrab.api.routes._preview_helpers import (
authenticate_ws_token,
stream_capture_test,
)
from wled_controller.api.dependencies import (
from ledgrab.api.dependencies import (
get_picture_source_store as _get_ps_store,
get_template_store as _get_t_store,
get_pp_template_store as _get_pp_store,
@@ -400,7 +437,9 @@ async def test_pp_template_ws(
return
if capture_template.engine_type not in EngineRegistry.get_available_engines():
await websocket.close(code=4003, reason=f"Engine '{capture_template.engine_type}' not available")
await websocket.close(
code=4003, reason=f"Engine '{capture_template.engine_type}' not available"
)
return
# Resolve PP filters
@@ -422,7 +461,9 @@ async def test_pp_template_ws(
try:
await stream_capture_test(
websocket, engine_factory, duration,
websocket,
engine_factory,
duration,
pp_filters=pp_filters,
preview_width=preview_width or None,
)
@@ -5,30 +5,30 @@ from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_output_target_store,
get_processor_manager,
get_scene_preset_store,
)
from wled_controller.api.schemas.scene_presets import (
from ledgrab.api.schemas.scene_presets import (
ActivateResponse,
ScenePresetCreate,
ScenePresetListResponse,
ScenePresetResponse,
ScenePresetUpdate,
)
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.core.scenes.scene_activator import (
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.core.scenes.scene_activator import (
apply_scene_state,
capture_current_snapshot,
)
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.storage.scene_preset import ScenePreset
from wled_controller.storage.scene_preset_store import ScenePresetStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.storage.scene_preset import ScenePreset
from ledgrab.storage.scene_preset_store import ScenePresetStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
router = APIRouter()
@@ -39,13 +39,16 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
id=preset.id,
name=preset.name,
description=preset.description,
targets=[{
"target_id": t.target_id,
"running": t.running,
"color_strip_source_id": t.color_strip_source_id,
"brightness_value_source_id": t.brightness_value_source_id,
"fps": t.fps,
} for t in preset.targets],
targets=[
{
"target_id": t.target_id,
"running": t.running,
"color_strip_source_id": t.color_strip_source_id,
"brightness_value_source_id": t.brightness_value_source_id,
"fps": t.fps,
}
for t in preset.targets
],
order=preset.order,
tags=preset.tags,
created_at=preset.created_at,
@@ -55,6 +58,7 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
# ===== CRUD =====
@router.post(
"/api/v1/scene-presets",
response_model=ScenePresetResponse,
@@ -180,7 +184,9 @@ async def update_scene_preset(
tags=data.tags,
)
except ValueError as e:
raise HTTPException(status_code=404 if "not found" in str(e).lower() else 400, detail=str(e))
raise HTTPException(
status_code=404 if "not found" in str(e).lower() else 400, detail=str(e)
)
fire_entity_event("scene_preset", "updated", preset_id)
return _preset_to_response(preset)
@@ -206,6 +212,7 @@ async def delete_scene_preset(
# ===== Recapture =====
@router.post(
"/api/v1/scene-presets/{preset_id}/recapture",
response_model=ScenePresetResponse,
@@ -244,6 +251,7 @@ async def recapture_scene_preset(
# ===== Activate =====
@router.post(
"/api/v1/scene-presets/{preset_id}/activate",
response_model=ActivateResponse,
@@ -2,25 +2,25 @@
from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_color_strip_store,
get_sync_clock_manager,
get_sync_clock_store,
)
from wled_controller.api.schemas.sync_clocks import (
from ledgrab.api.schemas.sync_clocks import (
SyncClockCreate,
SyncClockListResponse,
SyncClockResponse,
SyncClockUpdate,
)
from wled_controller.storage.sync_clock import SyncClock
from wled_controller.storage.sync_clock_store import SyncClockStore
from wled_controller.storage.color_strip_store import ColorStripStore
from wled_controller.core.processing.sync_clock_manager import SyncClockManager
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.storage.sync_clock import SyncClock
from ledgrab.storage.sync_clock_store import SyncClockStore
from ledgrab.storage.color_strip_store import ColorStripStore
from ledgrab.core.processing.sync_clock_manager import SyncClockManager
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -57,7 +57,9 @@ async def list_sync_clocks(
)
@router.post("/api/v1/sync-clocks", response_model=SyncClockResponse, status_code=201, tags=["Sync Clocks"])
@router.post(
"/api/v1/sync-clocks", response_model=SyncClockResponse, status_code=201, tags=["Sync Clocks"]
)
async def create_sync_clock(
data: SyncClockCreate,
_auth: AuthRequired,
@@ -81,7 +83,9 @@ async def create_sync_clock(
raise HTTPException(status_code=400, detail=str(e))
@router.get("/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"])
@router.get(
"/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"]
)
async def get_sync_clock(
clock_id: str,
_auth: AuthRequired,
@@ -96,7 +100,9 @@ async def get_sync_clock(
raise HTTPException(status_code=404, detail=str(e))
@router.put("/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"])
@router.put(
"/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"]
)
async def update_sync_clock(
clock_id: str,
data: SyncClockUpdate,
@@ -138,9 +144,7 @@ async def delete_sync_clock(
# Check references
for source in css_store.get_all_sources():
if getattr(source, "clock_id", None) == clock_id:
raise ValueError(
f"Cannot delete: referenced by color strip source '{source.name}'"
)
raise ValueError(f"Cannot delete: referenced by color strip source '{source.name}'")
manager.release_all_for(clock_id)
store.delete_clock(clock_id)
fire_entity_event("sync_clock", "deleted", clock_id)
@@ -153,7 +157,10 @@ async def delete_sync_clock(
# ── Runtime control ──────────────────────────────────────────────────
@router.post("/api/v1/sync-clocks/{clock_id}/pause", response_model=SyncClockResponse, tags=["Sync Clocks"])
@router.post(
"/api/v1/sync-clocks/{clock_id}/pause", response_model=SyncClockResponse, tags=["Sync Clocks"]
)
async def pause_sync_clock(
clock_id: str,
_auth: AuthRequired,
@@ -170,7 +177,9 @@ async def pause_sync_clock(
return _to_response(clock, manager)
@router.post("/api/v1/sync-clocks/{clock_id}/resume", response_model=SyncClockResponse, tags=["Sync Clocks"])
@router.post(
"/api/v1/sync-clocks/{clock_id}/resume", response_model=SyncClockResponse, tags=["Sync Clocks"]
)
async def resume_sync_clock(
clock_id: str,
_auth: AuthRequired,
@@ -187,7 +196,9 @@ async def resume_sync_clock(
return _to_response(clock, manager)
@router.post("/api/v1/sync-clocks/{clock_id}/reset", response_model=SyncClockResponse, tags=["Sync Clocks"])
@router.post(
"/api/v1/sync-clocks/{clock_id}/reset", response_model=SyncClockResponse, tags=["Sync Clocks"]
)
async def reset_sync_clock(
clock_id: str,
_auth: AuthRequired,
@@ -15,9 +15,9 @@ import os
import psutil
from fastapi import APIRouter, Depends, HTTPException, Query
from wled_controller import __version__, REPO_URL, DONATE_URL
from wled_controller.api.auth import AuthRequired, is_auth_enabled
from wled_controller.api.dependencies import (
from ledgrab import __version__, REPO_URL, DONATE_URL
from ledgrab.api.auth import AuthRequired, is_auth_enabled
from ledgrab.api.dependencies import (
get_audio_source_store,
get_audio_template_store,
get_automation_store,
@@ -34,7 +34,7 @@ from wled_controller.api.dependencies import (
get_template_store,
get_value_source_store,
)
from wled_controller.api.schemas.system import (
from ledgrab.api.schemas.system import (
DisplayInfo,
DisplayListResponse,
GpuInfo,
@@ -43,13 +43,13 @@ from wled_controller.api.schemas.system import (
ProcessListResponse,
VersionResponse,
)
from wled_controller.config import get_config, is_demo_mode
from wled_controller.core.capture.screen_capture import get_available_displays
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.config import get_config, is_demo_mode
from ledgrab.core.capture.screen_capture import get_available_displays
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
# Re-export load_external_url so existing callers still work
from wled_controller.api.routes.system_settings import load_external_url # noqa: F401
from ledgrab.api.routes.system_settings import load_external_url # noqa: F401
logger = get_logger(__name__)
@@ -59,7 +59,7 @@ _process = psutil.Process(os.getpid())
_process.cpu_percent(interval=None) # prime process-level counter
# GPU monitoring (initialized once in utils.gpu, shared with metrics_history)
from wled_controller.utils.gpu import ( # noqa: E402
from ledgrab.utils.gpu import ( # noqa: E402
nvml_available as _nvml_available,
nvml as _nvml,
nvml_handle as _nvml_handle,
@@ -139,7 +139,7 @@ async def get_version():
async def list_all_tags(_: AuthRequired):
"""Get all tags used across all entities."""
all_tags: set[str] = set()
from wled_controller.api.dependencies import get_asset_store
from ledgrab.api.dependencies import get_asset_store
store_getters = [
get_device_store,
@@ -185,7 +185,7 @@ async def get_displays(
logger.info(f"Listing available displays (engine_type={engine_type})")
try:
from wled_controller.core.capture_engines import EngineRegistry
from ledgrab.core.capture_engines import EngineRegistry
if engine_type:
engine_cls = EngineRegistry.get_engine(engine_type)
@@ -240,7 +240,7 @@ async def get_running_processes(_: AuthRequired):
Returns a sorted list of unique process names for use in automation conditions.
"""
from wled_controller.core.automations.platform_detector import PlatformDetector
from ledgrab.core.automations.platform_detector import PlatformDetector
try:
detector = PlatformDetector()
@@ -348,7 +348,7 @@ async def get_integrations_status(
Used by the dashboard to show connectivity indicators.
"""
from wled_controller.core.devices.mqtt_client import get_mqtt_service
from ledgrab.core.devices.mqtt_client import get_mqtt_service
# MQTT status
mqtt_service = get_mqtt_service()
@@ -10,9 +10,9 @@ import re
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from pydantic import BaseModel
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import get_database
from wled_controller.api.schemas.system import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import get_database
from ledgrab.api.schemas.system import (
ExternalUrlRequest,
ExternalUrlResponse,
LogLevelRequest,
@@ -20,9 +20,9 @@ from wled_controller.api.schemas.system import (
MQTTSettingsRequest,
MQTTSettingsResponse,
)
from wled_controller.config import get_config
from wled_controller.storage.database import Database
from wled_controller.utils import get_logger
from ledgrab.config import get_config
from ledgrab.storage.database import Database
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -76,7 +76,9 @@ async def get_mqtt_settings(_: AuthRequired, db: Database = Depends(get_database
response_model=MQTTSettingsResponse,
tags=["System"],
)
async def update_mqtt_settings(_: AuthRequired, body: MQTTSettingsRequest, db: Database = Depends(get_database)):
async def update_mqtt_settings(
_: AuthRequired, body: MQTTSettingsRequest, db: Database = Depends(get_database)
):
"""Update MQTT broker settings. If password is empty string, the existing password is preserved."""
current = _load_mqtt_settings(db)
@@ -110,10 +112,12 @@ async def update_mqtt_settings(_: AuthRequired, body: MQTTSettingsRequest, db: D
# External URL setting
# ---------------------------------------------------------------------------
def load_external_url(db: Database | None = None) -> str:
"""Load the external URL setting. Returns empty string if not set."""
if db is None:
from wled_controller.api.dependencies import get_database
from ledgrab.api.dependencies import get_database
db = get_database()
data = db.get_setting("external_url")
if data:
@@ -136,7 +140,9 @@ async def get_external_url(_: AuthRequired, db: Database = Depends(get_database)
response_model=ExternalUrlResponse,
tags=["System"],
)
async def update_external_url(_: AuthRequired, body: ExternalUrlRequest, db: Database = Depends(get_database)):
async def update_external_url(
_: AuthRequired, body: ExternalUrlRequest, db: Database = Depends(get_database)
):
"""Set the external base URL used in webhook URLs and other user-visible URLs."""
url = body.external_url.strip().rstrip("/")
db.set_setting("external_url", {"external_url": url})
@@ -159,8 +165,8 @@ async def logs_ws(
Auth via ``?token=<api_key>``. On connect, sends the last ~500 buffered
lines as individual text messages, then pushes new lines as they appear.
"""
from wled_controller.api.auth import verify_ws_token
from wled_controller.utils import log_broadcaster
from ledgrab.api.auth import verify_ws_token
from ledgrab.utils import log_broadcaster
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -205,9 +211,7 @@ async def logs_ws(
# ---------------------------------------------------------------------------
# Regex: IPv4 address with optional port, e.g. "192.168.1.5" or "192.168.1.5:5555"
_ADB_ADDRESS_RE = re.compile(
r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?$"
)
_ADB_ADDRESS_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?$")
class AdbConnectRequest(BaseModel):
@@ -244,7 +248,8 @@ def _validate_adb_address(address: str) -> None:
def _get_adb_path() -> str:
"""Get the adb binary path from the scrcpy engine's resolver."""
from wled_controller.core.capture_engines.scrcpy_engine import _get_adb
from ledgrab.core.capture_engines.scrcpy_engine import _get_adb
return _get_adb()
@@ -265,7 +270,9 @@ async def adb_connect(_: AuthRequired, request: AdbConnectRequest):
logger.info(f"Connecting ADB device: {address}")
try:
proc = await asyncio.create_subprocess_exec(
adb, "connect", address,
adb,
"connect",
address,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
@@ -295,7 +302,9 @@ async def adb_disconnect(_: AuthRequired, request: AdbConnectRequest):
logger.info(f"Disconnecting ADB device: {address}")
try:
proc = await asyncio.create_subprocess_exec(
adb, "disconnect", address,
adb,
"disconnect",
address,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
@@ -5,20 +5,20 @@ import time
import numpy as np
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_cspt_store,
get_picture_source_store,
get_pp_template_store,
get_template_store,
)
from wled_controller.api.schemas.common import (
from ledgrab.api.schemas.common import (
CaptureImage,
PerformanceMetrics,
TemplateTestResponse,
)
from wled_controller.api.schemas.templates import (
from ledgrab.api.schemas.templates import (
EngineInfo,
EngineListResponse,
TemplateCreate,
@@ -27,18 +27,18 @@ from wled_controller.api.schemas.templates import (
TemplateTestRequest,
TemplateUpdate,
)
from wled_controller.api.schemas.filters import (
from ledgrab.api.schemas.filters import (
FilterOptionDefSchema,
FilterTypeListResponse,
FilterTypeResponse,
)
from wled_controller.core.capture_engines import EngineRegistry
from wled_controller.core.filters import FilterRegistry
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore
from wled_controller.storage.picture_source import ScreenCapturePictureSource
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.core.capture_engines import EngineRegistry
from ledgrab.core.filters import FilterRegistry
from ledgrab.storage.template_store import TemplateStore
from ledgrab.storage.picture_source_store import PictureSourceStore
from ledgrab.storage.picture_source import ScreenCapturePictureSource
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -47,6 +47,7 @@ router = APIRouter()
# ===== CAPTURE TEMPLATE ENDPOINTS =====
@router.get("/api/v1/capture-templates", response_model=TemplateListResponse, tags=["Templates"])
async def list_templates(
_auth: AuthRequired,
@@ -80,7 +81,12 @@ async def list_templates(
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/api/v1/capture-templates", response_model=TemplateResponse, tags=["Templates"], status_code=201)
@router.post(
"/api/v1/capture-templates",
response_model=TemplateResponse,
tags=["Templates"],
status_code=201,
)
async def create_template(
template_data: TemplateCreate,
_auth: AuthRequired,
@@ -111,7 +117,6 @@ async def create_template(
except EntityNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
@@ -119,7 +124,9 @@ async def create_template(
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/api/v1/capture-templates/{template_id}", response_model=TemplateResponse, tags=["Templates"])
@router.get(
"/api/v1/capture-templates/{template_id}", response_model=TemplateResponse, tags=["Templates"]
)
async def get_template(
template_id: str,
_auth: AuthRequired,
@@ -143,7 +150,9 @@ async def get_template(
)
@router.put("/api/v1/capture-templates/{template_id}", response_model=TemplateResponse, tags=["Templates"])
@router.put(
"/api/v1/capture-templates/{template_id}", response_model=TemplateResponse, tags=["Templates"]
)
async def update_template(
template_id: str,
update_data: TemplateUpdate,
@@ -176,7 +185,6 @@ async def update_template(
except EntityNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
@@ -199,7 +207,10 @@ async def delete_template(
# Check if any streams are using this template
streams_using_template = []
for stream in stream_store.get_all_streams():
if isinstance(stream, ScreenCapturePictureSource) and stream.capture_template_id == template_id:
if (
isinstance(stream, ScreenCapturePictureSource)
and stream.capture_template_id == template_id
):
streams_using_template.append(stream.name)
if streams_using_template:
@@ -207,7 +218,7 @@ async def delete_template(
raise HTTPException(
status_code=409,
detail=f"Cannot delete template: it is used by the following stream(s): {stream_list}. "
f"Please reassign these streams to a different template before deleting."
f"Please reassign these streams to a different template before deleting.",
)
# Proceed with deletion
@@ -245,7 +256,7 @@ async def list_engines(_auth: AuthRequired):
name=engine_type.upper(),
default_config=engine_class.get_default_config(),
available=(engine_type in available_set),
has_own_displays=getattr(engine_class, 'HAS_OWN_DISPLAYS', False),
has_own_displays=getattr(engine_class, "HAS_OWN_DISPLAYS", False),
)
)
@@ -256,7 +267,9 @@ async def list_engines(_auth: AuthRequired):
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/api/v1/capture-templates/test", response_model=TemplateTestResponse, tags=["Templates"])
@router.post(
"/api/v1/capture-templates/test", response_model=TemplateTestResponse, tags=["Templates"]
)
def test_template(
test_request: TemplateTestRequest,
_auth: AuthRequired,
@@ -276,7 +289,7 @@ def test_template(
if test_request.engine_type not in EngineRegistry.get_available_engines():
raise HTTPException(
status_code=400,
detail=f"Engine '{test_request.engine_type}' is not available on this system"
detail=f"Engine '{test_request.engine_type}' is not available on this system",
)
# Create and initialize capture stream
@@ -286,7 +299,9 @@ def test_template(
stream.initialize()
# Run sustained capture test
logger.info(f"Starting {test_request.capture_duration}s capture test with {test_request.engine_type}")
logger.info(
f"Starting {test_request.capture_duration}s capture test with {test_request.engine_type}"
)
frame_count = 0
total_capture_time = 0.0
@@ -321,7 +336,7 @@ def test_template(
raise ValueError("Unexpected image format from engine")
image = last_frame.image
from wled_controller.utils.image_codec import (
from ledgrab.utils.image_codec import (
encode_jpeg_data_uri,
thumbnail as make_thumbnail,
)
@@ -361,7 +376,6 @@ def test_template(
except EntityNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e:
@@ -391,7 +405,7 @@ async def test_template_ws(
Config is sent as the first client message (JSON with engine_type,
engine_config, display_index, capture_duration).
"""
from wled_controller.api.routes._preview_helpers import (
from ledgrab.api.routes._preview_helpers import (
authenticate_ws_token,
stream_capture_test,
)
@@ -417,7 +431,9 @@ async def test_template_ws(
pw = int(config.get("preview_width", 0)) or None
if engine_type not in EngineRegistry.get_available_engines():
await websocket.send_json({"type": "error", "detail": f"Engine '{engine_type}' not available"})
await websocket.send_json(
{"type": "error", "detail": f"Engine '{engine_type}' not available"}
)
await websocket.close(code=4003)
return
@@ -428,7 +444,9 @@ async def test_template_ws(
s.initialize()
return s
logger.info(f"Capture template test WS connected ({engine_type}, display {display_index}, {duration}s)")
logger.info(
f"Capture template test WS connected ({engine_type}, display {display_index}, {duration}s)"
)
try:
await stream_capture_test(websocket, engine_factory, duration, preview_width=pw)
@@ -443,6 +461,7 @@ async def test_template_ws(
# ===== FILTER TYPE ENDPOINTS =====
@router.get("/api/v1/filters", response_model=FilterTypeListResponse, tags=["Filters"])
async def list_filter_types(
_auth: AuthRequired,
@@ -467,23 +486,31 @@ async def list_filter_types(
for opt in schema:
choices = opt.choices
# Enrich filter_template choices with current template list
if filter_id == "filter_template" and opt.key == "template_id" and template_choices is not None:
if (
filter_id == "filter_template"
and opt.key == "template_id"
and template_choices is not None
):
choices = template_choices
opt_schemas.append(FilterOptionDefSchema(
key=opt.key,
label=opt.label,
type=opt.option_type,
default=opt.default,
min_value=opt.min_value,
max_value=opt.max_value,
step=opt.step,
choices=choices,
))
responses.append(FilterTypeResponse(
filter_id=filter_cls.filter_id,
filter_name=filter_cls.filter_name,
options_schema=opt_schemas,
))
opt_schemas.append(
FilterOptionDefSchema(
key=opt.key,
label=opt.label,
type=opt.option_type,
default=opt.default,
min_value=opt.min_value,
max_value=opt.max_value,
step=opt.step,
choices=choices,
)
)
responses.append(
FilterTypeResponse(
filter_id=filter_cls.filter_id,
filter_name=filter_cls.filter_name,
options_schema=opt_schemas,
)
)
return FilterTypeListResponse(filters=responses, count=len(responses))
@@ -512,21 +539,29 @@ async def list_strip_filter_types(
opt_schemas = []
for opt in schema:
choices = opt.choices
if filter_id == "css_filter_template" and opt.key == "template_id" and cspt_choices is not None:
if (
filter_id == "css_filter_template"
and opt.key == "template_id"
and cspt_choices is not None
):
choices = cspt_choices
opt_schemas.append(FilterOptionDefSchema(
key=opt.key,
label=opt.label,
type=opt.option_type,
default=opt.default,
min_value=opt.min_value,
max_value=opt.max_value,
step=opt.step,
choices=choices,
))
responses.append(FilterTypeResponse(
filter_id=filter_cls.filter_id,
filter_name=filter_cls.filter_name,
options_schema=opt_schemas,
))
opt_schemas.append(
FilterOptionDefSchema(
key=opt.key,
label=opt.label,
type=opt.option_type,
default=opt.default,
min_value=opt.min_value,
max_value=opt.max_value,
step=opt.step,
choices=choices,
)
)
responses.append(
FilterTypeResponse(
filter_id=filter_cls.filter_id,
filter_name=filter_cls.filter_name,
options_schema=opt_schemas,
)
)
return FilterTypeListResponse(filters=responses, count=len(responses))
@@ -3,16 +3,16 @@
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import get_update_service
from wled_controller.api.schemas.update import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import get_update_service
from ledgrab.api.schemas.update import (
DismissRequest,
UpdateSettingsRequest,
UpdateSettingsResponse,
UpdateStatusResponse,
)
from wled_controller.core.update.update_service import UpdateService
from wled_controller.utils import get_logger
from ledgrab.core.update.update_service import UpdateService
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -57,7 +57,9 @@ async def apply_update(
if not status["can_auto_update"]:
return JSONResponse(
status_code=400,
content={"detail": f"Auto-update not supported for install type: {status['install_type']}"},
content={
"detail": f"Auto-update not supported for install type: {status['install_type']}"
},
)
try:
await service.apply_update()
@@ -5,14 +5,14 @@ from typing import Annotated, Optional
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_output_target_store,
get_processor_manager,
get_value_source_store,
)
from wled_controller.api.schemas.value_sources import (
from ledgrab.api.schemas.value_sources import (
AdaptiveSceneValueSourceResponse,
AdaptiveTimeColorValueSourceResponse,
AdaptiveTimeValueSourceResponse,
@@ -31,7 +31,7 @@ from wled_controller.api.schemas.value_sources import (
ValueSourceResponse,
ValueSourceUpdate,
)
from wled_controller.storage.value_source import (
from ledgrab.storage.value_source import (
AdaptiveTimeColorValueSource,
AdaptiveValueSource,
AnimatedColorValueSource,
@@ -46,12 +46,12 @@ from wled_controller.storage.value_source import (
SystemMetricsValueSource,
ValueSource,
)
from wled_controller.storage.value_source_store import ValueSourceStore
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.core.processing.value_stream import ValueStream
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
from ledgrab.storage.value_source_store import ValueSourceStore
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.core.processing.value_stream import ValueStream
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
@@ -340,7 +340,7 @@ async def delete_value_source(
"""Delete a value source."""
try:
# Check if any targets reference this value source
from wled_controller.storage.wled_output_target import WledOutputTarget
from ledgrab.storage.wled_output_target import WledOutputTarget
for target in target_store.get_all_targets():
if isinstance(target, WledOutputTarget):
@@ -370,7 +370,7 @@ async def test_value_source_ws(
Acquires a ValueStream for the given source, polls get_value() at ~20 Hz,
and streams {value: float} JSON to the client.
"""
from wled_controller.api.auth import verify_ws_token
from ledgrab.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
@@ -2,25 +2,25 @@
from fastapi import APIRouter, Depends, HTTPException
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
fire_entity_event,
get_weather_manager,
get_weather_source_store,
)
from wled_controller.api.schemas.weather_sources import (
from ledgrab.api.schemas.weather_sources import (
WeatherSourceCreate,
WeatherSourceListResponse,
WeatherSourceResponse,
WeatherSourceUpdate,
WeatherTestResponse,
)
from wled_controller.core.weather.weather_manager import WeatherManager
from wled_controller.core.weather.weather_provider import WMO_CONDITION_NAMES
from wled_controller.storage.base_store import EntityNotFoundError
from wled_controller.storage.weather_source import WeatherSource
from wled_controller.storage.weather_source_store import WeatherSourceStore
from wled_controller.utils import get_logger
from ledgrab.core.weather.weather_manager import WeatherManager
from ledgrab.core.weather.weather_provider import WMO_CONDITION_NAMES
from ledgrab.storage.base_store import EntityNotFoundError
from ledgrab.storage.weather_source import WeatherSource
from ledgrab.storage.weather_source_store import WeatherSourceStore
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -44,7 +44,9 @@ def _to_response(source: WeatherSource) -> WeatherSourceResponse:
)
@router.get("/api/v1/weather-sources", response_model=WeatherSourceListResponse, tags=["Weather Sources"])
@router.get(
"/api/v1/weather-sources", response_model=WeatherSourceListResponse, tags=["Weather Sources"]
)
async def list_weather_sources(
_auth: AuthRequired,
store: WeatherSourceStore = Depends(get_weather_source_store),
@@ -56,7 +58,12 @@ async def list_weather_sources(
)
@router.post("/api/v1/weather-sources", response_model=WeatherSourceResponse, status_code=201, tags=["Weather Sources"])
@router.post(
"/api/v1/weather-sources",
response_model=WeatherSourceResponse,
status_code=201,
tags=["Weather Sources"],
)
async def create_weather_source(
data: WeatherSourceCreate,
_auth: AuthRequired,
@@ -79,7 +86,11 @@ async def create_weather_source(
return _to_response(source)
@router.get("/api/v1/weather-sources/{source_id}", response_model=WeatherSourceResponse, tags=["Weather Sources"])
@router.get(
"/api/v1/weather-sources/{source_id}",
response_model=WeatherSourceResponse,
tags=["Weather Sources"],
)
async def get_weather_source(
source_id: str,
_auth: AuthRequired,
@@ -91,7 +102,11 @@ async def get_weather_source(
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
@router.put("/api/v1/weather-sources/{source_id}", response_model=WeatherSourceResponse, tags=["Weather Sources"])
@router.put(
"/api/v1/weather-sources/{source_id}",
response_model=WeatherSourceResponse,
tags=["Weather Sources"],
)
async def update_weather_source(
source_id: str,
data: WeatherSourceUpdate,
@@ -133,7 +148,11 @@ async def delete_weather_source(
fire_entity_event("weather_source", "deleted", source_id)
@router.post("/api/v1/weather-sources/{source_id}/test", response_model=WeatherTestResponse, tags=["Weather Sources"])
@router.post(
"/api/v1/weather-sources/{source_id}/test",
response_model=WeatherTestResponse,
tags=["Weather Sources"],
)
async def test_weather_source(
source_id: str,
_auth: AuthRequired,
@@ -13,11 +13,11 @@ from collections import defaultdict
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from wled_controller.api.dependencies import get_automation_engine, get_automation_store
from wled_controller.core.automations.automation_engine import AutomationEngine
from wled_controller.storage.automation import WebhookCondition
from wled_controller.storage.automation_store import AutomationStore
from wled_controller.utils import get_logger
from ledgrab.api.dependencies import get_automation_engine, get_automation_store
from ledgrab.core.automations.automation_engine import AutomationEngine
from ledgrab.storage.automation import WebhookCondition
from ledgrab.storage.automation_store import AutomationStore
from ledgrab.utils import get_logger
logger = get_logger(__name__)
router = APIRouter()
@@ -75,12 +75,17 @@ async def handle_webhook(
# Find the automation that owns this token
for automation in store.get_all_automations():
for condition in automation.conditions:
if isinstance(condition, WebhookCondition) and secrets.compare_digest(condition.token, token):
if isinstance(condition, WebhookCondition) and secrets.compare_digest(
condition.token, token
):
active = body.action == "activate"
await engine.set_webhook_state(token, active)
logger.info(
"Webhook %s: automation '%s' (%s) → %s",
token[:8], automation.name, automation.id, body.action,
token[:8],
automation.name,
automation.id,
body.action,
)
return {
"ok": True,
@@ -10,7 +10,9 @@ class AudioTemplateCreate(BaseModel):
"""Request to create an audio capture template."""
name: str = Field(description="Template name", min_length=1, max_length=100)
engine_type: str = Field(description="Audio engine type (e.g., 'wasapi', 'sounddevice')", min_length=1)
engine_type: str = Field(
description="Audio engine type (e.g., 'wasapi', 'sounddevice')", min_length=1
)
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
description: Optional[str] = Field(None, description="Template description", max_length=500)
tags: List[str] = Field(default_factory=list, description="User-defined tags")
@@ -12,7 +12,9 @@ class ColorStripProcessingTemplateCreate(BaseModel):
"""Request to create a color strip processing template."""
name: str = Field(description="Template name", min_length=1, max_length=100)
filters: List[FilterInstanceSchema] = Field(default_factory=list, description="Ordered list of filter instances")
filters: List[FilterInstanceSchema] = Field(
default_factory=list, description="Ordered list of filter instances"
)
description: Optional[str] = Field(None, description="Template description", max_length=500)
tags: List[str] = Field(default_factory=list, description="User-defined tags")
@@ -21,7 +23,9 @@ class ColorStripProcessingTemplateUpdate(BaseModel):
"""Request to update a color strip processing template."""
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
filters: Optional[List[FilterInstanceSchema]] = Field(None, description="Ordered list of filter instances")
filters: Optional[List[FilterInstanceSchema]] = Field(
None, description="Ordered list of filter instances"
)
description: Optional[str] = Field(None, description="Template description", max_length=500)
tags: Optional[List[str]] = None
@@ -5,7 +5,7 @@ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
from pydantic import BaseModel, Discriminator, Field, Tag, model_validator
from wled_controller.api.schemas.devices import Calibration
from ledgrab.api.schemas.devices import Calibration
# =====================================================================
@@ -48,5 +48,7 @@ class TemplateTestResponse(BaseModel):
"""Response from template test."""
full_capture: CaptureImage = Field(description="Full screen capture with thumbnail")
border_extraction: Optional[BorderExtraction] = Field(None, description="Extracted border images (deprecated)")
border_extraction: Optional[BorderExtraction] = Field(
None, description="Extracted border images (deprecated)"
)
performance: PerformanceMetrics = Field(description="Performance metrics")
@@ -22,8 +22,12 @@ class FilterOptionDefSchema(BaseModel):
min_value: Any = Field(description="Minimum value")
max_value: Any = Field(description="Maximum value")
step: Any = Field(description="Step increment")
choices: Optional[List[Dict[str, str]]] = Field(default=None, description="Available choices for select type")
max_length: Optional[int] = Field(default=None, description="Maximum string length for string type")
choices: Optional[List[Dict[str, str]]] = Field(
default=None, description="Available choices for select type"
)
max_length: Optional[int] = Field(
default=None, description="Maximum string length for string type"
)
class FilterTypeResponse(BaseModel):
@@ -12,7 +12,9 @@ class PatternTemplateCreate(BaseModel):
"""Request to create a pattern template."""
name: str = Field(description="Template name", min_length=1, max_length=100)
rectangles: List[KeyColorRectangleSchema] = Field(default_factory=list, description="List of named rectangles")
rectangles: List[KeyColorRectangleSchema] = Field(
default_factory=list, description="List of named rectangles"
)
description: Optional[str] = Field(None, description="Template description", max_length=500)
tags: List[str] = Field(default_factory=list, description="User-defined tags")
@@ -21,7 +23,9 @@ class PatternTemplateUpdate(BaseModel):
"""Request to update a pattern template."""
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
rectangles: Optional[List[KeyColorRectangleSchema]] = Field(None, description="List of named rectangles")
rectangles: Optional[List[KeyColorRectangleSchema]] = Field(
None, description="List of named rectangles"
)
description: Optional[str] = Field(None, description="Template description", max_length=500)
tags: Optional[List[str]] = None
@@ -12,7 +12,9 @@ class PostprocessingTemplateCreate(BaseModel):
"""Request to create a postprocessing template."""
name: str = Field(description="Template name", min_length=1, max_length=100)
filters: List[FilterInstanceSchema] = Field(default_factory=list, description="Ordered list of filter instances")
filters: List[FilterInstanceSchema] = Field(
default_factory=list, description="Ordered list of filter instances"
)
description: Optional[str] = Field(None, description="Template description", max_length=500)
tags: List[str] = Field(default_factory=list, description="User-defined tags")
@@ -21,7 +23,9 @@ class PostprocessingTemplateUpdate(BaseModel):
"""Request to update a postprocessing template."""
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
filters: Optional[List[FilterInstanceSchema]] = Field(None, description="Ordered list of filter instances")
filters: Optional[List[FilterInstanceSchema]] = Field(
None, description="Ordered list of filter instances"
)
description: Optional[str] = Field(None, description="Template description", max_length=500)
tags: Optional[List[str]] = None
@@ -41,7 +45,9 @@ class PostprocessingTemplateResponse(BaseModel):
class PostprocessingTemplateListResponse(BaseModel):
"""List of postprocessing templates response."""
templates: List[PostprocessingTemplateResponse] = Field(description="List of postprocessing templates")
templates: List[PostprocessingTemplateResponse] = Field(
description="List of postprocessing templates"
)
count: int = Field(description="Number of templates")
@@ -49,4 +55,6 @@ class PPTemplateTestRequest(BaseModel):
"""Request to test a postprocessing template against a source stream."""
source_stream_id: str = Field(description="ID of the source picture source to capture from")
capture_duration: float = Field(default=5.0, ge=1.0, le=30.0, description="Duration to capture in seconds")
capture_duration: float = Field(
default=5.0, ge=1.0, le=30.0, description="Duration to capture in seconds"
)
@@ -19,7 +19,9 @@ class ScenePresetCreate(BaseModel):
name: str = Field(description="Preset name", min_length=1, max_length=100)
description: str = Field(default="", max_length=500)
target_ids: Optional[List[str]] = Field(None, description="Target IDs to capture (all if omitted)")
target_ids: Optional[List[str]] = Field(
None, description="Target IDs to capture (all if omitted)"
)
tags: List[str] = Field(default_factory=list, description="User-defined tags")
@@ -29,7 +31,10 @@ class ScenePresetUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
order: Optional[int] = None
target_ids: Optional[List[str]] = Field(None, description="Update target list: keep state for existing, capture fresh for new, drop removed")
target_ids: Optional[List[str]] = Field(
None,
description="Update target list: keep state for existing, capture fresh for new, drop removed",
)
tags: Optional[List[str]] = None
@@ -53,7 +53,9 @@ class EngineInfo(BaseModel):
name: str = Field(description="Human-readable engine name")
default_config: Dict = Field(description="Default configuration for this engine")
available: bool = Field(description="Whether engine is available on this system")
has_own_displays: bool = Field(default=False, description="Engine has its own device list (not desktop monitors)")
has_own_displays: bool = Field(
default=False, description="Engine has its own device list (not desktop monitors)"
)
class EngineListResponse(BaseModel):
@@ -76,4 +78,6 @@ class TemplateTestRequest(BaseModel):
engine_config: Dict = Field(default={}, description="Engine configuration")
display_index: int = Field(description="Display index to capture")
border_width: int = Field(default=10, ge=1, le=100, description="Border width in pixels")
capture_duration: float = Field(default=5.0, ge=1.0, le=30.0, description="Duration to capture in seconds")
capture_duration: float = Field(
default=5.0, ge=1.0, le=30.0, description="Duration to capture in seconds"
)
@@ -10,11 +10,19 @@ class WeatherSourceCreate(BaseModel):
"""Request to create a weather source."""
name: str = Field(description="Source name", min_length=1, max_length=100)
provider: Literal["open_meteo"] = Field(default="open_meteo", description="Weather data provider")
provider: Literal["open_meteo"] = Field(
default="open_meteo", description="Weather data provider"
)
provider_config: Optional[Dict] = Field(None, description="Provider-specific configuration")
latitude: float = Field(default=50.0, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0)
longitude: float = Field(default=0.0, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0)
update_interval: int = Field(default=600, description="API poll interval in seconds (60-3600)", ge=60, le=3600)
latitude: float = Field(
default=50.0, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0
)
longitude: float = Field(
default=0.0, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0
)
update_interval: int = Field(
default=600, description="API poll interval in seconds (60-3600)", ge=60, le=3600
)
description: Optional[str] = Field(None, description="Optional description", max_length=500)
tags: List[str] = Field(default_factory=list, description="User-defined tags")
@@ -25,9 +33,15 @@ class WeatherSourceUpdate(BaseModel):
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
provider: Optional[Literal["open_meteo"]] = Field(None, description="Weather data provider")
provider_config: Optional[Dict] = Field(None, description="Provider-specific configuration")
latitude: Optional[float] = Field(None, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0)
longitude: Optional[float] = Field(None, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0)
update_interval: Optional[int] = Field(None, description="API poll interval in seconds (60-3600)", ge=60, le=3600)
latitude: Optional[float] = Field(
None, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0
)
longitude: Optional[float] = Field(
None, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0
)
update_interval: Optional[int] = Field(
None, description="API poll interval in seconds (60-3600)", ge=60, le=3600
)
description: Optional[str] = Field(None, description="Optional description", max_length=500)
tags: Optional[List[str]] = None
@@ -38,7 +52,9 @@ class WeatherSourceResponse(BaseModel):
id: str = Field(description="Source ID")
name: str = Field(description="Source name")
provider: str = Field(description="Weather data provider")
provider_config: Dict = Field(default_factory=dict, description="Provider-specific configuration")
provider_config: Dict = Field(
default_factory=dict, description="Provider-specific configuration"
)
latitude: float = Field(description="Geographic latitude")
longitude: float = Field(description="Geographic longitude")
update_interval: int = Field(description="API poll interval in seconds")
@@ -1,6 +1,7 @@
"""Configuration management for WLED Screen Controller."""
"""Configuration management for LedGrab."""
import os
import sys
from pathlib import Path
from typing import List, Literal
@@ -8,6 +9,43 @@ import yaml
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
# ── Legacy env var migration ─────────────────────────────────
# Warn users who still have WLED_ env vars from pre-rename installs.
_OLD_PREFIX = "WLED_"
_NEW_PREFIX = "LEDGRAB_"
_ENV_MIGRATION_MAP = {
"WLED_CONFIG_PATH": "LEDGRAB_CONFIG_PATH",
"WLED_DEMO": "LEDGRAB_DEMO",
"WLED_RESTART": "LEDGRAB_RESTART",
"WLED_TRAY": "LEDGRAB_TRAY",
}
def _migrate_legacy_env_vars() -> None:
"""Detect old WLED_ env vars and auto-forward them to LEDGRAB_ equivalents."""
migrated = []
for old_key, new_key in _ENV_MIGRATION_MAP.items():
old_val = os.environ.get(old_key)
if old_val is not None and os.environ.get(new_key) is None:
os.environ[new_key] = old_val
migrated.append(f" {old_key} -> {new_key}")
# Also forward any WLED_<nested> vars (e.g. WLED_SERVER__PORT)
for key in list(os.environ):
if key.startswith(_OLD_PREFIX) and key not in _ENV_MIGRATION_MAP:
new_key = _NEW_PREFIX + key[len(_OLD_PREFIX) :]
if os.environ.get(new_key) is None:
os.environ[new_key] = os.environ[key]
migrated.append(f" {key} -> {new_key}")
if migrated:
print(
"WARNING: Detected legacy WLED_ environment variables. "
"The app was renamed to LedGrab — please update your env vars:\n" + "\n".join(migrated),
file=sys.stderr,
)
_migrate_legacy_env_vars()
class ServerConfig(BaseSettings):
"""Server configuration."""
@@ -27,8 +65,8 @@ class AuthConfig(BaseSettings):
class AssetsConfig(BaseSettings):
"""Assets configuration."""
max_file_size_mb: int = 50 # Max upload size in MB
assets_dir: str = "data/assets" # Directory for uploaded asset files
max_file_size_mb: int = 50 # Max upload size in MB
assets_dir: str = "data/assets" # Directory for uploaded asset files
class StorageConfig(BaseSettings):
@@ -53,7 +91,7 @@ class LoggingConfig(BaseSettings):
"""Logging configuration."""
format: Literal["json", "text"] = "json"
file: str = "logs/wled_controller.log"
file: str = "logs/ledgrab.log"
max_size_mb: int = 100
backup_count: int = 5
@@ -62,7 +100,7 @@ class Config(BaseSettings):
"""Main application configuration."""
model_config = SettingsConfigDict(
env_prefix="WLED_",
env_prefix="LEDGRAB_",
env_nested_delimiter="__",
case_sensitive=False,
)
@@ -112,21 +150,21 @@ class Config(BaseSettings):
"""Load configuration from default locations.
Tries to load from:
1. Environment variable WLED_CONFIG_PATH
2. WLED_DEMO=true ./config/demo_config.yaml (if it exists)
1. Environment variable LEDGRAB_CONFIG_PATH
2. LEDGRAB_DEMO=true ./config/demo_config.yaml (if it exists)
3. ./config/default_config.yaml
4. Default values
Returns:
Config instance
"""
config_path = os.getenv("WLED_CONFIG_PATH")
config_path = os.getenv("LEDGRAB_CONFIG_PATH")
if config_path:
return cls.from_yaml(config_path)
# Demo mode: try dedicated demo config first
if os.getenv("WLED_DEMO", "").lower() in ("true", "1", "yes"):
if os.getenv("LEDGRAB_DEMO", "").lower() in ("true", "1", "yes"):
demo_path = Path("config/demo_config.yaml")
if demo_path.exists():
return cls.from_yaml(demo_path)
@@ -1,6 +1,6 @@
"""Core functionality for screen capture and WLED control."""
from wled_controller.core.capture.screen_capture import (
from ledgrab.core.capture.screen_capture import (
get_available_displays,
capture_display,
extract_border_pixels,
@@ -1,21 +1,21 @@
"""Audio capture engine abstraction layer."""
from wled_controller.core.audio.base import (
from ledgrab.core.audio.base import (
AudioCaptureEngine,
AudioCaptureStreamBase,
AudioDeviceInfo,
)
from wled_controller.core.audio.factory import AudioEngineRegistry
from wled_controller.core.audio.analysis import (
from ledgrab.core.audio.factory import AudioEngineRegistry
from ledgrab.core.audio.analysis import (
AudioAnalysis,
AudioAnalyzer,
NUM_BANDS,
DEFAULT_SAMPLE_RATE,
DEFAULT_CHUNK_SIZE,
)
from wled_controller.core.audio.wasapi_engine import WasapiEngine, WasapiCaptureStream
from wled_controller.core.audio.sounddevice_engine import SounddeviceEngine, SounddeviceCaptureStream
from wled_controller.core.audio.demo_engine import DemoAudioEngine, DemoAudioCaptureStream
from ledgrab.core.audio.wasapi_engine import WasapiEngine, WasapiCaptureStream
from ledgrab.core.audio.sounddevice_engine import SounddeviceEngine, SounddeviceCaptureStream
from ledgrab.core.audio.demo_engine import DemoAudioEngine, DemoAudioCaptureStream
# Auto-register available engines
AudioEngineRegistry.register(WasapiEngine)
@@ -40,7 +40,9 @@ class AudioAnalysis:
left_rms: float = 0.0
left_spectrum: np.ndarray = field(default_factory=lambda: np.zeros(NUM_BANDS, dtype=np.float32))
right_rms: float = 0.0
right_spectrum: np.ndarray = field(default_factory=lambda: np.zeros(NUM_BANDS, dtype=np.float32))
right_spectrum: np.ndarray = field(
default_factory=lambda: np.zeros(NUM_BANDS, dtype=np.float32)
)
def _build_log_bands(num_bands: int, fft_size: int, sample_rate: int) -> List[Tuple[int, int]]:
@@ -74,7 +76,9 @@ class AudioAnalyzer:
thread calls analyze() (the capture thread).
"""
def __init__(self, sample_rate: int = DEFAULT_SAMPLE_RATE, chunk_size: int = DEFAULT_CHUNK_SIZE):
def __init__(
self, sample_rate: int = DEFAULT_SAMPLE_RATE, chunk_size: int = DEFAULT_CHUNK_SIZE
):
self._sample_rate = sample_rate
self._chunk_size = chunk_size
@@ -102,12 +106,18 @@ class AudioAnalyzer:
# Double-buffered output spectra — avoids allocating new arrays each
# analyze() call. Consumers hold a reference to the "old" buffer while
# the analyzer writes into the alternate one.
self._out_spectrum = [np.zeros(NUM_BANDS, dtype=np.float32),
np.zeros(NUM_BANDS, dtype=np.float32)]
self._out_spectrum_left = [np.zeros(NUM_BANDS, dtype=np.float32),
np.zeros(NUM_BANDS, dtype=np.float32)]
self._out_spectrum_right = [np.zeros(NUM_BANDS, dtype=np.float32),
np.zeros(NUM_BANDS, dtype=np.float32)]
self._out_spectrum = [
np.zeros(NUM_BANDS, dtype=np.float32),
np.zeros(NUM_BANDS, dtype=np.float32),
]
self._out_spectrum_left = [
np.zeros(NUM_BANDS, dtype=np.float32),
np.zeros(NUM_BANDS, dtype=np.float32),
]
self._out_spectrum_right = [
np.zeros(NUM_BANDS, dtype=np.float32),
np.zeros(NUM_BANDS, dtype=np.float32),
]
self._out_idx = 0 # toggles 0/1 each analyze() call
# Pre-compute band start/end arrays and widths for vectorized binning
@@ -147,14 +157,14 @@ class AudioAnalyzer:
# Split channels and mix to mono
if channels > 1:
data = raw_data.reshape(-1, channels)
np.copyto(self._left_buf[:len(data)], data[:, 0])
np.copyto(self._left_buf[: len(data)], data[:, 0])
right_col = data[:, 1] if channels >= 2 else data[:, 0]
np.copyto(self._right_buf[:len(data)], right_col)
np.add(data[:, 0], right_col, out=self._mono_buf[:len(data)])
self._mono_buf[:len(data)] *= 0.5
samples = self._mono_buf[:len(data)]
left_samples = self._left_buf[:len(data)]
right_samples = self._right_buf[:len(data)]
np.copyto(self._right_buf[: len(data)], right_col)
np.add(data[:, 0], right_col, out=self._mono_buf[: len(data)])
self._mono_buf[: len(data)] *= 0.5
samples = self._mono_buf[: len(data)]
left_samples = self._left_buf[: len(data)]
right_samples = self._right_buf[: len(data)]
else:
samples = raw_data
left_samples = samples
@@ -176,13 +186,22 @@ class AudioAnalyzer:
right_rms = rms
# FFT for mono, left, right
self._fft_bands(samples, self._spectrum_buf, self._smooth_spectrum,
alpha, one_minus_alpha)
self._fft_bands(samples, self._spectrum_buf, self._smooth_spectrum, alpha, one_minus_alpha)
if channels > 1:
self._fft_bands(left_samples, self._spectrum_buf_left, self._smooth_spectrum_left,
alpha, one_minus_alpha)
self._fft_bands(right_samples, self._spectrum_buf_right, self._smooth_spectrum_right,
alpha, one_minus_alpha)
self._fft_bands(
left_samples,
self._spectrum_buf_left,
self._smooth_spectrum_left,
alpha,
one_minus_alpha,
)
self._fft_bands(
right_samples,
self._spectrum_buf_right,
self._smooth_spectrum_right,
alpha,
one_minus_alpha,
)
else:
np.copyto(self._smooth_spectrum_left, self._smooth_spectrum)
np.copyto(self._smooth_spectrum_right, self._smooth_spectrum)
@@ -233,7 +252,7 @@ class AudioAnalyzer:
chunk = np.pad(chunk, (0, chunk_size - len(chunk)))
np.multiply(chunk, self._window, out=self._fft_windowed)
fft_mag = np.abs(np.fft.rfft(self._fft_windowed))
fft_mag *= (1.0 / chunk_size)
fft_mag *= 1.0 / chunk_size
fft_len = len(fft_mag)
# Vectorized band binning using cumulative sum
valid = (self._band_starts < fft_len) & (self._band_ends <= fft_len) & (self._band_ends > 0)
@@ -246,6 +265,6 @@ class AudioAnalyzer:
buf[valid] = band_sums / self._band_widths[valid]
spec_max = float(np.max(buf))
if spec_max > 1e-6:
buf *= (1.0 / spec_max)
buf *= 1.0 / spec_max
smooth_buf *= one_minus_alpha
smooth_buf += alpha * buf
@@ -13,13 +13,13 @@ import threading
import time
from typing import Any, Dict, List, Optional, Tuple
from wled_controller.core.audio.analysis import (
from ledgrab.core.audio.analysis import (
AudioAnalysis,
AudioAnalyzer,
)
from wled_controller.core.audio.base import AudioCaptureStreamBase
from wled_controller.core.audio.factory import AudioEngineRegistry
from wled_controller.utils import get_logger
from ledgrab.core.audio.base import AudioCaptureStreamBase
from ledgrab.core.audio.factory import AudioEngineRegistry
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -36,6 +36,7 @@ __all__ = [
# ManagedAudioStream — wraps engine stream + analyzer in background thread
# ---------------------------------------------------------------------------
class ManagedAudioStream:
"""Wraps an AudioCaptureStreamBase + AudioAnalyzer in a background thread.
@@ -66,9 +67,10 @@ class ManagedAudioStream:
return
self._running = True
self._thread = threading.Thread(
target=self._capture_loop, daemon=True,
target=self._capture_loop,
daemon=True,
name=f"AudioCapture-{self._engine_type}-{self._device_index}-"
f"{'lb' if self._is_loopback else 'in'}",
f"{'lb' if self._is_loopback else 'in'}",
)
self._thread.start()
logger.info(
@@ -99,8 +101,10 @@ class ManagedAudioStream:
stream: Optional[AudioCaptureStreamBase] = None
try:
stream = AudioEngineRegistry.create_stream(
self._engine_type, self._device_index,
self._is_loopback, self._engine_config,
self._engine_type,
self._device_index,
self._is_loopback,
self._engine_config,
)
stream.initialize()
@@ -155,6 +159,7 @@ class ManagedAudioStream:
# AudioCaptureManager — ref-counted shared capture streams
# ---------------------------------------------------------------------------
class AudioCaptureManager:
"""Manages shared ManagedAudioStream instances with reference counting.
@@ -201,7 +206,10 @@ class AudioCaptureManager:
return stream
stream = ManagedAudioStream(
engine_type, device_index, is_loopback, engine_config,
engine_type,
device_index,
is_loopback,
engine_config,
)
stream.start()
self._streams[key] = (stream, 1)
@@ -279,15 +287,17 @@ class AudioCaptureManager:
if key in seen:
continue
seen.add(key)
result.append({
"index": dev.index,
"name": dev.name,
"is_input": dev.is_input,
"is_loopback": dev.is_loopback,
"channels": dev.channels,
"default_samplerate": dev.default_samplerate,
"engine_type": engine_type,
})
result.append(
{
"index": dev.index,
"name": dev.name,
"is_input": dev.is_input,
"is_loopback": dev.is_loopback,
"channels": dev.channels,
"default_samplerate": dev.default_samplerate,
"engine_type": engine_type,
}
)
except Exception as e:
logger.error(f"Error enumerating devices for engine '{engine_type}': {e}")
return result
@@ -306,15 +316,17 @@ class AudioCaptureManager:
continue
devices = []
for dev in engine_class.enumerate_devices():
devices.append({
"index": dev.index,
"name": dev.name,
"is_input": dev.is_input,
"is_loopback": dev.is_loopback,
"channels": dev.channels,
"default_samplerate": dev.default_samplerate,
"engine_type": engine_type,
})
devices.append(
{
"index": dev.index,
"name": dev.name,
"is_input": dev.is_input,
"is_loopback": dev.is_loopback,
"channels": dev.channels,
"default_samplerate": dev.default_samplerate,
"engine_type": engine_type,
}
)
result[engine_type] = devices
except Exception as e:
logger.error(f"Error enumerating devices for engine '{engine_type}': {e}")
@@ -9,7 +9,7 @@ from typing import Tuple
import numpy as np
from wled_controller.core.audio.analysis import NUM_BANDS
from ledgrab.core.audio.analysis import NUM_BANDS
def compute_band_mask(freq_low: float, freq_high: float) -> np.ndarray:
@@ -5,13 +5,13 @@ from typing import Any, Dict, List, Optional
import numpy as np
from wled_controller.config import is_demo_mode
from wled_controller.core.audio.base import (
from ledgrab.config import is_demo_mode
from ledgrab.core.audio.base import (
AudioCaptureEngine,
AudioCaptureStreamBase,
AudioDeviceInfo,
)
from wled_controller.utils import get_logger
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -126,14 +126,16 @@ class DemoAudioEngine(AudioCaptureEngine):
def enumerate_devices(cls) -> List[AudioDeviceInfo]:
devices = []
for idx, (name, is_loopback, channels, samplerate) in enumerate(_VIRTUAL_DEVICES):
devices.append(AudioDeviceInfo(
index=idx,
name=name,
is_input=True,
is_loopback=is_loopback,
channels=channels,
default_samplerate=samplerate,
))
devices.append(
AudioDeviceInfo(
index=idx,
name=name,
is_input=True,
is_loopback=is_loopback,
channels=channels,
default_samplerate=samplerate,
)
)
logger.debug(f"Demo audio engine: {len(devices)} virtual device(s)")
return devices
@@ -2,9 +2,9 @@
from typing import Any, Dict, List, Optional, Type
from wled_controller.core.audio.base import AudioCaptureEngine, AudioCaptureStreamBase
from wled_controller.config import is_demo_mode
from wled_controller.utils import get_logger
from ledgrab.core.audio.base import AudioCaptureEngine, AudioCaptureStreamBase
from ledgrab.config import is_demo_mode
from ledgrab.utils import get_logger
logger = get_logger(__name__)
@@ -78,9 +78,7 @@ class AudioEngineRegistry:
if engine_class.is_available():
available.append(engine_type)
except Exception as e:
logger.error(
f"Error checking availability for audio engine '{engine_type}': {e}"
)
logger.error(f"Error checking availability for audio engine '{engine_type}': {e}")
return available
@classmethod
@@ -101,9 +99,7 @@ class AudioEngineRegistry:
best_priority = engine_class.ENGINE_PRIORITY
best_type = engine_type
except Exception as e:
logger.error(
f"Error checking availability for audio engine '{engine_type}': {e}"
)
logger.error(f"Error checking availability for audio engine '{engine_type}': {e}")
return best_type
@classmethod
@@ -144,9 +140,7 @@ class AudioEngineRegistry:
engine_class = cls.get_engine(engine_type)
if not engine_class.is_available():
raise ValueError(
f"Audio engine '{engine_type}' is not available on this system"
)
raise ValueError(f"Audio engine '{engine_type}' is not available on this system")
try:
stream = engine_class.create_stream(device_index, is_loopback, config)
@@ -157,9 +151,7 @@ class AudioEngineRegistry:
return stream
except Exception as e:
logger.error(f"Failed to create stream for audio engine '{engine_type}': {e}")
raise RuntimeError(
f"Failed to create stream for audio engine '{engine_type}': {e}"
)
raise RuntimeError(f"Failed to create stream for audio engine '{engine_type}': {e}")
@classmethod
def clear_registry(cls):
@@ -0,0 +1,31 @@
"""Audio filter system.
Provides a pluggable filter architecture for audio analysis postprocessing.
Import this package to ensure all built-in filters are registered.
"""
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.pipeline import AudioFilterPipeline
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
# Import individual filters to trigger auto-registration
import ledgrab.core.audio.filters.audio_filter_template # noqa: F401
import ledgrab.core.audio.filters.channel_extract # noqa: F401
import ledgrab.core.audio.filters.band_extract # noqa: F401
import ledgrab.core.audio.filters.peak_hold # noqa: F401
import ledgrab.core.audio.filters.gain # noqa: F401
import ledgrab.core.audio.filters.noise_gate # noqa: F401
import ledgrab.core.audio.filters.envelope_follower # noqa: F401
import ledgrab.core.audio.filters.spectral_smoothing # noqa: F401
import ledgrab.core.audio.filters.compressor # noqa: F401
import ledgrab.core.audio.filters.inverter # noqa: F401
import ledgrab.core.audio.filters.beat_gate # noqa: F401
import ledgrab.core.audio.filters.delay # noqa: F401
import ledgrab.core.audio.filters.auto_gain # noqa: F401
__all__ = [
"AudioFilter",
"AudioFilterOptionDef",
"AudioFilterPipeline",
"AudioFilterRegistry",
]
@@ -7,9 +7,9 @@ referenced template's filters when building the filter chain.
from typing import List
from wled_controller.core.audio.analysis import AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.analysis import AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
@AudioFilterRegistry.register
@@ -10,9 +10,9 @@ from typing import Any, Dict, List
import numpy as np
from wled_controller.core.audio.analysis import AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.analysis import AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
@AudioFilterRegistry.register
@@ -3,10 +3,10 @@
from dataclasses import replace
from typing import Any, Dict, List
from wled_controller.core.audio.analysis import AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from wled_controller.core.audio.band_filter import apply_band_filter, compute_band_mask
from ledgrab.core.audio.analysis import AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.band_filter import apply_band_filter, compute_band_mask
# Preset frequency ranges
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from wled_controller.core.audio.analysis import AudioAnalysis
from ledgrab.core.audio.analysis import AudioAnalysis
@dataclass
@@ -6,9 +6,9 @@ from typing import Any, Dict, List
import numpy as np
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
_ZERO_SPECTRUM = np.zeros(NUM_BANDS, dtype=np.float32)
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
import numpy as np
from wled_controller.core.audio.analysis import AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.analysis import AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
@AudioFilterRegistry.register
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
import numpy as np
from wled_controller.core.audio.analysis import AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.analysis import AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
@AudioFilterRegistry.register
@@ -6,9 +6,9 @@ from typing import Any, Dict, List
import numpy as np
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
# Assumed update rate for sizing the ring buffer
_UPDATE_RATE_HZ = 30
@@ -6,9 +6,9 @@ from typing import Any, Dict, List
import numpy as np
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
def _time_constant_coeff(time_ms: float, dt: float) -> float:
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
import numpy as np
from wled_controller.core.audio.analysis import AudioAnalysis
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
from ledgrab.core.audio.analysis import AudioAnalysis
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
@AudioFilterRegistry.register

Some files were not shown because too many files have changed in this diff Show More