Maintainer Guide — Adding a New Device from Contributor Submissions¶
This guide is for maintainers receiving a my-device.yaml (probe output)
and optionally a draft.yaml (annotation output) or btsnoop_hci.log
from a contributor.
1. Triage¶
Run the validator on the probe output first:
voltkeeper validate-profile my-device.yaml
You’re looking for:
protocolisv1orv2.unknownmeans the contributor’s probe couldn’t reach the device — go back to them, don’t proceed.protocol_versionis non-null. For V1 this is in the 1016–1026 range; V2 is >= 2000.namematches a Bluetti BLE prefix (e.g.AC2A2305000). Confirm the prefix isn’t already in_device_registry().blockshas mostly-non-emptyraw_hexentries. All-zero or all-FF blocks suggest the device wasn’t responding — flag back to contributor.
2. Cross-reference the APK¶
The decompiled APK in bluetti-files/jadx_out/ is authoritative for any
device-class flag the contributor’s data can’t tell you.
grep -n '"<MODEL>"' bluetti-files/jadx_out/sources/net/poweroak/bluetticloud/ui/connect/DeviceConnUtil.java
In the matching new DeviceFunction(...) constructor call, the 6th
positional arg is isLowPower and the 3rd is minProtocolVer.
Other flags worth eyeballing: bmsPack, bmsPackV2, chargingMode,
chgModeCustom, acECOCtrl, dcECOCtrl, factoryReset.
If the contributor’s data disagrees with the APK, the APK wins.
Document the divergence in the new model file’s # ABOUTME: comment.
3. Pick the base class¶
|
Base class |
Notes |
|---|---|---|
|
|
Use V1Base default alarm/fault names |
|
|
Override to |
|
|
Default /10 voltage scale |
|
|
EP600 family |
4. Build the class from a template¶
Pick the closest existing model as your starting point:
V2 with controls: copy
ac60.pyV2 read-only: copy
ep600.pyV1 with controls: copy
ac300.pyV1 minimal: copy
eb3a.pyV1 lowPower variant: copy
ac200l.py
What to fill in:
WRITABLE_FIELD_NAMES: derive from APK’sDeviceFunctionflags_build_control_struct: V1 writable registers in 3000–3099, V2 in 2000–2299_fill_*helpers inparse(): only needed for array/struct fieldsTODO comment: leave
# TODO(<MODEL>): verify against hardware
5. Wire the registry¶
In src/voltkeeper/bluetooth/__init__.py:
Import the new class in
_device_registry()Add
"<PREFIX>": <ClassName>to the returned dictUpdate
_DEVICE_NAME_SN_REto include the new prefix
6. Tests¶
In tests/test_device_registry.py:
Add the prefix to
ALL_PREFIXESIf V1: add to
test_v1_models_are_v1If V2: add to
test_v2_models_protocol_versionIf it has writable fields: confirm via registry tests
Run uv run pytest -q — the parametrized registry tests will exercise
the new class automatically.
7. Decoding encrypted btsnoop captures (V2 only)¶
If the contributor sent btsnoop_hci.log from a V2 device:
Pair voltkeeper against the same physical device the contributor captured from. Patch
src/voltkeeper/bluetooth/handshake.pytemporarily to logshared_key.hex()andinitial_iv.hex()Run
voltkeeper status <ADDR>against the device with-vFeed key and IV into the parser:
python scripts/parse_btsnoop.py contributor.log --key <hex> --iv <hex> > capture.csv
Revert the handshake.py patch — don’t merge the key-logging
8. Merge¶
Verify
uv run pytest,uv run ruff check src/ tests/, anduv run mypy src/voltkeeperall passOpen a PR; lowercase imperative commit message
Reply to the contributor’s issue with the merged PR
If the model is hardware-verified, drop the
TODO: verify against hardwarecomment