Metadata-Version: 2.4
Name: aqara-ble
Version: 1.16.0
Summary: Autonomous BLE control library for the Aqara U200 smart lock — cloud KDF, auth handshake and AES-CCM control channel, reimplemented in pure Python.
Author: dani811
Maintainer: dani811
License-Expression: MIT
Project-URL: Homepage, https://github.com/dani811/Aqara
Project-URL: Repository, https://github.com/dani811/Aqara
Project-URL: Issues, https://github.com/dani811/Aqara/issues
Keywords: aqara,u200,smart-lock,bluetooth,ble,reverse-engineering,home-automation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Home Automation
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=43.0.0
Provides-Extra: ble
Requires-Dist: bleak>=0.22; extra == "ble"
Provides-Extra: bumble
Requires-Dist: bumble>=0.0.200; extra == "bumble"
Provides-Extra: dev
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Dynamic: license-file

# 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`](https://github.com/dani811/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

```bash
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

```python
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](tools/esp32s3_hci_usb/README.md)).

### Library — offline (cloud-cut) sessions

Fetch the LTMK once, then derive every session locally with **no per-operation
cloud call**:

```python
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/](docs/devices/u200/).

## Start here

- **[docs/](docs/README.md)** — the documentation entry point (understand · port ·
  diagnose).
- **[docs/devices/u200/validation.md](docs/devices/u200/validation.md)** — run it
  against a real U200 (facade, transports, troubleshooting).
- **[docs/devices/u200/library-vs-app.md](docs/devices/u200/library-vs-app.md)** —
  capability scorecard vs. the official app (offline).
- **[docs/architecture.md](docs/architecture.md)** — how it works end to end and
  the transversal-vs-device Layer Map.
- **[docs/porting-guide.md](docs/porting-guide.md)** — the numbered process to
  bring a new device online, with the CRC and login obstacles solved up front.
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — Spec-Driven Development workflow and the
  secret-hygiene rules.
- **[CHANGELOG.md](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`](.env.example)); raw captures live under a git-ignored `captures/`
tree. See Constitution Principle I in
[`.specify/memory/constitution.md`](.specify/memory/constitution.md).

## License

MIT — see [LICENSE](LICENSE).
