Govee H617A - BLE LED Strip Lights - Reverse Engineering

TL;DR
Controlling the Govee H617A LED strip locally over Bluetooth is possible.

Background:
I own a set of Govee H617A LED strips, and unfortunately, I didn’t realize when purchasing that they were Bluetooth-only, not Wi-Fi enabled. This meant they could only be controlled via the Govee app using a BLE connection, and therefore, only while my phone was in proximity.

I explored various Govee BLE integrations for Home Assistant, but none of them worked with this model. Meanwhile, I already had other lights that were smart-enabled and fully automated. The idea of needing to manually open an app or flick a switch every time just didn’t sit right with me.

So I decided to reverse engineer the Govee H617A aiming for seamless control using Home Assistant, and ideally without relying on any flaky cloud-based hacks.

The (Frustrating) Journey

Initial Assumptions:

I started by assuming Govee had a standard BLE implementation. I dug through community integrations, GitHub issues, and even unpacked code from existing Home Assistant custom components.

Nothing worked.

Each attempt either failed to connect, or wouldn’t trigger the light. So I did what any obsessed tinkerer would do: I went full reverse-engineer mode.

Enter the BLE Sniffer

To get actual insight into what the Govee app was sending, I bought a Nordic nRF52840 dongle. It works with Nordic’s nRF Sniffer firmware and Wireshark, allowing full inspection of BLE packets.

If you want to replicate this setup, here’s what you’ll need:

  • nRF52840 dongle flashed with the Sniffer firmware
  • Wireshark with the Nordic BLE plugin (shows ATT, GATT, L2CAP layers)
  • The Govee app on your phone
  • A little patience and a lot of coffee

The Capture Process

  1. Kill the Govee App so the light disconnects from your phone/app.
  2. Start Wireshark, with the dongle set to scan on the right BLE channel.
  3. Open the Govee app and start toggling your light ON and OFF.
  4. Watch for traffic: you’ll see packets with Write Without Response (0x52), these are your key targets.
  5. Export the capture to .pcapng or JSON for inspection. Or just use Wireshark to trawl through the capture, but I found it to be cumbersome.

Realization: It’s Not 0x12
Most documentation and other BLE devices use Write Request (0x12) with response packets. Govee instead uses 0x52 ,no response, just fire and forget. These packets are raw binary, but fairly consistent…

  • The payload is 19 bytes.
  • The last byte is always a checksum: sum of previous bytes modulo 256.

That’s what I uncovered:

  • Turn On: 33 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 33
  • Turn Off: 33 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 32

The device expects these exact sequences. Anything else is ignored (at least i think they are).

Applying this to Home Assistant
I created 2x python scripts govee_turn_on.py and govee_turn_off.py and put them in /config/python/ on my HA instance

govee_turn_on.py

import asyncio
from bleak import BleakClient

MAC_ADDRESS = "YOUR_MAC_HERE"
CHAR_WRITE_UUID = "00010203-0405-0607-0809-0a0b0c0d2b11"

def build_packet(data):
    checksum = sum(data) & 0xFF
    return bytes(data + [checksum])

async def turn_on():
    async with BleakClient(MAC_ADDRESS) as client:
        pkt = build_packet([0x33, 0x01, 0x01] + [0x00]*5)
        await client.write_gatt_char(CHAR_WRITE_UUID, pkt)

asyncio.run(turn_on())

and
govee_turn_off.py

import asyncio
from bleak import BleakClient

MAC_ADDRESS = "YOUR_MAC_HERE"
CHAR_WRITE_UUID = "00010203-0405-0607-0809-0a0b0c0d2b11"

def build_packet(data):
    checksum = sum(data) & 0xFF
    return bytes(data + [checksum])

async def turn_off():
    async with BleakClient(MAC_ADDRESS) as client:
        pkt = build_packet([0x33, 0x01, 0x00] + [0x00]*5)
        await client.write_gatt_char(CHAR_WRITE_UUID, pkt)

asyncio.run(turn_off())

Then added a reference to them in the configuration yaml

shell_command:
  govee_on: python3 /config/python/govee_turn_on.py
  govee_off: python3 /config/python/govee_turn_off.py

