refactor: rename project to LedGrab, split HA integration into separate repo
Lint & Test / test (push) Successful in 1m56s
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:
@@ -1,4 +1,4 @@
|
|||||||
# Claude Instructions for WLED Screen Controller
|
# Claude Instructions for LedGrab
|
||||||
|
|
||||||
## Code Search
|
## Code Search
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -9,8 +9,8 @@
|
|||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git
|
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git
|
||||||
cd wled-screen-controller-mixed/server
|
cd ledgrab/server
|
||||||
|
|
||||||
# Python environment
|
# Python environment
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
@@ -29,7 +29,7 @@ npm run build
|
|||||||
cd server
|
cd server
|
||||||
export PYTHONPATH=$(pwd)/src # Linux/Mac
|
export PYTHONPATH=$(pwd)/src # Linux/Mac
|
||||||
# set PYTHONPATH=%CD%\src # Windows
|
# set PYTHONPATH=%CD%\src # Windows
|
||||||
python -m wled_controller.main
|
python -m ledgrab.main
|
||||||
```
|
```
|
||||||
|
|
||||||
Open http://localhost:8080 to access the dashboard.
|
Open http://localhost:8080 to access the dashboard.
|
||||||
@@ -55,7 +55,7 @@ ruff check src/ tests/
|
|||||||
|
|
||||||
## Frontend Changes
|
## Frontend Changes
|
||||||
|
|
||||||
After modifying any file under `server/src/wled_controller/static/js/` or `static/css/`, rebuild the bundle:
|
After modifying any file under `server/src/ledgrab/static/js/` or `static/css/`, rebuild the bundle:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd server
|
cd server
|
||||||
|
|||||||
+29
-79
@@ -1,15 +1,17 @@
|
|||||||
# Installation Guide
|
# Installation Guide
|
||||||
|
|
||||||
Complete installation guide for LED Grab (WLED Screen Controller) server and Home Assistant integration.
|
Complete installation guide for the LedGrab server.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Docker Installation (recommended)](#docker-installation)
|
1. [Docker Installation (recommended)](#docker-installation)
|
||||||
2. [Manual Installation](#manual-installation)
|
2. [Manual Installation](#manual-installation)
|
||||||
3. [First-Time Setup](#first-time-setup)
|
3. [First-Time Setup](#first-time-setup)
|
||||||
4. [Home Assistant Integration](#home-assistant-integration)
|
4. [Configuration Reference](#configuration-reference)
|
||||||
5. [Configuration Reference](#configuration-reference)
|
5. [Troubleshooting](#troubleshooting)
|
||||||
6. [Troubleshooting](#troubleshooting)
|
|
||||||
|
> **Home Assistant integration** has moved to a separate repository:
|
||||||
|
> [ledgrab-haos-integration](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab-haos-integration)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,8 +22,8 @@ The fastest way to get running. Requires [Docker](https://docs.docker.com/get-do
|
|||||||
1. **Clone and start:**
|
1. **Clone and start:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git
|
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git
|
||||||
cd wled-screen-controller/server
|
cd ledgrab/server
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ cd server
|
|||||||
docker build -t ledgrab .
|
docker build -t ledgrab .
|
||||||
|
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name wled-screen-controller \
|
--name ledgrab \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v $(pwd)/data:/app/data \
|
-v $(pwd)/data:/app/data \
|
||||||
-v $(pwd)/logs:/app/logs \
|
-v $(pwd)/logs:/app/logs \
|
||||||
@@ -84,8 +86,8 @@ Screen capture from inside a container requires X11 access. Uncomment `network_m
|
|||||||
1. **Clone the repository:**
|
1. **Clone the repository:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git
|
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git
|
||||||
cd wled-screen-controller/server
|
cd ledgrab/server
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Build the frontend bundle:**
|
2. **Build the frontend bundle:**
|
||||||
@@ -95,7 +97,7 @@ Screen capture from inside a container requires X11 access. Uncomment `network_m
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
This compiles TypeScript and bundles JS/CSS into `src/wled_controller/static/dist/`.
|
This compiles TypeScript and bundles JS/CSS into `src/ledgrab/static/dist/`.
|
||||||
|
|
||||||
3. **Create a virtual environment:**
|
3. **Create a virtual environment:**
|
||||||
|
|
||||||
@@ -131,11 +133,11 @@ Screen capture from inside a container requires X11 access. Uncomment `network_m
|
|||||||
```bash
|
```bash
|
||||||
# Linux / macOS
|
# Linux / macOS
|
||||||
export PYTHONPATH=$(pwd)/src
|
export PYTHONPATH=$(pwd)/src
|
||||||
uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080
|
uvicorn ledgrab.main:app --host 0.0.0.0 --port 8080
|
||||||
|
|
||||||
# Windows (cmd)
|
# Windows (cmd)
|
||||||
set PYTHONPATH=%CD%\src
|
set PYTHONPATH=%CD%\src
|
||||||
uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080
|
uvicorn ledgrab.main:app --host 0.0.0.0 --port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Verify:** open <http://localhost:8080> in your browser.
|
6. **Verify:** open <http://localhost:8080> in your browser.
|
||||||
@@ -160,7 +162,7 @@ auth:
|
|||||||
Option B -- set an environment variable:
|
Option B -- set an environment variable:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export WLED_AUTH__API_KEYS__dev="your-secure-key-here"
|
export LEDGRAB_AUTH__API_KEYS__dev="your-secure-key-here"
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate a random key:
|
Generate a random key:
|
||||||
@@ -184,7 +186,7 @@ server:
|
|||||||
Or via environment variable:
|
Or via environment variable:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
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"]'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Discover devices
|
### Discover devices
|
||||||
@@ -193,57 +195,12 @@ Open the dashboard and go to the **Devices** tab. Click **Discover** to find WLE
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Home Assistant Integration
|
|
||||||
|
|
||||||
### Option 1: HACS (recommended)
|
|
||||||
|
|
||||||
1. Install [HACS](https://hacs.xyz/docs/setup/download) if you have not already.
|
|
||||||
2. Open HACS in Home Assistant.
|
|
||||||
3. Click the three-dot menu, then **Custom repositories**.
|
|
||||||
4. Add URL: `https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed`
|
|
||||||
5. Set category to **Integration** and click **Add**.
|
|
||||||
6. Search for "WLED Screen Controller" in HACS and click **Download**.
|
|
||||||
7. Restart Home Assistant.
|
|
||||||
8. Go to **Settings > Devices & Services > Add Integration** and search for "WLED Screen Controller".
|
|
||||||
9. Enter your server URL (e.g., `http://192.168.1.100:8080`) and API key.
|
|
||||||
|
|
||||||
### Option 2: Manual
|
|
||||||
|
|
||||||
Copy the `custom_components/wled_screen_controller/` folder from this repository into your Home Assistant `config/custom_components/` directory, then restart Home Assistant and add the integration as above.
|
|
||||||
|
|
||||||
### Automation example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
automation:
|
|
||||||
- alias: "Start ambient lighting when TV turns on"
|
|
||||||
trigger:
|
|
||||||
- platform: state
|
|
||||||
entity_id: media_player.living_room_tv
|
|
||||||
to: "on"
|
|
||||||
action:
|
|
||||||
- service: switch.turn_on
|
|
||||||
target:
|
|
||||||
entity_id: switch.living_room_tv_processing
|
|
||||||
|
|
||||||
- alias: "Stop ambient lighting when TV turns off"
|
|
||||||
trigger:
|
|
||||||
- platform: state
|
|
||||||
entity_id: media_player.living_room_tv
|
|
||||||
to: "off"
|
|
||||||
action:
|
|
||||||
- service: switch.turn_off
|
|
||||||
target:
|
|
||||||
entity_id: switch.living_room_tv_processing
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration Reference
|
## Configuration Reference
|
||||||
|
|
||||||
The server reads configuration from three sources (in order of priority):
|
The server reads configuration from three sources (in order of priority):
|
||||||
|
|
||||||
1. **Environment variables** -- prefix `WLED_`, double underscore as nesting delimiter (e.g., `WLED_SERVER__PORT=9090`)
|
1. **Environment variables** -- prefix `LEDGRAB_`, double underscore as nesting delimiter (e.g., `LEDGRAB_SERVER__PORT=9090`)
|
||||||
2. **YAML config file** -- `server/config/default_config.yaml` (or set `WLED_CONFIG_PATH` to override)
|
2. **YAML config file** -- `server/config/default_config.yaml` (or set `LEDGRAB_CONFIG_PATH` to override)
|
||||||
3. **Built-in defaults**
|
3. **Built-in defaults**
|
||||||
|
|
||||||
See [`server/.env.example`](server/.env.example) for every available variable with descriptions.
|
See [`server/.env.example`](server/.env.example) for every available variable with descriptions.
|
||||||
@@ -252,14 +209,14 @@ See [`server/.env.example`](server/.env.example) for every available variable wi
|
|||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
| -------- | ------- | ----------- |
|
| -------- | ------- | ----------- |
|
||||||
| `WLED_SERVER__PORT` | `8080` | HTTP listen port |
|
| `LEDGRAB_SERVER__PORT` | `8080` | HTTP listen port |
|
||||||
| `WLED_SERVER__LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
| `LEDGRAB_SERVER__LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
||||||
| `WLED_SERVER__CORS_ORIGINS` | `["http://localhost:8080"]` | Allowed CORS origins (JSON array) |
|
| `LEDGRAB_SERVER__CORS_ORIGINS` | `["http://localhost:8080"]` | Allowed CORS origins (JSON array) |
|
||||||
| `WLED_AUTH__API_KEYS` | `{"dev":"development-key..."}` | API keys (JSON object) |
|
| `LEDGRAB_AUTH__API_KEYS` | `{"dev":"development-key..."}` | API keys (JSON object) |
|
||||||
| `WLED_STORAGE__DATABASE_FILE` | `data/ledgrab.db` | SQLite database path |
|
| `LEDGRAB_STORAGE__DATABASE_FILE` | `data/ledgrab.db` | SQLite database path |
|
||||||
| `WLED_MQTT__ENABLED` | `false` | Enable MQTT for HA auto-discovery |
|
| `LEDGRAB_MQTT__ENABLED` | `false` | Enable MQTT for HA auto-discovery |
|
||||||
| `WLED_MQTT__BROKER_HOST` | `localhost` | MQTT broker address |
|
| `LEDGRAB_MQTT__BROKER_HOST` | `localhost` | MQTT broker address |
|
||||||
| `WLED_DEMO` | `false` | Enable demo mode (sandbox with virtual devices) |
|
| `LEDGRAB_DEMO` | `false` | Enable demo mode (sandbox with virtual devices) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -276,7 +233,7 @@ python --version # must be 3.11+
|
|||||||
**Check the frontend bundle exists:**
|
**Check the frontend bundle exists:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ls server/src/wled_controller/static/dist/app.bundle.js
|
ls server/src/ledgrab/static/dist/app.bundle.js
|
||||||
```
|
```
|
||||||
|
|
||||||
If missing, run `cd server && npm ci && npm run build`.
|
If missing, run `cd server && npm ci && npm run build`.
|
||||||
@@ -288,7 +245,7 @@ If missing, run `cd server && npm ci && npm run build`.
|
|||||||
docker compose logs -f
|
docker compose logs -f
|
||||||
|
|
||||||
# Manual install
|
# Manual install
|
||||||
tail -f logs/wled_controller.log
|
tail -f logs/ledgrab.log
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cannot access the dashboard from another machine
|
### Cannot access the dashboard from another machine
|
||||||
@@ -297,13 +254,6 @@ tail -f logs/wled_controller.log
|
|||||||
2. Check your firewall allows inbound traffic on port 8080.
|
2. Check your firewall allows inbound traffic on port 8080.
|
||||||
3. Add your server's LAN IP to `cors_origins` (see [Configure CORS](#configure-cors-for-lan-access) above).
|
3. Add your server's LAN IP to `cors_origins` (see [Configure CORS](#configure-cors-for-lan-access) above).
|
||||||
|
|
||||||
### Home Assistant integration not appearing
|
|
||||||
|
|
||||||
1. Verify HACS installed the component: check that `config/custom_components/wled_screen_controller/` exists.
|
|
||||||
2. Clear your browser cache.
|
|
||||||
3. Restart Home Assistant.
|
|
||||||
4. Check logs at **Settings > System > Logs** and search for `wled_screen_controller`.
|
|
||||||
|
|
||||||
### WLED device not responding
|
### WLED device not responding
|
||||||
|
|
||||||
1. Confirm the device is powered on and connected to Wi-Fi.
|
1. Confirm the device is powered on and connected to Wi-Fi.
|
||||||
@@ -324,4 +274,4 @@ tail -f logs/wled_controller.log
|
|||||||
|
|
||||||
- [API Documentation](docs/API.md)
|
- [API Documentation](docs/API.md)
|
||||||
- [Calibration Guide](docs/CALIBRATION.md)
|
- [Calibration Guide](docs/CALIBRATION.md)
|
||||||
- [Repository Issues](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/issues)
|
- [Repository Issues](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/issues)
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ A Home Assistant integration exposes devices as entities for smart home automati
|
|||||||
### Docker (recommended)
|
### Docker (recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git
|
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git
|
||||||
cd wled-screen-controller/server
|
cd ledgrab/server
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -97,8 +97,8 @@ docker compose up -d
|
|||||||
Requires Python 3.11+ and Node.js 18+.
|
Requires Python 3.11+ and Node.js 18+.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git
|
git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git
|
||||||
cd wled-screen-controller/server
|
cd ledgrab/server
|
||||||
|
|
||||||
# Build the frontend bundle
|
# Build the frontend bundle
|
||||||
npm ci && npm run build
|
npm ci && npm run build
|
||||||
@@ -112,7 +112,7 @@ pip install .
|
|||||||
# Start the server
|
# Start the server
|
||||||
export PYTHONPATH=$(pwd)/src # Linux/Mac
|
export PYTHONPATH=$(pwd)/src # Linux/Mac
|
||||||
# set PYTHONPATH=%CD%\src # Windows
|
# set PYTHONPATH=%CD%\src # Windows
|
||||||
uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080
|
uvicorn ledgrab.main:app --host 0.0.0.0 --port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Open **http://localhost:8080** to access the dashboard.
|
Open **http://localhost:8080** to access the dashboard.
|
||||||
@@ -125,17 +125,17 @@ See [INSTALLATION.md](INSTALLATION.md) for the full installation guide, includin
|
|||||||
|
|
||||||
Demo mode runs the server with virtual devices, sample data, and isolated storage — useful for exploring the UI without real hardware.
|
Demo mode runs the server with virtual devices, sample data, and isolated storage — useful for exploring the UI without real hardware.
|
||||||
|
|
||||||
Set the `WLED_DEMO` environment variable to `true`, `1`, or `yes`:
|
Set the `LEDGRAB_DEMO` environment variable to `true`, `1`, or `yes`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Docker
|
# Docker
|
||||||
docker compose run -e WLED_DEMO=true server
|
docker compose run -e LEDGRAB_DEMO=true server
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
WLED_DEMO=true uvicorn wled_controller.main:app --host 0.0.0.0 --port 8081
|
LEDGRAB_DEMO=true uvicorn ledgrab.main:app --host 0.0.0.0 --port 8081
|
||||||
|
|
||||||
# Windows (installed app)
|
# Windows (installed app)
|
||||||
set WLED_DEMO=true
|
set LEDGRAB_DEMO=true
|
||||||
LedGrab.bat
|
LedGrab.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -144,9 +144,9 @@ Demo mode uses port **8081**, config file `config/demo_config.yaml`, and stores
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```text
|
```text
|
||||||
wled-screen-controller/
|
ledgrab/
|
||||||
├── server/ # Python FastAPI backend
|
├── server/ # Python FastAPI backend
|
||||||
│ ├── src/wled_controller/
|
│ ├── src/ledgrab/
|
||||||
│ │ ├── main.py # Application entry point
|
│ │ ├── main.py # Application entry point
|
||||||
│ │ ├── config.py # YAML + env var configuration
|
│ │ ├── config.py # YAML + env var configuration
|
||||||
│ │ ├── api/
|
│ │ ├── api/
|
||||||
@@ -171,8 +171,6 @@ wled-screen-controller/
|
|||||||
│ ├── tests/ # pytest suite
|
│ ├── tests/ # pytest suite
|
||||||
│ ├── Dockerfile
|
│ ├── Dockerfile
|
||||||
│ └── docker-compose.yml
|
│ └── docker-compose.yml
|
||||||
├── custom_components/ # Home Assistant integration (HACS)
|
|
||||||
│ └── wled_screen_controller/
|
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── API.md # REST API reference
|
│ ├── API.md # REST API reference
|
||||||
│ └── CALIBRATION.md # LED calibration guide
|
│ └── CALIBRATION.md # LED calibration guide
|
||||||
@@ -182,7 +180,7 @@ wled-screen-controller/
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Edit `server/config/default_config.yaml` or use environment variables with the `WLED_` prefix:
|
Edit `server/config/default_config.yaml` or use environment variables with the `LEDGRAB_` prefix:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
server:
|
server:
|
||||||
@@ -200,11 +198,11 @@ storage:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
format: "json"
|
format: "json"
|
||||||
file: "logs/wled_controller.log"
|
file: "logs/ledgrab.log"
|
||||||
max_size_mb: 100
|
max_size_mb: 100
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment variable override example: `WLED_SERVER__PORT=9090`.
|
Environment variable override example: `LEDGRAB_SERVER__PORT=9090`.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -234,9 +232,7 @@ See [docs/CALIBRATION.md](docs/CALIBRATION.md) for a step-by-step guide.
|
|||||||
|
|
||||||
## Home Assistant
|
## Home Assistant
|
||||||
|
|
||||||
Install via HACS (add as a custom repository) or manually copy `custom_components/wled_screen_controller/` into your HA config directory. The integration creates light, switch, sensor, and number entities for each configured device.
|
For Home Assistant integration, see the separate [ledgrab-haos-integration](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab-haos-integration) repository.
|
||||||
|
|
||||||
See [INSTALLATION.md](INSTALLATION.md) for detailed setup instructions.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+125
-125
@@ -5,96 +5,96 @@ This release brings a major expansion of integrations and source types: Home Ass
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
#### Home Assistant Integration
|
#### Home Assistant Integration
|
||||||
- Home Assistant integration with WebSocket connection, automation conditions, and UI ([2153dde](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/2153dde))
|
- Home Assistant integration with WebSocket connection, automation conditions, and UI ([2153dde](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/2153dde))
|
||||||
- HA light output targets — cast LED colors to Home Assistant lights ([cb9289f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/cb9289f))
|
- HA light output targets — cast LED colors to Home Assistant lights ([cb9289f](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/cb9289f))
|
||||||
- Entity picker for HA light mapping — searchable EntitySelect for light entities ([324a308](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/324a308))
|
- Entity picker for HA light mapping — searchable EntitySelect for light entities ([324a308](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/324a308))
|
||||||
- HA light target live color preview — per-entity swatches via WebSocket ([40751fe](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/40751fe))
|
- HA light target live color preview — per-entity swatches via WebSocket ([40751fe](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/40751fe))
|
||||||
- HA source cards use health-dot indicators ([e7c9a56](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/e7c9a56))
|
- HA source cards use health-dot indicators ([e7c9a56](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/e7c9a56))
|
||||||
|
|
||||||
#### Integrations & Tabs
|
#### Integrations & Tabs
|
||||||
- New **Integrations** tab and responsive icon-only tabs ([b7da4ab](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b7da4ab))
|
- New **Integrations** tab and responsive icon-only tabs ([b7da4ab](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/b7da4ab))
|
||||||
- Multi-instance MQTT — refactored from global config to entity model ([c59107c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/c59107c))
|
- Multi-instance MQTT — refactored from global config to entity model ([c59107c](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/c59107c))
|
||||||
- Game integration system ([492bdb9](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/492bdb9))
|
- Game integration system ([492bdb9](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/492bdb9))
|
||||||
|
|
||||||
#### Audio
|
#### Audio
|
||||||
- Processed audio sources — audio filter framework ([86a9d34](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/86a9d34))
|
- Processed audio sources — audio filter framework ([86a9d34](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/86a9d34))
|
||||||
- 11 audio filters implemented ([eb94066](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/eb94066))
|
- 11 audio filters implemented ([eb94066](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/eb94066))
|
||||||
- Processed audio source model + runtime filter integration ([353c090](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/353c090), [ab43578](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ab43578))
|
- Processed audio source model + runtime filter integration ([353c090](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/353c090), [ab43578](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ab43578))
|
||||||
- Frontend audio processing templates + source type cleanup ([5534639](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/5534639), [1ce0dc6](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/1ce0dc6))
|
- Frontend audio processing templates + source type cleanup ([5534639](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/5534639), [1ce0dc6](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/1ce0dc6))
|
||||||
- Music sync viz modes and `auto_gain` audio filter ([b04978a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b04978a))
|
- Music sync viz modes and `auto_gain` audio filter ([b04978a](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/b04978a))
|
||||||
|
|
||||||
#### Value Sources
|
#### Value Sources
|
||||||
- **BindableFloat** — universal value source binding for all scalar properties ([8a17bb5](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/8a17bb5))
|
- **BindableFloat** — universal value source binding for all scalar properties ([8a17bb5](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/8a17bb5))
|
||||||
- New value source types: HA entity, gradient map, strip extract ([384362c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/384362c))
|
- New value source types: HA entity, gradient map, strip extract ([384362c](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/384362c))
|
||||||
- `system_metrics` value source type ([b6713be](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b6713be))
|
- `system_metrics` value source type ([b6713be](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/b6713be))
|
||||||
- Color value source test visualization ([f6c25cd](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f6c25cd))
|
- Color value source test visualization ([f6c25cd](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/f6c25cd))
|
||||||
- HA value source test — raw value axis + behavior IconSelect ([0a87371](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/0a87371))
|
- HA value source test — raw value axis + behavior IconSelect ([0a87371](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/0a87371))
|
||||||
- Value source card crosslinks + gradient_map test shows input value ([4b7a8d7](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/4b7a8d7))
|
- Value source card crosslinks + gradient_map test shows input value ([4b7a8d7](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/4b7a8d7))
|
||||||
|
|
||||||
#### Sources & Assets
|
#### Sources & Assets
|
||||||
- Asset-based image/video sources, notification sounds, UI improvements ([e2e1107](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/e2e1107))
|
- Asset-based image/video sources, notification sounds, UI improvements ([e2e1107](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/e2e1107))
|
||||||
- `math_wave` color strip source type ([ace2471](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ace2471))
|
- `math_wave` color strip source type ([ace2471](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ace2471))
|
||||||
- `api_input` LED interpolation; fixes for LED preview, FPS charts, dashboard layout ([3e0bf85](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/3e0bf85))
|
- `api_input` LED interpolation; fixes for LED preview, FPS charts, dashboard layout ([3e0bf85](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/3e0bf85))
|
||||||
- Custom file drop zone for asset upload modal ([f61a020](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f61a020))
|
- Custom file drop zone for asset upload modal ([f61a020](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/f61a020))
|
||||||
|
|
||||||
#### UI & UX
|
#### UI & UX
|
||||||
- Card glare effect on dashboard and perf chart cards ([ce53ca6](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ce53ca6))
|
- Card glare effect on dashboard and perf chart cards ([ce53ca6](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ce53ca6))
|
||||||
- System theme option + toast timer overlap fix ([db5008a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/db5008a))
|
- System theme option + toast timer overlap fix ([db5008a](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/db5008a))
|
||||||
- Donation banner, About tab, settings UI improvements ([f3d07fc](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f3d07fc))
|
- Donation banner, About tab, settings UI improvements ([f3d07fc](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/f3d07fc))
|
||||||
- Custom app icon for shortcuts and installer ([5f70302](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/5f70302))
|
- Custom app icon for shortcuts and installer ([5f70302](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/5f70302))
|
||||||
|
|
||||||
#### Runtime
|
#### Runtime
|
||||||
- Port busy check before starting the server ([ea812bb](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ea812bb))
|
- Port busy check before starting the server ([ea812bb](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ea812bb))
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- Tray: replace tkinter messagebox with Win32 `MessageBoxW` ([d037a2e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/d037a2e))
|
- Tray: replace tkinter messagebox with Win32 `MessageBoxW` ([d037a2e](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/d037a2e))
|
||||||
- Launcher: `start-hidden.vbs` must be ASCII + CRLF, use `python.exe` ([fc8ee34](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/fc8ee34))
|
- Launcher: `start-hidden.vbs` must be ASCII + CRLF, use `python.exe` ([fc8ee34](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/fc8ee34))
|
||||||
- Launcher: set `PYTHONPATH` and `WLED_CONFIG_PATH` in `start-hidden.vbs` ([e262a8b](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/e262a8b))
|
- Launcher: set `PYTHONPATH` and `LEDGRAB_CONFIG_PATH` in `start-hidden.vbs` ([e262a8b](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/e262a8b))
|
||||||
- Weather CSS card shows empty source name after hard refresh ([6e8b159](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/6e8b159))
|
- Weather CSS card shows empty source name after hard refresh ([6e8b159](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/6e8b159))
|
||||||
- Replace HA test icon with refresh; make automation rules collapsible ([edc6d27](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/edc6d27))
|
- Replace HA test icon with refresh; make automation rules collapsible ([edc6d27](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/edc6d27))
|
||||||
- `pystray` is a core dependency on Windows (no longer optional extra) ([99460a8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/99460a8))
|
- `pystray` is a core dependency on Windows (no longer optional extra) ([99460a8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/99460a8))
|
||||||
- Audio tree structure, filter i18n, IconSelect for filter options ([af2c89c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/af2c89c))
|
- Audio tree structure, filter i18n, IconSelect for filter options ([af2c89c](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/af2c89c))
|
||||||
- Reference check before deleting audio processing template ([d04192f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/d04192f))
|
- Reference check before deleting audio processing template ([d04192f](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/d04192f))
|
||||||
- Device card header layout — URL badge overflow and hide button gap ([11d5d6b](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/11d5d6b))
|
- Device card header layout — URL badge overflow and hide button gap ([11d5d6b](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/11d5d6b))
|
||||||
- KC color strip test preview uses `LiveStreamManager` instead of raw engine ([a9e6e8c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/a9e6e8c))
|
- KC color strip test preview uses `LiveStreamManager` instead of raw engine ([a9e6e8c](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/a9e6e8c))
|
||||||
- Composite layer opacity/brightness widgets + CSS layout ([78ce6c8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/78ce6c8))
|
- Composite layer opacity/brightness widgets + CSS layout ([78ce6c8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/78ce6c8))
|
||||||
- HA light target — brightness source, `transition=0`, dashboard type label ([381ee75](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/381ee75))
|
- HA light target — brightness source, `transition=0`, dashboard type label ([381ee75](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/381ee75))
|
||||||
- Rename HA Lights → Home Assistant, HA Light Targets → Light Targets ([89d1b13](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/89d1b13))
|
- Rename HA Lights → Home Assistant, HA Light Targets → Light Targets ([89d1b13](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/89d1b13))
|
||||||
- Command palette actions and automation condition button ([c0853ce](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/c0853ce))
|
- Command palette actions and automation condition button ([c0853ce](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/c0853ce))
|
||||||
- Show template name instead of ID in filter list and card badges ([be4c98b](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/be4c98b))
|
- Show template name instead of ID in filter list and card badges ([be4c98b](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/be4c98b))
|
||||||
- Clip graph node title and subtitle to prevent overflow ([dca2d21](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/dca2d21))
|
- Clip graph node title and subtitle to prevent overflow ([dca2d21](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/dca2d21))
|
||||||
- Replace emoji with SVG icons on weather and daylight cards ([53986f8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/53986f8))
|
- Replace emoji with SVG icons on weather and daylight cards ([53986f8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/53986f8))
|
||||||
- Send `gradient_id` instead of palette in effect transient preview ([a4a9f6f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/a4a9f6f))
|
- Send `gradient_id` instead of palette in effect transient preview ([a4a9f6f](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/a4a9f6f))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Development / Internal
|
### Development / Internal
|
||||||
|
|
||||||
#### Build
|
#### Build
|
||||||
- Drop `packaging` dependency, inline version parsing ([d4ffe2e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/d4ffe2e))
|
- Drop `packaging` dependency, inline version parsing ([d4ffe2e](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/d4ffe2e))
|
||||||
- Fix shell syntax error in `smoke_test_imports` heredoc ([feb91ad](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/feb91ad))
|
- Fix shell syntax error in `smoke_test_imports` heredoc ([feb91ad](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/feb91ad))
|
||||||
- Keep `.py` sources; smoke test skips uninstalled modules ([17c5c02](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/17c5c02))
|
- Keep `.py` sources; smoke test skips uninstalled modules ([17c5c02](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/17c5c02))
|
||||||
- Stop stripping `zeroconf/_services`; add import smoke test ([fd6776a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/fd6776a))
|
- Stop stripping `zeroconf/_services`; add import smoke test ([fd6776a](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/fd6776a))
|
||||||
- Stop stripping `numpy.lib`/`linalg` from site-packages ([9f34ffb](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/9f34ffb))
|
- Stop stripping `numpy.lib`/`linalg` from site-packages ([9f34ffb](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/9f34ffb))
|
||||||
- Normalize non-PEP440 versions, fix `.py`/`compileall` ordering, wipe NSIS payload dirs ([b5842e6](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b5842e6))
|
- Normalize non-PEP440 versions, fix `.py`/`compileall` ordering, wipe NSIS payload dirs ([b5842e6](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/b5842e6))
|
||||||
|
|
||||||
#### CI
|
#### CI
|
||||||
- Add manual build workflow for testing artifacts ([fb98e6e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/fb98e6e))
|
- Add manual build workflow for testing artifacts ([fb98e6e](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/fb98e6e))
|
||||||
- Use sparse checkout for release notes in release workflow ([9fcfdb8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/9fcfdb8))
|
- Use sparse checkout for release notes in release workflow ([9fcfdb8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/9fcfdb8))
|
||||||
|
|
||||||
#### Refactoring
|
#### Refactoring
|
||||||
- Split `color-strips.ts` into focused modules under `color-strips/` folder ([7a9c368](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/7a9c368))
|
- Split `color-strips.ts` into focused modules under `color-strips/` folder ([7a9c368](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/7a9c368))
|
||||||
- Key colors targets → CSS source type; HA target improvements ([3e6760f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/3e6760f))
|
- Key colors targets → CSS source type; HA target improvements ([3e6760f](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/3e6760f))
|
||||||
- Move Weather and Home Assistant sources to Integrations tree group ([3c2efd5](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/3c2efd5))
|
- Move Weather and Home Assistant sources to Integrations tree group ([3c2efd5](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/3c2efd5))
|
||||||
|
|
||||||
#### Tests
|
#### Tests
|
||||||
- Isolate tests from production database ([992495e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/992495e))
|
- Isolate tests from production database ([992495e](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/992495e))
|
||||||
- Processed audio sources: phase 7 testing and polish + phase 8 design review ([ce1f484](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ce1f484), [6b0e4e5](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/6b0e4e5))
|
- Processed audio sources: phase 7 testing and polish + phase 8 design review ([ce1f484](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ce1f484), [6b0e4e5](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/6b0e4e5))
|
||||||
|
|
||||||
#### Chores
|
#### Chores
|
||||||
- Remove processed-audio-sources plan files ([89990f8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/89990f8))
|
- Remove processed-audio-sources plan files ([89990f8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/89990f8))
|
||||||
- Remove python3.11 version pin from pre-commit config ([f345687](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f345687))
|
- Remove python3.11 version pin from pre-commit config ([f345687](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/f345687))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -103,69 +103,69 @@ This release brings a major expansion of integrations and source types: Home Ass
|
|||||||
|
|
||||||
| Hash | Message |
|
| Hash | Message |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| [d037a2e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/d037a2e) | fix(tray): replace tkinter messagebox with Win32 MessageBoxW |
|
| [d037a2e](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/d037a2e) | fix(tray): replace tkinter messagebox with Win32 MessageBoxW |
|
||||||
| [fc8ee34](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/fc8ee34) | fix(launcher): start-hidden.vbs must be ASCII + CRLF, use python.exe |
|
| [fc8ee34](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/fc8ee34) | fix(launcher): start-hidden.vbs must be ASCII + CRLF, use python.exe |
|
||||||
| [e262a8b](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/e262a8b) | fix(launcher): set PYTHONPATH and WLED_CONFIG_PATH in start-hidden.vbs |
|
| [e262a8b](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/e262a8b) | fix(launcher): set PYTHONPATH and LEDGRAB_CONFIG_PATH in start-hidden.vbs |
|
||||||
| [d4ffe2e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/d4ffe2e) | refactor: drop packaging dependency, inline version parsing |
|
| [d4ffe2e](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/d4ffe2e) | refactor: drop packaging dependency, inline version parsing |
|
||||||
| [feb91ad](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/feb91ad) | fix(build): fix shell syntax error in smoke_test_imports heredoc |
|
| [feb91ad](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/feb91ad) | fix(build): fix shell syntax error in smoke_test_imports heredoc |
|
||||||
| [17c5c02](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/17c5c02) | fix(build): keep .py sources + make smoke test skip uninstalled modules |
|
| [17c5c02](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/17c5c02) | fix(build): keep .py sources + make smoke test skip uninstalled modules |
|
||||||
| [fd6776a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/fd6776a) | fix(build): stop stripping zeroconf/_services + add import smoke test |
|
| [fd6776a](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/fd6776a) | fix(build): stop stripping zeroconf/_services + add import smoke test |
|
||||||
| [9f34ffb](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/9f34ffb) | fix(build): stop stripping numpy.lib/linalg from site-packages |
|
| [9f34ffb](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/9f34ffb) | fix(build): stop stripping numpy.lib/linalg from site-packages |
|
||||||
| [b5842e6](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b5842e6) | fix(build): normalize non-PEP440 versions, fix .py/compileall ordering, wipe NSIS payload dirs |
|
| [b5842e6](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/b5842e6) | fix(build): normalize non-PEP440 versions, fix .py/compileall ordering, wipe NSIS payload dirs |
|
||||||
| [7a9c368](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/7a9c368) | refactor: split color-strips.ts into focused modules under color-strips/ folder |
|
| [7a9c368](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/7a9c368) | refactor: split color-strips.ts into focused modules under color-strips/ folder |
|
||||||
| [ce53ca6](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ce53ca6) | feat: add card glare effect to dashboard and perf chart cards |
|
| [ce53ca6](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ce53ca6) | feat: add card glare effect to dashboard and perf chart cards |
|
||||||
| [b04978a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b04978a) | feat: add music sync viz modes and auto_gain audio filter |
|
| [b04978a](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/b04978a) | feat: add music sync viz modes and auto_gain audio filter |
|
||||||
| [6e8b159](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/6e8b159) | fix: weather CSS card shows empty source name after hard refresh |
|
| [6e8b159](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/6e8b159) | fix: weather CSS card shows empty source name after hard refresh |
|
||||||
| [ace2471](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ace2471) | feat: add math_wave color strip source type |
|
| [ace2471](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ace2471) | feat: add math_wave color strip source type |
|
||||||
| [edc6d27](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/edc6d27) | fix: replace HA test icon with refresh, make automation rules collapsible |
|
| [edc6d27](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/edc6d27) | fix: replace HA test icon with refresh, make automation rules collapsible |
|
||||||
| [b7da4ab](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b7da4ab) | feat: add Integrations tab and responsive icon-only tabs |
|
| [b7da4ab](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/b7da4ab) | feat: add Integrations tab and responsive icon-only tabs |
|
||||||
| [99460a8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/99460a8) | fix: make pystray a core dependency on Windows instead of optional extra |
|
| [99460a8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/99460a8) | fix: make pystray a core dependency on Windows instead of optional extra |
|
||||||
| [89990f8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/89990f8) | chore: remove processed-audio-sources plan files |
|
| [89990f8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/89990f8) | chore: remove processed-audio-sources plan files |
|
||||||
| [af2c89c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/af2c89c) | fix: audio tree structure, filter i18n, and IconSelect for filter options |
|
| [af2c89c](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/af2c89c) | fix: audio tree structure, filter i18n, and IconSelect for filter options |
|
||||||
| [d04192f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/d04192f) | fix: add reference check before deleting audio processing template |
|
| [d04192f](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/d04192f) | fix: add reference check before deleting audio processing template |
|
||||||
| [992495e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/992495e) | fix: isolate tests from production database |
|
| [992495e](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/992495e) | fix: isolate tests from production database |
|
||||||
| [6b0e4e5](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/6b0e4e5) | feat(processed-audio-sources): phase 8 - frontend design consistency review |
|
| [6b0e4e5](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/6b0e4e5) | feat(processed-audio-sources): phase 8 - frontend design consistency review |
|
||||||
| [ce1f484](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ce1f484) | feat(processed-audio-sources): phase 7 - testing and polish |
|
| [ce1f484](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ce1f484) | feat(processed-audio-sources): phase 7 - testing and polish |
|
||||||
| [1ce0dc6](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/1ce0dc6) | feat(processed-audio-sources): phase 6 - frontend source type cleanup |
|
| [1ce0dc6](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/1ce0dc6) | feat(processed-audio-sources): phase 6 - frontend source type cleanup |
|
||||||
| [5534639](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/5534639) | feat(processed-audio-sources): phase 5 - frontend audio processing templates |
|
| [5534639](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/5534639) | feat(processed-audio-sources): phase 5 - frontend audio processing templates |
|
||||||
| [ab43578](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ab43578) | feat(processed-audio-sources): phase 4 - runtime filter integration |
|
| [ab43578](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ab43578) | feat(processed-audio-sources): phase 4 - runtime filter integration |
|
||||||
| [353c090](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/353c090) | feat(processed-audio-sources): phase 3 - processed audio source model |
|
| [353c090](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/353c090) | feat(processed-audio-sources): phase 3 - processed audio source model |
|
||||||
| [eb94066](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/eb94066) | feat(processed-audio-sources): phase 2 - implement 11 audio filters |
|
| [eb94066](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/eb94066) | feat(processed-audio-sources): phase 2 - implement 11 audio filters |
|
||||||
| [86a9d34](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/86a9d34) | feat(processed-audio-sources): phase 1 - audio filter framework |
|
| [86a9d34](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/86a9d34) | feat(processed-audio-sources): phase 1 - audio filter framework |
|
||||||
| [c59107c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/c59107c) | feat: refactor MQTT from global config to multi-instance entity model |
|
| [c59107c](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/c59107c) | feat: refactor MQTT from global config to multi-instance entity model |
|
||||||
| [e7c9a56](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/e7c9a56) | feat: HA source cards use health-dot indicators |
|
| [e7c9a56](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/e7c9a56) | feat: HA source cards use health-dot indicators |
|
||||||
| [492bdb9](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/492bdb9) | feat: game integration system |
|
| [492bdb9](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/492bdb9) | feat: game integration system |
|
||||||
| [b6713be](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b6713be) | feat: system_metrics value source type |
|
| [b6713be](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/b6713be) | feat: system_metrics value source type |
|
||||||
| [db5008a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/db5008a) | feat: system theme option + fix toast timer overlap |
|
| [db5008a](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/db5008a) | feat: system theme option + fix toast timer overlap |
|
||||||
| [4b7a8d7](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/4b7a8d7) | feat: value source card crosslinks + gradient_map test shows input value |
|
| [4b7a8d7](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/4b7a8d7) | feat: value source card crosslinks + gradient_map test shows input value |
|
||||||
| [f6c25cd](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f6c25cd) | feat: color value source test visualization |
|
| [f6c25cd](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/f6c25cd) | feat: color value source test visualization |
|
||||||
| [0a87371](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/0a87371) | feat: HA value source test — raw value axis + behavior IconSelect |
|
| [0a87371](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/0a87371) | feat: HA value source test — raw value axis + behavior IconSelect |
|
||||||
| [11d5d6b](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/11d5d6b) | fix: device card header layout — URL badge overflow and hide button gap |
|
| [11d5d6b](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/11d5d6b) | fix: device card header layout — URL badge overflow and hide button gap |
|
||||||
| [384362c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/384362c) | feat: new value source types (HA entity, gradient map, strip extract) + UI fixes |
|
| [384362c](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/384362c) | feat: new value source types (HA entity, gradient map, strip extract) + UI fixes |
|
||||||
| [ea812bb](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/ea812bb) | feat: check if port is busy before starting the server |
|
| [ea812bb](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/ea812bb) | feat: check if port is busy before starting the server |
|
||||||
| [a9e6e8c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/a9e6e8c) | fix: KC color strip test preview — use LiveStreamManager instead of raw engine |
|
| [a9e6e8c](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/a9e6e8c) | fix: KC color strip test preview — use LiveStreamManager instead of raw engine |
|
||||||
| [78ce6c8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/78ce6c8) | fix: composite layer opacity/brightness widgets + CSS layout |
|
| [78ce6c8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/78ce6c8) | fix: composite layer opacity/brightness widgets + CSS layout |
|
||||||
| [8a17bb5](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/8a17bb5) | feat: BindableFloat — universal value source binding for all scalar properties |
|
| [8a17bb5](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/8a17bb5) | feat: BindableFloat — universal value source binding for all scalar properties |
|
||||||
| [5f70302](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/5f70302) | feat: use custom app icon for shortcuts and installer |
|
| [5f70302](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/5f70302) | feat: use custom app icon for shortcuts and installer |
|
||||||
| [40751fe](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/40751fe) | feat: HA light target live color preview — per-entity swatches via WebSocket |
|
| [40751fe](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/40751fe) | feat: HA light target live color preview — per-entity swatches via WebSocket |
|
||||||
| [381ee75](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/381ee75) | fix: HA light target — brightness source, transition=0, dashboard type label |
|
| [381ee75](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/381ee75) | fix: HA light target — brightness source, transition=0, dashboard type label |
|
||||||
| [3e6760f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/3e6760f) | refactor: key colors targets → CSS source type, HA target improvements |
|
| [3e6760f](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/3e6760f) | refactor: key colors targets → CSS source type, HA target improvements |
|
||||||
| [89d1b13](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/89d1b13) | fix: rename HA Lights → Home Assistant, HA Light Targets → Light Targets |
|
| [89d1b13](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/89d1b13) | fix: rename HA Lights → Home Assistant, HA Light Targets → Light Targets |
|
||||||
| [324a308](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/324a308) | feat: entity picker for HA light mapping — searchable EntitySelect for light entities |
|
| [324a308](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/324a308) | feat: entity picker for HA light mapping — searchable EntitySelect for light entities |
|
||||||
| [cb9289f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/cb9289f) | feat: HA light output targets — cast LED colors to Home Assistant lights |
|
| [cb9289f](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/cb9289f) | feat: HA light output targets — cast LED colors to Home Assistant lights |
|
||||||
| [fb98e6e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/fb98e6e) | ci: add manual build workflow for testing artifacts |
|
| [fb98e6e](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/fb98e6e) | ci: add manual build workflow for testing artifacts |
|
||||||
| [3c2efd5](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/3c2efd5) | refactor: move Weather and Home Assistant sources to Integrations tree group |
|
| [3c2efd5](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/3c2efd5) | refactor: move Weather and Home Assistant sources to Integrations tree group |
|
||||||
| [2153dde](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/2153dde) | feat: Home Assistant integration — WebSocket connection, automation conditions, UI |
|
| [2153dde](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/2153dde) | feat: Home Assistant integration — WebSocket connection, automation conditions, UI |
|
||||||
| [f3d07fc](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f3d07fc) | feat: donation banner, About tab, settings UI improvements |
|
| [f3d07fc](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/f3d07fc) | feat: donation banner, About tab, settings UI improvements |
|
||||||
| [f61a020](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f61a020) | feat: custom file drop zone for asset upload modal; fix review issues |
|
| [f61a020](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/f61a020) | feat: custom file drop zone for asset upload modal; fix review issues |
|
||||||
| [f345687](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f345687) | chore: remove python3.11 version pin from pre-commit config |
|
| [f345687](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/f345687) | chore: remove python3.11 version pin from pre-commit config |
|
||||||
| [e2e1107](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/e2e1107) | feat: asset-based image/video sources, notification sounds, UI improvements |
|
| [e2e1107](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/e2e1107) | feat: asset-based image/video sources, notification sounds, UI improvements |
|
||||||
| [c0853ce](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/c0853ce) | fix: improve command palette actions and automation condition button |
|
| [c0853ce](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/c0853ce) | fix: improve command palette actions and automation condition button |
|
||||||
| [3e0bf85](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/3e0bf85) | feat: add api_input LED interpolation; fix LED preview, FPS charts, dashboard layout |
|
| [3e0bf85](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/3e0bf85) | feat: add api_input LED interpolation; fix LED preview, FPS charts, dashboard layout |
|
||||||
| [be4c98b](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/be4c98b) | fix: show template name instead of ID in filter list and card badges |
|
| [be4c98b](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/be4c98b) | fix: show template name instead of ID in filter list and card badges |
|
||||||
| [dca2d21](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/dca2d21) | fix: clip graph node title and subtitle to prevent overflow |
|
| [dca2d21](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/dca2d21) | fix: clip graph node title and subtitle to prevent overflow |
|
||||||
| [53986f8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/53986f8) | fix: replace emoji with SVG icons on weather and daylight cards |
|
| [53986f8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/53986f8) | fix: replace emoji with SVG icons on weather and daylight cards |
|
||||||
| [a4a9f6f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/a4a9f6f) | fix: send gradient_id instead of palette in effect transient preview |
|
| [a4a9f6f](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/a4a9f6f) | fix: send gradient_id instead of palette in effect transient preview |
|
||||||
| [9fcfdb8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/9fcfdb8) | ci: use sparse checkout for release notes in release workflow |
|
| [9fcfdb8](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/9fcfdb8) | ci: use sparse checkout for release notes in release workflow |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
- [ ] Rename all references to "WLED" in user-facing strings, class names, module names, config keys, file paths, and documentation
|
- [ ] Rename all references to "WLED" in user-facing strings, class names, module names, config keys, file paths, and documentation
|
||||||
- [ ] The app is **LedGrab** — not tied to WLED specifically. WLED is just one of many supported output protocols
|
- [ ] The app is **LedGrab** — not tied to WLED specifically. WLED is just one of many supported output protocols
|
||||||
- [ ] Audit: i18n keys, page titles, tray labels, installer text, pyproject.toml description, README, CLAUDE.md, context files, API docs
|
- [ ] Audit: i18n keys, page titles, tray labels, installer text, pyproject.toml description, README, CLAUDE.md, context files, API docs
|
||||||
- [ ] Rename `wled_controller` package → decide on new package name (e.g. `ledgrab`)
|
- [ ] Rename `ledgrab` package → decide on new package name (e.g. `ledgrab`)
|
||||||
- [ ] Update import paths, entry points, config references, build scripts, Docker, CI/CD
|
- [ ] Update import paths, entry points, config references, build scripts, Docker, CI/CD
|
||||||
- [ ] **Migration required** if renaming storage paths or config keys (see data migration policy in CLAUDE.md)
|
- [ ] **Migration required** if renaming storage paths or config keys (see data migration policy in CLAUDE.md)
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -143,8 +143,8 @@ cleanup_site_packages() {
|
|||||||
find "$sp_dir" -name "*.$ext_suffix" -exec strip --strip-debug {} \; 2>/dev/null || true
|
find "$sp_dir" -name "*.$ext_suffix" -exec strip --strip-debug {} \; 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Remove wled_controller if pip-installed ───────────────
|
# ── Remove ledgrab if pip-installed ───────────────
|
||||||
rm -rf "$sp_dir"/wled_controller* "$sp_dir"/wled*.dist-info 2>/dev/null || true
|
rm -rf "$sp_dir"/ledgrab* "$sp_dir"/ledgrab*.dist-info 2>/dev/null || true
|
||||||
|
|
||||||
local cleaned_size
|
local cleaned_size
|
||||||
cleaned_size=$(du -sh "$sp_dir" | cut -f1)
|
cleaned_size=$(du -sh "$sp_dir" | cut -f1)
|
||||||
@@ -191,7 +191,7 @@ compile_and_strip_sources() {
|
|||||||
|
|
||||||
# ── Import smoke test ────────────────────────────────────────
|
# ── Import smoke test ────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# Verifies that every top-level dependency that wled_controller actually
|
# Verifies that every top-level dependency that ledgrab actually
|
||||||
# uses can be imported from the stripped site-packages. Catches regressions
|
# uses can be imported from the stripped site-packages. Catches regressions
|
||||||
# where cleanup_site_packages removes a submodule that turns out to be
|
# where cleanup_site_packages removes a submodule that turns out to be
|
||||||
# imported internally by the package (e.g. numpy.linalg, zeroconf._services).
|
# imported internally by the package (e.g. numpy.linalg, zeroconf._services).
|
||||||
@@ -200,7 +200,7 @@ compile_and_strip_sources() {
|
|||||||
# Args:
|
# Args:
|
||||||
# $1 — path to site-packages to test against
|
# $1 — path to site-packages to test against
|
||||||
# $2 — python executable
|
# $2 — python executable
|
||||||
# $3 — (optional) extra PYTHONPATH entry (e.g. app/src for wled_controller)
|
# $3 — (optional) extra PYTHONPATH entry (e.g. app/src for ledgrab)
|
||||||
|
|
||||||
smoke_test_imports() {
|
smoke_test_imports() {
|
||||||
local sp_dir="$1"
|
local sp_dir="$1"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ if ! grep -q 'Lib\\site-packages' "$PTH_FILE"; then
|
|||||||
echo 'Lib\site-packages' >> "$PTH_FILE"
|
echo 'Lib\site-packages' >> "$PTH_FILE"
|
||||||
fi
|
fi
|
||||||
# Embedded Python ._pth overrides PYTHONPATH, so we must add the app
|
# Embedded Python ._pth overrides PYTHONPATH, so we must add the app
|
||||||
# source directory here for wled_controller to be importable
|
# source directory here for ledgrab to be importable
|
||||||
if ! grep -q '\.\./app/src' "$PTH_FILE"; then
|
if ! grep -q '\.\./app/src' "$PTH_FILE"; then
|
||||||
echo '../app/src' >> "$PTH_FILE"
|
echo '../app/src' >> "$PTH_FILE"
|
||||||
fi
|
fi
|
||||||
@@ -325,14 +325,14 @@ cd /d "%~dp0"
|
|||||||
|
|
||||||
:: Set paths
|
:: Set paths
|
||||||
set PYTHONPATH=%~dp0app\src
|
set PYTHONPATH=%~dp0app\src
|
||||||
set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml
|
set LEDGRAB_CONFIG_PATH=%~dp0app\config\default_config.yaml
|
||||||
|
|
||||||
:: Create data directory if missing
|
:: Create data directory if missing
|
||||||
if not exist "%~dp0data" mkdir "%~dp0data"
|
if not exist "%~dp0data" mkdir "%~dp0data"
|
||||||
if not exist "%~dp0logs" mkdir "%~dp0logs"
|
if not exist "%~dp0logs" mkdir "%~dp0logs"
|
||||||
|
|
||||||
:: Start the server (tray icon handles UI and exit)
|
:: Start the server (tray icon handles UI and exit)
|
||||||
"%~dp0python\pythonw.exe" -m wled_controller
|
"%~dp0python\pythonw.exe" -m ledgrab
|
||||||
LAUNCHER
|
LAUNCHER
|
||||||
|
|
||||||
# Convert launcher to Windows line endings
|
# Convert launcher to Windows line endings
|
||||||
|
|||||||
+7
-7
@@ -58,7 +58,7 @@ if (-not $Version) {
|
|||||||
}
|
}
|
||||||
if (-not $Version) {
|
if (-not $Version) {
|
||||||
# Parse from __init__.py
|
# Parse from __init__.py
|
||||||
$initFile = Join-Path $ServerDir "src\wled_controller\__init__.py"
|
$initFile = Join-Path $ServerDir "src\ledgrab\__init__.py"
|
||||||
$match = Select-String -Path $initFile -Pattern '__version__\s*=\s*"([^"]+)"'
|
$match = Select-String -Path $initFile -Pattern '__version__\s*=\s*"([^"]+)"'
|
||||||
if ($match) { $Version = $match.Matches[0].Groups[1].Value }
|
if ($match) { $Version = $match.Matches[0].Groups[1].Value }
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ if ($pthContent -notmatch 'Lib\\site-packages') {
|
|||||||
$pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n"
|
$pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n"
|
||||||
}
|
}
|
||||||
# Embedded Python ._pth overrides PYTHONPATH, so add the app source path
|
# Embedded Python ._pth overrides PYTHONPATH, so add the app source path
|
||||||
# directly for wled_controller to be importable
|
# directly for ledgrab to be importable
|
||||||
if ($pthContent -notmatch '\.\.[/\\]app[/\\]src') {
|
if ($pthContent -notmatch '\.\.[/\\]app[/\\]src') {
|
||||||
$pthContent = $pthContent.TrimEnd() + "`n..\app\src`n"
|
$pthContent = $pthContent.TrimEnd() + "`n..\app\src`n"
|
||||||
}
|
}
|
||||||
@@ -144,10 +144,10 @@ if ($LASTEXITCODE -ne 0) {
|
|||||||
Write-Host " Some optional deps may have failed (continuing)..." -ForegroundColor Yellow
|
Write-Host " Some optional deps may have failed (continuing)..." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove the installed wled_controller package to avoid duplication
|
# Remove the installed ledgrab package to avoid duplication
|
||||||
$sitePackages = Join-Path $PythonDir "Lib\site-packages"
|
$sitePackages = Join-Path $PythonDir "Lib\site-packages"
|
||||||
Get-ChildItem -Path $sitePackages -Filter "wled*" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
Get-ChildItem -Path $sitePackages -Filter "ledgrab*" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Get-ChildItem -Path $sitePackages -Filter "wled*.dist-info" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
Get-ChildItem -Path $sitePackages -Filter "ledgrab*.dist-info" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# Clean up caches and test files to reduce size
|
# Clean up caches and test files to reduce size
|
||||||
Write-Host " Cleaning up caches..."
|
Write-Host " Cleaning up caches..."
|
||||||
@@ -206,14 +206,14 @@ cd /d "%~dp0"
|
|||||||
|
|
||||||
:: Set paths
|
:: Set paths
|
||||||
set PYTHONPATH=%~dp0app\src
|
set PYTHONPATH=%~dp0app\src
|
||||||
set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml
|
set LEDGRAB_CONFIG_PATH=%~dp0app\config\default_config.yaml
|
||||||
|
|
||||||
:: Create data directory if missing
|
:: Create data directory if missing
|
||||||
if not exist "%~dp0data" mkdir "%~dp0data"
|
if not exist "%~dp0data" mkdir "%~dp0data"
|
||||||
if not exist "%~dp0logs" mkdir "%~dp0logs"
|
if not exist "%~dp0logs" mkdir "%~dp0logs"
|
||||||
|
|
||||||
:: Start the server (tray icon handles UI and exit)
|
:: Start the server (tray icon handles UI and exit)
|
||||||
"%~dp0python\pythonw.exe" -m wled_controller
|
"%~dp0python\pythonw.exe" -m ledgrab
|
||||||
'@
|
'@
|
||||||
|
|
||||||
$launcherContent = $launcherContent -replace '%VERSION%', $VersionClean
|
$launcherContent = $launcherContent -replace '%VERSION%', $VersionClean
|
||||||
|
|||||||
+2
-2
@@ -83,12 +83,12 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
export PYTHONPATH="$SCRIPT_DIR/app/src"
|
export PYTHONPATH="$SCRIPT_DIR/app/src"
|
||||||
export WLED_CONFIG_PATH="$SCRIPT_DIR/app/config/default_config.yaml"
|
export LEDGRAB_CONFIG_PATH="$SCRIPT_DIR/app/config/default_config.yaml"
|
||||||
|
|
||||||
mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs"
|
mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/venv/bin/activate"
|
source "$SCRIPT_DIR/venv/bin/activate"
|
||||||
exec python -m wled_controller.main
|
exec python -m ledgrab.main
|
||||||
LAUNCHER
|
LAUNCHER
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh"
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## CSS Custom Properties (Variables)
|
## CSS Custom Properties (Variables)
|
||||||
|
|
||||||
Defined in `server/src/wled_controller/static/css/base.css`.
|
Defined in `server/src/ledgrab/static/css/base.css`.
|
||||||
|
|
||||||
**IMPORTANT:** There is NO `--accent` variable. Always use `--primary-color` for accent/brand color.
|
**IMPORTANT:** There is NO `--accent` variable. Always use `--primary-color` for accent/brand color.
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ Two independent server modes with separate configs, ports, and data directories:
|
|||||||
|
|
||||||
| Mode | Command | Config | Port | API Key | Data |
|
| Mode | Command | Config | Port | API Key | Data |
|
||||||
| ---- | ------- | ------ | ---- | ------- | ---- |
|
| ---- | ------- | ------ | ---- | ------- | ---- |
|
||||||
| **Real** | `python -m wled_controller` | `config/default_config.yaml` | 8080 | `development-key-change-in-production` | `data/` |
|
| **Real** | `python -m ledgrab` | `config/default_config.yaml` | 8080 | `development-key-change-in-production` | `data/` |
|
||||||
| **Demo** | `python -m wled_controller.demo` | `config/demo_config.yaml` | 8081 | `demo` | `data/demo/` |
|
| **Demo** | `python -m ledgrab.demo` | `config/demo_config.yaml` | 8081 | `demo` | `data/demo/` |
|
||||||
|
|
||||||
Demo mode can also be triggered via the `WLED_DEMO` environment variable (`true`, `1`, or `yes`). This works with any launch method — Python, Docker (`-e WLED_DEMO=true`), or the installed app (`set WLED_DEMO=true` before `LedGrab.bat`).
|
Demo mode can also be triggered via the `LEDGRAB_DEMO` environment variable (`true`, `1`, or `yes`). This works with any launch method — Python, Docker (`-e LEDGRAB_DEMO=true`), or the installed app (`set LEDGRAB_DEMO=true` before `LedGrab.bat`).
|
||||||
|
|
||||||
Both modes can run simultaneously on different ports.
|
Both modes can run simultaneously on different ports.
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ Both modes can run simultaneously on different ports.
|
|||||||
Use the PowerShell restart script — it reliably stops only the server process and starts a new detached instance:
|
Use the PowerShell restart script — it reliably stops only the server process and starts a new detached instance:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
powershell -ExecutionPolicy Bypass -File "c:\Users\Alexei\Documents\wled-screen-controller\server\restart.ps1"
|
powershell -ExecutionPolicy Bypass -File "c:\Users\Alexei\Documents\ledgrab\server\restart.ps1"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Demo server
|
### Demo server
|
||||||
@@ -35,7 +35,7 @@ powershell -Command "netstat -ano | Select-String ':8081.*LISTEN'"
|
|||||||
# Kill it
|
# Kill it
|
||||||
powershell -Command "Stop-Process -Id <PID> -Force"
|
powershell -Command "Stop-Process -Id <PID> -Force"
|
||||||
# Restart
|
# Restart
|
||||||
cd server && python -m wled_controller.demo
|
cd server && python -m ledgrab.demo
|
||||||
```
|
```
|
||||||
|
|
||||||
**Do NOT use** `Stop-Process -Name python` — it kills unrelated Python processes (VS Code extensions, etc.).
|
**Do NOT use** `Stop-Process -Name python` — it kills unrelated Python processes (VS Code extensions, etc.).
|
||||||
@@ -68,13 +68,13 @@ Auto-reload is disabled (`reload=False` in `main.py`) due to watchfiles causing
|
|||||||
2. **New capture engines**: Verify demo mode filtering works — demo engines use `is_demo_mode()` gate in `is_available()`.
|
2. **New capture engines**: Verify demo mode filtering works — demo engines use `is_demo_mode()` gate in `is_available()`.
|
||||||
3. **New audio engines**: Same as capture engines — `is_available()` must respect `is_demo_mode()`.
|
3. **New audio engines**: Same as capture engines — `is_available()` must respect `is_demo_mode()`.
|
||||||
4. **New device providers**: Gate discovery with `is_demo_mode()` like `DemoDeviceProvider.discover()`.
|
4. **New device providers**: Gate discovery with `is_demo_mode()` like `DemoDeviceProvider.discover()`.
|
||||||
5. **New seed data**: Update `server/src/wled_controller/core/demo_seed.py` to include sample entities.
|
5. **New seed data**: Update `server/src/ledgrab/core/demo_seed.py` to include sample entities.
|
||||||
6. **Frontend indicators**: Demo state exposed via `GET /api/v1/version` -> `demo_mode: bool`. Frontend stores it as `demoMode` in app state and sets `document.body.dataset.demo = 'true'`.
|
6. **Frontend indicators**: Demo state exposed via `GET /api/v1/version` -> `demo_mode: bool`. Frontend stores it as `demoMode` in app state and sets `document.body.dataset.demo = 'true'`.
|
||||||
7. **Backup/Restore**: New stores added to `STORE_MAP` in `system.py` automatically work in demo mode since the data directory is already isolated.
|
7. **Backup/Restore**: New stores added to `STORE_MAP` in `system.py` automatically work in demo mode since the data directory is already isolated.
|
||||||
|
|
||||||
### Key files
|
### Key files
|
||||||
|
|
||||||
- Config flag: `server/src/wled_controller/config.py` -> `Config.demo`, `is_demo_mode()`
|
- Config flag: `server/src/ledgrab/config.py` -> `Config.demo`, `is_demo_mode()`
|
||||||
- Demo engines: `core/capture_engines/demo_engine.py`, `core/audio/demo_engine.py`
|
- Demo engines: `core/capture_engines/demo_engine.py`, `core/audio/demo_engine.py`
|
||||||
- Demo devices: `core/devices/demo_provider.py`
|
- Demo devices: `core/devices/demo_provider.py`
|
||||||
- Seed data: `core/demo_seed.py`
|
- Seed data: `core/demo_seed.py`
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
"""The LED Screen Controller integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers import device_registry as dr
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
DOMAIN,
|
|
||||||
CONF_SERVER_NAME,
|
|
||||||
CONF_SERVER_URL,
|
|
||||||
CONF_API_KEY,
|
|
||||||
DEFAULT_SCAN_INTERVAL,
|
|
||||||
TARGET_TYPE_HA_LIGHT,
|
|
||||||
DATA_COORDINATOR,
|
|
||||||
DATA_EVENT_LISTENER,
|
|
||||||
)
|
|
||||||
from .coordinator import WLEDScreenControllerCoordinator
|
|
||||||
from .event_listener import EventStreamListener
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
|
||||||
Platform.BUTTON,
|
|
||||||
Platform.LIGHT,
|
|
||||||
Platform.SWITCH,
|
|
||||||
Platform.SENSOR,
|
|
||||||
Platform.NUMBER,
|
|
||||||
Platform.SELECT,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Set up LED Screen Controller from a config entry."""
|
|
||||||
server_name = entry.data.get(CONF_SERVER_NAME, "LED Screen Controller")
|
|
||||||
server_url = entry.data[CONF_SERVER_URL]
|
|
||||||
api_key = entry.data[CONF_API_KEY]
|
|
||||||
|
|
||||||
session = async_get_clientsession(hass)
|
|
||||||
coordinator = WLEDScreenControllerCoordinator(
|
|
||||||
hass,
|
|
||||||
session,
|
|
||||||
server_url,
|
|
||||||
api_key,
|
|
||||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
|
||||||
)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
|
||||||
|
|
||||||
event_listener = EventStreamListener(hass, server_url, api_key, coordinator)
|
|
||||||
await event_listener.start()
|
|
||||||
|
|
||||||
# Create device entries for each target and remove stale ones
|
|
||||||
device_registry = dr.async_get(hass)
|
|
||||||
current_identifiers: set[tuple[str, str]] = set()
|
|
||||||
if coordinator.data and "targets" in coordinator.data:
|
|
||||||
for target_id, target_data in coordinator.data["targets"].items():
|
|
||||||
info = target_data["info"]
|
|
||||||
target_type = info.get("target_type", "led")
|
|
||||||
if target_type == TARGET_TYPE_HA_LIGHT:
|
|
||||||
model = "HA Light Target"
|
|
||||||
else:
|
|
||||||
model = "LED Target"
|
|
||||||
device_registry.async_get_or_create(
|
|
||||||
config_entry_id=entry.entry_id,
|
|
||||||
identifiers={(DOMAIN, target_id)},
|
|
||||||
name=info.get("name", target_id),
|
|
||||||
manufacturer=server_name,
|
|
||||||
model=model,
|
|
||||||
configuration_url=server_url,
|
|
||||||
)
|
|
||||||
current_identifiers.add((DOMAIN, target_id))
|
|
||||||
|
|
||||||
# Create a single "Scenes" device for scene preset buttons
|
|
||||||
scenes_identifier = (DOMAIN, f"{entry.entry_id}_scenes")
|
|
||||||
scene_presets = coordinator.data.get("scene_presets", []) if coordinator.data else []
|
|
||||||
if scene_presets:
|
|
||||||
device_registry.async_get_or_create(
|
|
||||||
config_entry_id=entry.entry_id,
|
|
||||||
identifiers={scenes_identifier},
|
|
||||||
name="Scenes",
|
|
||||||
manufacturer=server_name,
|
|
||||||
model="Scene Presets",
|
|
||||||
configuration_url=server_url,
|
|
||||||
)
|
|
||||||
current_identifiers.add(scenes_identifier)
|
|
||||||
|
|
||||||
# Remove devices for targets that no longer exist
|
|
||||||
for device_entry in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
|
|
||||||
if not device_entry.identifiers & current_identifiers:
|
|
||||||
_LOGGER.info("Removing stale device: %s", device_entry.name)
|
|
||||||
device_registry.async_remove_device(device_entry.id)
|
|
||||||
|
|
||||||
# Store data
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = {
|
|
||||||
DATA_COORDINATOR: coordinator,
|
|
||||||
DATA_EVENT_LISTENER: event_listener,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Track target and scene IDs to detect changes
|
|
||||||
known_target_ids = set(coordinator.data.get("targets", {}).keys() if coordinator.data else [])
|
|
||||||
known_scene_ids = set(
|
|
||||||
p["id"] for p in (coordinator.data.get("scene_presets", []) if coordinator.data else [])
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_coordinator_update() -> None:
|
|
||||||
"""Detect target/scene list changes and trigger reload."""
|
|
||||||
nonlocal known_target_ids, known_scene_ids
|
|
||||||
|
|
||||||
if not coordinator.data:
|
|
||||||
return
|
|
||||||
|
|
||||||
targets = coordinator.data.get("targets", {})
|
|
||||||
|
|
||||||
# Reload if target or scene list changed
|
|
||||||
current_ids = set(targets.keys())
|
|
||||||
current_scene_ids = set(p["id"] for p in coordinator.data.get("scene_presets", []))
|
|
||||||
if current_ids != known_target_ids or current_scene_ids != known_scene_ids:
|
|
||||||
known_target_ids = current_ids
|
|
||||||
known_scene_ids = current_scene_ids
|
|
||||||
_LOGGER.info("Target or scene list changed, reloading integration")
|
|
||||||
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
|
|
||||||
|
|
||||||
coordinator.async_add_listener(_on_coordinator_update)
|
|
||||||
|
|
||||||
# Register set_leds service (once across all entries)
|
|
||||||
async def handle_set_leds(call) -> None:
|
|
||||||
"""Handle the set_leds service call."""
|
|
||||||
source_id = call.data["source_id"]
|
|
||||||
segments = call.data["segments"]
|
|
||||||
# Route to the coordinator that owns this source
|
|
||||||
for entry_data in hass.data[DOMAIN].values():
|
|
||||||
coord = entry_data.get(DATA_COORDINATOR)
|
|
||||||
if not coord or not coord.data:
|
|
||||||
continue
|
|
||||||
source_ids = {s["id"] for s in coord.data.get("css_sources", [])}
|
|
||||||
if source_id in source_ids:
|
|
||||||
await coord.push_segments(source_id, segments)
|
|
||||||
return
|
|
||||||
_LOGGER.error("No server found with source_id %s", source_id)
|
|
||||||
|
|
||||||
if not hass.services.has_service(DOMAIN, "set_leds"):
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"set_leds",
|
|
||||||
handle_set_leds,
|
|
||||||
schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required("source_id"): str,
|
|
||||||
vol.Required("segments"): list,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
await entry_data[DATA_EVENT_LISTENER].shutdown()
|
|
||||||
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
# Unregister service if no entries remain
|
|
||||||
if not hass.data[DOMAIN]:
|
|
||||||
hass.services.async_remove(DOMAIN, "set_leds")
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
"""Button platform for LED Screen Controller — scene preset activation."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN, DATA_COORDINATOR
|
|
||||||
from .coordinator import WLEDScreenControllerCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up scene preset buttons."""
|
|
||||||
data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
|
||||||
|
|
||||||
entities = []
|
|
||||||
if coordinator.data:
|
|
||||||
for preset in coordinator.data.get("scene_presets", []):
|
|
||||||
entities.append(
|
|
||||||
SceneActivateButton(coordinator, preset, entry.entry_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class SceneActivateButton(CoordinatorEntity, ButtonEntity):
|
|
||||||
"""Button that activates a scene preset."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
preset: dict[str, Any],
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the button."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._preset_id = preset["id"]
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{entry_id}_scene_{preset['id']}"
|
|
||||||
self._attr_translation_key = "activate_scene"
|
|
||||||
self._attr_translation_placeholders = {"scene_name": preset["name"]}
|
|
||||||
self._attr_icon = "mdi:palette"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
"""Return device information — all scene buttons belong to the Scenes device."""
|
|
||||||
return {"identifiers": {(DOMAIN, f"{self._entry_id}_scenes")}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return False
|
|
||||||
return self._preset_id in {
|
|
||||||
p["id"] for p in self.coordinator.data.get("scene_presets", [])
|
|
||||||
}
|
|
||||||
|
|
||||||
async def async_press(self) -> None:
|
|
||||||
"""Activate the scene preset."""
|
|
||||||
await self.coordinator.activate_scene(self._preset_id)
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
"""Config flow for LED Screen Controller integration."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
from urllib.parse import urlparse, urlunparse
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import DOMAIN, CONF_SERVER_NAME, CONF_SERVER_URL, CONF_API_KEY, DEFAULT_TIMEOUT
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(CONF_SERVER_NAME, default="LED Screen Controller"): str,
|
|
||||||
vol.Required(CONF_SERVER_URL, default="http://localhost:8080"): str,
|
|
||||||
vol.Optional(CONF_API_KEY, default=""): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_url(url: str) -> str:
|
|
||||||
"""Normalize URL to ensure port is an integer."""
|
|
||||||
parsed = urlparse(url)
|
|
||||||
|
|
||||||
if parsed.port is not None:
|
|
||||||
netloc = parsed.hostname or "localhost"
|
|
||||||
port = int(parsed.port)
|
|
||||||
if port != (443 if parsed.scheme == "https" else 80):
|
|
||||||
netloc = f"{netloc}:{port}"
|
|
||||||
parsed = parsed._replace(netloc=netloc)
|
|
||||||
|
|
||||||
return urlunparse(parsed)
|
|
||||||
|
|
||||||
|
|
||||||
async def validate_server(
|
|
||||||
hass: HomeAssistant, server_url: str, api_key: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Validate server connectivity and API key."""
|
|
||||||
session = async_get_clientsession(hass)
|
|
||||||
timeout = aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT)
|
|
||||||
|
|
||||||
# Step 1: Check connectivity via health endpoint (no auth needed)
|
|
||||||
try:
|
|
||||||
async with session.get(f"{server_url}/health", timeout=timeout) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
raise ConnectionError(f"Server returned status {resp.status}")
|
|
||||||
data = await resp.json()
|
|
||||||
version = data.get("version", "unknown")
|
|
||||||
except aiohttp.ClientError as err:
|
|
||||||
raise ConnectionError(f"Cannot connect to server: {err}") from err
|
|
||||||
|
|
||||||
# Step 2: Validate API key via authenticated endpoint (skip if no key and auth not required)
|
|
||||||
auth_required = data.get("auth_required", True)
|
|
||||||
if api_key:
|
|
||||||
headers = {"Authorization": f"Bearer {api_key}"}
|
|
||||||
try:
|
|
||||||
async with session.get(
|
|
||||||
f"{server_url}/api/v1/output-targets",
|
|
||||||
headers=headers,
|
|
||||||
timeout=timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status == 401:
|
|
||||||
raise PermissionError("Invalid API key")
|
|
||||||
resp.raise_for_status()
|
|
||||||
except PermissionError:
|
|
||||||
raise
|
|
||||||
except aiohttp.ClientError as err:
|
|
||||||
raise ConnectionError(f"API request failed: {err}") from err
|
|
||||||
elif auth_required:
|
|
||||||
raise PermissionError("Server requires an API key")
|
|
||||||
|
|
||||||
return {"version": version}
|
|
||||||
|
|
||||||
|
|
||||||
class WLEDScreenControllerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for LED Screen Controller."""
|
|
||||||
|
|
||||||
VERSION = 2
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle the initial step."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
server_name = user_input.get(CONF_SERVER_NAME, "LED Screen Controller")
|
|
||||||
server_url = normalize_url(user_input[CONF_SERVER_URL].rstrip("/"))
|
|
||||||
api_key = user_input[CONF_API_KEY]
|
|
||||||
|
|
||||||
try:
|
|
||||||
await validate_server(self.hass, server_url, api_key)
|
|
||||||
|
|
||||||
await self.async_set_unique_id(server_url)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=server_name,
|
|
||||||
data={
|
|
||||||
CONF_SERVER_NAME: server_name,
|
|
||||||
CONF_SERVER_URL: server_url,
|
|
||||||
CONF_API_KEY: api_key,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
except ConnectionError as err:
|
|
||||||
_LOGGER.error("Connection error: %s", err)
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except PermissionError:
|
|
||||||
errors["base"] = "invalid_api_key"
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.exception("Unexpected exception: %s", err)
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
"""Constants for the LED Screen Controller integration."""
|
|
||||||
|
|
||||||
DOMAIN = "wled_screen_controller"
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
CONF_SERVER_NAME = "server_name"
|
|
||||||
CONF_SERVER_URL = "server_url"
|
|
||||||
CONF_API_KEY = "api_key"
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
DEFAULT_SCAN_INTERVAL = 3 # seconds
|
|
||||||
DEFAULT_TIMEOUT = 10 # seconds
|
|
||||||
WS_RECONNECT_DELAY = 5 # seconds
|
|
||||||
WS_MAX_RECONNECT_DELAY = 60 # seconds
|
|
||||||
|
|
||||||
# Target types
|
|
||||||
TARGET_TYPE_LED = "led"
|
|
||||||
TARGET_TYPE_HA_LIGHT = "ha_light"
|
|
||||||
|
|
||||||
# Data keys stored in hass.data[DOMAIN][entry_id]
|
|
||||||
DATA_COORDINATOR = "coordinator"
|
|
||||||
DATA_EVENT_LISTENER = "event_listener"
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
"""Data update coordinator for LED Screen Controller."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
DOMAIN,
|
|
||||||
DEFAULT_TIMEOUT,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|
||||||
"""Class to manage fetching LED Screen Controller data."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
session: aiohttp.ClientSession,
|
|
||||||
server_url: str,
|
|
||||||
api_key: str,
|
|
||||||
update_interval: timedelta,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the coordinator."""
|
|
||||||
self.server_url = server_url
|
|
||||||
self.session = session
|
|
||||||
self.api_key = api_key
|
|
||||||
self.server_version = "unknown"
|
|
||||||
self._auth_headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
|
||||||
self._timeout = aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name=DOMAIN,
|
|
||||||
update_interval=update_interval,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, Any]:
|
|
||||||
"""Fetch data from API."""
|
|
||||||
try:
|
|
||||||
async with asyncio.timeout(DEFAULT_TIMEOUT * 3):
|
|
||||||
if self.server_version == "unknown":
|
|
||||||
await self._fetch_server_version()
|
|
||||||
|
|
||||||
targets_list = await self._fetch_targets()
|
|
||||||
|
|
||||||
# Fetch state and metrics for all targets in parallel
|
|
||||||
targets_data: dict[str, dict[str, Any]] = {}
|
|
||||||
|
|
||||||
async def fetch_target_data(target: dict) -> tuple[str, dict]:
|
|
||||||
target_id = target["id"]
|
|
||||||
try:
|
|
||||||
state, metrics = await asyncio.gather(
|
|
||||||
self._fetch_target_state(target_id),
|
|
||||||
self._fetch_target_metrics(target_id),
|
|
||||||
)
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Failed to fetch data for target %s: %s",
|
|
||||||
target_id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
state = None
|
|
||||||
metrics = None
|
|
||||||
|
|
||||||
return target_id, {
|
|
||||||
"info": target,
|
|
||||||
"state": state,
|
|
||||||
"metrics": metrics,
|
|
||||||
}
|
|
||||||
|
|
||||||
results = await asyncio.gather(
|
|
||||||
*(fetch_target_data(t) for t in targets_list),
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for r in results:
|
|
||||||
if isinstance(r, Exception):
|
|
||||||
_LOGGER.warning("Target fetch failed: %s", r)
|
|
||||||
continue
|
|
||||||
target_id, data = r
|
|
||||||
targets_data[target_id] = data
|
|
||||||
|
|
||||||
# Fetch devices, CSS sources, value sources, and scene presets in parallel
|
|
||||||
devices_data, css_sources, value_sources, scene_presets = await asyncio.gather(
|
|
||||||
self._fetch_devices(),
|
|
||||||
self._fetch_css_sources(),
|
|
||||||
self._fetch_value_sources(),
|
|
||||||
self._fetch_scene_presets(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"targets": targets_data,
|
|
||||||
"devices": devices_data,
|
|
||||||
"css_sources": css_sources,
|
|
||||||
"value_sources": value_sources,
|
|
||||||
"scene_presets": scene_presets,
|
|
||||||
"server_version": self.server_version,
|
|
||||||
}
|
|
||||||
|
|
||||||
except asyncio.TimeoutError as err:
|
|
||||||
raise UpdateFailed(f"Timeout fetching data: {err}") from err
|
|
||||||
except aiohttp.ClientError as err:
|
|
||||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
|
||||||
|
|
||||||
async def _fetch_server_version(self) -> None:
|
|
||||||
"""Fetch server version from health endpoint."""
|
|
||||||
try:
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/health",
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = await resp.json()
|
|
||||||
self.server_version = data.get("version", "unknown")
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.warning("Failed to fetch server version: %s", err)
|
|
||||||
self.server_version = "unknown"
|
|
||||||
|
|
||||||
async def _fetch_targets(self) -> list[dict[str, Any]]:
|
|
||||||
"""Fetch all output targets."""
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/api/v1/output-targets",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = await resp.json()
|
|
||||||
return data.get("targets", [])
|
|
||||||
|
|
||||||
async def _fetch_target_state(self, target_id: str) -> dict[str, Any]:
|
|
||||||
"""Fetch target processing state."""
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}/state",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
resp.raise_for_status()
|
|
||||||
return await resp.json()
|
|
||||||
|
|
||||||
async def _fetch_target_metrics(self, target_id: str) -> dict[str, Any]:
|
|
||||||
"""Fetch target metrics."""
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}/metrics",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
resp.raise_for_status()
|
|
||||||
return await resp.json()
|
|
||||||
|
|
||||||
async def _fetch_devices(self) -> dict[str, dict[str, Any]]:
|
|
||||||
"""Fetch all devices with capabilities and brightness."""
|
|
||||||
try:
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/api/v1/devices",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = await resp.json()
|
|
||||||
devices = data.get("devices", [])
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.warning("Failed to fetch devices: %s", err)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Fetch brightness for all capable devices in parallel
|
|
||||||
async def fetch_device_entry(device: dict) -> tuple[str, dict[str, Any]]:
|
|
||||||
device_id = device["id"]
|
|
||||||
entry: dict[str, Any] = {"info": device, "brightness": None}
|
|
||||||
if "brightness_control" in (device.get("capabilities") or []):
|
|
||||||
try:
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/api/v1/devices/{device_id}/brightness",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status == 200:
|
|
||||||
bri_data = await resp.json()
|
|
||||||
entry["brightness"] = bri_data.get("brightness")
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Failed to fetch brightness for device %s: %s",
|
|
||||||
device_id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
return device_id, entry
|
|
||||||
|
|
||||||
results = await asyncio.gather(
|
|
||||||
*(fetch_device_entry(d) for d in devices),
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
devices_data: dict[str, dict[str, Any]] = {}
|
|
||||||
for r in results:
|
|
||||||
if isinstance(r, Exception):
|
|
||||||
_LOGGER.warning("Device fetch failed: %s", r)
|
|
||||||
continue
|
|
||||||
device_id, entry = r
|
|
||||||
devices_data[device_id] = entry
|
|
||||||
|
|
||||||
return devices_data
|
|
||||||
|
|
||||||
async def set_brightness(self, device_id: str, brightness: int) -> None:
|
|
||||||
"""Set brightness for a device."""
|
|
||||||
async with self.session.put(
|
|
||||||
f"{self.server_url}/api/v1/devices/{device_id}/brightness",
|
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
|
||||||
json={"brightness": brightness},
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to set brightness for device %s: %s %s",
|
|
||||||
device_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
await self.async_request_refresh()
|
|
||||||
|
|
||||||
async def set_color(self, device_id: str, color: list[int] | None) -> None:
|
|
||||||
"""Set or clear the static color for a device."""
|
|
||||||
async with self.session.put(
|
|
||||||
f"{self.server_url}/api/v1/devices/{device_id}/color",
|
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
|
||||||
json={"color": color},
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to set color for device %s: %s %s",
|
|
||||||
device_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
await self.async_request_refresh()
|
|
||||||
|
|
||||||
async def _fetch_css_sources(self) -> list[dict[str, Any]]:
|
|
||||||
"""Fetch all color strip sources."""
|
|
||||||
try:
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/api/v1/color-strip-sources",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = await resp.json()
|
|
||||||
return data.get("sources", [])
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.warning("Failed to fetch CSS sources: %s", err)
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _fetch_value_sources(self) -> list[dict[str, Any]]:
|
|
||||||
"""Fetch all value sources."""
|
|
||||||
try:
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/api/v1/value-sources",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = await resp.json()
|
|
||||||
return data.get("sources", [])
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.warning("Failed to fetch value sources: %s", err)
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def _fetch_scene_presets(self) -> list[dict[str, Any]]:
|
|
||||||
"""Fetch all scene presets."""
|
|
||||||
try:
|
|
||||||
async with self.session.get(
|
|
||||||
f"{self.server_url}/api/v1/scene-presets",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = await resp.json()
|
|
||||||
return data.get("presets", [])
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.warning("Failed to fetch scene presets: %s", err)
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def push_colors(self, source_id: str, colors: list[list[int]]) -> None:
|
|
||||||
"""Push flat color array to an api_input CSS source."""
|
|
||||||
async with self.session.post(
|
|
||||||
f"{self.server_url}/api/v1/color-strip-sources/{source_id}/colors",
|
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
|
||||||
json={"colors": colors},
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status not in (200, 204):
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to push colors to source %s: %s %s",
|
|
||||||
source_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
async def push_segments(self, source_id: str, segments: list[dict]) -> None:
|
|
||||||
"""Push segment data to an api_input CSS source."""
|
|
||||||
async with self.session.post(
|
|
||||||
f"{self.server_url}/api/v1/color-strip-sources/{source_id}/colors",
|
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
|
||||||
json={"segments": segments},
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status not in (200, 204):
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to push segments to source %s: %s %s",
|
|
||||||
source_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
async def activate_scene(self, preset_id: str) -> None:
|
|
||||||
"""Activate a scene preset."""
|
|
||||||
async with self.session.post(
|
|
||||||
f"{self.server_url}/api/v1/scene-presets/{preset_id}/activate",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to activate scene %s: %s %s",
|
|
||||||
preset_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
await self.async_request_refresh()
|
|
||||||
|
|
||||||
async def update_source(self, source_id: str, **kwargs: Any) -> None:
|
|
||||||
"""Update a color strip source's fields."""
|
|
||||||
async with self.session.put(
|
|
||||||
f"{self.server_url}/api/v1/color-strip-sources/{source_id}",
|
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
|
||||||
json=kwargs,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to update source %s: %s %s",
|
|
||||||
source_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
async def update_target(self, target_id: str, **kwargs: Any) -> None:
|
|
||||||
"""Update an output target's fields."""
|
|
||||||
async with self.session.put(
|
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}",
|
|
||||||
headers={**self._auth_headers, "Content-Type": "application/json"},
|
|
||||||
json=kwargs,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status != 200:
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to update target %s: %s %s",
|
|
||||||
target_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
await self.async_request_refresh()
|
|
||||||
|
|
||||||
async def start_processing(self, target_id: str) -> None:
|
|
||||||
"""Start processing for a target."""
|
|
||||||
async with self.session.post(
|
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}/start",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status == 409:
|
|
||||||
_LOGGER.debug("Target %s already processing", target_id)
|
|
||||||
elif resp.status != 200:
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to start target %s: %s %s",
|
|
||||||
target_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
await self.async_request_refresh()
|
|
||||||
|
|
||||||
async def stop_processing(self, target_id: str) -> None:
|
|
||||||
"""Stop processing for a target."""
|
|
||||||
async with self.session.post(
|
|
||||||
f"{self.server_url}/api/v1/output-targets/{target_id}/stop",
|
|
||||||
headers=self._auth_headers,
|
|
||||||
timeout=self._timeout,
|
|
||||||
) as resp:
|
|
||||||
if resp.status == 409:
|
|
||||||
_LOGGER.debug("Target %s already stopped", target_id)
|
|
||||||
elif resp.status != 200:
|
|
||||||
body = await resp.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to stop target %s: %s %s",
|
|
||||||
target_id,
|
|
||||||
resp.status,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
await self.async_request_refresh()
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
"""WebSocket event listener for server state change notifications."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import contextlib
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import WS_RECONNECT_DELAY, WS_MAX_RECONNECT_DELAY
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class EventStreamListener:
|
|
||||||
"""Listens to server WS endpoint for state change events.
|
|
||||||
|
|
||||||
Triggers a coordinator refresh whenever a target starts or stops processing,
|
|
||||||
so HAOS entities react near-instantly to external state changes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
server_url: str,
|
|
||||||
api_key: str,
|
|
||||||
coordinator: DataUpdateCoordinator,
|
|
||||||
) -> None:
|
|
||||||
self._hass = hass
|
|
||||||
self._server_url = server_url
|
|
||||||
self._api_key = api_key
|
|
||||||
self._coordinator = coordinator
|
|
||||||
self._task: asyncio.Task | None = None
|
|
||||||
self._shutting_down = False
|
|
||||||
|
|
||||||
async def start(self) -> None:
|
|
||||||
"""Start listening to the event stream."""
|
|
||||||
self._task = self._hass.async_create_background_task(
|
|
||||||
self._ws_loop(),
|
|
||||||
"wled_screen_controller_events",
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _ws_loop(self) -> None:
|
|
||||||
"""WebSocket connection loop with reconnection."""
|
|
||||||
delay = WS_RECONNECT_DELAY
|
|
||||||
session = async_get_clientsession(self._hass)
|
|
||||||
ws_base = self._server_url.replace("http://", "ws://").replace(
|
|
||||||
"https://", "wss://"
|
|
||||||
)
|
|
||||||
url = f"{ws_base}/api/v1/events/ws?token={self._api_key}"
|
|
||||||
|
|
||||||
while not self._shutting_down:
|
|
||||||
try:
|
|
||||||
async with session.ws_connect(url) as ws:
|
|
||||||
delay = WS_RECONNECT_DELAY # reset on successful connect
|
|
||||||
_LOGGER.debug("Event stream connected")
|
|
||||||
async for msg in ws:
|
|
||||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
||||||
try:
|
|
||||||
data = json.loads(msg.data)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
if data.get("type") == "state_change":
|
|
||||||
await self._coordinator.async_request_refresh()
|
|
||||||
elif msg.type in (
|
|
||||||
aiohttp.WSMsgType.CLOSED,
|
|
||||||
aiohttp.WSMsgType.ERROR,
|
|
||||||
):
|
|
||||||
break
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
raise
|
|
||||||
except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as err:
|
|
||||||
_LOGGER.debug("Event stream connection error: %s", err)
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Unexpected event stream error: %s", err)
|
|
||||||
|
|
||||||
if self._shutting_down:
|
|
||||||
break
|
|
||||||
|
|
||||||
await asyncio.sleep(delay)
|
|
||||||
delay = min(delay * 2, WS_MAX_RECONNECT_DELAY)
|
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
|
||||||
"""Stop listening."""
|
|
||||||
self._shutting_down = True
|
|
||||||
if self._task:
|
|
||||||
self._task.cancel()
|
|
||||||
with contextlib.suppress(asyncio.CancelledError):
|
|
||||||
await self._task
|
|
||||||
self._task = None
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
"""Light platform for LED Screen Controller (api_input CSS sources)."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
|
||||||
ATTR_BRIGHTNESS,
|
|
||||||
ATTR_RGB_COLOR,
|
|
||||||
ColorMode,
|
|
||||||
LightEntity,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN, DATA_COORDINATOR
|
|
||||||
from .coordinator import WLEDScreenControllerCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up LED Screen Controller api_input lights."""
|
|
||||||
data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
|
||||||
|
|
||||||
entities = []
|
|
||||||
if coordinator.data:
|
|
||||||
for source in coordinator.data.get("css_sources", []):
|
|
||||||
if source.get("source_type") == "api_input":
|
|
||||||
entities.append(
|
|
||||||
ApiInputLight(coordinator, source, entry.entry_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class ApiInputLight(CoordinatorEntity, LightEntity):
|
|
||||||
"""Representation of an api_input CSS source as a light entity."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_color_mode = ColorMode.RGB
|
|
||||||
_attr_supported_color_modes = {ColorMode.RGB}
|
|
||||||
_attr_translation_key = "api_input_light"
|
|
||||||
_attr_icon = "mdi:led-strip-variant"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
source: dict[str, Any],
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the light."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._source_id: str = source["id"]
|
|
||||||
self._source_name: str = source.get("name", self._source_id)
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{self._source_id}_light"
|
|
||||||
|
|
||||||
# Restore state from fallback_color
|
|
||||||
fallback = self._get_fallback_color()
|
|
||||||
is_off = fallback == [0, 0, 0]
|
|
||||||
self._is_on: bool = not is_off
|
|
||||||
self._rgb_color: tuple[int, int, int] = (
|
|
||||||
(255, 255, 255) if is_off else tuple(fallback) # type: ignore[arg-type]
|
|
||||||
)
|
|
||||||
self._brightness: int = 255
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
"""Return device information — one virtual device per api_input source."""
|
|
||||||
return {
|
|
||||||
"identifiers": {(DOMAIN, self._source_id)},
|
|
||||||
"name": self._source_name,
|
|
||||||
"manufacturer": "WLED Screen Controller",
|
|
||||||
"model": "API Input CSS Source",
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
"""Return the entity name."""
|
|
||||||
return self._source_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Return true if the light is on."""
|
|
||||||
return self._is_on
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rgb_color(self) -> tuple[int, int, int]:
|
|
||||||
"""Return the current RGB color."""
|
|
||||||
return self._rgb_color
|
|
||||||
|
|
||||||
@property
|
|
||||||
def brightness(self) -> int:
|
|
||||||
"""Return the current brightness (0-255)."""
|
|
||||||
return self._brightness
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn on the light, optionally setting color and brightness."""
|
|
||||||
if ATTR_RGB_COLOR in kwargs:
|
|
||||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
|
||||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
|
||||||
|
|
||||||
# Scale RGB by brightness
|
|
||||||
scale = self._brightness / 255
|
|
||||||
r, g, b = self._rgb_color
|
|
||||||
scaled = [round(r * scale), round(g * scale), round(b * scale)]
|
|
||||||
|
|
||||||
await self.coordinator.push_segments(
|
|
||||||
self._source_id,
|
|
||||||
[{"start": 0, "length": 9999, "mode": "solid", "color": scaled}],
|
|
||||||
)
|
|
||||||
# Update fallback_color so the color persists beyond the timeout
|
|
||||||
await self.coordinator.update_source(
|
|
||||||
self._source_id, fallback_color=scaled,
|
|
||||||
)
|
|
||||||
self._is_on = True
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn off the light by pushing black and setting fallback to black."""
|
|
||||||
off_color = [0, 0, 0]
|
|
||||||
await self.coordinator.push_segments(
|
|
||||||
self._source_id,
|
|
||||||
[{"start": 0, "length": 9999, "mode": "solid", "color": off_color}],
|
|
||||||
)
|
|
||||||
await self.coordinator.update_source(
|
|
||||||
self._source_id, fallback_color=off_color,
|
|
||||||
)
|
|
||||||
self._is_on = False
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
def _get_fallback_color(self) -> list[int]:
|
|
||||||
"""Read fallback_color from the source config in coordinator data."""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return [0, 0, 0]
|
|
||||||
for source in self.coordinator.data.get("css_sources", []):
|
|
||||||
if source.get("id") == self._source_id:
|
|
||||||
fallback = source.get("fallback_color")
|
|
||||||
if fallback and len(fallback) >= 3:
|
|
||||||
return list(fallback[:3])
|
|
||||||
break
|
|
||||||
return [0, 0, 0]
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "wled_screen_controller",
|
|
||||||
"name": "LED Screen Controller",
|
|
||||||
"codeowners": ["@alexeidolgolyov"],
|
|
||||||
"config_flow": true,
|
|
||||||
"dependencies": [],
|
|
||||||
"documentation": "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed",
|
|
||||||
"iot_class": "local_push",
|
|
||||||
"issue_tracker": "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/issues",
|
|
||||||
"requirements": ["aiohttp>=3.9.0"],
|
|
||||||
"version": "0.2.0"
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
"""Number platform for LED Screen Controller (device brightness & HA light settings)."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity, NumberMode
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN, DATA_COORDINATOR, TARGET_TYPE_HA_LIGHT
|
|
||||||
from .coordinator import WLEDScreenControllerCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up LED Screen Controller number entities."""
|
|
||||||
data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
|
||||||
|
|
||||||
entities: list[NumberEntity] = []
|
|
||||||
if coordinator.data and "targets" in coordinator.data:
|
|
||||||
devices = coordinator.data.get("devices") or {}
|
|
||||||
|
|
||||||
for target_id, target_data in coordinator.data["targets"].items():
|
|
||||||
info = target_data["info"]
|
|
||||||
target_type = info.get("target_type", "led")
|
|
||||||
|
|
||||||
if target_type == TARGET_TYPE_HA_LIGHT:
|
|
||||||
# HA Light target — expose tunable settings
|
|
||||||
entities.append(HALightUpdateRate(coordinator, target_id, entry.entry_id))
|
|
||||||
entities.append(HALightTransition(coordinator, target_id, entry.entry_id))
|
|
||||||
entities.append(HALightMinBrightness(coordinator, target_id, entry.entry_id))
|
|
||||||
entities.append(HALightColorTolerance(coordinator, target_id, entry.entry_id))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# LED target — brightness lives on the device
|
|
||||||
device_id = info.get("device_id", "")
|
|
||||||
if not device_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
device_data = devices.get(device_id)
|
|
||||||
if not device_data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
capabilities = device_data.get("info", {}).get("capabilities") or []
|
|
||||||
if "brightness_control" not in capabilities or "static_color" in capabilities:
|
|
||||||
continue
|
|
||||||
|
|
||||||
entities.append(
|
|
||||||
WLEDScreenControllerBrightness(
|
|
||||||
coordinator,
|
|
||||||
target_id,
|
|
||||||
device_id,
|
|
||||||
entry.entry_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class WLEDScreenControllerBrightness(CoordinatorEntity, NumberEntity):
|
|
||||||
"""Brightness control for an LED device associated with a target."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_native_min_value = 0
|
|
||||||
_attr_native_max_value = 255
|
|
||||||
_attr_native_step = 1
|
|
||||||
_attr_mode = NumberMode.SLIDER
|
|
||||||
_attr_icon = "mdi:brightness-6"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
device_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the brightness number."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._device_id = device_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{target_id}_brightness"
|
|
||||||
self._attr_translation_key = "brightness"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
"""Return device information."""
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float | None:
|
|
||||||
"""Return the current brightness value."""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
device_data = self.coordinator.data.get("devices", {}).get(self._device_id)
|
|
||||||
if not device_data:
|
|
||||||
return None
|
|
||||||
return device_data.get("brightness")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return False
|
|
||||||
targets = self.coordinator.data.get("targets", {})
|
|
||||||
devices = self.coordinator.data.get("devices", {})
|
|
||||||
return self._target_id in targets and self._device_id in devices
|
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
|
||||||
"""Set brightness value."""
|
|
||||||
await self.coordinator.set_brightness(self._device_id, int(value))
|
|
||||||
|
|
||||||
|
|
||||||
# --- HA Light target number entities ---
|
|
||||||
|
|
||||||
|
|
||||||
class _HALightNumberBase(CoordinatorEntity, NumberEntity):
|
|
||||||
"""Base class for HA Light target number entities."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_mode = NumberMode.SLIDER
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
*,
|
|
||||||
field_name: str,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._field_name = field_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float | None:
|
|
||||||
target_data = self._get_target_data()
|
|
||||||
if not target_data:
|
|
||||||
return None
|
|
||||||
return target_data.get("info", {}).get(self._field_name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
return self._get_target_data() is not None
|
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
|
||||||
await self.coordinator.update_target(self._target_id, **{self._field_name: round(value, 2)})
|
|
||||||
|
|
||||||
def _get_target_data(self) -> dict[str, Any] | None:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
return self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
||||||
|
|
||||||
|
|
||||||
class HALightUpdateRate(_HALightNumberBase):
|
|
||||||
"""Update rate (Hz) for an HA Light target."""
|
|
||||||
|
|
||||||
_attr_native_min_value = 0.5
|
|
||||||
_attr_native_max_value = 5.0
|
|
||||||
_attr_native_step = 0.5
|
|
||||||
_attr_native_unit_of_measurement = "Hz"
|
|
||||||
_attr_icon = "mdi:update"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, coordinator: WLEDScreenControllerCoordinator, target_id: str, entry_id: str
|
|
||||||
) -> None:
|
|
||||||
super().__init__(coordinator, target_id, entry_id, field_name="update_rate")
|
|
||||||
self._attr_unique_id = f"{target_id}_update_rate"
|
|
||||||
self._attr_translation_key = "ha_light_update_rate"
|
|
||||||
|
|
||||||
|
|
||||||
class HALightTransition(_HALightNumberBase):
|
|
||||||
"""Transition time (seconds) for an HA Light target."""
|
|
||||||
|
|
||||||
_attr_native_min_value = 0.0
|
|
||||||
_attr_native_max_value = 10.0
|
|
||||||
_attr_native_step = 0.1
|
|
||||||
_attr_native_unit_of_measurement = "s"
|
|
||||||
_attr_icon = "mdi:transition-masked"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, coordinator: WLEDScreenControllerCoordinator, target_id: str, entry_id: str
|
|
||||||
) -> None:
|
|
||||||
super().__init__(coordinator, target_id, entry_id, field_name="transition")
|
|
||||||
self._attr_unique_id = f"{target_id}_transition"
|
|
||||||
self._attr_translation_key = "ha_light_transition"
|
|
||||||
|
|
||||||
|
|
||||||
class HALightMinBrightness(_HALightNumberBase):
|
|
||||||
"""Minimum brightness threshold for an HA Light target."""
|
|
||||||
|
|
||||||
_attr_native_min_value = 0
|
|
||||||
_attr_native_max_value = 255
|
|
||||||
_attr_native_step = 1
|
|
||||||
_attr_icon = "mdi:brightness-4"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, coordinator: WLEDScreenControllerCoordinator, target_id: str, entry_id: str
|
|
||||||
) -> None:
|
|
||||||
super().__init__(coordinator, target_id, entry_id, field_name="min_brightness_threshold")
|
|
||||||
self._attr_unique_id = f"{target_id}_min_brightness"
|
|
||||||
self._attr_translation_key = "ha_light_min_brightness"
|
|
||||||
|
|
||||||
|
|
||||||
class HALightColorTolerance(_HALightNumberBase):
|
|
||||||
"""Color tolerance (RGB delta skip threshold) for an HA Light target."""
|
|
||||||
|
|
||||||
_attr_native_min_value = 0
|
|
||||||
_attr_native_max_value = 50
|
|
||||||
_attr_native_step = 1
|
|
||||||
_attr_icon = "mdi:palette-outline"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, coordinator: WLEDScreenControllerCoordinator, target_id: str, entry_id: str
|
|
||||||
) -> None:
|
|
||||||
super().__init__(coordinator, target_id, entry_id, field_name="color_tolerance")
|
|
||||||
self._attr_unique_id = f"{target_id}_color_tolerance"
|
|
||||||
self._attr_translation_key = "ha_light_color_tolerance"
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
"""Select platform for LED Screen Controller (CSS source & brightness source)."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN, DATA_COORDINATOR, TARGET_TYPE_HA_LIGHT
|
|
||||||
from .coordinator import WLEDScreenControllerCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
NONE_OPTION = "\u2014 None \u2014"
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up LED Screen Controller select entities."""
|
|
||||||
data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
|
||||||
|
|
||||||
entities: list[SelectEntity] = []
|
|
||||||
if coordinator.data and "targets" in coordinator.data:
|
|
||||||
for target_id, target_data in coordinator.data["targets"].items():
|
|
||||||
info = target_data["info"]
|
|
||||||
target_type = info.get("target_type", "led")
|
|
||||||
|
|
||||||
# Both LED and HA Light targets have a CSS source
|
|
||||||
entities.append(CSSSourceSelect(coordinator, target_id, entry.entry_id))
|
|
||||||
|
|
||||||
# Only LED targets have a brightness value source
|
|
||||||
if target_type != TARGET_TYPE_HA_LIGHT:
|
|
||||||
entities.append(BrightnessSourceSelect(coordinator, target_id, entry.entry_id))
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class CSSSourceSelect(CoordinatorEntity, SelectEntity):
|
|
||||||
"""Select entity for choosing a color strip source for a target."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_icon = "mdi:palette"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{target_id}_css_source"
|
|
||||||
self._attr_translation_key = "color_strip_source"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def options(self) -> list[str]:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return []
|
|
||||||
sources = self.coordinator.data.get("css_sources") or []
|
|
||||||
return [s["name"] for s in sources]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_option(self) -> str | None:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
target_data = self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
||||||
if not target_data:
|
|
||||||
return None
|
|
||||||
current_id = target_data["info"].get("color_strip_source_id", "")
|
|
||||||
sources = self.coordinator.data.get("css_sources") or []
|
|
||||||
for s in sources:
|
|
||||||
if s["id"] == current_id:
|
|
||||||
return s["name"]
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return False
|
|
||||||
return self._target_id in self.coordinator.data.get("targets", {})
|
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
|
||||||
source_id = self._name_to_id_map().get(option)
|
|
||||||
if source_id is None:
|
|
||||||
_LOGGER.error("CSS source not found: %s", option)
|
|
||||||
return
|
|
||||||
await self.coordinator.update_target(self._target_id, color_strip_source_id=source_id)
|
|
||||||
|
|
||||||
def _name_to_id_map(self) -> dict[str, str]:
|
|
||||||
sources = (self.coordinator.data or {}).get("css_sources") or []
|
|
||||||
return {s["name"]: s["id"] for s in sources}
|
|
||||||
|
|
||||||
|
|
||||||
class BrightnessSourceSelect(CoordinatorEntity, SelectEntity):
|
|
||||||
"""Select entity for choosing a brightness value source for an LED target."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_icon = "mdi:brightness-auto"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{target_id}_brightness_source"
|
|
||||||
self._attr_translation_key = "brightness_source"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def options(self) -> list[str]:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return [NONE_OPTION]
|
|
||||||
sources = self.coordinator.data.get("value_sources") or []
|
|
||||||
return [NONE_OPTION] + [s["name"] for s in sources]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_option(self) -> str | None:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
target_data = self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
||||||
if not target_data:
|
|
||||||
return None
|
|
||||||
# BindableFloat: brightness is either a plain float or {"value": float, "source_id": str}
|
|
||||||
brightness = target_data["info"].get("brightness", "")
|
|
||||||
if isinstance(brightness, dict):
|
|
||||||
current_id = brightness.get("source_id", "")
|
|
||||||
else:
|
|
||||||
current_id = target_data["info"].get("brightness_value_source_id", "")
|
|
||||||
if not current_id:
|
|
||||||
return NONE_OPTION
|
|
||||||
sources = self.coordinator.data.get("value_sources") or []
|
|
||||||
for s in sources:
|
|
||||||
if s["id"] == current_id:
|
|
||||||
return s["name"]
|
|
||||||
return NONE_OPTION
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return False
|
|
||||||
return self._target_id in self.coordinator.data.get("targets", {})
|
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
|
||||||
if option == NONE_OPTION:
|
|
||||||
source_id = ""
|
|
||||||
else:
|
|
||||||
name_map = {
|
|
||||||
s["name"]: s["id"] for s in (self.coordinator.data or {}).get("value_sources") or []
|
|
||||||
}
|
|
||||||
source_id = name_map.get(option)
|
|
||||||
if source_id is None:
|
|
||||||
_LOGGER.error("Value source not found: %s", option)
|
|
||||||
return
|
|
||||||
await self.coordinator.update_target(
|
|
||||||
self._target_id,
|
|
||||||
brightness={"value": 1.0, "source_id": source_id} if source_id else 1.0,
|
|
||||||
)
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
"""Sensor platform for LED Screen Controller."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
DOMAIN,
|
|
||||||
TARGET_TYPE_HA_LIGHT,
|
|
||||||
DATA_COORDINATOR,
|
|
||||||
)
|
|
||||||
from .coordinator import WLEDScreenControllerCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up LED Screen Controller sensors."""
|
|
||||||
data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
|
||||||
|
|
||||||
entities: list[SensorEntity] = []
|
|
||||||
if coordinator.data and "targets" in coordinator.data:
|
|
||||||
for target_id, target_data in coordinator.data["targets"].items():
|
|
||||||
entities.append(WLEDScreenControllerFPSSensor(coordinator, target_id, entry.entry_id))
|
|
||||||
entities.append(
|
|
||||||
WLEDScreenControllerStatusSensor(coordinator, target_id, entry.entry_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add mapped lights sensor for HA Light targets
|
|
||||||
info = target_data["info"]
|
|
||||||
if info.get("target_type") == TARGET_TYPE_HA_LIGHT:
|
|
||||||
entities.append(HALightMappedLightsSensor(coordinator, target_id, entry.entry_id))
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class WLEDScreenControllerFPSSensor(CoordinatorEntity, SensorEntity):
|
|
||||||
"""FPS sensor for a LED Screen Controller target."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
|
||||||
_attr_native_unit_of_measurement = "FPS"
|
|
||||||
_attr_icon = "mdi:speedometer"
|
|
||||||
_attr_suggested_display_precision = 1
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{target_id}_fps"
|
|
||||||
self._attr_translation_key = "fps"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
"""Return device information."""
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float | None:
|
|
||||||
"""Return the FPS value."""
|
|
||||||
target_data = self._get_target_data()
|
|
||||||
if not target_data or not target_data.get("state"):
|
|
||||||
return None
|
|
||||||
state = target_data["state"]
|
|
||||||
if not state.get("processing"):
|
|
||||||
return None
|
|
||||||
return state.get("fps_actual")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
|
||||||
"""Return additional attributes."""
|
|
||||||
target_data = self._get_target_data()
|
|
||||||
if not target_data or not target_data.get("state"):
|
|
||||||
return {}
|
|
||||||
return {"fps_target": target_data["state"].get("fps_target")}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return self._get_target_data() is not None
|
|
||||||
|
|
||||||
def _get_target_data(self) -> dict[str, Any] | None:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
return self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
||||||
|
|
||||||
|
|
||||||
class WLEDScreenControllerStatusSensor(CoordinatorEntity, SensorEntity):
|
|
||||||
"""Status sensor for a LED Screen Controller target."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_icon = "mdi:information-outline"
|
|
||||||
_attr_device_class = SensorDeviceClass.ENUM
|
|
||||||
_attr_options = ["processing", "idle", "error", "unavailable"]
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{target_id}_status"
|
|
||||||
self._attr_translation_key = "status"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
"""Return device information."""
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> str:
|
|
||||||
"""Return the status."""
|
|
||||||
target_data = self._get_target_data()
|
|
||||||
if not target_data:
|
|
||||||
return "unavailable"
|
|
||||||
state = target_data.get("state")
|
|
||||||
if not state:
|
|
||||||
return "unavailable"
|
|
||||||
if state.get("processing"):
|
|
||||||
errors = state.get("errors", [])
|
|
||||||
if errors:
|
|
||||||
return "error"
|
|
||||||
return "processing"
|
|
||||||
return "idle"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return self._get_target_data() is not None
|
|
||||||
|
|
||||||
def _get_target_data(self) -> dict[str, Any] | None:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
return self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
||||||
|
|
||||||
|
|
||||||
class HALightMappedLightsSensor(CoordinatorEntity, SensorEntity):
|
|
||||||
"""Sensor showing the number of mapped HA lights for an HA Light target."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_icon = "mdi:lightbulb-group"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{target_id}_mapped_lights"
|
|
||||||
self._attr_translation_key = "mapped_lights"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
"""Return device information."""
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> int | None:
|
|
||||||
"""Return the number of mapped lights."""
|
|
||||||
target_data = self._get_target_data()
|
|
||||||
if not target_data:
|
|
||||||
return None
|
|
||||||
mappings = target_data.get("info", {}).get("light_mappings", [])
|
|
||||||
return len(mappings)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
|
||||||
"""Return light mapping details as attributes."""
|
|
||||||
target_data = self._get_target_data()
|
|
||||||
if not target_data:
|
|
||||||
return {}
|
|
||||||
mappings = target_data.get("info", {}).get("light_mappings", [])
|
|
||||||
entity_ids = [m.get("entity_id", "") for m in mappings]
|
|
||||||
return {
|
|
||||||
"entity_ids": entity_ids,
|
|
||||||
"mappings": [
|
|
||||||
{
|
|
||||||
"entity_id": m.get("entity_id", ""),
|
|
||||||
"led_start": m.get("led_start", 0),
|
|
||||||
"led_end": m.get("led_end", -1),
|
|
||||||
"brightness_scale": m.get("brightness_scale", 1.0),
|
|
||||||
}
|
|
||||||
for m in mappings
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return self._get_target_data() is not None
|
|
||||||
|
|
||||||
def _get_target_data(self) -> dict[str, Any] | None:
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
return self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
set_leds:
|
|
||||||
name: Set LEDs
|
|
||||||
description: Push segment data to an api_input color strip source
|
|
||||||
fields:
|
|
||||||
source_id:
|
|
||||||
name: Source ID
|
|
||||||
description: The api_input CSS source ID (e.g., css_abc12345)
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
segments:
|
|
||||||
name: Segments
|
|
||||||
description: >
|
|
||||||
List of segment objects. Each segment has: start (int), length (int),
|
|
||||||
mode ("solid"/"per_pixel"/"gradient"), color ([R,G,B] for solid),
|
|
||||||
colors ([[R,G,B],...] for per_pixel/gradient)
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
object:
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"step": {
|
|
||||||
"user": {
|
|
||||||
"title": "Set up LED Screen Controller",
|
|
||||||
"description": "Enter the URL and API key for your LED Screen Controller server.",
|
|
||||||
"data": {
|
|
||||||
"server_name": "Server Name",
|
|
||||||
"server_url": "Server URL",
|
|
||||||
"api_key": "API Key"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"server_name": "Display name for this server in Home Assistant",
|
|
||||||
"server_url": "URL of your LED Screen Controller server (e.g., http://192.168.1.100:8080)",
|
|
||||||
"api_key": "API key from your server's configuration file"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "Failed to connect to server.",
|
|
||||||
"invalid_api_key": "Invalid API key.",
|
|
||||||
"unknown": "Unexpected error occurred."
|
|
||||||
},
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "This server is already configured."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"button": {
|
|
||||||
"activate_scene": {
|
|
||||||
"name": "{scene_name}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"light": {
|
|
||||||
"api_input_light": {
|
|
||||||
"name": "Light"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"switch": {
|
|
||||||
"processing": {
|
|
||||||
"name": "Processing"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sensor": {
|
|
||||||
"fps": {
|
|
||||||
"name": "FPS"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "Status",
|
|
||||||
"state": {
|
|
||||||
"processing": "Processing",
|
|
||||||
"idle": "Idle",
|
|
||||||
"error": "Error",
|
|
||||||
"unavailable": "Unavailable"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mapped_lights": {
|
|
||||||
"name": "Mapped Lights"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"brightness": {
|
|
||||||
"name": "Brightness"
|
|
||||||
},
|
|
||||||
"ha_light_update_rate": {
|
|
||||||
"name": "Update Rate"
|
|
||||||
},
|
|
||||||
"ha_light_transition": {
|
|
||||||
"name": "Transition"
|
|
||||||
},
|
|
||||||
"ha_light_min_brightness": {
|
|
||||||
"name": "Min Brightness"
|
|
||||||
},
|
|
||||||
"ha_light_color_tolerance": {
|
|
||||||
"name": "Color Tolerance"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"select": {
|
|
||||||
"color_strip_source": {
|
|
||||||
"name": "Color Strip Source"
|
|
||||||
},
|
|
||||||
"brightness_source": {
|
|
||||||
"name": "Brightness Source"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services": {
|
|
||||||
"set_leds": {
|
|
||||||
"name": "Set LEDs",
|
|
||||||
"description": "Push segment data to an api_input color strip source.",
|
|
||||||
"fields": {
|
|
||||||
"source_id": {
|
|
||||||
"name": "Source ID",
|
|
||||||
"description": "The api_input CSS source ID (e.g., css_abc12345)."
|
|
||||||
},
|
|
||||||
"segments": {
|
|
||||||
"name": "Segments",
|
|
||||||
"description": "List of segment objects with start, length, mode, and color/colors fields."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
"""Switch platform for LED Screen Controller."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN, DATA_COORDINATOR
|
|
||||||
from .coordinator import WLEDScreenControllerCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up LED Screen Controller switches."""
|
|
||||||
data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
|
|
||||||
|
|
||||||
entities = []
|
|
||||||
if coordinator.data and "targets" in coordinator.data:
|
|
||||||
for target_id, target_data in coordinator.data["targets"].items():
|
|
||||||
entities.append(
|
|
||||||
WLEDScreenControllerSwitch(coordinator, target_id, entry.entry_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class WLEDScreenControllerSwitch(CoordinatorEntity, SwitchEntity):
|
|
||||||
"""Representation of a LED Screen Controller target processing switch."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: WLEDScreenControllerCoordinator,
|
|
||||||
target_id: str,
|
|
||||||
entry_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the switch."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._target_id = target_id
|
|
||||||
self._entry_id = entry_id
|
|
||||||
self._attr_unique_id = f"{target_id}_processing"
|
|
||||||
self._attr_translation_key = "processing"
|
|
||||||
self._attr_icon = "mdi:television-ambient-light"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> dict[str, Any]:
|
|
||||||
"""Return device information."""
|
|
||||||
return {"identifiers": {(DOMAIN, self._target_id)}}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Return true if processing is active."""
|
|
||||||
target_data = self._get_target_data()
|
|
||||||
if not target_data or not target_data.get("state"):
|
|
||||||
return False
|
|
||||||
return target_data["state"].get("processing", False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return self._get_target_data() is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
|
||||||
"""Return additional state attributes."""
|
|
||||||
target_data = self._get_target_data()
|
|
||||||
if not target_data:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
attrs: dict[str, Any] = {"target_id": self._target_id}
|
|
||||||
state = target_data.get("state") or {}
|
|
||||||
metrics = target_data.get("metrics") or {}
|
|
||||||
|
|
||||||
if state:
|
|
||||||
attrs["fps_target"] = state.get("fps_target")
|
|
||||||
attrs["fps_actual"] = state.get("fps_actual")
|
|
||||||
|
|
||||||
if metrics:
|
|
||||||
attrs["frames_processed"] = metrics.get("frames_processed")
|
|
||||||
attrs["errors_count"] = metrics.get("errors_count")
|
|
||||||
attrs["uptime_seconds"] = metrics.get("uptime_seconds")
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
||||||
"""Start processing."""
|
|
||||||
await self.coordinator.start_processing(self._target_id)
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
||||||
"""Stop processing."""
|
|
||||||
await self.coordinator.stop_processing(self._target_id)
|
|
||||||
|
|
||||||
def _get_target_data(self) -> dict[str, Any] | None:
|
|
||||||
"""Get target data from coordinator."""
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return None
|
|
||||||
return self.coordinator.data.get("targets", {}).get(self._target_id)
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"step": {
|
|
||||||
"user": {
|
|
||||||
"title": "Set up LED Screen Controller",
|
|
||||||
"description": "Enter the URL and API key for your LED Screen Controller server.",
|
|
||||||
"data": {
|
|
||||||
"server_name": "Server Name",
|
|
||||||
"server_url": "Server URL",
|
|
||||||
"api_key": "API Key"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"server_name": "Display name for this server in Home Assistant",
|
|
||||||
"server_url": "URL of your LED Screen Controller server (e.g., http://192.168.1.100:8080)",
|
|
||||||
"api_key": "API key from your server's configuration file"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "Failed to connect to server.",
|
|
||||||
"invalid_api_key": "Invalid API key.",
|
|
||||||
"unknown": "Unexpected error occurred."
|
|
||||||
},
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "This server is already configured."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"button": {
|
|
||||||
"activate_scene": {
|
|
||||||
"name": "{scene_name}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"light": {
|
|
||||||
"api_input_light": {
|
|
||||||
"name": "Light"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"switch": {
|
|
||||||
"processing": {
|
|
||||||
"name": "Processing"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sensor": {
|
|
||||||
"fps": {
|
|
||||||
"name": "FPS"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "Status",
|
|
||||||
"state": {
|
|
||||||
"processing": "Processing",
|
|
||||||
"idle": "Idle",
|
|
||||||
"error": "Error",
|
|
||||||
"unavailable": "Unavailable"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mapped_lights": {
|
|
||||||
"name": "Mapped Lights"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"brightness": {
|
|
||||||
"name": "Brightness"
|
|
||||||
},
|
|
||||||
"ha_light_update_rate": {
|
|
||||||
"name": "Update Rate"
|
|
||||||
},
|
|
||||||
"ha_light_transition": {
|
|
||||||
"name": "Transition"
|
|
||||||
},
|
|
||||||
"ha_light_min_brightness": {
|
|
||||||
"name": "Min Brightness"
|
|
||||||
},
|
|
||||||
"ha_light_color_tolerance": {
|
|
||||||
"name": "Color Tolerance"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"select": {
|
|
||||||
"color_strip_source": {
|
|
||||||
"name": "Color Strip Source"
|
|
||||||
},
|
|
||||||
"brightness_source": {
|
|
||||||
"name": "Brightness Source"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"step": {
|
|
||||||
"user": {
|
|
||||||
"title": "Настройка LED Screen Controller",
|
|
||||||
"description": "Введите URL и API-ключ вашего сервера LED Screen Controller.",
|
|
||||||
"data": {
|
|
||||||
"server_name": "Имя сервера",
|
|
||||||
"server_url": "URL сервера",
|
|
||||||
"api_key": "API-ключ"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"server_name": "Отображаемое имя сервера в Home Assistant",
|
|
||||||
"server_url": "URL сервера LED Screen Controller (например, http://192.168.1.100:8080)",
|
|
||||||
"api_key": "API-ключ из конфигурационного файла сервера"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "Не удалось подключиться к серверу.",
|
|
||||||
"invalid_api_key": "Неверный API-ключ.",
|
|
||||||
"unknown": "Произошла непредвиденная ошибка."
|
|
||||||
},
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "Этот сервер уже настроен."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"button": {
|
|
||||||
"activate_scene": {
|
|
||||||
"name": "{scene_name}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"light": {
|
|
||||||
"api_input_light": {
|
|
||||||
"name": "Подсветка"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"switch": {
|
|
||||||
"processing": {
|
|
||||||
"name": "Обработка"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sensor": {
|
|
||||||
"fps": {
|
|
||||||
"name": "FPS"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "Статус",
|
|
||||||
"state": {
|
|
||||||
"processing": "Обработка",
|
|
||||||
"idle": "Ожидание",
|
|
||||||
"error": "Ошибка",
|
|
||||||
"unavailable": "Недоступен"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mapped_lights": {
|
|
||||||
"name": "Привязанные светильники"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"brightness": {
|
|
||||||
"name": "Яркость"
|
|
||||||
},
|
|
||||||
"ha_light_update_rate": {
|
|
||||||
"name": "Частота обновления"
|
|
||||||
},
|
|
||||||
"ha_light_transition": {
|
|
||||||
"name": "Переход"
|
|
||||||
},
|
|
||||||
"ha_light_min_brightness": {
|
|
||||||
"name": "Мин. яркость"
|
|
||||||
},
|
|
||||||
"ha_light_color_tolerance": {
|
|
||||||
"name": "Допуск цвета"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"select": {
|
|
||||||
"color_strip_source": {
|
|
||||||
"name": "Источник цветовой полосы"
|
|
||||||
},
|
|
||||||
"brightness_source": {
|
|
||||||
"name": "Источник яркости"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# WLED Screen Controller API Documentation
|
# LedGrab API Documentation
|
||||||
|
|
||||||
Complete REST API reference for the WLED Screen Controller server.
|
Complete REST API reference for the LedGrab server.
|
||||||
|
|
||||||
**Base URL:** `http://localhost:8080`
|
**Base URL:** `http://localhost:8080`
|
||||||
**API Version:** v1
|
**API Version:** v1
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "WLED Screen Controller",
|
|
||||||
"render_readme": true,
|
|
||||||
"country": ["US"],
|
|
||||||
"homeassistant": "2023.1.0"
|
|
||||||
}
|
|
||||||
+7
-7
@@ -30,8 +30,8 @@ SetCompressor /SOLID lzma
|
|||||||
|
|
||||||
; ── Modern UI Configuration ─────────────────────────────────
|
; ── Modern UI Configuration ─────────────────────────────────
|
||||||
|
|
||||||
!define MUI_ICON "server\src\wled_controller\static\icons\icon.ico"
|
!define MUI_ICON "server\src\ledgrab\static\icons\icon.ico"
|
||||||
!define MUI_UNICON "server\src\wled_controller\static\icons\icon.ico"
|
!define MUI_UNICON "server\src\ledgrab\static\icons\icon.ico"
|
||||||
!define MUI_ABORTWARNING
|
!define MUI_ABORTWARNING
|
||||||
|
|
||||||
; ── Pages ───────────────────────────────────────────────────
|
; ── Pages ───────────────────────────────────────────────────
|
||||||
@@ -116,7 +116,7 @@ Section "!${APPNAME} (required)" SecCore
|
|||||||
CreateDirectory "$SMPROGRAMS\${APPNAME}"
|
CreateDirectory "$SMPROGRAMS\${APPNAME}"
|
||||||
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" \
|
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" \
|
||||||
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
||||||
"$INSTDIR\app\src\wled_controller\static\icons\icon.ico" 0
|
"$INSTDIR\app\src\ledgrab\static\icons\icon.ico" 0
|
||||||
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
; Registry: install location + Add/Remove Programs entry
|
; Registry: install location + Add/Remove Programs entry
|
||||||
@@ -132,11 +132,11 @@ Section "!${APPNAME} (required)" SecCore
|
|||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
"InstallLocation" "$INSTDIR"
|
"InstallLocation" "$INSTDIR"
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
"DisplayIcon" "$INSTDIR\app\src\wled_controller\static\icons\icon.ico"
|
"DisplayIcon" "$INSTDIR\app\src\ledgrab\static\icons\icon.ico"
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
"Publisher" "Alexei Dolgolyov"
|
"Publisher" "Alexei Dolgolyov"
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
"URLInfoAbout" "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
|
"URLInfoAbout" "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab"
|
||||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
"NoModify" 1
|
"NoModify" 1
|
||||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
@@ -152,13 +152,13 @@ SectionEnd
|
|||||||
Section "Desktop shortcut" SecDesktop
|
Section "Desktop shortcut" SecDesktop
|
||||||
CreateShortcut "$DESKTOP\${APPNAME}.lnk" \
|
CreateShortcut "$DESKTOP\${APPNAME}.lnk" \
|
||||||
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
||||||
"$INSTDIR\app\src\wled_controller\static\icons\icon.ico" 0
|
"$INSTDIR\app\src\ledgrab\static\icons\icon.ico" 0
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section "Start with Windows" SecAutostart
|
Section "Start with Windows" SecAutostart
|
||||||
CreateShortcut "$SMSTARTUP\${APPNAME}.lnk" \
|
CreateShortcut "$SMSTARTUP\${APPNAME}.lnk" \
|
||||||
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
||||||
"$INSTDIR\app\src\wled_controller\static\icons\icon.ico" 0
|
"$INSTDIR\app\src\ledgrab\static\icons\icon.ico" 0
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
; ── Section Descriptions ────────────────────────────────────
|
; ── Section Descriptions ────────────────────────────────────
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
# math_wave Color Strip Source — Implementation Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
A new CSS type that generates LED colors from configurable mathematical wave functions. Each LED position gets a wave value based on spatial position and time, mapped to a color via a gradient palette. Supports multiple superimposed wave layers, sync clocks, and bindable parameters.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Waveform types: sine, triangle, sawtooth, square
|
|
||||||
- Parameters per wave layer: waveform, frequency, amplitude, phase, offset
|
|
||||||
- Global parameters: speed (bindable), gradient_id (color mapping)
|
|
||||||
- Wave superposition: list of wave layers combined additively
|
|
||||||
- Spatial dimension: wave value depends on LED position (0.0-1.0) + time
|
|
||||||
- Sync clock integration for time parameter
|
|
||||||
- Color mapping: combined wave output (0.0-1.0) mapped through a gradient
|
|
||||||
|
|
||||||
## Phase 1: Storage Model
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/storage/color_strip_source.py`**
|
|
||||||
- Add `MathWaveColorStripSource` dataclass after `GameEventColorStripSource`
|
|
||||||
- Fields:
|
|
||||||
- `waves: list` — default `[{"waveform": "sine", "frequency": 1.0, "amplitude": 1.0, "phase": 0.0, "offset": 0.0}]`
|
|
||||||
- `speed: BindableFloat` — default 1.0
|
|
||||||
- `gradient_id: Optional[str]` — references Gradient entity
|
|
||||||
- Implement `to_dict`, `from_dict`, `create_from_kwargs`, `apply_update` (follow `CandlelightColorStripSource` pattern)
|
|
||||||
- Valid waveforms: `{"sine", "triangle", "sawtooth", "square"}`
|
|
||||||
- Add `"math_wave": MathWaveColorStripSource` to `_SOURCE_TYPE_MAP`
|
|
||||||
|
|
||||||
## Phase 2: Stream Implementation
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/core/processing/math_wave_stream.py`** (new)
|
|
||||||
- Class `MathWaveColorStripStream(ColorStripStream)` following `CandlelightColorStripStream` pattern
|
|
||||||
- Key methods: `__init__`, `_update_from_source`, `configure`, `start`, `stop`, `get_latest_colors`, `update_source`, `set_clock`, `set_gradient_store`
|
|
||||||
- Animation loop (`_animate_loop`):
|
|
||||||
- Get `t` from clock (if set) or wall clock
|
|
||||||
- For each LED at normalized position `p = i / (N-1)`:
|
|
||||||
- Sum all wave layers: `sum += amplitude * waveform(2*pi*frequency*(p + speed*t) + phase) + offset`
|
|
||||||
- Clamp result to [0.0, 1.0]
|
|
||||||
- Map per-LED values to RGB via gradient LUT
|
|
||||||
- Waveform functions (vectorized with numpy):
|
|
||||||
- `sine`: `0.5 + 0.5 * np.sin(x)`
|
|
||||||
- `triangle`: `2.0 * np.abs(np.mod(x / (2*pi), 1.0) - 0.5)`
|
|
||||||
- `sawtooth`: `np.mod(x / (2*pi), 1.0)`
|
|
||||||
- `square`: `(np.sin(x) >= 0).astype(float)`
|
|
||||||
- Double-buffering pattern (same as candlelight)
|
|
||||||
- Use `self.resolve("speed", self._speed)` for bindable speed
|
|
||||||
- Gradient resolution via `set_gradient_store` pattern
|
|
||||||
|
|
||||||
## Phase 3: API Integration
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/api/schemas/color_strip_sources.py`**
|
|
||||||
- Add `MathWaveCSSResponse`, `MathWaveCSSCreate`, `MathWaveCSSUpdate`
|
|
||||||
- Add to `ColorStripSourceResponse`, `ColorStripSourceCreate`, `ColorStripSourceUpdate` unions
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/core/processing/color_strip_stream_manager.py`**
|
|
||||||
- Import `MathWaveColorStripStream`
|
|
||||||
- Add `"math_wave": MathWaveColorStripStream` to `_SIMPLE_STREAM_MAP`
|
|
||||||
- Existing `set_gradient_store` and `_inject_clock` injection handles it automatically
|
|
||||||
|
|
||||||
## Phase 4: Frontend
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/static/js/types.ts`**
|
|
||||||
- Add `'math_wave'` to `CSSSourceType` union
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/static/js/core/icons.ts`**
|
|
||||||
- Add `math_wave: _svg(P.activity)` to `_colorStripTypeIcons`
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/templates/modals/css-editor.html`**
|
|
||||||
- Add `<option value="math_wave">` to type select
|
|
||||||
- Add `<div id="css-editor-math-wave-section">` with:
|
|
||||||
- Gradient picker (EntitySelect)
|
|
||||||
- Speed (BindableScalarWidget)
|
|
||||||
- Wave layers list (dynamic rows: waveform IconSelect, frequency/amplitude/phase/offset inputs, add/remove buttons)
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/static/js/features/color-strips.ts`**
|
|
||||||
- Add to `CSS_TYPE_KEYS`, `CSS_SECTION_MAP`, `CSS_TYPE_SETUP`, `NON_PICTURE_TYPES`
|
|
||||||
- Add to `clockTypes` array in `saveCSSEditor`
|
|
||||||
- Add type handler: `load(css)`, `reset()`, `getPayload(name)`
|
|
||||||
- Add card renderer showing wave count, gradient swatch, speed, clock badge
|
|
||||||
- Add wave layer management: `_renderMathWaveRow`, `addMathWaveLayer`, `removeMathWaveLayer`
|
|
||||||
|
|
||||||
**Files: `en.json`, `ru.json`, `zh.json`**
|
|
||||||
- Add i18n keys for type name, description, all field labels, waveform names
|
|
||||||
|
|
||||||
## Phase 5: Testing
|
|
||||||
|
|
||||||
**File: `server/tests/core/test_math_wave_stream.py`** (new)
|
|
||||||
- Test wave functions produce expected values at known inputs
|
|
||||||
- Test single wave spatial pattern
|
|
||||||
- Test wave superposition
|
|
||||||
- Test gradient color mapping
|
|
||||||
- Test clock integration
|
|
||||||
- Test `update_source` hot-update
|
|
||||||
- Test `configure` auto-sizing
|
|
||||||
|
|
||||||
**File: `server/tests/e2e/test_color_strip_flow.py`**
|
|
||||||
- Add `test_math_wave_crud` to lifecycle tests
|
|
||||||
|
|
||||||
**Storage model tests:**
|
|
||||||
- Test `from_dict` roundtrip
|
|
||||||
- Test `create_from_kwargs` with valid/invalid waveforms
|
|
||||||
- Test `apply_update`
|
|
||||||
- Test `_SOURCE_TYPE_MAP` dispatch
|
|
||||||
|
|
||||||
## Risks & Mitigations
|
|
||||||
|
|
||||||
- **Wave layer UI complexity** — Follow existing composite layers / game event mappings patterns
|
|
||||||
- **Performance with many layers** — Vectorize with numpy; cap max wave layers to 8
|
|
||||||
- **Gradient resolution** — Stream manager already injects `set_gradient_store` automatically
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# music_sync Color Strip Source — Implementation Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
A higher-level music-reactive CSS that provides semantic audio analysis (BPM detection, beat tracking, energy envelope, drop detection, frequency band energy) and multiple visualization modes. Builds on existing `AudioCaptureManager` and `AudioAnalysis` infrastructure. No new external dependencies — uses numpy-only analysis.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- BPM estimation from real-time audio
|
|
||||||
- Beat onset detection with configurable threshold
|
|
||||||
- Smoothed RMS energy envelope with attack/release
|
|
||||||
- Drop detection: energy drops/buildups
|
|
||||||
- Frequency band energy: bass (20-250 Hz), mid (250-4k Hz), treble (4k-20k Hz)
|
|
||||||
- Four visualization modes: `pulse_on_beat`, `energy_gradient`, `spectrum_bands`, `strobe_on_drop`
|
|
||||||
- Uses existing audio engine infrastructure (no new audio capture code)
|
|
||||||
- No external dependencies beyond numpy
|
|
||||||
|
|
||||||
## Phase 1: Music Analysis Engine
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/core/audio/music_analyzer.py`** (new)
|
|
||||||
|
|
||||||
### 1.1 `MusicFeatures` dataclass (frozen=True)
|
|
||||||
- `bpm: float` — estimated BPM (0 if unknown)
|
|
||||||
- `beat: bool` — beat detected this frame
|
|
||||||
- `beat_intensity: float` — 0.0-1.0
|
|
||||||
- `beat_phase: float` — 0.0-1.0 position in beat cycle
|
|
||||||
- `energy: float` — smoothed RMS 0.0-1.0
|
|
||||||
- `energy_delta: float` — rate of change
|
|
||||||
- `bass_energy, mid_energy, treble_energy: float` — 0.0-1.0
|
|
||||||
- `drop_state: str` — "idle"|"buildup"|"drop"|"recovery"
|
|
||||||
- `drop_intensity: float` — 0.0-1.0
|
|
||||||
|
|
||||||
### 1.2 `MusicAnalyzer` class
|
|
||||||
- **State**: rolling energy buffer (~4s at ~43 Hz = ~172 samples), beat history timestamps (last 30), smoothed band energies, BPM estimate, drop state machine
|
|
||||||
- **`update(analysis: AudioAnalysis) -> MusicFeatures`**: main entry point
|
|
||||||
- **BPM estimation**: Track beat timestamps, compute median inter-beat interval, exponential smoothing, clamp 40-220 BPM
|
|
||||||
- **Beat tracking**: Pass through `AudioAnalysis.beat` + compute `beat_phase` (position in current beat cycle)
|
|
||||||
- **Energy envelope**: Smoothed RMS with configurable attack/release
|
|
||||||
- **Drop detection**: State machine: `idle -> buildup` (energy rising steadily 1-2s), `buildup -> drop` (energy drops >50% within 100ms), `drop -> recovery` (after 500ms), `recovery -> idle`
|
|
||||||
- **Frequency bands**: Sum spectrum bins into 3 bands from 64-band spectrum
|
|
||||||
|
|
||||||
## Phase 2: Storage Model
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/storage/color_strip_source.py`**
|
|
||||||
- Add `MusicSyncColorStripSource` dataclass after `AudioColorStripSource`
|
|
||||||
- Fields:
|
|
||||||
- `visualization_mode: str` — default `"pulse_on_beat"`
|
|
||||||
- `audio_source_id: str` — references AudioSource
|
|
||||||
- `sensitivity: BindableFloat` — default 1.0
|
|
||||||
- `smoothing: BindableFloat` — default 0.3
|
|
||||||
- `palette: str` — default `"rainbow"`
|
|
||||||
- `gradient_id: Optional[str]`
|
|
||||||
- `color: BindableColor` — primary color
|
|
||||||
- `color_secondary: BindableColor` — for two-color modes
|
|
||||||
- `beat_decay: BindableFloat` — default 0.15
|
|
||||||
- `led_count: int` — 0 = auto-size
|
|
||||||
- `mirror: bool`
|
|
||||||
- Add `"music_sync": MusicSyncColorStripSource` to `_SOURCE_TYPE_MAP`
|
|
||||||
|
|
||||||
## Phase 3: Stream Implementation
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/core/processing/music_sync_stream.py`** (new)
|
|
||||||
- Class `MusicSyncColorStripStream(ColorStripStream)` following `AudioColorStripStream` pattern
|
|
||||||
- Constructor: accept source + audio_capture_manager + stores. Create `MusicAnalyzer` instance
|
|
||||||
- `start()`: Acquire audio stream, start background thread
|
|
||||||
- `stop()`: Release audio stream, stop thread
|
|
||||||
- `_animate_loop()`:
|
|
||||||
1. Get `AudioAnalysis` from audio stream
|
|
||||||
2. Apply audio filter pipeline (if any)
|
|
||||||
3. Feed to `MusicAnalyzer.update()` → `MusicFeatures`
|
|
||||||
4. Dispatch to visualization renderer
|
|
||||||
5. Double-buffer output
|
|
||||||
|
|
||||||
### Visualization Renderers
|
|
||||||
|
|
||||||
- **`pulse_on_beat`**: Full-strip flash on beat with exponential decay. Between beats: sine-wave pulsing synced to BPM. Color from palette indexed by beat_intensity.
|
|
||||||
- **`energy_gradient`**: Maps bass→warm, treble→cool. Overall brightness from energy. Gradient scrolls with beat_phase.
|
|
||||||
- **`spectrum_bands`**: 3 zones (bass/mid/treble), each fills proportionally to band energy. Mirror mode: bass center, treble edges.
|
|
||||||
- **`strobe_on_drop`**: Idle=gentle breathing. Buildup=increasing pulse. Drop=rapid strobe at 10 Hz. Recovery=fade back.
|
|
||||||
|
|
||||||
## Phase 4: API Integration
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/api/schemas/color_strip_sources.py`**
|
|
||||||
- Add `MusicSyncCSSResponse`, `MusicSyncCSSCreate`, `MusicSyncCSSUpdate`
|
|
||||||
- Add to all three union types
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/api/routes/color_strip_sources.py`**
|
|
||||||
- Import + add to `_RESPONSE_MAP`
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/core/processing/color_strip_stream_manager.py`**
|
|
||||||
- Add `music_sync` branch in `acquire()` (same pattern as `audio` branch)
|
|
||||||
- Update `refresh_audio_filter_pipelines()` to include `MusicSyncColorStripStream`
|
|
||||||
|
|
||||||
## Phase 5: Frontend
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/static/js/core/icons.ts`**
|
|
||||||
- Add `music_sync: _svg(P.radio)` icon
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/templates/modals/css-editor.html`**
|
|
||||||
- Add `<div id="css-editor-music-sync-section">` with:
|
|
||||||
- Visualization mode selector (IconSelect)
|
|
||||||
- Audio source dropdown
|
|
||||||
- Sensitivity, Smoothing, Beat Decay (BindableScalarWidget containers)
|
|
||||||
- Palette/gradient selector (EntitySelect)
|
|
||||||
- Primary + Secondary color (BindableColorWidget)
|
|
||||||
- Mirror checkbox
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/static/js/features/color-strips-music-sync.ts`** (new, extracted)
|
|
||||||
- Editor logic, widget factories, card renderer
|
|
||||||
|
|
||||||
**File: `server/src/wled_controller/static/js/features/color-strips.ts`**
|
|
||||||
- Register type in `CSS_TYPE_KEYS`, `CSS_SECTION_MAP`, `CSS_TYPE_SETUP`, `NON_PICTURE_TYPES`
|
|
||||||
- Import and register from `color-strips-music-sync.ts`
|
|
||||||
|
|
||||||
**Files: `en.json`, `ru.json`, `zh.json`**
|
|
||||||
- Add i18n keys for type, visualization modes, all field labels
|
|
||||||
|
|
||||||
## Phase 6: Testing
|
|
||||||
|
|
||||||
**File: `server/tests/core/audio/test_music_analyzer.py`** (new)
|
|
||||||
- BPM estimation from regular beats (within 5 BPM accuracy)
|
|
||||||
- BPM handles no beats gracefully
|
|
||||||
- Beat phase progression
|
|
||||||
- Energy envelope attack/release
|
|
||||||
- Frequency band splitting
|
|
||||||
- Drop detection state machine transitions
|
|
||||||
- No false drops on steady signal
|
|
||||||
|
|
||||||
**File: `server/tests/core/processing/test_music_sync_stream.py`** (new)
|
|
||||||
- Stream lifecycle (start/stop)
|
|
||||||
- Produces valid colors
|
|
||||||
- Hot-update parameters
|
|
||||||
- Auto-size from device
|
|
||||||
- All 4 visualization modes produce valid (n,3) uint8 arrays
|
|
||||||
- Mirror mode symmetry
|
|
||||||
|
|
||||||
**Storage + API tests:**
|
|
||||||
- `from_dict` roundtrip
|
|
||||||
- CRUD via test client
|
|
||||||
|
|
||||||
## Risks & Mitigations
|
|
||||||
|
|
||||||
- **BPM accuracy** — Median-of-recent-IBIs is robust for dance/electronic. For ambient, BPM noisy but `energy_gradient` and `spectrum_bands` don't depend on BPM
|
|
||||||
- **Drop detection false positives** — Require minimum energy threshold + sustained increase before buildup state
|
|
||||||
- **Frontend file size** — Extract to `color-strips-music-sync.ts` (following composite/notification pattern)
|
|
||||||
- **Strobe photosensitivity** — Cap at 10 Hz (below 15-25 Hz danger zone), add UI warning
|
|
||||||
- **Thread safety** — `MusicAnalyzer` owned exclusively by one stream thread, no shared access
|
|
||||||
|
|
||||||
## Dependency Order
|
|
||||||
|
|
||||||
`math_wave` should be implemented first (simpler, no audio dependency), then `music_sync`.
|
|
||||||
+22
-22
@@ -1,41 +1,41 @@
|
|||||||
# WLED Screen Controller — Environment Variables
|
# LedGrab — Environment Variables
|
||||||
# Copy this file to .env and adjust values as needed.
|
# 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 ──────────────────────────────────────────────
|
# ── Server ──────────────────────────────────────────────
|
||||||
# WLED_SERVER__HOST=0.0.0.0 # Listen address (default: 0.0.0.0)
|
# LEDGRAB_SERVER__HOST=0.0.0.0 # Listen address (default: 0.0.0.0)
|
||||||
# WLED_SERVER__PORT=8080 # Listen port (default: 8080)
|
# LEDGRAB_SERVER__PORT=8080 # Listen port (default: 8080)
|
||||||
# WLED_SERVER__LOG_LEVEL=INFO # Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)
|
# LEDGRAB_SERVER__LOG_LEVEL=INFO # Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)
|
||||||
# WLED_SERVER__CORS_ORIGINS=["*"] # JSON array of allowed CORS origins
|
# LEDGRAB_SERVER__CORS_ORIGINS=["*"] # JSON array of allowed CORS origins
|
||||||
|
|
||||||
# ── Authentication ──────────────────────────────────────
|
# ── Authentication ──────────────────────────────────────
|
||||||
# API keys are required. Format: JSON object {"label": "key"}.
|
# 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 ────────────────────────────────────────────
|
# ── Storage ────────────────────────────────────────────
|
||||||
# All data is stored in a single SQLite database.
|
# 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) ────────────────────────────────────
|
# ── MQTT (optional) ────────────────────────────────────
|
||||||
# WLED_MQTT__ENABLED=false
|
# LEDGRAB_MQTT__ENABLED=false
|
||||||
# WLED_MQTT__BROKER_HOST=localhost
|
# LEDGRAB_MQTT__BROKER_HOST=localhost
|
||||||
# WLED_MQTT__BROKER_PORT=1883
|
# LEDGRAB_MQTT__BROKER_PORT=1883
|
||||||
# WLED_MQTT__USERNAME=
|
# LEDGRAB_MQTT__USERNAME=
|
||||||
# WLED_MQTT__PASSWORD=
|
# LEDGRAB_MQTT__PASSWORD=
|
||||||
# WLED_MQTT__CLIENT_ID=ledgrab
|
# LEDGRAB_MQTT__CLIENT_ID=ledgrab
|
||||||
# WLED_MQTT__BASE_TOPIC=ledgrab
|
# LEDGRAB_MQTT__BASE_TOPIC=ledgrab
|
||||||
|
|
||||||
# ── Logging ─────────────────────────────────────────────
|
# ── Logging ─────────────────────────────────────────────
|
||||||
# WLED_LOGGING__FORMAT=json # json or text (default: json)
|
# LEDGRAB_LOGGING__FORMAT=json # json or text (default: json)
|
||||||
# WLED_LOGGING__FILE=logs/wled_controller.log
|
# LEDGRAB_LOGGING__FILE=logs/wled_controller.log
|
||||||
# WLED_LOGGING__MAX_SIZE_MB=100
|
# LEDGRAB_LOGGING__MAX_SIZE_MB=100
|
||||||
# WLED_LOGGING__BACKUP_COUNT=5
|
# LEDGRAB_LOGGING__BACKUP_COUNT=5
|
||||||
|
|
||||||
# ── Demo mode ───────────────────────────────────────────
|
# ── 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 ───────────────────────────────
|
# ── 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
|
# DISPLAY=:0 # X11 display for Linux screen capture
|
||||||
|
|||||||
+10
-10
@@ -1,15 +1,15 @@
|
|||||||
# Claude Instructions for WLED Screen Controller Server
|
# Claude Instructions for LedGrab Server
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
- `src/wled_controller/main.py` — FastAPI application entry point
|
- `src/ledgrab/main.py` — FastAPI application entry point
|
||||||
- `src/wled_controller/api/routes/` — REST API endpoints (one file per entity)
|
- `src/ledgrab/api/routes/` — REST API endpoints (one file per entity)
|
||||||
- `src/wled_controller/api/schemas/` — Pydantic request/response models (one file per entity)
|
- `src/ledgrab/api/schemas/` — Pydantic request/response models (one file per entity)
|
||||||
- `src/wled_controller/core/` — Core business logic (capture, devices, audio, processing, automations)
|
- `src/ledgrab/core/` — Core business logic (capture, devices, audio, processing, automations)
|
||||||
- `src/wled_controller/storage/` — Data models (dataclasses) and JSON persistence stores
|
- `src/ledgrab/storage/` — Data models (dataclasses) and JSON persistence stores
|
||||||
- `src/wled_controller/utils/` — Utility functions (logging, monitor detection, SSRF validation, sound playback)
|
- `src/ledgrab/utils/` — Utility functions (logging, monitor detection, SSRF validation, sound playback)
|
||||||
- `src/wled_controller/static/` — Frontend files (TypeScript, CSS, locales)
|
- `src/ledgrab/static/` — Frontend files (TypeScript, CSS, locales)
|
||||||
- `src/wled_controller/templates/` — Jinja2 HTML templates
|
- `src/ledgrab/templates/` — Jinja2 HTML templates
|
||||||
- `config/` — Configuration files (YAML)
|
- `config/` — Configuration files (YAML)
|
||||||
- `data/` — Runtime data (JSON stores, persisted state)
|
- `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.
|
Server uses API key authentication via Bearer token in `Authorization` header.
|
||||||
|
|
||||||
- Config: `config/default_config.yaml` under `auth.api_keys`
|
- 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
|
- 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"`)
|
- To enable auth, add key entries (e.g. `dev: "your-secret-key"`)
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -4,7 +4,7 @@ WORKDIR /build
|
|||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
RUN npm ci --ignore-scripts
|
RUN npm ci --ignore-scripts
|
||||||
COPY esbuild.mjs tsconfig.json ./
|
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
|
RUN npm run build
|
||||||
|
|
||||||
## Stage 2: Python application
|
## 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.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.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.version="${APP_VERSION}"
|
||||||
LABEL org.opencontainers.image.url="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/wled-screen-controller-mixed"
|
LABEL org.opencontainers.image.source="https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab"
|
||||||
LABEL org.opencontainers.image.licenses="MIT"
|
LABEL org.opencontainers.image.licenses="MIT"
|
||||||
|
|
||||||
WORKDIR /app
|
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.
|
# The real source is copied afterward, keeping the dep layer cached.
|
||||||
COPY pyproject.toml .
|
COPY pyproject.toml .
|
||||||
RUN sed -i "s/^version = .*/version = \"${APP_VERSION}\"/" 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]" \
|
&& 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 source code and config (invalidates cache only when source changes)
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
COPY config/ ./config/
|
COPY config/ ./config/
|
||||||
|
|
||||||
# Copy built frontend bundle from stage 1
|
# 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
|
# Create non-root user for security
|
||||||
RUN groupadd --gid 1000 ledgrab \
|
RUN groupadd --gid 1000 ledgrab \
|
||||||
@@ -67,4 +67,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
|||||||
ENV PYTHONPATH=/app/src
|
ENV PYTHONPATH=/app/src
|
||||||
|
|
||||||
# Run the application
|
# 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
@@ -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.
|
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
|
set PYTHONPATH=%CD%\src # Windows
|
||||||
|
|
||||||
# Run server
|
# 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
|
## Installation
|
||||||
@@ -85,20 +85,20 @@ storage:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
format: "json"
|
format: "json"
|
||||||
file: "logs/wled_controller.log"
|
file: "logs/ledgrab.log"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Server configuration
|
# Server configuration
|
||||||
export WLED_SERVER__HOST="0.0.0.0"
|
export LEDGRAB_SERVER__HOST="0.0.0.0"
|
||||||
export WLED_SERVER__PORT=8080
|
export LEDGRAB_SERVER__PORT=8080
|
||||||
export WLED_SERVER__LOG_LEVEL="INFO"
|
export LEDGRAB_SERVER__LOG_LEVEL="INFO"
|
||||||
|
|
||||||
# Processing configuration
|
# Processing configuration
|
||||||
export WLED_PROCESSING__DEFAULT_FPS=30
|
export LEDGRAB_PROCESSING__DEFAULT_FPS=30
|
||||||
export WLED_PROCESSING__BORDER_WIDTH=10
|
export LEDGRAB_PROCESSING__BORDER_WIDTH=10
|
||||||
|
|
||||||
# WLED configuration
|
# WLED configuration
|
||||||
export WLED_WLED__TIMEOUT=5
|
export WLED_WLED__TIMEOUT=5
|
||||||
@@ -147,7 +147,7 @@ curl http://localhost:8080/api/v1/devices/{device_id}/state
|
|||||||
pytest
|
pytest
|
||||||
|
|
||||||
# Run with coverage
|
# Run with coverage
|
||||||
pytest --cov=wled_controller --cov-report=html
|
pytest --cov=ledgrab --cov-report=html
|
||||||
|
|
||||||
# Run specific test
|
# Run specific test
|
||||||
pytest tests/test_screen_capture.py -v
|
pytest tests/test_screen_capture.py -v
|
||||||
@@ -158,7 +158,7 @@ pytest tests/test_screen_capture.py -v
|
|||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/wled_controller/
|
src/ledgrab/
|
||||||
├── main.py # FastAPI application
|
├── main.py # FastAPI application
|
||||||
├── config.py # Configuration
|
├── config.py # Configuration
|
||||||
├── api/ # API routes
|
├── api/ # API routes
|
||||||
@@ -188,4 +188,4 @@ MIT - see [../LICENSE](../LICENSE)
|
|||||||
## Support
|
## Support
|
||||||
|
|
||||||
- 📖 [Full Documentation](../docs/)
|
- 📖 [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)
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ mqtt:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
format: "json" # json or text
|
format: "json" # json or text
|
||||||
file: "logs/wled_controller.log"
|
file: "logs/ledgrab.log"
|
||||||
max_size_mb: 100
|
max_size_mb: 100
|
||||||
backup_count: 5
|
backup_count: 5
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Demo mode configuration
|
# 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
|
# Uses isolated data directory (data/demo/) and a pre-configured API key
|
||||||
# so the demo works out of the box with zero setup.
|
# so the demo works out of the box with zero setup.
|
||||||
|
|
||||||
@@ -26,6 +26,6 @@ mqtt:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
format: "text"
|
format: "text"
|
||||||
file: "logs/wled_controller.log"
|
file: "logs/ledgrab.log"
|
||||||
max_size_mb: 100
|
max_size_mb: 100
|
||||||
backup_count: 5
|
backup_count: 5
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ storage:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
format: "text"
|
format: "text"
|
||||||
file: "logs/wled_test.log"
|
file: "logs/ledgrab_test.log"
|
||||||
max_size_mb: 10
|
max_size_mb: 10
|
||||||
backup_count: 2
|
backup_count: 2
|
||||||
|
|||||||
+17
-17
@@ -1,14 +1,14 @@
|
|||||||
services:
|
services:
|
||||||
wled-controller:
|
ledgrab:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: ledgrab:latest
|
image: ledgrab:latest
|
||||||
container_name: wled-screen-controller
|
container_name: ledgrab
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "${WLED_PORT:-8080}:8080"
|
- "${LEDGRAB_PORT:-8080}:8080"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
# Persist device data and configuration across restarts
|
# Persist device data and configuration across restarts
|
||||||
@@ -22,37 +22,37 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
## Server
|
## Server
|
||||||
# Bind address and port (usually no need to change)
|
# Bind address and port (usually no need to change)
|
||||||
- WLED_SERVER__HOST=0.0.0.0
|
- LEDGRAB_SERVER__HOST=0.0.0.0
|
||||||
- WLED_SERVER__PORT=8080
|
- LEDGRAB_SERVER__PORT=8080
|
||||||
- WLED_SERVER__LOG_LEVEL=INFO
|
- LEDGRAB_SERVER__LOG_LEVEL=INFO
|
||||||
# CORS origins — add your LAN IP for remote access, e.g.:
|
# 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
|
## Auth
|
||||||
# Override the default API key (STRONGLY recommended for production):
|
# 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
|
# Generate a key: openssl rand -hex 32
|
||||||
|
|
||||||
## Display (Linux X11 only)
|
## Display (Linux X11 only)
|
||||||
- DISPLAY=${DISPLAY:-:0}
|
- DISPLAY=${DISPLAY:-:0}
|
||||||
|
|
||||||
## Processing defaults
|
## Processing defaults
|
||||||
#- WLED_PROCESSING__DEFAULT_FPS=30
|
#- LEDGRAB_PROCESSING__DEFAULT_FPS=30
|
||||||
#- WLED_PROCESSING__BORDER_WIDTH=10
|
#- LEDGRAB_PROCESSING__BORDER_WIDTH=10
|
||||||
|
|
||||||
## MQTT (optional — for Home Assistant auto-discovery)
|
## MQTT (optional — for Home Assistant auto-discovery)
|
||||||
#- WLED_MQTT__ENABLED=true
|
#- LEDGRAB_MQTT__ENABLED=true
|
||||||
#- WLED_MQTT__BROKER_HOST=192.168.1.2
|
#- LEDGRAB_MQTT__BROKER_HOST=192.168.1.2
|
||||||
#- WLED_MQTT__BROKER_PORT=1883
|
#- LEDGRAB_MQTT__BROKER_PORT=1883
|
||||||
#- WLED_MQTT__USERNAME=
|
#- LEDGRAB_MQTT__USERNAME=
|
||||||
#- WLED_MQTT__PASSWORD=
|
#- LEDGRAB_MQTT__PASSWORD=
|
||||||
|
|
||||||
# Uncomment for Linux screen capture (requires host network for X11 access)
|
# Uncomment for Linux screen capture (requires host network for X11 access)
|
||||||
# network_mode: host
|
# network_mode: host
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- wled-network
|
- ledgrab-network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
wled-network:
|
ledgrab-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# API Authentication Guide
|
# 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
|
## 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`:
|
The integration will prompt for an API key during setup if authentication is enabled. You can also configure it in `configuration.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
wled_screen_controller:
|
ledgrab:
|
||||||
server_url: "http://192.168.1.100:8080"
|
server_url: "http://192.168.1.100:8080"
|
||||||
api_key: "your-api-key-here" # Optional, only if auth is enabled
|
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:
|
services:
|
||||||
wled-controller:
|
wled-controller:
|
||||||
environment:
|
environment:
|
||||||
- WLED_AUTH__ENABLED=true
|
- LEDGRAB_AUTH__ENABLED=true
|
||||||
- WLED_AUTH__API_KEYS__0=your-key-here
|
- LEDGRAB_AUTH__API_KEYS__0=your-key-here
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use Docker secrets for better security.
|
Or use Docker secrets for better security.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import * as esbuild from 'esbuild';
|
import * as esbuild from 'esbuild';
|
||||||
|
|
||||||
const srcDir = 'src/wled_controller/static';
|
const srcDir = 'src/ledgrab/static';
|
||||||
const outDir = `${srcDir}/dist`;
|
const outDir = `${srcDir}/dist`;
|
||||||
|
|
||||||
const watch = process.argv.includes('--watch');
|
const watch = process.argv.includes('--watch');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ requires = ["setuptools>=68.0", "wheel"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "wled-screen-controller"
|
name = "ledgrab"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
description = "Ambient lighting system that captures screen content and drives LED strips in real time"
|
description = "Ambient lighting system that captures screen content and drives LED strips in real time"
|
||||||
authors = [
|
authors = [
|
||||||
@@ -83,10 +83,10 @@ perf = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
|
Homepage = "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab"
|
||||||
Repository = "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
|
Repository = "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab"
|
||||||
Documentation = "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/src/branch/master/INSTALLATION.md"
|
Documentation = "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/src/branch/master/INSTALLATION.md"
|
||||||
Issues = "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/issues"
|
Issues = "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/issues"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
package-dir = {"" = "src"}
|
package-dir = {"" = "src"}
|
||||||
@@ -97,7 +97,7 @@ where = ["src"]
|
|||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
asyncio_mode = "auto"
|
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]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
|||||||
+8
-8
@@ -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),
|
# Uses graceful shutdown first (lets the server persist data to disk),
|
||||||
# then force-kills as a fallback.
|
# 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
|
# Read API key from config for authenticated shutdown request
|
||||||
$configPath = Join-Path $serverRoot 'config\default_config.yaml'
|
$configPath = Join-Path $serverRoot 'config\default_config.yaml'
|
||||||
@@ -20,7 +20,7 @@ if (Test-Path $configPath) {
|
|||||||
|
|
||||||
# Find running server processes
|
# Find running server processes
|
||||||
$procs = Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
|
$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) {
|
if ($procs) {
|
||||||
# Step 1: Request graceful shutdown via API (triggers lifespan shutdown + store save)
|
# Step 1: Request graceful shutdown via API (triggers lifespan shutdown + store save)
|
||||||
@@ -46,7 +46,7 @@ if ($procs) {
|
|||||||
Start-Sleep -Seconds 1
|
Start-Sleep -Seconds 1
|
||||||
$waited++
|
$waited++
|
||||||
$still = Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
|
$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) {
|
if (-not $still) {
|
||||||
Write-Host " Server exited cleanly after ${waited}s"
|
Write-Host " Server exited cleanly after ${waited}s"
|
||||||
break
|
break
|
||||||
@@ -54,7 +54,7 @@ if ($procs) {
|
|||||||
}
|
}
|
||||||
# Step 3: Force-kill stragglers
|
# Step 3: Force-kill stragglers
|
||||||
$still = Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
|
$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) {
|
if ($still) {
|
||||||
Write-Host " Force-killing remaining processes..."
|
Write-Host " Force-killing remaining processes..."
|
||||||
foreach ($p in $still) {
|
foreach ($p in $still) {
|
||||||
@@ -85,13 +85,13 @@ if ($regUser) {
|
|||||||
|
|
||||||
# Start server detached (set WLED_RESTART=1 to skip browser open)
|
# Start server detached (set WLED_RESTART=1 to skip browser open)
|
||||||
Write-Host "Starting server..."
|
Write-Host "Starting server..."
|
||||||
$env:WLED_RESTART = "1"
|
$env:LEDGRAB_RESTART = "1"
|
||||||
$pythonExe = (Get-Command python -ErrorAction SilentlyContinue).Source
|
$pythonExe = (Get-Command python -ErrorAction SilentlyContinue).Source
|
||||||
if (-not $pythonExe) {
|
if (-not $pythonExe) {
|
||||||
# Fallback to known install location
|
# Fallback to known install location
|
||||||
$pythonExe = "$env:LOCALAPPDATA\Programs\Python\Python313\python.exe"
|
$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 `
|
-WorkingDirectory $serverRoot `
|
||||||
-WindowStyle Hidden
|
-WindowStyle Hidden
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ Start-Sleep -Seconds 3
|
|||||||
|
|
||||||
# Verify it's running
|
# Verify it's running
|
||||||
$check = Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
|
$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) {
|
if ($check) {
|
||||||
Write-Host "Server started (PID $($check[0].ProcessId))"
|
Write-Host "Server started (PID $($check[0].ProcessId))"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+5
-5
@@ -1,25 +1,25 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Restart the WLED Screen Controller server (Linux/macOS)
|
# Restart the LedGrab server (Linux/macOS)
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
# Stop any running instance
|
# 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
|
if [ -n "$PIDS" ]; then
|
||||||
echo "Stopping server (PID $PIDS)..."
|
echo "Stopping server (PID $PIDS)..."
|
||||||
pkill -f 'wled_controller\.main' 2>/dev/null || true
|
pkill -f 'ledgrab\.main' 2>/dev/null || true
|
||||||
sleep 2
|
sleep 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start server detached
|
# Start server detached
|
||||||
echo "Starting server..."
|
echo "Starting server..."
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
nohup python -m wled_controller.main > /dev/null 2>&1 &
|
nohup python -m ledgrab.main > /dev/null 2>&1 &
|
||||||
sleep 3
|
sleep 3
|
||||||
|
|
||||||
# Verify it's running
|
# 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
|
if [ -n "$NEW_PID" ]; then
|
||||||
echo "Server started (PID $NEW_PID)"
|
echo "Server started (PID $NEW_PID)"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
REM WLED Screen Controller Restart Script
|
REM LedGrab Restart Script
|
||||||
REM This script restarts the WLED screen controller server
|
REM This script restarts the WLED screen controller server
|
||||||
|
|
||||||
echo Restarting WLED Screen Controller...
|
echo Restarting LedGrab...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Stop the server first
|
REM Stop the server first
|
||||||
@@ -18,7 +18,7 @@ cd /d "%~dp0\.."
|
|||||||
REM Start the server
|
REM Start the server
|
||||||
echo.
|
echo.
|
||||||
echo [2/2] Starting server...
|
echo [2/2] Starting server...
|
||||||
python -m wled_controller
|
python -m ledgrab
|
||||||
|
|
||||||
REM If the server exits, pause to show any error messages
|
REM If the server exits, pause to show any error messages
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ WshShell.CurrentDirectory = appRoot
|
|||||||
' Set env vars for the child process (inherited via WshShell.Run)
|
' Set env vars for the child process (inherited via WshShell.Run)
|
||||||
Set procEnv = WshShell.Environment("Process")
|
Set procEnv = WshShell.Environment("Process")
|
||||||
procEnv("PYTHONPATH") = appRoot & "\app\src"
|
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.
|
' Use embedded python.exe (NOT pythonw.exe) with WindowStyle=0.
|
||||||
' Same pattern as the Media Server sibling app.
|
' Same pattern as the Media Server sibling app.
|
||||||
embeddedPython = appRoot & "\python\python.exe"
|
embeddedPython = appRoot & "\python\python.exe"
|
||||||
If fso.FileExists(embeddedPython) Then
|
If fso.FileExists(embeddedPython) Then
|
||||||
WshShell.Run """" & embeddedPython & """ -m wled_controller", 0, False
|
WshShell.Run """" & embeddedPython & """ -m ledgrab", 0, False
|
||||||
Else
|
Else
|
||||||
WshShell.Run "python -m wled_controller", 0, False
|
WshShell.Run "python -m ledgrab", 0, False
|
||||||
End If
|
End If
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ Set WshShell = CreateObject("WScript.Shell")
|
|||||||
Set FSO = CreateObject("Scripting.FileSystemObject")
|
Set FSO = CreateObject("Scripting.FileSystemObject")
|
||||||
' Get parent folder of scripts folder (server root)
|
' Get parent folder of scripts folder (server root)
|
||||||
WshShell.CurrentDirectory = FSO.GetParentFolderName(FSO.GetParentFolderName(WScript.ScriptFullName))
|
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 FSO = Nothing
|
||||||
Set WshShell = Nothing
|
Set WshShell = Nothing
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
@echo off
|
@echo off
|
||||||
REM WLED Screen Controller Startup Script
|
REM LedGrab Startup Script
|
||||||
REM This script starts the WLED screen controller server
|
REM This script starts the WLED screen controller server
|
||||||
|
|
||||||
echo Starting WLED Screen Controller...
|
echo Starting LedGrab...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Change to the server directory (parent of scripts folder)
|
REM Change to the server directory (parent of scripts folder)
|
||||||
cd /d "%~dp0\.."
|
cd /d "%~dp0\.."
|
||||||
|
|
||||||
REM Start the server
|
REM Start the server
|
||||||
python -m wled_controller
|
python -m ledgrab
|
||||||
|
|
||||||
REM If the server exits, pause to show any error messages
|
REM If the server exits, pause to show any error messages
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
@echo off
|
@echo off
|
||||||
REM WLED Screen Controller Stop Script
|
REM LedGrab Stop Script
|
||||||
REM This script stops the running WLED screen controller server
|
REM This script stops the running WLED screen controller server
|
||||||
|
|
||||||
echo Stopping WLED Screen Controller...
|
echo Stopping LedGrab...
|
||||||
echo.
|
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 (
|
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 (
|
if not errorlevel 1 (
|
||||||
taskkill /PID %%i /F
|
taskkill /PID %%i /F
|
||||||
echo WLED controller process (PID %%i) terminated.
|
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.
|
||||||
echo Done! WLED Screen Controller stopped.
|
echo Done! LedGrab stopped.
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from importlib.metadata import version, PackageNotFoundError
|
from importlib.metadata import version, PackageNotFoundError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
__version__ = version("wled-screen-controller")
|
__version__ = version("ledgrab")
|
||||||
except PackageNotFoundError:
|
except PackageNotFoundError:
|
||||||
# Running from source without pip install (e.g. dev, embedded Python)
|
# Running from source without pip install (e.g. dev, embedded Python)
|
||||||
__version__ = "0.0.0-dev"
|
__version__ = "0.0.0-dev"
|
||||||
@@ -13,6 +13,6 @@ __email__ = "dolgolyov.alexei@gmail.com"
|
|||||||
|
|
||||||
# ─── Project links ───────────────────────────────────────────
|
# ─── Project links ───────────────────────────────────────────
|
||||||
GITEA_BASE_URL = "https://git.dolgolyov-family.by"
|
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}"
|
REPO_URL = f"{GITEA_BASE_URL}/{GITEA_REPO}"
|
||||||
DONATE_URL = "" # TODO: set once donation platform is chosen
|
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,
|
Starts the uvicorn server and, on Windows when *pystray* is installed,
|
||||||
shows a system-tray icon with **Show UI** / **Exit** actions.
|
shows a system-tray icon with **Show UI** / **Exit** actions.
|
||||||
@@ -36,10 +36,10 @@ _fix_embedded_tcl_paths()
|
|||||||
|
|
||||||
import uvicorn # noqa: E402
|
import uvicorn # noqa: E402
|
||||||
|
|
||||||
from wled_controller.config import get_config # noqa: E402
|
from ledgrab.config import get_config # noqa: E402
|
||||||
from wled_controller.server_ref import set_server, set_tray # noqa: E402
|
from ledgrab.server_ref import set_server, set_tray # noqa: E402
|
||||||
from wled_controller.tray import PYSTRAY_AVAILABLE, TrayManager # noqa: E402
|
from ledgrab.tray import PYSTRAY_AVAILABLE, TrayManager # noqa: E402
|
||||||
from wled_controller.utils import setup_logging, get_logger # noqa: E402
|
from ledgrab.utils import setup_logging, get_logger # noqa: E402
|
||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -62,7 +62,7 @@ def _open_browser(port: int, delay: float = 2.0) -> None:
|
|||||||
|
|
||||||
def _is_restart() -> bool:
|
def _is_restart() -> bool:
|
||||||
"""Detect if this is a restart (vs first launch)."""
|
"""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:
|
def _check_port(host: str, port: int) -> None:
|
||||||
@@ -81,7 +81,7 @@ def main() -> None:
|
|||||||
_check_port(config.server.host, config.server.port)
|
_check_port(config.server.host, config.server.port)
|
||||||
|
|
||||||
uv_config = uvicorn.Config(
|
uv_config = uvicorn.Config(
|
||||||
"wled_controller.main:app",
|
"ledgrab.main:app",
|
||||||
host=config.server.host,
|
host=config.server.host,
|
||||||
port=config.server.port,
|
port=config.server.port,
|
||||||
log_level=config.server.log_level.lower(),
|
log_level=config.server.log_level.lower(),
|
||||||
@@ -133,10 +133,10 @@ def _request_shutdown(server: uvicorn.Server) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _force_tray() -> bool:
|
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
|
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__":
|
if __name__ == "__main__":
|
||||||
@@ -6,8 +6,8 @@ from typing import Annotated
|
|||||||
from fastapi import Depends, HTTPException, Security, status
|
from fastapi import Depends, HTTPException, Security, status
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
|
||||||
from wled_controller.config import get_config
|
from ledgrab.config import get_config
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
+31
-31
@@ -6,39 +6,39 @@ All getter function signatures remain unchanged for FastAPI Depends() compatibil
|
|||||||
|
|
||||||
from typing import Any, Dict, TypeVar
|
from typing import Any, Dict, TypeVar
|
||||||
|
|
||||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||||
from wled_controller.storage.database import Database
|
from ledgrab.storage.database import Database
|
||||||
from wled_controller.storage import DeviceStore
|
from ledgrab.storage import DeviceStore
|
||||||
from wled_controller.storage.template_store import TemplateStore
|
from ledgrab.storage.template_store import TemplateStore
|
||||||
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
|
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||||
from wled_controller.storage.audio_source_store import AudioSourceStore
|
from ledgrab.storage.audio_source_store import AudioSourceStore
|
||||||
from wled_controller.storage.audio_template_store import AudioTemplateStore
|
from ledgrab.storage.audio_template_store import AudioTemplateStore
|
||||||
from wled_controller.storage.value_source_store import ValueSourceStore
|
from ledgrab.storage.value_source_store import ValueSourceStore
|
||||||
from wled_controller.storage.automation_store import AutomationStore
|
from ledgrab.storage.automation_store import AutomationStore
|
||||||
from wled_controller.storage.scene_preset_store import ScenePresetStore
|
from ledgrab.storage.scene_preset_store import ScenePresetStore
|
||||||
from wled_controller.storage.sync_clock_store import SyncClockStore
|
from ledgrab.storage.sync_clock_store import SyncClockStore
|
||||||
from wled_controller.storage.color_strip_processing_template_store import (
|
from ledgrab.storage.color_strip_processing_template_store import (
|
||||||
ColorStripProcessingTemplateStore,
|
ColorStripProcessingTemplateStore,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.gradient_store import GradientStore
|
from ledgrab.storage.gradient_store import GradientStore
|
||||||
from wled_controller.storage.weather_source_store import WeatherSourceStore
|
from ledgrab.storage.weather_source_store import WeatherSourceStore
|
||||||
from wled_controller.storage.asset_store import AssetStore
|
from ledgrab.storage.asset_store import AssetStore
|
||||||
from wled_controller.core.automations.automation_engine import AutomationEngine
|
from ledgrab.core.automations.automation_engine import AutomationEngine
|
||||||
from wled_controller.core.weather.weather_manager import WeatherManager
|
from ledgrab.core.weather.weather_manager import WeatherManager
|
||||||
from wled_controller.core.backup.auto_backup import AutoBackupEngine
|
from ledgrab.core.backup.auto_backup import AutoBackupEngine
|
||||||
from wled_controller.core.processing.sync_clock_manager import SyncClockManager
|
from ledgrab.core.processing.sync_clock_manager import SyncClockManager
|
||||||
from wled_controller.core.update.update_service import UpdateService
|
from ledgrab.core.update.update_service import UpdateService
|
||||||
from wled_controller.storage.home_assistant_store import HomeAssistantStore
|
from ledgrab.storage.home_assistant_store import HomeAssistantStore
|
||||||
from wled_controller.core.home_assistant.ha_manager import HomeAssistantManager
|
from ledgrab.core.home_assistant.ha_manager import HomeAssistantManager
|
||||||
from wled_controller.storage.game_integration_store import GameIntegrationStore
|
from ledgrab.storage.game_integration_store import GameIntegrationStore
|
||||||
from wled_controller.core.game_integration.event_bus import GameEventBus
|
from ledgrab.core.game_integration.event_bus import GameEventBus
|
||||||
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
from ledgrab.storage.mqtt_source_store import MQTTSourceStore
|
||||||
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
from ledgrab.core.mqtt.mqtt_manager import MQTTManager
|
||||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||||
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
from ledgrab.storage.pattern_template_store import PatternTemplateStore
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
+13
-8
@@ -8,9 +8,9 @@ from typing import Callable, Optional
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from starlette.websockets import WebSocket
|
from starlette.websockets import WebSocket
|
||||||
|
|
||||||
from wled_controller.core.filters import FilterRegistry, ImagePool
|
from ledgrab.core.filters import FilterRegistry, ImagePool
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.utils.image_codec import (
|
from ledgrab.utils.image_codec import (
|
||||||
encode_jpeg,
|
encode_jpeg,
|
||||||
encode_jpeg_data_uri,
|
encode_jpeg_data_uri,
|
||||||
resize_down,
|
resize_down,
|
||||||
@@ -31,7 +31,8 @@ def authenticate_ws_token(token: str) -> bool:
|
|||||||
|
|
||||||
Delegates to the canonical implementation in auth module.
|
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)
|
return verify_ws_token(token)
|
||||||
|
|
||||||
|
|
||||||
@@ -160,14 +161,16 @@ async def stream_capture_test(
|
|||||||
thumb_uri = _encode_jpeg(thumb, PREVIEW_JPEG_QUALITY)
|
thumb_uri = _encode_jpeg(thumb, PREVIEW_JPEG_QUALITY)
|
||||||
fps = fc / elapsed if elapsed > 0 else 0
|
fps = fc / elapsed if elapsed > 0 else 0
|
||||||
avg_ms = (tc / fc * 1000) if fc > 0 else 0
|
avg_ms = (tc / fc * 1000) if fc > 0 else 0
|
||||||
await websocket.send_json({
|
await websocket.send_json(
|
||||||
|
{
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"thumbnail": thumb_uri,
|
"thumbnail": thumb_uri,
|
||||||
"frame_count": fc,
|
"frame_count": fc,
|
||||||
"elapsed_s": round(elapsed, 2),
|
"elapsed_s": round(elapsed, 2),
|
||||||
"fps": round(fps, 1),
|
"fps": round(fps, 1),
|
||||||
"avg_capture_ms": round(avg_ms, 1),
|
"avg_capture_ms": round(avg_ms, 1),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Wait for capture thread to fully finish
|
# Wait for capture thread to fully finish
|
||||||
await capture_future
|
await capture_future
|
||||||
@@ -199,7 +202,8 @@ async def stream_capture_test(
|
|||||||
thumb = _make_thumbnail(final_frame, FINAL_THUMBNAIL_WIDTH)
|
thumb = _make_thumbnail(final_frame, FINAL_THUMBNAIL_WIDTH)
|
||||||
thumb_uri = _encode_jpeg(thumb, 85)
|
thumb_uri = _encode_jpeg(thumb, 85)
|
||||||
|
|
||||||
await websocket.send_json({
|
await websocket.send_json(
|
||||||
|
{
|
||||||
"type": "result",
|
"type": "result",
|
||||||
"full_image": full_uri,
|
"full_image": full_uri,
|
||||||
"thumbnail": thumb_uri,
|
"thumbnail": thumb_uri,
|
||||||
@@ -209,7 +213,8 @@ async def stream_capture_test(
|
|||||||
"elapsed_s": round(elapsed, 2),
|
"elapsed_s": round(elapsed, 2),
|
||||||
"fps": round(fps, 1),
|
"fps": round(fps, 1),
|
||||||
"avg_capture_ms": round(avg_ms, 1),
|
"avg_capture_ms": round(avg_ms, 1),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# WebSocket disconnect or send error — signal capture thread to stop
|
# WebSocket disconnect or send error — signal capture thread to stop
|
||||||
+10
-8
@@ -5,17 +5,17 @@ from pathlib import Path
|
|||||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import fire_entity_event, get_asset_store
|
from ledgrab.api.dependencies import fire_entity_event, get_asset_store
|
||||||
from wled_controller.api.schemas.assets import (
|
from ledgrab.api.schemas.assets import (
|
||||||
AssetListResponse,
|
AssetListResponse,
|
||||||
AssetResponse,
|
AssetResponse,
|
||||||
AssetUpdate,
|
AssetUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.config import get_config
|
from ledgrab.config import get_config
|
||||||
from wled_controller.storage.asset_store import AssetStore
|
from ledgrab.storage.asset_store import AssetStore
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -103,7 +103,9 @@ async def upload_asset(
|
|||||||
if not data:
|
if not data:
|
||||||
raise HTTPException(status_code=400, detail="Empty file")
|
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:
|
try:
|
||||||
asset = store.create_asset(
|
asset = store.create_asset(
|
||||||
+2
-2
@@ -4,8 +4,8 @@ import asyncio
|
|||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.core.audio.audio_capture import AudioCaptureManager
|
from ledgrab.core.audio.audio_capture import AudioCaptureManager
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
+5
-5
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import get_audio_processing_template_store
|
from ledgrab.api.dependencies import get_audio_processing_template_store
|
||||||
from wled_controller.api.schemas.filters import (
|
from ledgrab.api.schemas.filters import (
|
||||||
FilterOptionDefSchema,
|
FilterOptionDefSchema,
|
||||||
FilterTypeListResponse,
|
FilterTypeListResponse,
|
||||||
FilterTypeResponse,
|
FilterTypeResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.audio.filters import AudioFilterRegistry
|
from ledgrab.core.audio.filters import AudioFilterRegistry
|
||||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
+8
-8
@@ -2,24 +2,24 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_audio_processing_template_store,
|
get_audio_processing_template_store,
|
||||||
get_audio_source_store,
|
get_audio_source_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.audio_processing import (
|
from ledgrab.api.schemas.audio_processing import (
|
||||||
AudioProcessingTemplateCreate,
|
AudioProcessingTemplateCreate,
|
||||||
AudioProcessingTemplateListResponse,
|
AudioProcessingTemplateListResponse,
|
||||||
AudioProcessingTemplateResponse,
|
AudioProcessingTemplateResponse,
|
||||||
AudioProcessingTemplateUpdate,
|
AudioProcessingTemplateUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.filters import FilterInstanceSchema
|
from ledgrab.api.schemas.filters import FilterInstanceSchema
|
||||||
from wled_controller.core.filters.filter_instance import FilterInstance
|
from ledgrab.core.filters.filter_instance import FilterInstance
|
||||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
+11
-11
@@ -6,8 +6,8 @@ from typing import Annotated, Optional
|
|||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_audio_processing_template_store,
|
get_audio_processing_template_store,
|
||||||
get_audio_source_store,
|
get_audio_source_store,
|
||||||
@@ -15,7 +15,7 @@ from wled_controller.api.dependencies import (
|
|||||||
get_color_strip_store,
|
get_color_strip_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.audio_sources import (
|
from ledgrab.api.schemas.audio_sources import (
|
||||||
AudioSourceCreate,
|
AudioSourceCreate,
|
||||||
AudioSourceListResponse,
|
AudioSourceListResponse,
|
||||||
AudioSourceResponse,
|
AudioSourceResponse,
|
||||||
@@ -23,15 +23,15 @@ from wled_controller.api.schemas.audio_sources import (
|
|||||||
CaptureAudioSourceResponse,
|
CaptureAudioSourceResponse,
|
||||||
ProcessedAudioSourceResponse,
|
ProcessedAudioSourceResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.audio_source import (
|
from ledgrab.storage.audio_source import (
|
||||||
AudioSource,
|
AudioSource,
|
||||||
CaptureAudioSource,
|
CaptureAudioSource,
|
||||||
ProcessedAudioSource,
|
ProcessedAudioSource,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.audio_source_store import AudioSourceStore
|
from ledgrab.storage.audio_source_store import AudioSourceStore
|
||||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ async def delete_audio_source(
|
|||||||
"""Delete an audio source."""
|
"""Delete an audio source."""
|
||||||
try:
|
try:
|
||||||
# Check if any CSS entities reference this audio source
|
# 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():
|
for css in css_store.get_all_sources():
|
||||||
if (
|
if (
|
||||||
@@ -215,8 +215,8 @@ async def test_audio_source_ws(
|
|||||||
analysis before sending, so the WebSocket output matches what running
|
analysis before sending, so the WebSocket output matches what running
|
||||||
streams see.
|
streams see.
|
||||||
"""
|
"""
|
||||||
from wled_controller.api.auth import verify_ws_token
|
from ledgrab.api.auth import verify_ws_token
|
||||||
from wled_controller.core.audio.filters.pipeline import build_pipeline_from_template_ids
|
from ledgrab.core.audio.filters.pipeline import build_pipeline_from_template_ids
|
||||||
|
|
||||||
if not verify_ws_token(token):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
+87
-35
@@ -4,9 +4,14 @@ import asyncio
|
|||||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.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 ledgrab.api.dependencies import (
|
||||||
from wled_controller.api.schemas.audio_templates import (
|
fire_entity_event,
|
||||||
|
get_audio_template_store,
|
||||||
|
get_audio_source_store,
|
||||||
|
get_processor_manager,
|
||||||
|
)
|
||||||
|
from ledgrab.api.schemas.audio_templates import (
|
||||||
AudioEngineInfo,
|
AudioEngineInfo,
|
||||||
AudioEngineListResponse,
|
AudioEngineListResponse,
|
||||||
AudioTemplateCreate,
|
AudioTemplateCreate,
|
||||||
@@ -14,11 +19,11 @@ from wled_controller.api.schemas.audio_templates import (
|
|||||||
AudioTemplateResponse,
|
AudioTemplateResponse,
|
||||||
AudioTemplateUpdate,
|
AudioTemplateUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.core.audio.factory import AudioEngineRegistry
|
from ledgrab.core.audio.factory import AudioEngineRegistry
|
||||||
from wled_controller.storage.audio_template_store import AudioTemplateStore
|
from ledgrab.storage.audio_template_store import AudioTemplateStore
|
||||||
from wled_controller.storage.audio_source_store import AudioSourceStore
|
from ledgrab.storage.audio_source_store import AudioSourceStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -27,7 +32,10 @@ router = APIRouter()
|
|||||||
|
|
||||||
# ===== AUDIO TEMPLATE ENDPOINTS =====
|
# ===== 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(
|
async def list_audio_templates(
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
store: AudioTemplateStore = Depends(get_audio_template_store),
|
store: AudioTemplateStore = Depends(get_audio_template_store),
|
||||||
@@ -37,10 +45,14 @@ async def list_audio_templates(
|
|||||||
templates = store.get_all_templates()
|
templates = store.get_all_templates()
|
||||||
responses = [
|
responses = [
|
||||||
AudioTemplateResponse(
|
AudioTemplateResponse(
|
||||||
id=t.id, name=t.name, engine_type=t.engine_type,
|
id=t.id,
|
||||||
engine_config=t.engine_config, tags=t.tags,
|
name=t.name,
|
||||||
|
engine_type=t.engine_type,
|
||||||
|
engine_config=t.engine_config,
|
||||||
|
tags=t.tags,
|
||||||
created_at=t.created_at,
|
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
|
for t in templates
|
||||||
]
|
]
|
||||||
@@ -50,7 +62,12 @@ async def list_audio_templates(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def create_audio_template(
|
||||||
data: AudioTemplateCreate,
|
data: AudioTemplateCreate,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -59,16 +76,22 @@ async def create_audio_template(
|
|||||||
"""Create a new audio capture template."""
|
"""Create a new audio capture template."""
|
||||||
try:
|
try:
|
||||||
template = store.create_template(
|
template = store.create_template(
|
||||||
name=data.name, engine_type=data.engine_type,
|
name=data.name,
|
||||||
engine_config=data.engine_config, description=data.description,
|
engine_type=data.engine_type,
|
||||||
|
engine_config=data.engine_config,
|
||||||
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
)
|
)
|
||||||
fire_entity_event("audio_template", "created", template.id)
|
fire_entity_event("audio_template", "created", template.id)
|
||||||
return AudioTemplateResponse(
|
return AudioTemplateResponse(
|
||||||
id=template.id, name=template.name, engine_type=template.engine_type,
|
id=template.id,
|
||||||
engine_config=template.engine_config, tags=template.tags,
|
name=template.name,
|
||||||
|
engine_type=template.engine_type,
|
||||||
|
engine_config=template.engine_config,
|
||||||
|
tags=template.tags,
|
||||||
created_at=template.created_at,
|
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:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(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")
|
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(
|
async def get_audio_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -92,14 +119,22 @@ async def get_audio_template(
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail=f"Audio template {template_id} not found")
|
raise HTTPException(status_code=404, detail=f"Audio template {template_id} not found")
|
||||||
return AudioTemplateResponse(
|
return AudioTemplateResponse(
|
||||||
id=t.id, name=t.name, engine_type=t.engine_type,
|
id=t.id,
|
||||||
engine_config=t.engine_config, tags=t.tags,
|
name=t.name,
|
||||||
|
engine_type=t.engine_type,
|
||||||
|
engine_config=t.engine_config,
|
||||||
|
tags=t.tags,
|
||||||
created_at=t.created_at,
|
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(
|
async def update_audio_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
data: AudioTemplateUpdate,
|
data: AudioTemplateUpdate,
|
||||||
@@ -109,16 +144,23 @@ async def update_audio_template(
|
|||||||
"""Update an audio template."""
|
"""Update an audio template."""
|
||||||
try:
|
try:
|
||||||
t = store.update_template(
|
t = store.update_template(
|
||||||
template_id=template_id, name=data.name,
|
template_id=template_id,
|
||||||
engine_type=data.engine_type, engine_config=data.engine_config,
|
name=data.name,
|
||||||
description=data.description, tags=data.tags,
|
engine_type=data.engine_type,
|
||||||
|
engine_config=data.engine_config,
|
||||||
|
description=data.description,
|
||||||
|
tags=data.tags,
|
||||||
)
|
)
|
||||||
fire_entity_event("audio_template", "updated", template_id)
|
fire_entity_event("audio_template", "updated", template_id)
|
||||||
return AudioTemplateResponse(
|
return AudioTemplateResponse(
|
||||||
id=t.id, name=t.name, engine_type=t.engine_type,
|
id=t.id,
|
||||||
engine_config=t.engine_config, tags=t.tags,
|
name=t.name,
|
||||||
|
engine_type=t.engine_type,
|
||||||
|
engine_config=t.engine_config,
|
||||||
|
tags=t.tags,
|
||||||
created_at=t.created_at,
|
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:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
@@ -155,7 +197,10 @@ async def delete_audio_template(
|
|||||||
|
|
||||||
# ===== AUDIO ENGINE ENDPOINTS =====
|
# ===== 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):
|
async def list_audio_engines(_auth: AuthRequired):
|
||||||
"""List all registered audio capture engines."""
|
"""List all registered audio capture engines."""
|
||||||
try:
|
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.
|
Auth via ?token=<api_key>. Device specified via ?device_index=N&is_loopback=0|1.
|
||||||
Streams AudioAnalysis snapshots as JSON at ~20 Hz.
|
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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
return
|
return
|
||||||
@@ -214,13 +260,17 @@ async def test_audio_template_ws(
|
|||||||
loopback = is_loopback != 0
|
loopback = is_loopback != 0
|
||||||
|
|
||||||
try:
|
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:
|
except RuntimeError as e:
|
||||||
await websocket.close(code=4003, reason=str(e))
|
await websocket.close(code=4003, reason=str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
await websocket.accept()
|
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
|
last_ts = 0.0
|
||||||
try:
|
try:
|
||||||
@@ -228,13 +278,15 @@ async def test_audio_template_ws(
|
|||||||
analysis = stream.get_latest_analysis()
|
analysis = stream.get_latest_analysis()
|
||||||
if analysis is not None and analysis.timestamp != last_ts:
|
if analysis is not None and analysis.timestamp != last_ts:
|
||||||
last_ts = analysis.timestamp
|
last_ts = analysis.timestamp
|
||||||
await websocket.send_json({
|
await websocket.send_json(
|
||||||
|
{
|
||||||
"spectrum": analysis.spectrum.tolist(),
|
"spectrum": analysis.spectrum.tolist(),
|
||||||
"rms": round(analysis.rms, 4),
|
"rms": round(analysis.rms, 4),
|
||||||
"peak": round(analysis.peak, 4),
|
"peak": round(analysis.peak, 4),
|
||||||
"beat": analysis.beat,
|
"beat": analysis.beat,
|
||||||
"beat_intensity": round(analysis.beat_intensity, 4),
|
"beat_intensity": round(analysis.beat_intensity, 4),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
logger.debug("Audio template test WebSocket disconnected")
|
logger.debug("Audio template test WebSocket disconnected")
|
||||||
+10
-10
@@ -4,22 +4,22 @@ import secrets
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_automation_engine,
|
get_automation_engine,
|
||||||
get_automation_store,
|
get_automation_store,
|
||||||
get_scene_preset_store,
|
get_scene_preset_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.automations import (
|
from ledgrab.api.schemas.automations import (
|
||||||
AutomationCreate,
|
AutomationCreate,
|
||||||
AutomationListResponse,
|
AutomationListResponse,
|
||||||
AutomationResponse,
|
AutomationResponse,
|
||||||
AutomationUpdate,
|
AutomationUpdate,
|
||||||
RuleSchema,
|
RuleSchema,
|
||||||
)
|
)
|
||||||
from wled_controller.core.automations.automation_engine import AutomationEngine
|
from ledgrab.core.automations.automation_engine import AutomationEngine
|
||||||
from wled_controller.storage.automation import (
|
from ledgrab.storage.automation import (
|
||||||
ApplicationRule,
|
ApplicationRule,
|
||||||
DisplayStateRule,
|
DisplayStateRule,
|
||||||
HomeAssistantRule,
|
HomeAssistantRule,
|
||||||
@@ -30,10 +30,10 @@ from wled_controller.storage.automation import (
|
|||||||
TimeOfDayRule,
|
TimeOfDayRule,
|
||||||
WebhookRule,
|
WebhookRule,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.automation_store import AutomationStore
|
from ledgrab.storage.automation_store import AutomationStore
|
||||||
from wled_controller.storage.scene_preset_store import ScenePresetStore
|
from ledgrab.storage.scene_preset_store import ScenePresetStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -97,7 +97,7 @@ def _automation_to_response(
|
|||||||
for r in automation.rules:
|
for r in automation.rules:
|
||||||
if isinstance(r, WebhookRule) and r.token:
|
if isinstance(r, WebhookRule) and r.token:
|
||||||
# Prefer configured external URL, fall back to request base URL
|
# 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()
|
ext = load_external_url()
|
||||||
if ext:
|
if ext:
|
||||||
+26
-13
@@ -15,20 +15,20 @@ from pathlib import Path
|
|||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import get_asset_store, get_auto_backup_engine, get_database
|
from ledgrab.api.dependencies import get_asset_store, get_auto_backup_engine, get_database
|
||||||
from wled_controller.api.schemas.system import (
|
from ledgrab.api.schemas.system import (
|
||||||
AutoBackupSettings,
|
AutoBackupSettings,
|
||||||
AutoBackupStatusResponse,
|
AutoBackupStatusResponse,
|
||||||
BackupFileInfo,
|
BackupFileInfo,
|
||||||
BackupListResponse,
|
BackupListResponse,
|
||||||
RestoreResponse,
|
RestoreResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.config import get_config
|
from ledgrab.config import get_config
|
||||||
from wled_controller.core.backup.auto_backup import AutoBackupEngine
|
from ledgrab.core.backup.auto_backup import AutoBackupEngine
|
||||||
from wled_controller.storage.asset_store import AssetStore
|
from ledgrab.storage.asset_store import AssetStore
|
||||||
from wled_controller.storage.database import Database, freeze_writes
|
from ledgrab.storage.database import Database, freeze_writes
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -42,11 +42,17 @@ def _schedule_restart() -> None:
|
|||||||
|
|
||||||
def _restart():
|
def _restart():
|
||||||
import time
|
import time
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
subprocess.Popen(
|
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,
|
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -71,6 +77,7 @@ def backup_config(
|
|||||||
):
|
):
|
||||||
"""Download a full backup as a .zip containing the database and asset files."""
|
"""Download a full backup as a .zip containing the database and asset files."""
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
|
||||||
tmp_path = Path(tmp.name)
|
tmp_path = Path(tmp.name)
|
||||||
|
|
||||||
@@ -95,6 +102,7 @@ def backup_config(
|
|||||||
zip_buffer.seek(0)
|
zip_buffer.seek(0)
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
|
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
|
||||||
filename = f"ledgrab-backup-{timestamp}.zip"
|
filename = f"ledgrab-backup-{timestamp}.zip"
|
||||||
|
|
||||||
@@ -129,7 +137,9 @@ async def restore_config(
|
|||||||
is_sqlite = raw[:16].startswith(b"SQLite format 3")
|
is_sqlite = raw[:16].startswith(b"SQLite format 3")
|
||||||
|
|
||||||
if not is_zip and not is_sqlite:
|
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:
|
if is_zip:
|
||||||
# Extract DB and assets from ZIP
|
# Extract DB and assets from ZIP
|
||||||
@@ -160,6 +170,7 @@ async def restore_config(
|
|||||||
tmp_path = Path(tmp.name)
|
tmp_path = Path(tmp.name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
def _restore():
|
def _restore():
|
||||||
db.restore_from(tmp_path)
|
db.restore_from(tmp_path)
|
||||||
|
|
||||||
@@ -181,7 +192,8 @@ async def restore_config(
|
|||||||
@router.post("/api/v1/system/restart", tags=["System"])
|
@router.post("/api/v1/system/restart", tags=["System"])
|
||||||
def restart_server(_: AuthRequired):
|
def restart_server(_: AuthRequired):
|
||||||
"""Schedule a server restart and return immediately."""
|
"""Schedule a server restart and return immediately."""
|
||||||
from wled_controller.server_ref import _broadcast_restarting
|
from ledgrab.server_ref import _broadcast_restarting
|
||||||
|
|
||||||
_broadcast_restarting()
|
_broadcast_restarting()
|
||||||
_schedule_restart()
|
_schedule_restart()
|
||||||
return {"status": "restarting"}
|
return {"status": "restarting"}
|
||||||
@@ -190,7 +202,8 @@ def restart_server(_: AuthRequired):
|
|||||||
@router.post("/api/v1/system/shutdown", tags=["System"])
|
@router.post("/api/v1/system/shutdown", tags=["System"])
|
||||||
def shutdown_server(_: AuthRequired):
|
def shutdown_server(_: AuthRequired):
|
||||||
"""Gracefully shut down the server."""
|
"""Gracefully shut down the server."""
|
||||||
from wled_controller.server_ref import request_shutdown
|
from ledgrab.server_ref import request_shutdown
|
||||||
|
|
||||||
request_shutdown()
|
request_shutdown()
|
||||||
return {"status": "shutting_down"}
|
return {"status": "shutting_down"}
|
||||||
|
|
||||||
+48
-20
@@ -6,27 +6,27 @@ import uuid as _uuid
|
|||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_color_strip_store,
|
get_color_strip_store,
|
||||||
get_cspt_store,
|
get_cspt_store,
|
||||||
get_device_store,
|
get_device_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.filters import FilterInstanceSchema
|
from ledgrab.api.schemas.filters import FilterInstanceSchema
|
||||||
from wled_controller.api.schemas.color_strip_processing import (
|
from ledgrab.api.schemas.color_strip_processing import (
|
||||||
ColorStripProcessingTemplateCreate,
|
ColorStripProcessingTemplateCreate,
|
||||||
ColorStripProcessingTemplateListResponse,
|
ColorStripProcessingTemplateListResponse,
|
||||||
ColorStripProcessingTemplateResponse,
|
ColorStripProcessingTemplateResponse,
|
||||||
ColorStripProcessingTemplateUpdate,
|
ColorStripProcessingTemplateUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.core.filters import FilterInstance
|
from ledgrab.core.filters import FilterInstance
|
||||||
from wled_controller.storage.color_strip_processing_template_store import ColorStripProcessingTemplateStore
|
from ledgrab.storage.color_strip_processing_template_store import ColorStripProcessingTemplateStore
|
||||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||||
from wled_controller.storage import DeviceStore
|
from ledgrab.storage import DeviceStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
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(
|
async def list_cspt(
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
store: ColorStripProcessingTemplateStore = Depends(get_cspt_store),
|
store: ColorStripProcessingTemplateStore = Depends(get_cspt_store),
|
||||||
@@ -61,7 +65,12 @@ async def list_cspt(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def create_cspt(
|
||||||
data: ColorStripProcessingTemplateCreate,
|
data: ColorStripProcessingTemplateCreate,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -88,7 +97,11 @@ async def create_cspt(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def get_cspt(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -99,10 +112,16 @@ async def get_cspt(
|
|||||||
template = store.get_template(template_id)
|
template = store.get_template(template_id)
|
||||||
return _cspt_to_response(template)
|
return _cspt_to_response(template)
|
||||||
except ValueError:
|
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(
|
async def update_cspt(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
data: ColorStripProcessingTemplateUpdate,
|
data: ColorStripProcessingTemplateUpdate,
|
||||||
@@ -111,7 +130,11 @@ async def update_cspt(
|
|||||||
):
|
):
|
||||||
"""Update a color strip processing template."""
|
"""Update a color strip processing template."""
|
||||||
try:
|
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 = store.update_template(
|
||||||
template_id=template_id,
|
template_id=template_id,
|
||||||
name=data.name,
|
name=data.name,
|
||||||
@@ -131,7 +154,11 @@ async def update_cspt(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def delete_cspt(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -165,6 +192,7 @@ async def delete_cspt(
|
|||||||
|
|
||||||
# ── Test / Preview WebSocket ──────────────────────────────────────────
|
# ── Test / Preview WebSocket ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/api/v1/color-strip-processing-templates/{template_id}/test/ws")
|
@router.websocket("/api/v1/color-strip-processing-templates/{template_id}/test/ws")
|
||||||
async def test_cspt_ws(
|
async def test_cspt_ws(
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
@@ -179,9 +207,9 @@ async def test_cspt_ws(
|
|||||||
Takes an input CSS source, applies the CSPT filter chain, and streams
|
Takes an input CSS source, applies the CSPT filter chain, and streams
|
||||||
the processed RGB frames. Auth via ``?token=<api_key>``.
|
the processed RGB frames. Auth via ``?token=<api_key>``.
|
||||||
"""
|
"""
|
||||||
from wled_controller.api.auth import verify_ws_token
|
from ledgrab.api.auth import verify_ws_token
|
||||||
from wled_controller.core.filters import FilterRegistry
|
from ledgrab.core.filters import FilterRegistry
|
||||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||||
|
|
||||||
if not verify_ws_token(token):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
+42
-42
@@ -9,8 +9,8 @@ from typing import Annotated
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_color_strip_store,
|
get_color_strip_store,
|
||||||
get_device_store,
|
get_device_store,
|
||||||
@@ -20,7 +20,7 @@ from wled_controller.api.dependencies import (
|
|||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
get_template_store,
|
get_template_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.color_strip_sources import (
|
from ledgrab.api.schemas.color_strip_sources import (
|
||||||
ApiInputCSSResponse,
|
ApiInputCSSResponse,
|
||||||
AudioCSSResponse,
|
AudioCSSResponse,
|
||||||
CandlelightCSSResponse,
|
CandlelightCSSResponse,
|
||||||
@@ -47,17 +47,17 @@ from wled_controller.api.schemas.color_strip_sources import (
|
|||||||
StaticCSSResponse,
|
StaticCSSResponse,
|
||||||
WeatherCSSResponse,
|
WeatherCSSResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.devices import (
|
from ledgrab.api.schemas.devices import (
|
||||||
Calibration as CalibrationSchema,
|
Calibration as CalibrationSchema,
|
||||||
CalibrationTestModeResponse,
|
CalibrationTestModeResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.capture.calibration import (
|
from ledgrab.core.capture.calibration import (
|
||||||
calibration_from_dict,
|
calibration_from_dict,
|
||||||
calibration_to_dict,
|
calibration_to_dict,
|
||||||
)
|
)
|
||||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
from ledgrab.core.capture.screen_capture import get_available_displays
|
||||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||||
from wled_controller.storage.color_strip_source import (
|
from ledgrab.storage.color_strip_source import (
|
||||||
AdvancedPictureColorStripSource,
|
AdvancedPictureColorStripSource,
|
||||||
ApiInputColorStripSource,
|
ApiInputColorStripSource,
|
||||||
AudioColorStripSource,
|
AudioColorStripSource,
|
||||||
@@ -76,17 +76,17 @@ from wled_controller.storage.color_strip_source import (
|
|||||||
StaticColorStripSource,
|
StaticColorStripSource,
|
||||||
WeatherColorStripSource,
|
WeatherColorStripSource,
|
||||||
)
|
)
|
||||||
from wled_controller.storage import DeviceStore
|
from ledgrab.storage import DeviceStore
|
||||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||||
from wled_controller.storage.template_store import TemplateStore
|
from ledgrab.storage.template_store import TemplateStore
|
||||||
from wled_controller.storage.picture_source import (
|
from ledgrab.storage.picture_source import (
|
||||||
ProcessedPictureSource,
|
ProcessedPictureSource,
|
||||||
ScreenCapturePictureSource,
|
ScreenCapturePictureSource,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -371,7 +371,7 @@ async def create_color_strip_source(
|
|||||||
if data.source_type == "composite" and kwargs.get("layers"):
|
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")]
|
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
|
# 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:
|
for cid in child_ids:
|
||||||
depth = store.get_nesting_depth(cid)
|
depth = store.get_nesting_depth(cid)
|
||||||
@@ -524,19 +524,19 @@ async def test_key_colors_source(
|
|||||||
pp_template_store=Depends(get_pp_template_store),
|
pp_template_store=Depends(get_pp_template_store),
|
||||||
):
|
):
|
||||||
"""Test a key_colors source: capture a frame, extract colors from each rectangle."""
|
"""Test a key_colors source: capture a frame, extract colors from each rectangle."""
|
||||||
from wled_controller.storage.color_strip_source import KeyColorsColorStripSource
|
from ledgrab.storage.color_strip_source import KeyColorsColorStripSource
|
||||||
from wled_controller.core.capture.screen_capture import (
|
from ledgrab.core.capture.screen_capture import (
|
||||||
calculate_average_color,
|
calculate_average_color,
|
||||||
calculate_dominant_color,
|
calculate_dominant_color,
|
||||||
calculate_median_color,
|
calculate_median_color,
|
||||||
)
|
)
|
||||||
from wled_controller.core.capture_engines import EngineRegistry
|
from ledgrab.core.capture_engines import EngineRegistry
|
||||||
from wled_controller.core.filters import FilterRegistry, ImagePool
|
from ledgrab.core.filters import FilterRegistry, ImagePool
|
||||||
from wled_controller.storage.picture_source import (
|
from ledgrab.storage.picture_source import (
|
||||||
ScreenCapturePictureSource,
|
ScreenCapturePictureSource,
|
||||||
StaticImagePictureSource,
|
StaticImagePictureSource,
|
||||||
)
|
)
|
||||||
from wled_controller.utils.image_codec import encode_jpeg_data_uri
|
from ledgrab.utils.image_codec import encode_jpeg_data_uri
|
||||||
|
|
||||||
stream = None
|
stream = None
|
||||||
try:
|
try:
|
||||||
@@ -553,10 +553,10 @@ async def test_key_colors_source(
|
|||||||
chain = source_store.resolve_stream_chain(source.picture_source_id)
|
chain = source_store.resolve_stream_chain(source.picture_source_id)
|
||||||
raw_stream = chain["raw_stream"]
|
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):
|
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()
|
asset_store = _get_asset_store()
|
||||||
image_path = (
|
image_path = (
|
||||||
@@ -681,15 +681,15 @@ async def test_key_colors_ws(
|
|||||||
"""WebSocket for real-time key_colors test preview with frame + rectangle overlay."""
|
"""WebSocket for real-time key_colors test preview with frame + rectangle overlay."""
|
||||||
import json as ws_json
|
import json as ws_json
|
||||||
import time as ws_time
|
import time as ws_time
|
||||||
from wled_controller.api.auth import verify_ws_token
|
from ledgrab.api.auth import verify_ws_token
|
||||||
from wled_controller.storage.color_strip_source import KeyColorsColorStripSource
|
from ledgrab.storage.color_strip_source import KeyColorsColorStripSource
|
||||||
from wled_controller.core.capture.screen_capture import (
|
from ledgrab.core.capture.screen_capture import (
|
||||||
calculate_average_color,
|
calculate_average_color,
|
||||||
calculate_dominant_color,
|
calculate_dominant_color,
|
||||||
calculate_median_color,
|
calculate_median_color,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.picture_source import ScreenCapturePictureSource
|
from ledgrab.storage.picture_source import ScreenCapturePictureSource
|
||||||
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 not verify_ws_token(token):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
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"])
|
@router.get("/api/v1/color-strip-sources/os-notifications/history", tags=["Color Strip Sources"])
|
||||||
async def os_notification_history(_auth: AuthRequired):
|
async def os_notification_history(_auth: AuthRequired):
|
||||||
"""Return recent OS notification capture history (newest first)."""
|
"""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,
|
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
|
Subsequent text messages are treated as config updates: if the source_type
|
||||||
changed the old stream is replaced; otherwise ``update_source()`` is used.
|
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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
@@ -1168,7 +1168,7 @@ async def preview_color_strip_ws(
|
|||||||
|
|
||||||
def _build_source(config: dict):
|
def _build_source(config: dict):
|
||||||
"""Build a ColorStripSource from a raw config dict, injecting synthetic id/name."""
|
"""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("id", "__preview__")
|
||||||
config.setdefault("name", "__preview__")
|
config.setdefault("name", "__preview__")
|
||||||
@@ -1176,7 +1176,7 @@ async def preview_color_strip_ws(
|
|||||||
|
|
||||||
def _create_stream(source):
|
def _create_stream(source):
|
||||||
"""Instantiate and start the appropriate stream class for *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)
|
stream_cls = _SIMPLE_STREAM_MAP.get(source.source_type)
|
||||||
if not stream_cls:
|
if not stream_cls:
|
||||||
@@ -1185,7 +1185,7 @@ async def preview_color_strip_ws(
|
|||||||
# Inject gradient store for palette resolution
|
# Inject gradient store for palette resolution
|
||||||
if hasattr(s, "set_gradient_store"):
|
if hasattr(s, "set_gradient_store"):
|
||||||
try:
|
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())
|
s.set_gradient_store(get_gradient_store())
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1283,7 +1283,7 @@ async def preview_color_strip_ws(
|
|||||||
|
|
||||||
# Handle "fire" command for notification streams
|
# Handle "fire" command for notification streams
|
||||||
if new_config.get("action") == "fire":
|
if new_config.get("action") == "fire":
|
||||||
from wled_controller.core.processing.notification_stream import (
|
from ledgrab.core.processing.notification_stream import (
|
||||||
NotificationColorStripStream,
|
NotificationColorStripStream,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1349,7 +1349,7 @@ async def css_api_input_ws(
|
|||||||
Auth via ?token=<api_key>. Accepts JSON frames ({"colors": [[R,G,B], ...]})
|
Auth via ?token=<api_key>. Accepts JSON frames ({"colors": [[R,G,B], ...]})
|
||||||
or binary frames (raw RGBRGB... bytes, 3 bytes per LED).
|
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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
@@ -1391,7 +1391,7 @@ async def css_api_input_ws(
|
|||||||
if "segments" in data:
|
if "segments" in data:
|
||||||
# Segment-based path — validate and push
|
# Segment-based path — validate and push
|
||||||
try:
|
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"]]
|
seg_dicts = [SegmentPayload(**s).model_dump() for s in data["segments"]]
|
||||||
except Exception as e:
|
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).
|
First message is JSON metadata (source_type, led_count, calibration segments).
|
||||||
Subsequent messages are binary RGB frames (``led_count * 3`` bytes).
|
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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
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})")
|
logger.info(f"CSS test WebSocket connected for {source_id} (fps={fps})")
|
||||||
|
|
||||||
try:
|
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)
|
is_api_input = isinstance(stream, ApiInputColorStripStream)
|
||||||
_last_push_gen = 0 # track api_input push generation to skip unchanged frames
|
_last_push_gen = 0 # track api_input push generation to skip unchanged frames
|
||||||
@@ -1643,7 +1643,7 @@ async def test_color_strip_ws(
|
|||||||
try:
|
try:
|
||||||
frame = _frame_live.get_latest_frame()
|
frame = _frame_live.get_latest_frame()
|
||||||
if frame is not None and frame.image is not None:
|
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
|
import cv2 as _cv2
|
||||||
|
|
||||||
img = frame.image
|
img = frame.image
|
||||||
+12
-12
@@ -3,19 +3,19 @@
|
|||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.core.devices.led_client import (
|
from ledgrab.core.devices.led_client import (
|
||||||
get_all_providers,
|
get_all_providers,
|
||||||
get_device_capabilities,
|
get_device_capabilities,
|
||||||
get_provider,
|
get_provider,
|
||||||
)
|
)
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_device_store,
|
get_device_store,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.devices import (
|
from ledgrab.api.schemas.devices import (
|
||||||
BrightnessRequest,
|
BrightnessRequest,
|
||||||
DeviceCreate,
|
DeviceCreate,
|
||||||
DeviceListResponse,
|
DeviceListResponse,
|
||||||
@@ -28,10 +28,10 @@ from wled_controller.api.schemas.devices import (
|
|||||||
OpenRGBZonesResponse,
|
OpenRGBZonesResponse,
|
||||||
PowerRequest,
|
PowerRequest,
|
||||||
)
|
)
|
||||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||||
from wled_controller.storage import DeviceStore
|
from ledgrab.storage import DeviceStore
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -300,14 +300,14 @@ async def get_openrgb_zones(
|
|||||||
"""List available zones on an OpenRGB device."""
|
"""List available zones on an OpenRGB device."""
|
||||||
import asyncio
|
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)
|
host, port, device_index, _zones = parse_openrgb_url(url)
|
||||||
|
|
||||||
def _fetch_zones():
|
def _fetch_zones():
|
||||||
from openrgb import OpenRGBClient
|
from openrgb import OpenRGBClient
|
||||||
|
|
||||||
client = OpenRGBClient(host, port, name="WLED Controller (zones)")
|
client = OpenRGBClient(host, port, name="LedGrab (zones)")
|
||||||
try:
|
try:
|
||||||
devices = client.devices
|
devices = client.devices
|
||||||
if device_index >= len(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 ...]
|
Wire format: [brightness_byte][R G B R G B ...]
|
||||||
Auth via ?token=<api_key>.
|
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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
@@ -760,7 +760,7 @@ async def device_ws_stream(
|
|||||||
|
|
||||||
await websocket.accept()
|
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 = get_ws_broadcaster()
|
||||||
broadcaster.add_client(device_id, websocket)
|
broadcaster.add_client(device_id, websocket)
|
||||||
+14
-14
@@ -10,14 +10,14 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_database,
|
get_database,
|
||||||
get_game_integration_store,
|
get_game_integration_store,
|
||||||
get_game_event_bus,
|
get_game_event_bus,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.game_integration import (
|
from ledgrab.api.schemas.game_integration import (
|
||||||
AdapterInfoResponse,
|
AdapterInfoResponse,
|
||||||
AdapterListResponse,
|
AdapterListResponse,
|
||||||
ApplyPresetRequest,
|
ApplyPresetRequest,
|
||||||
@@ -34,13 +34,13 @@ from wled_controller.api.schemas.game_integration import (
|
|||||||
PresetListResponse,
|
PresetListResponse,
|
||||||
RecentEventsResponse,
|
RecentEventsResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.game_integration.adapter_registry import AdapterRegistry
|
from ledgrab.core.game_integration.adapter_registry import AdapterRegistry
|
||||||
from wled_controller.core.game_integration.event_bus import GameEventBus
|
from ledgrab.core.game_integration.event_bus import GameEventBus
|
||||||
from wled_controller.core.game_integration.events import GameEvent
|
from ledgrab.core.game_integration.events import GameEvent
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
from wled_controller.storage.game_integration import EventMapping
|
from ledgrab.storage.game_integration import EventMapping
|
||||||
from wled_controller.storage.game_integration_store import GameIntegrationStore
|
from ledgrab.storage.game_integration_store import GameIntegrationStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ def _cleanup_state(integration_id: str) -> None:
|
|||||||
|
|
||||||
def _config_to_response(config: Any) -> GameIntegrationResponse:
|
def _config_to_response(config: Any) -> GameIntegrationResponse:
|
||||||
"""Convert a GameIntegrationConfig to its API response."""
|
"""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(
|
return GameIntegrationResponse(
|
||||||
id=config.id,
|
id=config.id,
|
||||||
@@ -171,7 +171,7 @@ def _config_to_response(config: Any) -> GameIntegrationResponse:
|
|||||||
)
|
)
|
||||||
async def list_presets(_auth: AuthRequired):
|
async def list_presets(_auth: AuthRequired):
|
||||||
"""List all available built-in effect presets."""
|
"""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()
|
presets = get_all_presets()
|
||||||
responses = [
|
responses = [
|
||||||
@@ -554,7 +554,7 @@ async def apply_preset(
|
|||||||
If replace=true, replaces all existing mappings.
|
If replace=true, replaces all existing mappings.
|
||||||
If replace=false (default), appends preset mappings to existing ones.
|
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:
|
try:
|
||||||
config = store.get_integration(integration_id)
|
config = store.get_integration(integration_id)
|
||||||
@@ -619,7 +619,7 @@ async def auto_setup_integration(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Determine server URL
|
# 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()
|
db = get_database()
|
||||||
server_url = load_external_url(db)
|
server_url = load_external_url(db)
|
||||||
+18
-13
@@ -2,23 +2,23 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_color_strip_store,
|
get_color_strip_store,
|
||||||
get_gradient_store,
|
get_gradient_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.gradients import (
|
from ledgrab.api.schemas.gradients import (
|
||||||
GradientCreate,
|
GradientCreate,
|
||||||
GradientListResponse,
|
GradientListResponse,
|
||||||
GradientResponse,
|
GradientResponse,
|
||||||
GradientUpdate,
|
GradientUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.gradient import Gradient
|
from ledgrab.storage.gradient import Gradient
|
||||||
from wled_controller.storage.gradient_store import GradientStore
|
from ledgrab.storage.gradient_store import GradientStore
|
||||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
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(
|
async def create_gradient(
|
||||||
data: GradientCreate,
|
data: GradientCreate,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -109,7 +111,12 @@ async def update_gradient(
|
|||||||
raise HTTPException(status_code=status, detail=str(e))
|
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(
|
async def clone_gradient(
|
||||||
gradient_id: str,
|
gradient_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -143,9 +150,7 @@ async def delete_gradient(
|
|||||||
# Check references
|
# Check references
|
||||||
for source in css_store.get_all_sources():
|
for source in css_store.get_all_sources():
|
||||||
if getattr(source, "gradient_id", None) == gradient_id:
|
if getattr(source, "gradient_id", None) == gradient_id:
|
||||||
raise ValueError(
|
raise ValueError(f"Cannot delete: referenced by color strip source '{source.name}'")
|
||||||
f"Cannot delete: referenced by color strip source '{source.name}'"
|
|
||||||
)
|
|
||||||
store.delete_gradient(gradient_id)
|
store.delete_gradient(gradient_id)
|
||||||
fire_entity_event("gradient", "deleted", gradient_id)
|
fire_entity_event("gradient", "deleted", gradient_id)
|
||||||
except (ValueError, EntityNotFoundError) as e:
|
except (ValueError, EntityNotFoundError) as e:
|
||||||
+9
-9
@@ -5,13 +5,13 @@ import json
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_ha_manager,
|
get_ha_manager,
|
||||||
get_ha_store,
|
get_ha_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.home_assistant import (
|
from ledgrab.api.schemas.home_assistant import (
|
||||||
HomeAssistantConnectionStatus,
|
HomeAssistantConnectionStatus,
|
||||||
HomeAssistantEntityListResponse,
|
HomeAssistantEntityListResponse,
|
||||||
HomeAssistantEntityResponse,
|
HomeAssistantEntityResponse,
|
||||||
@@ -22,12 +22,12 @@ from wled_controller.api.schemas.home_assistant import (
|
|||||||
HomeAssistantStatusResponse,
|
HomeAssistantStatusResponse,
|
||||||
HomeAssistantTestResponse,
|
HomeAssistantTestResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.home_assistant.ha_manager import HomeAssistantManager
|
from ledgrab.core.home_assistant.ha_manager import HomeAssistantManager
|
||||||
from wled_controller.core.home_assistant.ha_runtime import HARuntime
|
from ledgrab.core.home_assistant.ha_runtime import HARuntime
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
from wled_controller.storage.home_assistant_source import HomeAssistantSource
|
from ledgrab.storage.home_assistant_source import HomeAssistantSource
|
||||||
from wled_controller.storage.home_assistant_store import HomeAssistantStore
|
from ledgrab.storage.home_assistant_store import HomeAssistantStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
+8
-8
@@ -5,13 +5,13 @@ import asyncio
|
|||||||
import aiomqtt
|
import aiomqtt
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_mqtt_manager,
|
get_mqtt_manager,
|
||||||
get_mqtt_store,
|
get_mqtt_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.mqtt import (
|
from ledgrab.api.schemas.mqtt import (
|
||||||
MQTTConnectionStatus,
|
MQTTConnectionStatus,
|
||||||
MQTTSourceCreate,
|
MQTTSourceCreate,
|
||||||
MQTTSourceListResponse,
|
MQTTSourceListResponse,
|
||||||
@@ -20,11 +20,11 @@ from wled_controller.api.schemas.mqtt import (
|
|||||||
MQTTStatusResponse,
|
MQTTStatusResponse,
|
||||||
MQTTTestResponse,
|
MQTTTestResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
from ledgrab.core.mqtt.mqtt_manager import MQTTManager
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
from wled_controller.storage.mqtt_source import MQTTSource
|
from ledgrab.storage.mqtt_source import MQTTSource
|
||||||
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
from ledgrab.storage.mqtt_source_store import MQTTSourceStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
+11
-11
@@ -5,14 +5,14 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import APIRouter, Body, HTTPException, Depends
|
from fastapi import APIRouter, Body, HTTPException, Depends
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_device_store,
|
get_device_store,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.output_targets import (
|
from ledgrab.api.schemas.output_targets import (
|
||||||
HALightMappingSchema,
|
HALightMappingSchema,
|
||||||
HALightOutputTargetResponse,
|
HALightOutputTargetResponse,
|
||||||
LedOutputTargetResponse,
|
LedOutputTargetResponse,
|
||||||
@@ -21,17 +21,17 @@ from wled_controller.api.schemas.output_targets import (
|
|||||||
OutputTargetResponse,
|
OutputTargetResponse,
|
||||||
OutputTargetUpdate,
|
OutputTargetUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||||
from wled_controller.storage import DeviceStore
|
from ledgrab.storage import DeviceStore
|
||||||
from wled_controller.storage.bindable import BindableFloat
|
from ledgrab.storage.bindable import BindableFloat
|
||||||
from wled_controller.storage.wled_output_target import WledOutputTarget
|
from ledgrab.storage.wled_output_target import WledOutputTarget
|
||||||
from wled_controller.storage.ha_light_output_target import (
|
from ledgrab.storage.ha_light_output_target import (
|
||||||
HALightMapping,
|
HALightMapping,
|
||||||
HALightOutputTarget,
|
HALightOutputTarget,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
+15
-15
@@ -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 fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
get_color_strip_store,
|
get_color_strip_store,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
get_picture_source_store,
|
get_picture_source_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.output_targets import (
|
from ledgrab.api.schemas.output_targets import (
|
||||||
BulkTargetRequest,
|
BulkTargetRequest,
|
||||||
BulkTargetResponse,
|
BulkTargetResponse,
|
||||||
TargetMetricsResponse,
|
TargetMetricsResponse,
|
||||||
TargetProcessingState,
|
TargetProcessingState,
|
||||||
)
|
)
|
||||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
from ledgrab.core.capture.screen_capture import get_available_displays
|
||||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||||
from wled_controller.storage.color_strip_source import (
|
from ledgrab.storage.color_strip_source import (
|
||||||
AdvancedPictureColorStripSource,
|
AdvancedPictureColorStripSource,
|
||||||
PictureColorStripSource,
|
PictureColorStripSource,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||||
from wled_controller.storage.wled_output_target import WledOutputTarget
|
from ledgrab.storage.wled_output_target import WledOutputTarget
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ async def events_ws(
|
|||||||
token: str = Query(""),
|
token: str = Query(""),
|
||||||
):
|
):
|
||||||
"""WebSocket for real-time state change events. Auth via ?token=<api_key>."""
|
"""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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
@@ -272,7 +272,7 @@ async def start_target_overlay(
|
|||||||
):
|
):
|
||||||
calibration = css.calibration
|
calibration = css.calibration
|
||||||
# Resolve the display this CSS is capturing
|
# 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,
|
_resolve_display_index,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ async def ha_light_colors_ws(
|
|||||||
Streams: {"type": "colors_update", "colors": {entity_id: {r,g,b,hex}, ...}}
|
Streams: {"type": "colors_update", "colors": {entity_id: {r,g,b,hex}, ...}}
|
||||||
at the target's update_rate.
|
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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
@@ -390,7 +390,7 @@ async def led_preview_ws(
|
|||||||
token: str = Query(""),
|
token: str = Query(""),
|
||||||
):
|
):
|
||||||
"""WebSocket for real-time LED strip preview. Sends binary RGB frames. Auth via ?token=<api_key>."""
|
"""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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
+9
-9
@@ -2,24 +2,24 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_pattern_template_store,
|
get_pattern_template_store,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.pattern_templates import (
|
from ledgrab.api.schemas.pattern_templates import (
|
||||||
PatternTemplateCreate,
|
PatternTemplateCreate,
|
||||||
PatternTemplateListResponse,
|
PatternTemplateListResponse,
|
||||||
PatternTemplateResponse,
|
PatternTemplateResponse,
|
||||||
PatternTemplateUpdate,
|
PatternTemplateUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.output_targets import KeyColorRectangleSchema
|
from ledgrab.api.schemas.output_targets import KeyColorRectangleSchema
|
||||||
from wled_controller.storage.pattern_template import KeyColorRectangle
|
from ledgrab.storage.pattern_template import KeyColorRectangle
|
||||||
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
from ledgrab.storage.pattern_template_store import PatternTemplateStore
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
+29
-29
@@ -9,20 +9,20 @@ import numpy as np
|
|||||||
from fastapi import APIRouter, Body, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, Body, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_picture_source_store,
|
get_picture_source_store,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
get_pp_template_store,
|
get_pp_template_store,
|
||||||
get_template_store,
|
get_template_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.common import (
|
from ledgrab.api.schemas.common import (
|
||||||
CaptureImage,
|
CaptureImage,
|
||||||
PerformanceMetrics,
|
PerformanceMetrics,
|
||||||
TemplateTestResponse,
|
TemplateTestResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.picture_sources import (
|
from ledgrab.api.schemas.picture_sources import (
|
||||||
ImageValidateRequest,
|
ImageValidateRequest,
|
||||||
ImageValidateResponse,
|
ImageValidateResponse,
|
||||||
PictureSourceCreate,
|
PictureSourceCreate,
|
||||||
@@ -35,20 +35,20 @@ from wled_controller.api.schemas.picture_sources import (
|
|||||||
StaticImagePictureSourceResponse,
|
StaticImagePictureSourceResponse,
|
||||||
VideoPictureSourceResponse,
|
VideoPictureSourceResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.capture_engines import EngineRegistry
|
from ledgrab.core.capture_engines import EngineRegistry
|
||||||
from wled_controller.core.filters import FilterRegistry, ImagePool
|
from ledgrab.core.filters import FilterRegistry, ImagePool
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.storage.template_store import TemplateStore
|
from ledgrab.storage.template_store import TemplateStore
|
||||||
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
|
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||||
from wled_controller.storage.picture_source import (
|
from ledgrab.storage.picture_source import (
|
||||||
ProcessedPictureSource,
|
ProcessedPictureSource,
|
||||||
ScreenCapturePictureSource,
|
ScreenCapturePictureSource,
|
||||||
StaticImagePictureSource,
|
StaticImagePictureSource,
|
||||||
VideoCaptureSource,
|
VideoCaptureSource,
|
||||||
)
|
)
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
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."""
|
"""Validate an image source (URL or file path) and return a preview thumbnail."""
|
||||||
try:
|
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()
|
source = data.image_source.strip()
|
||||||
if not source:
|
if not source:
|
||||||
@@ -161,7 +161,7 @@ async def validate_image(
|
|||||||
img_bytes = path
|
img_bytes = path
|
||||||
|
|
||||||
def _process_image(src):
|
def _process_image(src):
|
||||||
from wled_controller.utils.image_codec import (
|
from ledgrab.utils.image_codec import (
|
||||||
encode_jpeg_data_uri,
|
encode_jpeg_data_uri,
|
||||||
load_image_bytes,
|
load_image_bytes,
|
||||||
load_image_file,
|
load_image_file,
|
||||||
@@ -198,7 +198,7 @@ async def get_full_image(
|
|||||||
):
|
):
|
||||||
"""Serve the full-resolution image for lightbox preview."""
|
"""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:
|
try:
|
||||||
if source.startswith(("http://", "https://")):
|
if source.startswith(("http://", "https://")):
|
||||||
@@ -214,7 +214,7 @@ async def get_full_image(
|
|||||||
img_bytes = path
|
img_bytes = path
|
||||||
|
|
||||||
def _encode_full(src):
|
def _encode_full(src):
|
||||||
from wled_controller.utils.image_codec import (
|
from ledgrab.utils.image_codec import (
|
||||||
encode_jpeg,
|
encode_jpeg,
|
||||||
load_image_bytes,
|
load_image_bytes,
|
||||||
load_image_file,
|
load_image_file,
|
||||||
@@ -375,9 +375,9 @@ async def get_video_thumbnail(
|
|||||||
store: PictureSourceStore = Depends(get_picture_source_store),
|
store: PictureSourceStore = Depends(get_picture_source_store),
|
||||||
):
|
):
|
||||||
"""Get a thumbnail for a video picture source (first frame)."""
|
"""Get a thumbnail for a video picture source (first frame)."""
|
||||||
from wled_controller.core.processing.video_stream import extract_thumbnail
|
from ledgrab.core.processing.video_stream import extract_thumbnail
|
||||||
from wled_controller.storage.picture_source import VideoCaptureSource
|
from ledgrab.storage.picture_source import VideoCaptureSource
|
||||||
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
|
||||||
|
|
||||||
try:
|
try:
|
||||||
source = store.get_stream(stream_id)
|
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")
|
raise HTTPException(status_code=400, detail="Not a video source")
|
||||||
|
|
||||||
# Resolve video asset to file path
|
# 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()
|
asset_store = _get_asset_store()
|
||||||
video_path = (
|
video_path = (
|
||||||
@@ -449,8 +449,8 @@ async def test_picture_source(
|
|||||||
|
|
||||||
if isinstance(raw_stream, StaticImagePictureSource):
|
if isinstance(raw_stream, StaticImagePictureSource):
|
||||||
# Static image stream: load image from asset
|
# Static image stream: load image 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
|
||||||
from wled_controller.utils.image_codec import load_image_file
|
from ledgrab.utils.image_codec import load_image_file
|
||||||
|
|
||||||
asset_store = _get_asset_store()
|
asset_store = _get_asset_store()
|
||||||
image_path = (
|
image_path = (
|
||||||
@@ -531,7 +531,7 @@ async def test_picture_source(
|
|||||||
image = last_frame.image
|
image = last_frame.image
|
||||||
|
|
||||||
# Create thumbnail + encode (CPU-bound — run in thread)
|
# 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,
|
encode_jpeg_data_uri,
|
||||||
thumbnail as make_thumbnail,
|
thumbnail as make_thumbnail,
|
||||||
)
|
)
|
||||||
@@ -628,11 +628,11 @@ async def test_picture_source_ws(
|
|||||||
preview_width: int = Query(0),
|
preview_width: int = Query(0),
|
||||||
):
|
):
|
||||||
"""WebSocket for picture source test with intermediate frame previews."""
|
"""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,
|
authenticate_ws_token,
|
||||||
stream_capture_test,
|
stream_capture_test,
|
||||||
)
|
)
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
get_picture_source_store as _get_ps_store,
|
get_picture_source_store as _get_ps_store,
|
||||||
get_template_store as _get_t_store,
|
get_template_store as _get_t_store,
|
||||||
get_pp_template_store as _get_pp_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
|
# Video sources: use VideoCaptureLiveStream for test preview
|
||||||
if isinstance(raw_stream, VideoCaptureSource):
|
if isinstance(raw_stream, VideoCaptureSource):
|
||||||
from wled_controller.core.processing.video_stream import VideoCaptureLiveStream
|
from ledgrab.core.processing.video_stream import VideoCaptureLiveStream
|
||||||
from wled_controller.api.dependencies import get_asset_store as _get_asset_store2
|
from ledgrab.api.dependencies import get_asset_store as _get_asset_store2
|
||||||
|
|
||||||
asset_store = _get_asset_store2()
|
asset_store = _get_asset_store2()
|
||||||
video_path = (
|
video_path = (
|
||||||
@@ -690,7 +690,7 @@ async def test_picture_source_ws(
|
|||||||
|
|
||||||
def _encode_video_frame(image, pw):
|
def _encode_video_frame(image, pw):
|
||||||
"""Encode numpy RGB image as JPEG base64 data URI."""
|
"""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:
|
if pw:
|
||||||
image = resize_down(image, pw)
|
image = resize_down(image, pw)
|
||||||
+70
-29
@@ -5,34 +5,34 @@ import time
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_picture_source_store,
|
get_picture_source_store,
|
||||||
get_pp_template_store,
|
get_pp_template_store,
|
||||||
get_template_store,
|
get_template_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.common import (
|
from ledgrab.api.schemas.common import (
|
||||||
CaptureImage,
|
CaptureImage,
|
||||||
PerformanceMetrics,
|
PerformanceMetrics,
|
||||||
TemplateTestResponse,
|
TemplateTestResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.filters import FilterInstanceSchema
|
from ledgrab.api.schemas.filters import FilterInstanceSchema
|
||||||
from wled_controller.api.schemas.postprocessing import (
|
from ledgrab.api.schemas.postprocessing import (
|
||||||
PostprocessingTemplateCreate,
|
PostprocessingTemplateCreate,
|
||||||
PostprocessingTemplateListResponse,
|
PostprocessingTemplateListResponse,
|
||||||
PostprocessingTemplateResponse,
|
PostprocessingTemplateResponse,
|
||||||
PostprocessingTemplateUpdate,
|
PostprocessingTemplateUpdate,
|
||||||
PPTemplateTestRequest,
|
PPTemplateTestRequest,
|
||||||
)
|
)
|
||||||
from wled_controller.core.capture_engines import EngineRegistry
|
from ledgrab.core.capture_engines import EngineRegistry
|
||||||
from wled_controller.core.filters import FilterRegistry, FilterInstance, ImagePool
|
from ledgrab.core.filters import FilterRegistry, FilterInstance, ImagePool
|
||||||
from wled_controller.storage.template_store import TemplateStore
|
from ledgrab.storage.template_store import TemplateStore
|
||||||
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
|
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||||
from wled_controller.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
|
from ledgrab.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
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(
|
async def list_pp_templates(
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
store: PostprocessingTemplateStore = Depends(get_pp_template_store),
|
store: PostprocessingTemplateStore = Depends(get_pp_template_store),
|
||||||
@@ -63,7 +67,12 @@ async def list_pp_templates(
|
|||||||
return PostprocessingTemplateListResponse(templates=responses, count=len(responses))
|
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(
|
async def create_pp_template(
|
||||||
data: PostprocessingTemplateCreate,
|
data: PostprocessingTemplateCreate,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -90,7 +99,11 @@ async def create_pp_template(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def get_pp_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -101,10 +114,16 @@ async def get_pp_template(
|
|||||||
template = store.get_template(template_id)
|
template = store.get_template(template_id)
|
||||||
return _pp_template_to_response(template)
|
return _pp_template_to_response(template)
|
||||||
except ValueError:
|
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(
|
async def update_pp_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
data: PostprocessingTemplateUpdate,
|
data: PostprocessingTemplateUpdate,
|
||||||
@@ -113,7 +132,11 @@ async def update_pp_template(
|
|||||||
):
|
):
|
||||||
"""Update a postprocessing template."""
|
"""Update a postprocessing template."""
|
||||||
try:
|
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 = store.update_template(
|
||||||
template_id=template_id,
|
template_id=template_id,
|
||||||
name=data.name,
|
name=data.name,
|
||||||
@@ -133,7 +156,11 @@ async def update_pp_template(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def delete_pp_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -165,7 +192,11 @@ async def delete_pp_template(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def test_pp_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
test_request: PPTemplateTestRequest,
|
test_request: PPTemplateTestRequest,
|
||||||
@@ -194,7 +225,7 @@ async def test_pp_template(
|
|||||||
|
|
||||||
raw_stream = chain["raw_stream"]
|
raw_stream = chain["raw_stream"]
|
||||||
|
|
||||||
from wled_controller.utils.image_codec import (
|
from ledgrab.utils.image_codec import (
|
||||||
encode_jpeg_data_uri,
|
encode_jpeg_data_uri,
|
||||||
load_image_file,
|
load_image_file,
|
||||||
thumbnail as make_thumbnail,
|
thumbnail as make_thumbnail,
|
||||||
@@ -202,10 +233,14 @@ async def test_pp_template(
|
|||||||
|
|
||||||
if isinstance(raw_stream, StaticImagePictureSource):
|
if isinstance(raw_stream, StaticImagePictureSource):
|
||||||
# Static image: load from asset
|
# 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()
|
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:
|
if not image_path:
|
||||||
raise HTTPException(status_code=400, detail="Image asset not found or missing file")
|
raise HTTPException(status_code=400, detail="Image asset not found or missing file")
|
||||||
|
|
||||||
@@ -238,7 +273,9 @@ async def test_pp_template(
|
|||||||
)
|
)
|
||||||
stream.initialize()
|
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
|
frame_count = 0
|
||||||
total_capture_time = 0.0
|
total_capture_time = 0.0
|
||||||
@@ -346,11 +383,11 @@ async def test_pp_template_ws(
|
|||||||
preview_width: int = Query(0),
|
preview_width: int = Query(0),
|
||||||
):
|
):
|
||||||
"""WebSocket for PP template test with intermediate frame previews."""
|
"""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,
|
authenticate_ws_token,
|
||||||
stream_capture_test,
|
stream_capture_test,
|
||||||
)
|
)
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
get_picture_source_store as _get_ps_store,
|
get_picture_source_store as _get_ps_store,
|
||||||
get_template_store as _get_t_store,
|
get_template_store as _get_t_store,
|
||||||
get_pp_template_store as _get_pp_store,
|
get_pp_template_store as _get_pp_store,
|
||||||
@@ -400,7 +437,9 @@ async def test_pp_template_ws(
|
|||||||
return
|
return
|
||||||
|
|
||||||
if capture_template.engine_type not in EngineRegistry.get_available_engines():
|
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
|
return
|
||||||
|
|
||||||
# Resolve PP filters
|
# Resolve PP filters
|
||||||
@@ -422,7 +461,9 @@ async def test_pp_template_ws(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await stream_capture_test(
|
await stream_capture_test(
|
||||||
websocket, engine_factory, duration,
|
websocket,
|
||||||
|
engine_factory,
|
||||||
|
duration,
|
||||||
pp_filters=pp_filters,
|
pp_filters=pp_filters,
|
||||||
preview_width=preview_width or None,
|
preview_width=preview_width or None,
|
||||||
)
|
)
|
||||||
+21
-13
@@ -5,30 +5,30 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
get_scene_preset_store,
|
get_scene_preset_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.scene_presets import (
|
from ledgrab.api.schemas.scene_presets import (
|
||||||
ActivateResponse,
|
ActivateResponse,
|
||||||
ScenePresetCreate,
|
ScenePresetCreate,
|
||||||
ScenePresetListResponse,
|
ScenePresetListResponse,
|
||||||
ScenePresetResponse,
|
ScenePresetResponse,
|
||||||
ScenePresetUpdate,
|
ScenePresetUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||||
from wled_controller.core.scenes.scene_activator import (
|
from ledgrab.core.scenes.scene_activator import (
|
||||||
apply_scene_state,
|
apply_scene_state,
|
||||||
capture_current_snapshot,
|
capture_current_snapshot,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.storage.scene_preset import ScenePreset
|
from ledgrab.storage.scene_preset import ScenePreset
|
||||||
from wled_controller.storage.scene_preset_store import ScenePresetStore
|
from ledgrab.storage.scene_preset_store import ScenePresetStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -39,13 +39,16 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
|||||||
id=preset.id,
|
id=preset.id,
|
||||||
name=preset.name,
|
name=preset.name,
|
||||||
description=preset.description,
|
description=preset.description,
|
||||||
targets=[{
|
targets=[
|
||||||
|
{
|
||||||
"target_id": t.target_id,
|
"target_id": t.target_id,
|
||||||
"running": t.running,
|
"running": t.running,
|
||||||
"color_strip_source_id": t.color_strip_source_id,
|
"color_strip_source_id": t.color_strip_source_id,
|
||||||
"brightness_value_source_id": t.brightness_value_source_id,
|
"brightness_value_source_id": t.brightness_value_source_id,
|
||||||
"fps": t.fps,
|
"fps": t.fps,
|
||||||
} for t in preset.targets],
|
}
|
||||||
|
for t in preset.targets
|
||||||
|
],
|
||||||
order=preset.order,
|
order=preset.order,
|
||||||
tags=preset.tags,
|
tags=preset.tags,
|
||||||
created_at=preset.created_at,
|
created_at=preset.created_at,
|
||||||
@@ -55,6 +58,7 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
|||||||
|
|
||||||
# ===== CRUD =====
|
# ===== CRUD =====
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/api/v1/scene-presets",
|
"/api/v1/scene-presets",
|
||||||
response_model=ScenePresetResponse,
|
response_model=ScenePresetResponse,
|
||||||
@@ -180,7 +184,9 @@ async def update_scene_preset(
|
|||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
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)
|
fire_entity_event("scene_preset", "updated", preset_id)
|
||||||
return _preset_to_response(preset)
|
return _preset_to_response(preset)
|
||||||
|
|
||||||
@@ -206,6 +212,7 @@ async def delete_scene_preset(
|
|||||||
|
|
||||||
# ===== Recapture =====
|
# ===== Recapture =====
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/api/v1/scene-presets/{preset_id}/recapture",
|
"/api/v1/scene-presets/{preset_id}/recapture",
|
||||||
response_model=ScenePresetResponse,
|
response_model=ScenePresetResponse,
|
||||||
@@ -244,6 +251,7 @@ async def recapture_scene_preset(
|
|||||||
|
|
||||||
# ===== Activate =====
|
# ===== Activate =====
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/api/v1/scene-presets/{preset_id}/activate",
|
"/api/v1/scene-presets/{preset_id}/activate",
|
||||||
response_model=ActivateResponse,
|
response_model=ActivateResponse,
|
||||||
+29
-18
@@ -2,25 +2,25 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_color_strip_store,
|
get_color_strip_store,
|
||||||
get_sync_clock_manager,
|
get_sync_clock_manager,
|
||||||
get_sync_clock_store,
|
get_sync_clock_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.sync_clocks import (
|
from ledgrab.api.schemas.sync_clocks import (
|
||||||
SyncClockCreate,
|
SyncClockCreate,
|
||||||
SyncClockListResponse,
|
SyncClockListResponse,
|
||||||
SyncClockResponse,
|
SyncClockResponse,
|
||||||
SyncClockUpdate,
|
SyncClockUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.sync_clock import SyncClock
|
from ledgrab.storage.sync_clock import SyncClock
|
||||||
from wled_controller.storage.sync_clock_store import SyncClockStore
|
from ledgrab.storage.sync_clock_store import SyncClockStore
|
||||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||||
from wled_controller.core.processing.sync_clock_manager import SyncClockManager
|
from ledgrab.core.processing.sync_clock_manager import SyncClockManager
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
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(
|
async def create_sync_clock(
|
||||||
data: SyncClockCreate,
|
data: SyncClockCreate,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -81,7 +83,9 @@ async def create_sync_clock(
|
|||||||
raise HTTPException(status_code=400, detail=str(e))
|
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(
|
async def get_sync_clock(
|
||||||
clock_id: str,
|
clock_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -96,7 +100,9 @@ async def get_sync_clock(
|
|||||||
raise HTTPException(status_code=404, detail=str(e))
|
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(
|
async def update_sync_clock(
|
||||||
clock_id: str,
|
clock_id: str,
|
||||||
data: SyncClockUpdate,
|
data: SyncClockUpdate,
|
||||||
@@ -138,9 +144,7 @@ async def delete_sync_clock(
|
|||||||
# Check references
|
# Check references
|
||||||
for source in css_store.get_all_sources():
|
for source in css_store.get_all_sources():
|
||||||
if getattr(source, "clock_id", None) == clock_id:
|
if getattr(source, "clock_id", None) == clock_id:
|
||||||
raise ValueError(
|
raise ValueError(f"Cannot delete: referenced by color strip source '{source.name}'")
|
||||||
f"Cannot delete: referenced by color strip source '{source.name}'"
|
|
||||||
)
|
|
||||||
manager.release_all_for(clock_id)
|
manager.release_all_for(clock_id)
|
||||||
store.delete_clock(clock_id)
|
store.delete_clock(clock_id)
|
||||||
fire_entity_event("sync_clock", "deleted", clock_id)
|
fire_entity_event("sync_clock", "deleted", clock_id)
|
||||||
@@ -153,7 +157,10 @@ async def delete_sync_clock(
|
|||||||
|
|
||||||
# ── Runtime control ──────────────────────────────────────────────────
|
# ── 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(
|
async def pause_sync_clock(
|
||||||
clock_id: str,
|
clock_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -170,7 +177,9 @@ async def pause_sync_clock(
|
|||||||
return _to_response(clock, manager)
|
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(
|
async def resume_sync_clock(
|
||||||
clock_id: str,
|
clock_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -187,7 +196,9 @@ async def resume_sync_clock(
|
|||||||
return _to_response(clock, manager)
|
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(
|
async def reset_sync_clock(
|
||||||
clock_id: str,
|
clock_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
+14
-14
@@ -15,9 +15,9 @@ import os
|
|||||||
import psutil
|
import psutil
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
||||||
from wled_controller import __version__, REPO_URL, DONATE_URL
|
from ledgrab import __version__, REPO_URL, DONATE_URL
|
||||||
from wled_controller.api.auth import AuthRequired, is_auth_enabled
|
from ledgrab.api.auth import AuthRequired, is_auth_enabled
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
get_audio_source_store,
|
get_audio_source_store,
|
||||||
get_audio_template_store,
|
get_audio_template_store,
|
||||||
get_automation_store,
|
get_automation_store,
|
||||||
@@ -34,7 +34,7 @@ from wled_controller.api.dependencies import (
|
|||||||
get_template_store,
|
get_template_store,
|
||||||
get_value_source_store,
|
get_value_source_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.system import (
|
from ledgrab.api.schemas.system import (
|
||||||
DisplayInfo,
|
DisplayInfo,
|
||||||
DisplayListResponse,
|
DisplayListResponse,
|
||||||
GpuInfo,
|
GpuInfo,
|
||||||
@@ -43,13 +43,13 @@ from wled_controller.api.schemas.system import (
|
|||||||
ProcessListResponse,
|
ProcessListResponse,
|
||||||
VersionResponse,
|
VersionResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.config import get_config, is_demo_mode
|
from ledgrab.config import get_config, is_demo_mode
|
||||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
from ledgrab.core.capture.screen_capture import get_available_displays
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
# Re-export load_external_url so existing callers still work
|
# 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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ _process = psutil.Process(os.getpid())
|
|||||||
_process.cpu_percent(interval=None) # prime process-level counter
|
_process.cpu_percent(interval=None) # prime process-level counter
|
||||||
|
|
||||||
# GPU monitoring (initialized once in utils.gpu, shared with metrics_history)
|
# 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_available as _nvml_available,
|
||||||
nvml as _nvml,
|
nvml as _nvml,
|
||||||
nvml_handle as _nvml_handle,
|
nvml_handle as _nvml_handle,
|
||||||
@@ -139,7 +139,7 @@ async def get_version():
|
|||||||
async def list_all_tags(_: AuthRequired):
|
async def list_all_tags(_: AuthRequired):
|
||||||
"""Get all tags used across all entities."""
|
"""Get all tags used across all entities."""
|
||||||
all_tags: set[str] = set()
|
all_tags: set[str] = set()
|
||||||
from wled_controller.api.dependencies import get_asset_store
|
from ledgrab.api.dependencies import get_asset_store
|
||||||
|
|
||||||
store_getters = [
|
store_getters = [
|
||||||
get_device_store,
|
get_device_store,
|
||||||
@@ -185,7 +185,7 @@ async def get_displays(
|
|||||||
logger.info(f"Listing available displays (engine_type={engine_type})")
|
logger.info(f"Listing available displays (engine_type={engine_type})")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from wled_controller.core.capture_engines import EngineRegistry
|
from ledgrab.core.capture_engines import EngineRegistry
|
||||||
|
|
||||||
if engine_type:
|
if engine_type:
|
||||||
engine_cls = EngineRegistry.get_engine(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.
|
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:
|
try:
|
||||||
detector = PlatformDetector()
|
detector = PlatformDetector()
|
||||||
@@ -348,7 +348,7 @@ async def get_integrations_status(
|
|||||||
|
|
||||||
Used by the dashboard to show connectivity indicators.
|
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 status
|
||||||
mqtt_service = get_mqtt_service()
|
mqtt_service = get_mqtt_service()
|
||||||
+26
-17
@@ -10,9 +10,9 @@ import re
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import get_database
|
from ledgrab.api.dependencies import get_database
|
||||||
from wled_controller.api.schemas.system import (
|
from ledgrab.api.schemas.system import (
|
||||||
ExternalUrlRequest,
|
ExternalUrlRequest,
|
||||||
ExternalUrlResponse,
|
ExternalUrlResponse,
|
||||||
LogLevelRequest,
|
LogLevelRequest,
|
||||||
@@ -20,9 +20,9 @@ from wled_controller.api.schemas.system import (
|
|||||||
MQTTSettingsRequest,
|
MQTTSettingsRequest,
|
||||||
MQTTSettingsResponse,
|
MQTTSettingsResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.config import get_config
|
from ledgrab.config import get_config
|
||||||
from wled_controller.storage.database import Database
|
from ledgrab.storage.database import Database
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -76,7 +76,9 @@ async def get_mqtt_settings(_: AuthRequired, db: Database = Depends(get_database
|
|||||||
response_model=MQTTSettingsResponse,
|
response_model=MQTTSettingsResponse,
|
||||||
tags=["System"],
|
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."""
|
"""Update MQTT broker settings. If password is empty string, the existing password is preserved."""
|
||||||
current = _load_mqtt_settings(db)
|
current = _load_mqtt_settings(db)
|
||||||
|
|
||||||
@@ -110,10 +112,12 @@ async def update_mqtt_settings(_: AuthRequired, body: MQTTSettingsRequest, db: D
|
|||||||
# External URL setting
|
# External URL setting
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def load_external_url(db: Database | None = None) -> str:
|
def load_external_url(db: Database | None = None) -> str:
|
||||||
"""Load the external URL setting. Returns empty string if not set."""
|
"""Load the external URL setting. Returns empty string if not set."""
|
||||||
if db is None:
|
if db is None:
|
||||||
from wled_controller.api.dependencies import get_database
|
from ledgrab.api.dependencies import get_database
|
||||||
|
|
||||||
db = get_database()
|
db = get_database()
|
||||||
data = db.get_setting("external_url")
|
data = db.get_setting("external_url")
|
||||||
if data:
|
if data:
|
||||||
@@ -136,7 +140,9 @@ async def get_external_url(_: AuthRequired, db: Database = Depends(get_database)
|
|||||||
response_model=ExternalUrlResponse,
|
response_model=ExternalUrlResponse,
|
||||||
tags=["System"],
|
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."""
|
"""Set the external base URL used in webhook URLs and other user-visible URLs."""
|
||||||
url = body.external_url.strip().rstrip("/")
|
url = body.external_url.strip().rstrip("/")
|
||||||
db.set_setting("external_url", {"external_url": url})
|
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
|
Auth via ``?token=<api_key>``. On connect, sends the last ~500 buffered
|
||||||
lines as individual text messages, then pushes new lines as they appear.
|
lines as individual text messages, then pushes new lines as they appear.
|
||||||
"""
|
"""
|
||||||
from wled_controller.api.auth import verify_ws_token
|
from ledgrab.api.auth import verify_ws_token
|
||||||
from wled_controller.utils import log_broadcaster
|
from ledgrab.utils import log_broadcaster
|
||||||
|
|
||||||
if not verify_ws_token(token):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
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"
|
# Regex: IPv4 address with optional port, e.g. "192.168.1.5" or "192.168.1.5:5555"
|
||||||
_ADB_ADDRESS_RE = re.compile(
|
_ADB_ADDRESS_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?$")
|
||||||
r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?$"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AdbConnectRequest(BaseModel):
|
class AdbConnectRequest(BaseModel):
|
||||||
@@ -244,7 +248,8 @@ def _validate_adb_address(address: str) -> None:
|
|||||||
|
|
||||||
def _get_adb_path() -> str:
|
def _get_adb_path() -> str:
|
||||||
"""Get the adb binary path from the scrcpy engine's resolver."""
|
"""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()
|
return _get_adb()
|
||||||
|
|
||||||
|
|
||||||
@@ -265,7 +270,9 @@ async def adb_connect(_: AuthRequired, request: AdbConnectRequest):
|
|||||||
logger.info(f"Connecting ADB device: {address}")
|
logger.info(f"Connecting ADB device: {address}")
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
adb, "connect", address,
|
adb,
|
||||||
|
"connect",
|
||||||
|
address,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=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}")
|
logger.info(f"Disconnecting ADB device: {address}")
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
adb, "disconnect", address,
|
adb,
|
||||||
|
"disconnect",
|
||||||
|
address,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
+73
-38
@@ -5,20 +5,20 @@ import time
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_cspt_store,
|
get_cspt_store,
|
||||||
get_picture_source_store,
|
get_picture_source_store,
|
||||||
get_pp_template_store,
|
get_pp_template_store,
|
||||||
get_template_store,
|
get_template_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.common import (
|
from ledgrab.api.schemas.common import (
|
||||||
CaptureImage,
|
CaptureImage,
|
||||||
PerformanceMetrics,
|
PerformanceMetrics,
|
||||||
TemplateTestResponse,
|
TemplateTestResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.templates import (
|
from ledgrab.api.schemas.templates import (
|
||||||
EngineInfo,
|
EngineInfo,
|
||||||
EngineListResponse,
|
EngineListResponse,
|
||||||
TemplateCreate,
|
TemplateCreate,
|
||||||
@@ -27,18 +27,18 @@ from wled_controller.api.schemas.templates import (
|
|||||||
TemplateTestRequest,
|
TemplateTestRequest,
|
||||||
TemplateUpdate,
|
TemplateUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.filters import (
|
from ledgrab.api.schemas.filters import (
|
||||||
FilterOptionDefSchema,
|
FilterOptionDefSchema,
|
||||||
FilterTypeListResponse,
|
FilterTypeListResponse,
|
||||||
FilterTypeResponse,
|
FilterTypeResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.capture_engines import EngineRegistry
|
from ledgrab.core.capture_engines import EngineRegistry
|
||||||
from wled_controller.core.filters import FilterRegistry
|
from ledgrab.core.filters import FilterRegistry
|
||||||
from wled_controller.storage.template_store import TemplateStore
|
from ledgrab.storage.template_store import TemplateStore
|
||||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||||
from wled_controller.storage.picture_source import ScreenCapturePictureSource
|
from ledgrab.storage.picture_source import ScreenCapturePictureSource
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
# ===== CAPTURE TEMPLATE ENDPOINTS =====
|
# ===== CAPTURE TEMPLATE ENDPOINTS =====
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/v1/capture-templates", response_model=TemplateListResponse, tags=["Templates"])
|
@router.get("/api/v1/capture-templates", response_model=TemplateListResponse, tags=["Templates"])
|
||||||
async def list_templates(
|
async def list_templates(
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -80,7 +81,12 @@ async def list_templates(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def create_template(
|
||||||
template_data: TemplateCreate,
|
template_data: TemplateCreate,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -111,7 +117,6 @@ async def create_template(
|
|||||||
except EntityNotFoundError as e:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -119,7 +124,9 @@ async def create_template(
|
|||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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(
|
async def get_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
_auth: AuthRequired,
|
_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(
|
async def update_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
update_data: TemplateUpdate,
|
update_data: TemplateUpdate,
|
||||||
@@ -176,7 +185,6 @@ async def update_template(
|
|||||||
except EntityNotFoundError as e:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -199,7 +207,10 @@ async def delete_template(
|
|||||||
# Check if any streams are using this template
|
# Check if any streams are using this template
|
||||||
streams_using_template = []
|
streams_using_template = []
|
||||||
for stream in stream_store.get_all_streams():
|
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)
|
streams_using_template.append(stream.name)
|
||||||
|
|
||||||
if streams_using_template:
|
if streams_using_template:
|
||||||
@@ -207,7 +218,7 @@ async def delete_template(
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
detail=f"Cannot delete template: it is used by the following stream(s): {stream_list}. "
|
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
|
# Proceed with deletion
|
||||||
@@ -245,7 +256,7 @@ async def list_engines(_auth: AuthRequired):
|
|||||||
name=engine_type.upper(),
|
name=engine_type.upper(),
|
||||||
default_config=engine_class.get_default_config(),
|
default_config=engine_class.get_default_config(),
|
||||||
available=(engine_type in available_set),
|
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")
|
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(
|
def test_template(
|
||||||
test_request: TemplateTestRequest,
|
test_request: TemplateTestRequest,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -276,7 +289,7 @@ def test_template(
|
|||||||
if test_request.engine_type not in EngineRegistry.get_available_engines():
|
if test_request.engine_type not in EngineRegistry.get_available_engines():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
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
|
# Create and initialize capture stream
|
||||||
@@ -286,7 +299,9 @@ def test_template(
|
|||||||
stream.initialize()
|
stream.initialize()
|
||||||
|
|
||||||
# Run sustained capture test
|
# 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
|
frame_count = 0
|
||||||
total_capture_time = 0.0
|
total_capture_time = 0.0
|
||||||
@@ -321,7 +336,7 @@ def test_template(
|
|||||||
raise ValueError("Unexpected image format from engine")
|
raise ValueError("Unexpected image format from engine")
|
||||||
image = last_frame.image
|
image = last_frame.image
|
||||||
|
|
||||||
from wled_controller.utils.image_codec import (
|
from ledgrab.utils.image_codec import (
|
||||||
encode_jpeg_data_uri,
|
encode_jpeg_data_uri,
|
||||||
thumbnail as make_thumbnail,
|
thumbnail as make_thumbnail,
|
||||||
)
|
)
|
||||||
@@ -361,7 +376,6 @@ def test_template(
|
|||||||
except EntityNotFoundError as e:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except RuntimeError as 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,
|
Config is sent as the first client message (JSON with engine_type,
|
||||||
engine_config, display_index, capture_duration).
|
engine_config, display_index, capture_duration).
|
||||||
"""
|
"""
|
||||||
from wled_controller.api.routes._preview_helpers import (
|
from ledgrab.api.routes._preview_helpers import (
|
||||||
authenticate_ws_token,
|
authenticate_ws_token,
|
||||||
stream_capture_test,
|
stream_capture_test,
|
||||||
)
|
)
|
||||||
@@ -417,7 +431,9 @@ async def test_template_ws(
|
|||||||
pw = int(config.get("preview_width", 0)) or None
|
pw = int(config.get("preview_width", 0)) or None
|
||||||
|
|
||||||
if engine_type not in EngineRegistry.get_available_engines():
|
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)
|
await websocket.close(code=4003)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -428,7 +444,9 @@ async def test_template_ws(
|
|||||||
s.initialize()
|
s.initialize()
|
||||||
return s
|
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:
|
try:
|
||||||
await stream_capture_test(websocket, engine_factory, duration, preview_width=pw)
|
await stream_capture_test(websocket, engine_factory, duration, preview_width=pw)
|
||||||
@@ -443,6 +461,7 @@ async def test_template_ws(
|
|||||||
|
|
||||||
# ===== FILTER TYPE ENDPOINTS =====
|
# ===== FILTER TYPE ENDPOINTS =====
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/v1/filters", response_model=FilterTypeListResponse, tags=["Filters"])
|
@router.get("/api/v1/filters", response_model=FilterTypeListResponse, tags=["Filters"])
|
||||||
async def list_filter_types(
|
async def list_filter_types(
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -467,9 +486,14 @@ async def list_filter_types(
|
|||||||
for opt in schema:
|
for opt in schema:
|
||||||
choices = opt.choices
|
choices = opt.choices
|
||||||
# Enrich filter_template choices with current template list
|
# 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
|
choices = template_choices
|
||||||
opt_schemas.append(FilterOptionDefSchema(
|
opt_schemas.append(
|
||||||
|
FilterOptionDefSchema(
|
||||||
key=opt.key,
|
key=opt.key,
|
||||||
label=opt.label,
|
label=opt.label,
|
||||||
type=opt.option_type,
|
type=opt.option_type,
|
||||||
@@ -478,12 +502,15 @@ async def list_filter_types(
|
|||||||
max_value=opt.max_value,
|
max_value=opt.max_value,
|
||||||
step=opt.step,
|
step=opt.step,
|
||||||
choices=choices,
|
choices=choices,
|
||||||
))
|
)
|
||||||
responses.append(FilterTypeResponse(
|
)
|
||||||
|
responses.append(
|
||||||
|
FilterTypeResponse(
|
||||||
filter_id=filter_cls.filter_id,
|
filter_id=filter_cls.filter_id,
|
||||||
filter_name=filter_cls.filter_name,
|
filter_name=filter_cls.filter_name,
|
||||||
options_schema=opt_schemas,
|
options_schema=opt_schemas,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
return FilterTypeListResponse(filters=responses, count=len(responses))
|
return FilterTypeListResponse(filters=responses, count=len(responses))
|
||||||
|
|
||||||
|
|
||||||
@@ -512,9 +539,14 @@ async def list_strip_filter_types(
|
|||||||
opt_schemas = []
|
opt_schemas = []
|
||||||
for opt in schema:
|
for opt in schema:
|
||||||
choices = opt.choices
|
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
|
choices = cspt_choices
|
||||||
opt_schemas.append(FilterOptionDefSchema(
|
opt_schemas.append(
|
||||||
|
FilterOptionDefSchema(
|
||||||
key=opt.key,
|
key=opt.key,
|
||||||
label=opt.label,
|
label=opt.label,
|
||||||
type=opt.option_type,
|
type=opt.option_type,
|
||||||
@@ -523,10 +555,13 @@ async def list_strip_filter_types(
|
|||||||
max_value=opt.max_value,
|
max_value=opt.max_value,
|
||||||
step=opt.step,
|
step=opt.step,
|
||||||
choices=choices,
|
choices=choices,
|
||||||
))
|
)
|
||||||
responses.append(FilterTypeResponse(
|
)
|
||||||
|
responses.append(
|
||||||
|
FilterTypeResponse(
|
||||||
filter_id=filter_cls.filter_id,
|
filter_id=filter_cls.filter_id,
|
||||||
filter_name=filter_cls.filter_name,
|
filter_name=filter_cls.filter_name,
|
||||||
options_schema=opt_schemas,
|
options_schema=opt_schemas,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
return FilterTypeListResponse(filters=responses, count=len(responses))
|
return FilterTypeListResponse(filters=responses, count=len(responses))
|
||||||
+8
-6
@@ -3,16 +3,16 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import get_update_service
|
from ledgrab.api.dependencies import get_update_service
|
||||||
from wled_controller.api.schemas.update import (
|
from ledgrab.api.schemas.update import (
|
||||||
DismissRequest,
|
DismissRequest,
|
||||||
UpdateSettingsRequest,
|
UpdateSettingsRequest,
|
||||||
UpdateSettingsResponse,
|
UpdateSettingsResponse,
|
||||||
UpdateStatusResponse,
|
UpdateStatusResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.update.update_service import UpdateService
|
from ledgrab.core.update.update_service import UpdateService
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -57,7 +57,9 @@ async def apply_update(
|
|||||||
if not status["can_auto_update"]:
|
if not status["can_auto_update"]:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=400,
|
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:
|
try:
|
||||||
await service.apply_update()
|
await service.apply_update()
|
||||||
+12
-12
@@ -5,14 +5,14 @@ from typing import Annotated, Optional
|
|||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
get_value_source_store,
|
get_value_source_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.value_sources import (
|
from ledgrab.api.schemas.value_sources import (
|
||||||
AdaptiveSceneValueSourceResponse,
|
AdaptiveSceneValueSourceResponse,
|
||||||
AdaptiveTimeColorValueSourceResponse,
|
AdaptiveTimeColorValueSourceResponse,
|
||||||
AdaptiveTimeValueSourceResponse,
|
AdaptiveTimeValueSourceResponse,
|
||||||
@@ -31,7 +31,7 @@ from wled_controller.api.schemas.value_sources import (
|
|||||||
ValueSourceResponse,
|
ValueSourceResponse,
|
||||||
ValueSourceUpdate,
|
ValueSourceUpdate,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.value_source import (
|
from ledgrab.storage.value_source import (
|
||||||
AdaptiveTimeColorValueSource,
|
AdaptiveTimeColorValueSource,
|
||||||
AdaptiveValueSource,
|
AdaptiveValueSource,
|
||||||
AnimatedColorValueSource,
|
AnimatedColorValueSource,
|
||||||
@@ -46,12 +46,12 @@ from wled_controller.storage.value_source import (
|
|||||||
SystemMetricsValueSource,
|
SystemMetricsValueSource,
|
||||||
ValueSource,
|
ValueSource,
|
||||||
)
|
)
|
||||||
from wled_controller.storage.value_source_store import ValueSourceStore
|
from ledgrab.storage.value_source_store import ValueSourceStore
|
||||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||||
from wled_controller.core.processing.value_stream import ValueStream
|
from ledgrab.core.processing.value_stream import ValueStream
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -340,7 +340,7 @@ async def delete_value_source(
|
|||||||
"""Delete a value source."""
|
"""Delete a value source."""
|
||||||
try:
|
try:
|
||||||
# Check if any targets reference this value source
|
# 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():
|
for target in target_store.get_all_targets():
|
||||||
if isinstance(target, WledOutputTarget):
|
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,
|
Acquires a ValueStream for the given source, polls get_value() at ~20 Hz,
|
||||||
and streams {value: float} JSON to the client.
|
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):
|
if not verify_ws_token(token):
|
||||||
await websocket.close(code=4001, reason="Unauthorized")
|
await websocket.close(code=4001, reason="Unauthorized")
|
||||||
+33
-14
@@ -2,25 +2,25 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_weather_manager,
|
get_weather_manager,
|
||||||
get_weather_source_store,
|
get_weather_source_store,
|
||||||
)
|
)
|
||||||
from wled_controller.api.schemas.weather_sources import (
|
from ledgrab.api.schemas.weather_sources import (
|
||||||
WeatherSourceCreate,
|
WeatherSourceCreate,
|
||||||
WeatherSourceListResponse,
|
WeatherSourceListResponse,
|
||||||
WeatherSourceResponse,
|
WeatherSourceResponse,
|
||||||
WeatherSourceUpdate,
|
WeatherSourceUpdate,
|
||||||
WeatherTestResponse,
|
WeatherTestResponse,
|
||||||
)
|
)
|
||||||
from wled_controller.core.weather.weather_manager import WeatherManager
|
from ledgrab.core.weather.weather_manager import WeatherManager
|
||||||
from wled_controller.core.weather.weather_provider import WMO_CONDITION_NAMES
|
from ledgrab.core.weather.weather_provider import WMO_CONDITION_NAMES
|
||||||
from wled_controller.storage.base_store import EntityNotFoundError
|
from ledgrab.storage.base_store import EntityNotFoundError
|
||||||
from wled_controller.storage.weather_source import WeatherSource
|
from ledgrab.storage.weather_source import WeatherSource
|
||||||
from wled_controller.storage.weather_source_store import WeatherSourceStore
|
from ledgrab.storage.weather_source_store import WeatherSourceStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
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(
|
async def list_weather_sources(
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
store: WeatherSourceStore = Depends(get_weather_source_store),
|
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(
|
async def create_weather_source(
|
||||||
data: WeatherSourceCreate,
|
data: WeatherSourceCreate,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -79,7 +86,11 @@ async def create_weather_source(
|
|||||||
return _to_response(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(
|
async def get_weather_source(
|
||||||
source_id: str,
|
source_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
@@ -91,7 +102,11 @@ async def get_weather_source(
|
|||||||
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
|
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(
|
async def update_weather_source(
|
||||||
source_id: str,
|
source_id: str,
|
||||||
data: WeatherSourceUpdate,
|
data: WeatherSourceUpdate,
|
||||||
@@ -133,7 +148,11 @@ async def delete_weather_source(
|
|||||||
fire_entity_event("weather_source", "deleted", source_id)
|
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(
|
async def test_weather_source(
|
||||||
source_id: str,
|
source_id: str,
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
+12
-7
@@ -13,11 +13,11 @@ from collections import defaultdict
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from wled_controller.api.dependencies import get_automation_engine, get_automation_store
|
from ledgrab.api.dependencies import get_automation_engine, get_automation_store
|
||||||
from wled_controller.core.automations.automation_engine import AutomationEngine
|
from ledgrab.core.automations.automation_engine import AutomationEngine
|
||||||
from wled_controller.storage.automation import WebhookCondition
|
from ledgrab.storage.automation import WebhookCondition
|
||||||
from wled_controller.storage.automation_store import AutomationStore
|
from ledgrab.storage.automation_store import AutomationStore
|
||||||
from wled_controller.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -75,12 +75,17 @@ async def handle_webhook(
|
|||||||
# Find the automation that owns this token
|
# Find the automation that owns this token
|
||||||
for automation in store.get_all_automations():
|
for automation in store.get_all_automations():
|
||||||
for condition in automation.conditions:
|
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"
|
active = body.action == "activate"
|
||||||
await engine.set_webhook_state(token, active)
|
await engine.set_webhook_state(token, active)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Webhook %s: automation '%s' (%s) → %s",
|
"Webhook %s: automation '%s' (%s) → %s",
|
||||||
token[:8], automation.name, automation.id, body.action,
|
token[:8],
|
||||||
|
automation.name,
|
||||||
|
automation.id,
|
||||||
|
body.action,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
+3
-1
@@ -10,7 +10,9 @@ class AudioTemplateCreate(BaseModel):
|
|||||||
"""Request to create an audio capture template."""
|
"""Request to create an audio capture template."""
|
||||||
|
|
||||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
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")
|
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
+6
-2
@@ -12,7 +12,9 @@ class ColorStripProcessingTemplateCreate(BaseModel):
|
|||||||
"""Request to create a color strip processing template."""
|
"""Request to create a color strip processing template."""
|
||||||
|
|
||||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
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)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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."""
|
"""Request to update a color strip processing template."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
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)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
tags: Optional[List[str]] = None
|
||||||
|
|
||||||
+1
-1
@@ -5,7 +5,7 @@ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
|||||||
|
|
||||||
from pydantic import BaseModel, Discriminator, Field, Tag, model_validator
|
from pydantic import BaseModel, Discriminator, Field, Tag, model_validator
|
||||||
|
|
||||||
from wled_controller.api.schemas.devices import Calibration
|
from ledgrab.api.schemas.devices import Calibration
|
||||||
|
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
+3
-1
@@ -48,5 +48,7 @@ class TemplateTestResponse(BaseModel):
|
|||||||
"""Response from template test."""
|
"""Response from template test."""
|
||||||
|
|
||||||
full_capture: CaptureImage = Field(description="Full screen capture with thumbnail")
|
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")
|
performance: PerformanceMetrics = Field(description="Performance metrics")
|
||||||
+6
-2
@@ -22,8 +22,12 @@ class FilterOptionDefSchema(BaseModel):
|
|||||||
min_value: Any = Field(description="Minimum value")
|
min_value: Any = Field(description="Minimum value")
|
||||||
max_value: Any = Field(description="Maximum value")
|
max_value: Any = Field(description="Maximum value")
|
||||||
step: Any = Field(description="Step increment")
|
step: Any = Field(description="Step increment")
|
||||||
choices: Optional[List[Dict[str, str]]] = Field(default=None, description="Available choices for select type")
|
choices: Optional[List[Dict[str, str]]] = Field(
|
||||||
max_length: Optional[int] = Field(default=None, description="Maximum string length for string type")
|
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):
|
class FilterTypeResponse(BaseModel):
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user