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

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.5
File Size Uploaded
aqara_ble-1.17.5.tar.gz 191.5 kB Details

Built distribution (wheel)

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

Total release size: 337.9 kB

Release files / aqara_ble-1.17.5.tar.gz

Download URL aqara_ble-1.17.5.tar.gz
Size 191.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c05194f44503ee1a84f4687bf8bd5797a6af2ea486b7490880fc05069e560948
BLAKE2b-256 checksum
How to use checksums
e0b88de2f211039d8395c35284332c9f98aa0146951c86302f4b982593a5628b
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.5-py3-none-any.whl

Download URL aqara_ble-1.17.5-py3-none-any.whl
Size 146.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7549595061e8ad513edcfea34a81c2129ed2fd5346767185b0383b00a021eaa2
BLAKE2b-256 checksum
How to use checksums
0c78076689212a54bc0d198c0acd238d4fb4591b62f640324976521cf6a5569f
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

This release

1.17.5 This release

2 release files

1.17.4

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