From 93cd2b831ae60c2023f06c4a2c8a86321717dd86 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 6 Aug 2025 15:18:08 +0200 Subject: [PATCH 1/6] Rename and improve discovery function --- airos/discovery.py | 6 ++++-- pyproject.toml | 2 +- tests/test_discovery.py | 12 ++++++------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/airos/discovery.py b/airos/discovery.py index 469980c..7b1b6de 100644 --- a/airos/discovery.py +++ b/airos/discovery.py @@ -276,7 +276,9 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None return parsed_info -async def async_discover_devices(timeout: int) -> dict[str, dict[str, Any]]: +async def airos_discover_devices( + timeout: int = 30, listen_ip: str = "0.0.0.0", port: int = DISCOVERY_PORT +) -> dict[str, dict[str, Any]]: """Discover unconfigured airOS devices on the network for a given timeout. This function sets up a listener, waits for a period, and returns @@ -301,7 +303,7 @@ async def _async_airos_device_found(device_info: dict[str, Any]) -> None: protocol, ) = await asyncio.get_running_loop().create_datagram_endpoint( lambda: AirOSDiscoveryProtocol(_async_airos_device_found), - local_addr=("0.0.0.0", DISCOVERY_PORT), + local_addr=(listen_ip, port), ) try: await asyncio.sleep(timeout) diff --git a/pyproject.toml b/pyproject.toml index 6e7b3d8..45ed8ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "airos" -version = "0.2.5" +version = "0.2.6a0" license = "MIT" description = "Ubiquity airOS module(s) for Python 3." readme = "README.md" diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 72eb032..a1710d1 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -8,7 +8,7 @@ from airos.discovery import ( DISCOVERY_PORT, AirOSDiscoveryProtocol, - async_discover_devices, + airos_discover_devices, ) from airos.exceptions import AirOSDiscoveryError, AirOSEndpointError, AirOSListenerError import pytest @@ -247,7 +247,7 @@ async def _simulate_discovery(): mock_protocol_factory(MagicMock()).callback(parsed_data) with patch("asyncio.sleep", new=AsyncMock()): - discovery_task = asyncio.create_task(async_discover_devices(timeout=1)) + discovery_task = asyncio.create_task(airos_discover_devices(timeout=1)) await _simulate_discovery() @@ -264,7 +264,7 @@ async def test_async_discover_devices_no_devices(mock_datagram_endpoint): mock_transport, _ = mock_datagram_endpoint with patch("asyncio.sleep", new=AsyncMock()): - result = await async_discover_devices(timeout=1) + result = await airos_discover_devices(timeout=1) assert result == {} mock_transport.close.assert_called_once() @@ -284,7 +284,7 @@ async def test_async_discover_devices_oserror(mock_datagram_endpoint): side_effect=OSError(98, "Address in use") ) - await async_discover_devices(timeout=1) + await airos_discover_devices(timeout=1) assert "address_in_use" in str(excinfo.value) mock_transport.close.assert_not_called() @@ -300,7 +300,7 @@ async def test_async_discover_devices_cancelled(mock_datagram_endpoint): patch("asyncio.sleep", new=AsyncMock(side_effect=asyncio.CancelledError)), pytest.raises(AirOSListenerError) as excinfo, ): - await async_discover_devices(timeout=1) + await airos_discover_devices(timeout=1) assert "cannot_connect" in str(excinfo.value) mock_transport.close.assert_called_once() @@ -373,7 +373,7 @@ async def test_async_discover_devices_generic_oserror(mock_datagram_endpoint): mock_loop.create_datagram_endpoint = AsyncMock( side_effect=OSError(13, "Permission denied") ) - await async_discover_devices(timeout=1) + await airos_discover_devices(timeout=1) assert "cannot_connect" in str(excinfo.value) mock_transport.close.assert_not_called() From 7f9a2c21584885759406ec4cec2ccd9cbd85a4b1 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 6 Aug 2025 16:44:33 +0200 Subject: [PATCH 2/6] New findings with nanobeam and liteac gps --- airos/data.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/airos/data.py b/airos/data.py index 522d57a..fd07921 100644 --- a/airos/data.py +++ b/airos/data.py @@ -152,6 +152,7 @@ class Polling: fixed_frame: bool gps_sync: bool ff_cap_rep: bool + flex_mode: int | None = None # Not present in all devices @dataclass @@ -207,9 +208,14 @@ class EthList: class GPSData: """Leaf definition.""" - lat: str - lon: str - fix: int + lat: str | None = None + lon: str | None = None + fix: int | None = None + sats: int | None = None # LiteAP GPS + dim: int | None = None # LiteAP GPS + dop: float | None = None # LiteAP GPS + alt: float | None = None # LiteAP GPS + time_synced: int | None = None # LiteAP GPS @dataclass @@ -235,7 +241,6 @@ class Remote: totalram: int freeram: int netrole: str - mode: WirelessMode sys_id: str tx_throughput: int rx_throughput: int @@ -263,11 +268,12 @@ class Remote: unms: UnmsStatus airview: int service: ServiceTime + mode: WirelessMode | None = None # Investigate why remotes can have no mode set @classmethod def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]: """Pre-deserialize hook for Wireless.""" - _check_and_log_unknown_enum_value(d, "mode", WirelessMode, "Wireless", "mode") + _check_and_log_unknown_enum_value(d, "mode", WirelessMode, "Remote", "mode") return d @@ -329,7 +335,6 @@ class Wireless: """Leaf definition.""" essid: str - mode: WirelessMode ieeemode: IeeeMode band: int compat_11n: int @@ -362,6 +367,7 @@ class Wireless: count: int sta: list[Station] sta_disconnected: list[Disconnected] + mode: WirelessMode | None = None # Investigate further (see WirelessMode in Remote) @classmethod def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]: From ac4418c7d2484ed6e7ba5a96799369487d9a404a Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 6 Aug 2025 19:18:14 +0200 Subject: [PATCH 3/6] Add redaction of data during exceptions, further testing and fixtures --- CHANGELOG.md | 13 + CONTRIBUTE.md | 2 + airos/airos8.py | 42 +- airos/data.py | 124 +- fixtures/airos_liteapgps_ap_ptmp_40mhz.json | 1080 +++++++++++++++++ fixtures/airos_loco5ac_ap-ptp.json | 8 +- fixtures/airos_loco5ac_sta-ptp.json | 8 +- fixtures/airos_mocked_sta-ptmp.json | 1059 ++++++++++++++++ .../airos_nanobeam5ac_sta_ptmp_40mhz.json | 652 ++++++++++ .../userdata/liteapgps_ap_ptmp_40mhz.json | 1 + .../mocked_invalid_wireless_mode.json | 159 +++ .../mocked_missing_wireless_mode.json | 158 +++ fixtures/userdata/mocked_sta-ptmp.json | 1 + .../userdata/nanobeam5ac_sta_ptmp_40mhz.json | 1 + pyproject.toml | 2 +- script/generate_ha_fixture.py | 27 + tests/test_stations.py | 113 +- 17 files changed, 3427 insertions(+), 23 deletions(-) create mode 100644 fixtures/airos_liteapgps_ap_ptmp_40mhz.json create mode 100644 fixtures/airos_mocked_sta-ptmp.json create mode 100644 fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json create mode 100644 fixtures/userdata/liteapgps_ap_ptmp_40mhz.json create mode 100644 fixtures/userdata/mocked_invalid_wireless_mode.json create mode 100644 fixtures/userdata/mocked_missing_wireless_mode.json create mode 100644 fixtures/userdata/mocked_sta-ptmp.json create mode 100644 fixtures/userdata/nanobeam5ac_sta_ptmp_40mhz.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e32147e..dc102b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## [0.2.6] - 2025-08-06 + +### Added + +- Added redaction of data in exceptions when requesting `status()` +- Additional settings in dataclass (HA Core Issue 150118) +- Added 'likely' mocked fixture for above issue +- Added additional devices (see [Contributing](CONTRIBUTE.md) for more information) + +### Changed + +- Changed name and kwargs for discovery function + ## [0.2.5] - 2025-08-05 ### Added diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index bd501f4..56e9f82 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -7,6 +7,8 @@ It would be very helpful if you would share your configuration data to this proj We currently have data on - Nanostation 5AC (LOCO5AC) - PTP - both AP and Station output of `/status.cgi` present (by @CoMPaTech) +- Nanobeam 5AC - PTMP - Station (by @PlayFaster) +- LiteAP GPS - PTMP - AP (by @PlayFaster) ## Secure your data diff --git a/airos/airos8.py b/airos/airos8.py index 8c00a22..0476509 100644 --- a/airos/airos8.py +++ b/airos/airos8.py @@ -10,7 +10,7 @@ import aiohttp from mashumaro.exceptions import InvalidFieldValue, MissingField -from .data import AirOS8Data as AirOSData +from .data import AirOS8Data as AirOSData, redact_data_smart from .exceptions import ( AirOSConnectionAuthenticationError, AirOSConnectionSetupError, @@ -268,28 +268,44 @@ async def status(self) -> AirOSData: if response.status == 200: try: response_json = json.loads(response_text) - try: - adjusted_json = self.derived_data(response_json) - airos_data = AirOSData.from_dict(adjusted_json) - except (MissingField, InvalidFieldValue) as err: - _LOGGER.exception("Failed to deserialize AirOS data") - raise AirOSKeyDataMissingError from err - - return airos_data + adjusted_json = self.derived_data(response_json) + airos_data = AirOSData.from_dict(adjusted_json) + except InvalidFieldValue as err: + # Log with .error() as this is a specific, known type of issue + redacted_data = redact_data_smart(response_json) + _LOGGER.error( + "Failed to deserialize AirOS data due to an invalid field value: %s", + redacted_data, + ) + raise AirOSKeyDataMissingError from err + except MissingField as err: + # Log with .exception() for a full stack trace + redacted_data = redact_data_smart(response_json) + _LOGGER.exception( + "Failed to deserialize AirOS data due to a missing field: %s", + redacted_data, + ) + raise AirOSKeyDataMissingError from err + except json.JSONDecodeError: _LOGGER.exception( "JSON Decode Error in authenticated status response" ) raise AirOSDataMissingError from None + + return airos_data else: - log = f"Authenticated status.cgi failed: {response.status}. Response: {response_text}" - _LOGGER.error(log) - raise AirOSDeviceConnectionError from None + _LOGGER.error( + "Status API call failed with status %d: %s", + response.status, + response_text, + ) + raise AirOSDeviceConnectionError except ( aiohttp.ClientError, aiohttp.client_exceptions.ConnectionTimeoutError, ) as err: - _LOGGER.exception("Error during authenticated status.cgi call") + _LOGGER.error("Status API call failed: %s", err) raise AirOSDeviceConnectionError from err async def stakick(self, mac_address: str = None) -> bool: diff --git a/airos/data.py b/airos/data.py index fd07921..d86590e 100644 --- a/airos/data.py +++ b/airos/data.py @@ -2,13 +2,41 @@ from dataclasses import dataclass from enum import Enum +import ipaddress import logging +import re from typing import Any from mashumaro import DataClassDictMixin logger = logging.getLogger(__name__) +# Regex for a standard MAC address format (e.g., 01:23:45:67:89:AB) +# This handles both colon and hyphen separators. +MAC_ADDRESS_REGEX = re.compile(r"^([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})$") + +# Regex for a MAC address mask (e.g., the redacted format 00:00:00:00:89:AB) +MAC_ADDRESS_MASK_REGEX = re.compile(r"^(00:){4}[0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}$") + + +def is_mac_address(value: str) -> bool: + """Check if a string is a valid MAC address.""" + return bool(MAC_ADDRESS_REGEX.match(value)) + + +def is_mac_address_mask(value: str) -> bool: + """Check if a string is a valid MAC address mask (e.g., the redacted format).""" + return bool(MAC_ADDRESS_MASK_REGEX.match(value)) + + +def is_ip_address(value: str) -> bool: + """Check if a string is a valid IPv4 or IPv6 address.""" + try: + ipaddress.ip_address(value) + return True + except ValueError: + return False + def _check_and_log_unknown_enum_value( data_dict: dict[str, Any], @@ -31,6 +59,98 @@ def _check_and_log_unknown_enum_value( del data_dict[key] +def redact_data_smart(data: dict) -> dict: + """Recursively redacts sensitive keys in a dictionary.""" + sensitive_keys = { + "hostname", + "essid", + "mac", + "apmac", + "hwaddr", + "lastip", + "ipaddr", + "ip6addr", + "device_id", + "sys_id", + "station_id", + "platform", + } + + def _redact(d: dict): + if not isinstance(d, dict): + return d + + redacted_d = {} + for k, v in d.items(): + if k in sensitive_keys: + if isinstance(v, str) and (is_mac_address(v) or is_mac_address_mask(v)): + # Redact only the first 6 hex characters of a MAC address + redacted_d[k] = "00:00:00:00:" + v.replace("-", ":").upper()[-5:] + elif isinstance(v, str) and is_ip_address(v): + # Redact to a dummy local IP address + redacted_d[k] = "127.0.0.3" + elif isinstance(v, list) and all( + isinstance(i, str) and is_ip_address(i) for i in v + ): + # Redact list of IPs to a dummy list + redacted_d[k] = ["127.0.0.3"] + else: + redacted_d[k] = "REDACTED" + elif isinstance(v, dict): + redacted_d[k] = _redact(v) + elif isinstance(v, list): + redacted_d[k] = [ + _redact(item) if isinstance(item, dict) else item for item in v + ] + else: + redacted_d[k] = v + return redacted_d + + return _redact(data) + + +def _redact_ip_addresses(addresses: str | list[str]) -> str | list[str]: + """Redacts the first three octets of an IPv4 address.""" + if isinstance(addresses, str): + addresses = [addresses] + + redacted_list = [] + for ip in addresses: + try: + parts = ip.split(".") + if len(parts) == 4: + # Keep the last octet, but replace the rest with a placeholder. + redacted_list.append(f"127.0.0.{parts[3]}") + else: + # Handle non-standard IPs or IPv6 if it shows up here + redacted_list.append("REDACTED") + except (IndexError, ValueError): + # In case the IP string is malformed + redacted_list.append("REDACTED") + + return redacted_list if isinstance(addresses, list) else redacted_list[0] + + +def _redact_mac_addresses(macs: str | list[str]) -> str | list[str]: + """Redacts the first four octets of a MAC address.""" + if isinstance(macs, str): + macs = [macs] + + redacted_list = [] + for mac in macs: + try: + parts = mac.split(":") + if len(parts) == 6: + # Keep the last two octets, replace the rest with a placeholder + redacted_list.append(f"00:11:22:33:{parts[4]}:{parts[5]}") + else: + redacted_list.append("REDACTED") + except (IndexError, ValueError): + redacted_list.append("REDACTED") + + return redacted_list if isinstance(macs, list) else redacted_list[0] + + class IeeeMode(Enum): """Enum definition.""" @@ -259,16 +379,16 @@ class Remote: rx_bytes: int antenna_gain: int cable_loss: int - height: int ethlist: list[EthList] ipaddr: list[str] - ip6addr: list[str] gps: GPSData oob: bool unms: UnmsStatus airview: int service: ServiceTime mode: WirelessMode | None = None # Investigate why remotes can have no mode set + ip6addr: list[str] | None = None # For v4 only devices + height: int | None = None @classmethod def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]: diff --git a/fixtures/airos_liteapgps_ap_ptmp_40mhz.json b/fixtures/airos_liteapgps_ap_ptmp_40mhz.json new file mode 100644 index 0000000..3a74542 --- /dev/null +++ b/fixtures/airos_liteapgps_ap_ptmp_40mhz.json @@ -0,0 +1,1080 @@ +{ + "chain_names": [ + { + "name": "Chain 0", + "number": 1 + }, + { + "name": "Chain 1", + "number": 2 + } + ], + "derived": { + "access_point": true, + "mac": "04:11:22:33:19:7E", + "mac_interface": "br0", + "ptmp": true, + "ptp": false, + "station": false + }, + "firewall": { + "eb6tables": false, + "ebtables": false, + "ip6tables": false, + "iptables": false + }, + "genuine": "/images/genuine.png", + "gps": { + "fix": 1, + "lat": 52.379894, + "lon": 4.901608 + }, + "host": { + "cpuload": 59.595959, + "device_id": "b222d222222f2222f0e2ecbcc22d2e22", + "devmodel": "LiteAP GPS", + "freeram": 13541376, + "fwversion": "v8.7.18", + "height": null, + "hostname": "House-Bridge", + "loadavg": 0.188965, + "netrole": "bridge", + "power_time": 1461661, + "temperature": 0, + "time": "2025-08-06 16:37:55", + "timestamp": 2148019169, + "totalram": 63447040, + "uptime": 81655 + }, + "interfaces": [ + { + "enabled": true, + "hwaddr": "05:11:22:33:19:7E", + "ifname": "eth0", + "mtu": 1500, + "status": { + "cable_len": 48, + "duplex": true, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": true, + "rx_bytes": 17307482485, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 208585470, + "snr": [ + 30, + 30, + 29, + 29 + ], + "speed": 1000, + "tx_bytes": 268785703196, + "tx_dropped": 1, + "tx_errors": 0, + "tx_packets": 212573426 + } + }, + { + "enabled": true, + "hwaddr": "04:11:22:33:19:7E", + "ifname": "ath0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": false, + "rx_bytes": 274358042002, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 212583924, + "snr": null, + "speed": 0, + "tx_bytes": 16450464430, + "tx_dropped": 227, + "tx_errors": 0, + "tx_packets": 150354889 + } + }, + { + "enabled": true, + "hwaddr": "04:11:22:33:19:7E", + "ifname": "br0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": null, + "ipaddr": "127.0.0.85", + "plugged": true, + "rx_bytes": 6053730278, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 51908268, + "snr": null, + "speed": 0, + "tx_bytes": 38072153, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 99493 + } + } + ], + "ntpclient": { + "last_sync": "2025-08-06 16:28:17" + }, + "portfw": false, + "provmode": {}, + "services": { + "airview": 2, + "dhcp6d_stateful": false, + "dhcpc": true, + "dhcpd": false, + "pppoe": false + }, + "unms": { + "status": 0, + "timestamp": null + }, + "wireless": { + "antenna_gain": 17, + "apmac": "03:11:22:33:19:7E", + "aprepeater": false, + "band": 2, + "cac_state": 0, + "cac_timeout": 0, + "center1_freq": 5690, + "chanbw": 40, + "compat_11n": 1, + "count": 2, + "dfs": 1, + "distance": 300, + "essid": "House-shed1", + "frequency": 5680, + "hide_essid": 0, + "ieeemode": "11ACVHT40", + "mode": "ap-ptmp", + "noisef": -92, + "nol_state": 0, + "nol_timeout": 0, + "polling": { + "atpc_status": 2, + "cb_capacity": 342000, + "dl_capacity": 342000, + "ff_cap_rep": false, + "fixed_frame": false, + "flex_mode": 1, + "gps_sync": false, + "rx_use": 98, + "tx_use": 168, + "ul_capacity": 342000, + "use": 266 + }, + "rstatus": 5, + "rx_chainmask": 3, + "rx_idx": 9, + "rx_nss": 2, + "security": "WPA2", + "service": { + "link": 1461418, + "time": 1461511 + }, + "sta": [ + { + "airmax": { + "actual_priority": 2, + "atpc_status": 4, + "beam": 0, + "cb_capacity": 343800, + "desired_priority": 2, + "dl_capacity": 342000, + "rx": { + "cinr": 33, + "evm": [ + [ + 33, + 34, + 37, + 33, + 34, + 34, + 35, + 34, + 35, + 36, + 33, + 33, + 31, + 34, + 33, + 33, + 33, + 32, + 32, + 33, + 34, + 31, + 33, + 34, + 34, + 36, + 33, + 35, + 34, + 34, + 33, + 35, + 34, + 36, + 33, + 39, + 31, + 33, + 34, + 35, + 31, + 29, + 32, + 33, + 36, + 33, + 33, + 33, + 32, + 35, + 33, + 32, + 32, + 32, + 35, + 37, + 34, + 34, + 32, + 34, + 36, + 33, + 32, + 31 + ], + [ + 42, + 42, + 42, + 42, + 43, + 42, + 42, + 43, + 43, + 43, + 42, + 42, + 42, + 41, + 43, + 42, + 42, + 43, + 42, + 42, + 42, + 42, + 43, + 43, + 43, + 42, + 44, + 42, + 42, + 42, + 42, + 42, + 43, + 43, + 41, + 42, + 42, + 41, + 42, + 42, + 42, + 42, + 42, + 42, + 41, + 42, + 41, + 43, + 41, + 42, + 42, + 43, + 41, + 44, + 43, + 43, + 43, + 42, + 44, + 42, + 42, + 42, + 42, + 42 + ] + ], + "usage": 34 + }, + "tx": { + "cinr": 33, + "evm": [ + [ + 34, + 31, + 33, + 35, + 34, + 35, + 34, + 31, + 34, + 33, + 33, + 29, + 26, + 35, + 34, + 35, + 35, + 28, + 32, + 32, + 32, + 27, + 28, + 36, + 34, + 32, + 31, + 33, + 28, + 34, + 35, + 33, + 33, + 34, + 37, + 33, + 33, + 32, + 29, + 32, + 34, + 37, + 29, + 33, + 32, + 33, + 33, + 32, + 29, + 32, + 32, + 31, + 32, + 32, + 35, + 32, + 33, + 31, + 35, + 33, + 33, + 30, + 32, + 33 + ], + [ + 44, + 43, + 43, + 43, + 43, + 43, + 43, + 44, + 42, + 44, + 43, + 43, + 43, + 44, + 44, + 44, + 44, + 44, + 44, + 44, + 43, + 43, + 44, + 44, + 43, + 42, + 43, + 43, + 43, + 44, + 43, + 43, + 43, + 43, + 44, + 44, + 44, + 43, + 44, + 43, + 44, + 43, + 43, + 43, + 43, + 43, + 43, + 43, + 42, + 43, + 43, + 43, + 43, + 43, + 43, + 43, + 42, + 43, + 43, + 43, + 43, + 43, + 42, + 44 + ] + ], + "usage": 6 + }, + "ul_capacity": 345600 + }, + "airos_connected": true, + "cb_capacity_expect": 286000, + "chainrssi": [ + 43, + 41, + 0 + ], + "distance": 300, + "dl_avg_linkscore": 100, + "dl_capacity_expect": 260000, + "dl_linkscore": 100, + "dl_rate_expect": 7, + "dl_signal_expect": -68, + "last_disc": 0, + "lastip": "127.0.0.82", + "mac": "00:11:22:33:2E:05", + "noisefloor": -92, + "remote": { + "age": 3, + "airview": 2, + "antenna_gain": 19, + "cable_loss": 0, + "chainrssi": [ + 44, + 39, + 0 + ], + "compat_11n": 0, + "cpuload": 13.0, + "device_id": "22222dd22222c2e2b22d0d2222aedb38", + "distance": 300, + "ethlist": [ + { + "cable_len": 1, + "duplex": true, + "enabled": true, + "ifname": "eth0", + "plugged": true, + "snr": [ + 30, + 29, + 30, + 30 + ], + "speed": 1000 + }, + { + "cable_len": 0, + "duplex": true, + "enabled": true, + "ifname": "eth1", + "plugged": false, + "snr": [ + 0, + 0, + 0, + 0 + ], + "speed": 0 + } + ], + "freeram": 20488192, + "gps": { + "alt": null, + "dim": null, + "dop": null, + "fix": 0, + "lat": "52.379894", + "lon": "4.901608", + "sats": null, + "time_synced": null + }, + "height": 2, + "hostname": "NanoBeam-shed2", + "ip6addr": null, + "ipaddr": [ + "127.0.0.82" + ], + "mode": "sta-ptmp", + "netrole": "bridge", + "noisefloor": -94, + "oob": false, + "platform": "NanoBeam 5AC", + "power_time": 16088831, + "rssi": 45, + "rx_bytes": 6168364701, + "rx_chainmask": 3, + "rx_throughput": 755, + "service": { + "link": 16087519, + "time": 16088594 + }, + "signal": -51, + "sys_id": "0xe7fc", + "temperature": 0, + "time": "2025-08-06 16:37:53", + "totalram": 63447040, + "tx_bytes": 35767943005, + "tx_power": -4, + "tx_ratedata": [ + 2, + 0, + 1, + 9, + 4150, + 89921, + 4, + 4, + 28560, + 7836817 + ], + "tx_throughput": 4666, + "unms": { + "status": 0, + "timestamp": null + }, + "uptime": 82335, + "version": "WA.ar934x.v8.7.18.48247.250728.0850" + }, + "rssi": 45, + "rx_idx": 9, + "rx_nss": 2, + "signal": -51, + "stats": { + "rx_bytes": 35530195638, + "rx_packets": 30533587, + "rx_pps": 256, + "tx_bytes": 6597810092, + "tx_packets": 47272530, + "tx_pps": 0 + }, + "tx_idx": 9, + "tx_latency": 1, + "tx_lretries": 0, + "tx_nss": 2, + "tx_packets": 0, + "tx_ratedata": [ + 2, + 0, + 1, + 8, + 8, + 15523, + 4, + 4, + 867, + 7181836 + ], + "tx_sretries": 0, + "ul_avg_linkscore": 100, + "ul_capacity_expect": 312000, + "ul_linkscore": 100, + "ul_rate_expect": 8, + "ul_signal_expect": -62, + "uptime": 81581 + }, + { + "airmax": { + "actual_priority": 2, + "atpc_status": 4, + "beam": 0, + "cb_capacity": 342000, + "desired_priority": 2, + "dl_capacity": 342000, + "rx": { + "cinr": 33, + "evm": [ + [ + 32, + 31, + 34, + 35, + 32, + 33, + 31, + 36, + 35, + 32, + 41, + 34, + 34, + 34, + 31, + 31, + 33, + 30, + 34, + 35, + 32, + 34, + 31, + 30, + 33, + 32, + 29, + 29, + 36, + 34, + 32, + 32, + 32, + 33, + 33, + 34, + 33, + 34, + 35, + 33, + 34, + 33, + 33, + 33, + 33, + 29, + 32, + 32, + 31, + 33, + 33, + 34, + 34, + 31, + 34, + 33, + 29, + 34, + 34, + 32, + 30, + 32, + 32, + 33 + ], + [ + 50, + 53, + 51, + 51, + 51, + 50, + 53, + 50, + 52, + 50, + 51, + 51, + 50, + 50, + 49, + 50, + 52, + 51, + 50, + 50, + 51, + 51, + 50, + 50, + 52, + 50, + 50, + 51, + 50, + 50, + 50, + 51, + 50, + 50, + 49, + 50, + 50, + 53, + 51, + 51, + 50, + 51, + 52, + 51, + 51, + 51, + 51, + 50, + 50, + 50, + 52, + 50, + 50, + 50, + 50, + 51, + 51, + 50, + 51, + 51, + 52, + 52, + 53, + 53 + ] + ], + "usage": 62 + }, + "tx": { + "cinr": 34, + "evm": [ + [ + 35, + 34, + 33, + 34, + 32, + 32, + 35, + 31, + 34, + 32, + 37, + 34, + 35, + 33, + 33, + 32, + 32, + 33, + 36, + 33, + 34, + 33, + 31, + 35, + 34, + 35, + 33, + 33, + 35, + 32, + 34, + 34, + 34, + 33, + 33, + 34, + 35, + 36, + 33, + 33, + 33, + 36, + 34, + 36, + 36, + 33, + 34, + 34, + 33, + 34, + 34, + 34, + 30, + 32, + 37, + 35, + 35, + 35, + 33, + 35, + 34, + 32, + 33, + 34 + ], + [ + 51, + 52, + 52, + 50, + 51, + 52, + 52, + 51, + 52, + 51, + 52, + 52, + 50, + 52, + 51, + 52, + 52, + 51, + 52, + 51, + 51, + 51, + 51, + 51, + 52, + 51, + 51, + 51, + 52, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 52, + 52, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 52, + 51, + 51, + 51, + 52, + 52, + 51, + 50, + 52, + 52, + 52, + 51, + 51, + 52, + 50, + 51 + ] + ], + "usage": 21 + }, + "ul_capacity": 342000 + }, + "airos_connected": true, + "cb_capacity_expect": 286000, + "chainrssi": [ + 50, + 51, + 0 + ], + "distance": 300, + "dl_avg_linkscore": 100, + "dl_capacity_expect": 260000, + "dl_linkscore": 100, + "dl_rate_expect": 7, + "dl_signal_expect": -68, + "last_disc": 0, + "lastip": "127.0.0.90", + "mac": "01:11:22:33:31:38", + "noisefloor": -92, + "remote": { + "age": 2, + "airview": 2, + "antenna_gain": 19, + "cable_loss": 0, + "chainrssi": [ + 50, + 52, + 0 + ], + "compat_11n": 0, + "cpuload": 23.5294, + "device_id": "2b2b22222b222222aa2fd22a22222c2c", + "distance": 300, + "ethlist": [ + { + "cable_len": 1, + "duplex": true, + "enabled": true, + "ifname": "eth0", + "plugged": true, + "snr": [ + 30, + 30, + 30, + 30 + ], + "speed": 1000 + }, + { + "cable_len": 0, + "duplex": true, + "enabled": true, + "ifname": "eth1", + "plugged": false, + "snr": [ + 0, + 0, + 0, + 0 + ], + "speed": 0 + } + ], + "freeram": 19714048, + "gps": { + "alt": null, + "dim": null, + "dop": null, + "fix": 0, + "lat": "52.379894", + "lon": "4.901608", + "sats": null, + "time_synced": null + }, + "height": 2, + "hostname": "NanoBeam-shed1", + "ip6addr": null, + "ipaddr": [ + "127.0.0.90" + ], + "mode": "sta-ptmp", + "netrole": "bridge", + "noisefloor": -91, + "oob": false, + "platform": "NanoBeam 5AC", + "power_time": 1461670, + "rssi": 54, + "rx_bytes": 14205619701, + "rx_chainmask": 3, + "rx_throughput": 1322, + "service": { + "link": 1461239, + "time": 1461517 + }, + "signal": -42, + "sys_id": "0xe7fc", + "temperature": 0, + "time": "2025-08-06 16:37:53", + "totalram": 63447040, + "tx_bytes": 242792119032, + "tx_power": -4, + "tx_ratedata": [ + 2, + 0, + 1, + 8, + 5296, + 373316, + 5, + 788, + 4003255, + 21672948 + ], + "tx_throughput": 23354, + "unms": { + "status": 0, + "timestamp": null + }, + "uptime": 83207, + "version": "WA.ar934x.v8.7.18.48247.250728.0850" + }, + "rssi": 54, + "rx_idx": 9, + "rx_nss": 2, + "signal": -42, + "stats": { + "rx_bytes": 238827846427, + "rx_packets": 182050337, + "rx_pps": 2641, + "tx_bytes": 14544700857, + "tx_packets": 131651046, + "tx_pps": 0 + }, + "tx_idx": 9, + "tx_latency": 1, + "tx_lretries": 0, + "tx_nss": 2, + "tx_packets": 0, + "tx_ratedata": [ + 2, + 0, + 1, + 8, + 12, + 31932, + 4, + 12, + 363974, + 20502633 + ], + "tx_sretries": 0, + "ul_avg_linkscore": 100, + "ul_capacity_expect": 312000, + "ul_linkscore": 100, + "ul_rate_expect": 8, + "ul_signal_expect": -62, + "uptime": 81580 + } + ], + "sta_disconnected": [], + "throughput": { + "rx": 24259, + "tx": 1565 + }, + "tx_chainmask": 3, + "tx_idx": 9, + "tx_nss": 2, + "txpower": -1 + } +} \ No newline at end of file diff --git a/fixtures/airos_loco5ac_ap-ptp.json b/fixtures/airos_loco5ac_ap-ptp.json index 2a5905e..cfe6484 100644 --- a/fixtures/airos_loco5ac_ap-ptp.json +++ b/fixtures/airos_loco5ac_ap-ptp.json @@ -168,6 +168,7 @@ "dl_capacity": 647400, "ff_cap_rep": false, "fixed_frame": false, + "flex_mode": null, "gps_sync": false, "rx_use": 42, "tx_use": 6, @@ -519,9 +520,14 @@ ], "freeram": 14290944, "gps": { + "alt": null, + "dim": null, + "dop": null, "fix": 0, "lat": "52.379894", - "lon": "4.901608" + "lon": "4.901608", + "sats": null, + "time_synced": null }, "height": 2, "hostname": "NanoStation 5AC sta name", diff --git a/fixtures/airos_loco5ac_sta-ptp.json b/fixtures/airos_loco5ac_sta-ptp.json index ff2115a..247bdb4 100644 --- a/fixtures/airos_loco5ac_sta-ptp.json +++ b/fixtures/airos_loco5ac_sta-ptp.json @@ -168,6 +168,7 @@ "dl_capacity": 647400, "ff_cap_rep": false, "fixed_frame": false, + "flex_mode": null, "gps_sync": false, "rx_use": 6, "tx_use": 40, @@ -519,9 +520,14 @@ ], "freeram": 16633856, "gps": { + "alt": null, + "dim": null, + "dop": null, "fix": 0, "lat": "52.379894", - "lon": "4.901608" + "lon": "4.901608", + "sats": null, + "time_synced": null }, "height": 3, "hostname": "NanoStation 5AC ap name", diff --git a/fixtures/airos_mocked_sta-ptmp.json b/fixtures/airos_mocked_sta-ptmp.json new file mode 100644 index 0000000..a698afd --- /dev/null +++ b/fixtures/airos_mocked_sta-ptmp.json @@ -0,0 +1,1059 @@ +{ + "chain_names": [ + { + "name": "Chain 0", + "number": 1 + }, + { + "name": "Chain 1", + "number": 2 + } + ], + "derived": { + "access_point": false, + "mac": "01:23:45:67:89:CD", + "mac_interface": "br0", + "ptmp": true, + "ptp": false, + "station": true + }, + "firewall": { + "eb6tables": false, + "ebtables": false, + "ip6tables": false, + "iptables": false + }, + "genuine": "/images/genuine.png", + "gps": { + "fix": 0, + "lat": 52.379894, + "lon": 4.901608 + }, + "host": { + "cpuload": 44.0, + "device_id": "d4f4cdf82961e619328a8f72f8d7653b", + "devmodel": "NanoStation 5AC loco", + "freeram": 16105472, + "fwversion": "v8.7.17", + "height": 2, + "hostname": "NanoStation 5AC sta name WITH TWO REMOTES", + "loadavg": 0.359863, + "netrole": "bridge", + "power_time": 268567, + "temperature": 0, + "time": "2025-06-23 23:14:49", + "timestamp": 2668800167, + "totalram": 63447040, + "uptime": 265375 + }, + "interfaces": [ + { + "enabled": true, + "hwaddr": "01:23:45:67:89:CD", + "ifname": "eth0", + "mtu": 1500, + "status": { + "cable_len": 14, + "duplex": true, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": true, + "rx_bytes": 206979884583, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 185401454, + "snr": [ + 30, + 30, + 30, + 30 + ], + "speed": 1000, + "tx_bytes": 4975926283, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 73329864 + } + }, + { + "enabled": true, + "hwaddr": "01:23:45:67:89:CD", + "ifname": "ath0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": false, + "rx_bytes": 3625607638, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 53038237, + "snr": null, + "speed": 0, + "tx_bytes": 212398179151, + "tx_dropped": 34, + "tx_errors": 0, + "tx_packets": 149906916 + } + }, + { + "enabled": true, + "hwaddr": "01:23:45:67:89:CD", + "ifname": "br0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": [ + { + "addr": "fe80::eea:14ff:fea4:89ab", + "plen": 64 + } + ], + "ipaddr": "192.168.1.3", + "plugged": true, + "rx_bytes": 198175800, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 1753443, + "snr": null, + "speed": 0, + "tx_bytes": 143856488, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 204749 + } + } + ], + "ntpclient": {}, + "portfw": false, + "provmode": {}, + "services": { + "airview": 2, + "dhcp6d_stateful": false, + "dhcpc": false, + "dhcpd": false, + "pppoe": false + }, + "unms": { + "status": 0, + "timestamp": null + }, + "wireless": { + "antenna_gain": 13, + "apmac": "01:23:45:67:89:AB", + "aprepeater": false, + "band": 2, + "cac_state": 0, + "cac_timeout": 0, + "center1_freq": 5530, + "chanbw": 80, + "compat_11n": 0, + "count": 1, + "dfs": 1, + "distance": 0, + "essid": "DemoSSID", + "frequency": 5500, + "hide_essid": 0, + "ieeemode": "AUTO", + "mode": "sta-ptmp", + "noisef": -89, + "nol_state": 0, + "nol_timeout": 0, + "polling": { + "atpc_status": 2, + "cb_capacity": 586950, + "dl_capacity": 647400, + "ff_cap_rep": false, + "fixed_frame": false, + "flex_mode": 1, + "gps_sync": false, + "rx_use": 6, + "tx_use": 40, + "ul_capacity": 526500, + "use": 46 + }, + "rstatus": 5, + "rx_chainmask": 3, + "rx_idx": 9, + "rx_nss": 2, + "security": "WPA2", + "service": { + "link": 266051, + "time": 267250 + }, + "sta": [ + { + "airmax": { + "actual_priority": 0, + "atpc_status": 2, + "beam": 0, + "cb_capacity": 586950, + "desired_priority": 0, + "dl_capacity": 647400, + "rx": { + "cinr": 31, + "evm": [ + [ + 30, + 32, + 30, + 29, + 33, + 31, + 29, + 33, + 31, + 31, + 30, + 33, + 34, + 33, + 31, + 33, + 32, + 32, + 31, + 29, + 31, + 30, + 32, + 31, + 30, + 29, + 32, + 31, + 32, + 31, + 31, + 32, + 29, + 31, + 29, + 30, + 32, + 32, + 31, + 32, + 32, + 33, + 31, + 28, + 29, + 31, + 31, + 33, + 32, + 33, + 32, + 32, + 32, + 31, + 33, + 29, + 27, + 31, + 28, + 29, + 31, + 31, + 34, + 28 + ], + [ + 39, + 39, + 39, + 39, + 39, + 41, + 39, + 39, + 39, + 39, + 39, + 39, + 38, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 40, + 39, + 39, + 40, + 39, + 39, + 39, + 40, + 39, + 40, + 39, + 39, + 39, + 39, + 39, + 38, + 39, + 39, + 39, + 39, + 39, + 39, + 40, + 39, + 39, + 40, + 39, + 38, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 38, + 39, + 39, + 39 + ] + ], + "usage": 6 + }, + "tx": { + "cinr": 31, + "evm": [ + [ + 32, + 31, + 30, + 26, + 32, + 32, + 31, + 28, + 33, + 32, + 32, + 32, + 31, + 31, + 31, + 29, + 30, + 32, + 30, + 27, + 34, + 31, + 31, + 30, + 32, + 29, + 31, + 29, + 31, + 33, + 31, + 31, + 32, + 30, + 31, + 34, + 33, + 31, + 30, + 31, + 30, + 31, + 31, + 32, + 31, + 30, + 33, + 31, + 30, + 31, + 27, + 31, + 30, + 30, + 30, + 30, + 30, + 29, + 32, + 34, + 31, + 30, + 28, + 30 + ], + [ + 35, + 35, + 37, + 36, + 36, + 35, + 36, + 36, + 37, + 36, + 37, + 37, + 36, + 36, + 36, + 36, + 36, + 36, + 36, + 36, + 37, + 37, + 36, + 36, + 37, + 36, + 35, + 35, + 37, + 36, + 36, + 37, + 36, + 37, + 36, + 36, + 37, + 36, + 36, + 35, + 36, + 36, + 36, + 36, + 36, + 37, + 37, + 37, + 36, + 37, + 35, + 36, + 36, + 36, + 36, + 37, + 37, + 36, + 36, + 36, + 36, + 36, + 36, + 36 + ] + ], + "usage": 40 + }, + "ul_capacity": 526500 + }, + "airos_connected": true, + "cb_capacity_expect": 658000, + "chainrssi": [ + 33, + 37, + 0 + ], + "distance": 1, + "dl_avg_linkscore": 93, + "dl_capacity_expect": 692000, + "dl_linkscore": 93, + "dl_rate_expect": 9, + "dl_signal_expect": -45, + "last_disc": 1, + "lastip": "192.168.1.3", + "mac": "01:23:45:67:89:GH", + "noisefloor": -89, + "remote": { + "age": 2, + "airview": 2, + "antenna_gain": 13, + "cable_loss": 0, + "chainrssi": [ + 35, + 31, + 0 + ], + "compat_11n": 0, + "cpuload": 5.0505, + "device_id": "03aa0d0b40fed0a47088293584ef4418", + "distance": 1, + "ethlist": [ + { + "cable_len": 18, + "duplex": true, + "enabled": true, + "ifname": "eth0", + "plugged": true, + "snr": [ + 30, + 30, + 30, + 30 + ], + "speed": 1000 + } + ], + "freeram": 16633856, + "gps": { + "alt": null, + "dim": null, + "dop": null, + "fix": 0, + "lat": "52.379894", + "lon": "4.901608", + "sats": null, + "time_synced": null + }, + "height": 3, + "hostname": "NanoStation 5AC ap name WITHOUT MODE", + "ip6addr": [ + "fe80::eea:14ff:fea4:89cd" + ], + "ipaddr": [ + "192.168.1.3" + ], + "mode": null, + "netrole": "bridge", + "noisefloor": -89, + "oob": false, + "platform": "NanoStation 5AC loco", + "power_time": 268736, + "rssi": 36, + "rx_bytes": 207021597130, + "rx_chainmask": 3, + "rx_throughput": 10548, + "service": { + "link": 266056, + "time": 267234 + }, + "signal": -60, + "sys_id": "0xe7fa", + "temperature": 0, + "time": "2025-06-23 23:07:34", + "totalram": 63447040, + "tx_bytes": 5267487876, + "tx_power": -3, + "tx_ratedata": [ + 175, + 4, + 47, + 200, + 673, + 158, + 163, + 138, + 68926, + 19583506 + ], + "tx_throughput": 314, + "unms": { + "status": 0, + "timestamp": null + }, + "uptime": 264941, + "version": "WA.ar934x.v8.7.17.48152.250620.2132" + }, + "rssi": 38, + "rx_idx": 9, + "rx_nss": 2, + "signal": -58, + "stats": { + "rx_bytes": 3622839202, + "rx_packets": 52999540, + "rx_pps": 446, + "tx_bytes": 212303079651, + "tx_packets": 149832292, + "tx_pps": 0 + }, + "tx_idx": 8, + "tx_latency": 0, + "tx_lretries": 0, + "tx_nss": 2, + "tx_packets": 0, + "tx_ratedata": [ + 14, + 4, + 372, + 2223, + 4708, + 4037, + 8142, + 486840, + 29437014, + 24752069 + ], + "tx_sretries": 0, + "ul_avg_linkscore": 87, + "ul_capacity_expect": 624000, + "ul_linkscore": 84, + "ul_rate_expect": 8, + "ul_signal_expect": -59, + "uptime": 170335 + }, + { + "airmax": { + "actual_priority": 0, + "atpc_status": 2, + "beam": 0, + "cb_capacity": 586950, + "desired_priority": 0, + "dl_capacity": 647400, + "rx": { + "cinr": 31, + "evm": [ + [ + 30, + 32, + 30, + 29, + 33, + 31, + 29, + 33, + 31, + 31, + 30, + 33, + 34, + 33, + 31, + 33, + 32, + 32, + 31, + 29, + 31, + 30, + 32, + 31, + 30, + 29, + 32, + 31, + 32, + 31, + 31, + 32, + 29, + 31, + 29, + 30, + 32, + 32, + 31, + 32, + 32, + 33, + 31, + 28, + 29, + 31, + 31, + 33, + 32, + 33, + 32, + 32, + 32, + 31, + 33, + 29, + 27, + 31, + 28, + 29, + 31, + 31, + 34, + 28 + ], + [ + 39, + 39, + 39, + 39, + 39, + 41, + 39, + 39, + 39, + 39, + 39, + 39, + 38, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 40, + 39, + 39, + 40, + 39, + 39, + 39, + 40, + 39, + 40, + 39, + 39, + 39, + 39, + 39, + 38, + 39, + 39, + 39, + 39, + 39, + 39, + 40, + 39, + 39, + 40, + 39, + 38, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 39, + 38, + 39, + 39, + 39 + ] + ], + "usage": 6 + }, + "tx": { + "cinr": 31, + "evm": [ + [ + 32, + 31, + 30, + 26, + 32, + 32, + 31, + 28, + 33, + 32, + 32, + 32, + 31, + 31, + 31, + 29, + 30, + 32, + 30, + 27, + 34, + 31, + 31, + 30, + 32, + 29, + 31, + 29, + 31, + 33, + 31, + 31, + 32, + 30, + 31, + 34, + 33, + 31, + 30, + 31, + 30, + 31, + 31, + 32, + 31, + 30, + 33, + 31, + 30, + 31, + 27, + 31, + 30, + 30, + 30, + 30, + 30, + 29, + 32, + 34, + 31, + 30, + 28, + 30 + ], + [ + 35, + 35, + 37, + 36, + 36, + 35, + 36, + 36, + 37, + 36, + 37, + 37, + 36, + 36, + 36, + 36, + 36, + 36, + 36, + 36, + 37, + 37, + 36, + 36, + 37, + 36, + 35, + 35, + 37, + 36, + 36, + 37, + 36, + 37, + 36, + 36, + 37, + 36, + 36, + 35, + 36, + 36, + 36, + 36, + 36, + 37, + 37, + 37, + 36, + 37, + 35, + 36, + 36, + 36, + 36, + 37, + 37, + 36, + 36, + 36, + 36, + 36, + 36, + 36 + ] + ], + "usage": 40 + }, + "ul_capacity": 526500 + }, + "airos_connected": true, + "cb_capacity_expect": 658000, + "chainrssi": [ + 33, + 37, + 0 + ], + "distance": 1, + "dl_avg_linkscore": 93, + "dl_capacity_expect": 692000, + "dl_linkscore": 93, + "dl_rate_expect": 9, + "dl_signal_expect": -45, + "last_disc": 1, + "lastip": "192.168.1.3", + "mac": "01:23:45:67:89:EF", + "noisefloor": -89, + "remote": { + "age": 2, + "airview": 2, + "antenna_gain": 13, + "cable_loss": 0, + "chainrssi": [ + 35, + 31, + 0 + ], + "compat_11n": 0, + "cpuload": 5.0505, + "device_id": "03aa0d0b40fed0a47088293584ef4418", + "distance": 1, + "ethlist": [ + { + "cable_len": 18, + "duplex": true, + "enabled": true, + "ifname": "eth0", + "plugged": true, + "snr": [ + 30, + 30, + 30, + 30 + ], + "speed": 1000 + } + ], + "freeram": 16633856, + "gps": { + "alt": null, + "dim": null, + "dop": null, + "fix": 0, + "lat": "52.379894", + "lon": "4.901608", + "sats": null, + "time_synced": null + }, + "height": 3, + "hostname": "NanoStation 5AC ap name WITH MODE", + "ip6addr": [ + "fe80::eea:14ff:fea4:89cd" + ], + "ipaddr": [ + "192.168.1.3" + ], + "mode": "ap-ptmp", + "netrole": "bridge", + "noisefloor": -89, + "oob": false, + "platform": "NanoStation 5AC loco", + "power_time": 268736, + "rssi": 36, + "rx_bytes": 207021597130, + "rx_chainmask": 3, + "rx_throughput": 10548, + "service": { + "link": 266056, + "time": 267234 + }, + "signal": -60, + "sys_id": "0xe7fa", + "temperature": 0, + "time": "2025-06-23 23:07:34", + "totalram": 63447040, + "tx_bytes": 5267487876, + "tx_power": -3, + "tx_ratedata": [ + 175, + 4, + 47, + 200, + 673, + 158, + 163, + 138, + 68926, + 19583506 + ], + "tx_throughput": 314, + "unms": { + "status": 0, + "timestamp": null + }, + "uptime": 264941, + "version": "WA.ar934x.v8.7.17.48152.250620.2132" + }, + "rssi": 38, + "rx_idx": 9, + "rx_nss": 2, + "signal": -58, + "stats": { + "rx_bytes": 3622839202, + "rx_packets": 52999540, + "rx_pps": 446, + "tx_bytes": 212303079651, + "tx_packets": 149832292, + "tx_pps": 0 + }, + "tx_idx": 8, + "tx_latency": 0, + "tx_lretries": 0, + "tx_nss": 2, + "tx_packets": 0, + "tx_ratedata": [ + 14, + 4, + 372, + 2223, + 4708, + 4037, + 8142, + 486840, + 29437014, + 24752069 + ], + "tx_sretries": 0, + "ul_avg_linkscore": 87, + "ul_capacity_expect": 624000, + "ul_linkscore": 84, + "ul_rate_expect": 8, + "ul_signal_expect": -59, + "uptime": 170335 + } + ], + "sta_disconnected": [], + "throughput": { + "rx": 267, + "tx": 12014 + }, + "tx_chainmask": 3, + "tx_idx": 8, + "tx_nss": 2, + "txpower": -4 + } +} \ No newline at end of file diff --git a/fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json b/fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json new file mode 100644 index 0000000..36a537a --- /dev/null +++ b/fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json @@ -0,0 +1,652 @@ +{ + "chain_names": [ + { + "name": "Chain 0", + "number": 1 + }, + { + "name": "Chain 1", + "number": 2 + } + ], + "derived": { + "access_point": false, + "mac": "22:22:33:44:31:38", + "mac_interface": "br0", + "ptmp": true, + "ptp": false, + "station": true + }, + "firewall": { + "eb6tables": false, + "ebtables": false, + "ip6tables": false, + "iptables": false + }, + "genuine": "/images/genuine.png", + "gps": { + "fix": 0, + "lat": 52.379894, + "lon": 4.901608 + }, + "host": { + "cpuload": 32.673267, + "device_id": "3b3b33333b033333aa3fd33a33333c3c", + "devmodel": "NanoBeam 5AC", + "freeram": 18821120, + "fwversion": "v8.7.18", + "height": 2, + "hostname": "NanoBeam-shed1", + "loadavg": 0.662109, + "netrole": "bridge", + "power_time": 1463263, + "temperature": 0, + "time": "2025-08-06 17:04:27", + "timestamp": 2149610414, + "totalram": 63447040, + "uptime": 84800 + }, + "interfaces": [ + { + "enabled": true, + "hwaddr": "21:22:33:44:31:38", + "ifname": "eth0", + "mtu": 1500, + "status": { + "cable_len": 1, + "duplex": true, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": true, + "rx_bytes": 239874919363, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 188552468, + "snr": [ + 30, + 30, + 30, + 30 + ], + "speed": 1000, + "tx_bytes": 11330281132, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 136749032 + } + }, + { + "enabled": true, + "hwaddr": "23:22:33:44:31:38", + "ifname": "eth1", + "mtu": 1500, + "status": { + "cable_len": 0, + "duplex": true, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": false, + "rx_bytes": 0, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 0, + "snr": [ + 0, + 0, + 0, + 0 + ], + "speed": 0, + "tx_bytes": 0, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 0 + } + }, + { + "enabled": true, + "hwaddr": "22:22:33:44:31:38", + "ifname": "ath0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": false, + "rx_bytes": 14476938277, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 136786005, + "snr": null, + "speed": 0, + "tx_bytes": 247447969267, + "tx_dropped": 142, + "tx_errors": 0, + "tx_packets": 188601116 + } + }, + { + "enabled": true, + "hwaddr": "22:22:33:44:31:38", + "ifname": "br0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": null, + "ipaddr": "169.254.49.56", + "plugged": true, + "rx_bytes": 3479709661, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 30469651, + "snr": null, + "speed": 0, + "tx_bytes": 22880575, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 56372 + } + } + ], + "ntpclient": { + "last_sync": "2025-08-06 16:59:11" + }, + "portfw": false, + "provmode": {}, + "services": { + "airview": 2, + "dhcp6d_stateful": false, + "dhcpc": true, + "dhcpd": false, + "pppoe": false + }, + "unms": { + "status": 0, + "timestamp": null + }, + "wireless": { + "antenna_gain": 19, + "apmac": "24:5A:4C:B0:19:7E", + "aprepeater": false, + "band": 2, + "cac_state": 0, + "cac_timeout": 0, + "center1_freq": 5690, + "chanbw": 40, + "compat_11n": 0, + "count": 1, + "dfs": 1, + "distance": 300, + "essid": "House-shed1", + "frequency": 5680, + "hide_essid": 0, + "ieeemode": "AUTO", + "mode": "sta-ptmp", + "noisef": -92, + "nol_state": 0, + "nol_timeout": 0, + "polling": { + "atpc_status": 4, + "cb_capacity": 343800, + "dl_capacity": 342000, + "ff_cap_rep": false, + "fixed_frame": false, + "flex_mode": 1, + "gps_sync": false, + "rx_use": 19, + "tx_use": 71, + "ul_capacity": 345600, + "use": 90 + }, + "rstatus": 5, + "rx_chainmask": 3, + "rx_idx": 9, + "rx_nss": 2, + "security": "WPA2", + "service": { + "link": 1462832, + "time": 1463110 + }, + "sta": [ + { + "airmax": { + "actual_priority": 2, + "atpc_status": 0, + "beam": 0, + "cb_capacity": 343800, + "desired_priority": 2, + "dl_capacity": 342000, + "rx": { + "cinr": 34, + "evm": [ + [ + 34, + 38, + 32, + 32, + 34, + 38, + 34, + 33, + 32, + 36, + 31, + 34, + 33, + 34, + 34, + 34, + 31, + 36, + 32, + 33, + 35, + 35, + 33, + 34, + 33, + 36, + 37, + 35, + 31, + 38, + 34, + 35, + 34, + 34, + 33, + 31, + 35, + 33, + 35, + 35, + 36, + 33, + 36, + 32, + 34, + 34, + 36, + 31, + 36, + 36, + 31, + 35, + 36, + 36, + 34, + 31, + 34, + 34, + 35, + 33, + 32, + 35, + 35, + 33 + ], + [ + 50, + 51, + 52, + 52, + 52, + 52, + 51, + 51, + 51, + 51, + 52, + 51, + 51, + 51, + 52, + 51, + 51, + 52, + 51, + 51, + 51, + 51, + 51, + 50, + 51, + 51, + 51, + 51, + 52, + 52, + 52, + 51, + 51, + 51, + 51, + 51, + 52, + 50, + 51, + 52, + 51, + 52, + 51, + 52, + 51, + 51, + 51, + 52, + 52, + 51, + 51, + 52, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 51, + 52, + 51, + 51, + 51 + ] + ], + "usage": 19 + }, + "tx": { + "cinr": 33, + "evm": [ + [ + 32, + 32, + 35, + 36, + 34, + 33, + 34, + 32, + 32, + 31, + 31, + 31, + 33, + 32, + 35, + 36, + 33, + 35, + 36, + 32, + 34, + 35, + 31, + 36, + 34, + 32, + 30, + 34, + 34, + 35, + 32, + 31, + 33, + 35, + 35, + 32, + 37, + 33, + 33, + 32, + 30, + 34, + 32, + 31, + 32, + 36, + 35, + 32, + 33, + 32, + 34, + 34, + 33, + 31, + 30, + 35, + 35, + 31, + 37, + 32, + 34, + 34, + 37, + 29 + ], + [ + 50, + 53, + 52, + 52, + 50, + 51, + 52, + 49, + 50, + 50, + 51, + 51, + 50, + 49, + 50, + 52, + 51, + 50, + 50, + 51, + 51, + 50, + 53, + 53, + 51, + 51, + 53, + 52, + 51, + 51, + 50, + 50, + 50, + 50, + 50, + 50, + 51, + 53, + 50, + 50, + 53, + 50, + 51, + 50, + 53, + 52, + 52, + 50, + 52, + 50, + 52, + 49, + 50, + 50, + 50, + 52, + 51, + 52, + 50, + 50, + 50, + 50, + 52, + 51 + ] + ], + "usage": 71 + }, + "ul_capacity": 345600 + }, + "airos_connected": true, + "cb_capacity_expect": 247000, + "chainrssi": [ + 50, + 52, + 0 + ], + "distance": 300, + "dl_avg_linkscore": 100, + "dl_capacity_expect": 260000, + "dl_linkscore": 100, + "dl_rate_expect": 7, + "dl_signal_expect": -68, + "last_disc": 0, + "lastip": "127.0.0.85", + "mac": "24:5A:4C:B0:19:7E", + "noisefloor": -92, + "remote": { + "age": 3, + "airview": 2, + "antenna_gain": 17, + "cable_loss": 0, + "chainrssi": [ + 50, + 50, + 0 + ], + "compat_11n": 1, + "cpuload": 31.313101, + "device_id": "b333d333333f3333f0e3ecbcc33d3e33", + "distance": 300, + "ethlist": [ + { + "cable_len": 48, + "duplex": true, + "enabled": true, + "ifname": "eth0", + "plugged": true, + "snr": [ + 30, + 30, + 29, + 30 + ], + "speed": 1000 + } + ], + "freeram": 13537280, + "gps": { + "alt": 251.4, + "dim": 3, + "dop": 0.96, + "fix": 1, + "lat": "52.379894", + "lon": "4.901608", + "sats": 9, + "time_synced": 0 + }, + "height": null, + "hostname": "House-Bridge", + "ip6addr": null, + "ipaddr": [ + "127.0.0.85" + ], + "mode": "ap-ptmp", + "netrole": "bridge", + "noisefloor": -92, + "oob": false, + "platform": "LiteAP GPS", + "power_time": 1463250, + "rssi": 53, + "rx_bytes": 279603431363, + "rx_chainmask": 3, + "rx_throughput": 26145, + "service": { + "link": 1463007, + "time": 1463100 + }, + "signal": -43, + "sys_id": "0xe7fd", + "temperature": 0, + "time": "2025-08-06 17:04:24", + "totalram": 63447040, + "tx_bytes": 16765807136, + "tx_power": -1, + "tx_ratedata": [ + 2, + 0, + 1, + 8, + 12, + 32556, + 4, + 12, + 373371, + 20880444 + ], + "tx_throughput": 1626, + "unms": { + "status": 0, + "timestamp": null + }, + "uptime": 83244, + "version": "WA.ar934x.v8.7.18.48247.250728.0850" + }, + "rssi": 54, + "rx_idx": 9, + "rx_nss": 2, + "signal": -42, + "stats": { + "rx_bytes": 14244495724, + "rx_packets": 134594169, + "rx_pps": 1656, + "tx_bytes": 243474280087, + "tx_packets": 185581450, + "tx_pps": 0 + }, + "tx_idx": 9, + "tx_latency": 2, + "tx_lretries": 0, + "tx_nss": 2, + "tx_packets": 0, + "tx_ratedata": [ + 2, + 0, + 1, + 8, + 5371, + 380598, + 5, + 788, + 4105441, + 22057146 + ], + "tx_sretries": 0, + "ul_avg_linkscore": 100, + "ul_capacity_expect": 234000, + "ul_linkscore": 100, + "ul_rate_expect": 6, + "ul_signal_expect": -71, + "uptime": 83171 + } + ], + "sta_disconnected": [], + "throughput": { + "rx": 1458, + "tx": 25282 + }, + "tx_chainmask": 3, + "tx_idx": 9, + "tx_nss": 2, + "txpower": -4 + } +} \ No newline at end of file diff --git a/fixtures/userdata/liteapgps_ap_ptmp_40mhz.json b/fixtures/userdata/liteapgps_ap_ptmp_40mhz.json new file mode 100644 index 0000000..0de350a --- /dev/null +++ b/fixtures/userdata/liteapgps_ap_ptmp_40mhz.json @@ -0,0 +1 @@ +{"chain_names":[{"number":1,"name": "Chain 0"},{"number":2,"name": "Chain 1"}],"host":{"hostname": "House-Bridge","device_id": "b222d222222f2222f0e2ecbcc22d2e22","uptime":81655,"power_time":1461661,"time": "2025-08-06 16:37:55","timestamp":2148019169,"fwversion": "v8.7.18","devmodel": "LiteAP GPS","netrole": "bridge","loadavg":0.188965,"totalram":63447040,"freeram":13541376,"temperature":0,"cpuload":59.595959,"height":null},"genuine": "/images/genuine.png","services":{"dhcpc":true,"dhcpd":false,"dhcp6d_stateful":false,"pppoe":false,"airview":2},"firewall":{"iptables":false,"ebtables":false,"ip6tables":false,"eb6tables":false},"portfw":false,"wireless":{"essid": "House-shed1","mode": "ap-ptmp","ieeemode": "11ACVHT40","band":2,"compat_11n":1,"hide_essid":0,"apmac":"03:11:22:33:19:7E","antenna_gain":17,"frequency":5680,"center1_freq":5690,"dfs":1,"distance":300,"security": "WPA2","noisef":-92,"txpower":-1,"aprepeater":false,"rstatus":5,"chanbw":40,"rx_chainmask":3,"tx_chainmask":3,"nol_state":0,"nol_timeout":0,"cac_state":0,"cac_timeout":0,"rx_idx":9,"rx_nss":2,"tx_idx":9,"tx_nss":2,"throughput":{"tx":1565,"rx":24259},"service":{"time":1461511,"link":1461418},"polling":{"cb_capacity":342000,"dl_capacity":342000,"ul_capacity":342000,"use":266,"tx_use":168,"rx_use":98,"atpc_status":2,"fixed_frame":false,"gps_sync":false,"flex_mode":1,"ff_cap_rep":false},"count":2,"sta":[{"mac":"00:11:22:33:2E:05","lastip":"127.0.0.82","signal":-51,"rssi":45,"noisefloor":-92,"chainrssi":[43,41,0],"tx_idx":9,"rx_idx":9,"tx_nss":2,"rx_nss":2,"tx_latency":1,"distance":300,"tx_packets":0,"tx_lretries":0,"tx_sretries":0,"uptime":81581,"dl_signal_expect":-68,"ul_signal_expect":-62,"cb_capacity_expect":286000,"dl_capacity_expect":260000,"ul_capacity_expect":312000,"dl_rate_expect":7,"ul_rate_expect":8,"dl_linkscore":100,"ul_linkscore":100,"dl_avg_linkscore":100,"ul_avg_linkscore":100,"tx_ratedata":[2,0,1,8,8,15523,4,4,867,7181836],"stats":{"rx_bytes":35530195638,"rx_packets":30533587,"rx_pps":256,"tx_bytes":6597810092,"tx_packets":47272530,"tx_pps":0},"airmax":{"actual_priority":2,"beam":0,"desired_priority":2,"cb_capacity":343800,"dl_capacity":342000,"ul_capacity":345600,"atpc_status":4,"rx":{"usage":34,"cinr":33,"evm":[[33,34,37,33,34,34,35,34,35,36,33,33,31,34,33,33,33,32,32,33,34,31,33,34,34,36,33,35,34,34,33,35,34,36,33,39,31,33,34,35,31,29,32,33,36,33,33,33,32,35,33,32,32,32,35,37,34,34,32,34,36,33,32,31],[42,42,42,42,43,42,42,43,43,43,42,42,42,41,43,42,42,43,42,42,42,42,43,43,43,42,44,42,42,42,42,42,43,43,41,42,42,41,42,42,42,42,42,42,41,42,41,43,41,42,42,43,41,44,43,43,43,42,44,42,42,42,42,42]]},"tx":{"usage":6,"cinr":33,"evm":[[34,31,33,35,34,35,34,31,34,33,33,29,26,35,34,35,35,28,32,32,32,27,28,36,34,32,31,33,28,34,35,33,33,34,37,33,33,32,29,32,34,37,29,33,32,33,33,32,29,32,32,31,32,32,35,32,33,31,35,33,33,30,32,33],[44,43,43,43,43,43,43,44,42,44,43,43,43,44,44,44,44,44,44,44,43,43,44,44,43,42,43,43,43,44,43,43,43,43,44,44,44,43,44,43,44,43,43,43,43,43,43,43,42,43,43,43,43,43,43,43,42,43,43,43,43,43,42,44]]}},"last_disc":0,"remote":{"age":3,"device_id": "22222dd22222c2e2b22d0d2222aedb38","hostname": "NanoBeam-shed2","platform": "NanoBeam 5AC","version": "WA.ar934x.v8.7.18.48247.250728.0850","time": "2025-08-06 16:37:53","cpuload":13.000000,"temperature":0,"totalram":63447040,"freeram":20488192,"netrole": "bridge","mode": "sta-ptmp","sys_id":"0xe7fc","tx_throughput":4666,"rx_throughput":755,"uptime":82335,"power_time":16088831,"compat_11n":0,"signal":-51,"rssi":45,"noisefloor":-94,"tx_power":-4,"distance":300,"rx_chainmask":3,"chainrssi":[44,39,0],"tx_ratedata":[2,0,1,9,4150,89921,4,4,28560,7836817],"tx_bytes":35767943005,"rx_bytes":6168364701,"antenna_gain":19,"cable_loss":0,"height":2,"ethlist":[{"ifname": "eth0","enabled":true,"plugged":true,"duplex":true,"speed":1000,"snr":[30,29,30,30],"cable_len":1},{"ifname": "eth1","enabled":true,"plugged":false,"duplex":true,"speed":0,"snr":[0,0,0,0],"cable_len":0}],"ipaddr":["127.0.0.82"],"gps":{"lat": "52.379894","lon": "4.901608","fix":0},"oob":false,"unms":{"status":0},"airview":2,"service":{"time":16088594,"link":16087519}}},{"mac":"01:11:22:33:31:38","lastip":"127.0.0.90","signal":-42,"rssi":54,"noisefloor":-92,"chainrssi":[50,51,0],"tx_idx":9,"rx_idx":9,"tx_nss":2,"rx_nss":2,"tx_latency":1,"distance":300,"tx_packets":0,"tx_lretries":0,"tx_sretries":0,"uptime":81580,"dl_signal_expect":-68,"ul_signal_expect":-62,"cb_capacity_expect":286000,"dl_capacity_expect":260000,"ul_capacity_expect":312000,"dl_rate_expect":7,"ul_rate_expect":8,"dl_linkscore":100,"ul_linkscore":100,"dl_avg_linkscore":100,"ul_avg_linkscore":100,"tx_ratedata":[2,0,1,8,12,31932,4,12,363974,20502633],"stats":{"rx_bytes":238827846427,"rx_packets":182050337,"rx_pps":2641,"tx_bytes":14544700857,"tx_packets":131651046,"tx_pps":0},"airmax":{"actual_priority":2,"beam":0,"desired_priority":2,"cb_capacity":342000,"dl_capacity":342000,"ul_capacity":342000,"atpc_status":4,"rx":{"usage":62,"cinr":33,"evm":[[32,31,34,35,32,33,31,36,35,32,41,34,34,34,31,31,33,30,34,35,32,34,31,30,33,32,29,29,36,34,32,32,32,33,33,34,33,34,35,33,34,33,33,33,33,29,32,32,31,33,33,34,34,31,34,33,29,34,34,32,30,32,32,33],[50,53,51,51,51,50,53,50,52,50,51,51,50,50,49,50,52,51,50,50,51,51,50,50,52,50,50,51,50,50,50,51,50,50,49,50,50,53,51,51,50,51,52,51,51,51,51,50,50,50,52,50,50,50,50,51,51,50,51,51,52,52,53,53]]},"tx":{"usage":21,"cinr":34,"evm":[[35,34,33,34,32,32,35,31,34,32,37,34,35,33,33,32,32,33,36,33,34,33,31,35,34,35,33,33,35,32,34,34,34,33,33,34,35,36,33,33,33,36,34,36,36,33,34,34,33,34,34,34,30,32,37,35,35,35,33,35,34,32,33,34],[51,52,52,50,51,52,52,51,52,51,52,52,50,52,51,52,52,51,52,51,51,51,51,51,52,51,51,51,52,51,51,51,51,51,51,51,51,51,51,52,52,51,51,51,51,51,51,51,52,51,51,51,52,52,51,50,52,52,52,51,51,52,50,51]]}},"last_disc":0,"remote":{"age":2,"device_id": "2b2b22222b222222aa2fd22a22222c2c","hostname": "NanoBeam-shed1","platform": "NanoBeam 5AC","version": "WA.ar934x.v8.7.18.48247.250728.0850","time": "2025-08-06 16:37:53","cpuload":23.529400,"temperature":0,"totalram":63447040,"freeram":19714048,"netrole": "bridge","mode": "sta-ptmp","sys_id":"0xe7fc","tx_throughput":23354,"rx_throughput":1322,"uptime":83207,"power_time":1461670,"compat_11n":0,"signal":-42,"rssi":54,"noisefloor":-91,"tx_power":-4,"distance":300,"rx_chainmask":3,"chainrssi":[50,52,0],"tx_ratedata":[2,0,1,8,5296,373316,5,788,4003255,21672948],"tx_bytes":242792119032,"rx_bytes":14205619701,"antenna_gain":19,"cable_loss":0,"height":2,"ethlist":[{"ifname": "eth0","enabled":true,"plugged":true,"duplex":true,"speed":1000,"snr":[30,30,30,30],"cable_len":1},{"ifname": "eth1","enabled":true,"plugged":false,"duplex":true,"speed":0,"snr":[0,0,0,0],"cable_len":0}],"ipaddr":["127.0.0.90"],"gps":{"lat": "52.379894","lon": "4.901608","fix":0},"oob":false,"unms":{"status":0},"airview":2,"service":{"time":1461517,"link":1461239}}}],"sta_disconnected":[]},"interfaces":[{"ifname": "eth0","hwaddr":"05:11:22:33:19:7E","enabled":true,"mtu":1500,"status":{"plugged":true,"tx_bytes":268785703196,"rx_bytes":17307482485,"tx_packets":212573426,"rx_packets":208585470,"tx_errors":0,"rx_errors":0,"tx_dropped":1,"rx_dropped":0,"ipaddr":"0.0.0.0","speed":1000,"duplex":true,"snr":[30,30,29,29],"cable_len":48}},{"ifname": "ath0","hwaddr":"04:11:22:33:19:7E","enabled":true,"mtu":1500,"status":{"plugged":false,"tx_bytes":16450464430,"rx_bytes":274358042002,"tx_packets":150354889,"rx_packets":212583924,"tx_errors":0,"rx_errors":0,"tx_dropped":227,"rx_dropped":0,"ipaddr":"0.0.0.0","speed":0,"duplex":false}},{"ifname": "br0","hwaddr":"04:11:22:33:19:7E","enabled":true,"mtu":1500,"status":{"plugged":true,"tx_bytes":38072153,"rx_bytes":6053730278,"tx_packets":99493,"rx_packets":51908268,"tx_errors":0,"rx_errors":0,"tx_dropped":0,"rx_dropped":0,"ipaddr":"127.0.0.85","speed":0,"duplex":false}}],"provmode":{},"ntpclient":{"last_sync": "2025-08-06 16:28:17"},"unms":{"status":0},"gps":{"lat":52.379894,"lon":4.901608,"fix":1,"sats":8,"dim":3,"dop":1.520000,"alt":252.500000,"time_sync_enabled":0}} diff --git a/fixtures/userdata/mocked_invalid_wireless_mode.json b/fixtures/userdata/mocked_invalid_wireless_mode.json new file mode 100644 index 0000000..0b06c0a --- /dev/null +++ b/fixtures/userdata/mocked_invalid_wireless_mode.json @@ -0,0 +1,159 @@ +{ + "chain_names": [], + "host": { + "hostname": "NanoStation 5AC sta name", + "device_id": "d4f4cdf82961e619328a8f72f8d66666", + "uptime": 265375, + "power_time": 268567, + "time": "2025-06-23 23:14:49", + "timestamp": 2668800167, + "fwversion": "v8.7.17", + "devmodel": "NanoStation 5AC loco", + "netrole": "bridge", + "loadavg": 0.359863, + "totalram": 63447040, + "freeram": 16105472, + "temperature": 0, + "cpuload": 44, + "height": 2 + }, + "genuine": "/images/genuine.png", + "services": { + "dhcpc": false, + "dhcpd": false, + "dhcp6d_stateful": false, + "pppoe": false, + "airview": 2 + }, + "firewall": { + "iptables": false, + "ebtables": false, + "ip6tables": false, + "eb6tables": false + }, + "portfw": false, + "wireless": { + "essid": "House-XXX", + "mode": "sta-ptp-INVALID", + "ieeemode": "AUTO", + "band": 2, + "compat_11n": 0, + "hide_essid": 0, + "apmac": "01:23:45:67:89:AB", + "antenna_gain": 13, + "frequency": 5500, + "center1_freq": 5530, + "dfs": 1, + "distance": 0, + "security": "WPA2", + "noisef": -89, + "txpower": -4, + "aprepeater": false, + "rstatus": 5, + "throughput": { + "tx": 12014, + "rx": 267 + }, + "chanbw": 80, + "rx_chainmask": 3, + "tx_chainmask": 3, + "nol_state": 0, + "nol_timeout": 0, + "cac_state": 0, + "cac_timeout": 0, + "rx_idx": 9, + "rx_nss": 2, + "tx_idx": 8, + "tx_nss": 2 + }, + "interfaces": [ + { + "ifname": "eth0", + "hwaddr": "01:23:45:67:89:CD", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": true, + "tx_bytes": 4975926283, + "rx_bytes": 206979884583, + "tx_packets": 73329864, + "rx_packets": 185401454, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 0, + "rx_dropped": 0, + "ipaddr": "0.0.0.0", + "speed": 1000, + "duplex": true, + "snr": [ + 30, + 30, + 30, + 30 + ], + "cable_len": 14 + } + }, + { + "ifname": "ath0", + "hwaddr": "01:23:45:67:89:CD", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": false, + "tx_bytes": 212398179151, + "rx_bytes": 3625607638, + "tx_packets": 149906916, + "rx_packets": 53038237, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 34, + "rx_dropped": 0, + "ipaddr": "0.0.0.0", + "speed": 0, + "duplex": false + } + }, + { + "ifname": "br0", + "hwaddr": "01:23:45:67:89:CD", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": true, + "tx_bytes": 143856488, + "rx_bytes": 198175800, + "tx_packets": 204749, + "rx_packets": 1753443, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 0, + "rx_dropped": 0, + "ipaddr": "192.168.1.3", + "ip6addr": [ + { + "addr": "fe80::eea:14ff:fea4:89ab", + "plen": 64 + } + ], + "speed": 0, + "duplex": false + } + } + ], + "provmode": {}, + "ntpclient": {}, + "unms": { + "status": 0 + }, + "gps": { + "lat": 52.379894, + "lon": 4.901608, + "fix": 0, + "sats": 9, + "dim": 3, + "dop": "0.91", + "alt": "248.6", + "time_synced": 0 + } +} diff --git a/fixtures/userdata/mocked_missing_wireless_mode.json b/fixtures/userdata/mocked_missing_wireless_mode.json new file mode 100644 index 0000000..fe63a17 --- /dev/null +++ b/fixtures/userdata/mocked_missing_wireless_mode.json @@ -0,0 +1,158 @@ +{ + "chain_names": [], + "host": { + "hostname": "NanoStation 5AC sta name", + "device_id": "d4f4cdf82961e619328a8f72f8d66666", + "uptime": 265375, + "power_time": 268567, + "time": "2025-06-23 23:14:49", + "timestamp": 2668800167, + "fwversion": "v8.7.17", + "devmodel": "NanoStation 5AC loco", + "netrole": "bridge", + "loadavg": 0.359863, + "totalram": 63447040, + "freeram": 16105472, + "temperature": 0, + "cpuload": 44, + "height": 2 + }, + "genuine": "/images/genuine.png", + "services": { + "dhcpc": false, + "dhcpd": false, + "dhcp6d_stateful": false, + "pppoe": false, + "airview": 2 + }, + "firewall": { + "iptables": false, + "ebtables": false, + "ip6tables": false, + "eb6tables": false + }, + "portfw": false, + "wireless": { + "essid": "House-XXX", + "ieeemode": "AUTO", + "band": 2, + "compat_11n": 0, + "hide_essid": 0, + "apmac": "01:23:45:67:89:AB", + "antenna_gain": 13, + "frequency": 5500, + "center1_freq": 5530, + "dfs": 1, + "distance": 0, + "security": "WPA2", + "noisef": -89, + "txpower": -4, + "aprepeater": false, + "rstatus": 5, + "throughput": { + "tx": 12014, + "rx": 267 + }, + "chanbw": 80, + "rx_chainmask": 3, + "tx_chainmask": 3, + "nol_state": 0, + "nol_timeout": 0, + "cac_state": 0, + "cac_timeout": 0, + "rx_idx": 9, + "rx_nss": 2, + "tx_idx": 8, + "tx_nss": 2 + }, + "interfaces": [ + { + "ifname": "eth0", + "hwaddr": "01:23:45:67:89:CD", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": true, + "tx_bytes": 4975926283, + "rx_bytes": 206979884583, + "tx_packets": 73329864, + "rx_packets": 185401454, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 0, + "rx_dropped": 0, + "ipaddr": "0.0.0.0", + "speed": 1000, + "duplex": true, + "snr": [ + 30, + 30, + 30, + 30 + ], + "cable_len": 14 + } + }, + { + "ifname": "ath0", + "hwaddr": "01:23:45:67:89:CD", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": false, + "tx_bytes": 212398179151, + "rx_bytes": 3625607638, + "tx_packets": 149906916, + "rx_packets": 53038237, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 34, + "rx_dropped": 0, + "ipaddr": "0.0.0.0", + "speed": 0, + "duplex": false + } + }, + { + "ifname": "br0", + "hwaddr": "01:23:45:67:89:CD", + "enabled": true, + "mtu": 1500, + "status": { + "plugged": true, + "tx_bytes": 143856488, + "rx_bytes": 198175800, + "tx_packets": 204749, + "rx_packets": 1753443, + "tx_errors": 0, + "rx_errors": 0, + "tx_dropped": 0, + "rx_dropped": 0, + "ipaddr": "192.168.1.3", + "ip6addr": [ + { + "addr": "fe80::eea:14ff:fea4:89ab", + "plen": 64 + } + ], + "speed": 0, + "duplex": false + } + } + ], + "provmode": {}, + "ntpclient": {}, + "unms": { + "status": 0 + }, + "gps": { + "lat": 52.379894, + "lon": 4.901608, + "fix": 0, + "sats": 9, + "dim": 3, + "dop": "0.91", + "alt": "248.6", + "time_synced": 0 + } +} diff --git a/fixtures/userdata/mocked_sta-ptmp.json b/fixtures/userdata/mocked_sta-ptmp.json new file mode 100644 index 0000000..4813370 --- /dev/null +++ b/fixtures/userdata/mocked_sta-ptmp.json @@ -0,0 +1 @@ +{"chain_names":[{"number":1,"name": "Chain 0"},{"number":2,"name": "Chain 1"}],"host":{"hostname": "NanoStation 5AC sta name WITH TWO REMOTES","device_id": "d4f4cdf82961e619328a8f72f8d7653b","uptime":265375,"power_time":268567,"time": "2025-06-23 23:14:49","timestamp":2668800167,"fwversion": "v8.7.17","devmodel": "NanoStation 5AC loco","netrole": "bridge","loadavg":0.359863,"totalram":63447040,"freeram":16105472,"temperature":0,"cpuload":44.000000,"height":2},"genuine": "/images/genuine.png","services":{"dhcpc":false,"dhcpd":false,"dhcp6d_stateful":false,"pppoe":false,"airview":2},"firewall":{"iptables":false,"ebtables":false,"ip6tables":false,"eb6tables":false},"portfw":false,"wireless":{"essid": "DemoSSID","mode": "sta-ptmp","ieeemode": "AUTO","band":2,"compat_11n":0,"hide_essid":0,"apmac":"01:23:45:67:89:AB","antenna_gain":13,"frequency":5500,"center1_freq":5530,"dfs":1,"distance":0,"security": "WPA2","noisef":-89,"txpower":-4,"aprepeater":false,"rstatus":5,"chanbw":80,"rx_chainmask":3,"tx_chainmask":3,"nol_state":0,"nol_timeout":0,"cac_state":0,"cac_timeout":0,"rx_idx":9,"rx_nss":2,"tx_idx":8,"tx_nss":2,"throughput":{"tx":12014,"rx":267},"service":{"time":267250,"link":266051},"polling":{"cb_capacity":586950,"dl_capacity":647400,"ul_capacity":526500,"use":46,"tx_use":40,"rx_use":6,"atpc_status":2,"fixed_frame":false,"gps_sync":false,"flex_mode":1,"ff_cap_rep":false},"count":1,"sta":[{"mac":"01:23:45:67:89:GH","lastip":"192.168.1.3","signal":-58,"rssi":38,"noisefloor":-89,"chainrssi":[33,37,0],"tx_idx":8,"rx_idx":9,"tx_nss":2,"rx_nss":2,"tx_latency":0,"distance":1,"tx_packets":0,"tx_lretries":0,"tx_sretries":0,"uptime":170335,"dl_signal_expect":-45,"ul_signal_expect":-59,"cb_capacity_expect":658000,"dl_capacity_expect":692000,"ul_capacity_expect":624000,"dl_rate_expect":9,"ul_rate_expect":8,"dl_linkscore":93,"ul_linkscore":84,"dl_avg_linkscore":93,"ul_avg_linkscore":87,"tx_ratedata":[14,4,372,2223,4708,4037,8142,486840,29437014,24752069],"stats":{"rx_bytes":3622839202,"rx_packets":52999540,"rx_pps":446,"tx_bytes":212303079651,"tx_packets":149832292,"tx_pps":0},"airmax":{"actual_priority":0,"beam":0,"desired_priority":0,"cb_capacity":586950,"dl_capacity":647400,"ul_capacity":526500,"atpc_status":2,"rx":{"usage":6,"cinr":31,"evm":[[30,32,30,29,33,31,29,33,31,31,30,33,34,33,31,33,32,32,31,29,31,30,32,31,30,29,32,31,32,31,31,32,29,31,29,30,32,32,31,32,32,33,31,28,29,31,31,33,32,33,32,32,32,31,33,29,27,31,28,29,31,31,34,28],[39,39,39,39,39,41,39,39,39,39,39,39,38,39,39,39,39,39,39,39,40,39,39,40,39,39,39,40,39,40,39,39,39,39,39,38,39,39,39,39,39,39,40,39,39,40,39,38,39,39,39,39,39,39,39,39,39,39,39,39,38,39,39,39]]},"tx":{"usage":40,"cinr":31,"evm":[[32,31,30,26,32,32,31,28,33,32,32,32,31,31,31,29,30,32,30,27,34,31,31,30,32,29,31,29,31,33,31,31,32,30,31,34,33,31,30,31,30,31,31,32,31,30,33,31,30,31,27,31,30,30,30,30,30,29,32,34,31,30,28,30],[35,35,37,36,36,35,36,36,37,36,37,37,36,36,36,36,36,36,36,36,37,37,36,36,37,36,35,35,37,36,36,37,36,37,36,36,37,36,36,35,36,36,36,36,36,37,37,37,36,37,35,36,36,36,36,37,37,36,36,36,36,36,36,36]]}},"last_disc":1,"remote":{"age":2,"device_id": "03aa0d0b40fed0a47088293584ef4418","hostname": "NanoStation 5AC ap name WITHOUT MODE","platform": "NanoStation 5AC loco","version": "WA.ar934x.v8.7.17.48152.250620.2132","time": "2025-06-23 23:07:34","cpuload":5.050500,"temperature":0,"totalram":63447040,"freeram":16633856,"netrole": "bridge","sys_id":"0xe7fa","tx_throughput":314,"rx_throughput":10548,"uptime":264941,"power_time":268736,"compat_11n":0,"signal":-60,"rssi":36,"noisefloor":-89,"tx_power":-3,"distance":1,"rx_chainmask":3,"chainrssi":[35,31,0],"tx_ratedata":[175,4,47,200,673,158,163,138,68926,19583506],"tx_bytes":5267487876,"rx_bytes":207021597130,"antenna_gain":13,"cable_loss":0,"height":3,"ethlist":[{"ifname": "eth0","enabled":true,"plugged":true,"duplex":true,"speed":1000,"snr":[30,30,30,30],"cable_len":18}],"ipaddr":["192.168.1.3"],"ip6addr":["fe80::eea:14ff:fea4:89cd"],"gps":{"lat": "52.379894","lon": "4.901608","fix":0},"oob":false,"unms":{"status":0},"airview":2,"service":{"time":267234,"link":266056}}},{"mac":"01:23:45:67:89:EF","lastip":"192.168.1.3","signal":-58,"rssi":38,"noisefloor":-89,"chainrssi":[33,37,0],"tx_idx":8,"rx_idx":9,"tx_nss":2,"rx_nss":2,"tx_latency":0,"distance":1,"tx_packets":0,"tx_lretries":0,"tx_sretries":0,"uptime":170335,"dl_signal_expect":-45,"ul_signal_expect":-59,"cb_capacity_expect":658000,"dl_capacity_expect":692000,"ul_capacity_expect":624000,"dl_rate_expect":9,"ul_rate_expect":8,"dl_linkscore":93,"ul_linkscore":84,"dl_avg_linkscore":93,"ul_avg_linkscore":87,"tx_ratedata":[14,4,372,2223,4708,4037,8142,486840,29437014,24752069],"stats":{"rx_bytes":3622839202,"rx_packets":52999540,"rx_pps":446,"tx_bytes":212303079651,"tx_packets":149832292,"tx_pps":0},"airmax":{"actual_priority":0,"beam":0,"desired_priority":0,"cb_capacity":586950,"dl_capacity":647400,"ul_capacity":526500,"atpc_status":2,"rx":{"usage":6,"cinr":31,"evm":[[30,32,30,29,33,31,29,33,31,31,30,33,34,33,31,33,32,32,31,29,31,30,32,31,30,29,32,31,32,31,31,32,29,31,29,30,32,32,31,32,32,33,31,28,29,31,31,33,32,33,32,32,32,31,33,29,27,31,28,29,31,31,34,28],[39,39,39,39,39,41,39,39,39,39,39,39,38,39,39,39,39,39,39,39,40,39,39,40,39,39,39,40,39,40,39,39,39,39,39,38,39,39,39,39,39,39,40,39,39,40,39,38,39,39,39,39,39,39,39,39,39,39,39,39,38,39,39,39]]},"tx":{"usage":40,"cinr":31,"evm":[[32,31,30,26,32,32,31,28,33,32,32,32,31,31,31,29,30,32,30,27,34,31,31,30,32,29,31,29,31,33,31,31,32,30,31,34,33,31,30,31,30,31,31,32,31,30,33,31,30,31,27,31,30,30,30,30,30,29,32,34,31,30,28,30],[35,35,37,36,36,35,36,36,37,36,37,37,36,36,36,36,36,36,36,36,37,37,36,36,37,36,35,35,37,36,36,37,36,37,36,36,37,36,36,35,36,36,36,36,36,37,37,37,36,37,35,36,36,36,36,37,37,36,36,36,36,36,36,36]]}},"last_disc":1,"remote":{"age":2,"device_id": "03aa0d0b40fed0a47088293584ef4418","hostname": "NanoStation 5AC ap name WITH MODE","platform": "NanoStation 5AC loco","version": "WA.ar934x.v8.7.17.48152.250620.2132","time": "2025-06-23 23:07:34","cpuload":5.050500,"temperature":0,"totalram":63447040,"freeram":16633856,"netrole": "bridge","mode": "ap-ptmp","sys_id":"0xe7fa","tx_throughput":314,"rx_throughput":10548,"uptime":264941,"power_time":268736,"compat_11n":0,"signal":-60,"rssi":36,"noisefloor":-89,"tx_power":-3,"distance":1,"rx_chainmask":3,"chainrssi":[35,31,0],"tx_ratedata":[175,4,47,200,673,158,163,138,68926,19583506],"tx_bytes":5267487876,"rx_bytes":207021597130,"antenna_gain":13,"cable_loss":0,"height":3,"ethlist":[{"ifname": "eth0","enabled":true,"plugged":true,"duplex":true,"speed":1000,"snr":[30,30,30,30],"cable_len":18}],"ipaddr":["192.168.1.3"],"ip6addr":["fe80::eea:14ff:fea4:89cd"],"gps":{"lat": "52.379894","lon": "4.901608","fix":0},"oob":false,"unms":{"status":0},"airview":2,"service":{"time":267234,"link":266056}}}],"sta_disconnected":[]},"interfaces":[{"ifname": "eth0","hwaddr":"01:23:45:67:89:CD","enabled":true,"mtu":1500,"status":{"plugged":true,"tx_bytes":4975926283,"rx_bytes":206979884583,"tx_packets":73329864,"rx_packets":185401454,"tx_errors":0,"rx_errors":0,"tx_dropped":0,"rx_dropped":0,"ipaddr":"0.0.0.0","speed":1000,"duplex":true,"snr":[30,30,30,30],"cable_len":14}},{"ifname": "ath0","hwaddr":"01:23:45:67:89:CD","enabled":true,"mtu":1500,"status":{"plugged":false,"tx_bytes":212398179151,"rx_bytes":3625607638,"tx_packets":149906916,"rx_packets":53038237,"tx_errors":0,"rx_errors":0,"tx_dropped":34,"rx_dropped":0,"ipaddr":"0.0.0.0","speed":0,"duplex":false}},{"ifname": "br0","hwaddr":"01:23:45:67:89:CD","enabled":true,"mtu":1500,"status":{"plugged":true,"tx_bytes":143856488,"rx_bytes":198175800,"tx_packets":204749,"rx_packets":1753443,"tx_errors":0,"rx_errors":0,"tx_dropped":0,"rx_dropped":0,"ipaddr":"192.168.1.3","ip6addr":[{"addr":"fe80::eea:14ff:fea4:89ab","plen":64}],"speed":0,"duplex":false}}],"provmode":{},"ntpclient":{},"unms":{"status":0},"gps":{"lat":52.379894,"lon":4.901608,"fix":0,"sats":9,"dim":3,"dop":"0.91","alt":"248.6","time_synced":0}} diff --git a/fixtures/userdata/nanobeam5ac_sta_ptmp_40mhz.json b/fixtures/userdata/nanobeam5ac_sta_ptmp_40mhz.json new file mode 100644 index 0000000..7273987 --- /dev/null +++ b/fixtures/userdata/nanobeam5ac_sta_ptmp_40mhz.json @@ -0,0 +1 @@ +{"chain_names":[{"number":1,"name": "Chain 0"},{"number":2,"name": "Chain 1"}],"host":{"hostname": "NanoBeam-shed1","device_id": "3b3b33333b033333aa3fd33a33333c3c","uptime":84800,"power_time":1463263,"time": "2025-08-06 17:04:27","timestamp":2149610414,"fwversion": "v8.7.18","devmodel": "NanoBeam 5AC","netrole": "bridge","loadavg":0.662109,"totalram":63447040,"freeram":18821120,"temperature":0,"cpuload":32.673267,"height":2},"genuine": "/images/genuine.png","services":{"dhcpc":true,"dhcpd":false,"dhcp6d_stateful":false,"pppoe":false,"airview":2},"firewall":{"iptables":false,"ebtables":false,"ip6tables":false,"eb6tables":false},"portfw":false,"wireless":{"essid": "House-shed1","mode": "sta-ptmp","ieeemode": "AUTO","band":2,"compat_11n":0,"hide_essid":0,"apmac":"24:5A:4C:B0:19:7E","antenna_gain":19,"frequency":5680,"center1_freq":5690,"dfs":1,"distance":300,"security": "WPA2","noisef":-92,"txpower":-4,"aprepeater":false,"rstatus":5,"chanbw":40,"rx_chainmask":3,"tx_chainmask":3,"nol_state":0,"nol_timeout":0,"cac_state":0,"cac_timeout":0,"rx_idx":9,"rx_nss":2,"tx_idx":9,"tx_nss":2,"throughput":{"tx":25282,"rx":1458},"service":{"time":1463110,"link":1462832},"polling":{"cb_capacity":343800,"dl_capacity":342000,"ul_capacity":345600,"use":90,"tx_use":71,"rx_use":19,"atpc_status":4,"fixed_frame":false,"gps_sync":false,"flex_mode":1,"ff_cap_rep":false},"count":1,"sta":[{"mac":"24:5A:4C:B0:19:7E","lastip":"127.0.0.85","signal":-42,"rssi":54,"noisefloor":-92,"chainrssi":[50,52,0],"tx_idx":9,"rx_idx":9,"tx_nss":2,"rx_nss":2,"tx_latency":2,"distance":300,"tx_packets":0,"tx_lretries":0,"tx_sretries":0,"uptime":83171,"dl_signal_expect":-68,"ul_signal_expect":-71,"cb_capacity_expect":247000,"dl_capacity_expect":260000,"ul_capacity_expect":234000,"dl_rate_expect":7,"ul_rate_expect":6,"dl_linkscore":100,"ul_linkscore":100,"dl_avg_linkscore":100,"ul_avg_linkscore":100,"tx_ratedata":[2,0,1,8,5371,380598,5,788,4105441,22057146],"stats":{"rx_bytes":14244495724,"rx_packets":134594169,"rx_pps":1656,"tx_bytes":243474280087,"tx_packets":185581450,"tx_pps":0},"airmax":{"actual_priority":2,"beam":0,"desired_priority":2,"cb_capacity":343800,"dl_capacity":342000,"ul_capacity":345600,"atpc_status":0,"rx":{"usage":19,"cinr":34,"evm":[[34,38,32,32,34,38,34,33,32,36,31,34,33,34,34,34,31,36,32,33,35,35,33,34,33,36,37,35,31,38,34,35,34,34,33,31,35,33,35,35,36,33,36,32,34,34,36,31,36,36,31,35,36,36,34,31,34,34,35,33,32,35,35,33],[50,51,52,52,52,52,51,51,51,51,52,51,51,51,52,51,51,52,51,51,51,51,51,50,51,51,51,51,52,52,52,51,51,51,51,51,52,50,51,52,51,52,51,52,51,51,51,52,52,51,51,52,51,51,51,51,51,51,51,51,52,51,51,51]]},"tx":{"usage":71,"cinr":33,"evm":[[32,32,35,36,34,33,34,32,32,31,31,31,33,32,35,36,33,35,36,32,34,35,31,36,34,32,30,34,34,35,32,31,33,35,35,32,37,33,33,32,30,34,32,31,32,36,35,32,33,32,34,34,33,31,30,35,35,31,37,32,34,34,37,29],[50,53,52,52,50,51,52,49,50,50,51,51,50,49,50,52,51,50,50,51,51,50,53,53,51,51,53,52,51,51,50,50,50,50,50,50,51,53,50,50,53,50,51,50,53,52,52,50,52,50,52,49,50,50,50,52,51,52,50,50,50,50,52,51]]}},"last_disc":0,"remote":{"age":3,"device_id": "b333d333333f3333f0e3ecbcc33d3e33","hostname": "House-Bridge","platform": "LiteAP GPS","version": "WA.ar934x.v8.7.18.48247.250728.0850","time": "2025-08-06 17:04:24","cpuload":31.313101,"temperature":0,"totalram":63447040,"freeram":13537280,"netrole": "bridge","mode": "ap-ptmp","sys_id":"0xe7fd","tx_throughput":1626,"rx_throughput":26145,"uptime":83244,"power_time":1463250,"compat_11n":1,"signal":-43,"rssi":53,"noisefloor":-92,"tx_power":-1,"distance":300,"rx_chainmask":3,"chainrssi":[50,50,0],"tx_ratedata":[2,0,1,8,12,32556,4,12,373371,20880444],"tx_bytes":16765807136,"rx_bytes":279603431363,"antenna_gain":17,"cable_loss":0,"height":null,"ethlist":[{"ifname": "eth0","enabled":true,"plugged":true,"duplex":true,"speed":1000,"snr":[30,30,29,30],"cable_len":48}],"ipaddr":["127.0.0.85"],"gps":{"lat": "52.379894","lon": "4.901608","fix":1,"sats":9,"dim":3,"dop": "0.96","alt": "251.4","time_synced":0},"oob":false,"unms":{"status":0},"airview":2,"service":{"time":1463100,"link":1463007}}}],"sta_disconnected":[]},"interfaces":[{"ifname": "eth0","hwaddr":"21:22:33:44:31:38","enabled":true,"mtu":1500,"status":{"plugged":true,"tx_bytes":11330281132,"rx_bytes":239874919363,"tx_packets":136749032,"rx_packets":188552468,"tx_errors":0,"rx_errors":0,"tx_dropped":0,"rx_dropped":0,"ipaddr":"0.0.0.0","speed":1000,"duplex":true,"snr":[30,30,30,30],"cable_len":1}},{"ifname": "eth1","hwaddr":"23:22:33:44:31:38","enabled":true,"mtu":1500,"status":{"plugged":false,"tx_bytes":0,"rx_bytes":0,"tx_packets":0,"rx_packets":0,"tx_errors":0,"rx_errors":0,"tx_dropped":0,"rx_dropped":0,"ipaddr":"0.0.0.0","speed":0,"duplex":true,"snr":[0,0,0,0],"cable_len":0}},{"ifname": "ath0","hwaddr":"22:22:33:44:31:38","enabled":true,"mtu":1500,"status":{"plugged":false,"tx_bytes":247447969267,"rx_bytes":14476938277,"tx_packets":188601116,"rx_packets":136786005,"tx_errors":0,"rx_errors":0,"tx_dropped":142,"rx_dropped":0,"ipaddr":"0.0.0.0","speed":0,"duplex":false}},{"ifname": "br0","hwaddr":"22:22:33:44:31:38","enabled":true,"mtu":1500,"status":{"plugged":true,"tx_bytes":22880575,"rx_bytes":3479709661,"tx_packets":56372,"rx_packets":30469651,"tx_errors":0,"rx_errors":0,"tx_dropped":0,"rx_dropped":0,"ipaddr":"169.254.49.56","speed":0,"duplex":false}}],"provmode":{},"ntpclient":{"last_sync": "2025-08-06 16:59:11"},"unms":{"status":0},"gps":{"lat":52.379894,"lon":4.901608,"fix":0}} diff --git a/pyproject.toml b/pyproject.toml index 45ed8ba..935c83f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "airos" -version = "0.2.6a0" +version = "0.2.6a1" license = "MIT" description = "Ubiquity airOS module(s) for Python 3." readme = "README.md" diff --git a/script/generate_ha_fixture.py b/script/generate_ha_fixture.py index 84d3228..4774d1b 100644 --- a/script/generate_ha_fixture.py +++ b/script/generate_ha_fixture.py @@ -35,3 +35,30 @@ derived_data = AirOS.derived_data(None, source_data) new_data = AirOSData.from_dict(derived_data) json.dump(new_data.to_dict(), new, indent=2, sort_keys=True) + +new_fixture_path = os.path.join(fixture_dir, "airos_mocked_sta-ptmp.json") +base_fixture_path = os.path.join(userdata_dir, "mocked_sta-ptmp.json") + +with open(base_fixture_path) as source, open(new_fixture_path, "w") as new: + source_data = json.loads(source.read()) + derived_data = AirOS.derived_data(None, source_data) + new_data = AirOSData.from_dict(derived_data) + json.dump(new_data.to_dict(), new, indent=2, sort_keys=True) + +new_fixture_path = os.path.join(fixture_dir, "airos_liteapgps_ap_ptmp_40mhz.json") +base_fixture_path = os.path.join(userdata_dir, "liteapgps_ap_ptmp_40mhz.json") + +with open(base_fixture_path) as source, open(new_fixture_path, "w") as new: + source_data = json.loads(source.read()) + derived_data = AirOS.derived_data(None, source_data) + new_data = AirOSData.from_dict(derived_data) + json.dump(new_data.to_dict(), new, indent=2, sort_keys=True) + +new_fixture_path = os.path.join(fixture_dir, "airos_nanobeam5ac_sta_ptmp_40mhz.json") +base_fixture_path = os.path.join(userdata_dir, "nanobeam5ac_sta_ptmp_40mhz.json") + +with open(base_fixture_path) as source, open(new_fixture_path, "w") as new: + source_data = json.loads(source.read()) + derived_data = AirOS.derived_data(None, source_data) + new_data = AirOSData.from_dict(derived_data) + json.dump(new_data.to_dict(), new, indent=2, sort_keys=True) diff --git a/tests/test_stations.py b/tests/test_stations.py index 695da70..0a4d8bc 100644 --- a/tests/test_stations.py +++ b/tests/test_stations.py @@ -5,17 +5,19 @@ import os from unittest.mock import AsyncMock, MagicMock, patch -from airos.data import AirOS8Data as AirOSData +from airos.data import AirOS8Data as AirOSData, Wireless import airos.exceptions +from airos.exceptions import AirOSDeviceConnectionError, AirOSKeyDataMissingError import pytest import aiofiles import aiohttp +from mashumaro.exceptions import MissingField -async def _read_fixture(fixture: str = "airos_loco5ac_ap-ptp"): +async def _read_fixture(fixture: str = "loco5ac_ap-ptp"): """Read fixture file per device type.""" - fixture_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + fixture_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures", "userdata") path = os.path.join(fixture_dir, f"{fixture}.json") try: async with aiofiles.open(path, encoding="utf-8") as f: @@ -26,9 +28,109 @@ async def _read_fixture(fixture: str = "airos_loco5ac_ap-ptp"): pytest.fail(f"Invalid JSON in fixture file {path}: {e}") +@patch("airos.airos8._LOGGER") +@pytest.mark.asyncio +async def test_status_logs_redacted_data_on_invalid_value(mock_logger, airos_device): + """Test that the status method correctly logs redacted data when it encounters an InvalidFieldValue during deserialization.""" + # --- Prepare fake POST /api/auth response with cookies --- + cookie = SimpleCookie() + cookie["session_id"] = "test-cookie" + cookie["AIROS_TOKEN"] = "abc123" + mock_login_response = MagicMock() + mock_login_response.__aenter__.return_value = mock_login_response + mock_login_response.text = AsyncMock(return_value="{}") + mock_login_response.status = 200 + mock_login_response.cookies = cookie + mock_login_response.headers = {"X-CSRF-ID": "test-csrf-token"} + + # --- Prepare a response with data that would be redacted --- + fixture_data = await _read_fixture("mocked_invalid_wireless_mode") + mock_status_response = MagicMock() + mock_status_response.__aenter__.return_value = mock_status_response + mock_status_response.text = AsyncMock(return_value=json.dumps(fixture_data)) + mock_status_response.status = 200 + mock_status_response.json = AsyncMock(return_value=fixture_data) + + # --- Patch `from_dict` to force the desired exception --- + # We use a valid fixture response, but force the exception to be a MissingField + with ( + patch.object(airos_device.session, "post", return_value=mock_login_response), + patch.object(airos_device.session, "get", return_value=mock_status_response), + patch( + "airos.airos8.AirOSData.from_dict", + side_effect=MissingField( + field_name="wireless", field_type=Wireless, holder_class=AirOSData + ), + ), + ): + await airos_device.login() + with pytest.raises(AirOSKeyDataMissingError): + await airos_device.status() + + # --- Assertions for the logging and redaction --- + assert mock_logger.exception.called + assert mock_logger.exception.call_count == 1 + assert mock_logger.error.called is False + + # Get the dictionary that was passed as the second argument to the logger + logged_data = mock_logger.exception.call_args[0][1] + + # Assert that the dictionary has been redacted + assert logged_data["wireless"]["essid"] == "REDACTED" + assert logged_data["host"]["hostname"] == "REDACTED" + assert logged_data["wireless"]["apmac"] == "00:00:00:00:89:AB" + assert logged_data["interfaces"][2]["status"]["ipaddr"] == "127.0.0.3" + + +@patch("airos.airos8._LOGGER") +@pytest.mark.asyncio +async def test_status_logs_exception_on_missing_field(mock_logger, airos_device): + """Test that the status method correctly logs a full exception when it encounters a MissingField during deserialization.""" + # --- Prepare fake POST /api/auth response with cookies --- + cookie = SimpleCookie() + cookie["session_id"] = "test-cookie" + cookie["AIROS_TOKEN"] = "abc123" + mock_login_response = MagicMock() + mock_login_response.__aenter__.return_value = mock_login_response + mock_login_response.text = AsyncMock(return_value="{}") + mock_login_response.status = 200 + mock_login_response.cookies = cookie + mock_login_response.headers = {"X-CSRF-ID": "test-csrf-token"} + + # --- Prepare fake GET /api/status response with the missing field fixture --- + mock_status_response = MagicMock() + mock_status_response.__aenter__.return_value = mock_status_response + mock_status_response.status = 500 # Non-200 status + mock_status_response.text = AsyncMock(return_value="Error") + mock_status_response.json = AsyncMock(return_value={}) + + with ( + patch.object(airos_device.session, "post", return_value=mock_login_response), + patch.object(airos_device.session, "get", return_value=mock_status_response), + ): + await airos_device.login() + with pytest.raises(AirOSDeviceConnectionError): + await airos_device.status() + + # Assert the logger was called correctly + assert mock_logger.error.called + assert mock_logger.error.call_count == 1 + + log_args = mock_logger.error.call_args[0] + assert log_args[0] == "Status API call failed with status %d: %s" + assert log_args[1] == 500 + assert log_args[2] == "Error" + + @pytest.mark.parametrize( - "mode,fixture", - [("ap-ptp", "airos_loco5ac_ap-ptp"), ("sta-ptp", "airos_loco5ac_sta-ptp")], + ("mode", "fixture"), + [ + ("ap-ptp", "loco5ac_ap-ptp"), + ("sta-ptp", "loco5ac_sta-ptp"), + ("sta-ptmp", "mocked_sta-ptmp"), + ("ap-ptmp", "liteapgps_ap_ptmp_40mhz"), + ("sta-ptmp", "nanobeam5ac_sta_ptmp_40mhz"), + ], ) @pytest.mark.asyncio async def test_ap_object(airos_device, base_url, mode, fixture): @@ -58,6 +160,7 @@ async def test_ap_object(airos_device, base_url, mode, fixture): patch.object(airos_device.session, "get", return_value=mock_status_response), ): assert await airos_device.login() + status: AirOSData = await airos_device.status() # Implies return_json = False # Verify the fixture returns the correct mode From 0d1f8318185957d36d8999aff028598b345481ab Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 6 Aug 2025 19:34:20 +0200 Subject: [PATCH 4/6] CRAI suggestions and feedback --- airos/data.py | 86 ++++++------------- fixtures/airos_liteapgps_ap_ptmp_40mhz.json | 8 +- fixtures/airos_loco5ac_ap-ptp.json | 4 +- fixtures/airos_loco5ac_sta-ptp.json | 4 +- fixtures/airos_mocked_sta-ptmp.json | 8 +- .../airos_nanobeam5ac_sta_ptmp_40mhz.json | 4 +- tests/test_discovery.py | 4 +- tests/test_stations.py | 11 ++- 8 files changed, 50 insertions(+), 79 deletions(-) diff --git a/airos/data.py b/airos/data.py index d86590e..a1d850f 100644 --- a/airos/data.py +++ b/airos/data.py @@ -19,6 +19,7 @@ MAC_ADDRESS_MASK_REGEX = re.compile(r"^(00:){4}[0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}$") +# Helper functions def is_mac_address(value: str) -> bool: """Check if a string is a valid MAC address.""" return bool(MAC_ADDRESS_REGEX.match(value)) @@ -38,27 +39,6 @@ def is_ip_address(value: str) -> bool: return False -def _check_and_log_unknown_enum_value( - data_dict: dict[str, Any], - key: str, - enum_class: type[Enum], - dataclass_name: str, - field_name: str, -) -> None: - """Clean unsupported parameters with logging.""" - value = data_dict.get(key) - if value is not None and isinstance(value, str): - if value not in [e.value for e in enum_class]: - logger.warning( - "Unknown value '%s' for %s.%s. Please report at " - "https://github.com/CoMPaTech/python-airos/issues so we can add support.", - value, - dataclass_name, - field_name, - ) - del data_dict[key] - - def redact_data_smart(data: dict) -> dict: """Recursively redacts sensitive keys in a dictionary.""" sensitive_keys = { @@ -85,7 +65,7 @@ def _redact(d: dict): if k in sensitive_keys: if isinstance(v, str) and (is_mac_address(v) or is_mac_address_mask(v)): # Redact only the first 6 hex characters of a MAC address - redacted_d[k] = "00:00:00:00:" + v.replace("-", ":").upper()[-5:] + redacted_d[k] = "00:11:22:33:" + v.replace("-", ":").upper()[-5:] elif isinstance(v, str) and is_ip_address(v): # Redact to a dummy local IP address redacted_d[k] = "127.0.0.3" @@ -109,46 +89,28 @@ def _redact(d: dict): return _redact(data) -def _redact_ip_addresses(addresses: str | list[str]) -> str | list[str]: - """Redacts the first three octets of an IPv4 address.""" - if isinstance(addresses, str): - addresses = [addresses] +# Data class start - redacted_list = [] - for ip in addresses: - try: - parts = ip.split(".") - if len(parts) == 4: - # Keep the last octet, but replace the rest with a placeholder. - redacted_list.append(f"127.0.0.{parts[3]}") - else: - # Handle non-standard IPs or IPv6 if it shows up here - redacted_list.append("REDACTED") - except (IndexError, ValueError): - # In case the IP string is malformed - redacted_list.append("REDACTED") - - return redacted_list if isinstance(addresses, list) else redacted_list[0] - - -def _redact_mac_addresses(macs: str | list[str]) -> str | list[str]: - """Redacts the first four octets of a MAC address.""" - if isinstance(macs, str): - macs = [macs] - - redacted_list = [] - for mac in macs: - try: - parts = mac.split(":") - if len(parts) == 6: - # Keep the last two octets, replace the rest with a placeholder - redacted_list.append(f"00:11:22:33:{parts[4]}:{parts[5]}") - else: - redacted_list.append("REDACTED") - except (IndexError, ValueError): - redacted_list.append("REDACTED") - return redacted_list if isinstance(macs, list) else redacted_list[0] +def _check_and_log_unknown_enum_value( + data_dict: dict[str, Any], + key: str, + enum_class: type[Enum], + dataclass_name: str, + field_name: str, +) -> None: + """Clean unsupported parameters with logging.""" + value = data_dict.get(key) + if value is not None and isinstance(value, str): + if value not in [e.value for e in enum_class]: + logger.warning( + "Unknown value '%s' for %s.%s. Please report at " + "https://github.com/CoMPaTech/python-airos/issues so we can add support.", + value, + dataclass_name, + field_name, + ) + del data_dict[key] class IeeeMode(Enum): @@ -328,8 +290,8 @@ class EthList: class GPSData: """Leaf definition.""" - lat: str | None = None - lon: str | None = None + lat: float | None = None + lon: float | None = None fix: int | None = None sats: int | None = None # LiteAP GPS dim: int | None = None # LiteAP GPS diff --git a/fixtures/airos_liteapgps_ap_ptmp_40mhz.json b/fixtures/airos_liteapgps_ap_ptmp_40mhz.json index 3a74542..f3b2824 100644 --- a/fixtures/airos_liteapgps_ap_ptmp_40mhz.json +++ b/fixtures/airos_liteapgps_ap_ptmp_40mhz.json @@ -535,8 +535,8 @@ "dim": null, "dop": null, "fix": 0, - "lat": "52.379894", - "lon": "4.901608", + "lat": 52.379894, + "lon": 4.901608, "sats": null, "time_synced": null }, @@ -977,8 +977,8 @@ "dim": null, "dop": null, "fix": 0, - "lat": "52.379894", - "lon": "4.901608", + "lat": 52.379894, + "lon": 4.901608, "sats": null, "time_synced": null }, diff --git a/fixtures/airos_loco5ac_ap-ptp.json b/fixtures/airos_loco5ac_ap-ptp.json index cfe6484..23622e3 100644 --- a/fixtures/airos_loco5ac_ap-ptp.json +++ b/fixtures/airos_loco5ac_ap-ptp.json @@ -524,8 +524,8 @@ "dim": null, "dop": null, "fix": 0, - "lat": "52.379894", - "lon": "4.901608", + "lat": 52.379894, + "lon": 4.901608, "sats": null, "time_synced": null }, diff --git a/fixtures/airos_loco5ac_sta-ptp.json b/fixtures/airos_loco5ac_sta-ptp.json index 247bdb4..4b65c20 100644 --- a/fixtures/airos_loco5ac_sta-ptp.json +++ b/fixtures/airos_loco5ac_sta-ptp.json @@ -524,8 +524,8 @@ "dim": null, "dop": null, "fix": 0, - "lat": "52.379894", - "lon": "4.901608", + "lat": 52.379894, + "lon": 4.901608, "sats": null, "time_synced": null }, diff --git a/fixtures/airos_mocked_sta-ptmp.json b/fixtures/airos_mocked_sta-ptmp.json index a698afd..6d3b25e 100644 --- a/fixtures/airos_mocked_sta-ptmp.json +++ b/fixtures/airos_mocked_sta-ptmp.json @@ -524,8 +524,8 @@ "dim": null, "dop": null, "fix": 0, - "lat": "52.379894", - "lon": "4.901608", + "lat": 52.379894, + "lon": 4.901608, "sats": null, "time_synced": null }, @@ -954,8 +954,8 @@ "dim": null, "dop": null, "fix": 0, - "lat": "52.379894", - "lon": "4.901608", + "lat": 52.379894, + "lon": 4.901608, "sats": null, "time_synced": null }, diff --git a/fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json b/fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json index 36a537a..a6df0eb 100644 --- a/fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json +++ b/fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json @@ -549,8 +549,8 @@ "dim": 3, "dop": 0.96, "fix": 1, - "lat": "52.379894", - "lon": "4.901608", + "lat": 52.379894, + "lon": 4.901608, "sats": 9, "time_synced": 0 }, diff --git a/tests/test_discovery.py b/tests/test_discovery.py index a1710d1..fe20bb0 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -246,7 +246,7 @@ async def _simulate_discovery(): mock_protocol_factory(MagicMock()).callback(parsed_data) - with patch("asyncio.sleep", new=AsyncMock()): + with patch("airos.discovery.asyncio.sleep", new=AsyncMock()): discovery_task = asyncio.create_task(airos_discover_devices(timeout=1)) await _simulate_discovery() @@ -263,7 +263,7 @@ async def test_async_discover_devices_no_devices(mock_datagram_endpoint): """Test discovery returns an empty dict if no devices are found.""" mock_transport, _ = mock_datagram_endpoint - with patch("asyncio.sleep", new=AsyncMock()): + with patch("airos.discovery.asyncio.sleep", new=AsyncMock()): result = await airos_discover_devices(timeout=1) assert result == {} diff --git a/tests/test_stations.py b/tests/test_stations.py index 0a4d8bc..dadddf5 100644 --- a/tests/test_stations.py +++ b/tests/test_stations.py @@ -76,9 +76,18 @@ async def test_status_logs_redacted_data_on_invalid_value(mock_logger, airos_dev logged_data = mock_logger.exception.call_args[0][1] # Assert that the dictionary has been redacted + assert "wireless" in logged_data + assert "essid" in logged_data["wireless"] assert logged_data["wireless"]["essid"] == "REDACTED" + assert "host" in logged_data + assert "hostname" in logged_data["host"] assert logged_data["host"]["hostname"] == "REDACTED" - assert logged_data["wireless"]["apmac"] == "00:00:00:00:89:AB" + assert "apmac" in logged_data["wireless"] + assert logged_data["wireless"]["apmac"] == "00:11:22:33:89:AB" + assert "interfaces" in logged_data + assert len(logged_data["interfaces"]) > 2 + assert "status" in logged_data["interfaces"][2] + assert "ipaddr" in logged_data["interfaces"][2]["status"] assert logged_data["interfaces"][2]["status"]["ipaddr"] == "127.0.0.3" From 1391a9b3f8028cd08fbe24bb7fde9733233dbda4 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 6 Aug 2025 19:40:12 +0200 Subject: [PATCH 5/6] Bumnp to a2 for testing --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 935c83f..19e6a12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "airos" -version = "0.2.6a1" +version = "0.2.6a2" license = "MIT" description = "Ubiquity airOS module(s) for Python 3." readme = "README.md" From 508c04f3655ba717ce93e8c2879c19cf41a63bb0 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 6 Aug 2025 19:51:35 +0200 Subject: [PATCH 6/6] Bump release --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 19e6a12..c5965cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "airos" -version = "0.2.6a2" +version = "0.2.6" license = "MIT" description = "Ubiquity airOS module(s) for Python 3." readme = "README.md"