Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to this project will be documented in this file.

## [0.3.0] - 2025-08-15

### Added

- Implementation of `[AP|Sta]-[MODE]` to Enums.
- Added update check (non-forced) endpoint
- Added warnings fetch endpoint

## [0.2.11] - 2025-08-14

### Changed
Expand Down
81 changes: 80 additions & 1 deletion airos/airos8.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
import aiohttp
from mashumaro.exceptions import InvalidFieldValue, MissingField

from .data import AirOS8Data as AirOSData, redact_data_smart
from .data import (
AirOS8Data as AirOSData,
DerivedWirelessMode,
DerivedWirelessRole,
redact_data_smart,
)
from .exceptions import (
AirOSConnectionAuthenticationError,
AirOSConnectionSetupError,
Expand Down Expand Up @@ -54,6 +59,8 @@ def __init__(
self._status_cgi_url = f"{self.base_url}/status.cgi" # AirOS 8
self._stakick_cgi_url = f"{self.base_url}/stakick.cgi" # AirOS 8
self._provmode_url = f"{self.base_url}/api/provmode" # AirOS 8
self._warnings_url = f"{self.base_url}/api/warnings" # AirOS 8
self._update_check_url = f"{self.base_url}/api/fw/update-check" # AirOS 8
self.current_csrf_token: str | None = None

self._use_json_for_login_post = False
Expand Down Expand Up @@ -201,6 +208,8 @@ def derived_data(response: dict[str, Any]) -> dict[str, Any]:
"access_point": False,
"ptp": False,
"ptmp": False,
"role": DerivedWirelessRole.STATION,
"mode": DerivedWirelessMode.PTP,
}

# Access Point / Station vs PTP/PtMP
Expand All @@ -209,12 +218,16 @@ def derived_data(response: dict[str, Any]) -> dict[str, Any]:
case "ap-ptmp":
derived["access_point"] = True
derived["ptmp"] = True
derived["role"] = DerivedWirelessRole.ACCESS_POINT
derived["mode"] = DerivedWirelessMode.PTMP
case "sta-ptmp":
derived["station"] = True
derived["ptmp"] = True
derived["mode"] = DerivedWirelessMode.PTMP
case "ap-ptp":
derived["access_point"] = True
derived["ptp"] = True
derived["role"] = DerivedWirelessRole.ACCESS_POINT
case "sta-ptp":
derived["station"] = True
derived["ptp"] = True
Expand Down Expand Up @@ -384,3 +397,69 @@ async def provmode(self, active: bool = False) -> bool:
except asyncio.CancelledError:
_LOGGER.info("Provisioning mode change task was cancelled")
raise

async def warnings(self) -> dict[str, Any] | Any:
"""Get warnings."""
if not self.connected:
_LOGGER.error("Not connected, login first")
raise AirOSDeviceConnectionError from None

request_headers = {**self._common_headers}
if self.current_csrf_token:
request_headers["X-CSRF-ID"] = self.current_csrf_token

# Formal call is '/api/warnings?_=1755249683586'
try:
async with self.session.get(
self._warnings_url,
headers=request_headers,
) as response:
response_text = await response.text()
if response.status == 200:
return json.loads(response_text)
log = f"Unable to fech warning status {response.status} with {response_text}"
_LOGGER.error(log)
raise AirOSDataMissingError from None
except json.JSONDecodeError:
_LOGGER.exception("JSON Decode Error in warning response")
raise AirOSDataMissingError from None
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.exception("Error during call to retrieve warnings: %s", err)
raise AirOSDeviceConnectionError from err
except asyncio.CancelledError:
_LOGGER.info("Warning check task was cancelled")
raise

async def update_check(self) -> dict[str, Any] | Any:
"""Get warnings."""
if not self.connected:
_LOGGER.error("Not connected, login first")
raise AirOSDeviceConnectionError from None

request_headers = {**self._common_headers}
if self.current_csrf_token:
request_headers["X-CSRF-ID"] = self.current_csrf_token
request_headers["Content-type"] = "application/json"

# Post without data
try:
async with self.session.post(
self._update_check_url,
headers=request_headers,
json={},
) as response:
response_text = await response.text()
if response.status == 200:
return json.loads(response_text)
log = f"Unable to fech update status {response.status} with {response_text}"
_LOGGER.error(log)
raise AirOSDataMissingError from None
except json.JSONDecodeError:
_LOGGER.exception("JSON Decode Error in warning response")
raise AirOSDataMissingError from None
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.exception("Error during call to retrieve update status: %s", err)
raise AirOSDeviceConnectionError from err
except asyncio.CancelledError:
_LOGGER.info("Warning update status task was cancelled")
raise
23 changes: 20 additions & 3 deletions airos/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@ class IeeeMode(Enum):
# More to be added when known


class DerivedWirelessRole(Enum):
"""Enum definition."""

STATION = "station"
ACCESS_POINT = "access_point"


class DerivedWirelessMode(Enum):
"""Enum definition."""

PTP = "point_to_point"
PTMP = "point_to_multipoint"


class WirelessMode(Enum):
"""Enum definition."""

Expand Down Expand Up @@ -350,7 +364,7 @@ class Remote(AirOSDataClass):
rssi: int
noisefloor: int
tx_power: int
distance: int
distance: int # In meters
rx_chainmask: int
chainrssi: list[int]
tx_ratedata: list[int]
Expand Down Expand Up @@ -408,7 +422,7 @@ class Station(AirOSDataClass):
tx_nss: int
rx_nss: int
tx_latency: int
distance: int
distance: int # In meters
tx_packets: int
tx_lretries: int
tx_sretries: int
Expand Down Expand Up @@ -446,7 +460,7 @@ class Wireless(AirOSDataClass):
frequency: int
center1_freq: int
dfs: int
distance: int
distance: int # In meters
security: Security
noisef: int
txpower: int
Expand Down Expand Up @@ -554,6 +568,9 @@ class Derived(AirOSDataClass):
ptp: bool
ptmp: bool

role: DerivedWirelessRole
mode: DerivedWirelessMode


@dataclass
class AirOS8Data(AirOSDataClass):
Expand Down
2 changes: 2 additions & 0 deletions fixtures/airos_LiteBeam5AC_ap-ptp_30mhz.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"access_point": true,
"mac": "68:D7:9A:9A:08:BB",
"mac_interface": "br0",
"mode": "point_to_point",
"ptmp": false,
"ptp": true,
"role": "access_point",
"station": false
},
"firewall": {
Expand Down
2 changes: 2 additions & 0 deletions fixtures/airos_LiteBeam5AC_sta-ptp_30mhz.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"access_point": false,
"mac": "68:D7:9A:98:FB:FF",
"mac_interface": "br0",
"mode": "point_to_point",
"ptmp": false,
"ptp": true,
"role": "station",
"station": true
},
"firewall": {
Expand Down
2 changes: 2 additions & 0 deletions fixtures/airos_liteapgps_ap_ptmp_40mhz.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"access_point": true,
"mac": "04:11:22:33:19:7E",
"mac_interface": "br0",
"mode": "point_to_multipoint",
"ptmp": true,
"ptp": false,
"role": "access_point",
"station": false
},
"firewall": {
Expand Down
2 changes: 2 additions & 0 deletions fixtures/airos_loco5ac_ap-ptp.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"access_point": true,
"mac": "01:23:45:67:89:AB",
"mac_interface": "br0",
"mode": "point_to_point",
"ptmp": false,
"ptp": true,
"role": "access_point",
"station": false
},
"firewall": {
Expand Down
2 changes: 2 additions & 0 deletions fixtures/airos_loco5ac_sta-ptp.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"access_point": false,
"mac": "01:23:45:67:89:CD",
"mac_interface": "br0",
"mode": "point_to_point",
"ptmp": false,
"ptp": true,
"role": "station",
"station": true
},
"firewall": {
Expand Down
2 changes: 2 additions & 0 deletions fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"access_point": false,
"mac": "22:22:33:44:31:38",
"mac_interface": "br0",
"mode": "point_to_multipoint",
"ptmp": true,
"ptp": false,
"role": "station",
"station": true
},
"firewall": {
Expand Down
2 changes: 2 additions & 0 deletions fixtures/airos_nanostation_ap-ptp_8718_missing_gps.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"access_point": true,
"mac": "00:11:22:33:34:66",
"mac_interface": "br0",
"mode": "point_to_point",
"ptmp": false,
"ptp": true,
"role": "access_point",
"station": false
},
"firewall": {
Expand Down
1 change: 1 addition & 0 deletions fixtures/update_check_available.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"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"}
1 change: 1 addition & 0 deletions fixtures/warnings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"isDefaultPasswd": false, "customScripts": false, "isWatchdogReset": 0, "label": 0, "chAvailable": false, "emergReasonCode": -1, "firmware": {"requirePasswd": false, "isThirdParty": false, "version": "", "uploaded": false}}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "airos"
version = "0.2.11"
version = "0.3.0"
license = "MIT"
description = "Ubiquity airOS module(s) for Python 3."
readme = "README.md"
Expand Down
2 changes: 2 additions & 0 deletions script/generate_ha_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ def generate_airos_fixtures() -> None:

except json.JSONDecodeError:
_LOGGER.error("Skipping '%s': Not a valid JSON file.", filename)
raise
except Exception as e:
_LOGGER.error("Error processing '%s': %s", filename, e)
raise


if __name__ == "__main__":
Expand Down
Loading
Loading