Reversing a Bluetooth LE Light for Home Assistant Integration
hacking
A Bluetooth LE device controlled via an HA automation.1
I was recently gifted a beautiful LED sign with a significantly less beautiful mobile app to control it. There are no buttons on the device, so ultimately I was left no choice but to reverse engineer how this LED strip worked to get it operable through Home Assistant.
The device i’m targetting has a device name of ‘ELK-BLEDOM’ and uses the ‘duoCo’ iOS/Android app. Lots of people online have already reversed this device more extensively than I have (including an independent Android app and more feature-rich Home Assistant plugins for all sorts of brands). I’m sharing my strategy here if it’s useful for understanding more esoteric devices you may have, or simply for educational purposes.
Sniff BT Communication
This approach uses a $25 Adafruit Bluefruit LE Sniffer to sniff the Bluetooth Low Energy communication between the mobile app and the peripheral. With this tool we’re able to sit “in the middle” of the communication and watch the commands, later to be replayed or further understood.
If you don’t want to pick up additional hardware, you can collect a similar packet capture with an Android device, enabling Bluetooth HCI Snoop under Developer Settings. This to-the-point YouTube video is a good watch for that technique.
After installing the drivers and configuring Wireshark to interface with the BLE sniffer, disconnect from the peripheral so that it begins to send out advertisement packets. I’ve found the Wireshark integration is a bit fiddly - for example some of the filters applied from a right-click aren’t actually correct. You’ll want to prefix your filters with btle
or similar. There are also some potentially useful tools hidden under the ‘Wireless’ menu.
Open the provided toolbar View > Interface Toolbars > nRF Sniffer for Bluetooth LE
, and start the capture with ‘All Advertising Devices’ selected. Wait a moment for your device’s advertisements to get picked up, and then pick your target device from the list.
I knew the advertising address of my target device from the manufacturer’s app so it was easy to pick out.
From the broadcast packets, we can see the device’s name and other information.
The next step is to connect to the device with your phone. You’ll start to see different kinds of protocols, notably ATT
(Attribute Protocol) and GAP
(Generic Access Profile). The ATT
packets are the ones we care about, as they contain the actual data being sent to the device.
My strategy was to one-by-one invoke an action from the app. In my case, that resulted in a single ATT packet for each action. I would then inspect the packet, and add the ‘Value’ as a column (Right Click > ‘Apply as Column’).
I then commented directly on the packet with the action I observed.
There’s an observable pattern to the data being sent (that others have dove into more deeply). In this case, replaying the values works too.
The determine the correct UUID for the characteristic I used service_explorer.py
from the Bleak python package. I chose the one that indicated it was writable.
$ python service_explorer.py --address be:60:15:80:58:a6
2025-01-10 17:25:41,757 __main__ INFO: starting scan...
2025-01-10 17:25:41,990 __main__ INFO: connecting to device...
2025-01-10 17:25:44,187 __main__ INFO: connected
2025-01-10 17:25:44,187 __main__ INFO: [Service] 0000fff0-0000-1000-8000-00805f9b34fb (Handle: 4): Vendor specific
2025-01-10 17:25:45,006 __main__ INFO: [Characteristic] 0000fff3-0000-1000-8000-00805f9b34fb (Handle: 8): Vendor specific (read,write-without-response), Value: bytearray(b'ELKP10Y60V052_BRG\x00\x00\x00'), Max write w/o rsp size: 513
2025-01-10 17:25:45,006 __main__ INFO: [Characteristic] 0000fff4-0000-1000-8000-00805f9b34fb (Handle: 5): Vendor specific (notify)
2025-01-10 17:25:45,426 __main__ INFO: [Descriptor] 00002902-0000-1000-8000-00805f9b34fb (Handle: 7): Client Characteristic Configuration, Value: bytearray(b'')
2025-01-10 17:25:45,426 __main__ INFO: disconnecting...
You can also use Bleak to build a proof-of-concept Python script. These are some of the commands I extracted from the pcap.
import asyncio
from bleak import BleakClient
ADDR = "be:60:15:80:58:a6"
CHARACTERISTIC_UUID = "0000fff3-0000-1000-8000-00805f9b34fb"
# Command constants
CMD_TURN_OFF = bytes.fromhex('7e0404000000ff00ef')
CMD_TURN_ON = bytes.fromhex('7e0404f00001ff00ef')
# Static color commands
CMD_COLOR_RED = bytes.fromhex('7e070503ff000010ef')
CMD_COLOR_GREEN = bytes.fromhex('7e07050300ff0010ef')
CMD_COLOR_BLUE = bytes.fromhex('7e0705030000ff10ef')
CMD_COLOR_WHITE = bytes.fromhex('7e070503ffffff10ef')
# Strobe effect commands
CMD_STROBE_WHITE = bytes.fromhex('7e05039c03ffff00ef')
CMD_STROBE_BLUE = bytes.fromhex('7e05039803ffff00ef')
CMD_STROBE_GREEN = bytes.fromhex('7e05039703ffff00ef')
CMD_STROBE_RED = bytes.fromhex('7e05039603ffff00ef')
CMD_STROBE_COLOR = bytes.fromhex('7e05039503ffff00ef')
async def main():
async with BleakClient(ADDR) as client:
print("Connected: {0}".format(client.is_connected))
await client.write_gatt_char(CHARACTERISTIC_UUID, CMD_COLOR_RED)
asyncio.run(main())
Integrate with Home Assistant
I already control Bluetooth peripherals with Home Assistant, and was pleasantly surprised how easy it was to build my own custom integration. To get the basics working is surprising straight-forward. Note that these files should be copied into /config/custom_components/on-air-sign-btle
.
There are already several full-featured integrations available from the community if you don’t wish to build your own (1, 2).
After a reboot of Home Assistant, add through the UI from Settings > Devices & Services > Add Integration
.
Have a comment? Let me know
This post helpful? Buy me a coffee!