xydac/sp542e-ha-bridge

GitHub: xydac/sp542e-ha-bridge

Stars: 0 | Forks: 0

# SP542E MQTT ↔ BLE bridge Controls the cloud-locked **SP542E "BedRoof" LED controller** from Home Assistant with **no new hardware**. A small Python service on the always-on Mac connects to the controller over BLE and exposes it to HA as an MQTT light. HA (VirtualBox) ──MQTT──> Mosquitto (on HA) ──MQTT──> bridge.py (Mac) ──BLE──> SP542E HA needs **no Bluetooth** and **no custom component** — the entity `light.bed_roof` appears automatically via MQTT discovery. This works around the VirtualBox VM having no usable Bluetooth. ## Files | File | Purpose | |---|---| | `protocol.py` | BanlanX_6xx (`53 ..`) frame builders (power/CCT/brightness). Single source of truth. | | `probe.py` | One-shot: drives the strip directly to confirm the protocol. | | `bridge.py` | The service: MQTT light ↔ BLE. | | `run.sh` | Bootstraps `.venv` (via `uv`), loads `.env`, runs bridge or probe. | | `.env.example`| MQTT credentials template → copy to `.env`. | | `com.xydac.sp542e-bridge.plist` | launchd LaunchAgent for always-on auto-start. | ## ⚠️ macOS Bluetooth requirement BLE only works from the Mac's **local GUI session**, never over SSH (CoreBluetooth TCC restriction). Run everything from a Terminal sitting at the Mac. First run will prompt to grant Bluetooth to the Python binary (System Settings → Privacy & Security → Bluetooth). ## Setup cd sp542e-ha-bridge cp .env.example .env # then fill in MQTT_USER / MQTT_PASS # 1. Confirm the protocol actually drives the light (watch the strip): ./run.sh probe # 2. Run the bridge in the foreground to test HA integration: ./run.sh # -> light.bed_roof should appear in Home Assistant automatically. # 3. Make it always-on (auto-start at login, restart on crash): cp com.xydac.sp542e-bridge.plist ~/Library/LaunchAgents/ launchctl load -w ~/Library/LaunchAgents/com.xydac.sp542e-bridge.plist ## Protocol (BanlanX_6xx, plaintext, over FFE0/FFE1) This is a **CCT (tunable-white)** strip — HA exposes color-temp + brightness + on/off, no RGB. The controller speaks the **BanlanX_6xx** family (SP630E-style), *not* the LED-BLE `7E..EF` protocol and *not* idealLED AES (both were tried and silently ignored). It advertises manufacturer id `0x5053` with advert data `5d 10..` (model id `0x5d`). Frames are plaintext on write characteristic **FFE1** (service FFE0), shaped `53 00 01 00 `. **Writes must be acknowledged** (`response=True`) or the device ignores them — this was the key gotcha. | Command | Bytes | |---|---| | Power ON / OFF | `53 50 00 01 00 01 01` / `53 50 00 01 00 01 00` | | Static-white mode | `53 53 00 01 00 02 02 01` (set before CCT/white-brightness) | | Color temp (static) | `53 61 00 01 00 02 ` (``/`` 0–255; `0x60` = dynamic) | | Brightness | `53 51 00 01 00 02 ` (`` 0=color 1=white; `` 0–255) | | State query | `53 02 00 01 00 01 01` → device replies multi-packet status (fw, IP, name) | `color_temp` is in **mireds**: `MAX_MIREDS`=370 (~2700K warm), `MIN_MIREDS`=153 (~6500K cool); `protocol.py:cct()` maps mireds → cold/warm bytes. Protocol reference: [`monty68/uniled`](https://github.com/monty68/uniled) `custom_components/uniled/lib/ble/banlanx_6xx.py` (the SP542E isn't in UniLED's model list, but it's this family). ## Caveats - **Availability = Mac uptime.** If the Mac sleeps/powers off, the light is uncontrollable and HA shows it unavailable. (Always-on desktop = fine.) - **Optimistic state.** The controller has no reliable state read, so the bridge tracks state locally; changes made via the original app/remote won't reflect back in HA. - **One BLE connection.** Keep the original phone app disconnected.