Metadata-Version: 2.4
Name: pyliebherrhomeapi
Version: 0.5.0
Summary: Python library for Liebherr Home API
Author-email: Mario Mett <mario@mett.ee>
License: MIT
Project-URL: Homepage, https://github.com/mettolen/pyliebherrhomeapi
Project-URL: Repository, https://github.com/mettolen/pyliebherrhomeapi
Project-URL: Issues, https://github.com/mettolen/pyliebherrhomeapi/issues
Keywords: liebherr,home-automation,api
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Home Automation
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.9.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: pytest-timeout>=2.1.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: twine>=4.0.0; extra == "dev"
Dynamic: license-file

# pyliebherrhomeapi

[![CI](https://github.com/mettolen/pyliebherrhomeapi/actions/workflows/ci.yml/badge.svg)](https://github.com/mettolen/pyliebherrhomeapi/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/mettolen/pyliebherrhomeapi/branch/main/graph/badge.svg)](https://codecov.io/gh/mettolen/pyliebherrhomeapi)
[![PyPI version](https://badge.fury.io/py/pyliebherrhomeapi.svg)](https://badge.fury.io/py/pyliebherrhomeapi)
[![PyPI Downloads](https://img.shields.io/pypi/dm/pyliebherrhomeapi.svg)](https://pypi.org/project/pyliebherrhomeapi/)
[![Python versions](https://img.shields.io/pypi/pyversions/pyliebherrhomeapi.svg)](https://pypi.org/project/pyliebherrhomeapi/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Python library for the [Liebherr SmartDevice Home API](https://developer.liebherr.com/apis/smartdevice-homeapi/).

---

This library is used by the [Liebherr](https://www.home-assistant.io/integrations/liebherr) Home Assistant integration.

<a href="https://www.home-assistant.io/integrations/liebherr/" target="_blank"><img alt="Dynamic Regex Badge" src="https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fwww.home-assistant.io%2Fintegrations%2Fliebherr%2F&search=(%5B%5Cd%2C%5D%2B)%5Cs*%3C%2Fa%3E%5Cs*active%20installations&replace=%241&style=for-the-badge&logo=homeassistant&label=Active%20installations&labelColor=%23f2f4f9&color=%2346bdf4"></a> <a href="https://www.home-assistant.io/docs/quality_scale/" target="_blank"><img alt="Dynamic Regex Badge" src="https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fwww.home-assistant.io%2Fintegrations%2Fliebherr%2F&search=%3Ca%20href%3D'%5B%5E'%5D*quality_scale%5B%5E'%5D*'%3E(%5B%5E%3C%5D%2B%3F)%20quality%3C%2Fa%3E&replace=%241&style=for-the-badge&logo=homeassistant&label=Quality%20scale&labelColor=%23f2f4f9&color=%2346bdf4"></a> <a href="https://my.home-assistant.io/redirect/config_flow_start?domain=liebherr" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/Liebherr-i?style=for-the-badge&logo=homeassistant&label=Add%20integration&labelColor=%23f2f4f9&color=%2346bdf4"></a>

## Features

- 🔌 **Async/await support** using asyncio with comprehensive error handling
- 🌡️ **Temperature control** for all zones in your Liebherr appliances
- ❄️ **SuperFrost/SuperCool** control for quick cooling/freezing
- 🎉 **Special modes** (Party Mode, Night Mode, Presentation Light)
- 🧊 **Ice maker control** with Max Ice support
- 💧 **HydroBreeze and BioFreshPlus** mode management
- 🚪 **Auto door** control for supported appliances
- 📱 **Device management** - list and query all connected appliances
- 📡 **Realtime updates via Server-Sent Events (SSE)** _(beta)_ - subscribe to live control state changes
- 🛡️ **Type hints** for better IDE support and development experience
- ✅ **Input validation** with proper error handling
- 📊 **Comprehensive data models** for all control types
- 📝 **Configurable logging** with privacy-focused debug output
- 🧪 **100% test coverage** ensuring reliability and code quality

## Requirements

- Python 3.12+ (matches the typed codebase and test matrix)
- Asyncio environment with `aiohttp` (installed automatically)
- Network access to `https://home-api.smartdevice.liebherr.com`

## Installation

- From PyPI (when published):

  ```bash
  pip install pyliebherrhomeapi
  ```

- From source (current repository):

  ```bash
  pip install .
  ```

## Prerequisites

Before using this library, you need:

1. **Connect your appliance**: Connect your Liebherr appliance via the [SmartDevice app](https://smartdevice.onelink.me/OrY5/8neax8lp) to your home WiFi network
   - [Download the SmartDevice app](https://smartdevice.onelink.me/OrY5/8neax8lp)
   - [Instructions for connecting your appliance](https://go.liebherr.com/cb2ct1)

2. **Get your API Key** (via the SmartDevice app):
   - Go to **Settings** in the SmartDevice app
   - Select **"Beta features"**
   - Activate the **HomeAPI**
   - Copy the API Key (⚠️ **Important**: The API key can only be copied once. Once you leave the screen, it cannot be copied again. If you forget your key, you'll need to create a new one via the app)

3. **Connected appliances only**: Only appliances that are connected to the internet via the SmartDevice app can be accessed through the HomeAPI. Appliances that are only registered but not connected will not appear

## Quick Start

```python
import asyncio
from pyliebherrhomeapi import (
    LiebherrClient,
    TemperatureUnit,
    IceMakerMode,
)

async def main():
    # Create client with your API key
    async with LiebherrClient(api_key="your-api-key-here") as client:
        # Get all devices (only connected devices are returned)
        devices = await client.get_devices()
        print(f"Found {len(devices)} device(s)")

        for device in devices:
            # device_id is the serial number of the appliance
            print(f"Device: {device.nickname} ({device.device_id})")
            print(f"  Type: {device.device_type}")
            print(f"  Model: {device.device_name}")

            # Get all controls for this device
            controls = await client.get_controls(device.device_id)
            print(f"  Controls: {len(controls)}")

if __name__ == "__main__":
    asyncio.run(main())
```

## Important Notes

### Device Zones

- Each device has at least one zone (cooling zone, freezing zone, etc.)
- **Zone numbering**: The top zone is zone 0, zone numbers ascend from top to bottom
- Zone controls (like temperature, SuperFrost, SuperCool) always require a `zone_id`
- Base controls (like Party Mode, Night Mode) apply to the whole device and don't need a zone

### Polling Recommendations

⚠️ **Beta Version Notice**: The REST endpoints do not push updates, so they need to be polled regularly. For realtime updates without polling, see the [Realtime Updates (SSE)](#realtime-updates-sse-beta) section below.

**Recommended polling intervals:**

- **Controls**: Poll every 30 seconds using `/v1/devices/{deviceId}/controls` to get all states in one call
- **Device list**: Poll manually only when appliances are added/removed or nicknames change
- **Rate limits**: Be mindful of API call limits. Avoid too many calls at once as there are restrictions for security and performance

### Control Types

**Base Controls** (apply to entire device, no `zone_id` needed):

- Party Mode
- Night Mode

**Zone Controls** (require `zone_id`, even if device has only one zone):

- Temperature
- SuperFrost
- SuperCool
- Ice Maker
- HydroBreeze
- BioFreshPlus
- Auto Door

## Usage Examples

### Temperature Control

```python
from pyliebherrhomeapi import LiebherrClient, TemperatureUnit

async with LiebherrClient(api_key="your-api-key") as client:
    # Set temperature for zone 0 (top zone) to 4°C
    await client.set_temperature(
        device_id="12.345.678.9",
        zone_id=0,  # Zone 0 is the top zone
        target=4,
        unit=TemperatureUnit.CELSIUS
    )

    # Get temperature control info
    controls = await client.get_control(
        device_id="12.345.678.9",
        control_name="temperature",
        zone_id=0
    )
```

### SuperCool and SuperFrost

```python
# Enable SuperCool for zone 0
await client.set_super_cool(
    device_id="12.345.678.9",
    zone_id=0,
    value=True
)

# Enable SuperFrost for zone 1
await client.set_super_frost(
    device_id="12.345.678.9",
    zone_id=1,
    value=True
)
```

### Special Modes

```python
# Enable Party Mode
await client.set_party_mode(
    device_id="12.345.678.9",
    value=True
)

# Enable Night Mode
await client.set_night_mode(
    device_id="12.345.678.9",
    value=True
)

# Set presentation light intensity (0-5)
await client.set_presentation_light(
    device_id="12.345.678.9",
    target=3
)
```

### Ice Maker Control

```python
from pyliebherrhomeapi import IceMakerMode

# Turn on ice maker
await client.set_ice_maker(
    device_id="12.345.678.9",
    zone_id=0,
    mode=IceMakerMode.ON
)

# Enable Max Ice mode
await client.set_ice_maker(
    device_id="12.345.678.9",
    zone_id=0,
    mode=IceMakerMode.MAX_ICE
)
```

### HydroBreeze Control

```python
from pyliebherrhomeapi import HydroBreezeMode

# Set HydroBreeze to medium
await client.set_hydro_breeze(
    device_id="12.345.678.9",
    zone_id=0,
    mode=HydroBreezeMode.MEDIUM
)
```

### BioFreshPlus Control

```python
from pyliebherrhomeapi import BioFreshPlusMode

# Set BioFreshPlus mode
await client.set_bio_fresh_plus(
    device_id="12.345.678.9",
    zone_id=0,
    mode=BioFreshPlusMode.ZERO_ZERO
)
```

### Auto Door Control

```python
# Open the door
await client.trigger_auto_door(
    device_id="12.345.678.9",
    zone_id=0,
    value=True  # True to open, False to close
)
```

### Query Device Controls

```python
# Get all controls (recommended for polling - gets all states in one call)
all_controls = await client.get_controls(device_id="12.345.678.9")

# Get specific control by name
temp_controls = await client.get_control(
    device_id="12.345.678.9",
    control_name="temperature"
)

# Get control for specific zone
zone_temp = await client.get_control(
    device_id="12.345.678.9",
    control_name="temperature",
    zone_id=0  # Top zone
)
```

### Efficient Polling Pattern

```python
import asyncio
from pyliebherrhomeapi import LiebherrClient

async def poll_device_state(client: LiebherrClient, device_id: str):
    """Poll device state every 30 seconds (recommended interval)."""
    while True:
        try:
            # Get all controls in a single API call
            device_state = await client.get_device_state(device_id)

            # Process the controls
            for control in device_state.controls:
                print(f"{control.name}: {control}")

            # Wait 30 seconds before next poll (recommended by Liebherr)
            await asyncio.sleep(30)

        except Exception as e:
            print(f"Error polling device: {e}")
            await asyncio.sleep(30)

async def main():
    async with LiebherrClient(api_key="your-api-key") as client:
        devices = await client.get_devices()
        if devices:
            await poll_device_state(client, devices[0].device_id)
```

### Realtime Updates (SSE) _(beta)_

⚠️ **Beta**: The SSE endpoint is in beta on the Liebherr side and not yet part of the official OpenAPI spec. The payload format is based on observed traffic and may change.

The client subscribes to `/v1/sse/devices/{deviceId}/controls` and yields a list of parsed controls on each update. Based on observed traffic, the server sends a snapshot of all controls then closes the connection, so you must reconnect to keep receiving updates. Malformed events are logged and skipped.

#### Recommended: `stream_controls_forever()`

Reconnects automatically with exponential backoff + jitter on recoverable errors (drops, timeouts, 5xx) and clean closures. Non-recoverable errors (`LiebherrAuthenticationError`, `LiebherrNotFoundError`, `LiebherrPreconditionFailedError`) are re-raised. Optional non-blocking `on_connect` / `on_disconnect` callbacks let you track availability (e.g. in a Home Assistant integration); an exception in a callback is logged and does not break the stream.

```python
import asyncio
from pyliebherrhomeapi import LiebherrClient

async def main() -> None:
    async with LiebherrClient(api_key="your-api-key") as client:
        devices = await client.get_devices()
        if not devices:
            return

        async for controls in client.stream_controls_forever(
            devices[0].device_id,
            on_connect=lambda: print("SSE connected"),
            on_disconnect=lambda: print("SSE disconnected, reconnecting..."),
        ):
            for control in controls:
                zone_id = getattr(control, "zone_id", None)
                print(f"Update: {control.name} (zone={zone_id}) -> {control}")

asyncio.run(main())
```

#### Low-level: `stream_controls()`

Opens a single connection and yields events until it ends. Use it only if you want to manage reconnection yourself; otherwise prefer `stream_controls_forever()`.

**SSE vs. polling:** prefer SSE for low-latency, self-healing subscriptions (e.g. a long-running Home Assistant integration); prefer polling for simple scripts or environments where long-lived connections are problematic.

## Logging

The library uses Python's standard `logging` module for diagnostics. By default, it uses a `NullHandler`, so no logs are emitted unless you configure logging in your application.

### Enable Debug Logging

```python
import logging

# Enable debug logging for the library
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# Or configure just for pyliebherrhomeapi
logger = logging.getLogger('pyliebherrhomeapi')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
```

### Log Levels

- **DEBUG**: Detailed information about API requests, responses, and session lifecycle
- **INFO**: General information about operations (currently not used)
- **WARNING**: HTTP errors and connection issues
- **ERROR**: Severe errors like server failures

### Privacy

Device IDs are automatically masked in debug logs (showing only last 4 characters) to protect sensitive information.

## Error Handling

```python
from pyliebherrhomeapi import (
    LiebherrClient,
    LiebherrAuthenticationError,
    LiebherrBadRequestError,
    LiebherrNotFoundError,
    LiebherrPreconditionFailedError,
    LiebherrUnsupportedError,
    LiebherrConnectionError,
    LiebherrTimeoutError,
)

async with LiebherrClient(api_key="your-api-key") as client:
    try:
        await client.set_temperature(
            device_id="12.345.678.9",
            zone_id=0,
            target=4
        )
    except LiebherrAuthenticationError:
        print("Invalid API key")
    except LiebherrBadRequestError as e:
        print(f"Invalid request: {e}")
    except LiebherrNotFoundError:
        print("Device not reachable")
    except LiebherrPreconditionFailedError:
        print("Device not onboarded to your household")
    except LiebherrUnsupportedError:
        print("Feature not supported on this device")
    except (LiebherrConnectionError, LiebherrTimeoutError) as e:
        print(f"Connection error: {e}")
```

## Development

### Setup

```bash
# Clone the repository
git clone https://github.com/mettolen/pyliebherrhomeapi.git
cd pyliebherrhomeapi

# Install development dependencies
pip install -e ".[dev]"
```

### Testing

```bash
# Run tests
pytest

# Run tests with coverage
pytest --cov=pyliebherrhomeapi --cov-report=html
```

### Code Quality

```bash
# Format code
ruff format .

# Lint code
ruff check .

# Type checking
mypy src
```

## API Documentation

For detailed API documentation, visit:

- [SmartDevice HomeAPI Overview](https://developer.liebherr.com/apis/smartdevice-homeapi/)
- [Swagger UI Documentation](https://developer.liebherr.com/apis/smartdevice-homeapi/swagger-ui/)
- [Release Notes](https://developer.liebherr.com/apis/smartdevice-homeapi/releasenotes/)

**API Base URL**: `https://home-api.smartdevice.liebherr.com`

## Implementation Notes

This client library is generated based on the official `openapi.json` specification downloaded from the Liebherr Developer Portal, which reflects the latest API state. When Liebherr updates their API and releases a new version of the OpenAPI specification, this client library will be updated accordingly to maintain compatibility and support new features.

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