Now I can use them in automations via the shell commands, and I’m (kinda) happy. Like I said, this isn’t an ideal solution. There isn’t an entity for the device. But if you want to do anything other than turning them on and off (set colors, scenes etc.) this process is repeatable to sniff what the app is sending, and go from there.

6 Likes

Thanks for sharing! I did pick up a couple of H617A during prime days and just trying to get the on / off functionality working but no luck using your code. I was able to find the MAC address using BLE Scanner but seems like the pkt length you are using is missing a few bytes from the 19 bytes you mention. I did try increasing the pkt calc to *16…

Was able to get it to work by changing this line in both scripts:

pkt = build_packet([0x33, 0x01, 0x01] + [0x00]*16)

and then hardcoded the checksum for each:

turn_on:
checksum = 0x33 & 0xFF

turn_off:
checksum = 0x32 & 0xFF

Thank you for doing the heavy lifting on this. At some point, I may dig deeper into exploring the different effects.

You guys are awesome.

sorry, I’m a bit slow here, but why did you hard code the checksum?

I hardcoded the checksum because the calculation in the provided code was not working. For me, the specific checksum values that worked were static so I just chose a hardcoded value rather than try to calculate a value. If I get a chance to test for other features, I may try to see if it can be calculated to give more flexibilty.

Hope that helps.

It does. Thanks for replying.

May I also ask how you got this to work? I tried installing pyscripts, I tried regular python on HASS, but nothing is working. Was there another configuration I’d left out? I wanted to try with the HACS Govee integration, but there were a lot of configurations we couldn’t take advantage of through automation, so I want to see if I can help build up this library you guys had started.

In my config.yaml, I added these lines:

python_script:

shell_command:
  govee_on: python3 /config/python_scripts/govee_turn_on.py
  govee_off: python3 /config/python_scripts/govee_turn_off.py

Then make sure you create a folder called python_scripts in the same folder that contains my config.yaml.

Then I copied the scripts for turning on and off the lights into that folder. My scripts were called:
goove_turn_on.py
goove_turn_off.py

Restarted HA.

Then used in the “action” tab under developer tools to run the shell commands: goveen_on and goove_off.

I don’t recall being successful in trying to use Python.script actions…

Good Luck

File "/usr/local/lib/python3.13/site-packages/bleak/backends/bluezdbus/manager.py", line 353, in get_default_adapter
 raise BleakError("No Bluetooth adapters found.")
bleak.exc.BleakError: No Bluetooth adapters found.
returncode: 1

I did something wrong, because it’s doing this…is it because I attached my Govee lights to the HACS integration for Govee?

Here is a perfect working python script. Tested this on my H617A and it works perfectly. thanks @rnodern @cbloy

import asyncio
from bleak import BleakClient

MAC_ADDRESS = "6BA95308-E31F-D3B7-66B6-2324AECF6E79"
CHAR_WRITE_UUID = "00010203-0405-0607-0809-0a0b0c0d2b11"

def build_packet(on: bool):
    data = [0x33, 0x01, 0x01 if on else 0x00] + [0x00] * 16
    checksum = 0x33 if on else 0x32
    data[-1] = checksum & 0xFF
    return bytes(data)

async def handle_notification(sender, data):
    print(f"[Notification] From {sender}: {data.hex()}")

async def send_command(on: bool):
    async with BleakClient(MAC_ADDRESS) as client:
        await client.connect()
        if not client.is_connected:
            print("Failed to connect.")
            return
        print(f"Connected to {MAC_ADDRESS}")

        # Try enabling notifications (if supported)
        try:
            await client.start_notify(CHAR_WRITE_UUID, handle_notification)
            print("Notifications enabled.")
        except Exception as e:
            print(f"Could not enable notifications: {e}")

        pkt = build_packet(on)
        print(f"Sending packet: {pkt.hex()}")
        await client.write_gatt_char(CHAR_WRITE_UUID, pkt)
        print("Command sent:", "ON" if on else "OFF")

        # Wait a bit to receive any responses
        await asyncio.sleep(2)

        try:
            await client.stop_notify(CHAR_WRITE_UUID)
        except Exception:
            pass

        print("Disconnected.")

def main():
    choice = input("Enter 'on' or 'off': ").strip().lower()
    if choice not in ["on", "off"]:
        print("Invalid choice.")
        return
    asyncio.run(send_command(choice == "on"))

if __name__ == "__main__":
    main()
1 Like