Metadata-Version: 2.4
Name: pybticino
Version: 1.8.2
Summary: Simple API to access BTicino/Legrand Classe 100X/300X devices
Author-email: k-the-hidden-hero <git@k8s.one>
Maintainer-email: k-the-hidden-hero <git@k8s.one>
Project-URL: Homepage, https://github.com/k-the-hidden-hero/pybticino
Project-URL: Documentation, https://k-the-hidden-hero.github.io/pybticino/
Project-URL: Repository, https://github.com/k-the-hidden-hero/pybticino.git
Project-URL: Issues, https://github.com/k-the-hidden-hero/pybticino/issues
Project-URL: Changelog, https://github.com/k-the-hidden-hero/pybticino/blob/main/CHANGELOG.md
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Home Automation
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: websockets>=14.0
Requires-Dist: aiohttp>=3.11.16
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pytest-mock; extra == "test"
Requires-Dist: aioresponses; extra == "test"
Provides-Extra: dev
Requires-Dist: ruff; extra == "dev"
Requires-Dist: bandit; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: types-requests; extra == "dev"
Provides-Extra: docs
Requires-Dist: mkdocs-material; extra == "docs"
Requires-Dist: mkdocstrings[python]; extra == "docs"
Dynamic: license-file

# pybticino

[![PyPI](https://img.shields.io/pypi/v/pybticino)](https://pypi.org/project/pybticino/)
[![CI](https://github.com/k-the-hidden-hero/pybticino/actions/workflows/ci.yaml/badge.svg)](https://github.com/k-the-hidden-hero/pybticino/actions/workflows/ci.yaml)
[![Python](https://img.shields.io/pypi/pyversions/pybticino)](https://pypi.org/project/pybticino/)
[![License](https://img.shields.io/github/license/k-the-hidden-hero/pybticino)](LICENSE.txt)

Async Python library for the BTicino/Netatmo API. Controls BTicino Classe 100X/300X video intercom systems via the Netatmo cloud.

Used by the [bticino_intercom](https://github.com/k-the-hidden-hero/bticino_intercom) Home Assistant integration.

## Installation

```bash
pip install pybticino
```

Requires Python 3.13 or later.

## Features

- **Authentication**: OAuth2 password grant with automatic token refresh and persistence support
- **Home topology**: fetch homes, modules, and their configuration
- **Device control**: lock/unlock doors, turn lights on/off
- **Events**: fetch call history with snapshots and vignettes
- **WebSocket**: real-time push notifications (call events, connection status, state changes)
- **Re-subscribe**: refresh OAuth token on existing connection without reconnecting
- **WebRTC signaling**: offer/answer/ICE exchange for live video calls (experimental)

## Quick start

```python
import asyncio
from pybticino import AuthHandler, AsyncAccount

async def on_token_change(tokens):
    """Called when tokens are refreshed — persist them to avoid re-auth."""
    print(f"New token expires at {tokens['expires_at']}")

async def main():
    auth = AuthHandler(
        "your_email@example.com",
        "your_password",
        token_callback=on_token_change,  # optional: persist tokens
    )
    account = AsyncAccount(auth)

    await account.async_update_topology()
    for home_id, home in account.homes.items():
        print(f"Home: {home.name} ({len(home.modules)} modules)")

        status = await account.async_get_home_status(home_id)
        events = await account.async_get_events(home_id, size=5)

    await auth.close_session()

asyncio.run(main())
```

To restore tokens from a previous session (skip initial auth):

```python
auth = AuthHandler("email", "password", token_callback=on_token_change)
auth.set_tokens(
    access_token=saved["access_token"],
    refresh_token=saved["refresh_token"],
    expires_at=saved["expires_at"],
)
```

## WebSocket (real-time events)

```python
import asyncio
from pybticino import AuthHandler, WebsocketClient

async def on_message(message):
    print(f"Event: {message.get('push_type')} - {message}")

async def main():
    auth = AuthHandler("your_email@example.com", "your_password")
    ws = WebsocketClient(auth, on_message)

    # Option 1: run forever with auto-reconnect (recommended)
    await ws.run_forever(reconnect_delay=30)

    # Option 2: manual connect + listen
    await ws.connect()
    task = ws.get_listener_task()
    if task:
        await task
    await ws.disconnect()

    await auth.close_session()

asyncio.run(main())
```

### Re-subscribe (keep connection alive)

```python
# Refresh token on existing connection without disconnecting.
# Call this periodically (e.g., every hour) to prevent token expiry.
await ws.resubscribe()
```

## API endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `async_update_topology()` | `/api/homesdata` | Fetch homes and modules |
| `async_get_home_status(home_id)` | `/syncapi/v1/homestatus` | Get module status |
| `async_set_module_state(home_id, module_id, state)` | `/syncapi/v1/setstate` | Control devices |
| `async_get_events(home_id, size)` | `/api/getevents` | Get event history |
| `async_get_turn_servers()` | `/turn` | Fetch TURN/STUN ICE server credentials for WebRTC |

## WebSocket event types

Events are delivered with `push_type` in format `{DEVICE_TYPE}-{EVENT_TYPE}`:

| push_type | Description |
|-----------|-------------|
| `BNC1-rtc` | Incoming WebRTC call (with SDP offer) |
| `BNC1-incoming_call` | Doorbell ring (with snapshot URL) |
| `BNC1-missed_call` | Unanswered call |
| `BNC1-accepted_call` | Call answered |
| `BNC1-connection` | Bridge connected |
| `BNC1-disconnection` | Bridge disconnected |

## Documentation

Full documentation at **[k-the-hidden-hero.github.io/pybticino](https://k-the-hidden-hero.github.io/pybticino/)**

Key guides from reverse engineering:

- **[WebRTC Signaling Protocol](https://k-the-hidden-hero.github.io/pybticino/webrtc-signaling/)** — complete offer/answer/ICE flow, message formats, session state, two call modes
- **[WebRTC Audio Mechanism](https://k-the-hidden-hero.github.io/pybticino/webrtc-audio/)** — how to activate the device microphone (requires real RTP audio packets)
- **[WebSocket Events](https://k-the-hidden-hero.github.io/pybticino/websocket-events/)** — all push notification types and their JSON structures
- **[Reverse Engineering Notes](https://k-the-hidden-hero.github.io/pybticino/reverse-engineering-notes/)** — findings from decompiling the official BTicino/Netatmo app

### Examples

Working example scripts in [`examples/`](examples/):

| Script | Description |
|--------|-------------|
| [`webrtc_offer_mode.py`](examples/webrtc_offer_mode.py) | On-demand video: send offer → receive answer → ICE → connected |
| [`webrtc_answer_mode.py`](examples/webrtc_answer_mode.py) | Answer incoming call: wait for doorbell → answer via signaling |
| [`webrtc_audio_test.html`](examples/webrtc_audio_test.html) | Browser-based audio test with silence oscillator |
| [`websocket_test.py`](examples/websocket_test.py) | Real-time push notification listener |

## Related projects

- [bticino_intercom](https://github.com/k-the-hidden-hero/bticino_intercom) — Home Assistant custom integration
- [bticino_ha_extras](https://github.com/k-the-hidden-hero/bticino_ha_extras) — Blueprints, Lovelace cards, and companion resources

## Contributing

```bash
pip install -e ".[test,dev]"
pytest tests/ -v
ruff check src/ tests/
```

## License

MIT
