Aqara BLE
Autonomous control of Aqara Bluetooth Low Energy locks from Python — no app, no phone — reconstructed by reverse-engineering the official application's observed behaviour. The U200 is the fully solved reference device.
Goals
- Document the Aqara BLE + cloud protocol precisely enough to reproduce it.
- Provide a cross-platform Python library that exposes the lock's full operation surface, offline where the hardware allows.
- Enable a native Home Assistant integration (the primary target —
haos_aqara). - Make porting to other Aqara-family devices a methodical, repeatable process.
Cloud vs. local
The U200 is a Bluetooth lock (no Wi-Fi); all control is BLE. The cloud is only a key source: session keys are derived either from the cloud per operation, or locally from the device's LTMK (long-term master key), fetched from your account once. There is no fully cloud-free first contact — identifying the lock and getting the LTMK need one authenticated cloud read — but after that, control (lock/unlock, state, battery, settings, credentials, access log) is 100% local. Only the spoken-language voice OTA needs the cloud again (to download the audio pack). The LTMK cannot be faked; it must be the real per-device key from your account.
Quick start
Two ways in — a terminal command and an importable API. Integrations couple to the API; the CLI is a thin adapter over that same API.
Terminal — the aqara command
pip install -e . # puts `aqara` on your PATH
aqara login # account login only (no radio)
aqara scan --transport bleak
aqara lock --transport bumble --port serial:/dev/cu.usbmodemNNNN,115200
aqara unlock ; aqara operate keepalive
Only account + password + device_id are needed — from --account/--password
or the environment/.env (AQARA_ACCOUNT, AQARA_PASSWORD, AQARA_DEVICE_ID;
AQARA_REGION optional). The app-global appid/appkey are baked in and
client_id/phone_id are generated per install, so they are optional overrides,
not requirements.
Library — cloud-assisted sessions
from aqara_ble import BleakTransport, CloudAuthManager, U200Client
auth = CloudAuthManager(account="me@example.com", password="…") # only these two
async with await U200Client.connect(
auth=auth, transport=BleakTransport(), device_id="lumi1.xxxx"
) as lock:
await lock.lock()
print(await lock.battery())
The facade logs in (and re-authenticates on token expiry), scans and identifies
the lock by what it advertises, connects, discovers services and runs the
authenticated operation. Swap BleakTransport() for
BumbleTransport("serial:/dev/…") to drive an ESP32‑S3 controller
(firmware).
Library — offline (cloud-cut) sessions
Fetch the LTMK once, then derive every session locally with no per-operation cloud call:
ltmk = auth.fetch_ltmk("lumi1.xxxx") # one cloud read; keep it in memory
async with await U200Client.connect(
auth=auth, transport=BleakTransport(), device_id="lumi1.xxxx",
ltmk=ltmk, login_first=False,
) as lock:
await lock.unlock() # no cloud touched
Operation surface (U200)
All BLE, offline-capable (the LTMK path above). Most operations run against the always-on back panel; a few are served by the front keypad panel, which sleeps and must be awake for them (see the presence note).
| Group | Methods |
|---|---|
| Control | lock() · unlock() · operate() · status() · listen() (real-time ff62 events) |
| Reads | battery() · read_lock_status() · read_settings() · read_door_type() · read_assist_turn() · read_pull_spring() |
| Settings (write) | set_alert_volume() · set_alarm_volume() · set_alert_delay() · set_verify_fail_time() · set_auto_lockup_delay() · set_auto_lock_on_close_delay() · enable_auxiliary_locking_on_close() · enable_auxiliary_locking_relock() · set_language_english() / set_language_deutsch() |
| Credentials | read_user_table() · add_visitor_password() · delete_user() |
| History | read_access_log() |
| Presence | read_front_connection() (is the keypad awake?) |
| Language OTA | change_language() (cloud voice-pack download + push) |
| Cloud (once) | CloudAuthManager.fetch_ltmk() · account login · device resolve |
Presence: the credential table (read + add/delete) and front-panel settings
are served by the sleeping keypad panel, so they need it awake (a physical touch
or a fingerbot). read_front_connection() reports whether it is; the access log and
lock/unlock/state/battery need no keypad. The voice OTA needs it too (audio → front
speaker). This is a hardware constraint, not policy — see
docs/devices/u200/.
Start here
- docs/ — the documentation entry point (understand · port · diagnose).
- docs/devices/u200/validation.md — run it against a real U200 (facade, transports, troubleshooting).
- docs/devices/u200/library-vs-app.md — capability scorecard vs. the official app (offline).
- docs/architecture.md — how it works end to end and the transversal-vs-device Layer Map.
- docs/porting-guide.md — the numbered process to bring a new device online, with the CRC and login obstacles solved up front.
- CONTRIBUTING.md — Spec-Driven Development workflow and the secret-hygiene rules.
- CHANGELOG.md — released versions (PyPI:
aqara-ble).
Secrets — non-negotiable
No real secret, capture, or app source ever enters this repository. Credentials
and device identifiers live only in a local, git-ignored .env (see
.env.example); raw captures live under a git-ignored captures/
tree. See Constitution Principle I in
.specify/memory/constitution.md.
License
MIT — see LICENSE.
Metadata
Release files for aqara-ble 1.16.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aqara_ble-1.16.0.tar.gz | 170.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aqara_ble-1.16.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 300.3 kB
Release files / aqara_ble-1.16.0.tar.gz
| Download URL | aqara_ble-1.16.0.tar.gz |
|---|---|
| Size | 170.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f2e71e27e4fce8607158f04560a94232bb26c46aafa4a1fa7c9b768002b5c966
|
|
BLAKE2b-256 checksum How to use checksums |
9a89c84ec45fd1793665dd1bfe2f1444cdaac6f696fbf927ccddfffa253dc55c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.
Transparency logRelease files / aqara_ble-1.16.0-py3-none-any.whl
| Download URL | aqara_ble-1.16.0-py3-none-any.whl |
|---|---|
| Size | 129.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
84e7782f84c6a571e7d691f1d425a73f5f7a63114ec38e3c83a5cd396fe9dcd5
|
|
BLAKE2b-256 checksum How to use checksums |
cd5c113d56d02cf96e63f5c1bff9aff40beba0a6b9cbe473cd27c6fd70dfd0ff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.
Transparency log