alnor-sdk 0.3.1
Python SDK for controlling Alnor ventilation devices via Modbus TCP and cloud API
License: MIT
Requires Python: >=3.13
Keywords: alnor, ventilation, hvac, modbus, automation

> **🤖 AI-Generated Code**: This entire SDK was generated by [Claude Code](https://claude.com/claude-code).
> All code, documentation, tests, and CI/CD infrastructure were created through AI assistance.

# Alnor SDK

Python SDK for controlling Alnor ventilation devices via **Modbus TCP** (local) or **Cloud API** (remote).

[![CI](https://github.com/nashant/alnor-sdk/workflows/CI/badge.svg)](https://github.com/nashant/alnor-sdk/actions)
[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

> **⚠️ Development Status**: This SDK is in active development and requires Python 3.13+ for modern type system features.
>
> **⚠️ Testing Status**: Currently tested only with **HRU-PremAir-450** heat recovery unit via **Cloud API**.
> **Modbus TCP functionality is completely untested** and may not work correctly. If you test with Modbus or other devices, please report your results!

## 🚀 Quick Start

**New to the SDK?** Check out the [Quick Start Guide](QUICKSTART.md) for a 5-minute tutorial!

```python
from alnor_sdk import AlnorClient, ProductType

async with AlnorClient() as client:
    device = await client.connect("my_hru", "192.168.1.100",
                                   ProductType.HEAT_RECOVERY_UNIT)
    state = await client.get_state(device.device_id)
    print(f"Temperature: {state.indoor_temperature}°C, Speed: {state.speed}%")
```

## Features

- **Two connection methods**: Local Modbus TCP or cloud API
- **Command-line interface**: Control devices from your terminal
- Async/await API for non-blocking I/O
- **Tested devices**:
  - HRU-PremAir-450 (Heat Recovery Unit) via Cloud API
- Device discovery and connection management
- Type-safe models with Pydantic
- Comprehensive error handling
- Protocol-based architecture for flexibility

## Installation

### SDK Only

```bash
pip install alnor-sdk
```

### With CLI Tool

```bash
# Install with command-line interface
pip install alnor-sdk[cli]

# Or with interactive TUI (Terminal UI)
pip install alnor-sdk[cli,tui]
```

### For Development

```bash
pip install alnor-sdk[dev]
```

## CLI Tool

The SDK includes a powerful command-line interface for controlling devices:

```bash
# Connect to a device
alnor device connect 192.168.1.100 living_room --type HRU

# Check device status
alnor device status living_room

# Control device speed
alnor control speed living_room 75

# Set ventilation mode
alnor control mode living_room auto

# Monitor device in real-time
alnor monitor living_room

# Zone management
alnor zone create upstairs
alnor zone add upstairs living_room
alnor zone control upstairs 60
```

**Features:**
- Beautiful terminal output with colors and tables
- JSON output for scripting and automation
- Real-time monitoring with live updates
- Zone management for controlling multiple devices
- Configuration persistence across sessions

See the [CLI Documentation](docs/CLI.md) for complete usage guide.

## Connection Methods

### Local Modbus TCP (Direct Connection)
Connect directly to Alnor devices on your local network using the Modbus TCP protocol on port 502. This method:
- Requires devices to have local Modbus enabled
- Works without internet connection
- Provides fastest response times
- Best for home automation systems

### Cloud API (Remote Connection)
Connect to devices through the Alnor cloud gateway at `api.connect2myhome.eu`. This method:
- Requires Alnor cloud account (same as mobile app)
- Works from anywhere with internet
- Goes through internet gateway device
- No special firewall configuration needed

**Note**: Many Alnor internet gateways only support cloud mode by default. If port 502 is closed on your device, use the cloud API method.

## Quick Start

### Local Modbus TCP Connection

```python
import asyncio
from alnor_sdk import AlnorClient, ProductType

async def main():
    async with AlnorClient() as client:
        # Connect to a device
        device = await client.connect(
            device_id="living_room_hrv",
            host="192.168.1.100",
            product_type=ProductType.HEAT_RECOVERY_UNIT
        )

        # Set ventilation speed to 75%
        await client.set_speed(device.device_id, 75)

        # Get current state
        state = await client.get_state(device.device_id)
        print(f"Speed: {state.speed}%")
        print(f"Indoor temperature: {state.indoor_temperature}°C")
        print(f"Filter days remaining: {state.filter_days_remaining}")

asyncio.run(main())
```

### Cloud API Connection

#### Option 1: Using Environment Variables (Recommended)

Set environment variables:
```bash
export ALNOR_CLOUD_USERNAME=your_email@example.com
export ALNOR_CLOUD_PASSWORD=your_password
```

Then use the client without passing credentials:
```python
import asyncio
from alnor_sdk.communication import AlnorCloudApi, CloudClient
from alnor_sdk.controllers import HeatRecoveryUnitController
from alnor_sdk.models.enums import VentilationMode

async def main():
    # Credentials loaded automatically from environment variables
    async with AlnorCloudApi() as api:
        await api.connect()

        # Get your bridges (gateways)
        bridges = await api.get_bridges()
        bridge_id = bridges[0]["bridgeId"]

        # Get devices
        devices = await api.get_devices(bridge_id)
        device_id = devices[0]["deviceId"]
        product_id = devices[0]["productId"]

        # Create register-based client and controller
        client = CloudClient(api, device_id)
        controller = HeatRecoveryUnitController(client, device_id, product_id)

        # Set ventilation mode
        await controller.set_mode(VentilationMode.HOME)

        # Get current state
        state = await controller.get_state()
        print(f"Speed: {state.speed}%")
        print(f"Indoor temperature: {state.indoor_temperature}°C")
        print(f"Filter: {state.filter_days_remaining} days remaining")

asyncio.run(main())
```

#### Option 2: Passing Credentials Directly

```python
async with AlnorCloudApi("your_email@example.com", "your_password") as api:
    await api.connect()
    # ... rest of code
```

See `examples/cloud_control.py` for a complete cloud API example.

## Supported Devices

> ⚠️ **Testing Status**: Only the **HRU-PremAir-450** has been tested and verified to work. All other devices listed below are based on product IDs found in the SDK but remain **completely untested**. If you test any of these devices, please report your results!

### Exhaust Fans
- ⚠️ **VMC-02VJ04** (Product ID: 0001c844) - **UNTESTED**

### Heat Recovery Units
- ✅ **HRU-PremAir-450** - **TESTED & WORKING** (via Cloud API)
- ⚠️ **VMD-02RMS37-2** (Product ID: 0001c89f) - **UNTESTED**
- ⚠️ **VMD-02RPS54** (Product ID: 0001c84f) - **UNTESTED**
- ⚠️ **VMD-02RPS66** (Product ID: 0001c88e) - **UNTESTED**
- ⚠️ **VMD-02RPS78** (Product ID: 0001c8a0) - **UNTESTED**

### CO2 Sensors
- ⚠️ **VMS-02C05** (Product ID: 0001c845) - **UNTESTED**
- ⚠️ **VMI-02MC02** (Product ID: 0001c86c) - **UNTESTED**

### Humidity Sensors
- ⚠️ **VMS-02HB04** (Product ID: 0001c846) - **UNTESTED**
- ⚠️ **VMI-02MPH04** (Product ID: 0001c86a) - **UNTESTED**

## Usage Examples

### Device Discovery

```python
async with AlnorClient() as client:
    # Scan network for devices
    devices = await client.discover_devices([
        "192.168.1.100",
        "192.168.1.101",
        "192.168.1.102"
    ])

    for device in devices:
        print(f"Found: {device.device_id} at {device.host}")
```

### Reading Sensors

```python
async with AlnorClient() as client:
    # Connect to CO2 sensor
    device = await client.connect(
        device_id="bedroom_co2",
        host="192.168.1.101",
        product_type=ProductType.CO2_SENSOR_VMS
    )

    # Read CO2 level
    co2_level = await client.read_co2(device.device_id)
    print(f"CO2: {co2_level} ppm")

    # Connect to humidity sensor
    device = await client.connect(
        device_id="bathroom_humidity",
        host="192.168.1.102",
        product_type=ProductType.HUMIDITY_SENSOR_VMS
    )

    # Read temperature and humidity
    temperature, humidity = await client.read_humidity(device.device_id)
    print(f"Temperature: {temperature}°C, Humidity: {humidity}%")
```

### Zone Management

```python
async with AlnorClient() as client:
    # Connect devices
    await client.connect("living_room", "192.168.1.100", ProductType.HEAT_RECOVERY_UNIT)
    await client.connect("bedroom", "192.168.1.101", ProductType.EXHAUST_FAN)

    # Create zone
    zone = await client.create_zone("upstairs", "Upstairs Zone")

    # Add devices to zone
    await client.add_device_to_zone("upstairs", "living_room")
    await client.add_device_to_zone("upstairs", "bedroom")

    # Control all devices in zone
    await client.control_zone("upstairs", speed=60)
```

### Ventilation Modes

```python
from alnor_sdk import VentilationMode

async with AlnorClient() as client:
    device = await client.connect("hrv", "192.168.1.100", ProductType.HEAT_RECOVERY_UNIT)

    # Set mode
    await client.set_mode(device.device_id, VentilationMode.AUTO)

    # Available modes:
    # - VentilationMode.AWAY  (low speed)
    # - VentilationMode.HOME  (medium speed)
    # - VentilationMode.AUTO  (automatic)
    # - VentilationMode.PARTY (high speed)
```

## Architecture

### Protocol
- **Modbus TCP**: Industry-standard protocol for device communication
- **Port**: 502 (default Modbus TCP port)
- **Registers**: Holding registers (40001-49999 range)

### Models
- **Device**: Physical device representation
- **DeviceState**: Current state including sensor readings
- **Product**: Product catalog information
- **Zone**: Logical grouping of devices

### Controllers
- **ExhaustFanController**: Controls exhaust fan devices
- **HeatRecoveryUnitController**: Controls heat recovery units
- **SensorController**: Reads sensor data

## Development

### Setup
```bash
git clone https://github.com/nashant/alnor-sdk
cd alnor-sdk
python -m venv venv
source venv/bin/activate
pip install -e .[dev]
```

### Testing
```bash
# Run tests
pytest

# Run with coverage
pytest --cov=alnor_sdk --cov-report=html

# Type checking
mypy src/alnor_sdk

# Linting
pylint src/alnor_sdk

# Formatting
black src/ tests/
isort src/ tests/
```

### Building
```bash
python -m build
```

## Protocol Details

### Modbus Register Mapping

#### Exhaust Fan (VMC-02VJ04)
- `41001`: Fan speed (read)
- `41003`: Ventilation speed (read)
- `41051`: Ventilation speed set (write)

#### Heat Recovery Unit (VMD-02RMS37-2, etc.)
- `41000`: Ventilation speed (read)
- `41001-41002`: Fan speeds (read)
- `41005-41011`: Temperatures (read, value/10)
- `41040`: Filter days remaining (read)
- `41500`: Ventilation speed set (write)

#### Sensors
- CO2: `41000` (read, ppm)
- Humidity: `41000` (temperature, value/10), `41001` (humidity %)

## Troubleshooting

### Local Modbus Connection Issues
- Ensure device is powered and connected to network
- Verify IP address and port 502 is accessible
- Check firewall settings
- **Port 502 closed?** Your gateway may only support cloud mode - use Cloud API instead

### Cloud Authentication Issues
- Error 10103 (Invalid credentials):
  - Verify username/password in the Alnor mobile app
  - Check for verification emails from `alnor.eu` or `connect2myhome.eu`
  - Try resetting password in the mobile app
  - Ensure account is verified (click email verification link)
- Error 10100 (User not found):
  - Create account in the Alnor mobile app first
- Error 10102 (Account locked):
  - Contact Alnor support to unlock account

### Timeout Errors
- Increase timeout: `AlnorClient(timeout=10.0)` or `AlnorCloudApi(username, password, timeout=15.0)`
- Check network latency
- For cloud API: Verify gateway is online and connected

### Register Read/Write Errors
- Verify device type matches ProductType
- Check Modbus register addresses for your device model
- For cloud API: Check device is connected to gateway

## Testing

The SDK includes comprehensive unit and integration tests.

### Running Unit Tests

Unit tests require no external dependencies (devices or network):

```bash
# Run all unit tests
pytest tests/unit -m unit -v

# Run with coverage report
pytest tests/unit --cov=alnor_sdk --cov-report=html

# View coverage report
open htmlcov/index.html
```

### Running Integration Tests

Integration tests require real devices and credentials:

```bash
# Set up environment variables
export ALNOR_DEVICE_HOST=192.168.1.100
export ALNOR_CLOUD_USERNAME=your_email@example.com
export ALNOR_CLOUD_PASSWORD=your_password

# Run integration tests
pytest tests/integration -m integration -v

# Run all tests
pytest -v
```

Required environment variables for integration tests:
- `ALNOR_DEVICE_HOST`: IP address of your Modbus device
- `ALNOR_CLOUD_USERNAME`: Your Alnor cloud email
- `ALNOR_CLOUD_PASSWORD`: Your Alnor cloud password

### Test Markers

Tests are organized using pytest markers:
- `@pytest.mark.unit`: Fast tests with no external dependencies
- `@pytest.mark.integration`: Tests requiring real devices/network
- `@pytest.mark.slow`: Long-running tests

```bash
# Run only fast unit tests
pytest -m unit

# Run everything except slow tests
pytest -m "not slow"
```

## License

MIT License - see LICENSE file for details

## Project Structure

```
alnor-sdk/
├── src/alnor_sdk/       # Main SDK source code
│   ├── communication/   # Modbus and Cloud API clients
│   ├── controllers/     # Device controllers
│   ├── models/          # Pydantic data models
│   └── utils/           # Utility functions and constants
├── tests/               # Test suite
│   ├── unit/            # Unit tests (no external dependencies)
│   └── integration/     # Integration tests (requires devices)
├── examples/            # Usage examples
│   ├── basic_connection.py
│   ├── cloud_control.py
│   └── ...
├── scripts/             # Development/debug scripts (NOT part of SDK)
│   ├── test_*.py        # Debug/integration scripts
│   └── README.md        # ⚠️ See warning about credentials
└── docs/                # Documentation
```

**Note**: The `scripts/` directory contains development and debugging scripts that may have hardcoded credentials. These are **not** part of the official SDK API. Use the `examples/` directory for proper usage examples.

## Contributing

Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes with tests
4. Submit a pull request

## Disclaimer

This SDK is an unofficial implementation based on reverse engineering the Alnor Android app.
Use at your own risk. The author is not affiliated with Alnor.