Conversation
The Anker manufacturer record (company id 0xffff) carries the device MAC, model, and a capability byte declaring which negotiation path the device accepts -- readable at scan time, before any frame is sent. Add a parser for it as the basis for choosing the cleartext vs encrypted handshake per device rather than by product class. Capability is length-relative (last byte when present, absent on the F3800), so it is derived from the sku length rather than read at a fixed offset. Decoded against the app's own field values for five bench records. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the AES-GCM negotiation out of PrimeDevice into SolixBLEDevice so any device can take it. The path is selected from the capability byte in the device's advertisement when the caller passes one, otherwise from the class default (encrypted for Prime devices, plain text for the Solix power stations). The device's declared MTU and auth mode from 4803 are echoed in 4005, and the timezone confer carries the local UTC offset. A fresh P-256 key pair is generated for every negotiation instead of the hard-coded key pairs; the keys the recorded test vectors were captured with move to the test constants and are pinned by the fake_time fixture. The cipher and ECDH primitives live in utilities as plain functions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Confirming this is needed on the C1000 Gen 2 (A1763) as well. Setup: Home Assistant 2026.9.1 (Docker), BlueZ, Raspberry Pi onboard BT (BCM43438), SolixBLE 4.0.0b1. Advertisement: manufacturer data (company With 4.0.0b1 (plaintext path): GATT connect and notify subscription succeed, but ~200 ms after the first With the Prime/GCM handshake: I used a quick subclass Two observations that may be useful:
So the C1000 G2 on current firmware needs the capability-byte selection from this PR. Happy to test a branch on hardware. |
|
|
||
| .. _BLEDevice: https://bleak.readthedocs.io/en/latest/api/index.html#bleak.backends.device.BLEDevice/ | ||
|
|
||
| Anker devices share one packet format but negotiate a session in more than one |
There was a problem hiding this comment.
This is annoyingly not always true, it might be a good idea to mention #64 here.
| capability: int | None | ||
|
|
||
|
|
||
| def parse_manufacturer_record(data: bytes) -> AnkerAdvertisement | None: |
There was a problem hiding this comment.
It would be good if this used construct like is used for packet parsing since it combines the parsing and structure of the data into one thing.
In theory it could be as simple as:
format = Struct(
"version_code" / Int8ub,
"mac" / Bytes(6),
"bind_type" / Int8ub,
"product_type" / Bytes(2),
"sku" / Switch(
this.version_code,
{
1: String(3, encoding="ascii"),
2: String(4, encoding="ascii"),
},
),
"capability" / Optional(Int8ub),
)
parsed = format.parse(data)
print(parsed.mac)I got this by pasting it into the free Google AI so its probably wrong, but you get the idea, its way neater than manually parsing stuff and using data classes.
| """Initialise device object. Does not connect automatically.""" | ||
| #: Whether to negotiate on the encrypted (AES-GCM, ``4xxx``) path when the | ||
| #: advertised capability byte is not known. A known capability overrides it. | ||
| _DEFAULT_ENCRYPTED_NEGOTIATION: bool = False |
There was a problem hiding this comment.
I don't like the idea of using a bool for this since I expect more Anker shenanigans to change things up yet again in future for which a bool would not be enough.
flip-dots
left a comment
There was a problem hiding this comment.
This is way more digestible, I appreciate you taking the time to break things up.
I don't love the idea of having the device.py class handle the negotiation for Prime and Solix devices in itself, my idea behind having a separate class for Prime devices was to allow it to share the same interface as before but still support the different way these protocols operate and allow me to make changes to the prime protocol without risking breaking support for Solix devices, it also has the benefit of reducing merge conflicts.
Now that its clear there are even more protocols I think it might be a good idea to make device.py an interface only which just defines function definitions and then move the Solix style protocol into its own file and each protocol variant can have its own class which devices can choose to inherit from.
It should be possible to support multiple protocols on the same physical device by having a class which just implements the properties for each (power, battery, etc) which is then inherited by a class which inherits from that and a protocol.
I think it should then be possible to make a class/function which takes the advertisement data and decides what type of device it is and what protocol to use and returns the correct class.
Any thoughts? I am reasonably confident its a better approach but I don’t have any experience with the newer protocol variant which could throw a wrench into the works of this plan.
|
Replying to @flip-dots:
I took your suggestions and the inline review and restructured #70 into four stacked PRs; two are up. #77 is a bug fix:
The remaining two PRs handle the negotiation itself. The outer protocol is plain (CBC session) or encrypted (static GCM, then dynamic GCM). The path inside it is chosen by the device's own capability reply: ECDH, legacy AES (#63), or an On interface vs inheritance: I went with composition rather than one class per protocol that devices inherit. The protocol turned out to belong to the device's firmware, not its model. A C2000 G2 on v0.3.3.0 refuses the plain handshake that older units accept, and a firmware update can change the answer between two connections. So each device instance holds a negotiated session (outer + path, behind One thing I noticed on |
Replaces the negotiation half of #49, rebuilt from
mainon Harvey's review.Anker devices announce which handshake they accept: the
0xffffmanufacturer record in the advertisement ends in a capability byte whose0x04bit means the encrypted4xxxnegotiation (AES-GCM under a static key until the ECDH exchange, then under the shared secret). The C2000 G2 advertises it and its current comms firmware (v0.3.3.0) rejects the plain-text path; the Prime chargers use it already.SolixBLE/advertisement.py:capability_from_advertisement(advertising_data).SolixBLEDevice(ble_device, capability=None): the capability byte selects the path; without it the class default applies (Prime devices encrypted, everything else plain text), soDeviceClass(ble_device)keeps working for HaSolixBLE.PrimeDeviceinto the base class behind_encrypted_negotiation;PrimeDevicekeeps only its UUID, the encrypted default and its post-authorize commands (4200,420a).gcm_encrypt/gcm_decrypt,cbc_encrypt/cbc_decrypt,generate_ecdh_key,ecdh_public_bytes,ecdh_shared_secret,_offset_seconds_west) live inutilities.py.const.pyand the tests pin their own keys throughfake_time.4803carries the device MTU and auth mode, both echoed back in4005;4022carries the POSIX timezone and the UTC offset in seconds west, which newer firmware validates.docs/source/protocols.rst(per-path sections, as suggested in the feat(prime): A91B2 (240W) + A2345 (250W) live BLE telemetry + control #51 review) replaces the encrypted-negotiation page; the index toctree points at it.Validated on hardware: C2000 G2 (A1783, encrypted path) and Prime Charger 250W (A2345, encrypted path); the plain-text path is covered by the existing recorded frames. Not re-tested on hardware: the 160W charger and the power bank, which inherit the moved handshake.
Suite: 363 passed (main: 345).
🤖 Generated with Claude Code