diff --git a/airos/airos8.py b/airos/airos8.py index c5086c8..f073fbf 100644 --- a/airos/airos8.py +++ b/airos/airos8.py @@ -4,6 +4,7 @@ import json import logging +from typing import Any from urllib.parse import urlparse import aiohttp @@ -190,6 +191,38 @@ async def login(self) -> bool: _LOGGER.exception("Error during login") raise DeviceConnectionError from err + def derived_data( + self, response: dict[str, Any] | None = None + ) -> dict[str, Any] | None: + """Add derived data to the device response.""" + addresses = {} + interface_order = ["br0", "eth0", "ath0"] + + interfaces = response.get("interfaces", []) + + # No interfaces, no mac, no usability + if not interfaces: + raise KeyDataMissingError from None + + for interface in interfaces: + if interface["enabled"]: # Only consider if enabled + addresses[interface["ifname"]] = interface["hwaddr"] + + for interface in interface_order: + if interface in addresses: + response["derived"] = { + "mac": addresses[interface], + "mac_interface": interface, + } + return response + + # Fallback take fist alternate interface found + response["derived"] = { + "mac": interfaces[0]["hwaddr"], + "mac_interface": interfaces[0]["ifname"], + } + return response + async def status(self) -> AirOSData: """Retrieve status from the device.""" if not self.connected: @@ -211,7 +244,8 @@ async def status(self) -> AirOSData: response_text = await response.text() response_json = json.loads(response_text) try: - airos_data = AirOSData.from_dict(response_json) + 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 KeyDataMissingError from err diff --git a/airos/data.py b/airos/data.py index 0dc69a5..d969293 100644 --- a/airos/data.py +++ b/airos/data.py @@ -427,6 +427,14 @@ class GPSMain: fix: int +@dataclass +class Derived: + """Contain custom data generated by this module.""" + + mac: str # Base device MAC address (i.e. eth0) + mac_interface: str # Interface derived from + + @dataclass class AirOS8Data(DataClassDictMixin): """Dataclass for AirOS v8 devices.""" @@ -443,3 +451,4 @@ class AirOS8Data(DataClassDictMixin): ntpclient: Any unms: UnmsStatus gps: GPSMain + derived: Derived diff --git a/fixtures/sta-ptp.json b/fixtures/sta-ptp.json index ba78d08..a0ce69c 100644 --- a/fixtures/sta-ptp.json +++ b/fixtures/sta-ptp.json @@ -1 +1 @@ -{"chain_names":[{"number":1,"name": "Chain 0"},{"number":2,"name": "Chain 1"}],"host":{"hostname": "NanoStation 5AC sta name","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-ptp","ieeemode": "AUTO","band":2,"compat_11n":0,"hide_essid":0,"apmac":"01:23:45:67:89:CD","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,"ff_cap_rep":false},"count":1,"sta":[{"mac":"01:23:45:67:89:CD","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","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-ptp","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:7b8"],"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":"0C:EA:14:A5:08:06","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":"0C:EA:14:A4:08:06","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":"0C:EA:14:A4:08:06","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:806","plen":64}],"speed":0,"duplex":false}}],"provmode":{},"ntpclient":{},"unms":{"status":0},"gps":{"lat":52.379894,"lon":4.901608,"fix":0}} +{"chain_names":[{"number":1,"name": "Chain 0"},{"number":2,"name": "Chain 1"}],"host":{"hostname": "NanoStation 5AC sta name","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-ptp","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,"ff_cap_rep":false},"count":1,"sta":[{"mac":"01:23:45:67:89:CD","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","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-ptp","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:7b8"],"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:806","plen":64}],"speed":0,"duplex":false}}],"provmode":{},"ntpclient":{},"unms":{"status":0},"gps":{"lat":52.379894,"lon":4.901608,"fix":0}} diff --git a/pyproject.toml b/pyproject.toml index 66800dc..d597ad1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "airos" -version = "0.2.0" +version = "0.2.1" license = "MIT" description = "Ubiquity airOS module(s) for Python 3." readme = "README.md" diff --git a/tests/test_stations.py b/tests/test_stations.py index 029afcd..a523b03 100644 --- a/tests/test_stations.py +++ b/tests/test_stations.py @@ -59,6 +59,7 @@ async def test_ap_object(airos_device, base_url, mode): # Verify the fixture returns the correct mode assert status.wireless.mode.value == mode + assert status.derived.mac_interface == "br0" @pytest.mark.asyncio