8f9d490063
Adds support for LIFX smart bulbs and lightstrips that speak the LIFX binary UDP protocol on port 56700, with broadcast LAN discovery via the standard GetService/StateService probe. Backend: - LIFXClient is a single-pixel UDP adapter: averages the strip to one RGB triple, converts to LIFX HSBK (16-bit hue/saturation/brightness + kelvin), and pushes a tagged SetColor packet so all bulbs on the subnet act on it. Brightness folds into the HSBK brightness channel. - Hand-rolled packet builder: 36-byte LIFX header (frame + frame-address + protocol-header) + variable-length payload. Source ID 'LGGR' identifies LedGrab in protocol logs. - supports_fast_send=True with a synchronous send_pixels_fast hot path -- UDP costs nothing, so the default rate gate is 50 ms (~20 Hz) to match LIFX's documented <=20 cmd/sec recommendation. - Broadcast discovery sends GetService and parses StateService replies back into IP + MAC + service-port triples. Broadcast failures yield [] rather than raising. - Health check sends GetService and waits 1.5s for any reply on a one-shot UDP socket. - LIFXConfig joins the typed config union; Device storage gains a lifx_min_interval_ms field; full to_dict/from_dict/to_config wiring. - 47 unit tests cover URL parsing, RGB->HSBK conversion (red/green/ blue/white/black/clamping), packet construction (size, msg type, tagged flag, target MAC, sequence byte), SetColor and SetPower payload layouts, StateService reply parsing (including rejection of wrong msg types and runt payloads), strip averaging, rate limiting, fast-send hot path, provider validate/discover/health, and Device.to_config round-trip. Frontend: - 'lifx' in DEVICE_TYPE_KEYS (next to 'wiz'), lightbulb icon (deliberate smart-bulb family grouping with Hue + Yeelight + WiZ). - isLifxDevice predicate + per-type field show/hide in create and settings modals. - Rate-limit number input (default 50 ms) in both modals with hint text referencing LIFX's documented <=20 cmd/sec ceiling. - Locale strings in en/ru/zh. LIFX bulbs are reachable from the existing "Scan network" button -- no new discovery UI affordance was needed. No brightness_control capability exposed; LIFX brightness is folded into the HSBK on the wire.
750 lines
47 KiB
Markdown
750 lines
47 KiB
Markdown
# LedGrab TODO
|
|
|
|
## Multi-broker MQTT refactor
|
|
|
|
Goal: drop the global `MQTTService` / `MQTTConfig`. Every MQTT consumer
|
|
references an `MQTTSource.id`; `MQTTManager` is the only entry point.
|
|
`MQTTManager` + `MQTTRuntime` already exist — the job is to migrate every
|
|
caller off the legacy path, then delete it.
|
|
|
|
### Phase 1 — `mqtt_source_id` on Z2M target
|
|
|
|
- [x] Field on `Z2MLightOutputTarget` storage dataclass (+ to/from_dict)
|
|
- [x] Field on Z2M create/update/response schemas
|
|
- [x] Validate referenced `MQTTSource` exists at create/update
|
|
- [x] Thread through `output_target_store.create_z2m_light_target` + update
|
|
- [x] Thread through `ProcessorManager.add_z2m_light_target`
|
|
- [x] Thread through `Z2MLightTargetProcessor` constructor
|
|
|
|
### Phase 2 — Z2M processor uses `MQTTManager`
|
|
|
|
- [x] Replace `_mqtt_service` with `_mqtt_runtime` acquired from manager
|
|
- [x] `start()` acquire / `stop()` release
|
|
- [x] `_publish_payload` → `self._mqtt_runtime.publish(...)`
|
|
- [x] `turn_off_lights` borrow-pattern via manager (mirror HA-light)
|
|
- [x] Add `mqtt_manager` to `ProcessorDependencies` / `TargetContext`
|
|
|
|
### Phase 3 — Z2M editor UI
|
|
|
|
- [x] Add MQTT broker `EntitySelect` in Routing
|
|
- [x] Reuse `mqttSourcesCache`
|
|
- [x] Wire `mqtt_source_id` into edit-load + save payload + validation
|
|
|
|
### Phase 4 — DIY MQTT device (`MQTTLEDClient`)
|
|
|
|
- [x] `mqtt_source_id` field on `Device` storage
|
|
- [x] Field on `device_config.MQTTConfig`
|
|
- [x] `MQTTLEDClient` acquires runtime in `connect()`, releases in `close()`
|
|
- [x] Provider threads `mqtt_manager` via `ProviderDeps`
|
|
- [ ] Device editor: MQTT source picker shown for `device_type=mqtt` *(UI still
|
|
pending — backend accepts the field, but the device-create form doesn't
|
|
expose it yet)*
|
|
|
|
### Phase 5 — `AutomationEngine`
|
|
|
|
- [x] Drop `mqtt_service` ctor parameter
|
|
- [x] Drop legacy fallback in `_evaluate_mqtt` (rule must reference a source)
|
|
|
|
### Phase 6 — `api/routes/system.py`
|
|
|
|
- [x] Replace integration status with `mqtt_manager.get_all_sources_status()`
|
|
- [ ] Update frontend dashboard payload (MQTT widget now expects a list of
|
|
sources instead of a single `enabled`/`connected` pair — surface in UI)
|
|
|
|
### Phase 7 — Startup migration
|
|
|
|
- [x] Seed a "Default Broker" `MQTTSource` if legacy YAML / env had a
|
|
broker configured and the store is empty (`core.mqtt.legacy_migration`)
|
|
- [x] Deprecation warning logged on migration; YAML/env no longer read after
|
|
|
|
### Phase 8 — Remove legacy
|
|
|
|
- [x] Delete `core/mqtt/mqtt_service.py`
|
|
- [x] Delete `set_mqtt_service` / `get_mqtt_service` (mqtt_client.py)
|
|
- [x] Remove `MQTTService` from `main.py`
|
|
- [x] Remove `MQTTConfig` + `resolve_mqtt_password` from `config.py`
|
|
- [x] Remove `mqtt: MQTTConfig` from `Config` (with `extra="ignore"` so legacy
|
|
YAML still loads)
|
|
|
|
### Phase 9 — Verification
|
|
|
|
- [x] `pytest tests/ --no-cov -q` clean (973 passing; removed obsolete
|
|
`test_default_mqtt_disabled`)
|
|
- [x] `ruff check src/` clean
|
|
- [x] `tsc --noEmit` + `npm run build`
|
|
- [ ] Smoke test: Z2M target on a configured MQTT Source publishes to broker
|
|
(manual)
|
|
|
|
## Refactor: typed output-target factories + auto-registry
|
|
|
|
Replaced `target_type` string elif chains in `OutputTargetStore` and
|
|
`OutputTarget.from_dict` with: (1) `__init_subclass__` registry for
|
|
deserialization, (2) per-type typed `create_*_target` /
|
|
`update_*_target` methods called directly from the route layer's
|
|
`match data:` dispatch. API contract unchanged, no DB migration.
|
|
|
|
### Phase 1 — Registry on `OutputTarget`
|
|
|
|
- [x] Added `_registry` + `_type_key` ClassVars + `__init_subclass__(*, type_key)`
|
|
- [x] Rewrote `OutputTarget.from_dict` to dispatch via registry
|
|
- [x] Declared `type_key="led"` / `"ha_light"` / `"z2m_light"` on the three subclasses
|
|
|
|
### Phase 2 — Typed `create_*_target` methods
|
|
|
|
- [x] Extracted `_resolve_brightness`, `_resolve_transition`, `_check_unique_name`,
|
|
`_new_id_and_now`, `_finalize` helpers on the store
|
|
- [x] Added `create_wled_target` / `create_ha_light_target` / `create_z2m_light_target`
|
|
with per-type defaults (transition 0.5/0.3, update_rate 2.0/5.0) baked into
|
|
their signatures
|
|
|
|
### Phase 3 — Typed `update_*_target` methods
|
|
|
|
- [x] Added `update_wled_target` / `update_ha_light_target` / `update_z2m_light_target`
|
|
with `_begin_update` / `_commit_update` helpers
|
|
- [x] Each typed update method validates the target's class before mutating
|
|
|
|
### Phase 4 — Route migration
|
|
|
|
- [x] `create_target` route uses `match data:` to call typed store methods —
|
|
no more `getattr(data, "x", default)` pyramid
|
|
- [x] `update_target` route uses `match data:` and computes `settings_changed` /
|
|
`css_changed` / `brightness_changed` per-arm from typed fields
|
|
- [x] Helpers `_build_ha_mappings`, `_build_z2m_mappings`,
|
|
`_validate_device_exists`, `_resolve_effective_color_vs_id` extracted
|
|
|
|
### Phase 5 — Decision: keep both shims
|
|
|
|
After grepping for callers, `src/ledgrab/core/scenes/scene_activator.py:90`
|
|
calls `target_store.update_target(target_id, **changed)` with a dynamically
|
|
built dict — it legitimately doesn't know the target's type at the call site.
|
|
The shims are now ~30-line dispatchers that route to typed methods (no more
|
|
inline construction elif chains), so the original anti-pattern is gone while
|
|
the generic API remains available for "don't-know-the-type" callers like the
|
|
scene activator. Tests continue to use the shorthand `create_target("A", "led")`
|
|
form without churn.
|
|
|
|
### Phase 6 — Verify
|
|
|
|
- [x] `ruff check` clean on all modified files
|
|
- [x] `py -3.13 -m pytest tests/ --no-cov -q` — 974 passed (was 974 before)
|
|
- [ ] Manual smoke test in UI: create/edit/delete each of the three target types
|
|
|
|
## Custom card icons — extend to all card types
|
|
|
|
Migrate the existing icon-plate work (devices, LED targets, HA-light targets)
|
|
to all remaining card types. ~17 entity types. Branch: `feat/icons-everywhere`.
|
|
|
|
### Foundation
|
|
|
|
- [x] Refactor `icon-picker.ts` — replace hardcoded 2-entry `_adapters`
|
|
record with a `Map<EntityType, EntityTypeAdapter>` and expose
|
|
`registerIconEntityType()` for feature modules to register their
|
|
own. Added `makeSimpleIconAdapter()` helper that reduces a
|
|
registration to ~6 lines.
|
|
- [x] Generalised `bodyExtras` for discriminated routes (output-targets
|
|
`target_type` etc.) — now keyed off id, adapter does its own
|
|
lookup.
|
|
- [x] `_onDocumentClick` accepts any registered type instead of
|
|
hardcoded device/target check.
|
|
- [x] Locale entity-type labels added to en/ru/zh for 18 new types
|
|
(picture_source, audio_source, weather_source, value_source,
|
|
mqtt_source, ha_source, automation, scene_preset, sync_clock,
|
|
game_integration, audio_processing_template, pattern_template,
|
|
capture_template, pp_template, cspt, audio_template, gradient,
|
|
color_strip_source, asset).
|
|
|
|
### Backend (storage + schemas + routes per entity)
|
|
|
|
Recipe: add `icon: str = ""` + `icon_color: str = ""` to dataclass,
|
|
emit-when-truthy in `to_dict`, default `""` in `from_dict`; add 3
|
|
`Optional[str]` Field defs to Create/Response/Update schemas; thread
|
|
`getattr(entity, "icon", "") or ""` into the response builder.
|
|
SQLite JSON-blob storage means **no migration required**.
|
|
|
|
- [x] Integrations (6): weather_sources, value_sources, mqtt_source,
|
|
home_assistant_source, sync_clocks, game_integration
|
|
- [x] Streams (10): picture_source, audio_source, audio_template,
|
|
audio_processing_template, pattern_template, postprocessing_template,
|
|
color_strip_processing_template, color_strip_source, gradient,
|
|
capture_template (`storage/template.py` — was missed by initial pass)
|
|
- [x] Other (3): automation, scene_preset, asset
|
|
|
|
### Frontend (per feature module)
|
|
|
|
For each card render call:
|
|
|
|
- Use the new `core/card-icon.ts` helper:
|
|
`...makeCardIconFields('<type>', entity.id, entity)` spread into the
|
|
mod-card head — computes `iconHtml`/`iconColor`/`iconAttrs` in one go.
|
|
- Register the entity type in the feature module via
|
|
`registerIconEntityType('<type>', makeSimpleIconAdapter({ … }))`.
|
|
|
|
Modules wired:
|
|
|
|
- [x] streams.ts (7 cards: picture, capture, pp, cspt, audio source,
|
|
audio template, gradient — built-in gradients skip the plate)
|
|
- [x] automations.ts
|
|
- [x] scene-presets.ts
|
|
- [x] sync-clocks.ts
|
|
- [x] weather-sources.ts
|
|
- [x] value-sources.ts (bodyExtras propagates `source_type`)
|
|
- [x] mqtt-sources.ts
|
|
- [x] home-assistant-sources.ts
|
|
- [x] game-integration.ts
|
|
- [x] audio-processing-templates.ts
|
|
- [x] assets.ts
|
|
- [x] color-strips/cards.ts (bodyExtras propagates `source_type`)
|
|
- [WONTDO] pattern-templates.ts — uses legacy `wrapCard({content, actions})`
|
|
string API, not the mod-card system. Migration would be a separate
|
|
effort and the cards are tiny (name + rect count) so the value is low.
|
|
|
|
### Discriminated routes
|
|
|
|
Adapters provide `bodyExtras` to inject the discriminator field on PUT
|
|
so the Pydantic discriminated-union route validators don't reject the
|
|
icon-only update:
|
|
|
|
- output-targets → `target_type` (already wired before)
|
|
- color-strip-sources → `source_type`
|
|
- audio-sources → `source_type`
|
|
- value-sources → `source_type`
|
|
- picture-sources → `stream_type`
|
|
|
|
### Verification
|
|
|
|
- [x] `cd server && ruff check src/ tests/` clean
|
|
- [x] `cd server && npx tsc --noEmit` clean
|
|
- [x] `cd server && npm run build` produces 2.6 MB bundle
|
|
- [x] `cd server && py -3.13 -m pytest tests/ --no-cov -q` — 949 passed
|
|
- [ ] Manual: open picker on each card type, confirm save persists,
|
|
confirm channel-color preview matches the live card
|
|
|
|
## Device Event Notifications
|
|
|
|
Notify the user when LED devices come online/go offline (configured targets), and when new
|
|
WLED/serial devices are discovered or disappear from the LAN/USB. Each event class has a
|
|
configurable channel: `none` | `snack` | `os` | `both`. OS channel uses Web Notifications
|
|
(works in any browser tab and in the PWA shell — no platform-specific Python).
|
|
|
|
Branch: `feat/device-event-notifications`. Default ON.
|
|
|
|
### Backend
|
|
|
|
- [x] `core/devices/discovery_watcher.py` — long-running mDNS browser
|
|
(`AsyncServiceBrowser` kept alive for the process lifetime) + 10 s serial-port
|
|
poller. Fires `device_discovered`/`device_lost` via `processor_manager.fire_event`,
|
|
suppresses events for URLs already in `device_store`. Seeded ports do NOT generate
|
|
startup-time toasts.
|
|
- [x] Wired into `lifespan` (`main.py`). Gated by `notification_preferences.
|
|
background_discovery_enabled`. Default True. Stops before health monitor stop.
|
|
- [x] `api/schemas/preferences.py` — `NotificationPreferences` Pydantic v2 model with
|
|
the 4-event channel matrix, `background_discovery_enabled`, `startup_grace_sec`
|
|
(0..300), `flap_debounce_sec` (0..60).
|
|
- [x] `api/routes/preferences.py` — `GET/PUT /api/v1/preferences/notifications`,
|
|
persisted under `db.set_setting("notification_preferences", …)`. Corrupt stored
|
|
values fall back to defaults instead of 500.
|
|
- [x] Reuses existing `device_health_changed` event from `device_health.py` (already
|
|
fires online/offline transitions on the same event bus).
|
|
- [x] Tests: 7 in `tests/test_preferences_notifications_api.py`, 6 in
|
|
`tests/test_discovery_watcher.py`. Full pytest suite still 899 passing.
|
|
|
|
### Frontend
|
|
|
|
- [x] `js/features/notifications-watcher.ts` — listens to the three `server:*` DOM
|
|
events. Applies user prefs. Pipeline: startup grace → flap debounce → bulk
|
|
coalesce (≥3 events / 800 ms collapse to one summary).
|
|
- [x] Web Notification permission requested from the Settings → Notifications panel
|
|
via a user-gesture button. State chip reflects granted/denied/default.
|
|
- [x] Settings panel — new "Notifications" subtab between Backup and Appearance.
|
|
4 IconSelects (`none`/`snack`/`os`/`both`) + background-discovery toggle +
|
|
permission row + Test-notification button.
|
|
- [x] i18n: `settings.notifications.*` and `notifications.*` keys in en/ru/zh.
|
|
|
|
### Verification (notifications)
|
|
|
|
- [x] `npx tsc --noEmit` clean, `npm run build` produces 2.5 MB bundle.
|
|
- [x] `ruff check src/ tests/` clean. 899/899 pytest pass.
|
|
- [x] App import smoke-test (`from ledgrab.main import app`) loads 233 routes
|
|
without errors.
|
|
- [ ] Real-hardware test pending — verify on user's network:
|
|
(1) plug a fresh WLED in → snack toast appears, (2) configure it → next
|
|
offline transition fires both snack + OS toast, (3) Background-discovery
|
|
toggle off → no more discovered/lost events.
|
|
|
|
### Out of scope for v1
|
|
|
|
- Per-device-type granularity (we ship one matrix per event-type, no device-type split)
|
|
- Per-device mute list (deferred — user can globally toggle off if noisy)
|
|
- Native OS toast via Windows winrt API (Web Notifications cover the use case;
|
|
also avoids the `os_notification_listener` feedback loop)
|
|
- Notification history panel — could land later as the reserved `alerts` dashboard cell
|
|
|
|
## Server shutdown action
|
|
|
|
Let user choose what happens to LED targets on server shutdown.
|
|
|
|
- [x] Backend storage: `shutdown_action` in `db.settings` (`"stop_targets"` default | `"nothing"`)
|
|
- [x] Backend route: `GET/PUT /api/v1/system/shutdown-action` in `system_settings.py`
|
|
- [x] Backend schema: `ShutdownActionResponse/Request` in `schemas/system.py`
|
|
- [x] Backend wiring: lifespan shutdown in `main.py` reads action, passes `restore_devices` flag to `processor_manager.stop_all()`
|
|
- [x] `processor_manager.stop_all(restore_devices: bool = True)` — when False, calls public `proc.cancel_task()` (defined on `TargetProcessor`) which awaits cancellation without restoring device state; skips `_restore_device_idle_state` loop. No reach into private `_task` attribute.
|
|
- [x] Frontend: hidden `<select>` + IconSelect in `settings.html` General tab (icons via `ICON_SQUARE` / `ICON_CIRCLE` from `core/icons.ts`)
|
|
- [x] Frontend: load/save handlers in `features/settings.ts`, wired into `openSettingsModal()`
|
|
- [x] i18n: en / ru / zh keys for label, hint, item descriptions
|
|
- [ ] Real-hardware test pending — verify that "nothing" actually leaves a WLED + a serial device on the last frame after `Ctrl+C`/SIGTERM.
|
|
|
|
## WebUI Redesign — "Lumenworks" Studio-Console Aesthetic
|
|
|
|
Full-app UI/UX refresh. Design direction committed to by user 2026-04-24.
|
|
Mockup lives at [server/docs/ui-redesign-mockup.html](server/docs/ui-redesign-mockup.html).
|
|
Phases are independent and CSS-only where possible — backend untouched.
|
|
|
|
### Phase 1 — Design tokens & font embed
|
|
|
|
- [x] Embed variable fonts (`server/src/ledgrab/static/fonts/`):
|
|
Manrope (latin + latin-ext + cyrillic + cyrillic-ext),
|
|
JetBrains Mono (same 4 subsets),
|
|
Big Shoulders Display (latin + latin-ext). Total +201 KB gzipped,
|
|
served via `unicode-range` so only latin paints on first load.
|
|
- [x] `fonts.css` — declare `@font-face` entries for all new families with
|
|
proper `unicode-range` subsetting; keep DM Sans + Orbitron registered
|
|
for legacy-token callers during migration.
|
|
- [x] `base.css` — add additive Lumenworks tokens:
|
|
`--font-display/--font-brand/--font-body`, `--lux-r-*`, `--lux-hairline`,
|
|
`--lux-rule`. Both `[data-theme="dark"]` and `[data-theme="light"]`
|
|
define `--lux-bg-0…3`, `--lux-line/-bold`, `--lux-ink/-dim/-mute/-faint`,
|
|
`--ch-signal/-cyan/-magenta/-amber/-coral/-violet`, `--lux-signal-glow`,
|
|
`--lux-shadow-rack`. Existing tokens untouched — no visual regression.
|
|
|
|
### Phase 2 — Shell (header → transport bar + channel-strip sidebar)
|
|
|
|
- [x] `index.html` — `.tab-bar` moved out of `<header>` into a new
|
|
`<aside class="sidebar">`; wrapped content in `.app-body` 2-col grid
|
|
(sidebar | main). `.transport-center` section added between
|
|
`.header-title` and `.header-toolbar` with a placeholder `.transport-status`
|
|
chip ("Ready" → "Armed · N live" wired in Phase 3). All tab-button IDs,
|
|
`data-tab` attributes, and `onclick="switchTab(…)"` handlers preserved.
|
|
- [x] `layout.css` — `<header>` rebuilt as the transport bar: 3-column grid
|
|
(brand | center | toolbar), 60 px fixed height, sticky, gradient bottom
|
|
rule with channel-color wash. `.header-title::before/::after` render
|
|
the glowing LED brand mark; `#server-status` repositioned as the LED
|
|
core pip. `#server-version` restyled as a mono-type console badge.
|
|
- [x] `sidebar.css` (new) — vertical channel-strip navigation. Active tab
|
|
gets a glowing left stripe + radial tint. `.sidebar-foot` contains
|
|
a `.cpu-meter` plate with two live bars (Load, FPS) ready to be
|
|
JS-bound in Phase 3. Collapses to a 56 px icon rail at ≤1100 px;
|
|
hides entirely at ≤600 px via `display: contents` so `.tab-bar`
|
|
falls through to `mobile.css`'s fixed-bottom strip unchanged.
|
|
- [x] `all.css` — new sidebar import after layout.
|
|
- [x] `base.css` — body font-family switched to `var(--font-body)` which
|
|
resolves to Manrope (with DM Sans + system fallbacks). Added
|
|
`font-feature-settings` for stylistic set + alternate 1.
|
|
- [x] Locale additions: `sidebar.workspaces`, `sidebar.load`, `sidebar.fps`,
|
|
`transport.status.ready`, `transport.status.armed` in en/ru/zh.
|
|
- [x] Tutorial + auth selectors (`header .header-title`, `#tab-btn-*`,
|
|
`.tab-bar` querySelector, `a.header-link[href="/docs"]`, onclick
|
|
markers on theme/settings/search) all survive the move.
|
|
- [ ] JS: bind `.cpu-meter` + `.transport-status` chip to existing
|
|
`performance` WebSocket / poller. Done as part of Phase 3.
|
|
- [ ] Tablet-range visual polish pass once other phases render (some tabs
|
|
currently have their own internal sticky headers that may overlap
|
|
the transport bar on narrow viewports).
|
|
|
|
### Phase 3 — Dashboard hero + module redesign
|
|
|
|
- [x] `cards.css` — `.card` gets rack-module treatment: channel stripe on
|
|
left edge (color-coded via `data-card-type` + `.ch-*` utility classes),
|
|
`::after` corner bracket in top-right, mono-typed metric labels
|
|
planned for Phase 4. Running cards glow the stripe brighter + emit a
|
|
`signalFlow` keyframe strip along the bottom edge.
|
|
- [x] Removed the `@property --border-angle` rotating conic-gradient border
|
|
(retired the WebKit mask workaround + light-theme variant + fallback
|
|
for `@supports not (mask-composite: exclude)`). Replaced with the
|
|
signal-flow strip — one animated linear-gradient on a 2 px line, no
|
|
GPU layer compositing per card.
|
|
- [x] `dashboard.css` — `.dashboard-target` rows pick up the same channel
|
|
stripe + signal-flow treatment. Section headers now use mono caps
|
|
with a channel-green underline accent. Metric values use mono with
|
|
tabular numerics; labels use silkscreened micro-caps.
|
|
- [x] Skeleton-card rewritten: left hairline + corner bracket so it reads
|
|
as "loading module" instead of a generic flashing block.
|
|
`skeletonShimmer` gradient replaces the old opacity-pulse on
|
|
`--text-color`.
|
|
- [x] `_updateSidebarMeter` binds CPU% (Load) and app-CPU share (FPS)
|
|
to the sidebar meter plate on every perf poll.
|
|
- [x] `_updateTransportStatus` updates the transport chip ("Ready" →
|
|
"Armed · N live") whenever the dashboard's running-target set is
|
|
recomputed.
|
|
- [ ] `.hero` 4-cell readout row (Active Patches / Throughput / CPU /
|
|
Latency + inline sparklines) — CSS tokens + layout are ready; HTML
|
|
render deferred until the dashboard JS is refactored to emit it
|
|
(Phase 3b, non-blocking).
|
|
|
|
### Phase 4 — Other tabs adopt module language
|
|
|
|
- [x] `tree-nav.css` — trigger pill gets a channel stripe on its left edge
|
|
(glows + widens when open). Trigger title uses mono-uppercase with
|
|
wide letter-spacing. Dropdown panel has a gradient channel-accent
|
|
rule across its top edge. Group headers use silkscreened micro-caps
|
|
with a small square marker instead of the old bold-uppercase. Active
|
|
leaf has a pulsing LED pip on the left and a channel tint behind it.
|
|
Count badges switched to mono tabular-nums in 2-px-radius pills.
|
|
- [x] `.subtab-section-header` — channel-green underline accent + mono
|
|
micro-caps. Consistent with the dashboard-section pattern so the
|
|
whole app shares one section-header language.
|
|
- [x] `.stream-tab-btn` sub-tabs — mono uppercase with wide tracking,
|
|
active tab shows channel-green underline + glowing count badge.
|
|
- [x] `.perf-chart-card` — channel stripe on the left (replaces old
|
|
`border-top` accent). Per-metric accents swapped to channel palette
|
|
(`--ch-coral` for CPU, `--ch-violet` for RAM, `--ch-signal` for GPU,
|
|
`--ch-amber` for temp). Corner bracket added. Metric values pick up
|
|
`tabular-nums` + a soft glow.
|
|
- [x] `cards.css` — channel-color mapping extended to attributes the JS
|
|
already emits (`data-target-id` → green, `data-stream-id` → cyan,
|
|
`data-audio-source-id` → magenta, `data-automation-id` /
|
|
`data-scene-id` → violet). No JS changes required; cards pick up
|
|
their correct stripe automatically on the Targets/Sources/Automations
|
|
tabs.
|
|
- [x] Graph editor — toolbar gets a gradient background + hairline +
|
|
rack shadow + backdrop blur. Canvas and nodes untouched.
|
|
- [x] `.template-card` — Lumenworks treatment (channel stripe on left,
|
|
corner bracket top-right, hairline border, hover lift + stripe
|
|
glow). Brings Inputs (streams / capture / pp / cspt / pattern
|
|
templates) and Integrations (HA / MQTT / weather / value /
|
|
sync-clock / game-integration cards) up to the same visual
|
|
language as `.card` and `.dashboard-target`.
|
|
- [x] `cards.css` — channel mapping extended to `.template-card`.
|
|
Direct attr hooks for `data-stream-id`/`data-template-id`/`data-pp-template-id`
|
|
(cyan), `data-cspt-id`/`data-pattern-template-id` (signal),
|
|
`data-audio-template-id`/`data-apt-id` (magenta). Section-scoped
|
|
hooks via `[data-card-section="…"]` for cards that share a
|
|
generic `data-id` (HA / MQTT / weather / value → cyan;
|
|
game-integrations → amber; sync-clocks → violet; HA-light-targets
|
|
→ signal). No JS changes — uses the section markup `CardSection`
|
|
already emits.
|
|
- [x] Graph editor nodes — body fill `--lux-bg-1` with hairline stroke,
|
|
hover bold-line, selected/running stroke `--ch-signal` with
|
|
drop-shadow glow. Title font switched from DM Sans to
|
|
`--font-display`; subtitle to mono uppercase wide-tracking.
|
|
Port-drop-target glow recoloured to `--ch-signal`. Port labels
|
|
adopt the mono caption treatment. Grid dots use `--lux-line`.
|
|
Running gradient stops switched from `--primary-color`/`--success-color`
|
|
to channel palette (signal → cyan → signal).
|
|
|
|
### Phase 5 — Modal restyle
|
|
|
|
- [x] `modal.css` — backdrop gains a radial dim + 6 px blur for stronger
|
|
separation. `.modal-content` gets a gradient background + hairline +
|
|
deep rack shadow. Channel-accent rule across the top edge driven by
|
|
`--modal-ch` (per-modal override). Corner bracket bottom-right on
|
|
desktop. `.modal-header` gains a vertical channel-color stripe to
|
|
the left of the title; `.modal-footer` picks up a hairline divider.
|
|
- [x] Per-modal channel mapping by modal ID:
|
|
- Target editors → green
|
|
- Input/Source editors → cyan
|
|
- Audio editors → magenta
|
|
- Automation / Scene / Game editors → violet
|
|
- Settings / API key / Setup / Notifications → amber
|
|
- Confirm dialog → coral
|
|
- [x] `components.css` — inputs use hairline borders, tabular-nums mono
|
|
for `input[type="number"]`, channel-green focus ring + glow. Buttons
|
|
use mono-uppercase type, signal-glow on primary, coral-glow on
|
|
danger. `<select>` audit deferred (project already enforces via
|
|
CLAUDE.md rule + IconSelect/EntitySelect wrappers).
|
|
|
|
### Phase 6 — Mobile dedicated shell
|
|
|
|
- [x] `mobile.css` (existing file, not forked) — fixed-bottom `.tab-bar`
|
|
promoted to full Lumenworks treatment: gradient background + hairline
|
|
divider at top + channel-accent rule matching the transport-bar
|
|
bottom. Active tab gets an LED pip above the icon and a channel-tint
|
|
background. Tab labels + badges use mono uppercase to match the
|
|
rest of the app. Phone (≤600 px): modal corner-bracket hidden
|
|
(fullscreen modals), modal-header stripe slimmed to 18 px.
|
|
- [x] Phase 2's layout.css already strips the transport-center on phones
|
|
and collapses the sidebar via `display: contents`, so the mobile
|
|
shell automatically routes the tab-bar to the bottom without a
|
|
separate JS hook.
|
|
- [WONTDO] Fork into `mobile-shell.css` — keeping changes in `mobile.css`
|
|
since the cascade was already organized by viewport. A rename adds
|
|
churn without improving maintainability.
|
|
|
|
### Phase 7 — Microcopy + retire legacy
|
|
|
|
- [x] Locale rename: `targets.title` + `dashboard.section.targets` →
|
|
"Channels" (en) / "Каналы" (ru) / "通道" (zh);
|
|
`streams.title` → "Inputs" / "Входы" / "输入".
|
|
Automations kept as-is (Automations + Scenes is a meaningful
|
|
distinction; "Patches" would conflate them). Internal tab keys
|
|
(`dashboard` / `automations` / `targets` / `streams` / `integrations`
|
|
/ `graph`) unchanged so no JS or localStorage migration needed.
|
|
- [x] Ambient WebGL background — default is already `off`; kept the
|
|
toggle button and localStorage preference so users who want the
|
|
shader can turn it on. No entry-point change needed: `data-bg-anim`
|
|
is initialized from localStorage with `off` fallback.
|
|
- [DEFERRED] Delete DM Sans + legacy color tokens — would cascade through
|
|
every file that reads `--primary-color` / `--text-color` etc. Safer
|
|
as a separate cleanup PR after the new design has soaked.
|
|
- [WONTDO] Delete `mobile.css` — Phase 6 kept the filename.
|
|
|
|
## Dashboard Customization
|
|
|
|
Per-account dashboard layout — slide-in Customize panel lets users
|
|
toggle section / perf-cell visibility, reorder via drag, change density,
|
|
pick presets, and import/export the layout as JSON. Server-synced via
|
|
`db.get_setting('dashboard_layout')` so settings follow the user.
|
|
|
|
- [x] `js/features/dashboard-layout.ts` — schema (open registry of section
|
|
/ perf-cell keys so v1.1 cards slot in with no migration), defaults,
|
|
5 built-in presets (Studio/Operator/Showrunner/Diagnostics/TV),
|
|
localStorage cache + server sync, legacy-key migration from
|
|
`dashboard_collapsed`, `perfMetricsMode`, `perfChartColor_*`.
|
|
- [x] `api/routes/preferences.py` — `GET/PUT/DELETE
|
|
/api/v1/preferences/dashboard-layout`. Treats payload as opaque
|
|
(frontend owns the schema); validates only that body is an object
|
|
with a numeric `version`. 6 pytest tests in
|
|
`tests/test_preferences_api.py` cover round-trip, default-empty,
|
|
validation, delete, and unknown-field passthrough.
|
|
- [x] `js/features/dashboard.ts` — sections rendered into a fragment map,
|
|
then assembled in layout-driven order; perf section stays pinned
|
|
top (chart-persistence reasons) but its visibility is layout-
|
|
driven. Layout-change subscription invalidates the in-place-update
|
|
optimization so density / order / visibility changes always
|
|
rebuild section HTML.
|
|
- [x] `js/features/perf-charts.ts` — `renderPerfSection()` iterates
|
|
`getOrderedPerfCells()`; existing legacy `setPerfMode` writes
|
|
through to the layout so the global toggle and the customize
|
|
panel stay in sync.
|
|
- [x] `js/features/dashboard-customize.ts` + `css/dashboard-customize.css`
|
|
— slide-in panel, hand-rolled HTML5 drag-and-drop reorder, ↑/↓
|
|
buttons for keyboard / TV remote, debounced (300 ms) autosave,
|
|
live preview while open. Reset / export / import actions.
|
|
- [x] i18n keys for `dashboard.customize.*` in en/ru/zh.
|
|
- [ ] (v1.1) Audio meters section — peak / RMS / BPM bars per audio
|
|
source. Schema key `audio-meters` already reserved.
|
|
- [ ] (v1.1) Alerts section — quiet by default, loud on issues.
|
|
Reserved key `alerts`.
|
|
- [ ] (v1.1) Live LED preview strip per running device. Reserved
|
|
key `led-preview`.
|
|
- [ ] (v1.1) Source thumbnails grid (1 fps multiviewer). Reserved
|
|
key `source-thumbs`.
|
|
- [ ] (v1.2) Pinned section (user-curated mix of targets / scenes /
|
|
devices). Reserved key `pinned`.
|
|
- [ ] (v1.2) Patch/flow map — read-only mini graph of routing.
|
|
Reserved key `flow`.
|
|
|
|
## BLE LED Controller Support (SP110E / Triones / Zengge / Govee)
|
|
|
|
Add support for Bluetooth Low Energy LED controllers driven by mobile apps like "LED Hue", HappyLighting, iLightsIn. Whole-strip ambient-color output only — these protocols don't support per-pixel streaming.
|
|
|
|
- [x] Add `bleak>=0.22` as optional extra `[ble]` in `server/pyproject.toml` (desktop-only, NOT in android `build.gradle.kts`)
|
|
- [x] `core/devices/ble_transport.py` — bleak wrapper: scan, connect, write-with/without-response
|
|
- [x] `core/devices/ble_protocols/` package
|
|
- [x] `__init__.py` — `BLEProtocol` dataclass + registry (family → encoder)
|
|
- [x] `sp110e.py` — SP110E / SP108E (service FFE0, char FFE1, `RR GG BB 00 1E` static-color frame)
|
|
- [x] `triones.py` — Triones / HappyLighting / LEDnet (service FFE5, char FFE9, `7E 07 05 03 RR GG BB 10 EF`)
|
|
- [x] `zengge.py` — Zengge / iLightsIn (service FFE0, framing `56 RR GG BB 00 F0 AA`)
|
|
- [x] `govee.py` — Govee unencrypted framed protocol (AES keyed variants — marked experimental)
|
|
- [x] `core/devices/ble_client.py` — unified `BLEClient(LEDClient)` — picks protocol by `ble_family`, averages strip → one color, drops duplicate frames, rate-limits to BLE connection interval
|
|
- [x] `core/devices/ble_provider.py` — `BLEDeviceProvider` + discovery via `BleakScanner`
|
|
- [x] Register in `core/devices/led_client.py::_register_builtin_providers` (guarded `try/except ImportError`)
|
|
- [x] Storage: `ble_family`, `ble_govee_key` fields threaded through `Device.__init__`/`to_dict`/`from_dict`/`_UPDATABLE_FIELDS`/`create_device`
|
|
- [x] Schemas: BLE fields on `DeviceCreate`, `DeviceUpdate`, `DeviceResponse`
|
|
- [x] Routes: BLE fields propagated through create/update in `api/routes/devices.py` + `_device_to_response`
|
|
- [x] ProcessorManager: `ble_family`/`ble_govee_key` added to `_DEVICE_FIELD_DEFAULTS` and `DeviceInfo`; passed through `wled_target_processor.py` and `group_client.py` to `create_led_client`
|
|
- [x] Tests: 21 protocol encoder unit tests + 16 BLEClient fake-transport tests — all passing, 814 total tests still green
|
|
- [x] Frontend: BLE option in the device type picker with a bluetooth Lucide icon; add-device modal shows a 4-option `IconSelect` for protocol family (SP110E / Triones / Zengge / Govee) with a Govee-only AES key field that auto-hides for the other three families; URL label/placeholder/hint adapt to `ble://<address>` pattern; submit payload carries `ble_family` (+ optional `ble_govee_key`); clone flow pre-fills family and key; modal dirty-check snapshots the new fields; network scan button now also discovers BLE peripherals via the existing `/api/v1/devices/discover?device_type=ble` endpoint
|
|
- [x] Frontend: `isBleDevice` helper in `core/api.ts`; `ICON_BLUETOOTH` + `ICON_LIGHTBULB` constants in `core/icons.ts`; `bluetooth` path in `core/icon-paths.ts`; i18n keys in `en.json` / `ru.json` / `zh.json`; TypeScript compiles; esbuild bundle rebuilt
|
|
- [x] Android BLE via Kotlin bridge — `BleBridge.kt` singleton (scan/connect/write/disconnect); `android_ble_transport.py` Python wrapper; `make_transport()` factory in `ble_transport.py` auto-selects backend; `BleBridge.init()` called from `LedGrabApp.onCreate`; BLE permissions in `AndroidManifest.xml`
|
|
- [x] Govee per-model AES key — `_encrypt_govee_frame()` in `ble_client.py` uses AES-128-ECB from `cryptography`; key validated on `BLEClient` construction; applied to both `send_pixels` and `set_power`; 8 new AES unit tests
|
|
|
|
## Android — Restore Multi-ABI Wheels
|
|
|
|
During emulator testing, we switched the build to **x86 only** (see `android/app/build.gradle.kts` `abiFilters`) to avoid having to keep the arm64-v8a / x86_64 pydantic-core wheels current. Before shipping, restore all three ABIs:
|
|
|
|
- [x] Rebuild `pydantic-core` wheels for all three ABIs with the current SOABI + libpython linking settings (`android/build-scripts/build-pydantic-core.sh` — now supports `arm64`, `x86_64`, `x86` args; defaults to all three).
|
|
- [x] Verify wheels: all three now list `libpython3.11.so` in `NEEDED` (`llvm-readelf -d`), automated in the build script.
|
|
- [x] Restored `abiFilters += listOf("arm64-v8a", "x86_64", "x86")` in `build.gradle.kts`. Multi-ABI debug APK builds cleanly (~99 MB).
|
|
- [ ] Re-test on real ARM64 Android TV hardware (still pending — only emulator-verified build).
|
|
|
|
Build cache + scripts live in `android/build-scripts/` and `android/.build-cache/` (junction host + sysconfigdata for each ABI).
|
|
|
|
## Android CI Pipeline
|
|
|
|
Build the Android APK automatically on push/tag.
|
|
|
|
- [x] Generate Gradle wrapper (`gradlew`) and commit it
|
|
- [x] Create CI workflow (`.gitea/workflows/build-android.yml`)
|
|
- JDK 17 + Android SDK + NDK setup
|
|
- Python 3.11 for Chaquopy build
|
|
- Recreate the directory junction via `ln -s` on Linux CI
|
|
- `./gradlew assembleDebug` on master push, `assembleRelease` on `v*` tags (if signing secrets set)
|
|
- Uploads APK as CI artifact; attaches to Gitea release on tag push
|
|
- [x] Commit pre-built pydantic-core wheels to `android/wheels/` (arm64, x86, x86_64)
|
|
- [x] APK signing for release builds — conditional signing config reads keystore from env vars (`ANDROID_KEYSTORE_PATH/_PASSWORD/_ALIAS/_KEY_PASSWORD`), falls back to debug signing locally
|
|
- [ ] Provision a real keystore and add the four CI secrets:
|
|
- `ANDROID_KEYSTORE_BASE64` (base64-encoded .jks)
|
|
- `ANDROID_KEYSTORE_PASSWORD`
|
|
- `ANDROID_KEY_ALIAS`
|
|
- `ANDROID_KEY_PASSWORD`
|
|
- [ ] Add `LedGrab-{tag}-android-release.apk` row to the release description table in `.gitea/workflows/release.yml` → `create-release` job
|
|
- [ ] Verify the CI workflow passes end-to-end with the now-restored multi-ABI build (larger APK, longer Android build step)
|
|
|
|
## Android Root Capture (No Permission Dialog, No System Indicator)
|
|
|
|
MediaProjection shows a mandatory system overlay/indicator while capturing — unavoidable on stock Android. Many cheap Android TV boxes ship pre-rooted, so an alternative root-only path gives much better UX.
|
|
|
|
- [x] Root detection — `Root.kt` checks common `su` binary paths and, on demand, runs `su -c id` to actually prove UID 0. First call triggers Magisk's grant dialog; grant is cached per session. Exposed to Python via Chaquopy.
|
|
- [x] `RootScreenrecord.kt` — spawns `su -c screenrecord --output-format=h264 --size=WxH -`, feeds the H.264 stdout through a MediaCodec decoder whose output Surface is wired into an ImageReader (RGBA_8888, row-stride-aware). Decoded frames reach the Python pipeline via `PythonBridge.pushRootFrame`.
|
|
- [x] Python-side `RootScreenrecordEngine` (`core/capture_engines/root_screenrecord_engine.py`) mirrors `MediaProjectionEngine` with `ENGINE_PRIORITY=110` (> MediaProjection's 100) so the factory picks it automatically when available.
|
|
- [x] `MainActivity` tries `Root.requestGrant()` before launching the MediaProjection consent flow — on rooted devices the consent dialog is skipped entirely. `CaptureService` has a `createRootIntent()` entry point that bypasses the MediaProjection path.
|
|
- [x] Fallback: if `Root.requestGrant()` returns false (no root, user denied, or `su` timeout) the existing MediaProjection flow runs unchanged.
|
|
- [ ] Real-hardware test pending — need to verify on the user's Magisk'd TV box that: (1) grant dialog appears once, (2) frames actually flow through MediaCodec without the Android 14 capture indicator showing, (3) stop/start cycle terminates the `su` process cleanly.
|
|
- [WONTDO] `SurfaceControl.screenshot()` via reflection — renamed/moved across API 28/29/30/33, hidden-API blocklist varies by release, even rooted apps hit it; days of maintenance for a marginal latency win over the screenrecord path. Not worth it.
|
|
- [WONTDO] `adb screencap` fallback — full-PNG-per-frame pipeline is slower than MediaProjection, no value as a last resort.
|
|
|
|
Known projects using the screenrecord approach for reference: scrcpy (over ADB), scrcpy-hidden-api, shizuku.
|
|
|
|
## Android Autostart on Boot
|
|
|
|
Boot-time, zero-interaction startup so LedGrab always has display capture and control on rooted TV boxes.
|
|
|
|
- [x] Manifest: declare `RECEIVE_BOOT_COMPLETED`, `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`, `WAKE_LOCK`; register `.BootReceiver` for `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED` / `MY_PACKAGE_REPLACED`.
|
|
- [x] `BootReceiver.kt` — gated by `AutostartPrefs` + `Root.looksRooted()`; dispatches `CaptureService.createRootIntent()` via `ContextCompat.startForegroundService`. Unrooted devices are a no-op because MediaProjection consent cannot be bypassed silently.
|
|
- [x] `AutostartPrefs.kt` — thin SharedPreferences wrapper, defaults to enabled. Shown as a CheckBox on the stopped panel; greyed out on unrooted devices.
|
|
- [x] `CaptureService` returns `START_REDELIVER_INTENT` for root-mode intents so the OS can cleanly restart the service after being killed (token-free path). MediaProjection-mode keeps `START_NOT_STICKY` — restart is pointless with a dead consent token.
|
|
- [x] `isRunning` race: moved assignment to after `startForeground` succeeds, resets on exception; `onStartCommand` wraps `startForeground` in try/catch and stops the service cleanly if the FG transition fails.
|
|
- [x] Root-capture watchdog: coroutine on `serviceScope` checks `RootScreenrecord.framesDelivered` every 5s after a 5s grace. Respawns the pipeline (reusing the existing Python bridge) on stall, caps at 3 consecutive restarts before giving up.
|
|
- [x] `RootScreenrecord.framesDelivered` exposed as a property backed by `AtomicInteger` (was `@Volatile var framesDelivered = 0` with non-atomic `+= 1`).
|
|
- [x] `ScreenCapture` accepts `onProjectionStopped` lambda — `MediaProjection.Callback.onStop` now tears the whole service down instead of leaving a stale FG notification.
|
|
- [x] `MainActivity` wires the autostart toggle to `AutostartPrefs`; enabling it prompts `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` so Doze doesn't kill the FG service on phones.
|
|
- [x] `versionCode` derived from `git rev-list --count HEAD` (or `ANDROID_VERSION_CODE` env var in CI). Was stuck at 1 — sideload updates were silently refusing to install.
|
|
- [ ] Real-hardware test pending — need to verify on the user's Magisk'd TV box that: (1) boot-time autostart dispatches the service without UI, (2) capture indicator still absent under root mode post-reboot, (3) watchdog respawns the pipeline when `screenrecord` is externally killed, (4) sideload upgrade installs cleanly after the versionCode bump.
|
|
- [ ] Optional follow-up: "kiosk" mode — add `<category android:name="android.intent.category.HOME" />` to `MainActivity` so power users can set LedGrab as the default TV launcher for truly always-running behavior.
|
|
|
|
## Android USB Serial Support
|
|
|
|
Drive USB LED controllers (APA102, WS2812) connected directly to the Android TV box via USB-to-serial adapters.
|
|
|
|
- [x] Added `com.github.mik3y:usb-serial-for-android:3.8.1` (via JitPack) to `android/app/build.gradle.kts`.
|
|
- [x] Kotlin `UsbSerialBridge` singleton (`android/app/src/main/java/com/ledgrab/android/UsbSerialBridge.kt`) — exposes `listDevices()`, `open(vid, pid, serial, baud)`, `write(handle, ByteArray)`, `close(handle)`. Permission request fires automatically from `open()` when the user hasn't granted access yet. Handles are opaque integers, port map is synchronized, so Python threads can share one bridge.
|
|
- [x] Python `AndroidSerialTransport` in `server/src/ledgrab/core/devices/android_serial_transport.py` drives the bridge through Chaquopy. `SerialTransport` Protocol + `PySerialTransport` + `list_serial_ports()` factory live in `serial_transport.py`; `AdalightClient` and `SerialDeviceProvider` now go through the abstraction instead of importing `pyserial` directly.
|
|
- [x] URL scheme extended: `usb:VID:PID[:serial][@baud]` on Android alongside the existing `COM3[:baud]` / `/dev/ttyUSB0[:baud]` desktop paths.
|
|
- [x] App initializes the bridge on startup (`LedGrabApp.onCreate` → `UsbSerialBridge.init(this)`); manifest declares `uses-feature android.hardware.usb.host`.
|
|
- [ ] Real-device test pending — no USB-serial hardware on dev machine. Need to verify on a TV box with CH340, CP2102, or FTDI adapter.
|
|
- [ ] Document supported USB LED controllers in README (once real-device test passes).
|
|
- [ ] Optional: auto-launch the app when a known USB-serial adapter is plugged in (intent-filter on `USB_DEVICE_ATTACHED` + `res/xml/device_filter.xml`). Skipped in v1 — users can just open LedGrab and hit "Discover".
|
|
- [x] ESP-NOW client (`espnow_client.py` / `espnow_provider.py`) now routes through `SerialTransport` — `open_transport()` for the gateway serial link, `list_serial_ports()` + `port_exists()` for discovery/validation. Works transparently with `usb:VID:PID` URLs on Android. (Gateway protocol is write-only, so no `read()` extension was needed after all.)
|
|
|
|
## Performance Metrics Abstraction
|
|
|
|
- [x] `MetricsProvider` protocol + dataclass DTOs (`MemorySnapshot`, `ProcessSnapshot`) live in `server/src/ledgrab/utils/metrics/types.py`. Each provider has its own module: `psutil_provider.py`, `null_provider.py`, `android_provider.py`.
|
|
- [x] Factory `get_metrics_provider()` in `utils/metrics/__init__.py` selects Android → psutil → Null. `psutil` import is now confined to one place.
|
|
- [x] `api/routes/system.py` and `core/processing/metrics_history.py` use the provider; no more `if psutil is not None` guards in the hot paths.
|
|
- [x] Android `/proc`-backed provider implemented (`/proc/stat`, `/proc/meminfo`, `/proc/self/stat`, `/proc/self/status`). Carries previous-sample state for delta-based CPU%; degrades to zeros if any `/proc` file is locked down. 12 unit tests cover both desktop and Android paths.
|
|
|
|
## Android Performance Metrics — Future Enhancements
|
|
|
|
Beyond the `/proc`-based AndroidMetricsProvider that's now in place:
|
|
|
|
- [x] Device battery + thermal-zone readings (`/sys/class/power_supply/battery/{capacity,temp}`, `/sys/class/thermal/thermal_zone*/temp` filtered by zone type). Surfaced through `MetricsProvider.thermals()`, `PerformanceResponse.{cpu_temp_c,battery_percent,battery_temp_c}`, the metrics-history snapshot, and a new dashboard temperature chart that hides itself when the backend reports null. GPU card now hides (no "unavailable" placeholder) when no GPU is present.
|
|
- [WONTDO] Optional: app-specific memory via `Debug.getMemoryInfo()` through a Kotlin → Python Chaquopy bridge (more accurate than `VmRSS` for split-app-process accounting)
|
|
- [WONTDO] Optional: GPU usage via `/sys/class/kgsl/kgsl-3d0/gpubusy` on Adreno, Mali-specific paths for Mali GPUs
|
|
|
|
## Refactor: Per-Provider Device Configs
|
|
|
|
Replace flat `DeviceInfo` + `**kwargs` provider contract with a discriminated union of typed per-provider config dataclasses.
|
|
|
|
- [x] Phase 1 — `DeviceConfig` hierarchy + `Device.to_config()` (non-breaking, additive only)
|
|
- [x] Phases 2+3 — narrow `LEDDeviceProvider.create_client` to typed configs; migrate 3 call sites; delete `DeviceInfo` + `_get_device_info` + `_DEVICE_FIELD_DEFAULTS` (single PR)
|
|
- [x] Phase 4 — migrate `tests/test_group_device.py` to `GroupConfig`/`ProviderDeps`; remove legacy `GroupLEDClient` init path; 47-test config suite with 100% coverage on `device_config.py`
|
|
- [ ] Phase 5 (separate PR, optional) — Pydantic v2 discriminated union in `api/schemas/devices.py`; scope frontend POST/PATCH payloads by `device_type`
|
|
|
|
## Expand device support (Phase 1: open protocols)
|
|
|
|
Branch: `feat/expand-device-support`.
|
|
|
|
Goal: maximize the universe of LED controllers LedGrab can drive by adding aggregator + open-protocol providers in roughly-this order. Each driver follows the established `LEDDeviceProvider` + `*Config` + tests pattern.
|
|
|
|
### Phase 1.1 — Standalone DDP target ✅ shipped (commit `8f1140a`)
|
|
|
|
DDP packet layer (previously WLED-internal) promoted to a first-class device
|
|
type. Pixelblaze, ESPixelStick, xLights/Falcon endpoints, and generic DDP
|
|
receivers are now drivable directly without WLED in the path.
|
|
|
|
### Phase 1.2 — Yeelight LAN
|
|
|
|
Xiaomi/Yeelight bulbs, port 55443 TCP JSON. Direct protocol (no
|
|
`python-yeelight` dependency — implementation is ~200 lines).
|
|
|
|
- [x] `YeelightConfig` dataclass with `yeelight_min_interval_ms` rate limit
|
|
- [x] `YeelightClient` in `core/devices/yeelight_client.py` — TCP JSON-RPC,
|
|
averaging single-pixel adapter, client-side rate gate
|
|
- [x] SSDP-style discovery (Yeelight's variant on `239.255.255.250:1982`)
|
|
- [x] `YeelightDeviceProvider` with validate/health/discover
|
|
- [x] Storage + API schemas + route handler wiring
|
|
- [x] 34 unit tests (URL parsing, RGB packing, averaging, rate limit, SSDP
|
|
parsing, provider validate/discover, Device.to_config round-trip)
|
|
- [ ] Frontend: Yeelight in device-type picker + edit form (spawned to a
|
|
`frontend-design` subagent)
|
|
- [ ] Locale strings (en/ru/zh)
|
|
- [ ] Music mode (~60 Hz updates via reverse-TCP) — follow-up, current
|
|
MVP caps at ~2 Hz via the client-side rate gate
|
|
|
|
### Phase 1.3 — WiZ Connected
|
|
|
|
Philips' UDP-local budget tier. Port 38899 JSON UDP.
|
|
|
|
- [x] `WiZConfig` + `WiZClient` + `WiZDeviceProvider`
|
|
- [x] UDP broadcast discovery on 255.255.255.255:38899 with the standard
|
|
`registration` envelope; replies parsed for IP+MAC.
|
|
- [x] Sync `send_pixels_fast` for the hot loop (UDP is fire-and-forget,
|
|
no async needed). 50 ms default min interval → ~20 Hz cap.
|
|
- [x] Health check sends `getPilot` and waits for any reply.
|
|
- [x] Storage + API schemas + route handler wiring
|
|
- [x] 36 unit tests
|
|
- [ ] Frontend: WiZ in device-type picker + edit form
|
|
- [ ] Locale strings (en/ru/zh)
|
|
|
|
### Phase 2 — Unified discovery + pairing UX layer
|
|
|
|
After phase 1 the codebase will have 3 fresh examples of "ping the LAN, listen for replies, present a list". Factor that out into a generic discovery scaffold + a "first-run pairing" UX component before adding Tuya/Govee/etc., which each need a one-time pairing dance.
|
|
|
|
- [ ] Generic `NetworkDiscoveryService` that fan-outs mDNS + SSDP + UDP-broadcast probes in parallel
|
|
- [ ] Unified "scan network for devices" UI affordance instead of per-type buttons
|
|
- [ ] Reusable "pair device" component (consent button, countdown, retry)
|
|
|
|
### Phase 3 — Big aggregator unlocks
|
|
|
|
- [ ] ESPHome native API (`aioesphomeapi`)
|
|
- [ ] Tuya Local (`tinytuya`) — biggest single market unlock; needs the pairing UX from Phase 2
|
|
- [ ] Matter over IP (forward-looking)
|
|
- [ ] Hyperion JSON downstream
|
|
|
|
### Phase 4 — Major consumer brands
|
|
|
|
- [x] **LIFX LAN** — UDP binary protocol on port 56700; RGB→HSBK 16-bit
|
|
conversion; broadcast discovery via GetService/StateService probe;
|
|
47 unit tests. Single-pixel adapter shape, identical to WiZ
|
|
structurally. Frontend wired via subagent.
|
|
- [ ] Govee LAN API (2023+)
|
|
- [ ] Twinkly
|
|
- [ ] Nanoleaf OpenAPI
|
|
- [ ] Mi-Light / MiBoxer UDP gateway
|
|
|
|
### Phase 5 — Open pixel protocols (cheap completionism)
|
|
|
|
- [ ] OPC (Open Pixel Control)
|
|
- [ ] TPM2.net
|
|
|
|
### Phase 6 — PC gaming RGB completion
|
|
|
|
- [ ] Corsair iCUE SDK
|
|
- [ ] Logitech LIGHTSYNC
|
|
- [ ] ASUS Aura SDK
|
|
|
|
### Phase 7 — Proprietary USB HID ambient kits
|
|
|
|
- [ ] Generic HID-ambient framework + VID/PID registry
|
|
- [ ] First reverse-engineered target (probably Govee Immersion / DreamView)
|