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.

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()

can you write down the steps to implement and use it? :slight_smile:

how is this a valid MAC address?

your updated works, just want to add to turn off you need to modify

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

thanks OP for the initial hard work, thank you for getting it to the finish line for me

I think this checksumming function should work:

def append_xor_checksum(data: list[int]) ->  list[int]:
    checksum = 0
    for byte in data:
        checksum ^= byte
    result = data.copy()
    result.append(checksum)
    return result

BTW it’s stupid for them to use checksums over BLE - there is already a 24-bit packet CRC :slight_smile:

Can someone add steps on how to install this? Thank you!

How do I get the MAC address and change to UUID?

Found this GitHub - Laserology/govee_ble_lights: The Ultimate Govee BLE Lighting Integration for HomeAssistant - Now with H617A and H617C support! · GitHub for H617A

Hi all, I picked up where this thread left off, on a slightly different model — Govee H617E, same RGBIC family — and with Claude code, managed to go beyond on/off. Sharing here in case it helps anyone with H617A/B/C/D/E/F.

Setup: instead of running bleak scripts on the HA host, I flashed a cheap ESP32 DevKit V1 with ESPHome and use it as a dedicated BLE bridge. The big advantage is a native light entity in HA with RGB picker and brightness slider, plus a persistent BLE connection (no per-command reconnect).

Three things I had to figure out that are not in this thread (or are wrong elsewhere):

  1. Checksum is XOR, not sum mod 256. AJ's original post says "sum of previous bytes modulo 256" but if you check the on/off packets it actually XORs: 0x33 ^ 0x01 ^ 0x01 = 0x33, 0x33 ^ 0x01 ^ 0x00 = 0x32. Beshelmek's govee_utils.py confirms it (sign_payload). Sum and XOR happen to match for on/off but diverge as soon as you add more non-zero bytes.

  2. Keep-alive packet is mandatory for persistent connections. The H617 controller drops idle BLE clients after ~10 seconds (HCI reason 0x13, "remote user terminated"). The Govee app sends a heartbeat every 2s; the packet is aa 01 00 00 ... 00 ab (0xab = 0xaa XOR 0x01). With an ESPHome interval: 2s that fires this write, the link stays up indefinitely.

  3. Color needs the SEGMENTS opcode with a specific trailer. This is where it gets interesting: Beshelmek's light.py excludes the H617 family from SEGMENTED_MODELS = ['H6053', 'H6072', 'H6102', 'H6199'] and so falls back to 33 05 02 R G B (manual color for plain RGB). On the H617E that command is silently ignored — power and brightness work but no color change. The fix is to use the segmented format anyway:

    33 05 15 01 R G B 00 00 00 00 00 FF 7F 00 00 00 00 00 [XOR]
    

    The 0xFF 0x7F at positions 12-13 is required — without them the controller ignores the packet. This matches the segmented payload Beshelmek sends to e.g. H6199 ([LedMode.SEGMENTS, 0x01, R, G, B, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F]). Looks like the H617 family was just never added to the segmented list, but the protocol is identical.

Prerequisite — finding your strip's MAC: install nRF Connect (Android) or LightBlue (iOS), scan, and look for an advertisement named Govee_H617X_XXXX. Tap it to see the MAC. Alternatively, if your HA already has the Bluetooth integration active, the strip should show up under Settings → Devices → Bluetooth as a discoverable device (you may need to put the strip in pairing mode — on the H617E that's power on + center button held 4 times on the control box; the strip starts cycling colors when it's ready).

ESPHome config (relevant parts):

esp32_ble_tracker:

ble_client:
  - mac_address: XX:XX:XX:XX:XX:XX
    id: govee_strip

# Keep-alive every 2s
interval:
  - interval: 2s
    then:
      - ble_client.ble_write:
          id: govee_strip
          service_uuid: "00010203-0405-0607-0809-0a0b0c0d1910"
          characteristic_uuid: "00010203-0405-0607-0809-0a0b0c0d2b11"
          value: [0xAA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                  0x00, 0x00, 0x00, 0xAB]

globals:
  - id: gov_r
    type: int
    initial_value: '255'
  - id: gov_g
    type: int
    initial_value: '255'
  - id: gov_b
    type: int
    initial_value: '255'

output:
  - platform: template
    id: out_r
    type: float
    write_action:
      - globals.set: { id: gov_r, value: !lambda 'return (int)(state * 255);' }
      - script.execute: send_color
  - platform: template
    id: out_g
    type: float
    write_action:
      - globals.set: { id: gov_g, value: !lambda 'return (int)(state * 255);' }
      - script.execute: send_color
  - platform: template
    id: out_b
    type: float
    write_action:
      - globals.set: { id: gov_b, value: !lambda 'return (int)(state * 255);' }
      - script.execute: send_color

light:
  - platform: rgb
    name: "Govee H617E"
    id: govee_light
    red: out_r
    green: out_g
    blue: out_b
    on_turn_on:
      - ble_client.ble_write:
          id: govee_strip
          service_uuid: "00010203-0405-0607-0809-0a0b0c0d1910"
          characteristic_uuid: "00010203-0405-0607-0809-0a0b0c0d2b11"
          value: [0x33, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
                  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                  0x00, 0x00, 0x00, 0x33]
    on_turn_off:
      - ble_client.ble_write:
          id: govee_strip
          service_uuid: "00010203-0405-0607-0809-0a0b0c0d1910"
          characteristic_uuid: "00010203-0405-0607-0809-0a0b0c0d2b11"
          value: [0x33, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                  0x00, 0x00, 0x00, 0x32]

script:
  - id: send_color
    mode: restart
    then:
      - delay: 50ms   # debounce: R, G, B update in sequence
      - ble_client.ble_write:
          id: govee_strip
          service_uuid: "00010203-0405-0607-0809-0a0b0c0d1910"
          characteristic_uuid: "00010203-0405-0607-0809-0a0b0c0d2b11"
          value: !lambda |-
            uint8_t r = (uint8_t)id(gov_r);
            uint8_t g = (uint8_t)id(gov_g);
            uint8_t b = (uint8_t)id(gov_b);
            std::vector<uint8_t> data = {
              0x33, 0x05, 0x15, 0x01, r, g, b,
              0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F,
              0x00, 0x00, 0x00, 0x00, 0x00
            };
            uint8_t cs = 0;
            for (auto v : data) cs ^= v;
            data.push_back(cs);
            return data;

Result in HA: a single light.govee_h617e entity with full RGB + brightness, exposable to Alexa/Google via Nabu Casa.

Open question — scenes: I also implemented the 0xa3 multi-packet protocol following Beshelmek's prepareMultiplePacketsData and extracted scene parameters from H617E.json (the 1.68 MB blob in his repo). Packets are emitted correctly (verified the structure and XOR), but on my H617E the strip does not react. Tried inter-packet delays from 30 to 200 ms, with and without keep-alive running in parallel, with the strip on and off. No visible effect. Two hypotheses:

  • The scene JSON in Beshelmek's repo may have been captured from a different controller revision and the H617E firmware silently drops the payload (would explain why he never put H617 in the segmented list either).
  • There may be a missing "apply" command at the end of the sequence.

If anyone has captured BLE traffic from the Govee app while activating a scene on an actual H617A/E, comparing those packets against what Beshelmek's JSON produces would settle it. Planning to do an HCI snoop dump when I get time.

Hope this helps the next person who finds this thread.