apple-ble
Read AirPods battery (case / left / right + charging) and detect nearby Apple devices from Python, straight off the unencrypted Apple Continuity proximity-pairing BLE advertisement — no pairing, no connection.
This library powers the
Apple BLE Home Assistant integration,
but works standalone in any Python project (or straight from the command line).
The parser is pure and dependency-free; bleak is only needed for the optional
scanner CLI.
What works: AirPods broadcast their battery in an unencrypted Continuity advert when the lid is opened. What doesn't: Apple Watch / iPhone do not put battery in their adverts — BLE can only tell you an Apple device is nearby. Apple rotates the BLE MAC (~15 min), so there is no stable per-device id.
Features
- 🔋 AirPods battery — case, left, and right levels plus per-pod and case
charging flags, decoded from the manufacturer-
0x004Cproximity-pairing advert. - 👂 In-ear / in-case / lid — which bud is in an ear, how many are in the case, and whether the case lid is open (layout verified on real AirPods Pro 2).
- 🔐 Exact battery with the AirPods keys —
decrypt_status()decrypts the AES half of the advert (1 % battery, charging, connected host) andresolve_rpa()recognises your AirPods behind the rotating address. - 🎧 Model detection — AirPods 1/2/3, Pro, Pro 2, Max.
- 📡 Presence-friendly — exposes the Apple manufacturer id so callers can count nearby Apple devices.
- 🧪 Pure parser —
parse_proximity_pairing(bytes) -> AirPodsData | None, no I/O, trivially unit-testable. - 🖥️ CLI —
apple-blescans and prints any AirPods it sees (needs thecliextra).
Requirements
- Python 3.12 or newer.
- A Bluetooth LE adapter (built-in or USB) — only for live scanning; the parser itself needs nothing.
Installation
pip install apple-ble # core (parsing only)
pip install "apple-ble[cli]" # + bleak scanner CLI
CLI usage
apple-ble # 15s BLE scan; open your AirPods lid nearby and watch the battery
Library usage
from apple_ble import parse_proximity_pairing, APPLE_MANUFACTURER_ID
# `payload` is manufacturer_data[76] from any BLE stack (bleak, Home Assistant, ...)
data = parse_proximity_pairing(payload)
if data:
print(data.model, data.left_battery, data.right_battery, data.case_battery)
print(data.left_charging, data.right_charging, data.case_charging)
With the per-AirPods keys (on a Mac: keychain item MobileBluetooth, account =
AirPods address, fields MagicAccIRK / MagicAccEncKey):
from apple_ble import decrypt_status, parse_key, resolve_rpa
irk, enc = parse_key(irk_text), parse_key(enc_text) # accepts "FE-47-…" or hex
if resolve_rpa(address, irk): # these are *your* AirPods
st = decrypt_status(payload, enc, flipped=data.flipped)
print(st.left_battery, st.right_battery, st.case_battery, st.host_suffix)
parse_proximity_pairing returns None for anything that isn't a complete
AirPods proximity-pairing advert, so it is safe to feed it every Apple advert you
see and keep the non-None results.
API
| Symbol | Description |
|---|---|
parse_proximity_pairing(data) |
Parse manufacturer_data[76] bytes into AirPodsData, or None if it isn't AirPods. |
AirPodsData |
Frozen dataclass: model, left_battery, right_battery, case_battery (int % or None), left_charging, right_charging, case_charging, model_id, flipped, left_in_ear, right_in_ear, pods_in_case, lid_open (None when no bud in the case is visible), color. |
decrypt_status(data, enc_key, *, flipped) |
Decrypt bytes 11–26 → DecryptedStatus (exact %, charging, host_suffix), or None on a wrong key. |
resolve_rpa(address, irk) |
True if the random address was generated from this IRK (keychain byte order). |
parse_key(text) |
Parse a 16-byte key from FE-47-…, fe:47:… or plain hex. |
decode_battery_nibble(nibble) |
Pure helper: a 0x0–0xF battery nibble to a percentage (None when not present). |
APPLE_MANUFACTURER_ID |
76 (0x004C) — Apple's Bluetooth company id. |
MODEL_BY_CHAR |
Mapping of the model nibble to a human model name. |
Credits
Continuity reverse-engineering: furiousMAC/continuity, kavishdevar/librepods, delphiki/AirStatus, d4rken-org/capod. Author @hudsonbrendon.
License
MIT — see LICENSE.
Metadata
Release files for apple-ble 0.2.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 | Provenance | Actions |
|---|---|---|---|---|
| apple_ble-0.2.0.tar.gz | 35.5 kB | Without provenance | Details for apple_ble-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| apple_ble-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for apple_ble-0.2.0-py3-none-any.whl |
Total release size: 46.2 kB
Release files / apple_ble-0.2.0.tar.gz
| Download URL | apple_ble-0.2.0.tar.gz |
|---|---|
| Size | 35.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f2cb539ec4af194297907373e8a828487b97d334b0f23d7e3c75e3a8e207547b
|
|
BLAKE2b-256 checksum How to use checksums |
53d680d44ab0cb3984fce69811e7c6fd223de1d098eec22d8f529e0400d5486b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|
Release files / apple_ble-0.2.0-py3-none-any.whl
| Download URL | apple_ble-0.2.0-py3-none-any.whl |
|---|---|
| Size | 10.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
df04e547fb1ab2eefb72ecd630a68a553015fda6a9b9f59af69eb55d4fcce386
|
|
BLAKE2b-256 checksum How to use checksums |
fe86e15ca96632aaac395dc8f5baeac906cdab528ae3ce2ea0d4fe248695f642
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|
Security for apple-ble 0.2.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.