1+ from zeroconf import ServiceBrowser , Zeroconf
2+ import ipaddress
3+ import logging
4+ import time
5+
6+ from pprint import pprint
7+
8+ from thing import FoundThing
9+
10+ class Browser :
11+ def __init__ (self , service = "labthing" , protocol = "tcp" ):
12+ self .service_record = f"_{ service } ._{ protocol } .local."
13+
14+ self .services = {}
15+
16+ self .add_service_callbacks = set ()
17+ self .remove_service_callbacks = set ()
18+
19+ self ._zeroconf = Zeroconf ()
20+ self ._browser = None
21+
22+ def __enter__ (self ):
23+ self .open ()
24+ return self
25+
26+ def __exit__ (self ,type , value , traceback ):
27+ return self .close ()
28+
29+ def open (self ):
30+ self ._browser = ServiceBrowser (self ._zeroconf , self .service_record , self )
31+ return self
32+
33+ def close (self , * args , ** kwargs ):
34+ logging .info (f"Closing browser { self } " )
35+ return self ._zeroconf .close (* args , ** kwargs )
36+
37+ def remove_service (self , zeroconf , type , name ):
38+ service = zeroconf .get_service_info (type , name )
39+ if name in self .services :
40+ for callback in self .remove_service_callbacks :
41+ callback (self .services [name ])
42+ del self .services [name ]
43+
44+ def add_service (self , zeroconf , type , name ):
45+ service = zeroconf .get_service_info (type , name )
46+ self .services [name ] = parse_service (service )
47+ for callback in self .add_service_callbacks :
48+ callback (self .services [name ])
49+
50+ ### TODO: The names of these functions are an abomination and should be renamed
51+ def add_add_service_callback (self , callback , run_on_existing : bool = True ):
52+ self .add_service_callbacks .add (callback )
53+ if run_on_existing :
54+ for service in self .services :
55+ callback (service )
56+
57+ def remove_add_service_callback (self , callback ):
58+ self .add_service_callbacks .discard (callback )
59+
60+ def add_remove_service_callback (self , callback ):
61+ self .remove_service_callbacks .add (callback )
62+
63+ def remove_add_service_callback (self , callback ):
64+ self .remove_service_callbacks .discard (callback )
65+
66+
67+ class ThingBrowser (Browser ):
68+ def __init__ (self , * args , ** kwargs ):
69+ Browser .__init__ (self , * args , ** kwargs )
70+ self ._things = set ()
71+ self .add_add_service_callback (self .add_service_to_things )
72+ self .add_remove_service_callback (self .remove_service_from_things )
73+
74+ @property
75+ def things (self ):
76+ return list (self ._things )
77+
78+ def add_service_to_things (self , service ):
79+ self ._things .add (service_to_thing (service ))
80+
81+ def remove_service_from_things (self , service ):
82+ discards = set ()
83+ for thing in self ._things :
84+ if thing .name == service .get ("name" ):
85+ discards .add (thing )
86+ for discard_thing in discards :
87+ self ._things .discard (discard_thing )
88+
89+ def wait_for_first (self ):
90+ while len (self .things ) == 0 :
91+ time .sleep (0.1 )
92+ return self .things [0 ]
93+
94+ def parse_service (service ):
95+ properties = {}
96+ for k , v in service .properties .items ():
97+ properties [k .decode ()] = v .decode ()
98+
99+ return {
100+ "address" : ipaddress .ip_address (service .address ),
101+ "addresses" : {ipaddress .ip_address (a ) for a in service .addresses },
102+ "port" : service .port ,
103+ "name" : service .name ,
104+ "server" : service .server ,
105+ "properties" : properties ,
106+ }
107+
108+
109+ def service_to_thing (service : dict ):
110+ if not ("addresses" in service or "port" in service or "path" in service .get ("properties" , {})):
111+ raise KeyError ("Invalid service. Missing keys." )
112+ return FoundThing (service .get ("name" ), service .get ("addresses" ), service .get ("port" ), service .get ("properties" ).get ("path" ))
113+
114+
115+ if __name__ == "__main__" :
116+ import atexit
117+ import time
118+
119+ logging .getLogger ().setLevel (logging .DEBUG )
120+
121+ browser = ThingBrowser ().open ()
122+ atexit .register (browser .close )
123+
124+ thing = browser .wait_for_first ()
0 commit comments