Local BLE control of Govee lights at ~20 ms warm latency (held connections), plus notes on running a Bluetooth proxy on a Linux box

Govee lights in Home Assistant usually mean either the cloud path (0.3-0.7 s
per command) or BLE integrations that connect per command (multi-second, and
flaky because Govee firmware drops idle clients after about 10 seconds).
I spent some time on the BLE path and got warm-command latency down to
17-23 ms. Numbers and code below in case they’re useful to anyone.

Measured latencies (H605C at -50 RSSI, H601C via proxy):

Path Latency
Govee Platform API (cloud) ~700 ms
AWS IoT push (govee2mqtt) 300-500 ms
BLE, cold connect ~600 ms
BLE, held connection 17-23 ms

How: hold the GATT connection instead of reconnecting per command. The
firmware kills idle links after ~10 s, so a keep-alive frame every 4 s keeps
it open; the link is released after 10 min without commands. That’s the core
of a fork of the (inactive since 2025) govee_ble_lights integration:

Verified on H601C/H601D (Glide downlights) and H605C; basic power/brightness/
color uses the generic 0x33 frame protocol so most BLE-capable Govee lights
should work. Installable as a HACS custom repository. Maintained as-is for
the hardware I own; issues with model number and debug log are welcome.

Firmware quirks found along the way (apply to any implementation, not
just this fork):

  • On 60:74:F4-prefixed devices the BLE MAC is the WiFi MAC + 1. Older
    devices embed the BLE MAC directly in the govee2mqtt device ID.
  • H601x takes brightness as 0-100; other models take raw 0-255. Sending
    0-255 to an H601x overshoots to full at 40%.
  • Some firmware exposes the control characteristic as write-without-response
    only. A write-with-response gets ATT WRITE_NOT_PERMITTED on a perfectly
    healthy link. If your stack treats that as a dead connection you get
    reconnect churn; retry the same payload without response instead.
  • H601x has no status readback I could find (the 0xAA 0x01 poll that works
    on strips gets no reply), so state is optimistic. If you also run
    govee2mqtt, its AWS IoT entities mirror BLE-initiated changes within
    ~1-2 s and can serve as readback.
  • The lights must be powered to be reachable: a wall switch that’s off
    means no BLE, no matter what software you run.

Radio side: my fixtures are out of range of the HA host, so the
connection runs through a Bluetooth proxy on a Linux server that speaks the
ESPHome native API. HA treats it exactly like an ESP32 proxy, but it’s the
server’s BlueZ radio. I hardened a small existing project for this (the
original repo is now archived): write-without-response fallback, forced
active scanning, and a shutdown path that actually exits under systemd:

Caveat for held connections on any proxy: each held link occupies a
connection slot and connects pause scanning on a single radio, so size
max-connections to your fixture count and expect contention if one radio
serves many rooms.

This looks excellent! I would test it, but I got rid of my BLE Govee devices in favor of the very fast and simple Govee lights local - Home Assistant