Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ radon==6.0.1
types-aiofiles==24.1.0.20250809
mypy==1.17.1
pylint==3.3.7
aiofiles==24.1.0
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