diff --git a/CLAUDE.md b/CLAUDE.md index 0ff977e..244ec8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# Claude Instructions for WLED Screen Controller +# Claude Instructions for LedGrab ## Code Search diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d46ee17..41c37f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,8 +9,8 @@ ## Development Setup ```bash -git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git -cd wled-screen-controller-mixed/server +git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git +cd ledgrab/server # Python environment python -m venv venv @@ -29,7 +29,7 @@ npm run build cd server export PYTHONPATH=$(pwd)/src # Linux/Mac # set PYTHONPATH=%CD%\src # Windows -python -m wled_controller.main +python -m ledgrab.main ``` Open http://localhost:8080 to access the dashboard. @@ -55,7 +55,7 @@ ruff check src/ tests/ ## 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 cd server diff --git a/INSTALLATION.md b/INSTALLATION.md index 4ba9c10..70be695 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -1,15 +1,17 @@ # 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 1. [Docker Installation (recommended)](#docker-installation) 2. [Manual Installation](#manual-installation) 3. [First-Time Setup](#first-time-setup) -4. [Home Assistant Integration](#home-assistant-integration) -5. [Configuration Reference](#configuration-reference) -6. [Troubleshooting](#troubleshooting) +4. [Configuration Reference](#configuration-reference) +5. [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:** ```bash - git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git - cd wled-screen-controller/server + git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git + cd ledgrab/server docker compose up -d ``` @@ -54,7 +56,7 @@ cd server docker build -t ledgrab . docker run -d \ - --name wled-screen-controller \ + --name ledgrab \ -p 8080:8080 \ -v $(pwd)/data:/app/data \ -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:** ```bash - git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git - cd wled-screen-controller/server + git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git + cd ledgrab/server ``` 2. **Build the frontend bundle:** @@ -95,7 +97,7 @@ Screen capture from inside a container requires X11 access. Uncomment `network_m 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:** @@ -131,11 +133,11 @@ Screen capture from inside a container requires X11 access. Uncomment `network_m ```bash # Linux / macOS 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) 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 in your browser. @@ -160,7 +162,7 @@ auth: Option B -- set an environment variable: ```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: @@ -184,7 +186,7 @@ server: Or via environment variable: ```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 @@ -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 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`) -2. **YAML config file** -- `server/config/default_config.yaml` (or set `WLED_CONFIG_PATH` to override) +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 `LEDGRAB_CONFIG_PATH` to override) 3. **Built-in defaults** 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 | | -------- | ------- | ----------- | -| `WLED_SERVER__PORT` | `8080` | HTTP listen port | -| `WLED_SERVER__LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` | -| `WLED_SERVER__CORS_ORIGINS` | `["http://localhost:8080"]` | Allowed CORS origins (JSON array) | -| `WLED_AUTH__API_KEYS` | `{"dev":"development-key..."}` | API keys (JSON object) | -| `WLED_STORAGE__DATABASE_FILE` | `data/ledgrab.db` | SQLite database path | -| `WLED_MQTT__ENABLED` | `false` | Enable MQTT for HA auto-discovery | -| `WLED_MQTT__BROKER_HOST` | `localhost` | MQTT broker address | -| `WLED_DEMO` | `false` | Enable demo mode (sandbox with virtual devices) | +| `LEDGRAB_SERVER__PORT` | `8080` | HTTP listen port | +| `LEDGRAB_SERVER__LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` | +| `LEDGRAB_SERVER__CORS_ORIGINS` | `["http://localhost:8080"]` | Allowed CORS origins (JSON array) | +| `LEDGRAB_AUTH__API_KEYS` | `{"dev":"development-key..."}` | API keys (JSON object) | +| `LEDGRAB_STORAGE__DATABASE_FILE` | `data/ledgrab.db` | SQLite database path | +| `LEDGRAB_MQTT__ENABLED` | `false` | Enable MQTT for HA auto-discovery | +| `LEDGRAB_MQTT__BROKER_HOST` | `localhost` | MQTT broker address | +| `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:** ```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`. @@ -288,7 +245,7 @@ If missing, run `cd server && npm ci && npm run build`. docker compose logs -f # Manual install -tail -f logs/wled_controller.log +tail -f logs/ledgrab.log ``` ### 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. 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 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) - [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) diff --git a/README.md b/README.md index 8e19515..fba6433 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ A Home Assistant integration exposes devices as entities for smart home automati ### Docker (recommended) ```bash -git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git -cd wled-screen-controller/server +git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git +cd ledgrab/server docker compose up -d ``` @@ -97,8 +97,8 @@ docker compose up -d Requires Python 3.11+ and Node.js 18+. ```bash -git clone https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed.git -cd wled-screen-controller/server +git clone https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git +cd ledgrab/server # Build the frontend bundle npm ci && npm run build @@ -112,7 +112,7 @@ pip install . # Start the server export PYTHONPATH=$(pwd)/src # Linux/Mac # 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. @@ -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. -Set the `WLED_DEMO` environment variable to `true`, `1`, or `yes`: +Set the `LEDGRAB_DEMO` environment variable to `true`, `1`, or `yes`: ```bash # Docker -docker compose run -e WLED_DEMO=true server +docker compose run -e LEDGRAB_DEMO=true server # 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) -set WLED_DEMO=true +set LEDGRAB_DEMO=true LedGrab.bat ``` @@ -144,9 +144,9 @@ Demo mode uses port **8081**, config file `config/demo_config.yaml`, and stores ## Architecture ```text -wled-screen-controller/ +ledgrab/ ├── server/ # Python FastAPI backend -│ ├── src/wled_controller/ +│ ├── src/ledgrab/ │ │ ├── main.py # Application entry point │ │ ├── config.py # YAML + env var configuration │ │ ├── api/ @@ -171,8 +171,6 @@ wled-screen-controller/ │ ├── tests/ # pytest suite │ ├── Dockerfile │ └── docker-compose.yml -├── custom_components/ # Home Assistant integration (HACS) -│ └── wled_screen_controller/ ├── docs/ │ ├── API.md # REST API reference │ └── CALIBRATION.md # LED calibration guide @@ -182,7 +180,7 @@ wled-screen-controller/ ## 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 server: @@ -200,11 +198,11 @@ storage: logging: format: "json" - file: "logs/wled_controller.log" + file: "logs/ledgrab.log" max_size_mb: 100 ``` -Environment variable override example: `WLED_SERVER__PORT=9090`. +Environment variable override example: `LEDGRAB_SERVER__PORT=9090`. ## API @@ -234,9 +232,7 @@ See [docs/CALIBRATION.md](docs/CALIBRATION.md) for a step-by-step guide. ## 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. - -See [INSTALLATION.md](INSTALLATION.md) for detailed setup instructions. +For Home Assistant integration, see the separate [ledgrab-haos-integration](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab-haos-integration) repository. ## Development diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 920b320..29ef10a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,96 +5,96 @@ This release brings a major expansion of integrations and source types: Home Ass ### Features #### 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)) -- 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)) -- 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)) -- 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 source cards use health-dot indicators ([e7c9a56](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/e7c9a56)) +- 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/ledgrab/commit/cb9289f)) +- 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/ledgrab/commit/40751fe)) +- HA source cards use health-dot indicators ([e7c9a56](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/e7c9a56)) #### Integrations & Tabs -- New **Integrations** tab and responsive icon-only tabs ([b7da4ab](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Game integration system ([492bdb9](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/492bdb9)) +- 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/ledgrab/commit/c59107c)) +- Game integration system ([492bdb9](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/492bdb9)) #### Audio -- Processed audio sources — audio filter framework ([86a9d34](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/86a9d34)) -- 11 audio filters implemented ([eb94066](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- 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)) -- Music sync viz modes and `auto_gain` audio filter ([b04978a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b04978a)) +- 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/ledgrab/commit/eb94066)) +- 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/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/ledgrab/commit/b04978a)) #### 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)) -- New value source types: HA entity, gradient map, strip extract ([384362c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/384362c)) -- `system_metrics` value source type ([b6713be](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/b6713be)) -- Color value source test visualization ([f6c25cd](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Value source card crosslinks + gradient_map test shows input value ([4b7a8d7](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/4b7a8d7)) +- **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/ledgrab/commit/384362c)) +- `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/ledgrab/commit/f6c25cd)) +- 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/ledgrab/commit/4b7a8d7)) #### 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)) -- `math_wave` color strip source type ([ace2471](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Custom file drop zone for asset upload modal ([f61a020](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f61a020)) +- 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/ledgrab/commit/ace2471)) +- `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/ledgrab/commit/f61a020)) #### 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)) -- System theme option + toast timer overlap fix ([db5008a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/db5008a)) -- Donation banner, About tab, settings UI improvements ([f3d07fc](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/f3d07fc)) -- Custom app icon for shortcuts and installer ([5f70302](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/5f70302)) +- 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/ledgrab/commit/db5008a)) +- 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/ledgrab/commit/5f70302)) #### 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 -- Tray: replace tkinter messagebox with Win32 `MessageBoxW` ([d037a2e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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: set `PYTHONPATH` and `WLED_CONFIG_PATH` in `start-hidden.vbs` ([e262a8b](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Replace HA test icon with refresh; make automation rules collapsible ([edc6d27](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Audio tree structure, filter i18n, IconSelect for filter options ([af2c89c](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/af2c89c)) -- Reference check before deleting audio processing template ([d04192f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- 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)) -- Composite layer opacity/brightness widgets + CSS layout ([78ce6c8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Rename HA Lights → Home Assistant, HA Light Targets → Light Targets ([89d1b13](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/89d1b13)) -- Command palette actions and automation condition button ([c0853ce](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Clip graph node title and subtitle to prevent overflow ([dca2d21](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Send `gradient_id` instead of palette in effect transient preview ([a4a9f6f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/a4a9f6f)) +- 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/ledgrab/commit/fc8ee34)) +- 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/ledgrab/commit/6e8b159)) +- 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/ledgrab/commit/99460a8)) +- 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/ledgrab/commit/d04192f)) +- 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/ledgrab/commit/a9e6e8c)) +- 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/ledgrab/commit/381ee75)) +- 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/ledgrab/commit/c0853ce)) +- 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/ledgrab/commit/dca2d21)) +- 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/ledgrab/commit/a4a9f6f)) --- ### Development / Internal #### Build -- Drop `packaging` dependency, inline version parsing ([d4ffe2e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) -- Keep `.py` sources; smoke test skips uninstalled modules ([17c5c02](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 `numpy.lib`/`linalg` from site-packages ([9f34ffb](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) +- 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/ledgrab/commit/feb91ad)) +- 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/ledgrab/commit/fd6776a)) +- 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/ledgrab/commit/b5842e6)) #### CI -- Add manual build workflow for testing artifacts ([fb98e6e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) +- 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/ledgrab/commit/9fcfdb8)) #### 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)) -- Key colors targets → CSS source type; HA target improvements ([3e6760f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) +- 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/ledgrab/commit/3e6760f)) +- Move Weather and Home Assistant sources to Integrations tree group ([3c2efd5](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/3c2efd5)) #### Tests -- Isolate tests from production database ([992495e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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)) +- 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/ledgrab/commit/ce1f484), [6b0e4e5](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab/commit/6b0e4e5)) #### Chores -- Remove processed-audio-sources plan files ([89990f8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 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/ledgrab/commit/f345687)) --- @@ -103,69 +103,69 @@ This release brings a major expansion of integrations and source types: Home Ass | Hash | Message | |------|---------| -| [d037a2e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [d4ffe2e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [fd6776a](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [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 | -| [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 | -| [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 | -| [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 | -| [ace2471](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [b7da4ab](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [89990f8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [d04192f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [6b0e4e5](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [1ce0dc6](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [ab43578](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [eb94066](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [e7c9a56](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [b6713be](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [f6c25cd](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [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 | -| [ea812bb](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [78ce6c8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [5f70302](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [3e6760f](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [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 | -| [fb98e6e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [2153dde](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [f345687](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [c0853ce](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [dca2d21](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/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 | -| [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 | -| [9fcfdb8](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/9fcfdb8) | ci: use sparse checkout for release notes in release workflow | +| [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/ledgrab/commit/fc8ee34) | fix(launcher): start-hidden.vbs must be ASCII + CRLF, use python.exe | +| [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/ledgrab/commit/d4ffe2e) | refactor: drop packaging dependency, inline version parsing | +| [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/ledgrab/commit/17c5c02) | fix(build): keep .py sources + make smoke test skip uninstalled modules | +| [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/ledgrab/commit/9f34ffb) | fix(build): stop stripping numpy.lib/linalg from site-packages | +| [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/ledgrab/commit/7a9c368) | refactor: split color-strips.ts into focused modules under color-strips/ folder | +| [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/ledgrab/commit/b04978a) | feat: add music sync viz modes and auto_gain audio filter | +| [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/ledgrab/commit/ace2471) | feat: add math_wave color strip source type | +| [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/ledgrab/commit/b7da4ab) | feat: add Integrations tab and responsive icon-only tabs | +| [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/ledgrab/commit/89990f8) | chore: remove processed-audio-sources plan files | +| [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/ledgrab/commit/d04192f) | fix: add reference check before deleting audio processing template | +| [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/ledgrab/commit/6b0e4e5) | feat(processed-audio-sources): phase 8 - frontend design consistency review | +| [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/ledgrab/commit/1ce0dc6) | feat(processed-audio-sources): phase 6 - frontend source type cleanup | +| [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/ledgrab/commit/ab43578) | feat(processed-audio-sources): phase 4 - runtime filter integration | +| [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/ledgrab/commit/eb94066) | feat(processed-audio-sources): phase 2 - implement 11 audio filters | +| [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/ledgrab/commit/c59107c) | feat: refactor MQTT from global config to multi-instance entity model | +| [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/ledgrab/commit/492bdb9) | feat: game integration system | +| [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/ledgrab/commit/db5008a) | feat: system theme option + fix toast timer overlap | +| [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/ledgrab/commit/f6c25cd) | feat: color value source test visualization | +| [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/ledgrab/commit/11d5d6b) | fix: device card header layout — URL badge overflow and hide button gap | +| [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/ledgrab/commit/ea812bb) | feat: check if port is busy before starting the server | +| [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/ledgrab/commit/78ce6c8) | fix: composite layer opacity/brightness widgets + CSS layout | +| [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/ledgrab/commit/5f70302) | feat: use custom app icon for shortcuts and installer | +| [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/ledgrab/commit/381ee75) | fix: HA light target — brightness source, transition=0, dashboard type label | +| [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/ledgrab/commit/89d1b13) | fix: rename HA Lights → Home Assistant, HA Light Targets → Light Targets | +| [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/ledgrab/commit/cb9289f) | feat: HA light output targets — cast LED colors to Home Assistant lights | +| [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/ledgrab/commit/3c2efd5) | refactor: move Weather and Home Assistant sources to Integrations tree group | +| [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/ledgrab/commit/f3d07fc) | feat: donation banner, About tab, settings UI improvements | +| [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/ledgrab/commit/f345687) | chore: remove python3.11 version pin from pre-commit config | +| [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/ledgrab/commit/c0853ce) | fix: improve command palette actions and automation condition button | +| [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/ledgrab/commit/be4c98b) | fix: show template name instead of ID in filter list and card badges | +| [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/ledgrab/commit/53986f8) | fix: replace emoji with SVG icons on weather and daylight cards | +| [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/ledgrab/commit/9fcfdb8) | ci: use sparse checkout for release notes in release workflow | diff --git a/TODO-release.md b/TODO-release.md index 495af0a..de13eaf 100644 --- a/TODO-release.md +++ b/TODO-release.md @@ -5,7 +5,7 @@ - [ ] 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 - [ ] 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 - [ ] **Migration required** if renaming storage paths or config keys (see data migration policy in CLAUDE.md) diff --git a/build-common.sh b/build-common.sh index f35ca4c..fae9476 100644 --- a/build-common.sh +++ b/build-common.sh @@ -143,8 +143,8 @@ cleanup_site_packages() { find "$sp_dir" -name "*.$ext_suffix" -exec strip --strip-debug {} \; 2>/dev/null || true fi - # ── Remove wled_controller if pip-installed ─────────────── - rm -rf "$sp_dir"/wled_controller* "$sp_dir"/wled*.dist-info 2>/dev/null || true + # ── Remove ledgrab if pip-installed ─────────────── + rm -rf "$sp_dir"/ledgrab* "$sp_dir"/ledgrab*.dist-info 2>/dev/null || true local cleaned_size cleaned_size=$(du -sh "$sp_dir" | cut -f1) @@ -191,7 +191,7 @@ compile_and_strip_sources() { # ── 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 # where cleanup_site_packages removes a submodule that turns out to be # imported internally by the package (e.g. numpy.linalg, zeroconf._services). @@ -200,7 +200,7 @@ compile_and_strip_sources() { # Args: # $1 — path to site-packages to test against # $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() { local sp_dir="$1" diff --git a/build-dist-windows.sh b/build-dist-windows.sh index 7d33427..a48dcfa 100644 --- a/build-dist-windows.sh +++ b/build-dist-windows.sh @@ -66,7 +66,7 @@ if ! grep -q 'Lib\\site-packages' "$PTH_FILE"; then echo 'Lib\site-packages' >> "$PTH_FILE" fi # 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 echo '../app/src' >> "$PTH_FILE" fi @@ -325,14 +325,14 @@ cd /d "%~dp0" :: Set paths 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 if not exist "%~dp0data" mkdir "%~dp0data" if not exist "%~dp0logs" mkdir "%~dp0logs" :: Start the server (tray icon handles UI and exit) -"%~dp0python\pythonw.exe" -m wled_controller +"%~dp0python\pythonw.exe" -m ledgrab LAUNCHER # Convert launcher to Windows line endings diff --git a/build-dist.ps1 b/build-dist.ps1 index 8b3357b..15613ad 100644 --- a/build-dist.ps1 +++ b/build-dist.ps1 @@ -58,7 +58,7 @@ if (-not $Version) { } if (-not $Version) { # 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*"([^"]+)"' 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" } # 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') { $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 } -# Remove the installed wled_controller package to avoid duplication +# Remove the installed ledgrab package to avoid duplication $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 "wled*.dist-info" -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 "ledgrab*.dist-info" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue # Clean up caches and test files to reduce size Write-Host " Cleaning up caches..." @@ -206,14 +206,14 @@ cd /d "%~dp0" :: Set paths 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 if not exist "%~dp0data" mkdir "%~dp0data" if not exist "%~dp0logs" mkdir "%~dp0logs" :: 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 diff --git a/build-dist.sh b/build-dist.sh index b35101d..93b1a8d 100644 --- a/build-dist.sh +++ b/build-dist.sh @@ -83,12 +83,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 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" source "$SCRIPT_DIR/venv/bin/activate" -exec python -m wled_controller.main +exec python -m ledgrab.main LAUNCHER sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh" diff --git a/contexts/frontend.md b/contexts/frontend.md index 5a236e3..0270be7 100644 --- a/contexts/frontend.md +++ b/contexts/frontend.md @@ -4,7 +4,7 @@ ## 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. diff --git a/contexts/server-operations.md b/contexts/server-operations.md index 610465e..009339d 100644 --- a/contexts/server-operations.md +++ b/contexts/server-operations.md @@ -8,10 +8,10 @@ Two independent server modes with separate configs, ports, and data directories: | Mode | Command | Config | Port | API Key | Data | | ---- | ------- | ------ | ---- | ------- | ---- | -| **Real** | `python -m wled_controller` | `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/` | +| **Real** | `python -m ledgrab` | `config/default_config.yaml` | 8080 | `development-key-change-in-production` | `data/` | +| **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. @@ -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: ```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 @@ -35,7 +35,7 @@ powershell -Command "netstat -ano | Select-String ':8081.*LISTEN'" # Kill it powershell -Command "Stop-Process -Id -Force" # 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.). @@ -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()`. 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()`. -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'`. 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 -- 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 devices: `core/devices/demo_provider.py` - Seed data: `core/demo_seed.py` diff --git a/custom_components/wled_screen_controller/__init__.py b/custom_components/wled_screen_controller/__init__.py deleted file mode 100644 index 84f869a..0000000 --- a/custom_components/wled_screen_controller/__init__.py +++ /dev/null @@ -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 diff --git a/custom_components/wled_screen_controller/button.py b/custom_components/wled_screen_controller/button.py deleted file mode 100644 index e597cfd..0000000 --- a/custom_components/wled_screen_controller/button.py +++ /dev/null @@ -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) diff --git a/custom_components/wled_screen_controller/config_flow.py b/custom_components/wled_screen_controller/config_flow.py deleted file mode 100644 index b05e30e..0000000 --- a/custom_components/wled_screen_controller/config_flow.py +++ /dev/null @@ -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, - ) diff --git a/custom_components/wled_screen_controller/const.py b/custom_components/wled_screen_controller/const.py deleted file mode 100644 index f2e58c0..0000000 --- a/custom_components/wled_screen_controller/const.py +++ /dev/null @@ -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" diff --git a/custom_components/wled_screen_controller/coordinator.py b/custom_components/wled_screen_controller/coordinator.py deleted file mode 100644 index dedeaed..0000000 --- a/custom_components/wled_screen_controller/coordinator.py +++ /dev/null @@ -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() diff --git a/custom_components/wled_screen_controller/event_listener.py b/custom_components/wled_screen_controller/event_listener.py deleted file mode 100644 index ec0a956..0000000 --- a/custom_components/wled_screen_controller/event_listener.py +++ /dev/null @@ -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 diff --git a/custom_components/wled_screen_controller/light.py b/custom_components/wled_screen_controller/light.py deleted file mode 100644 index 943a014..0000000 --- a/custom_components/wled_screen_controller/light.py +++ /dev/null @@ -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] diff --git a/custom_components/wled_screen_controller/manifest.json b/custom_components/wled_screen_controller/manifest.json deleted file mode 100644 index d37de18..0000000 --- a/custom_components/wled_screen_controller/manifest.json +++ /dev/null @@ -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" -} diff --git a/custom_components/wled_screen_controller/number.py b/custom_components/wled_screen_controller/number.py deleted file mode 100644 index cc31648..0000000 --- a/custom_components/wled_screen_controller/number.py +++ /dev/null @@ -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" diff --git a/custom_components/wled_screen_controller/select.py b/custom_components/wled_screen_controller/select.py deleted file mode 100644 index 6e7f6b0..0000000 --- a/custom_components/wled_screen_controller/select.py +++ /dev/null @@ -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, - ) diff --git a/custom_components/wled_screen_controller/sensor.py b/custom_components/wled_screen_controller/sensor.py deleted file mode 100644 index f788be8..0000000 --- a/custom_components/wled_screen_controller/sensor.py +++ /dev/null @@ -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) diff --git a/custom_components/wled_screen_controller/services.yaml b/custom_components/wled_screen_controller/services.yaml deleted file mode 100644 index 8499fdd..0000000 --- a/custom_components/wled_screen_controller/services.yaml +++ /dev/null @@ -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: diff --git a/custom_components/wled_screen_controller/strings.json b/custom_components/wled_screen_controller/strings.json deleted file mode 100644 index 8060675..0000000 --- a/custom_components/wled_screen_controller/strings.json +++ /dev/null @@ -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." - } - } - } - } -} diff --git a/custom_components/wled_screen_controller/switch.py b/custom_components/wled_screen_controller/switch.py deleted file mode 100644 index 436d658..0000000 --- a/custom_components/wled_screen_controller/switch.py +++ /dev/null @@ -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) diff --git a/custom_components/wled_screen_controller/translations/en.json b/custom_components/wled_screen_controller/translations/en.json deleted file mode 100644 index 2e7cb7a..0000000 --- a/custom_components/wled_screen_controller/translations/en.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/custom_components/wled_screen_controller/translations/ru.json b/custom_components/wled_screen_controller/translations/ru.json deleted file mode 100644 index b7e4e1a..0000000 --- a/custom_components/wled_screen_controller/translations/ru.json +++ /dev/null @@ -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": "Источник яркости" - } - } - } -} diff --git a/docs/API.md b/docs/API.md index e1823f3..89f383d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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` **API Version:** v1 diff --git a/hacs.json b/hacs.json deleted file mode 100644 index 81530d6..0000000 --- a/hacs.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "WLED Screen Controller", - "render_readme": true, - "country": ["US"], - "homeassistant": "2023.1.0" -} diff --git a/installer.nsi b/installer.nsi index e6f777c..d402076 100644 --- a/installer.nsi +++ b/installer.nsi @@ -30,8 +30,8 @@ SetCompressor /SOLID lzma ; ── Modern UI Configuration ───────────────────────────────── -!define MUI_ICON "server\src\wled_controller\static\icons\icon.ico" -!define MUI_UNICON "server\src\wled_controller\static\icons\icon.ico" +!define MUI_ICON "server\src\ledgrab\static\icons\icon.ico" +!define MUI_UNICON "server\src\ledgrab\static\icons\icon.ico" !define MUI_ABORTWARNING ; ── Pages ─────────────────────────────────────────────────── @@ -116,7 +116,7 @@ Section "!${APPNAME} (required)" SecCore CreateDirectory "$SMPROGRAMS\${APPNAME}" CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" \ "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" ; Registry: install location + Add/Remove Programs entry @@ -132,11 +132,11 @@ Section "!${APPNAME} (required)" SecCore WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ "InstallLocation" "$INSTDIR" 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}" \ "Publisher" "Alexei Dolgolyov" 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}" \ "NoModify" 1 WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \ @@ -152,13 +152,13 @@ SectionEnd Section "Desktop shortcut" SecDesktop CreateShortcut "$DESKTOP\${APPNAME}.lnk" \ "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 Section "Start with Windows" SecAutostart CreateShortcut "$SMSTARTUP\${APPNAME}.lnk" \ "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 ; ── Section Descriptions ──────────────────────────────────── diff --git a/plans/math-wave/PLAN.md b/plans/math-wave/PLAN.md deleted file mode 100644 index dc0a94c..0000000 --- a/plans/math-wave/PLAN.md +++ /dev/null @@ -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 `