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.
Files changed (48) hide show
  1. govee_ble_local-1.2.0/CHANGELOG.md +294 -0
  2. govee_ble_local-1.2.0/LICENSE +21 -0
  3. govee_ble_local-1.2.0/MANIFEST.in +11 -0
  4. govee_ble_local-1.2.0/PKG-INFO +181 -0
  5. govee_ble_local-1.2.0/README.md +126 -0
  6. govee_ble_local-1.2.0/pyproject.toml +138 -0
  7. govee_ble_local-1.2.0/setup.cfg +4 -0
  8. govee_ble_local-1.2.0/src/govee_ble_local/__init__.py +69 -0
  9. govee_ble_local-1.2.0/src/govee_ble_local/_generated/__init__.py +1 -0
  10. govee_ble_local-1.2.0/src/govee_ble_local/_generated/govee_advertisement.py +132 -0
  11. govee_ble_local-1.2.0/src/govee_ble_local/_generated/govee_ble_frame.py +3005 -0
  12. govee_ble_local-1.2.0/src/govee_ble_local/cloud/__init__.py +14 -0
  13. govee_ble_local-1.2.0/src/govee_ble_local/cloud/account.py +323 -0
  14. govee_ble_local-1.2.0/src/govee_ble_local/const.py +94 -0
  15. govee_ble_local-1.2.0/src/govee_ble_local/crypto.py +80 -0
  16. govee_ble_local-1.2.0/src/govee_ble_local/debug.py +203 -0
  17. govee_ble_local-1.2.0/src/govee_ble_local/devices/__init__.py +1 -0
  18. govee_ble_local-1.2.0/src/govee_ble_local/devices/device.py +928 -0
  19. govee_ble_local-1.2.0/src/govee_ble_local/devices/profile.py +185 -0
  20. govee_ble_local-1.2.0/src/govee_ble_local/exceptions.py +34 -0
  21. govee_ble_local-1.2.0/src/govee_ble_local/identify.py +115 -0
  22. govee_ble_local-1.2.0/src/govee_ble_local/models.py +91 -0
  23. govee_ble_local-1.2.0/src/govee_ble_local/py.typed +0 -0
  24. govee_ble_local-1.2.0/src/govee_ble_local/registry.py +50 -0
  25. govee_ble_local-1.2.0/src/govee_ble_local/scanner.py +77 -0
  26. govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6006.json +474 -0
  27. govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6008.json +474 -0
  28. govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6047.json +522 -0
  29. govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6052.json +346 -0
  30. govee_ble_local-1.2.0/src/govee_ble_local/scenes/H60A6.json +674 -0
  31. govee_ble_local-1.2.0/src/govee_ble_local/scenes/H61A8.json +1186 -0
  32. govee_ble_local-1.2.0/src/govee_ble_local/scenes/H6641.json +1186 -0
  33. govee_ble_local-1.2.0/src/govee_ble_local/scenes/__init__.py +168 -0
  34. govee_ble_local-1.2.0/src/govee_ble_local/transport/__init__.py +2 -0
  35. govee_ble_local-1.2.0/src/govee_ble_local/transport/connection.py +775 -0
  36. govee_ble_local-1.2.0/src/govee_ble_local/transport/framelog.py +175 -0
  37. govee_ble_local-1.2.0/src/govee_ble_local/transport/handshake.py +62 -0
  38. govee_ble_local-1.2.0/src/govee_ble_local/wire/__init__.py +5 -0
  39. govee_ble_local-1.2.0/src/govee_ble_local/wire/build.py +324 -0
  40. govee_ble_local-1.2.0/src/govee_ble_local/wire/describe.py +210 -0
  41. govee_ble_local-1.2.0/src/govee_ble_local/wire/parse.py +370 -0
  42. govee_ble_local-1.2.0/src/govee_ble_local/wire/reassemble.py +164 -0
  43. govee_ble_local-1.2.0/src/govee_ble_local.egg-info/PKG-INFO +181 -0
  44. govee_ble_local-1.2.0/src/govee_ble_local.egg-info/SOURCES.txt +46 -0
  45. govee_ble_local-1.2.0/src/govee_ble_local.egg-info/dependency_links.txt +1 -0
  46. govee_ble_local-1.2.0/src/govee_ble_local.egg-info/entry_points.txt +2 -0
  47. govee_ble_local-1.2.0/src/govee_ble_local.egg-info/requires.txt +36 -0
  48. 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