feat(mqtt): multi-broker MQTT + Zigbee2MQTT light target
- New Z2MLightOutputTarget storage, processor, editor and routes for Zigbee2MQTT light entities (shares the HA-Light editor UI via the new light-target-editor module) - Replace global MQTTService/MQTTConfig with per-source MQTTManager + MQTTRuntime; thread mqtt_source_id through Z2M targets, DIY MQTT devices, and the automation engine - Migrate legacy single-broker YAML/env config to a "Default Broker" MQTTSource on startup (core/mqtt/legacy_migration.py) and drop the obsolete core/mqtt/mqtt_service.py - Refresh /api/v1/system integration status to surface every MQTT source - Extract shared light-target editor and refactor OutputTargetStore + output_targets routes around typed factories / auto-registry - Modal CSS polish, locale strings, and storage/bindable test coverage
This commit is contained in:
@@ -1,5 +1,134 @@
|
||||
# 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)
|
||||
@@ -131,7 +260,7 @@ Branch: `feat/device-event-notifications`. Default ON.
|
||||
permission row + Test-notification button.
|
||||
- [x] i18n: `settings.notifications.*` and `notifications.*` keys in en/ru/zh.
|
||||
|
||||
### Verification
|
||||
### 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.
|
||||
|
||||
Reference in New Issue
Block a user