govee-ble-local 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- govee_ble_local-1.2.0/CHANGELOG.md +294 -0
- govee_ble_local-1.2.0/LICENSE +21 -0
- govee_ble_local-1.2.0/MANIFEST.in +11 -0
- govee_ble_local-1.2.0/PKG-INFO +181 -0
- govee_ble_local-1.2.0/README.md +126 -0
- govee_ble_local-1.2.0/pyproject.toml +138 -0
- govee_ble_local-1.2.0/setup.cfg +4 -0
- govee_ble_local-1.2.0/src/govee_ble_local/__init__.py +69 -0
- govee_ble_local-1.2.0/src/govee_ble_local/_generated/__init__.py +1 -0
- govee_ble_local-1.2.0/src/govee_ble_local/_generated/govee_advertisement.py +132 -0
- govee_ble_local-1.2.0/src/govee_ble_local/_generated/govee_ble_frame.py +3005 -0
- govee_ble_local-1.2.0/src/govee_ble_local/cloud/__init__.py +14 -0
- govee_ble_local-1.2.0/src/govee_ble_local/cloud/account.py +323 -0
- govee_ble_local-1.2.0/src/govee_ble_local/const.py +94 -0
- govee_ble_local-1.2.0/src/govee_ble_local/crypto.py +80 -0
- govee_ble_local-1.2.0/src/govee_ble_local/debug.py +203 -0
- govee_ble_local-1.2.0/src/govee_ble_local/devices/__init__.py +1 -0
- govee_ble_local-1.2.0/src/govee_ble_local/devices/device.py +928 -0
- govee_ble_local-1.2.0/src/govee_ble_local/devices/profile.py +185 -0
- govee_ble_local-1.2.0/src/govee_ble_local/exceptions.py +34 -0
- govee_ble_local-1.2.0/src/govee_ble_local/identify.py +115 -0
- govee_ble_local-1.2.0/src/govee_ble_local/models.py +91 -0
- govee_ble_local-1.2.0/src/govee_ble_local/py.typed +0 -0
- govee_ble_local-1.2.0/src/govee_ble_local/registry.py +50 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scanner.py +77 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6006.json +474 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6008.json +474 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6047.json +522 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6052.json +346 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scenes/H60A6.json +674 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scenes/H61A8.json +1186 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6641.json +1186 -0
- govee_ble_local-1.2.0/src/govee_ble_local/scenes/__init__.py +168 -0
- govee_ble_local-1.2.0/src/govee_ble_local/transport/__init__.py +2 -0
- govee_ble_local-1.2.0/src/govee_ble_local/transport/connection.py +775 -0
- govee_ble_local-1.2.0/src/govee_ble_local/transport/framelog.py +175 -0
- govee_ble_local-1.2.0/src/govee_ble_local/transport/handshake.py +62 -0
- govee_ble_local-1.2.0/src/govee_ble_local/wire/__init__.py +5 -0
- govee_ble_local-1.2.0/src/govee_ble_local/wire/build.py +324 -0
- govee_ble_local-1.2.0/src/govee_ble_local/wire/describe.py +210 -0
- govee_ble_local-1.2.0/src/govee_ble_local/wire/parse.py +370 -0
- govee_ble_local-1.2.0/src/govee_ble_local/wire/reassemble.py +164 -0
- govee_ble_local-1.2.0/src/govee_ble_local.egg-info/PKG-INFO +181 -0
- govee_ble_local-1.2.0/src/govee_ble_local.egg-info/SOURCES.txt +46 -0
- govee_ble_local-1.2.0/src/govee_ble_local.egg-info/dependency_links.txt +1 -0
- govee_ble_local-1.2.0/src/govee_ble_local.egg-info/entry_points.txt +2 -0
- govee_ble_local-1.2.0/src/govee_ble_local.egg-info/requires.txt +36 -0
- govee_ble_local-1.2.0/src/govee_ble_local.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to `govee-ble-local`. Format loosely follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/); the project follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/) and is versioned in `pyproject.toml`. Entries are grouped
|
|
6
|
+
**Added / Changed / Fixed / Spec** (Spec = the Kaitai `spec/*.ksy` + `spec/devices.yaml` protocol
|
|
7
|
+
model, from which the shipped readers are generated).
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [1.2.0] — 2026-10-12
|
|
12
|
+
|
|
13
|
+
First release published to PyPI. `1.1.0` was bumped on a development branch but never tagged or
|
|
14
|
+
released, so its changes ship here too (see the `1.1.0` entry below).
|
|
15
|
+
|
|
16
|
+
Hardening release candidate: input validation, connection/lifecycle reliability, secret handling
|
|
17
|
+
and packaging. Behaviour changes that can affect callers are listed under **Behaviour changes**.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `GoveeBleInvalidValue` (a `ValueError` subclass, also a `GoveeBleError`): raised for bad setter
|
|
21
|
+
input: colour channels, kelvin outside the device's `min_kelvin`/`max_kelvin`, segment/zone
|
|
22
|
+
indices and masks, scene payloads, secret length. Builders and AES/RC4 key lengths are validated too.
|
|
23
|
+
- `Device.connect()`, an `idle_disconnect` argument on `Device`/`create_device`, and
|
|
24
|
+
`Device.scene_upload_frames(scene)` (the planned upload frames, or `None`).
|
|
25
|
+
- `__version__` and more public exports: `Encryption`, `Scene`, `StateCallback`, `profile_for`,
|
|
26
|
+
`GoveeBleInvalidValue`.
|
|
27
|
+
- `app_version` argument on the cloud account (overrides the `appVersion` request header).
|
|
28
|
+
- `govee-ble-analyze --from-frames-log` input is validated (malformed lines are skipped, reported
|
|
29
|
+
and counted).
|
|
30
|
+
- Dev tooling: `dev` and `audit` extras, `scripts/audit.sh`, a `[tool.vulture]` config, an
|
|
31
|
+
independent AES reference test (`pycryptodome`, test-only), ruff/mypy-strict/bandit/pip-audit
|
|
32
|
+
gates in `scripts/check.sh` and CI, and a tag-triggered PyPI trusted-publishing workflow.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- `kaitaistruct>=0.11` (was unpinned below); SPDX `license = "MIT"` metadata; classifiers, `Issues`
|
|
36
|
+
URL and a markdown `readme` content type for PyPI.
|
|
37
|
+
- The sdist no longer bundles `tests/`; it carries only `src/`, `pyproject.toml`, `README.md`,
|
|
38
|
+
`LICENSE` and `CHANGELOG.md`.
|
|
39
|
+
- Profile fields are `Literal`-typed and validated at construction.
|
|
40
|
+
- Segment merge / scene planning moved into `wire/` and `scenes/`; `Device` uses public
|
|
41
|
+
`scene_upload_frames`; the private scene alias was removed.
|
|
42
|
+
- CI: SHA-pinned actions with minimal permissions, Python 3.11-3.13 matrix, lowest-dependency job;
|
|
43
|
+
optional test dependencies fail instead of skipping when `CI` is set.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- A disconnect racing an in-flight acknowledgement no longer leaves the write waiting or raises a
|
|
47
|
+
non-library error; a link lost while the write lock is held raises a typed error.
|
|
48
|
+
- A slow poll can no longer overwrite state set by a newer setter (mutation-sequence guard).
|
|
49
|
+
- Relay power: power frames are acknowledged and the resulting state is applied; time sync is
|
|
50
|
+
best-effort.
|
|
51
|
+
- Bar-switch devices (H6047): the read-modify-write of the two-bar frame is serialized; the other
|
|
52
|
+
bar's state is read before use.
|
|
53
|
+
- Lifecycle leaks: client/link no longer leaked on handshake, device-info and disconnect error
|
|
54
|
+
paths; detached disconnects are bounded in time.
|
|
55
|
+
- Plaintext receive queue (no decrypt with a stale session key); a queued frame arriving at the
|
|
56
|
+
deadline is not lost; the end-of-burst sentinel is never dropped.
|
|
57
|
+
- Device-info link errors no longer fail `update()`; device info is retried.
|
|
58
|
+
- Encryption fallback when the advertised mode is wrong; secret length is checked.
|
|
59
|
+
- State callbacks no longer break the notify pump or each other (exceptions are logged).
|
|
60
|
+
- Colour groups with an invalid index are skipped instead of producing negative segment indices;
|
|
61
|
+
V1 segment brightness stays `None`.
|
|
62
|
+
- Cloud client: response shapes are validated (status checked before JSON parsing, token type
|
|
63
|
+
check); the owned session is closed when login fails; one malformed device secret no longer
|
|
64
|
+
fails the whole account.
|
|
65
|
+
- Scene catalogs: SKUs are validated, malformed catalogs and bad base64 raise library errors, and
|
|
66
|
+
catalog writes by `tools/fetch_scenes.py` are atomic.
|
|
67
|
+
- Hardware tools: SKU/path validation, argparse, btsnoop length caps, restore-in-`finally`;
|
|
68
|
+
`tools/decrypt_btsnoop.py`/`btsnoop_mtu_scan.py` mask the account secret.
|
|
69
|
+
|
|
70
|
+
### Security
|
|
71
|
+
- Frame-log files are created `0600`, written with `O_NOFOLLOW`, refused if not a regular file,
|
|
72
|
+
bounded (oldest records dropped when full) and flushed at interpreter exit; a dead writer is
|
|
73
|
+
revived. Docs now state that captures are sensitive (the session key is derivable from them).
|
|
74
|
+
- Cloud responses are shape-validated; malformed device secrets are never echoed.
|
|
75
|
+
- Secret masking in captures shares one implementation and recomputes the frame checksum.
|
|
76
|
+
- CI runs `bandit` (Medium+) and `pip-audit`; the three accepted `cryptography` advisories are
|
|
77
|
+
documented (AES-ECB/ARC4 only; PKCS#7/X.509 unused).
|
|
78
|
+
|
|
79
|
+
### Behaviour changes (may affect callers)
|
|
80
|
+
- Bar-switch devices: `set_zone_power` raises when the other bar's state is unknown and cannot be
|
|
81
|
+
read, instead of assuming it is on.
|
|
82
|
+
- `GoveeBleAuthError` is no longer retried.
|
|
83
|
+
- `send(expect_ack=True)` rejects non-write frames.
|
|
84
|
+
- Setters raise `GoveeBleInvalidValue` (still a `ValueError`) for invalid input, including kelvin
|
|
85
|
+
outside the profile's `min_kelvin`/`max_kelvin`. (Brightness percent is still clamped to 1..100.)
|
|
86
|
+
- `Capability` and `Encryption` are `StrEnum`: `str(member)` now returns the value
|
|
87
|
+
(e.g. `"power"`), not `"Capability.POWER"`.
|
|
88
|
+
- `load_scenes` returns copies (mutating the result does not affect the cache) and raises library
|
|
89
|
+
errors for malformed catalogs.
|
|
90
|
+
- Cloud `get_devices` returns `secret=None` and logs a warning for a malformed secret rather than
|
|
91
|
+
failing.
|
|
92
|
+
|
|
93
|
+
## [1.1.0] — 2026-10-12
|
|
94
|
+
|
|
95
|
+
Reliability overhaul driven by five days of live frame captures (11 devices, 2 ESPHome proxies +
|
|
96
|
+
a local adapter) and validated live on the same hardware: steady-state polls went from ~7.7 s
|
|
97
|
+
with a 5 s stall (H60A6) and ~40 % failures to 1.5–4.5 s with no failures.
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
- **Secret leaked through `wire=` in frame captures.** Only the `plain=` field of the `33 b2` / `aa b1` secret frames was masked; on a plaintext link `wire=` is the same bytes, and on an encrypted link it is decryptable from the captured handshake plus the public PSK. Both fields (and the bad-checksum debug line) are now masked.
|
|
101
|
+
- `set_segment_brightness` raised a bare `ValueError` on colour schemes without per-segment
|
|
102
|
+
brightness (H61A8); it now raises `GoveeBleNotSupported`, like `set_segment_color_temp`.
|
|
103
|
+
- **Handshake raced the device.** The device delivers its `e7 01` reply twice and re-sends the
|
|
104
|
+
`e7 02` echo ~1 s later. The duplicate step-1 reply was consumed as the step-2 "ack", so the
|
|
105
|
+
first query fired while the confirm was still in flight; ~90 % of H60A6 sessions then lost the
|
|
106
|
+
`0xFF` terminator of their first status burst (5 s stall + retry on every poll). Replies are
|
|
107
|
+
now matched by content, the real confirm is awaited, and late retransmits are dropped.
|
|
108
|
+
- Frames failing the XOR checksum are dropped at the notify pump (they were the source of the
|
|
109
|
+
intermittent "unknown proType 0x67/0x7f/0xa5" warnings: handshake echoes decrypted with the
|
|
110
|
+
session key that happened to pass the checksum).
|
|
111
|
+
- A link that stops acknowledging writes (half-dead while bleak still reports "connected") is
|
|
112
|
+
torn down and the write retried once on a fresh link, instead of failing the command.
|
|
113
|
+
- A disconnect wakes pending waits immediately; a late disconnect callback from a superseded
|
|
114
|
+
client no longer clears the current one.
|
|
115
|
+
- The idle-disconnect timer can no longer fire after newer activity (it used to disconnect
|
|
116
|
+
right after a command and force a reconnect); its task is now held so it can't be GC'd.
|
|
117
|
+
- `establish_connection` gets `ble_device_callback`, so retries use the freshest proxy path.
|
|
118
|
+
- Every public operation has a hard deadline (`OPERATION_TIMEOUT`) and raises only
|
|
119
|
+
`GoveeBleError` subclasses (bleak/asyncio errors no longer leak out of `send`/`query`).
|
|
120
|
+
- 0xAC reassembly joins only the contiguous chunk prefix (a gap used to splice later chunks at
|
|
121
|
+
the wrong offset and decode garbage as real state) and decodes a truncated burst up to its last
|
|
122
|
+
whole TLV, so a lost tail yields partial-but-true state without a retry; queries end on a short
|
|
123
|
+
idle gap instead of the full timeout when a terminator is lost.
|
|
124
|
+
- 0x0d bulbs (H6006/H6008/H6052): the mode report is `[r, g, b, kelvin u2be]`; white + kelvin is
|
|
125
|
+
colour-temperature mode. H6052's "mechanism C" decoded it as RGB-only (reporting white instead
|
|
126
|
+
of 2700 K, live capture `aa 05 0d ff ff ff 0a 8c`) and issued a duplicate mode query; all three
|
|
127
|
+
bulbs now read colour/CCT back from the report they already send.
|
|
128
|
+
- Polled devices always re-read power (previously skipped once a device was off, so a light
|
|
129
|
+
turned on externally was never noticed).
|
|
130
|
+
- H6047: changing one bar no longer assumes the other bar is ON when its state is unknown (it
|
|
131
|
+
turned the other bar on); the bar state is read back (`aa 36`) and polled.
|
|
132
|
+
- Advertised on/off is ignored for 15 s after a command/read (stale broadcasts flipped state).
|
|
133
|
+
- Brightness is never sent as 0; the reported-brightness scale is an explicit profile field
|
|
134
|
+
(`brightness_max`) instead of a `>100 means 0-255` guess.
|
|
135
|
+
- Scene uploads are paced instead of blasted (dropped frames over proxies).
|
|
136
|
+
- Account-lock secrets are masked in frame captures (logs/diagnostics).
|
|
137
|
+
- `FrameLog` file writes moved off the event loop.
|
|
138
|
+
- Cloud client: request timeouts, the documented-but-missing re-login on 401, no response bodies
|
|
139
|
+
in error messages, no forced login when a token is supplied.
|
|
140
|
+
|
|
141
|
+
### Added
|
|
142
|
+
- `Device.turn_on(brightness=, rgb=, kelvin=, power=)` / `turn_off()` and
|
|
143
|
+
`GoveeConnection.send_many()`: multi-attribute changes go out as one batched, ack-verified
|
|
144
|
+
operation on one connection. Whole-fixture power is always one frame: H60A6 uses the global
|
|
145
|
+
`33 01` switch (live-verified to drive both zones: off clears them, on lights both even when
|
|
146
|
+
each was off; previously one `33 30` frame per zone), H6047 the combined `33 36` bar frame.
|
|
147
|
+
- `GoveeBleAuthError` is raised when a device rejects its secret key.
|
|
148
|
+
- `Device.encryption` (the negotiated channel mode); colour temperature read back from the mode
|
|
149
|
+
report; segment writes update `state.segments`.
|
|
150
|
+
- `tools/power_semantics_probe.py`: hardware experiment for global vs. zone power.
|
|
151
|
+
- Fake-peripheral test harness (`tests/fake_ble.py`) reproducing the captured failure modes;
|
|
152
|
+
release workflow publishing to PyPI via trusted publishing.
|
|
153
|
+
|
|
154
|
+
### Removed
|
|
155
|
+
- Dead code: `ble/frame.py`, duplicate `checksum_ok`, unused `CONNECT_TIMEOUT`.
|
|
156
|
+
|
|
157
|
+
## [1.0.3] — 2026-07-12
|
|
158
|
+
|
|
159
|
+
Field-tested and confirmed live on H60A6 before merge (see PR discussion): a truncated 9-of-11-chunk
|
|
160
|
+
burst recovered fully on retry with no warning; an H6047/H6641-style zero-chunk case correctly
|
|
161
|
+
logged `state left stale` with chunk indices after the retry, rather than papering over it.
|
|
162
|
+
|
|
163
|
+
### Fixed
|
|
164
|
+
- `Device._read_status()` (the `0xAC` status read-back for `readback="status"` devices, e.g. H60A6)
|
|
165
|
+
now **retries once** on an empty parse before leaving state stale. A dropped BLE notification
|
|
166
|
+
mid-burst yields a truncated or empty reassembled buffer — reported in the field as
|
|
167
|
+
`status reply did not parse (0 bytes): ... requested 1 bytes, but only 0 bytes available` and
|
|
168
|
+
`status reply did not parse (148 bytes): ... requested 17 bytes, but only 12 bytes available`
|
|
169
|
+
(`wire/reassemble.py` DEBUG log). This was already non-fatal (fields are applied conditionally,
|
|
170
|
+
so a failed parse can't erase known-good state) and already surfaced a WARNING
|
|
171
|
+
(`"... state left stale"`), but a single dropped notification cost a whole poll cycle; a fresh
|
|
172
|
+
query is independent and usually succeeds immediately. Both the retry-attempt DEBUG line and the
|
|
173
|
+
final WARNING now also report which `0xAC` chunk indices were actually received (e.g.
|
|
174
|
+
`0x00,0x01,0x02,0x04,0xff`, or `<none>`), to distinguish a dropped-mid-burst from a
|
|
175
|
+
completely-empty read on any future occurrence.
|
|
176
|
+
|
|
177
|
+
## [1.0.2] — 2026-07-12
|
|
178
|
+
|
|
179
|
+
### Fixed
|
|
180
|
+
- **H6047/H6641's `1.0.1` colour read-back fix reused the wrong controller's decoder.** `1.0.1`
|
|
181
|
+
correctly identified that neither SKU dispatches the `0xAC` status burst, but wired their
|
|
182
|
+
per-segment colour read-back onto H61A8's `mechanism_b` (`BulbGroupColorV2`) decoder —
|
|
183
|
+
a *different, unrelated controller* that only happens to share the `0xA5` wire opcode with
|
|
184
|
+
the one H6047/H6641 actually use (`Controller4ColorInfoByGroup`, confirmed via a full re-read
|
|
185
|
+
of `devices.yaml`: *"SAME PARSER ... but TWO different TRANSPORTS"*, explicitly distinct from
|
|
186
|
+
H61A8's separately-listed `BulbGroupColor`/mechanism-B). The two decoders disagree on group
|
|
187
|
+
size (3 vs 4), so segment indices were silently misassigned. Caught by the test suite itself
|
|
188
|
+
before any further guessing — a second wrong per-batch value (5, conflating two unrelated
|
|
189
|
+
"5"s from two different code paths) was caught the same way and never shipped.
|
|
190
|
+
- New `color_readback="mechanism_a_direct"` (`wire.parse.parse_direct_color_group`,
|
|
191
|
+
`Device._read_mechanism_a_direct`) correctly decodes `Controller4ColorInfoByGroup`'s per-group
|
|
192
|
+
reply — group size **4**, source-confirmed independently for both SKUs
|
|
193
|
+
(`devices.yaml`: *"4/group"* / *"4 records/group"*), not extrapolated from H61A8.
|
|
194
|
+
- **H6641's group-count approximation is now resolved, not just documented**: a live `0x40`
|
|
195
|
+
IC-count read (`build.ic_count_query`, `wire.parse.parse_ic_count`, the already-modeled
|
|
196
|
+
`ic_segment_read` ksy type) returns the device's own precomputed group count directly —
|
|
197
|
+
no client-side ceil-division/divisor guess needed. New `DeviceProfile.color_readback_live_ic`
|
|
198
|
+
flag gates this per-SKU (H6641 only; H6047 keeps its static, source-confirmed piece count).
|
|
199
|
+
- `Device._read_mechanism_b` is now H61A8-only again (its docstring no longer over-claims
|
|
200
|
+
sharing with H6047/H6641).
|
|
201
|
+
|
|
202
|
+
## [1.0.1] — 2026-07-12
|
|
203
|
+
|
|
204
|
+
### Fixed
|
|
205
|
+
- **H6047 and H6641 never actually answer the `0xAC` status burst** — they were wrongly modeled
|
|
206
|
+
as mechanism-A (the same `0xAC → 0xA5` path as H60A6), which produced a deterministic
|
|
207
|
+
zero-frame read on real hardware (source-confirmed, not a wire/timing issue: `H6047`, goodsType
|
|
208
|
+
119, routes to `Compose4InfoBleIot`, `Support.isGoodsTypeH6047:177`; `H6641`, goodsType 247,
|
|
209
|
+
never reaches the `afterConnected` `0xAC` dispatch at all, `H61D3Support.f0(247)=false`). Both
|
|
210
|
+
read per-segment colour via **direct per-group requests** instead (`AA A5 <group>`,
|
|
211
|
+
`Controller4ColorInfoByGroup` — the same decode H61A8 already used as `mechanism_b`).
|
|
212
|
+
`DeviceProfile.readback` for both switches `"status"` → `"polled"` (power/brightness/scene via
|
|
213
|
+
`aa 01/04/05`); `color_readback="mechanism_b"` added for both, reusing the existing decoder.
|
|
214
|
+
New `DeviceProfile.color_readback_segments` field lets H6047's batch math use its true
|
|
215
|
+
read-back piece count (`getColorPieceSize`=12) instead of its write/addressable count (10),
|
|
216
|
+
which differ for this SKU.
|
|
217
|
+
**Two related approximations remain open (flagged in `profile.py`, not fixed by this change):**
|
|
218
|
+
H6641's true colour-group *count* is IC-driven (needs a live `0x40` read this library doesn't
|
|
219
|
+
perform yet — falls back to the write-mask width as an approximation), and its per-reply
|
|
220
|
+
*record count* assumption (3, matching H61A8's `BulbStringColorControllerV2`) is unconfirmed —
|
|
221
|
+
H6641 may use an unmodeled `V3` controller with a different count/layout. Neither SKU is yet
|
|
222
|
+
live-verified.
|
|
223
|
+
|
|
224
|
+
### Spec
|
|
225
|
+
- `devices.yaml` / `docs/GOVEE_BLE_GATT_PROTOCOL.md`: documented the H6047/H6641 `0xAC`
|
|
226
|
+
non-dispatch (source citations above) and their `per_segment_color_read` direct-request
|
|
227
|
+
transport; reconciled `H61D3Support.e()`'s divisor (`d ∈ {3,4}` → `d ∈ {3,4,5}`, H6641/247 = 5)
|
|
228
|
+
against the `ceil(IC/5)` group-count math.
|
|
229
|
+
|
|
230
|
+
## [1.0.0] — 2026-07-11
|
|
231
|
+
|
|
232
|
+
First public release. Prior `v1`/`v2`/`v3` were internal rewrite generations (never tagged or
|
|
233
|
+
published); `1.0.0` is the initial Semantic-Versioning baseline, committing to the
|
|
234
|
+
`govee_ble_local.__all__` public API. This release is the ground-up **v3-generation rewrite**: a
|
|
235
|
+
data-driven `DeviceProfile` table + one capability-gated `Device` class over a spec-first `wire/`
|
|
236
|
+
layer (build / parse / reassemble) and the shipped, ksy-generated readers. Clean break from the
|
|
237
|
+
old `GoveeBleClient` API.
|
|
238
|
+
|
|
239
|
+
### Added
|
|
240
|
+
- Capability-driven `Device` + `create_device()` / `discover()` / `DeviceProfile` public API.
|
|
241
|
+
- Colour temperature per zone/segment: `Device.set_zone_color_temp()` / `set_segment_color_temp()`
|
|
242
|
+
+ `build.segment_color_temp()` (the 0x15 CCT frame's segment mask, previously all-segments only).
|
|
243
|
+
- Read-back: plug relay power, H6047 segment colours (mechanism-A status), and device-info
|
|
244
|
+
(`serial` / `wifi_mac` / `firmware` / `hardware`), plus `DeviceState.ble_mac`.
|
|
245
|
+
- `Device.set_gradual()` — the `0xA3` gradual/fade-on-BLE↔wifi-handoff flag (curated: H61A8;
|
|
246
|
+
read back into `DeviceState.gradual` on `update()`).
|
|
247
|
+
- `Device.read_secret()` and `Device.ingest_advertisement()` restored (v2 parity).
|
|
248
|
+
- Diagnostics: a coherent `govee_ble_local.*` log-level scheme, a `govee_ble_local.frames`
|
|
249
|
+
frame-tier logger for full-session capture (incl. over a Home Assistant Bluetooth proxy),
|
|
250
|
+
the `govee-ble-analyze` console script + a frames-log→JSONL converter, and `docs/DIAGNOSTICS.md`.
|
|
251
|
+
- Live H60A6 tools: CCT choreography + a segment-map probe.
|
|
252
|
+
|
|
253
|
+
### Changed
|
|
254
|
+
- The reassembled `0xAC` status buffer is now parsed **entirely by the generated `StatusReply`
|
|
255
|
+
reader** — the hand TLV-walk (`walk_tlvs`), `_add_color_group`, and the MAC-anchor device-info
|
|
256
|
+
heuristic are all retired. Only the cross-frame de-chunk stays hand-done (Kaitai can't join
|
|
257
|
+
frames). The offline analyzer uses the same reader.
|
|
258
|
+
- README + version brought to the v3 API; description now "Govee devices" (plugs included).
|
|
259
|
+
- **Dependency hygiene:** `PyYAML` moved to the `test` extra — it isn't used at runtime (the device
|
|
260
|
+
table is hardcoded Python; `yaml` appears only in the test suite). Runtime deps are now
|
|
261
|
+
`bleak` / `bleak-retry-connector` / `cryptography` (HA defaults) + `kaitaistruct` (the one accepted
|
|
262
|
+
pure-Python exception). Added `[project.urls]`.
|
|
263
|
+
- **Docs:** README install-from-source instructions (not yet on PyPI); a `pdoc` API reference
|
|
264
|
+
(`.[docs]` extra + `tools/gen_docs.sh`); a `CONTRIBUTING.md` (branching / commit / changelog /
|
|
265
|
+
release conventions); project conventions recorded for maintainers.
|
|
266
|
+
|
|
267
|
+
### Fixed
|
|
268
|
+
- H60A6 segment→zone map corrected (index 12 = main panel, 0–11 = background ring), making
|
|
269
|
+
per-zone colour/CCT genuinely independent — live-verified.
|
|
270
|
+
- H60A6 `wifi_mac` / `hardware_version` read-back (regressed in the v3 clean break) restored,
|
|
271
|
+
now from the modeled `0x07` device-info TLV; zeroed `aa 07` replies no longer clobber real
|
|
272
|
+
values (`0.00.00` / all-zero → None).
|
|
273
|
+
- `reassemble()` de-duplicates doubled status chunks (devices double-deliver notifications).
|
|
274
|
+
|
|
275
|
+
### Spec
|
|
276
|
+
- Modeled the reassembled `0xAC` nested TLV values (source-verified, `Compose4BaseInfoSingleRead`):
|
|
277
|
+
`status_tlv` switches `0x05→mode_status`, `0x07→device_info_read`, `0xA5→color_group_status`;
|
|
278
|
+
`status_reply` uses `repeat: until type==0` + `if: type != 0` to survive the trailing zero-pad.
|
|
279
|
+
- Added `gradual_read` (0xA3) — now surfaced (`Device.set_gradual`) — and tag-grouped
|
|
280
|
+
`color_strip_write`, which is modeled but **not exposed**: it has no curated-SKU binding in
|
|
281
|
+
`devices.yaml` (needs source: which SKU uses `MultipleColorStripControllerV1` / comType 0x40, and
|
|
282
|
+
whether it complements or replaces `set_segment_rgb`).
|
|
283
|
+
- Fixed a YAML-1.1 boolean id trap (`id: on` → parsed as `.true`); renamed to `state` / `on_flag`.
|
|
284
|
+
- `devices.yaml`: H60A6 segment→zone map; H6641 colour records are 4-byte; mechanism-B/C read-back
|
|
285
|
+
and scene-dialect notes.
|
|
286
|
+
- Adopted the parametric **Tier-C** grammar variants (`op15_color_typed`, `mode_color_0d_typed`;
|
|
287
|
+
joining `color_group_read`) so the ksy owns 100% of byte structure — the client passes one
|
|
288
|
+
discriminator and the branch runs inside the grammar. `devices.yaml` gained per-family
|
|
289
|
+
`client_profile` blocks (discriminators as data) + schema support. Readers regenerated; additive,
|
|
290
|
+
no runtime behaviour change.
|
|
291
|
+
- `spec/` is now a self-documenting protocol package: added the canonical protocol docs
|
|
292
|
+
(`spec/docs/GOVEE_BLE_GATT_PROTOCOL.md`, `SCENE_UPLOAD_ENCODING.md`, `USING_THE_KSY.md`) and an
|
|
293
|
+
executable reference codec (`spec/govee_reference.py`) — the framing layer Kaitai can't express
|
|
294
|
+
(AES-ECB+RC4 decrypt, `0xAC` reassemble, XOR BCC) + dispatch wiring, with a self-test.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brady Woods
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# sdist contents: the package, metadata and top-level docs only. No tests, tools, spec,
|
|
2
|
+
# docs, scripts or CI config (those live in the git repo, not on PyPI).
|
|
3
|
+
include LICENSE README.md CHANGELOG.md pyproject.toml
|
|
4
|
+
graft src
|
|
5
|
+
prune tests
|
|
6
|
+
prune tools
|
|
7
|
+
prune spec
|
|
8
|
+
prune docs
|
|
9
|
+
prune scripts
|
|
10
|
+
prune .github
|
|
11
|
+
global-exclude *.py[cod] __pycache__ .DS_Store
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: govee-ble-local
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Local Bluetooth LE control of Govee devices — no cloud, no LAN API.
|
|
5
|
+
Author: Brady Woods
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Brady-Woods/govee-ble-local
|
|
8
|
+
Project-URL: Repository, https://github.com/Brady-Woods/govee-ble-local
|
|
9
|
+
Project-URL: Changelog, https://github.com/Brady-Woods/govee-ble-local/blob/master/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/Brady-Woods/govee-ble-local/issues
|
|
11
|
+
Keywords: govee,bluetooth,ble,home-assistant,led,light
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Home Automation
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Framework :: AsyncIO
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: bleak>=0.21
|
|
26
|
+
Requires-Dist: bleak-retry-connector>=3.0.0
|
|
27
|
+
Requires-Dist: cryptography<49,>=41.0.0
|
|
28
|
+
Requires-Dist: kaitaistruct>=0.11
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest; extra == "test"
|
|
31
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
33
|
+
Requires-Dist: jsonschema>=4.0; extra == "test"
|
|
34
|
+
Requires-Dist: PyYAML>=6.0; extra == "test"
|
|
35
|
+
Requires-Dist: aiohttp>=3.9; extra == "test"
|
|
36
|
+
Requires-Dist: pycryptodome; extra == "test"
|
|
37
|
+
Provides-Extra: cloud
|
|
38
|
+
Requires-Dist: aiohttp>=3.9; extra == "cloud"
|
|
39
|
+
Provides-Extra: typing
|
|
40
|
+
Requires-Dist: mypy; extra == "typing"
|
|
41
|
+
Requires-Dist: types-PyYAML; extra == "typing"
|
|
42
|
+
Provides-Extra: lint
|
|
43
|
+
Requires-Dist: ruff; extra == "lint"
|
|
44
|
+
Requires-Dist: bandit; extra == "lint"
|
|
45
|
+
Requires-Dist: pip-audit; extra == "lint"
|
|
46
|
+
Provides-Extra: docs
|
|
47
|
+
Requires-Dist: pdoc; extra == "docs"
|
|
48
|
+
Provides-Extra: audit
|
|
49
|
+
Requires-Dist: vulture; extra == "audit"
|
|
50
|
+
Requires-Dist: radon; extra == "audit"
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: govee-ble-local[audit,cloud,lint,test,typing]; extra == "dev"
|
|
53
|
+
Requires-Dist: uv; extra == "dev"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# govee-ble-local
|
|
57
|
+
|
|
58
|
+
**Local Bluetooth LE control of Govee devices — no cloud, no LAN API.**
|
|
59
|
+
|
|
60
|
+
A standalone Python library implementing Govee's BLE control protocol (AES-ECB+RC4
|
|
61
|
+
handshake, 20-byte command framing, multi-packet scene upload, and status read-back),
|
|
62
|
+
derived from the decompiled Govee Home app and verified against real hardware. It's the
|
|
63
|
+
protocol engine behind the
|
|
64
|
+
[`hass-govee-ble-local`](https://github.com/Brady-Woods/hass-govee-ble-local)
|
|
65
|
+
Home Assistant integration, but has no Home Assistant dependency and can be used on its own.
|
|
66
|
+
|
|
67
|
+
> ⚠️ **Unofficial.** Not affiliated with or endorsed by Govee. Reverse-engineered from the
|
|
68
|
+
> app + hardware; behavior may change with device firmware. Live-verified on the **H60A6**
|
|
69
|
+
> (Ceiling Light Pro); the other curated SKUs are modelled from the app and are not all
|
|
70
|
+
> hardware-verified (see the profile notes).
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
Not on PyPI yet — install from source:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install "git+https://github.com/Brady-Woods/govee-ble-local.git"
|
|
78
|
+
# or, from a clone (editable):
|
|
79
|
+
git clone https://github.com/Brady-Woods/govee-ble-local.git
|
|
80
|
+
cd govee-ble-local && pip install -e .
|
|
81
|
+
# optional Govee cloud-account helpers (pulls in aiohttp):
|
|
82
|
+
pip install 'govee-ble-local[cloud]'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Requires Python 3.11+. Runtime dependencies are deliberately minimal and Home-Assistant-friendly:
|
|
86
|
+
`bleak`, `bleak-retry-connector`, `cryptography` (all shipped with HA), plus `kaitaistruct` (a
|
|
87
|
+
pure-Python, stdlib-only wire-parser runtime). Dev extras: `.[dev]` (everything a contributor needs), or individually `.[test]`, `.[typing]`,
|
|
88
|
+
`.[lint]`, `.[audit]`, `.[docs]`.
|
|
89
|
+
|
|
90
|
+
## Quick start
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import asyncio
|
|
94
|
+
from govee_ble_local import create_device, discover
|
|
95
|
+
|
|
96
|
+
async def main():
|
|
97
|
+
# Active-scan for supported Govee devices (strongest advertisement per address).
|
|
98
|
+
devices = await discover(timeout=8.0)
|
|
99
|
+
found = next(d for d in devices if d.sku == "H60A6")
|
|
100
|
+
|
|
101
|
+
dev = create_device(found.ble_device, found.sku)
|
|
102
|
+
|
|
103
|
+
await dev.turn_on()
|
|
104
|
+
await dev.set_brightness(60) # percent
|
|
105
|
+
await dev.set_rgb((255, 0, 0))
|
|
106
|
+
await dev.set_zone_power("background", False)
|
|
107
|
+
|
|
108
|
+
state = await dev.update() # read-back into dev.state
|
|
109
|
+
print(state.is_on, state.brightness, state.rgb_color, state.segments)
|
|
110
|
+
|
|
111
|
+
await dev.stop()
|
|
112
|
+
|
|
113
|
+
asyncio.run(main())
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
The library follows Home-Assistant BLE-library conventions: identify a device from its
|
|
119
|
+
advertisement, build a capability-driven `Device`, then drive it.
|
|
120
|
+
|
|
121
|
+
- **`discover(timeout=10.0, *, supported_only=True)`** — active-scan → `list[DiscoveredDevice]`
|
|
122
|
+
(`.ble_device`, `.advertisement`, `.sku`, `.rssi`, `.supported`). `supported(name, mfg)` and
|
|
123
|
+
`match(ble_device, adv)` are the passive-matcher hooks for a HA-style scanner.
|
|
124
|
+
- **`create_device(ble_device, sku, advertisement_data=None, *, secret=None, frame_log=None)`**
|
|
125
|
+
— construct a `Device` for the SKU (raises `GoveeBleNotSupported` if uncurated).
|
|
126
|
+
`is_supported_sku(sku)` / `supported_skus()` / `device_profile_for(sku)` query the table.
|
|
127
|
+
- **`Device`** — one capability-gated class (behaviour comes from its `DeviceProfile`, not
|
|
128
|
+
subclasses). Commands are ACK-confirmed on the wire:
|
|
129
|
+
`turn_on` / `turn_off` / `set_power`, `set_brightness`, `set_rgb`, `set_color_temp`,
|
|
130
|
+
`set_segment_rgb` / `set_segment_brightness` / `set_segment_color_temp`,
|
|
131
|
+
`set_zone_power` / `set_zone_rgb` / `set_zone_color_temp`,
|
|
132
|
+
`set_scene` / `set_scene_by_name`, and `update()` (read-back). Introspection:
|
|
133
|
+
`capabilities`, `zones`, `min_kelvin` / `max_kelvin`, `scene_names`, `state`,
|
|
134
|
+
`register_callback`. Methods for unsupported capabilities raise `GoveeBleNotSupported`.
|
|
135
|
+
- **`DeviceState`** — parsed state: `is_on`, `brightness` (0–100), `rgb_color`,
|
|
136
|
+
`color_temp_kelvin`, `segments` (`list[Segment]`), `zone_power`, `scene_code`, plus
|
|
137
|
+
device-info (`serial_number`, `wifi_mac`, `hardware_version`, `firmware_version`, `ble_mac`).
|
|
138
|
+
- **`Capability`** — `POWER`, `BRIGHTNESS`, `RGB`, `COLOR_TEMP`, `SEGMENTS`, `SCENES`.
|
|
139
|
+
|
|
140
|
+
Curated SKUs: **H60A6**, **H6047**, **H61A8**, **H6006/H6008**, **H6052**, **H6641**, and the
|
|
141
|
+
plug family (**H5080/H5082/H5083/H5085/H5089/H5160/H5161**). Segment-colour read-back and some
|
|
142
|
+
scene dialects on the non-H60A6 SKUs are source-modelled and not yet hardware-verified.
|
|
143
|
+
|
|
144
|
+
The full API reference is generated from the docstrings with [`pdoc`](https://pdoc.dev)
|
|
145
|
+
(`pip install '.[docs]' && bash tools/gen_docs.sh` → `docs/api/`); `govee_ble_local.__all__` is the
|
|
146
|
+
public contract.
|
|
147
|
+
|
|
148
|
+
## Security notes
|
|
149
|
+
|
|
150
|
+
The Govee BLE protocol itself has well-known limits that this library cannot fix:
|
|
151
|
+
|
|
152
|
+
- Its encryption uses a public pre-shared key, AES-ECB for the handshake and an RC4 stream for
|
|
153
|
+
traffic, and integrity is only an XOR checksum. Anyone within radio range can drive an
|
|
154
|
+
encrypted device.
|
|
155
|
+
- Captures made with `GOVEE_FRAME_LOG` contain wire frames from which the session key can be
|
|
156
|
+
derived. Treat them as sensitive and do not post them publicly. Frame-log files are created
|
|
157
|
+
with owner-only permissions.
|
|
158
|
+
|
|
159
|
+
## Diagnostics & session capture
|
|
160
|
+
|
|
161
|
+
The library logs under the `govee_ble_local.*` hierarchy and can capture a full protocol
|
|
162
|
+
session (over a local adapter or a Home Assistant Bluetooth proxy) for analysis. Enable the
|
|
163
|
+
flow trace, or the per-frame firehose, and decode a capture with the bundled
|
|
164
|
+
`govee-ble-analyze` CLI. See [`docs/DIAGNOSTICS.md`](docs/DIAGNOSTICS.md).
|
|
165
|
+
|
|
166
|
+
## Protocol
|
|
167
|
+
|
|
168
|
+
The wire protocol is specified as Kaitai Struct definitions — [`spec/govee_ble.ksy`](spec/govee_ble.ksy)
|
|
169
|
+
(command/reply frames) and [`spec/govee_adv.ksy`](spec/govee_adv.ksy) (advertisements) — with the
|
|
170
|
+
per-device table in [`spec/devices.yaml`](spec/devices.yaml). The runtime readers under
|
|
171
|
+
`govee_ble_local/_generated/` are generated from those (source of truth: the decompiled Govee app).
|
|
172
|
+
|
|
173
|
+
## Contributing & versioning
|
|
174
|
+
|
|
175
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, branching, commit, and release
|
|
176
|
+
conventions. Changes are tracked in [`CHANGELOG.md`](CHANGELOG.md); the project follows
|
|
177
|
+
[Semantic Versioning](https://semver.org/). Live hardware tests run against the curated SKUs above.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
[MIT](LICENSE) © Brady Woods
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# govee-ble-local
|
|
2
|
+
|
|
3
|
+
**Local Bluetooth LE control of Govee devices — no cloud, no LAN API.**
|
|
4
|
+
|
|
5
|
+
A standalone Python library implementing Govee's BLE control protocol (AES-ECB+RC4
|
|
6
|
+
handshake, 20-byte command framing, multi-packet scene upload, and status read-back),
|
|
7
|
+
derived from the decompiled Govee Home app and verified against real hardware. It's the
|
|
8
|
+
protocol engine behind the
|
|
9
|
+
[`hass-govee-ble-local`](https://github.com/Brady-Woods/hass-govee-ble-local)
|
|
10
|
+
Home Assistant integration, but has no Home Assistant dependency and can be used on its own.
|
|
11
|
+
|
|
12
|
+
> ⚠️ **Unofficial.** Not affiliated with or endorsed by Govee. Reverse-engineered from the
|
|
13
|
+
> app + hardware; behavior may change with device firmware. Live-verified on the **H60A6**
|
|
14
|
+
> (Ceiling Light Pro); the other curated SKUs are modelled from the app and are not all
|
|
15
|
+
> hardware-verified (see the profile notes).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Not on PyPI yet — install from source:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install "git+https://github.com/Brady-Woods/govee-ble-local.git"
|
|
23
|
+
# or, from a clone (editable):
|
|
24
|
+
git clone https://github.com/Brady-Woods/govee-ble-local.git
|
|
25
|
+
cd govee-ble-local && pip install -e .
|
|
26
|
+
# optional Govee cloud-account helpers (pulls in aiohttp):
|
|
27
|
+
pip install 'govee-ble-local[cloud]'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Python 3.11+. Runtime dependencies are deliberately minimal and Home-Assistant-friendly:
|
|
31
|
+
`bleak`, `bleak-retry-connector`, `cryptography` (all shipped with HA), plus `kaitaistruct` (a
|
|
32
|
+
pure-Python, stdlib-only wire-parser runtime). Dev extras: `.[dev]` (everything a contributor needs), or individually `.[test]`, `.[typing]`,
|
|
33
|
+
`.[lint]`, `.[audit]`, `.[docs]`.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from govee_ble_local import create_device, discover
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
# Active-scan for supported Govee devices (strongest advertisement per address).
|
|
43
|
+
devices = await discover(timeout=8.0)
|
|
44
|
+
found = next(d for d in devices if d.sku == "H60A6")
|
|
45
|
+
|
|
46
|
+
dev = create_device(found.ble_device, found.sku)
|
|
47
|
+
|
|
48
|
+
await dev.turn_on()
|
|
49
|
+
await dev.set_brightness(60) # percent
|
|
50
|
+
await dev.set_rgb((255, 0, 0))
|
|
51
|
+
await dev.set_zone_power("background", False)
|
|
52
|
+
|
|
53
|
+
state = await dev.update() # read-back into dev.state
|
|
54
|
+
print(state.is_on, state.brightness, state.rgb_color, state.segments)
|
|
55
|
+
|
|
56
|
+
await dev.stop()
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
The library follows Home-Assistant BLE-library conventions: identify a device from its
|
|
64
|
+
advertisement, build a capability-driven `Device`, then drive it.
|
|
65
|
+
|
|
66
|
+
- **`discover(timeout=10.0, *, supported_only=True)`** — active-scan → `list[DiscoveredDevice]`
|
|
67
|
+
(`.ble_device`, `.advertisement`, `.sku`, `.rssi`, `.supported`). `supported(name, mfg)` and
|
|
68
|
+
`match(ble_device, adv)` are the passive-matcher hooks for a HA-style scanner.
|
|
69
|
+
- **`create_device(ble_device, sku, advertisement_data=None, *, secret=None, frame_log=None)`**
|
|
70
|
+
— construct a `Device` for the SKU (raises `GoveeBleNotSupported` if uncurated).
|
|
71
|
+
`is_supported_sku(sku)` / `supported_skus()` / `device_profile_for(sku)` query the table.
|
|
72
|
+
- **`Device`** — one capability-gated class (behaviour comes from its `DeviceProfile`, not
|
|
73
|
+
subclasses). Commands are ACK-confirmed on the wire:
|
|
74
|
+
`turn_on` / `turn_off` / `set_power`, `set_brightness`, `set_rgb`, `set_color_temp`,
|
|
75
|
+
`set_segment_rgb` / `set_segment_brightness` / `set_segment_color_temp`,
|
|
76
|
+
`set_zone_power` / `set_zone_rgb` / `set_zone_color_temp`,
|
|
77
|
+
`set_scene` / `set_scene_by_name`, and `update()` (read-back). Introspection:
|
|
78
|
+
`capabilities`, `zones`, `min_kelvin` / `max_kelvin`, `scene_names`, `state`,
|
|
79
|
+
`register_callback`. Methods for unsupported capabilities raise `GoveeBleNotSupported`.
|
|
80
|
+
- **`DeviceState`** — parsed state: `is_on`, `brightness` (0–100), `rgb_color`,
|
|
81
|
+
`color_temp_kelvin`, `segments` (`list[Segment]`), `zone_power`, `scene_code`, plus
|
|
82
|
+
device-info (`serial_number`, `wifi_mac`, `hardware_version`, `firmware_version`, `ble_mac`).
|
|
83
|
+
- **`Capability`** — `POWER`, `BRIGHTNESS`, `RGB`, `COLOR_TEMP`, `SEGMENTS`, `SCENES`.
|
|
84
|
+
|
|
85
|
+
Curated SKUs: **H60A6**, **H6047**, **H61A8**, **H6006/H6008**, **H6052**, **H6641**, and the
|
|
86
|
+
plug family (**H5080/H5082/H5083/H5085/H5089/H5160/H5161**). Segment-colour read-back and some
|
|
87
|
+
scene dialects on the non-H60A6 SKUs are source-modelled and not yet hardware-verified.
|
|
88
|
+
|
|
89
|
+
The full API reference is generated from the docstrings with [`pdoc`](https://pdoc.dev)
|
|
90
|
+
(`pip install '.[docs]' && bash tools/gen_docs.sh` → `docs/api/`); `govee_ble_local.__all__` is the
|
|
91
|
+
public contract.
|
|
92
|
+
|
|
93
|
+
## Security notes
|
|
94
|
+
|
|
95
|
+
The Govee BLE protocol itself has well-known limits that this library cannot fix:
|
|
96
|
+
|
|
97
|
+
- Its encryption uses a public pre-shared key, AES-ECB for the handshake and an RC4 stream for
|
|
98
|
+
traffic, and integrity is only an XOR checksum. Anyone within radio range can drive an
|
|
99
|
+
encrypted device.
|
|
100
|
+
- Captures made with `GOVEE_FRAME_LOG` contain wire frames from which the session key can be
|
|
101
|
+
derived. Treat them as sensitive and do not post them publicly. Frame-log files are created
|
|
102
|
+
with owner-only permissions.
|
|
103
|
+
|
|
104
|
+
## Diagnostics & session capture
|
|
105
|
+
|
|
106
|
+
The library logs under the `govee_ble_local.*` hierarchy and can capture a full protocol
|
|
107
|
+
session (over a local adapter or a Home Assistant Bluetooth proxy) for analysis. Enable the
|
|
108
|
+
flow trace, or the per-frame firehose, and decode a capture with the bundled
|
|
109
|
+
`govee-ble-analyze` CLI. See [`docs/DIAGNOSTICS.md`](docs/DIAGNOSTICS.md).
|
|
110
|
+
|
|
111
|
+
## Protocol
|
|
112
|
+
|
|
113
|
+
The wire protocol is specified as Kaitai Struct definitions — [`spec/govee_ble.ksy`](spec/govee_ble.ksy)
|
|
114
|
+
(command/reply frames) and [`spec/govee_adv.ksy`](spec/govee_adv.ksy) (advertisements) — with the
|
|
115
|
+
per-device table in [`spec/devices.yaml`](spec/devices.yaml). The runtime readers under
|
|
116
|
+
`govee_ble_local/_generated/` are generated from those (source of truth: the decompiled Govee app).
|
|
117
|
+
|
|
118
|
+
## Contributing & versioning
|
|
119
|
+
|
|
120
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, branching, commit, and release
|
|
121
|
+
conventions. Changes are tracked in [`CHANGELOG.md`](CHANGELOG.md); the project follows
|
|
122
|
+
[Semantic Versioning](https://semver.org/). Live hardware tests run against the curated SKUs above.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
[MIT](LICENSE) © Brady Woods
|