Skip to content

Commit b19f019

Browse files
committed
Add detection helper
1 parent aa10da7 commit b19f019

File tree

3 files changed

+209
-1
lines changed

3 files changed

+209
-1
lines changed

airos/helpers.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Ubiquiti AirOS firmware helpers."""
2+
3+
from typing import TypedDict
4+
5+
import aiohttp
6+
7+
from .airos6 import AirOS6
8+
from .airos8 import AirOS
9+
from .exceptions import AirOSKeyDataMissingError
10+
11+
12+
class DetectDeviceData(TypedDict):
13+
"""Container for device data."""
14+
15+
fw_major: int
16+
mac: str
17+
hostname: str
18+
19+
20+
async def async_get_firmware_data(
21+
host: str,
22+
username: str,
23+
password: str,
24+
session: aiohttp.ClientSession,
25+
use_ssl: bool = True,
26+
) -> DetectDeviceData:
27+
"""Connect to a device and return the major firmware version."""
28+
detect = AirOS(host, username, password, session, use_ssl)
29+
30+
await detect.login()
31+
raw_status = await detect._request_json( # noqa: SLF001
32+
"GET",
33+
detect._status_cgi_url, # noqa: SLF001
34+
authenticated=True,
35+
)
36+
37+
fw_version = (raw_status.get("host") or {}).get("fwversion")
38+
if not fw_version:
39+
raise AirOSKeyDataMissingError("Missing host.fwversion in API response")
40+
41+
try:
42+
fw_major = int(fw_version.lstrip("v").split(".", 1)[0])
43+
except (ValueError, AttributeError) as exc:
44+
raise AirOSKeyDataMissingError(
45+
f"Invalid firmware version '{fw_version}'"
46+
) from exc
47+
48+
if fw_major == 6:
49+
derived_data = AirOS6._derived_data_helper( # noqa: SLF001
50+
raw_status, AirOS6.derived_wireless_data
51+
)
52+
else: # Assume AirOS 8 for all other versions
53+
derived_data = AirOS._derived_data_helper( # noqa: SLF001
54+
raw_status, AirOS.derived_wireless_data
55+
)
56+
57+
# Extract MAC address and hostname from the derived data
58+
hostname = derived_data.get("host", {}).get("hostname")
59+
mac = derived_data.get("derived", {}).get("mac")
60+
61+
if not hostname:
62+
raise AirOSKeyDataMissingError("Missing hostname")
63+
64+
if not mac:
65+
raise AirOSKeyDataMissingError("Missing MAC address")
66+
67+
return {
68+
"fw_major": fw_major,
69+
"mac": mac,
70+
"hostname": hostname,
71+
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "airos"
7-
version = "0.5.0a2"
7+
version = "0.5.0a3"
88
license = "MIT"
99
description = "Ubiquiti airOS module(s) for Python 3."
1010
readme = "README.md"

tests/test_helpers.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""Test helpers for Ubiquiti airOS devices."""
2+
3+
from typing import Any
4+
from unittest.mock import AsyncMock, MagicMock, patch
5+
6+
import aiohttp
7+
import pytest
8+
9+
from airos.airos8 import AirOS
10+
from airos.exceptions import AirOSKeyDataMissingError
11+
from airos.helpers import DetectDeviceData, async_get_firmware_data
12+
13+
# pylint: disable=redefined-outer-name
14+
15+
16+
@pytest.fixture
17+
def mock_session() -> MagicMock:
18+
"""Return a mock aiohttp ClientSession."""
19+
return MagicMock(spec=aiohttp.ClientSession)
20+
21+
22+
@pytest.mark.asyncio
23+
@pytest.mark.parametrize(
24+
(
25+
"mock_response",
26+
"expected_fw_major",
27+
"expected_mac",
28+
"expected_hostname",
29+
"expected_exception",
30+
),
31+
[
32+
# Success case for AirOS 8
33+
(
34+
{
35+
"host": {"fwversion": "v8.7.4", "hostname": "test-host-8"},
36+
"interfaces": [
37+
{"hwaddr": "AA:BB:CC:DD:EE:FF", "ifname": "br0", "enabled": True}
38+
],
39+
},
40+
8,
41+
"AA:BB:CC:DD:EE:FF",
42+
"test-host-8",
43+
None,
44+
),
45+
# Success case for AirOS 6
46+
(
47+
{
48+
"host": {"fwversion": "v6.3.16", "hostname": "test-host-6"},
49+
"wireless": {"mode": "sta", "apmac": "11:22:33:44:55:66"},
50+
"interfaces": [
51+
{"hwaddr": "11:22:33:44:55:66", "ifname": "br0", "enabled": True}
52+
],
53+
},
54+
6,
55+
"11:22:33:44:55:66",
56+
"test-host-6",
57+
None,
58+
),
59+
# Failure case: Missing host key
60+
({"wireless": {}}, 0, "", "", AirOSKeyDataMissingError),
61+
# Failure case: Missing fwversion key
62+
(
63+
{"host": {"hostname": "test-host"}, "interfaces": []},
64+
0,
65+
"",
66+
"",
67+
AirOSKeyDataMissingError,
68+
),
69+
# Failure case: Invalid fwversion value
70+
(
71+
{
72+
"host": {"fwversion": "not-a-number", "hostname": "test-host"},
73+
"interfaces": [],
74+
},
75+
0,
76+
"",
77+
"",
78+
AirOSKeyDataMissingError,
79+
),
80+
# Failure case: Missing hostname key
81+
(
82+
{"host": {"fwversion": "v8.7.4"}, "interfaces": []},
83+
0,
84+
"",
85+
"",
86+
AirOSKeyDataMissingError,
87+
),
88+
# Failure case: Missing MAC address
89+
(
90+
{"host": {"fwversion": "v8.7.4", "hostname": "test-host"}},
91+
0,
92+
"",
93+
"",
94+
AirOSKeyDataMissingError,
95+
),
96+
],
97+
)
98+
async def test_firmware_detection(
99+
mock_session: aiohttp.ClientSession,
100+
mock_response: dict[str, Any],
101+
expected_fw_major: int,
102+
expected_mac: str,
103+
expected_hostname: str,
104+
expected_exception: Any,
105+
) -> None:
106+
"""Test helper firmware detection."""
107+
108+
mock_request_json = AsyncMock(
109+
side_effect=[
110+
{}, # First call for login()
111+
mock_response, # Second call for the status() endpoint
112+
]
113+
)
114+
115+
with patch.object(AirOS, "_request_json", new=mock_request_json):
116+
if expected_exception:
117+
with pytest.raises(expected_exception):
118+
await async_get_firmware_data(
119+
host="192.168.1.3",
120+
username="testuser",
121+
password="testpassword",
122+
session=mock_session,
123+
use_ssl=True,
124+
)
125+
else:
126+
# Test the success case
127+
device_data: DetectDeviceData = await async_get_firmware_data(
128+
host="192.168.1.3",
129+
username="testuser",
130+
password="testpassword",
131+
session=mock_session,
132+
use_ssl=True,
133+
)
134+
135+
assert device_data["fw_major"] == expected_fw_major
136+
assert device_data["mac"] == expected_mac
137+
assert device_data["hostname"] == expected_hostname

0 commit comments

Comments
 (0)