Metadata-Version: 2.4
Name: blesession
Version: 0.7.0
Summary: One BLE session, instrumented: connect, notifications, stage timings, where it failed and why — for Home Assistant BLE integrations.
Author: eigger
License-Expression: MIT
Project-URL: Homepage, https://github.com/eigger/blesession
Project-URL: Repository, https://github.com/eigger/blesession
Project-URL: Changelog, https://github.com/eigger/blesession/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/eigger/blesession/blob/main/docs/design.md
Project-URL: Issues, https://github.com/eigger/blesession/issues
Keywords: ble,bluetooth,bleak,home-assistant,session,diagnostics
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Framework :: AsyncIO
Classifier: Topic :: System :: Hardware
Classifier: Operating System :: OS Independent
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: bleak>=0.22
Requires-Dist: bleak-retry-connector>=3.6
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: mypy>=1.14; extra == "dev"
Dynamic: license-file

# blesession

[![PyPI](https://img.shields.io/pypi/v/blesession.svg)](https://pypi.org/project/blesession/)
[![Python versions](https://img.shields.io/pypi/pyversions/blesession.svg)](https://pypi.org/project/blesession/)
[![CI](https://github.com/eigger/blesession/actions/workflows/ci.yml/badge.svg)](https://github.com/eigger/blesession/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

One BLE session, instrumented — for Home Assistant BLE integrations built on
[bleak](https://github.com/hbldh/bleak).

Connect, subscribe to notifications, exchange frames with per-step timeouts,
disconnect — and come out the other side with **where the time went, where
it failed, which radio it went over, and one sentence on what that most
likely means**, ready to publish as sensor attributes so a failed session at
3 am can be read off the entity without debug logging.

## Status

Verified on device over a Bluetooth proxy. The library is tested without
Home Assistant (`pytest`).
## Where to start

| you want to | read |
|---|---|
| integrate a device | [`docs/adopting.md`](docs/adopting.md): a whole integration end to end, what the library writes for you, how to test it without bleak or Home Assistant |
| know exactly what a call guarantees | [`docs/contract.md`](docs/contract.md): every public name, error, report key, cause key and callback shape, and the versioning rule |
| understand why it is shaped this way | [`docs/design.md`](docs/design.md): what belongs here, what deliberately does not, how a Bluetooth-proxy route works without importing Home Assistant |
| see what changed | [`CHANGELOG.md`](CHANGELOG.md) |

Adopting needs only this repository: the guide and the contract are written
so you can integrate a device without reading the source.

The design is extracted from integrations that already carry this
instrumentation (and had drifted apart), and is meant to be adopted by
others that today each hand-roll the same notification wait and have no
failure attribution at all.

## Install

```bash
pip install blesession
```

Requires Python 3.13+. The core depends only on bleak / bleak-retry-connector;
`blesession.hass` imports Home Assistant lazily and is not needed outside HA.

## What it provides

| piece | one line |
|---|---|
| `ble_session()` | connect inside the block (or reuse a link left up), watch the link, bounded disconnect in `finally` |
| `Notifications` | queued replies from one characteristic; every wait names its `step` and ends the moment the link drops; `request()` is write-then-reply |
| `write_chunks()` / `guarded_write()` | a payload as consecutive writes; every write is bounded and a dropped link ends it as `SessionDropped` |
| `characteristic_or_raise()` | the service/characteristic lookup with required properties and write size, or `GattMismatch` |
| `SessionTrace` | nested stage timings; the innermost stage an exception escaped from |
| stage vocabulary | `unreachable · connect · session · auth · transfer · finish · disconnect`, plus a device `detail` |
| `run_attempts()` | the lock-per-attempt / fresh-handle-per-attempt contract; policy stays yours |
| `blesession.hass.ble_device_or_raise()` | the handle, resolved fresh inside the attempt, or `Unreachable` |
| `blesession.hass.radio_facts()` | `via`, `via_type`, `rssi`, `paths`, `advertised_via` as scanner names, and `via_unconfirmed` (a flag) |
| `link.py` | the *one* place that probes bleak / habluetooth internals for the radio a link took |
| `blesession.testing` | `FakeClient` / `fake_connect()` so every integration's tests fake bleak the same way |
| `build_report()` | fixed attribute key order; generic likely-cause sentences with a translatable key, your device sentences first |
| `SessionReports` | the last session and the last failure, so a success does not erase the evidence |
| errors | `ConnectionError` subclasses so an off device never becomes a traceback; `retryable` says whether another attempt can help; `DeviceError` is yours to raise for a device-reported fault |
| `Failure` | what your `cause` callback receives: the stage, the error, the radio facts |

## What it does not provide

Retry counts, backoff, packet pacing, lock scope, bonding and pairing,
cooldowns, advertisement parsing, protocol frames. Those are the parts each
integration learned from its own device and keeps.

## Layout

```
src/blesession/         pure Python + bleak, tested without Home Assistant
src/blesession/hass.py  imports homeassistant lazily; only used inside HA
docs/adopting.md        how to use it, with a whole integration
docs/contract.md        what every name guarantees; versioning and deprecation
docs/design.md          why it is shaped this way
```

```python
from blesession import Notifications, SessionTrace, ble_session, build_report, stages

trace = SessionTrace(stage_map={"start": stages.AUTH})
try:
    async with ble_session(ble_device, trace=trace) as client:
        async with Notifications(client, NOTIFY_UUID, settle=0.5) as replies:
            with trace.timed("start"):
                await client.write_gatt_char(WRITE_UUID, START, response=False)
                await replies.next(timeout=5, step="start")
            with trace.timed("transfer"):
                ...
except ConnectionError as exc:          # every session error is one
    report = build_report(operation="write", trace=trace, exc=exc,
                          facts=radio_facts(hass, address, trace.link), noun="device")
    # {'operation': 'write', 'success': False, 'error': ..., 'failed_stage': 'auth',
    #  'failed_detail': 'start', 'likely_cause': ..., 'via': ..., 'connect_s': ..., ...}
```

## Development & testing

```bash
pip install -e ".[dev]"
pytest                 # unit tests; FakeClient fakes bleak the same way for adopters
ruff check . && ruff format --check .
mypy                   # type-check src/ (the package ships py.typed)
python -m build
```

CI runs on every push/PR (`.github/workflows/ci.yml`): ruff lint+format, mypy,
the test suite on Python 3.13/3.14 (plus a lowest-pinned-dependencies job), and
a build that asserts `py.typed` and the licence are in the wheel.

**Releasing**: add a version section to [`CHANGELOG.md`](CHANGELOG.md)
(behaviour changes go under *Changed* with before/after), bump `version` in
`src/blesession/__init__.py`, and merge. Then create a GitHub Release whose
title and tag use the version name (for example, `v0.5.0`). Publishing the
release triggers `.github/workflows/release.yml` to build and publish to PyPI
with trusted publishing.

## License

MIT
