Summary
The current SetSceneCode::encode in src/ble.rs doesn't know the target SKU and hardcodes a single byte of the "pre-chunking" header:
https://github.com/wez/govee2mqtt/blob/main/src/ble.rs#L380
let mut data = vec![0xa3, 0x00, 0x01, 0x00 /* line count */, 0x02];
// ^^^^ model-specific
That 0x02 happens to match the hex_prefix_add value in AlgoClaw's per-SKU spec and justabaka's model_specific_parameters.json for roughly a dozen SKUs (H6039, H6072, H6167, H6172, H619C, H61A2, H61A8, H7039, H7075, H70C2, H805A, ...), so it likely works well for most users in practice. For a few SKUs, including H6052 (Aura Table Lamp), the correct byte is different and the catalog's scenceParam also needs a small adjustment before the chunking loop.
Not a bug report: the current code is fine for every SKU it was tested against. This is a datapoint and a question about whether you'd be open to a small refactor that makes the header construction SKU-aware.
H6052 datapoint
Reverse-engineered by capturing the Govee Android app's GATT writes via Android's Bluetooth HCI snoop log and diffing the observed bytes against the output of a re-implementation of the AlgoClaw algorithm. Firmware 2.00.19.
The rule, expressed in the same vocabulary as justabaka's JSON:
hex_prefix_remove = 01 11 (strip these two bytes off the front of the decoded scenceParam)
hex_prefix_add = 07 (prepend this byte to the stripped payload)
insert_scene_type_byte = false (don't insert sceneType between 01 <num_lines> and the payload; the 07 effectively takes that slot)
Concrete example - Sunrise, sceneType=3, sceneCode=300:
scenceParam (b64-decoded, first 16 bytes):
01 11 09 00 64 00 00 00 08 0c 8b 00 ff 48 49 4a
what the Govee app writes over GATT (first sub-command, incl. a3/idx/xor):
a3 00 01 0b 07 09 00 64 00 00 00 08 0c 8b 00 ff 48 49 4a d9
^^^^^ ^^ ^^^^^
01 <n=11> ^ stripped `01 11` absent
07 = hex_prefix_add, not sceneType (which is 3)
what `SetSceneCode::encode` would emit today (same scene):
a3 00 01 0b 02 01 11 09 00 64 00 00 00 08 0c 8b 00 ff 48 ..
^^ ^^^^^
02 hardcoded; `01 11` not stripped
Matching 4 separate multi-line scenes (Sunrise, Sunset, Feather, Lightning, all sceneType=3) byte-for-byte against a btsnoop capture of the Govee app confirms the rule; all four render correctly on the lamp when the rule is applied.
Two caveats I'd want to flag if this moves forward:
- H6052 also has
sceneType=5 scenes (Heartbeat, Firefly, ...) whose scenceParam starts with 13 01 rather than 01 11. I don't have BT captures of those yet, so whether the 07 prefix still applies or there's a second per-sceneType rule is unknown. A fallback that applies only to payloads actually starting with 01 11 would be the safe default.
- Govee occasionally changes framing across firmware updates for a given SKU, so any per-SKU table should be easy to extend.
Proposed direction (happy to PR)
The minimal shape I'd propose:
SetSceneCode::new takes the SKU (you already have it at the callsite in src/lan_api.rs::set_scene_by_name):
https://github.com/wez/govee2mqtt/blob/main/src/lan_api.rs#L263
- A small
SkuSceneRule struct with hex_prefix_remove: &'static [u8], hex_prefix_add: &'static [u8], insert_scene_type_byte: bool (and maybe a scene_type_byte_override: Option<u8> to preserve the existing behaviour where 0x02 is effectively hardcoded).
- A static
phf / HashMap from SKU string to rule, with a default rule that reproduces the current encode output byte-for-byte - so every SKU currently working stays working.
- H6052 entry, plus whatever existing mental model you have for which SKUs are "the
02 family".
I'd keep it scoped tightly: no new dependencies, no behaviour change for any existing SKU, just the extra SKU parameter plumbed through and the table consulted. Snapshot-test the "no-op for today's SKUs" case with whatever bytes test-data/ already contains.
Does an approach along those lines sound reasonable, or would you rather land this as a config knob in the YAML / add a snapshot test only / do something different? Happy to follow the repo's conventions, just didn't want to spring a refactor PR on you unsolicited.
References / prior art
Summary
The current
SetSceneCode::encodeinsrc/ble.rsdoesn't know the target SKU and hardcodes a single byte of the "pre-chunking" header:https://github.com/wez/govee2mqtt/blob/main/src/ble.rs#L380
That
0x02happens to match thehex_prefix_addvalue in AlgoClaw's per-SKU spec and justabaka'smodel_specific_parameters.jsonfor roughly a dozen SKUs (H6039, H6072, H6167, H6172, H619C, H61A2, H61A8, H7039, H7075, H70C2, H805A, ...), so it likely works well for most users in practice. For a few SKUs, including H6052 (Aura Table Lamp), the correct byte is different and the catalog'sscenceParamalso needs a small adjustment before the chunking loop.Not a bug report: the current code is fine for every SKU it was tested against. This is a datapoint and a question about whether you'd be open to a small refactor that makes the header construction SKU-aware.
H6052 datapoint
Reverse-engineered by capturing the Govee Android app's GATT writes via Android's Bluetooth HCI snoop log and diffing the observed bytes against the output of a re-implementation of the AlgoClaw algorithm. Firmware 2.00.19.
The rule, expressed in the same vocabulary as justabaka's JSON:
hex_prefix_remove = 01 11(strip these two bytes off the front of the decodedscenceParam)hex_prefix_add = 07(prepend this byte to the stripped payload)insert_scene_type_byte = false(don't insertsceneTypebetween01 <num_lines>and the payload; the07effectively takes that slot)Concrete example - Sunrise, sceneType=3, sceneCode=300:
Matching 4 separate multi-line scenes (Sunrise, Sunset, Feather, Lightning, all
sceneType=3) byte-for-byte against a btsnoop capture of the Govee app confirms the rule; all four render correctly on the lamp when the rule is applied.Two caveats I'd want to flag if this moves forward:
sceneType=5scenes (Heartbeat, Firefly, ...) whosescenceParamstarts with13 01rather than01 11. I don't have BT captures of those yet, so whether the07prefix still applies or there's a second per-sceneType rule is unknown. A fallback that applies only to payloads actually starting with01 11would be the safe default.Proposed direction (happy to PR)
The minimal shape I'd propose:
SetSceneCode::newtakes the SKU (you already have it at the callsite insrc/lan_api.rs::set_scene_by_name):https://github.com/wez/govee2mqtt/blob/main/src/lan_api.rs#L263
SkuSceneRulestruct withhex_prefix_remove: &'static [u8],hex_prefix_add: &'static [u8],insert_scene_type_byte: bool(and maybe ascene_type_byte_override: Option<u8>to preserve the existing behaviour where0x02is effectively hardcoded).phf/HashMapfrom SKU string to rule, with a default rule that reproduces the currentencodeoutput byte-for-byte - so every SKU currently working stays working.02family".I'd keep it scoped tightly: no new dependencies, no behaviour change for any existing SKU, just the extra SKU parameter plumbed through and the table consulted. Snapshot-test the "no-op for today's SKUs" case with whatever bytes
test-data/already contains.Does an approach along those lines sound reasonable, or would you rather land this as a config knob in the YAML / add a snapshot test only / do something different? Happy to follow the repo's conventions, just didn't want to spring a refactor PR on you unsolicited.
References / prior art
model_specific_parameters.json- happy to link the discussion once it's filed).