Add diagnostics to UISP AirOS (#149631)

This commit is contained in:
Tom 2025-08-01 09:24:22 +02:00 committed by GitHub
parent 8b53b26333
commit 22e054f4cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 689 additions and 1 deletions

View File

@ -0,0 +1,33 @@
"""Diagnostics support for airOS."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from .coordinator import AirOSConfigEntry
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
TO_REDACT_AIROS = [
"hostname", # Prevent leaking device naming
"essid", # Network SSID
"lat", # GPS latitude to prevent exposing location data.
"lon", # GPS longitude to prevent exposing location data.
*HW_REDACT,
*IP_REDACT,
]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirOSConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
}

View File

@ -41,7 +41,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info: todo
discovery: todo
docs-data-update: done

View File

@ -0,0 +1,623 @@
# serializer version: 1
# name: test_diagnostics
dict({
'data': dict({
'chain_names': list([
dict({
'name': 'Chain 0',
'number': 1,
}),
dict({
'name': 'Chain 1',
'number': 2,
}),
]),
'derived': dict({
'mac': '**REDACTED**',
'mac_interface': 'br0',
}),
'firewall': dict({
'eb6tables': False,
'ebtables': False,
'ip6tables': False,
'iptables': False,
}),
'genuine': '/images/genuine.png',
'gps': dict({
'fix': 0,
'lat': '**REDACTED**',
'lon': '**REDACTED**',
}),
'host': dict({
'cpuload': 10.10101,
'device_id': '03aa0d0b40fed0a47088293584ef5432',
'devmodel': 'NanoStation 5AC loco',
'freeram': 16564224,
'fwversion': 'v8.7.17',
'height': 3,
'hostname': '**REDACTED**',
'loadavg': 0.412598,
'netrole': 'bridge',
'power_time': 268683,
'temperature': 0,
'time': '2025-06-23 23:06:42',
'timestamp': 2668313184,
'totalram': 63447040,
'uptime': 264888,
}),
'interfaces': list([
dict({
'enabled': True,
'hwaddr': '**REDACTED**',
'ifname': 'eth0',
'mtu': 1500,
'status': dict({
'cable_len': 18,
'duplex': True,
'ip6addr': None,
'ipaddr': '**REDACTED**',
'plugged': True,
'rx_bytes': 3984971949,
'rx_dropped': 0,
'rx_errors': 4,
'rx_packets': 73564835,
'snr': list([
30,
30,
30,
30,
]),
'speed': 1000,
'tx_bytes': 209900085624,
'tx_dropped': 10,
'tx_errors': 0,
'tx_packets': 185866883,
}),
}),
dict({
'enabled': True,
'hwaddr': '**REDACTED**',
'ifname': 'ath0',
'mtu': 1500,
'status': dict({
'cable_len': None,
'duplex': False,
'ip6addr': None,
'ipaddr': '**REDACTED**',
'plugged': False,
'rx_bytes': 206938324766,
'rx_dropped': 0,
'rx_errors': 0,
'rx_packets': 149767200,
'snr': None,
'speed': 0,
'tx_bytes': 5265602738,
'tx_dropped': 2005,
'tx_errors': 0,
'tx_packets': 52980390,
}),
}),
dict({
'enabled': True,
'hwaddr': '**REDACTED**',
'ifname': 'br0',
'mtu': 1500,
'status': dict({
'cable_len': None,
'duplex': False,
'ip6addr': '**REDACTED**',
'ipaddr': '**REDACTED**',
'plugged': True,
'rx_bytes': 204802727,
'rx_dropped': 0,
'rx_errors': 0,
'rx_packets': 1791592,
'snr': None,
'speed': 0,
'tx_bytes': 236295176,
'tx_dropped': 0,
'tx_errors': 0,
'tx_packets': 298119,
}),
}),
]),
'ntpclient': dict({
}),
'portfw': False,
'provmode': dict({
}),
'services': dict({
'airview': 2,
'dhcp6d_stateful': False,
'dhcpc': False,
'dhcpd': False,
'pppoe': False,
}),
'unms': dict({
'status': 0,
'timestamp': None,
}),
'wireless': dict({
'antenna_gain': 13,
'apmac': '**REDACTED**',
'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': '**REDACTED**',
'frequency': 5500,
'hide_essid': 0,
'ieeemode': '11ACVHT80',
'mode': 'ap-ptp',
'noisef': -89,
'nol_state': 0,
'nol_timeout': 0,
'polling': dict({
'atpc_status': 2,
'cb_capacity': 593970,
'dl_capacity': 647400,
'ff_cap_rep': False,
'fixed_frame': False,
'gps_sync': False,
'rx_use': 42,
'tx_use': 6,
'ul_capacity': 540540,
'use': 48,
}),
'rstatus': 5,
'rx_chainmask': 3,
'rx_idx': 8,
'rx_nss': 2,
'security': 'WPA2',
'service': dict({
'link': 266003,
'time': 267181,
}),
'sta': list([
dict({
'airmax': dict({
'actual_priority': 0,
'atpc_status': 2,
'beam': 0,
'cb_capacity': 593970,
'desired_priority': 0,
'dl_capacity': 647400,
'rx': dict({
'cinr': 31,
'evm': list([
list([
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,
29,
35,
31,
33,
32,
29,
]),
list([
34,
34,
35,
34,
35,
35,
34,
34,
34,
34,
34,
34,
34,
34,
35,
35,
34,
34,
35,
34,
33,
33,
35,
34,
34,
35,
34,
35,
34,
34,
35,
34,
34,
33,
34,
34,
34,
34,
34,
35,
35,
35,
34,
35,
33,
34,
34,
34,
34,
35,
35,
34,
34,
34,
34,
34,
34,
34,
34,
34,
34,
34,
35,
35,
]),
]),
'usage': 42,
}),
'tx': dict({
'cinr': 31,
'evm': list([
list([
32,
34,
28,
33,
35,
30,
31,
33,
30,
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,
]),
list([
37,
37,
37,
38,
38,
37,
36,
38,
38,
37,
37,
37,
37,
37,
39,
37,
37,
37,
37,
37,
37,
36,
37,
37,
37,
37,
37,
37,
37,
38,
37,
37,
38,
37,
37,
37,
38,
37,
38,
37,
37,
37,
37,
37,
36,
37,
37,
37,
37,
37,
37,
38,
37,
37,
38,
37,
36,
37,
37,
37,
37,
37,
37,
37,
]),
]),
'usage': 6,
}),
'ul_capacity': 540540,
}),
'airos_connected': True,
'cb_capacity_expect': 416000,
'chainrssi': list([
35,
32,
0,
]),
'distance': 1,
'dl_avg_linkscore': 100,
'dl_capacity_expect': 208000,
'dl_linkscore': 100,
'dl_rate_expect': 3,
'dl_signal_expect': -80,
'last_disc': 1,
'lastip': '**REDACTED**',
'mac': '**REDACTED**',
'noisefloor': -89,
'remote': dict({
'age': 1,
'airview': 2,
'antenna_gain': 13,
'cable_loss': 0,
'chainrssi': list([
33,
37,
0,
]),
'compat_11n': 0,
'cpuload': 43.564301,
'device_id': 'd4f4cdf82961e619328a8f72f8d7653b',
'distance': 1,
'ethlist': list([
dict({
'cable_len': 14,
'duplex': True,
'enabled': True,
'ifname': 'eth0',
'plugged': True,
'snr': list([
30,
30,
29,
30,
]),
'speed': 1000,
}),
]),
'freeram': 14290944,
'gps': dict({
'fix': 0,
'lat': '**REDACTED**',
'lon': '**REDACTED**',
}),
'height': 2,
'hostname': '**REDACTED**',
'ip6addr': '**REDACTED**',
'ipaddr': '**REDACTED**',
'mode': 'sta-ptp',
'netrole': 'bridge',
'noisefloor': -90,
'oob': False,
'platform': 'NanoStation 5AC loco',
'power_time': 268512,
'rssi': 38,
'rx_bytes': 3624206478,
'rx_chainmask': 3,
'rx_throughput': 251,
'service': dict({
'link': 265996,
'time': 267195,
}),
'signal': -58,
'sys_id': '0xe7fa',
'temperature': 0,
'time': '2025-06-23 23:13:54',
'totalram': 63447040,
'tx_bytes': 212308148210,
'tx_power': -4,
'tx_ratedata': list([
14,
4,
372,
2223,
4708,
4037,
8142,
485763,
29420892,
24748154,
]),
'tx_throughput': 16023,
'unms': dict({
'status': 0,
'timestamp': None,
}),
'uptime': 265320,
'version': 'WA.ar934x.v8.7.17.48152.250620.2132',
}),
'rssi': 37,
'rx_idx': 8,
'rx_nss': 2,
'signal': -59,
'stats': dict({
'rx_bytes': 206938324814,
'rx_packets': 149767200,
'rx_pps': 846,
'tx_bytes': 5265602739,
'tx_packets': 52980390,
'tx_pps': 0,
}),
'tx_idx': 9,
'tx_latency': 0,
'tx_lretries': 0,
'tx_nss': 2,
'tx_packets': 0,
'tx_ratedata': list([
175,
4,
47,
200,
673,
158,
163,
138,
68895,
19577430,
]),
'tx_sretries': 0,
'ul_avg_linkscore': 88,
'ul_capacity_expect': 624000,
'ul_linkscore': 86,
'ul_rate_expect': 8,
'ul_signal_expect': -55,
'uptime': 170281,
}),
]),
'sta_disconnected': list([
]),
'throughput': dict({
'rx': 9907,
'tx': 222,
}),
'tx_chainmask': 3,
'tx_idx': 9,
'tx_nss': 2,
'txpower': -3,
}),
}),
'entry_data': dict({
'host': '**REDACTED**',
'password': '**REDACTED**',
'username': 'ubnt',
}),
})
# ---

View File

@ -0,0 +1,32 @@
"""Diagnostic tests for airOS."""
from unittest.mock import MagicMock
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.airos.coordinator import AirOSData
from homeassistant.core import HomeAssistant
from . import setup_integration
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_airos_client: MagicMock,
mock_config_entry: MockConfigEntry,
ap_fixture: AirOSData,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics."""
await setup_integration(hass, mock_config_entry)
assert (
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
== snapshot
)