Skip to content

LAN scene encoding hardcodes the 0x02 model-specific byte; H6052 (Aura Table Lamp) needs a different rule #666

Description

@pepa-husek

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:

  1. 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.
  2. 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:

  1. 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
  2. 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).
  3. 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.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions