Skip to main content

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

Where the LTMK comes from

The offline path above needs the account LTMK. Two sources, one caveat:

  • Cloud, once (recommended): auth.fetch_ltmk(device_id) — a single authenticated read of your device's key. This is the key the current pairing actually uses.
  • Bind-ceremony derivation (derive_ltmk_over_ble, derive_account_ltmk): the library can reproduce the BLE bind KDF (HKDF-SHA256 over the bind ECDH). Caveat — proven live (E50): the non-destructive run (stop_before_confirm=True) uses an ephemeral registration public key, so the key it derives does not match the LTMK the lock has already persisted (MATCH=False). A derivation that yields the usable account LTMK requires a destructive re-bind (sending the CONFIRM), which rotates the lock's keys and breaks the current pairing. For everyday offline control, use fetch_ltmk. See docs/devices/u200/account-bind-ltmk-derivation.md.

It stops before the CONFIRM, so the lock's existing pairing is never rotated.

See tools/account_bind/ for the standalone tool and docs/devices/u200/account-bind-ltmk-derivation.md for the derivation.

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

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.17.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aqara-ble 1.17.4
File Size Uploaded
aqara_ble-1.17.4.tar.gz 189.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aqara-ble 1.17.4
File Interpreter ABI Platform
aqara_ble-1.17.4-py3-none-any.whl Python 3 none any Details

Total release size: 334.4 kB

Release files / aqara_ble-1.17.4.tar.gz

Download URL aqara_ble-1.17.4.tar.gz
Size 189.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a4bfb3a1343f5e732012175ec0c1807b1169ace902466c76aa0c681f54e2f2fa
BLAKE2b-256 checksum
How to use checksums
6ea9ecc2b651c46497e77f56308710562a57a84ed4ab82eea208c2e4f5d0e581
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 15, 2026.

Transparency log

Release files / aqara_ble-1.17.4-py3-none-any.whl

Download URL aqara_ble-1.17.4-py3-none-any.whl
Size 144.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
233bd1148a2a6471058eaa6d02f514afa1a0ff2ceeb9c77e445160fd23e4b8b9
BLAKE2b-256 checksum
How to use checksums
0da77ba02109a2991757e0e37938d8ca4d9fc72b7c15c4aad993a36016299c9d
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 15, 2026.

Transparency log

Release history Release notifications | RSS feed

1.17.6

2 release files

1.17.5

2 release files

This release

1.17.4 This release

2 release files

1.17.3

2 release files

1.17.2

2 release files

1.17.1

2 release files

1.17.0

2 release files

1.9.0

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page