Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ version: 2
sphinx:
configuration: docs/source/conf.py

# Set the version of Python and other tools you might need
# Set the version of Python and other tools you might need.
# The package requires Python >=3.11 (see pyproject.toml), so the docs build
# must use a compatible interpreter or `pip install .` fails outright.
build:
os: ubuntu-20.04
os: ubuntu-22.04
tools:
python: "3.9"
python: "3.12"

# Optionally declare the Python requirements required to build your docs
python:
Expand Down
86 changes: 86 additions & 0 deletions docs/source/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# API reference

This page documents the public, supported API of `xiaomi_ble`. Everything listed
here is re-exported from the top-level package, so the canonical import is:

```python
from xiaomi_ble import XiaomiBluetoothDeviceData, EncryptionScheme
```

For a task-oriented walkthrough, see {doc}`usage`.

## Parsing advertisements

The heart of the library is {class}`~xiaomi_ble.XiaomiBluetoothDeviceData`. You
feed it `BluetoothServiceInfo` objects (from
[`home-assistant-bluetooth`](https://pypi.org/project/home-assistant-bluetooth/))
and it returns a `SensorUpdate` describing the device and any sensor, binary
sensor and event values decoded from the advertisement.

```{eval-rst}
.. autoclass:: xiaomi_ble.XiaomiBluetoothDeviceData
:members: supported, update, set_bindkey, poll_needed, async_poll
:show-inheritance:
```

### Encryption

Some Xiaomi devices encrypt their MiBeacon payloads and require a per-device
*bindkey*. The encryption scheme is detected automatically from the
advertisement flags and exposed on the
:attr:`~xiaomi_ble.XiaomiBluetoothDeviceData.encryption_scheme` attribute.

```{eval-rst}
.. autoclass:: xiaomi_ble.EncryptionScheme
:members:
:undoc-members:
```

## Retrieving bindkeys from the Xiaomi cloud

Encrypted devices need a bindkey before their payloads can be decoded. The
bindkey can be fetched from the Xiaomi cloud account that the device is paired
with using {class}`~xiaomi_ble.XiaomiCloudTokenFetch`.

```{eval-rst}
.. autoclass:: xiaomi_ble.XiaomiCloudTokenFetch
:members:
:show-inheritance:

.. autoclass:: xiaomi_ble.XiaomiCloudBLEDevice
:members:
:show-inheritance:
```

### Cloud exceptions

All cloud errors derive from {class}`~xiaomi_ble.XiaomiCloudException`, so a
single `except XiaomiCloudException` clause catches every failure mode.

```{eval-rst}
.. autoexception:: xiaomi_ble.XiaomiCloudException
:show-inheritance:

.. autoexception:: xiaomi_ble.XiaomiCloudInvalidAuthenticationException
:show-inheritance:

.. autoexception:: xiaomi_ble.XiaomiCloudInvalidUsernameException
:show-inheritance:

.. autoexception:: xiaomi_ble.XiaomiCloudInvalidPasswordException
:show-inheritance:

.. autoexception:: xiaomi_ble.XiaomiCloudTwoFactorAuthenticationException
:show-inheritance:
```

## Module constants

```{eval-rst}
.. autodata:: xiaomi_ble.SLEEPY_DEVICE_MODELS
:no-value:
```

`SLEEPY_DEVICE_MODELS` is the set of device models that advertise irregularly
(e.g. motion sensors and buttons that only transmit on activity). Consumers can
use it to relax availability timeouts for these "sleepy" devices.
29 changes: 26 additions & 3 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# The package is installed (``pip install .[docs]``) when the docs are built, so
# autodoc can simply import ``xiaomi_ble`` without any sys.path manipulation.
from typing import Any, List

import xiaomi_ble

# -- Project information -----------------------------------------------------

project = "Xiaomi BLE"
copyright = "2020, J. Nick Koston"
author = "J. Nick Koston"
release = xiaomi_ble.__version__
version = release


# -- General configuration ---------------------------------------------------
Expand All @@ -29,8 +32,28 @@
# ones.
extensions = [
"myst_parser",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.napoleon",
"sphinx.ext.viewcode",
"sphinx.ext.intersphinx",
]

# -- Autodoc / autosummary ---------------------------------------------------

autosummary_generate = True
autodoc_member_order = "bysource"
autodoc_typehints = "description"
autodoc_default_options = {
"members": True,
"show-inheritance": True,
}

# Link out to the standard library and key runtime dependencies.
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
}

# The suffix of source filenames.
source_suffix = [".rst", ".md"]

Expand Down
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

installation
usage
api
```

```{toctree}
Expand Down
2 changes: 1 addition & 1 deletion docs/source/installation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Installation

The package is published on [PyPI](https://pypi.org/project/deezer-python/) and can be installed with `pip` (or any equivalent):
The package is published on [PyPI](https://pypi.org/project/xiaomi-ble/) and can be installed with `pip` (or any equivalent):

```bash
pip install xiaomi-ble
Expand Down
107 changes: 104 additions & 3 deletions docs/source/usage.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,110 @@
# Usage

To use this package, import it:
`xiaomi-ble` is a passive parser for the BLE advertisements broadcast by Xiaomi
MiBeacon devices (thermometers, scales, motion sensors, buttons, locks, and
more). You hand it the advertisement data your BLE stack already receives and it
returns a structured `SensorUpdate`. It never needs to connect to the device for
the common case — everything rides on the broadcast.

See {doc}`api` for the full reference.

## Parsing an advertisement

The entry point is {class}`~xiaomi_ble.XiaomiBluetoothDeviceData`. Create one
instance per device (it is stateful — it remembers the encryption scheme and the
last payload it saw), then feed it `BluetoothServiceInfo` objects:

```python
from xiaomi_ble import XiaomiBluetoothDeviceData
from home_assistant_bluetooth import BluetoothServiceInfo

device = XiaomiBluetoothDeviceData()

# `service_info` comes from your BLE scanner (e.g. Home Assistant's bluetooth
# integration or a bleak BLEDevice + AdvertisementData pair).
if device.supported(service_info):
update = device.update(service_info)

# Decoded numeric sensors (temperature, humidity, battery, ...)
for key, value in update.entity_values.items():
print(value.name, value.native_value)

# Decoded binary sensors (motion, door open, ...)
for key, value in update.binary_entity_values.items():
print(value.name, value.native_value)

# Stateless events (button presses, dimmer rotations, ...)
for event in update.events.values():
print(event.event_type)
```

`update()` returns a `SensorUpdate` (from
[`sensor-state-data`](https://pypi.org/project/sensor-state-data/)) with the
device metadata in `update.devices`, sensor definitions in
`update.entity_descriptions`, and the decoded readings in `update.entity_values`,
`update.binary_entity_values`, and `update.events`.

## Encrypted devices

Many Xiaomi devices encrypt their payloads and need a per-device **bindkey**.
After the first advertisement with a payload, the detected scheme is available on
`device.encryption_scheme` (see {class}`~xiaomi_ble.EncryptionScheme`).

Pass the bindkey when you construct the parser, or set it later with
{meth}`~xiaomi_ble.XiaomiBluetoothDeviceData.set_bindkey`:

```python
device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex("814aac74c4f17b6c1581e1ab87816b99"))
```

Two flags tell you whether decryption is healthy:

- `device.bindkey_verified` — `True` once at least one payload has been decrypted
successfully with the supplied key.
- `device.decryption_failed` — `True` while decryption has not yet succeeded
(wrong key, or no encrypted payload seen yet).

A consumer that wants to prompt the user to re-enter the key can watch for
`decryption_failed` becoming `True` after the key was previously verified.

## Fetching a bindkey from the Xiaomi cloud

If you don't already have the bindkey, you can retrieve it from the Xiaomi cloud
account the device is paired with, using
{class}`~xiaomi_ble.XiaomiCloudTokenFetch`:

```python
import aiohttp
from xiaomi_ble import XiaomiCloudTokenFetch, XiaomiCloudException

async with aiohttp.ClientSession() as session:
fetcher = XiaomiCloudTokenFetch(username, password, session)
try:
cloud_device = await fetcher.get_device_info("A4:C1:38:D4:3C:48")
except XiaomiCloudException:
cloud_device = None

if cloud_device is not None:
device = XiaomiBluetoothDeviceData(
bindkey=bytes.fromhex(cloud_device.bindkey)
)
```

`get_device_info()` returns a {class}`~xiaomi_ble.XiaomiCloudBLEDevice`
(`name`, `mac`, `bindkey`) or `None` if the MAC is not found in the account. All
failure modes raise a subclass of {class}`~xiaomi_ble.XiaomiCloudException`.

## Active polling (optional)

A few devices expose values that are not in the broadcast (for example the
battery level on some sensors). For those,
{meth}`~xiaomi_ble.XiaomiBluetoothDeviceData.poll_needed` tells you when an active
connection is worthwhile, and
{meth}`~xiaomi_ble.XiaomiBluetoothDeviceData.async_poll` performs the GATT read:

```python
import xiaomi_ble
if device.poll_needed(service_info, last_poll):
update = await device.async_poll(ble_device)
```

TODO: Document usage
Most devices never need this — `poll_needed()` returns `False` for them.
Loading