<div align="center">
<img src="https://github.com/home-assistant/brands/blob/master/core_brands/ubiquiti/icon.png?raw=true" alt="Ubiquiti airOS Logo" width="150" />
<h1>python-airos</h1>
<p>An asynchronous Python module to interact with Ubiquiti airOS devices, emulating a web browser client.</p>
</div>

<div align="center">

</div>

[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/python-airos)
[![CodeRabbit.ai is Awesome](https://img.shields.io/badge/AI-orange?label=CodeRabbit&color=orange&link=https%3A%2F%2Fcoderabbit.ai)](https://coderabbit.ai)
[![renovate maintained](https://img.shields.io/badge/maintained%20with-renovate-blue?logo=renovatebot)](https://github.com/compatech/python-airos/issues/8)

[![PyPI version fury.io](https://badge.fury.io/py/airos.svg)](https://pypi.python.org/pypi/airos/)
[![Latest release](https://github.com/compatech/python-airos/workflows/Latest%20release/badge.svg)](https://github.com/compatech/python-airos/actions)
[![Newest commit](https://github.com/compatech/python-airos/workflows/Latest%20commit/badge.svg)](https://github.com/compatech/python-airos/actions)

[![CodeFactor](https://www.codefactor.io/repository/github/compatech/python-airos/badge)](https://www.codefactor.io/repository/github/plugwise/python-airos)
[![codecov](https://codecov.io/gh/compatech/python-airos/graph/badge.svg?token=WI5K2IZWNS)](https://codecov.io/gh/compatech/python-airos)

[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=CoMPaTech_python-airos&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=CoMPaTech_python-airos)
[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=CoMPaTech_python-airos&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=CoMPaTech_python-airos)
[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=CoMPaTech_python-airos&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=CoMPaTech_python-airos)

# Overview

`python-airos` or [`airos`](https://pypi.org/projects/airos) from pypi is an asynchronous Python library designed to programmatically interact with Ubiquiti airOS devices. It mimics a web browser client to fetch device status, configuration, and perform actions like kicking connected stations.

This library is a key component for a potential future core integration with [Home Assistant](https://www.home-assistant.io), with the initial pull request for core integration targeted for the 2025.8 release.

More details on the integration can be found on the [Ubiquiti UISP airOS](https://www.home-assistant.io/integrations/airos/) page. To add airOS directly feel free to use the button below:

[![Open your Home Assistant instance and show your integrations.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/_change/?redirect=config_flow_start%2F%3Fdomain%3Dairos)

## Features

- Asynchronous Operations: Built with `asyncio` and `aiohttp` for non-blocking I/O, which is perfect for integrations and background tasks.
- Client Emulation: Authenticates and interacts with airOS devices by emulating a client browser, ensuring a high degree of compatibility.
- Data Retrieval: Fetches comprehensive device status information, including:
- Wireless mode and signal strength.
- Connected stations and their statistics.
- System information and uptime.
- Device Control: Provides methods to perform actions, such as reconnecting/kicking a connected wireless station and rebooting the device itself.
- Discovery of airOS devices on your local network (by listening to announcements these devices broadcast).

## Installation

You can install python-airos from PyPI using pip:

```Bash
pip install airos
```

## Usage

Here is a more detailed example of how to use the library to connect, fetch status, and perform an action on an airOS device.

```Python
import aiohttp
import asyncio
from airos.airos6 import AirOS6, AirOS6Data
from airos.airos8 import AirOS8, AirOS8Data
from airos.helpers import DetectDeviceData, async_get_firmware_data

async def main():
    """Main function to demonstrate library usage."""
    # Create an aiohttp session with SSL verification disabled.
    # Be cautious with this setting; it's useful for self-signed certificates
    # but not recommended for production environments without proper validation.
    session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False))

    # Connection data
    conn_data = {
        "host": "192.168.1.2",
        "username": "ubnt",
        "password": "password",
        "session": session
    }

    device_data: DetectDeviceData = await async_get_firmware_data(**conn_data)
    airos_class: type[AirOS8 | AirOS6] = AirOS8
    AirOSDataDetect = AirOS8Data | AirOS6Data

    if device_data["fw_major"] == 6:
        airos_class = AirOS6

    # Initialize the AirOS device object.
    airos_device = airos_class(**conn_data)


    try:
        # Step 1: Login to the device.
        login_result = await airos_device.login()
        print(f"Login successful: {login_result}")

        # Step 2: Fetch the device status.
        status_data = await airos_device.status()
        print("\n--- Device Status ---")
        print(f"Device Name: {status_data.host.hostname}")
        print(f"Wireless Mode: {status_data.wireless.mode}")
        print(f"Firmware Version: {status_data.host.fwversion}")

        # Fetch and display connected stations if available
        if status_data.wireless.sta:
            print("\n--- Connected Stations ---")
            for station in status_data.wireless.sta:
                print(f"  - MAC: {station.mac}")
                print(f"    Signal: {station.signal} dBm")
                print(f"    Uptime: {station.uptime} seconds")

        # Step 3: Perform an action, e.g., kick a station.
        # Replace '01:23:45:67:89:AB' with the MAC address of a station to kick.
        # kick_result = await device.stakick("01:23:45:67:89:AB")
        # print(f"\nKick station result: {kick_result}")

    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        # Ensure the aiohttp session is closed properly.
        await session.close()

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

## Supported API classes and calls

Note: For firmware 6 we only support the login and status calls currently.

### Classes

- `airos.data` (directly) as well as `airos.airos8` (indirectly) provides `AirOSData`, a [mashumaro](https://pypi.org/project/mashumaro/) based dataclass
- `airos.discovery` provides `AirOSDiscoveryProtocol` for the actual discovery, we recommend to use the `async_discover_devices` function for consumption as described below

### Calls

- `airos.airos8`: initializes with `host: str, username: str, password: str, session: aiohttp.ClientSession`

  - `login()`: Authenticates with the device.
  - `status()`: Fetches a comprehensive dictionary of the device's status and statistics.
  - `warnings()`: Retrieves warning status dict.

  - `stakick(mac_address: str)`: Disconnects a specific station by its MAC address.
  - `reboot()`: Reboots the device.
  - `provmode(active: bool = False)`: Enables or disables the provisioning mode.

  - `update_check(force: bool = False)`: Checks if new firmware has been discovered (or force to force check).

  - `download()`: Starts downloading (not installing) new firmware.
  - `progress()`: Fetches the firmware download (not install!) progress.
  - `install()`: Installs the new firmware.

- `airos.discovery`
  - `async_discover_devices(timeout: int)` mainly for consumption by HA's `config_flow` returning a dict mapping mac-addresses to discovered info.

#### Information

##### Update

Will return either ```{"update": False}``` or the full information regarding the available update:

```json
{"checksum": "b1bea879a9f518f714ce638172e3a860", "version": "v8.7.19", "security": "", "date": "250811", "url": "https://dl.ubnt.com/firmwares/XC-fw/v8.7.19/WA.v8.7.19.48279.250811.0636.bin", "update": True, "changelog": "https://dl.ubnt.com/firmwares/XC-fw/v8.7.19/changelog.txt"}
```

##### Progress

If no progress to report ```{"progress": -1}``` otherwise a positive value between 0 and 100.

##### Install

Only a positive outcome is expected from the user experience; the call should return:

```json
{
    "ok": true,
    "code": 0
}
```

#### Warnings

Will respond with something like:

```json
{
    "isDefaultPasswd": false,
    "customScripts": false,
    "isWatchdogReset": 0,
    "label": 0,
    "chAvailable": false,
    "emergReasonCode": -1,
    "firmware": {
        "isThirdParty": false,
        "version": "",
        "uploaded": false
    }
}
```

## Contributing

We welcome contributions as well as additional codeowners to python-airos.
