From 112b519ab155474e3f000ae6a1e57471740ff71f Mon Sep 17 00:00:00 2001 From: Bluetooth Devices Bot Date: Thu, 4 Jun 2026 14:18:38 +0000 Subject: [PATCH] docs: add API reference and usage guide, fix RTD Python version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #284. What: Replace the docs skeleton with a real usage guide and an autodoc-based API reference covering the fundamental public classes and methods. Why: docs/source/usage.md was a "TODO: Document usage" stub and there was no API reference at all. Separately, .readthedocs.yml pinned Python 3.9 while the package requires >=3.11 (pyproject), so `pip install .` — and thus the entire RTD build — would fail. How: - .readthedocs.yml: bump to ubuntu-22.04 + Python 3.12 (>=3.11 compatible). - conf.py: enable sphinx.ext.autodoc/autosummary/napoleon/viewcode/intersphinx; import the package for the version string. - api.md: autodoc the public surface (XiaomiBluetoothDeviceData, EncryptionScheme, the cloud bindkey-fetch classes/exceptions, SLEEPY_DEVICE_MODELS). - usage.md: real walkthrough — passive parsing, encrypted devices/bindkeys, cloud bindkey fetch, optional active polling. - installation.md: fix stray "deezer-python" PyPI link. Testing: `pip install .[docs]` + `sphinx-build -b html` builds cleanly under Python 3.12 with all classes/methods rendered; isort/black/flake8/codespell pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .readthedocs.yml | 8 ++- docs/source/api.md | 86 +++++++++++++++++++++++++++++ docs/source/conf.py | 29 +++++++++- docs/source/index.md | 1 + docs/source/installation.md | 2 +- docs/source/usage.md | 107 +++++++++++++++++++++++++++++++++++- 6 files changed, 223 insertions(+), 10 deletions(-) create mode 100644 docs/source/api.md diff --git a/.readthedocs.yml b/.readthedocs.yml index 801e792..405c99c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -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: diff --git a/docs/source/api.md b/docs/source/api.md new file mode 100644 index 0000000..8a2a116 --- /dev/null +++ b/docs/source/api.md @@ -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. diff --git a/docs/source/conf.py b/docs/source/conf.py index b838c64..4d36e9b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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 --------------------------------------------------- @@ -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"] diff --git a/docs/source/index.md b/docs/source/index.md index 4c83b6c..fae5224 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -6,6 +6,7 @@ installation usage +api ``` ```{toctree} diff --git a/docs/source/installation.md b/docs/source/installation.md index e79fba7..52f9c59 100644 --- a/docs/source/installation.md +++ b/docs/source/installation.md @@ -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 diff --git a/docs/source/usage.md b/docs/source/usage.md index cb5ee29..375670d 100644 --- a/docs/source/usage.md +++ b/docs/source/usage.md @@ -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.