Metadata-Version: 2.4
Name: govee-ble-local
Version: 1.4.0
Summary: Local Bluetooth LE control of Govee devices — no cloud, no LAN API.
Author: Brady Woods
License-Expression: MIT
Project-URL: Homepage, https://github.com/Brady-Woods/govee-ble-local
Project-URL: Repository, https://github.com/Brady-Woods/govee-ble-local
Project-URL: Changelog, https://github.com/Brady-Woods/govee-ble-local/blob/master/CHANGELOG.md
Project-URL: Issues, https://github.com/Brady-Woods/govee-ble-local/issues
Keywords: govee,bluetooth,ble,home-assistant,led,light
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Home Automation
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Framework :: AsyncIO
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: bleak>=0.21
Requires-Dist: bleak-retry-connector>=3.0.0
Requires-Dist: cryptography>=41.0.0
Requires-Dist: kaitaistruct>=0.11
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: jsonschema>=4.0; extra == "test"
Requires-Dist: PyYAML>=6.0; extra == "test"
Requires-Dist: aiohttp>=3.9; extra == "test"
Requires-Dist: pycryptodome; extra == "test"
Provides-Extra: cloud
Requires-Dist: aiohttp>=3.9; extra == "cloud"
Provides-Extra: typing
Requires-Dist: mypy; extra == "typing"
Requires-Dist: types-PyYAML; extra == "typing"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Requires-Dist: bandit; extra == "lint"
Requires-Dist: pip-audit; extra == "lint"
Provides-Extra: docs
Requires-Dist: pdoc; extra == "docs"
Provides-Extra: audit
Requires-Dist: vulture; extra == "audit"
Requires-Dist: radon; extra == "audit"
Provides-Extra: dev
Requires-Dist: govee-ble-local[audit,cloud,lint,test,typing]; extra == "dev"
Requires-Dist: uv; extra == "dev"
Dynamic: license-file

# govee-ble-local

**Local Bluetooth LE control of Govee devices — no cloud, no LAN API.**

