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

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.2
File Size Uploaded
aqara_ble-1.17.2.tar.gz 187.0 kB Details

Built distribution (wheel)

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

Total release size: 330.7 kB

Release files / aqara_ble-1.17.2.tar.gz

Download URL aqara_ble-1.17.2.tar.gz
Size 187.0 kB
Tags Source
SHA-256 checksum
How to use checksums
6f609de3ecd738d2a04c8fb08e5baef21342302996489f8c5e1146c3a823c1f3
BLAKE2b-256 checksum
How to use checksums
66b0d4dd5359518db820bb3871fd680737895bcf44f2cad22f32faffacfd16c4
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 14, 2026.

Transparency log

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

Download URL aqara_ble-1.17.2-py3-none-any.whl
Size 143.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ae1b4f3f3c93fad653c0cb7ba253cccac98e4b5cd76561a657325ac4f137950a
BLAKE2b-256 checksum
How to use checksums
de9f68e443aa791231595fc3bc4d5aaebe856011201de2122230c99752f52778
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 14, 2026.

Transparency log

Release history Release notifications | RSS feed

1.17.6

2 release files

1.17.5

2 release files

1.17.4

2 release files

1.17.3

2 release files

This release

1.17.2 This release

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