feat(ble): dynamic-frame negotiation + owner_user_id for hardened firmware (#22) - #49
feat(ble): dynamic-frame negotiation + owner_user_id for hardened firmware (#22)#49kb1ibt wants to merge 4 commits into
Conversation
…flip-dots#42) Fragment reassembly lived inside _process_telemetry_packet and only ran for _TELEMETRY_COMMANDS, so any other multi-fragment session frame (e.g. the C2000 G2 c490 device-info blob) had only its first fragment decrypted and the rest dropped (flip-dots#42). Extract it into a shared _reassemble()/_join_fragments() that runs in _process_notification ahead of the cipher split, so telemetry and unknown session frames share one reassembler regardless of the AES variant (GCM vs CBC). Single vs fragment is decided by the live notification length (ATT_MTU - 3, via the ff09 _FRAME_OVERHEAD) rather than the frag byte, so families that put no frag byte on singles (the A91B2 station) need no per-device override; a short single keeps a 0x11 frag byte only when it is a valid single marker. Runs start only on index 1 and terminate on the <index><total> count (so an exact multiple of the cap, with no short tail, still completes); a partial/cold fragment that cannot decrypt is dropped rather than crashing the notification handler. Adds tests/test_reassembly.py (single-no-frag, 0x11 single, two-fragment, exact-multiple-no-tail, cold index!=1) and gives the mock client a realistic 256-byte MTU so the length gate exercises as it does on device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mware Replace the fixed-replay negotiation frames with live/dynamic frames -- each carries the current timestamp (and the stage-5 confer the local timezone), which newer firmware requires and rejects a stale one -- and add the account owner_user_id binding that hardened Prime devices need before they arm telemetry (without it they ack 09 and withhold updates). This is the shared connection layer that lets the C1000 G2 / C2000 G2 and hardened Prime chargers complete negotiation and stream, rather than being dropped mid-handshake (addresses flip-dots#22). Split out of flip-dots#45 per review; the c490 summary decode, Prime device support, and docs follow as separate PRs. Based on the reassembly PR (flip-dots#48). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The base 4022 confer sent a dynamic local timezone but declared a fixed a5 length of 0x16 (22 bytes), which only matches zones like EST5EDT,M3.2.0,M11.1.0. Any other zone misframes the TLV -- CET-1CEST,M3.5.0,M10.5.0/3 is 26 bytes, GMT0BST,M3.5.0/1,M10.5.0 is 24, JST-9 is 5 -- so the device reads a truncated timezone and parses the remainder as garbage. Compute the length from the string, as the Prime confer in prime_device.py already does. Byte-identical on 22-byte (US Eastern/Mountain) hosts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
| Requirements: | ||
|
|
||
| - `anker-solix-api <https://pypi.org/project/anker-solix-api/>`_ (``pip install anker-solix-api``) |
There was a problem hiding this comment.
anker-solix-api is not published on PyPI, and installing from the git repo (git+https://github.com/thomluther/anker-solix-api) with pip or uv doesn't seem to install all dependencies. It does look like there is some poetry support in the pyproject.toml though.
pkolbus
left a comment
There was a problem hiding this comment.
@kb1ibt, nice work!
@flip-dots this (PR stack) gets my 250W charging station working -- addressing the problem we tried to work through in #17. I'm guessing that it's this PR in particular, although the others probably help.
Some comments on the owner-id collection here.
I did take a pass at improving the script snippet in owner_user_id.rst, as https://gist.github.com/pkolbus/7cdb87b04d6673ae93218ac4cf90b818. uv provides a way to run a script in an isolated env with declared dependencies, and the Anker credentials and country ID are collected as terminal input. It's probably fine to have this collection remain separate and owner ID be a configuration item on the HA side, at least initially. anker_solix_api doesn't have any published packages on PyPI and keeping the script separate reduces the dependency burden and keeps the integration clearly cloud-free.
| import asyncio | ||
|
|
||
| from aiohttp import ClientSession | ||
| from api.api import AnkerSolixApi # anker-solix-api |
There was a problem hiding this comment.
| from api.api import AnkerSolixApi # anker-solix-api | |
| from anker_solix_api.api import AnkerSolixApi |
|
|
||
| async def main() -> None: | ||
| async with ClientSession() as session: | ||
| api = AnkerSolixApi("EMAIL", "PASSWORD", "US", session) |
There was a problem hiding this comment.
Users outside the US may benefit from a bit of discussion about how the countryId should be set...
https://github.com/thomluther/anker-solix-api/blob/9a57579d468281395098ff55c8c61b6541cf9596/src/anker_solix_api/apitypes.py#L23 or https://github.com/search?q=repo%3Athomluther/anker-solix-api%20API_COUNTRIES&type=code
…dance, link the page Review feedback from @pkolbus on flip-dots#49; all three points verified before changing anything: - anker-solix-api is NOT on PyPI (pypi.org/pypi/anker-solix-api returns 404), so both 'pip install anker-solix-api' and the PyPI link were wrong. It is a Poetry project and a plain pip install from git does not pull its dependencies, so document clone + poetry install, plus a uv inline-metadata script that resolves the declared deps into a throwaway env -- nothing lands in the system or HA Python. The uv approach and prompting for credentials are @pkolbus's suggestions. - The import 'from api.api import AnkerSolixApi' does not resolve (the package is anker_solix_api), so the snippet failed on copy-paste. Now 'from anker_solix_api.api import AnkerSolixApi'. - Added a Country code section: the third constructor argument selects the API server region, so a wrong value fails the login outright rather than returning a different result. Points at API_COUNTRIES in apitypes.py for the valid codes. Also adds the page to the index toctree -- it was introduced here without one, so Sphinx warned 'document isn't included in any toctree' and the page was unreachable by navigation. Verified with the declared sphinx 8.2/rtd-theme 3.1.0: no warnings from this file, the page links from index, and the countryid anchor resolves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks — all three were right, and one was a real copy-paste breaker. Fixed in
Also noticed while building the docs that the page wasn't in any toctree, so Sphinx warned and it was unreachable by navigation — added it in the same commit. Agreed it should stay separate, and for a slightly broader reason than HA: SolixBLE gets consumed outside Home Assistant too. Mine is a standalone collector that ships both cloud-MQTT and local-BLE telemetry into InfluxDB line protocol — it takes On #17 — glad the stack fixed it. For the record, the |
Split out of #45 per review — the shared connection layer.
Replaces the fixed-replay negotiation frames with live/dynamic frames — each carries the current timestamp (and the stage-5 confer the local timezone), which newer firmware requires and rejects a stale one — and adds the account
owner_user_idbinding that hardened Prime devices need before they arm telemetry (without it they ack09and withhold updates).This is what lets the C1000 G2 / C2000 G2 and hardened Prime chargers complete negotiation and stream instead of being dropped mid-handshake — the disconnect reported in #22.
Stacked on #48 (reassembly). The c490 summary decode, Prime device support, and docs follow as separate PRs on top of this one. 77 tests pass.