nickovs / unificontrol

A high-level Python interface to the Unifi controller software
Apache License 2.0
96 stars 41 forks source link

Provide a dataclass abstraction for clients and other objects #5

Open nickovs opened 4 years ago

nickovs commented 4 years ago

Currently the representation of clients in unificontrol is just the raw data that comes back from the underlying API. In a comment on another issue @WoLpH suggested that it would be useful to have an abstraction that represents clients as their own data objects and provide a higher level API for manipulating them.

wolph commented 4 years ago

For Python 3.6 there's a backport available so that's easily fixed: https://pypi.org/project/dataclasses/ The requirement would be:

dataclasses; python_version < '3.7'

Another question would be how this new API would be exposed. The way I see it there are a few options:

  1. Add an option to client so client.list_clients() returns this object when the new API is enabled
  2. Add a parameter to methods such as client.list_client() so it returns the new style
  3. Add a new method for this so that client.clients would be a collection class that is both iterable and subscriptable using something like client.clients[mac_address].
  4. Add a dict api to the dataclasses to maintain backwards compatibility

My vote would be option 3, but that's your decision :)

wolph commented 4 years ago

Example:

@dataclasses.dataclass                       
class Client:                                
    _id: str                                 
    assoc_time: int                          
    authorized: bool                         
    first_seen: int                          
    ip: str                                  
    is_guest: bool                           
    is_wired: bool                           
    last_seen: int                           
    latest_assoc_time: int                   
    mac: str                                 
    oui: str                                 
    qos_policy_applied: bool                 
    rx_bytes: int                            
    rx_bytes_r: int                          
    rx_packets: int                          
    site_id: str                             
    tx_bytes: int                            
    tx_bytes_r: int                          
    tx_packets: int                          
    tx_retries: int                          
    uptime: int                              
    user_id: str                             
    wifi_tx_attempts: int                    
    _is_guest_by_uap: bool = None            
    _is_guest_by_ugw: bool = None            
    _is_guest_by_usw: bool = None            
    _last_seen_by_uap: int = 0               
    _last_seen_by_ugw: int = 0               
    _last_seen_by_usw: int = 0               
    _uptime_by_uap: int = 0                  
    _uptime_by_ugw: int = 0                  
    _uptime_by_usw: int = 0                  
    anomalies: int = 0                       
    ap_mac: str = ''                         
    bssid: str = ''                          
    bytes_r: int = 0                         
    ccq: int = 0                             
    channel: int = 0                         
    confidence: int = 0                      
    dev_cat: int = 0                         
    dev_family: int = 0                      
    dev_id: int = 0                          
    dev_vendor: int = 0                      
    device_name: str = ''                    
    dhcpend_time: int = 0                    
    essid: str = ''                          
    fingerprint_source: int = 0              
    fixed_ip: str = ''                       
    fw_version: str = ''                     
    gw_mac: str = ''                         
    hostname: str = ''                       
    idletime: int = 0                        
    is_11r: bool = None                      
    name: str = ''                           
    network: str = ''                        
    network_id: str = ''                     
    noise: int = 0                           
    noted: bool = None                       
    os_class: int = 0                        
    os_name: int = 0                         
    powersave_enabled: bool = None           
    priority: int = 0                        
    radio: str = ''                          
    radio_name: str = ''                     
    radio_proto: str = ''                    
    roam_count: int = 0                      
    rssi: int = 0                            
    rx_rate: int = 0                         
    satisfaction: int = 0                    
    score: int = 0                           
    signal: int = 0                          
    sw_depth: int = 0                        
    sw_mac: str = ''                         
    sw_port: int = 0                         
    tx_power: int = 0                        
    tx_rate: int = 0                         
    use_fixedip: bool = None                 
    usergroup_id: str = ''                   
    vlan: int = 0                            

Generated by:

 import collections                                          
 keys = collections.defaultdict(list)                        
 counter = 0                                                 
 for data in client.list_clients():                                        
     counter += 1                                            
     data = {k.replace('-', '_'): v for k, v in data.items()}
     for key, value in data.items():                         
         keys[key].append(value)                             

 required = dict()                                           
 optional = dict()                                           
 for key, values in sorted(keys.items()):                    
     type_ = type(values[0])                                 
     if type_ is int:                                        
         default = 0                                         
     elif type_ is str:                                      
         default = ''                                        
     elif type_ is bool:                                     
         default = None                                      
     else:                                                   
         raise TypeError()                                   

     type_str = f'{type_.__name__}'                          

     if len(values) == counter:                              
         required[key] = type_str                            
     else:                                                   
         optional[key] = f'{type_str} = {default!r}'         

 for key, type_ in sorted(required.items()):                 
     print(f'{key}: {type_}')                                

 for key, type_ in sorted(optional.items()):                 
     print(f'{key}: {type_}')                                
wolph commented 4 years ago

Using these we can create a high-level API that could work something like this:

fixed_hosts = dict(...)

for client in api.clients:
    fixed_host = fixed_hosts.get(client.mac)
    if not fixed_host:
        continue

    client.name = fixed_hostname.name
    client.fixed_ip = fixed_hostname.ip

# Or:
for mac, fixed_host in fixed_hosts.items():
    api.clients[mac].name = fixed_host.name
    api.clients[mac].fixed_ip = fixed_host.ip