A standalone Python library implementing Govee's BLE control protocol (AES-ECB+RC4
handshake, 20-byte command framing, multi-packet scene upload, and status read-back),
derived from the decompiled Govee Home app and verified against real hardware. It's the
protocol engine behind the
[`hass-govee-ble-local`](https://github.com/Brady-Woods/hass-govee-ble-local)
Home Assistant integration, but has no Home Assistant dependency and can be used on its own.

> ⚠️ **Unofficial.** Not affiliated with or endorsed by Govee. Reverse-engineered from the
> app + hardware; behavior may change with device firmware. Live-verified on the **H60A6**
> (Ceiling Light Pro); the other curated SKUs are modelled from the app and are not all
> hardware-verified (see the profile notes).

## Install

```bash
pip install govee-ble-local
# optional Govee cloud-account helpers (pulls in aiohttp):
pip install 'govee-ble-local[cloud]'
# or, from a clone (editable, for development; needs the JVM kaitai-struct-compiler 0.11:
#   brew install kaitai-struct-compiler, or bash scripts/install_ksc.sh on Debian/Ubuntu;
#   see CONTRIBUTING.md):
git clone https://github.com/Brady-Woods/govee-ble-local.git
cd govee-ble-local && pip install -e '.[dev]'
```

Requires Python 3.11+. Installing from PyPI needs no compiler: the wheel ships the generated Kaitai classes. Runtime dependencies are deliberately minimal and Home-Assistant-friendly:
`bleak`, `bleak-retry-connector`, `cryptography` (all shipped with HA), plus `kaitaistruct` (a
pure-Python, stdlib-only wire-parser runtime). Dev extras: `.[dev]` (everything a contributor needs), or individually `.[test]`, `.[typing]`,
`.[lint]`, `.[audit]`, `.[docs]`.

## Quick start

```python
import asyncio
from govee_ble_local import create_device, discover

async def main():
    # Active-scan for supported Govee devices (strongest advertisement per address).
    devices = await discover(timeout=8.0)
    found = next(d for d in devices if d.sku == "H60A6")

    dev = create_device(found.ble_device, found.sku, found.advertisement)

    await dev.turn_on()
    await dev.set_brightness(60)             # percent
    await dev.set_rgb((255, 0, 0))
    await dev.set_zone_power("background", False)

    state = await dev.update()               # read-back into dev.state
    print(state.is_on, state.brightness, state.rgb_color, state.segments)

    await dev.stop()

asyncio.run(main())
```

## API

The library follows Home-Assistant BLE-library conventions: identify a device from its
advertisement, build a capability-driven `Device`, then drive it.

- **`discover(timeout=10.0, *, supported_only=True)`** — active-scan → `list[DiscoveredDevice]`
  (`.ble_device`, `.advertisement`, `.sku`, `.rssi`, `.supported`); raises
  `GoveeBleConnectionError` when the scan itself fails (no adapter, adapter off, no BlueZ/D-Bus);
  that error is also the `BleakError` / `OSError` / `EOFError` 1.3.0 raised, so either handler matches.
  `supported(name, mfg)` and `match(ble_device, adv)` are the passive-matcher hooks for a
  HA-style scanner.
- **`create_device(ble_device, sku, advertisement_data=None, *, secret=None, frame_log=None,
  idle_disconnect=None)`** — construct a `Device` for the SKU (raises `GoveeBleNotSupported` if
  uncurated). Pass the advertisement (`DiscoveredDevice.advertisement` or bleak's own): its
  encrypt flag selects the command channel, which some firmwares set differently from the SKU's
  default (H6008). `is_supported_sku(sku)` / `supported_skus()` / `profile_for(sku)` query the
  table (`device_profile_for(sku)` is a deprecated alias of `profile_for`, removed in 2.0).
- **`Device`** — one capability-gated class (behaviour comes from its `DeviceProfile`, not
  subclasses). Commands are ACK-confirmed on the wire:
  `turn_on` / `turn_off` / `set_power`, `set_brightness`, `set_rgb`, `set_color_temp`,
  `set_segment_rgb` / `set_segment_brightness` / `set_segment_color_temp`,
  `set_zone_power` / `set_zone_rgb` / `set_zone_color_temp`,
  `set_scene` / `set_scene_by_name`, and `update()` (read-back). Introspection:
  `capabilities`, `zones`, `min_kelvin` / `max_kelvin`, `scene_names`, `state`,
  `register_callback`. Methods for unsupported capabilities raise `GoveeBleNotSupported`.
- **`DeviceState`** — parsed state: `is_on`, `brightness` (0–100), `rgb_color`,
  `color_temp_kelvin`, `segments` (`list[Segment]`), `zone_power`, `zone_color_temp_kelvin` (H6047
  reads one Kelvin per bar), `gradual`, `scene_code`, plus
  device-info (`device_id` / `serial_number`, `wifi_mac`, `hardware_version`, `firmware_version`,
  `ble_mac`), read with the Govee app's own frames (`aa 06`, `aa 07 03`, `aa 07 02`).
- **`Capability`** — `POWER`, `BRIGHTNESS`, `RGB`, `COLOR_TEMP`, `SEGMENTS`, `SCENES`.

Curated SKUs: **H60A6**, **H6047**, **H61A8**, **H6004** (the BLE revision; the Wi-Fi-only one can't be controlled over BLE), **H6006/H6008**, **H6052**, **H6641**, **H6188**, **H6062**, **H6061** (Glide Hexa: whole-device power, brightness, colour and colour temperature), and the
plug family (**H5080/H5082/H5083/H5085/H5089/H5160/H5161**). Segment-colour read-back and some
scene dialects on the non-H60A6 SKUs are source-modelled and not yet hardware-verified.

Colour temperature matches the Govee app's frames: like the app, an out-of-range Kelvin is clamped to
the device range, not rejected (`set_color_temp(1500)` on a 2700-6500 K device sends 2700 K), and the
result is rounded to a whole 100 K step (`set_color_temp(2750)` sends and reports 2800 K). `state`
holds the Kelvin actually sent. Only a non-integer raises `GoveeBleInvalidValue`. Every CCT frame
carries the app's Kelvin tint, and whole-device CCT is one frame (on the H60A6 the all-segments mask,
as the app's Kelvin sliders send it).

Like the app, a picked RGB colour that is exactly one of its Kelvin tint colours (178 of them, two per
100 K step) and whose Kelvin is inside the SKU's range is sent as a colour-temperature frame instead of
an RGB frame, and `state` then reports `color_temp_kelvin` with `rgb_color` unset: `set_rgb`, `turn_on(rgb=)`,
`set_zone_rgb` and `set_segment_rgb` on the 0x15 devices (H60A6, H61A8, H6641, H6062, H6047, with the same mask
the RGB frame would use), the 0x0d bulbs and strip (H6004, H6006, H6008, H6052, H6188) and H6061. Colours outside
the range or one channel off the table stay RGB. Not modelled: colour-set / mixed-colour picks and the real-time
drag frames (RGB only). The H61A8's Kelvin range is [2000, 9000], or [2700, 6500] from hardware 3.08.01; until its
hardware version is read (once per device, on the first poll) the range valid on both, [2700, 6500], applies
(`Device.min_kelvin` / `max_kelvin` give the range in force). Its mode report carries no Kelvin, so after a
colour-temperature write `state` keeps the Kelvin that was sent until an RGB or scene write or a scene
report. Because it reports the same `15 01 00 00` after an RGB write too, a Kelvin the library sent stays in
`state` across a colour change made in the Govee app.

Read-back decodes the `aa 05` mode report the way the app's control screen does for each SKU. The
H6052's screen reads its colour report as RGB only, so after a poll an H6052 in colour-temperature
mode reports the report's colour as `rgb_color` (for a CCT the app or this library set, the Kelvin's
tint) and no `color_temp_kelvin`.

The H6047 follows the app's frames too: whole-device power is `33 01` (`33 36` is only the per-bar
buttons, `set_zone_power`), read-back covers its 10 segments, `Device.zone_color_temp_kelvin(name)`
gives each bar's Kelvin, and `set_gradual` toggles the fade flag.

The full API reference is generated from the docstrings with [`pdoc`](https://pdoc.dev)
(`pip install '.[docs]' && bash tools/gen_docs.sh` → `docs/api/`); `govee_ble_local.__all__` is the
public contract.

## Security notes

The Govee BLE protocol itself has well-known limits that this library cannot fix:

- Its encryption uses a public pre-shared key, AES-ECB for the handshake and an RC4 stream for
  traffic, and integrity is only an XOR checksum. Anyone within radio range can drive an
  encrypted device.
- Captures made with `GOVEE_FRAME_LOG` contain wire frames from which the session key can be
  derived. Treat them as sensitive and do not post them publicly. Frame-log files are created
  with owner-only permissions.

## Diagnostics & session capture

The library logs under the `govee_ble_local.*` hierarchy and can capture a full protocol
session (over a local adapter or a Home Assistant Bluetooth proxy) for analysis. Enable the
flow trace, or the per-frame firehose, and decode a capture with the bundled
`govee-ble-analyze` CLI. See [`docs/DIAGNOSTICS.md`](docs/DIAGNOSTICS.md).

## Protocol

The wire protocol is specified as Kaitai Struct definitions in [`spec/`](spec/): one root per message
class — [`govee_ble_frame.ksy`](spec/govee_ble_frame.ksy) (every command/reply frame, typed by its
direction and the device's [`govee_profile`](spec/govee_profile.ksy)), the decoded state
([`govee_state.ksy`](spec/govee_state.ksy), [`govee_status_stream.ksy`](spec/govee_status_stream.ksy)),
the upload values ([`govee_upload.ksy`](spec/govee_upload.ksy)), the advertisement
([`govee_manufacturer_data.ksy`](spec/govee_manufacturer_data.ksy),
[`govee_scan_record.ksy`](spec/govee_scan_record.ksy)) and the BGC-info value
([`govee_bgc_info.ksy`](spec/govee_bgc_info.ksy)), sharing [`govee_common.ksy`](spec/govee_common.ksy)
— with the per-device table in [`spec/devices.yaml`](spec/devices.yaml). The Kaitai read-write classes
under `govee_ble_local/_generated/` are generated from those, and the library parses and builds every
frame with them (source of truth: the decompiled Govee app).

## Contributing & versioning

See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, branching, commit, and release
conventions. Changes are tracked in [`CHANGELOG.md`](CHANGELOG.md); the project follows
[Semantic Versioning](https://semver.org/). Live hardware tests run against the curated SKUs above.

## License

[MIT](LICENSE) © Brady Woods
