Bridging a cloud-locked LED controller into Home Assistant, without new hardware A developer integrated a cloud-locked BanlanX SP542E LED controller into Home Assistant without new hardware by running a 150-line Python bridge on a Mac that translates Bluetooth commands to MQTT. The solution bypasses the controller's proprietary phone app and gives Home Assistant full control over on/off, brightness, and color temperature. This approach avoids buying additional hardware by leveraging the Mac's existing Bluetooth radio and Home Assistant's MQTT auto-discovery. Bridging a cloud-locked LED controller into Home Assistant, without new hardware tldrPart 3 of my Home Assistant journey: a stubborn BanlanX LED controller that only spoke a phone-app dialect, a Mac that already had the Bluetooth radio I needed, and 150 lines of Python that taught Home Assistant to talk to it over MQTT. This is part 3 of my Home Assistant journey. In part 1 /blog/cyd-smart-display/ I turned a $21 ESP32 touchscreen into a wall panel. In part 2 /blog/claude-code-home-assistant/ I started running the whole smart home from a terminal conversation with Claude. This one is about a stubborn little LED controller that refused to be integrated, and the slightly ridiculous lengths I went to so it would join the family anyway. The short version: I have a cheap CCT tunable-white LED controller called an SP542E . It does Bluetooth and Wi-Fi, but only through its own phone app. No Home Assistant integration, no local API, nothing. I got it working in Home Assistant full on/off, brightness, and color temperature without buying a single new part. The whole thing runs as a tiny Python service on my Mac. Here’s the story, dead ends included. Those were the interesting part. The problem The SP542E is one of those generic LED controllers you buy with a strip kit this is the exact one I have https://www.amazon.com/dp/B0CYNVQPXD , about $13, and it even came with a little handheld remote . It’s controlled by an app sold under names like “iDeal LED”, “LED Chord”, and “BanlanX” . So it shipped with two ways to control it, the app and the remote, and neither was the one I wanted: there’s no official way to get it into Home Assistant. Normally the answer is “flash WLED” or “buy a controller that speaks a known protocol.” But I wanted to see if I could make this one work, as-is. The constraint that shaped everything My Home Assistant runs in a VirtualBox VM on a Mac. That detail matters more than anything else in this post. A VM doesn’t get Bluetooth. VirtualBox USB passthrough on macOS is famously flaky, and even if I fought it into working, the Bluetooth range would be wherever my Mac happens to sit. So Home Assistant simply cannot reach a Bluetooth device directly. There are a few standard ways to give a Bluetooth-less server a radio I’ll get to that shopping list in a minute , but I had a cheaper realization first: Home Assistant has no Bluetooth. But the Mac it runs on has perfectly good Bluetooth. So instead of adding hardware, I could run a small bridge on the Mac : it talks to the LED controller over Bluetooth on one side, and talks to Home Assistant over the network on the other. The Mac becomes the radio. The architecture The cleanest way to connect the two sides turned out to use stuff I already had. Home Assistant already runs a Mosquitto MQTT broker my wall display uses it . MQTT has an auto-discovery feature: publish a specially-formatted message and an entity just appears in Home Assistant, no config editing required. So the design is: The Python bridge: - Connects to the LED controller over Bluetooth - Connects to the MQTT broker and announces a light entity via discovery - Translates Home Assistant commands on/off, brightness, color temp into Bluetooth commands - Reconnects automatically when anything drops Home Assistant just sees a normal light called light.bed roof . It has no idea there’s a Mac and a Bluetooth stack involved. ”Why not just buy a Bluetooth radio?” Fair question. There are good hardware options; I weighed three before going software. An ESP32 Bluetooth proxy ~$5 is the canonical answer. You flash a cheap board with ESPHome’s bluetooth proxy firmware, plug it in anywhere, and it shows up in Home Assistant as a remote radio over Wi-Fi. With a houseful of Bluetooth sensors, this is what I’d reach for. But a proxy only gives HA a radio , not understanding : it shuttles bytes without knowing what they mean, and HA can only act on a proxied device it already has an integration for. HA didn’t understand my SP542E, so a proxy alone would have changed nothing about the hard part. A USB Bluetooth dongle ~$5 gives the HA machine a local radio. Cheap, except mine runs in a VirtualBox VM, and USB passthrough on macOS is fragile and breaks on host sleep. I also burned an evening on a spare mini-PC whose built-in radio wouldn’t enumerate at all. Rabbit hole. The software bridge on the Mac $0 won by default: the Mac was already on 24/7, already had a working radio, already ran the Home Assistant VM and its MQTT broker, and was already in range. The tradeoff is that the light only works while the Mac is awake. Fine for an always-on desktop, less so for a laptop that travels. Whichever radio you pick, you still have to teach Home Assistant the device’s command language. The radio only carries the bytes; it doesn’t speak them. A macOS gotcha: Bluetooth only works “in person” First snag. I was driving all of this from a terminal over SSH, and every Bluetooth scan came back “not authorized.” It turns out macOS only grants Bluetooth access to processes running in the logged-in GUI session. An SSH session can’t touch CoreBluetooth no matter what you toggle in System Settings. The fix was simply to run the scripts from a Terminal window sitting at the actual Mac, where macOS pops the “allow Bluetooth?” prompt and remembers it. Mildly annoying, completely logical in hindsight. Finding the device A quick Bluetooth scan turned up the controller advertising itself as “BedRoof” a name I’d set in the app long ago , with a strong signal. Connecting and dumping its services showed a single, classic layout: one service, one characteristic FFE1 that supports both writing and notifications. That’s the textbook “serial over Bluetooth” setup these cheap modules use. Now I just had to figure out what bytes to send it. A two-minute primer on BLE serial expand if "one service, one characteristic" doesn't already mean something to you Every Bluetooth Low Energy device exposes a little menu of its capabilities called a GATT table : the device has services logical groupings , and each service contains characteristics the actual data endpoints . A characteristic is the thing you interact with: you can read it, write to it, or subscribe to it so the device pushes updates notifications . A fitness band might have a “heart rate” service with a “measurement” characteristic; a thermometer might have a “temperature” characteristic. The cheap LED controller had none of that nice structure. One custom service, one characteristic FFE1 supporting both writing and notifications. That pattern is the signature of a BLE serial module: the HM-10 chip and its many clones. BLE serial is a clever hack. Bluetooth LE wasn’t designed to be a serial cable; manufacturers wanted a cheap “just send my microcontroller some bytes over Bluetooth” pipe, so these modules repurpose a single characteristic into a transparent UART . Whatever bytes you write to FFE1 get handed straight to the microcontroller, as if down a serial wire. Whatever it wants to say back comes as notifications on the same characteristic. The crucial consequence: Bluetooth imposes no meaning on those bytes. The transport just shuttles them. The command language what 53 50 00 01 00 01 01 means, what order to send things in, whether there’s a checksum is entirely invented by the device’s firmware. So discovering FFE1 told me “this is a byte pipe to a microcontroller,” and nothing about the language that microcontroller speaks. That language was the actual puzzle. One more practical detail: BLE writes come in two flavors, write-with-response acknowledged and write-without-response fire-and-forget . Some devices accept both; some only honor one. Guess wrong and your perfectly-formed commands vanish into the void. Which, foreshadowing, is exactly what happened to me. The protocol detective story The characteristic was there. The connection worked. But what language did it speak? Wrong guess 1: the AES protocol. There’s a well-documented reverse-engineering project for “iDeal LED” devices that uses AES encryption on a different service. I got excited, until I noticed my device didn’t have that service at all. Different beast. Wrong guess 2: the plaintext “LED-BLE” protocol. Lots of these FFE1 controllers use a simple 7E ... EF framed command format. Power on is 7e ff 04 01 ff ff ff ff ef , and so on. I built a whole probe around it, fired the commands at the strip… and nothing happened. The Bluetooth writes were accepted, but the light just sat there, indifferent. That “accepted but ignored” behavior is a useful clue: you’re talking to the right characteristic but speaking the wrong dialect. The breakthrough: read the nameplate. The device’s Bluetooth advertisement included a manufacturer ID of 0x5053 . In decimal that’s 20563 , a number registered to BanlanX , the company behind the app. So this wasn’t a generic controller at all. It was a BanlanX device using BanlanX’s own protocol. There’s a community Home Assistant integration called UniLED that supports a long list of BanlanX controllers. I dug into its source and found three different BanlanX protocol families, each with completely different command framing. Wrong guess 3: BanlanX “v2”. The first family uses commands prefixed with 0xA0 . Power off is A0 62 01 00 . I tried it. Still nothing, though I did learn one important thing from UniLED’s code: these devices require acknowledged Bluetooth writes write-with-response . I’d been firing off unacknowledged writes the whole time, which was a second reason for the silence. Finally, the match. UniLED identifies devices by the data in their Bluetooth advertisement. The newest family “6xx” expects manufacturer data starting with model id, 0x10 . My device’s data started with 5d 10 . That 10 was the tell. My controller was a BanlanX 6xx-family device, model id 0x5d . That family wraps commands like this: 53