Metadata-Version: 2.3
Name: pybrofer
Version: 0.1.0
Summary: Async Python client for the Brofer Cloud VMC API
Author: Tiago Mota
Author-email: Tiago Mota <tiago@swissborg.com>
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Classifier: Framework :: AsyncIO
Requires-Dist: aiohttp>=3.9
Requires-Python: >=3.12
Project-URL: Homepage, https://github.com/tiagomota/pybrofer
Project-URL: Repository, https://github.com/tiagomota/pybrofer
Project-URL: Bug Tracker, https://github.com/tiagomota/pybrofer/issues
Description-Content-Type: text/markdown

# pybrofer

Async Python client for the [Brofer Cloud](https://www.broferwebservices.it) VMC (Ventilation with Heat Recovery) API.

Built to work cleanly inside Home Assistant integrations, but usable in any async Python application.

## Disclaimer

This project is not affiliated with, endorsed by, or supported by Brofer or Brofer Web Services in any way. Use at your own risk.

## Installation

```bash
pip install pybrofer
```

Or with uv:

```bash
uv add pybrofer
```

Requires Python 3.12+. The only runtime dependency is `aiohttp`.

## Quick start

```python
import asyncio
import aiohttp
from pybrofer import authenticate, BroferClient

async def main():
    async with aiohttp.ClientSession() as session:
        # 1. Authenticate — returns a short-lived Bearer token
        token = await authenticate(
            session,
            base_url="https://api.broferwebservices.it",
            username="your@email.com",
            password="yourpassword",
            access_token="your-device-access-token",  # from the Brofer app
        )

        # 2. Create a client and query your device
        client = BroferClient(session, token)
        state = await client.get_state("your-device-id")

        print(state.ventilation_speed)    # e.g. "home"
        print(state.indoor_temperature)   # e.g. 21.5
        print(state.is_filter_dirty)      # True / False

        # 3. Change ventilation mode
        await client.set_ventilation_speed("your-device-id", "boost")

asyncio.run(main())
```

## API reference

### `authenticate(session, base_url, username, password, access_token, *, ssl=True) → str`

Authenticates with the Brofer Cloud API using HTTP Basic Auth and a device access token, and returns a Bearer token for use with `BroferClient`.

| Parameter | Type | Description |
|---|---|---|
| `session` | `aiohttp.ClientSession` | Caller-managed session |
| `base_url` | `str` | API base URL (e.g. `"https://api.broferwebservices.it"`) |
| `username` | `str` | Brofer account email |
| `password` | `str` | Brofer account password |
| `access_token` | `str` | Device-specific token from the Brofer app |
| `ssl` | `bool` | Set to `False` to skip TLS verification (not recommended) |

Raises `BroferAuthError` on bad credentials, `BroferConnectionError` on network failure.

---

### `BroferClient(session, access_token, *, ssl=True)`

The caller owns the `aiohttp.ClientSession` — the client never creates one internally.

#### `get_state(device_id) → VMCState`

Fetches the current state of a VMC unit from `GET /bridges/{device_id}/data`.

#### `set_ventilation_speed(device_id, speed)`

Posts a ventilation mode change to `POST /bridges/{device_id}/ventilation-speed`.

Valid speeds: `"away"`, `"home"`, `"boost"`, `"party"`. Raises `ValueError` for unknown values.

#### `update_token(new_token)`

Replaces the stored Bearer token in place — useful when a coordinator re-authenticates and needs to hand the new token to an existing client instance.

---

### `VMCState`

Frozen dataclass returned by `get_state`.

| Field | Type | Description |
|---|---|---|
| `read_at` | `datetime` | Timestamp the bridge read the state |
| `is_online` | `bool` | Whether the device is reachable |
| `filter_dirty` | `int` | Raw filter status (`0` = clean, `1` = dirty) |
| `outdoor_temperature` | `float` | Outdoor temperature in °C |
| `indoor_temperature` | `float` | Indoor temperature in °C |
| `ventilation_speed` | `str` | Current mode (`"away"`, `"home"`, `"boost"`, `"party"`) |
| `is_filter_dirty` | `bool` | Computed property — `True` when filter needs replacement |

---

### Exceptions

All exceptions inherit from `BroferError`.

| Exception | Raised when |
|---|---|
| `BroferAuthError` | HTTP 401/403, or no token in auth response |
| `BroferAPIError` | Any other non-2xx response (exposes `status_code`, `response_body`) |
| `BroferConnectionError` | Transport-level failure (DNS, timeout, SSL) |

## Development

```bash
uv sync                       # install all dependencies
uv run pytest                 # tests + coverage
uv run ruff check src tests   # lint
uv run ruff format src tests  # format
uv run mypy                   # type check
```

CI runs on Python 3.12 and 3.13. Coverage must stay above 80%.

## License

MIT
