I've been going all out on my smart home recently, and that includes finding new ways to integrate older hardware that I have lying around into Home Assistant. That hasn't been too much of an issue; many of my older Tuya devices work with Local Tuya, and I have a lot of self-hosted services that are useful for feeding info into Home Assistant, too. I hit a major snag when it came to my Govee H615B strip lights, though, which led me down a rabbit hole of reverse engineering to integrate them like I had everything else.
Govee has both a web-based API and a local API, and if the web-based API was good enough, I'd have probably thrown in the towel and used that. However, it rate limits very quickly. Want to adjust your brightness to get it just right? Good luck. You'll be locked out for a minute after just a few state changes. As for the local API, while it technically exists, it simply can't be enabled on my Govee lights. I don't know why. I tried to get it working, but the option is completely grayed out in the Govee app's settings.
After doing some reading on reverse engineering projects relating to other Govee lighting equipment, I figured it was worth a shot to try to reverse engineer mine. Thus began my journey, armed with Wireshark and Python, so that I could figure out how these lights worked and if I could control them from any Bluetooth device rather than just the official app.
All of the code used in this article is open-source, and you can find it at the bottom of the article!
Assessing the problem
Take a step back and evaluate your tools and what your goals are

The first step when it comes to reverse-engineering is assessing the problem, what tools are available to you, and what your end goal is. I was left with the following tools at my disposal:
- A MacBook M4 Pro
- A Google Pixel 8 Pro
- The Govee app (which can control the lights when there's no network connection via Bluetooth LE)
- Wireshark
- Bleak, a Bluetooth framework in Python
- A Milk-V Duo S (a microcontroller with both Arm and RISC-V cores capable of running Python, and with Wi-Fi/Bluetooth support built-in)
I wanted to avoid rooting my Google Pixel 8 Pro if possible, and I figured it was possible to avoid that, so I considered it off the table unless I ran out of options. It all seemed very doable, and if there was no authentication to control these lights, I could feasibly broadcast my own commands to the lights to control them.
The end goal was to write a Python script that could run on my Milk-V Duo S, with a server accepting commands from Home Assistant for later broadcasting to the lights.
Collecting the data
If you have a Google Pixel, this is easy

In order to figure out how to control these lights remotely, I figured my best bet was to simply log the Bluetooth packets that were sent back and forth. That's why I used the Google Pixel 8 Pro. There's hardware you can use to sniff Bluetooth packets in transit, but I don't have that hardware, and Android has a built-in Bluetooth HCI logger that should work just fine. HCI stands for Host Controller Interface, and this log is a very low-level capture of everything your phone sends out and everything your phone receives. You can enable this in developer options.
The next question is why I used the Google Pixel 8 Pro and not my Oppo Find N5 or any of the other devices at my disposal. Where is the log stored? The typical way to access it these days is via adb; you can initiate a bug report that's saved to your computer, and the log should be saved in the Bluetooth logs folder, but that wasn't the case on my Oppo Find N5. I had a file, but it was empty. Given that many companies make modifications to systems like these, I later elected to go for the Pixel 8 Pro to avoid any OEM shenanigans. Switching over to the Google Pixel 8 Pro enabled me to capture a Bluetooth HCI log that actually contained data.
Before pulling the log, though, it needs to be filled with usable data first. I installed the Govee app and logged into my account, and then switched off Wi-Fi. This meant the app had to use Bluetooth to control the lights, so the data would be saved in the log. After modifying the brightness and the colors and switching the lights on and off a bunch of times, it was time to plug my Pixel 8 Pro into my laptop and pull the bug report using adb.
Figuring out the basics first with Wireshark
Don't go all out; focus on the small things first

The Bluetooth HCI log will be located in FS/data/misc/bluetooth/logs and will have the .cfa file extension once you unzip the bug report file. The .cfa file is a BTSnoop file containing L2CAP packets. These packets detail all of the communication that your device makes over Bluetooth and is often used by Bluetooth Low Energy devices for communication. In this particular case, it was pretty easy to discover what device I needed to look at, but I also used the Python library Bleak to scan for devices and their IDs.
I wrote a simple scanner in Bleak, and when I identified the UUID of the device I needed, I was able to then query it for characteristics. Characteristics are essentially profiles. A client can initiate commands targeting a characteristic and can receive responses, and a server can accept those commands and act on them. As a side note, macOS and its CoreBluetooth framework will provide you a UUID for Bluetooth Low Energy devices rather than a MAC address for communication. This is fine, but it is something to keep in mind if you're writing code to port to another device later. The MAC address that you need to use (instead of a UUID) will be in the log.
In my scan, it's pretty easy to spot the Govee H615B. I was able to identify a number of characteristics, which gave me the information I needed to investigate further in my log that I copied from my phone. These are:
- Handle: 0x0009, UUID: 00010203-0405-0607-0809-0a0b0c0d2b10
- Handle: 0x000d, UUID: 00010203-0405-0607-0809-0a0b0c0d2b11
- Handle: 0x0012, UUID: f000ffc1-0451-4000-b000-000000000000
- Handle: 0x0016, UUID: f000ffc2-0451-4000-b000-000000000000
Note that the two characteristics ending in "b10" and "b11" are likely related, as are the two that have "c1" and "c2". We'll focus on the two ending in b10 and b11, as from my research of other Govee devices, it appears that these two are the characteristics that are related to setting states for the lights, and other devices match exactly these strings.
Here's another piece of context when it comes to these devices: while they're all similar in how they interact, it appears that they are all slightly different in how they accept commands. Some have segments for different sections of the light strip (so they can be controlled individually), and some have authentication in the pairing process. I was particularly worried about this authentication step, but I discovered that when my Pixel 8 Pro connected (and it was the first time I ever connected it), there were no checks on what device was sending commands. This is somewhat a security flaw (though a pretty non-damaging one on the surface), but we'll be able to use it to our advantage.

One thing I did find in that initial connection process between my phone and the lights became a crucial piece of the puzzle to making all of this work. Remember the characteristic I mentioned ending in "b10"? The client device (the Pixel 8 Pro in this case) is sent a notification by the Bluetooth controller belonging to the lights, and it comes from the characteristic ending in b10. We now know that we need to be listening to this service, so we'll keep that in mind when writing our code to connect to it later on.
Next, I noticed many packets sent from my Pixel to the lights that contained the following value:
- aa010000000000000000000000000000000000ab
These seem to be keep-alive packets, which inform the Govee lights that we're still looking to send and receive information. These were sent approximately every two seconds and made up the bulk of the logs. I noticed that when I connected to the lights normally using Bleak, they would disconnect from my laptop in just a few seconds. Given that my phone seemed to have no other contact for extended periods of time aside from sending values similar to those, I figured they must be keep-alive packets. This also matches what other Govee devices appear to do.
While I'm not sure what the data is after the "aa", the last byte of the string (so, the last two characters) were very important. Bluetooth LE packets are 20 bytes in length (without an extended MTU), and it seems that these packets use zero padding to meet that packet length. We can conclusively say at this point that packets starting with 0xaa denote a keep-alive packet, but what about the last two digits? We'll get to those later.

Next, we'll look through the commands that I sent. When I first opened the app and connected to the lights, I turned them on and off. In the logs, the first piece of data that was transferred and doesn't look like a keep-alive packet or related to the initial connection is the one above. I turned them off again, and I found a very similar value to the one above but slightly different, and this matched the times that I took in my notes. I could then deduce that the two values to turn on and turn off the lights were as follows:
- Power on: 3301010000000000000000000000000000000033
- Power off: 3301000000000000000000000000000000000032
Notice the structure of the packet; again, we have a lot of zero bytes and two different bytes at the end. As a primer, we say "0x" to denote a hexadecimal value, which uses base 16. "0x" makes it clear we're talking about hexadecimal, rather than a regular decimal, and 0x33 is "51" in decimal. 0x33 appears to denote a command, with the following data issuing the instruction to execute. In this case, we have 0x33, 0x01, and 0x01 to turn on the lights and 0x33, 0x01, and 0x00 to turn them off, suggesting that a boolean in the third byte controls the on-off state. We already have enough to test by setting up our notification receiver and essentially repeating these same instructions back at the lights. We don't need to worry about the last two digits as they've been calculated for us, but we'll get to calculating our own ones, too.
Setting light colors and brightness
We want to do more than just turn them on and off

Right now, we can already turn on and off the lights via Bluetooth. That's a pretty big step, but colorful smart lights like these have more than just an on and off switch. I investigated color and brightness, as I had modified both of those in the app as well to see what that would look like on a packet level. I found the following packet:
- 33050dfe0e1f00000000000000000000000d4
There's that 0x33 starting byte again, so we know we're getting a command. I wasn't sure about 050d, but "fe0e1f" looked like a color hex code. When I converted it from hex to an actual color, it was very prominently red, and I had changed my lights to red in my testing. I wanted to test if I could replace "fe0e1" with my own color, but there's one problem. Previously, we could just replay packets back at the lights, and they would redo commands that we had seen in our logs. How do we craft new commands? We can't just swap those color hex values for our own. The reason we can't is the inclusion of that last byte.
That last byte is a checksum, which essentially confirms that the data has arrived in complete condition and isn't malformed. It's calculated by performing a cumulative XOR operation on each byte. An XOR is a type of logic gate that outputs a '1' where two input values differ. Each byte is then XORed with the previous until it reaches the 19th byte. The final calculation is appended to the end of the packet into the 20th byte, and this is sent to the device. Finally, the device performs its own XOR operation on the first 19 bytes, checking if the last byte matches what it calculated. If it does, it knows the data arrived as intended and is safe to execute.
Let's try to change the lights to magenta, hex code #FF00FF. This will look like the following:
- 33050d[ff00ff]00000000000000000000000d4[checksum]
ff00ff (placed in square brackets up above for ease of understanding) is our color code, and our [checksum] is what we want to calculate. We start with the value of an empty byte, or 00000000.
- Start with 0x00 (binary: 00000000).
- XOR with 0x33 (00110011), result: 0x33 (00110011).
- XOR with 0x05 (00000101), result: 0x36 (00110110).
- Continue XORing each byte in the message with the previous.
- Final result: 0xff (binary: 11111111).
We work our way through the string until we get to the very end. This results in a value of ff, with a binary value of 11111111. The final value that we would send our lights is:
- 33050dff00ff00000000000000000000000d4ff
But that's not exactly convenient to calculate every single time we want to change the color of the lights. Instead, we can automate this process, which I did in Python. I implemented a method that takes the RGB hex values, inserts them in the string, and then calculates a checksum to append to the rest of the string. I won't bore you with the details, as it just implements the calculation we did above in a programmatic way to get a new checksum every time before we send a color change command.
Finally, let's look at brightness. Using the same process, I discovered that setting the brightness appeared to be the following command:
- 3304[brightness]00000000000000000000000000000000[checksum]
Brightness is a single byte, ranging from 00 (0) to FF (255). The checksum again has to be calculated, but this is easy to do now that we've figured out how it's calculated. To set the brightness to 100%, for example, is just:
- 3304ff00000000000000000000000000000000c8
We have now fully figured out how to control our lights! We can:
- Turn the H615B on and off
- Set the color
- Set the brightness
And we can do all of this without needing to use the official app! It bypasses a cloud-based API, gets around the issue of not being able to use a local API, and means that we can automate their control from another device by integrating them into our smart home.
Reverse engineering is a long, fun process
You'll face setbacks, but don't let them hold you back

Reverse engineering can be difficult, and you can hit multiple brick walls as you go. There are countless resources out there to try and help you, but chances are, if you're reverse engineering something, then you're doing it because nobody else already has. I was able to take all of the data I had collected through this and build a webpage to control these lights in my browser, but I got lucky in that it was a relatively simple process to identify what I needed to change and how.
The goal of this article is to walk you through the steps needed when reverse engineering something like this. There are so many cheap smart devices on the market similar to the Govee H615B that require a proprietary app to control them. It's not impossible to figure out how they work, though, and with hours or even days of slaving away at your computer, you can sometimes break through and figure out a way to control them yourself. That's exactly what I did. What began as something I hoped would be a fun little weekend project ended up turning into a gruelling multi-day affair that I knew I wanted to get to the bottom of.
From this point, it's trivial to implement a control mechanism from Home Assistant so that you can use these lights like you can any of your others. For example, you can implement a REST API in Flask and then use the rest_command integration in Home Assistant to send commands. Finally, create a script that will turn it on or off, and you can build a template switch, an input slider, or a custom Lovelace control for power, brightness, and color. This is what I did when deploying it on my Milk-V Duo S, and it works perfectly. While it wasn't explored in this article, you can also assess the current state of the Govee lights (powered on or off) by printing the data broadcast by it when scanning.
If you have these lights, you can check out my GitHub repository to control them yourself from the comfort of any of your Bluetooth-enabled devices. All you need is the MAC address (which you can get using an app like nRF Connect on your phone), and the rest should just work. It was a hugely educational process, and I hope that this inspires you to take a closer look at the devices around you to figure out what makes them tick and how you can take control of them yourself!