22
33from dataclasses import dataclass
44from enum import Enum
5+ import ipaddress
56import logging
7+ import re
68from typing import Any
79
810from mashumaro import DataClassDictMixin
911
1012logger = logging .getLogger (__name__ )
1113
14+ # Regex for a standard MAC address format (e.g., 01:23:45:67:89:AB)
15+ # This handles both colon and hyphen separators.
16+ MAC_ADDRESS_REGEX = re .compile (r"^([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})$" )
17+
18+ # Regex for a MAC address mask (e.g., the redacted format 00:00:00:00:89:AB)
19+ MAC_ADDRESS_MASK_REGEX = re .compile (r"^(00:){4}[0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}$" )
20+
21+
22+ # Helper functions
23+ def is_mac_address (value : str ) -> bool :
24+ """Check if a string is a valid MAC address."""
25+ return bool (MAC_ADDRESS_REGEX .match (value ))
26+
27+
28+ def is_mac_address_mask (value : str ) -> bool :
29+ """Check if a string is a valid MAC address mask (e.g., the redacted format)."""
30+ return bool (MAC_ADDRESS_MASK_REGEX .match (value ))
31+
32+
33+ def is_ip_address (value : str ) -> bool :
34+ """Check if a string is a valid IPv4 or IPv6 address."""
35+ try :
36+ ipaddress .ip_address (value )
37+ return True
38+ except ValueError :
39+ return False
40+
41+
42+ def redact_data_smart (data : dict ) -> dict :
43+ """Recursively redacts sensitive keys in a dictionary."""
44+ sensitive_keys = {
45+ "hostname" ,
46+ "essid" ,
47+ "mac" ,
48+ "apmac" ,
49+ "hwaddr" ,
50+ "lastip" ,
51+ "ipaddr" ,
52+ "ip6addr" ,
53+ "device_id" ,
54+ "sys_id" ,
55+ "station_id" ,
56+ "platform" ,
57+ }
58+
59+ def _redact (d : dict ):
60+ if not isinstance (d , dict ):
61+ return d
62+
63+ redacted_d = {}
64+ for k , v in d .items ():
65+ if k in sensitive_keys :
66+ if isinstance (v , str ) and (is_mac_address (v ) or is_mac_address_mask (v )):
67+ # Redact only the first 6 hex characters of a MAC address
68+ redacted_d [k ] = "00:11:22:33:" + v .replace ("-" , ":" ).upper ()[- 5 :]
69+ elif isinstance (v , str ) and is_ip_address (v ):
70+ # Redact to a dummy local IP address
71+ redacted_d [k ] = "127.0.0.3"
72+ elif isinstance (v , list ) and all (
73+ isinstance (i , str ) and is_ip_address (i ) for i in v
74+ ):
75+ # Redact list of IPs to a dummy list
76+ redacted_d [k ] = ["127.0.0.3" ]
77+ else :
78+ redacted_d [k ] = "REDACTED"
79+ elif isinstance (v , dict ):
80+ redacted_d [k ] = _redact (v )
81+ elif isinstance (v , list ):
82+ redacted_d [k ] = [
83+ _redact (item ) if isinstance (item , dict ) else item for item in v
84+ ]
85+ else :
86+ redacted_d [k ] = v
87+ return redacted_d
88+
89+ return _redact (data )
90+
91+
92+ # Data class start
93+
1294
1395def _check_and_log_unknown_enum_value (
1496 data_dict : dict [str , Any ],
@@ -152,6 +234,7 @@ class Polling:
152234 fixed_frame : bool
153235 gps_sync : bool
154236 ff_cap_rep : bool
237+ flex_mode : int | None = None # Not present in all devices
155238
156239
157240@dataclass
@@ -207,9 +290,14 @@ class EthList:
207290class GPSData :
208291 """Leaf definition."""
209292
210- lat : str
211- lon : str
212- fix : int
293+ lat : float | None = None
294+ lon : float | None = None
295+ fix : int | None = None
296+ sats : int | None = None # LiteAP GPS
297+ dim : int | None = None # LiteAP GPS
298+ dop : float | None = None # LiteAP GPS
299+ alt : float | None = None # LiteAP GPS
300+ time_synced : int | None = None # LiteAP GPS
213301
214302
215303@dataclass
@@ -235,7 +323,6 @@ class Remote:
235323 totalram : int
236324 freeram : int
237325 netrole : str
238- mode : WirelessMode
239326 sys_id : str
240327 tx_throughput : int
241328 rx_throughput : int
@@ -254,20 +341,21 @@ class Remote:
254341 rx_bytes : int
255342 antenna_gain : int
256343 cable_loss : int
257- height : int
258344 ethlist : list [EthList ]
259345 ipaddr : list [str ]
260- ip6addr : list [str ]
261346 gps : GPSData
262347 oob : bool
263348 unms : UnmsStatus
264349 airview : int
265350 service : ServiceTime
351+ mode : WirelessMode | None = None # Investigate why remotes can have no mode set
352+ ip6addr : list [str ] | None = None # For v4 only devices
353+ height : int | None = None
266354
267355 @classmethod
268356 def __pre_deserialize__ (cls , d : dict [str , Any ]) -> dict [str , Any ]:
269357 """Pre-deserialize hook for Wireless."""
270- _check_and_log_unknown_enum_value (d , "mode" , WirelessMode , "Wireless " , "mode" )
358+ _check_and_log_unknown_enum_value (d , "mode" , WirelessMode , "Remote " , "mode" )
271359 return d
272360
273361
@@ -329,7 +417,6 @@ class Wireless:
329417 """Leaf definition."""
330418
331419 essid : str
332- mode : WirelessMode
333420 ieeemode : IeeeMode
334421 band : int
335422 compat_11n : int
@@ -362,6 +449,7 @@ class Wireless:
362449 count : int
363450 sta : list [Station ]
364451 sta_disconnected : list [Disconnected ]
452+ mode : WirelessMode | None = None # Investigate further (see WirelessMode in Remote)
365453
366454 @classmethod
367455 def __pre_deserialize__ (cls , d : dict [str , Any ]) -> dict [str , Any ]:
0 commit comments