feat(devices): Yeelight LAN target type

Adds support for Xiaomi/Yeelight smart bulbs and lightstrips that speak
the bulb-vendor's JSON-RPC protocol over TCP port 55443 with SSDP-style
LAN discovery on 239.255.255.250:1982.

Backend:
- YeelightClient is a single-pixel adapter: it averages the incoming
  strip down to one RGB triple, packs it into the 24-bit color int the
  bulb expects, and pushes it via set_rgb with sudden+0ms effect.
- Brightness folds into the RGB scaling on the wire so we burn one
  command per frame instead of two.
- A configurable client-side rate gate (yeelight_min_interval_ms, default
  500) keeps us under the bulb's ~1 cmd/sec cap. Frames that arrive
  inside the gate no-op without TX. Music mode (~60 Hz via reverse-TCP)
  is deferred -- the MVP caps at ~2 Hz and that's fine for a strip-to-
  single-pixel averaging device.
- SSDP discovery scans 239.255.255.250:1982 with the bulb-specific
  ST: wifi_bulb header; replies are parsed into DiscoveredDevice
  entries. Multicast failures (no network, firewall) yield [] rather
  than raising -- discovery is best-effort.
- Health check opens a TCP socket to the bulb and closes it.
- YeelightConfig joins the typed config union; Device storage gains a
  yeelight_min_interval_ms field; full to_dict/from_dict/to_config wiring.
- 34 unit tests cover URL parsing, RGB packing, strip averaging, rate
  limiting, SSDP response parsing, provider validate/discover/health,
  and Device.to_config round-trip.

Frontend:
- 'yeelight' in DEVICE_TYPE_KEYS (next to 'hue'), lightbulb icon
  (intentional family-grouping signal with Hue).
- isYeelightDevice predicate + per-type field show/hide in create and
  settings modals.
- Rate-limit number input (default 500 ms) in both modals with hint
  text explaining the trade-off.
- Locale strings in en/ru/zh.
- Drive-by: types.ts DeviceType union backfilled with 'ddp' and 'ble'
  for type-safety consistency.

Yeelight bulbs are now reachable from the existing "Scan network"
button -- no new discovery UI affordance was needed.
This commit is contained in:
2026-05-16 01:44:13 +03:00
parent 8f1140abad
commit 4b65005823
19 changed files with 1032 additions and 22 deletions
+19 -17
View File
@@ -666,28 +666,30 @@ 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
### Phase 1.1 — Standalone DDP target ✅ shipped (commit `8f1140a`)
The DDP packet layer already exists (`ddp_client.py`) — currently only used inside `WLEDClient`. Promote it to a first-class device type so any DDP-speaking controller (Pixelblaze, ESPixelStick, xLights/Falcon endpoints, generic DDP receivers) can be driven directly without WLED firmware in the path.
- [ ] `DDPConfig` dataclass in `device_config.py` (port, destination_id, color_order)
- [ ] `DDPLEDClient` in `core/devices/ddp_led_client.py` — `LEDClient` wrapper around the existing `DDPClient` transport with `supports_fast_send=True` for the hot loop
- [ ] `DDPDeviceProvider` in `core/devices/ddp_provider.py` — discovery is a no-op (DDP has no native discovery; UI accepts manual IP), validate_device pings the host, capabilities = `{"manual_led_count", "health_check"}`
- [ ] Register provider in `led_client._register_builtin_providers`
- [ ] Add `ddp` branch to `Device.to_config()` in `device_store.py` + storage fields for DDP-specific options
- [ ] API schemas: extend device schema to accept DDP fields
- [ ] Unit tests for client (packet construction is already tested under `test_ddp_client.py`; new tests cover the LEDClient wrapper, provider validate/health, config round-trip)
- [ ] Frontend: add DDP to the device-type picker + edit form (spawned to a `frontend-design` subagent)
- [ ] Locale strings (en/ru/zh)
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. Use `python-yeelight` or direct protocol.
Xiaomi/Yeelight bulbs, port 55443 TCP JSON. Direct protocol (no
`python-yeelight` dependency — implementation is ~200 lines).
- [ ] `YeelightConfig` + `YeelightLEDClient` + `YeelightDeviceProvider`
- [ ] mDNS / SSDP discovery (Yeelight uses SSDP-like UDP multicast `239.255.255.250:1982`)
- [ ] Single-pixel output: map strip → averaged RGB → bulb color
- [ ] Frontend additions + locales
- [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