Metadata-Version: 2.4
Name: pyairobotrest
Version: 0.3.0
Summary: Python library for Airobot thermostat TE1 local REST API
Author-email: Mario Mett <mario@mett.ee>
License-Expression: MIT
Project-URL: Homepage, https://github.com/mettolen/pyairobotrest
Project-URL: Repository, https://github.com/mettolen/pyairobotrest
Project-URL: Issues, https://github.com/mettolen/pyairobotrest/issues
Keywords: airobot,thermostat,rest-api,home-automation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Home Automation
Requires-Python: >=3.11
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

# pyairobotrest

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

Python library for controlling [Airobot](https://airobothome.com/) TE1 thermostats via local REST API.

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

<a href="https://my.home-assistant.io/redirect/config_flow_start?domain=airobot" class="my badge" target="_blank"><img src="https://my.home-assistant.io/badges/config_flow_start.svg"></a>

## Features

- 🔌 **Async/await support** using asyncio with comprehensive error handling
- 🌡️ **Temperature control** for HOME and AWAY modes (5-35°C range)
- 🏠 **Mode management** (HOME/AWAY mode switching)
- ⚙️ **Hysteresis control** (0.0-0.5°C range)
- 💨 **Boost mode** for temporary heating (1 hour duration)
- 🔒 **Child lock** control for safety
- 💡 **Sensor monitoring** (temperature, humidity, CO2, AQI)
- 🛡️ **Type hints** for better IDE support and development experience
- ✅ **Input validation** with range checking for all settings
- 📊 **Comprehensive data model** with optional sensor support

## Installation

```bash
pip install pyairobotrest
```

## Prerequisites

Before using this library, ensure your Airobot thermostat is properly configured:

1. **Network Connection**: Connect the thermostat to your local WiFi/Ethernet network
2. **Initial Registration**: Connect to the internet at least once to register with the server and obtain credentials (Device ID and password)
3. **Enable Local API**: In thermostat settings, navigate to "Connectivity → Local API → Enable"
4. **Get Credentials**: Find your Device ID (username) and password in the thermostat menu under "Mobile app" screen

**Note**: After initial setup, internet connectivity is not required for local API access.

## Quick Start

```python
import asyncio
from pyairobotrest import AirobotClient, AirobotConnectionError

async def main():
    # Create client - replace with your thermostat's IP and credentials
    client = AirobotClient(
        host="192.168.1.100",       # or "airobot-thermostat-t01xxxxxx.local"
        username="T01XXXXXX",       # Your thermostat Device ID
        password="your_password"    # Password from "Mobile app" screen
    )

    try:
        # Read current status
        status = await client.get_statuses()
        print(f"Device: {status.device_id}")
        print(f"Hardware: v{status.hw_version_string}")
        print(f"Firmware: v{status.fw_version_string}")
        print(f"Current temperature: {status.temp_air:.1f}°C")
        print(f"Target temperature: {status.setpoint_temp:.1f}°C")
        print(f"Humidity: {status.hum_air:.1f}%")
        print(f"Heating: {'ON' if status.is_heating else 'OFF'}")

        # Check optional sensors
        if status.has_floor_sensor:
            print(f"Floor temperature: {status.temp_floor:.1f}°C")
        if status.has_co2_sensor:
            print(f"CO2: {status.co2} ppm (AQI: {status.aqi})")

        # Read current settings
        settings = await client.get_settings()
        print(f"Mode: {'HOME' if settings.mode == 1 else 'AWAY'}")
        print(f"HOME temp: {settings.setpoint_temp}°C")
        print(f"AWAY temp: {settings.setpoint_temp_away}°C")

        # Control the thermostat
        await client.set_home_temperature(23.0)  # Set HOME temperature
        await client.set_mode(1)                 # Switch to HOME mode
        await client.set_boost_mode(True)        # Enable boost for 1 hour

    except AirobotConnectionError as err:
        print(f"Connection error: {err}")
    finally:
        await client.close()

asyncio.run(main())
```

## Context Manager Usage (Recommended)

```python
import asyncio
from pyairobotrest import AirobotClient

async def main():
    try:
        # Using async context manager automatically handles connection cleanup
        async with AirobotClient(
            host="192.168.1.100",
            username="T01XXXXXX",
            password="your_password"
        ) as client:
            # Read status
            status = await client.get_statuses()
            print(f"Temperature: {status.temp_air:.1f}°C")
            print(f"Heating: {status.is_heating}")

            # Configure thermostat
            await client.set_home_temperature(22.5)
            await client.set_hysteresis_band(0.3)
            await client.set_child_lock(True)

    except Exception as err:
        print(f"Error: {err}")

asyncio.run(main())
```

## Factory Method (Recommended for Home Assistant)

For explicit session initialization, use the factory method pattern:

```python
import asyncio
from pyairobotrest import AirobotClient

async def main():
    # Factory method ensures session is initialized before use
    client = await AirobotClient.create(
        host="192.168.1.100",
        username="T01XXXXXX",
        password="your_password"
    )

    try:
        status = await client.get_statuses()
        print(f"Temperature: {status.temp_air:.1f}°C")
    finally:
        await client.close()

asyncio.run(main())
```

## API Reference

### Client Initialization

| Method                   | Description                                 | Returns       |
| ------------------------ | ------------------------------------------- | ------------- |
| `AirobotClient()`        | Standard constructor                        | AirobotClient |
| `AirobotClient.create()` | Factory method with session pre-initialized | AirobotClient |

### Main Client Methods

| Method                               | Description                   | Parameters                   |
| ------------------------------------ | ----------------------------- | ---------------------------- |
| `get_statuses()`                     | Read all current measurements | None                         |
| `get_settings()`                     | Read all thermostat settings  | None                         |
| `set_mode(mode)`                     | Set HOME/AWAY mode            | `mode: int` (1=HOME, 2=AWAY) |
| `set_home_temperature(temp)`         | Set HOME temperature          | `temp: float` (5.0-35.0°C)   |
| `set_away_temperature(temp)`         | Set AWAY temperature          | `temp: float` (5.0-35.0°C)   |
| `set_hysteresis_band(hyst)`          | Set temperature hysteresis    | `hyst: float` (0.0-0.5°C)    |
| `set_device_name(name)`              | Set device name               | `name: str` (1-20 chars)     |
| `set_child_lock(enabled)`            | Enable/disable child lock     | `enabled: bool`              |
| `set_boost_mode(enabled)`            | Enable/disable boost mode     | `enabled: bool`              |
| `reboot_thermostat()`                | Reboot the thermostat         | None                         |
| `recalibrate_co2_sensor()`           | Recalibrate CO2 sensor        | None                         |
| `toggle_actuator_exercise(disabled)` | Enable/disable actuator test  | `disabled: bool`             |

### Data Model (ThermostatStatus)

```python
@dataclass
class ThermostatStatus:
    # Device identification
    device_id: str                      # Unique device ID
    hw_version: int                     # Hardware version (raw)
    fw_version: int                     # Firmware version (raw)

    # Temperature measurements
    temp_air: float                     # Air temperature in °C
    temp_floor: float | None            # Floor temperature in °C (None if not attached)
    setpoint_temp: float                # Target temperature in °C

    # Environmental sensors
    hum_air: float                      # Relative humidity in %
    co2: int | None                     # CO2 in ppm (None if not equipped)
    aqi: int | None                     # Air quality index 0-5 (None if no CO2)

    # Status
    is_heating: bool                    # True if actively heating
    has_error: bool                     # True if error present
    error_code: int                     # Error code (0 = no error)
    uptime: int                         # Device uptime in seconds

    # Sensor availability
    has_floor_sensor: bool              # True if floor sensor attached
    has_co2_sensor: bool                # True if CO2 sensor equipped

    # Version properties (human-readable)
    hw_version_string: str              # Hardware version (e.g., "1.11")
    fw_version_string: str              # Firmware version (e.g., "1.11")
```

### Data Model (ThermostatSettings)

```python
@dataclass
class ThermostatSettings:
    device_id: str                      # Unique device ID
    mode: int                           # 1=HOME, 2=AWAY
    setpoint_temp: float                # HOME temperature in °C
    setpoint_temp_away: float           # AWAY temperature in °C
    hysteresis_band: float              # Temperature hysteresis in °C
    device_name: str                    # Device name (1-20 characters)

    # Setting flags
    setting_flags: SettingFlags
        childlock_enabled: bool         # Child lock status
        boost_enabled: bool             # Boost mode status (1 hour)
        reboot: bool                    # Reboot flag
        recalibrate_co2: bool           # CO2 calibration flag
        actuator_exercise_disabled: bool # Actuator exercise status
```

### Exception Handling

```python
from pyairobotrest import (
    AirobotConnectionError,     # Connection issues
    AirobotAuthError,           # Authentication failures
    AirobotTimeoutError,        # Timeout errors
    AirobotError,               # General API errors
)

try:
    async with AirobotClient("192.168.1.100", "T01XXXXXX", "password") as client:
        status = await client.get_statuses()
except AirobotAuthError:
    print("Authentication failed - check username/password")
except AirobotConnectionError:
    print("Failed to connect to thermostat")
except AirobotTimeoutError:
    print("Operation timed out")
except AirobotError as err:
    print(f"API error: {err}")
```

## Development

### Setup

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

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

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

### Testing & Quality

```bash
# Run all tests with coverage
pytest --cov=pyairobotrest --cov-report=term-missing

# Run type checking
mypy src/pyairobotrest

# Run linting and formatting
ruff check src/pyairobotrest
ruff format src/pyairobotrest

# Run pre-commit hooks (if installed)
pre-commit run --all-files
```

## Requirements

- Python 3.11+
- Dependencies:
  - `aiohttp` >= 3.9.0 (HTTP client for REST API communication)
  - `asyncio` (built-in, async/await support)

## Compatibility

This library is tested and compatible with:

- Airobot TE1 thermostats with local REST API enabled
  - Tested with hardware v1.1 (257) and firmware v1.11 (267)
- Home Assistant integration
- Python 3.11, 3.12, 3.13+

## Advanced Usage

### Firmware and Hardware Version Decoding

The thermostat reports firmware and hardware versions as encoded integers. The library automatically decodes these to human-readable strings:

```python
status = await client.get_statuses()

# Raw version numbers (as stored in device)
print(f"Raw HW version: {status.hw_version}")  # e.g., 267
print(f"Raw FW version: {status.fw_version}")  # e.g., 257

# Human-readable version strings (automatically decoded)
print(f"HW version: v{status.hw_version_string}")  # e.g., v1.11
print(f"FW version: v{status.fw_version_string}")  # e.g., v1.1

# Version encoding: value = major * 256 + minor
# Example: 267 = 1 * 256 + 11 = v1.11
#          257 = 1 * 256 + 1  = v1.1
```

### Input Validation

All setter methods validate input values before sending to the API:

```python
# These will raise AirobotError before making API calls
await client.set_home_temperature(40.0)   # Error: outside 5-35°C range
await client.set_hysteresis_band(1.0)     # Error: outside 0-0.5°C range
await client.set_device_name("x" * 25)    # Error: name too long (max 20 chars)
await client.set_mode(3)                  # Error: invalid mode (must be 1 or 2)
```

### Strict Validation Mode

For testing or strict data validation scenarios, enable strict mode to raise exceptions for out-of-range sensor values:

```python
from pyairobotrest.models import ThermostatStatus

# Normal mode (default): logs warnings for out-of-range values
status = ThermostatStatus.from_dict(api_response_data)

# Strict mode: raises ValueError for out-of-range values
try:
    status = ThermostatStatus.from_dict(api_response_data, strict=True)
except ValueError as e:
    print(f"Invalid sensor data: {e}")
```

**Use cases for strict mode:**

- Unit testing with known good data
- Development and debugging
- Data quality validation pipelines

**Default mode (strict=False) is recommended for production** as it handles edge cases gracefully by logging warnings.

### Polling Best Practices

The thermostat measures air every 30 seconds, which is the minimum recommended polling interval:

```python
import asyncio

async def monitor_temperature():
    async with AirobotClient("192.168.1.100", "T01XXXXXX", "password") as client:
        while True:
            status = await client.get_statuses()
            print(f"Temperature: {status.temp_air:.1f}°C")
            await asyncio.sleep(30)  # Poll every 30 seconds
```

## Troubleshooting

### Connection Issues

1. **Check IP address**: Ensure the thermostat IP is correct
2. **Local API enabled**: Verify "Connectivity - Local API – Enable" is turned on
3. **Network connectivity**: Ensure the thermostat is on the same network
4. **Credentials**: Check username (Device ID) and password from "Mobile app" screen

### Common Error Patterns

```python
# Handle specific error types
try:
    await client.get_statuses()
except AirobotAuthError:
    print("Authentication failed - check credentials")
except AirobotTimeoutError:
    print("Request timed out - thermostat may be offline")
except AirobotConnectionError as err:
    if "refused" in str(err).lower():
        print("Connection refused - check IP and ensure local API is enabled")
    else:
        print(f"Connection error: {err}")
```

## License

MIT License - see LICENSE file for details.

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines on:

- Setting up your development environment
- Code style and testing requirements
- Submitting pull requests
- Reporting bugs and requesting features

For major changes, please open an issue first to discuss what you would like to change.

## Credits

This library is designed to work with [Airobot](https://airobot.net/) TE1 thermostats and is used by the [Home Assistant Airobot integration](https://www.home-assistant.io/integrations/airobot/).

**Key Features Developed:**

- Complete REST API implementation for Airobot thermostats
- Comprehensive error handling and input validation
- Type-safe API with full asyncio support
- Individual setter methods for granular control
- Production-ready reliability and performance
