1+ """Utilities to support zeroconf discovery of new plugs on the network."""
2+
13import asyncio
2- from typing import Optional
4+ from contextlib import suppress
5+ import logging
6+
7+ from zeroconf import BadTypeInNameException , ServiceBrowser , ServiceListener , Zeroconf
8+ from zeroconf .asyncio import AsyncServiceInfo
39
10+ import homeassistant .components .zeroconf
411from homeassistant .core import HomeAssistant , callback
5- from homeassistant .loader import bind_hass
6- from zeroconf import ServiceBrowser , ServiceListener , Zeroconf
712from homeassistant .helpers .dispatcher import async_dispatcher_send
8- import homeassistant .components . zeroconf
13+ from homeassistant .loader import bind_hass
914
1015from .const import (
1116 ZEROCONF_ADD_PLUG_SIGNAL ,
1217 ZEROCONF_REMOVE_PLUG_SIGNAL ,
1318 ZEROCONF_UPDATE_PLUG_SIGNAL ,
1419)
1520
16- import logging
1721_LOGGER = logging .getLogger (__name__ )
1822
1923
2024class PowersensorServiceListener (ServiceListener ):
21- def __init__ (self , hass : HomeAssistant , debounce_timeout : float = 60 ):
25+ """A zeroconf service listener that handles the discovery of plugs and signals the dispatcher."""
26+
27+ def __init__ (self , hass : HomeAssistant , debounce_timeout : float = 60 ) -> None :
28+ """Initialize the listener, set up various buffers to hold info."""
2229 self ._hass = hass
23- self ._plugs = {}
24- self ._discoveries = {}
25- self ._pending_removals = {}
30+ self ._plugs : dict [ str , dict ] = {}
31+ self ._discoveries : dict [ str , AsyncServiceInfo ] = {}
32+ self ._pending_removals : dict [ str , asyncio . Task ] = {}
2633 self ._debounce_seconds = debounce_timeout
2734
2835 def add_service (self , zc , type_ , name ):
36+ """Handle zeroconf messages for adding new devices."""
2937 self .cancel_any_pending_removal (name , "request to add" )
3038 info = self .__add_plug (zc , type_ , name )
3139 if info :
3240 asyncio .run_coroutine_threadsafe (
33- self ._async_service_add (self ._plugs [name ]),
34- self ._hass .loop
41+ self ._async_service_add (self ._plugs [name ]), self ._hass .loop
3542 )
3643
3744 async def _async_service_add (self , * args ):
45+ """Send add signal to dispatcher."""
3846 self .dispatch (ZEROCONF_ADD_PLUG_SIGNAL , * args )
3947
40-
4148 async def _async_delayed_remove (self , name ):
4249 """Actually process the removal after delay."""
4350 try :
4451 await asyncio .sleep (self ._debounce_seconds )
45- _LOGGER .info (f"Request to remove service { name } still pending after timeout. Processing remove request..." )
52+ _LOGGER .info (
53+ "Request to remove service %s still pending after timeout. Processing remove request... " ,
54+ name ,
55+ )
4656 if name in self ._plugs :
4757 data = self ._plugs [name ].copy ()
4858 del self ._plugs [name ]
4959 else :
5060 data = None
5161 asyncio .run_coroutine_threadsafe (
52- self ._async_service_remove (name , data ),
53- self ._hass .loop
62+ self ._async_service_remove (name , data ), self ._hass .loop
5463 )
5564 except asyncio .CancelledError :
5665 # Task was cancelled because service came back
57- _LOGGER .info (f"Request to remove service { name } was canceled by request to update or add plug." )
66+ _LOGGER .info (
67+ "Request to remove service %s was canceled by request to update or add plug. " ,
68+ name ,
69+ )
5870 raise
5971 finally :
6072 # Either way were done with this task
6173 self ._pending_removals .pop (name , None )
6274
63-
6475 def remove_service (self , zc , type_ , name ):
76+ """Handle zeroconf messages for removal of devices."""
6577 if name in self ._pending_removals :
6678 # removal for this service is already pending
6779 return
6880
69- _LOGGER .info (f "Scheduling removal for { name } " )
81+ _LOGGER .info ("Scheduling removal for %s" , name )
7082 self ._pending_removals [name ] = asyncio .run_coroutine_threadsafe (
71- self ._async_delayed_remove (name ),
72- self ._hass .loop
83+ self ._async_delayed_remove (name ), self ._hass .loop
7384 )
7485
7586 async def _async_service_remove (self , * args ):
76- self .dispatch ( ZEROCONF_REMOVE_PLUG_SIGNAL , * args )
87+ """Send remove signal to dispatcher."""
88+ self .dispatch (ZEROCONF_REMOVE_PLUG_SIGNAL , * args )
7789
7890 def update_service (self , zc , type_ , name ):
91+ """Handle zeroconf messages for updating device info."""
7992 self .cancel_any_pending_removal (name , "request to update" )
8093 info = self .__add_plug (zc , type_ , name )
8194 if info :
8295 asyncio .run_coroutine_threadsafe (
83- self ._async_service_update ( self ._plugs [name ]),
84- self ._hass .loop
96+ self ._async_service_update (self ._plugs [name ]), self ._hass .loop
8597 )
8698
8799 async def _async_service_update (self , * args ):
100+ """Send update signal to dispatcher."""
88101 # remove from pending tasks if update received
89- self .dispatch ( ZEROCONF_UPDATE_PLUG_SIGNAL , * args )
102+ self .dispatch (ZEROCONF_UPDATE_PLUG_SIGNAL , * args )
90103
91104 async def _async_get_service_info (self , zc , type_ , name ):
92105 try :
93106 info = await zc .async_get_service_info (type_ , name , timeout = 3000 )
94107 self ._discoveries [name ] = info
95- except Exception as e :
96- _LOGGER .error (f"Error retrieving info for { name } " )
97-
108+ except (
109+ TimeoutError ,
110+ OSError ,
111+ BadTypeInNameException ,
112+ NotImplementedError ,
113+ ) as err : # expected possible exceptions
114+ _LOGGER .error ("Error retrieving info for %s: %s" , name , err )
98115
99116 def __add_plug (self , zc , type_ , name ):
100117 info = zc .get_service_info (type_ , name )
101118
102119 if info :
103- self ._plugs [name ] = {'type' : type_ ,
104- 'name' : name ,
105- 'addresses' : ['.' .join (str (b ) for b in addr ) for addr in info .addresses ],
106- 'port' : info .port ,
107- 'server' : info .server ,
108- 'properties' : info .properties
109- }
120+ self ._plugs [name ] = {
121+ "type" : type_ ,
122+ "name" : name ,
123+ "addresses" : [
124+ "." .join (str (b ) for b in addr ) for addr in info .addresses
125+ ],
126+ "port" : info .port ,
127+ "server" : info .server ,
128+ "properties" : info .properties ,
129+ }
110130 return info
111131
112132 def cancel_any_pending_removal (self , name , source ):
133+ """Cancel pending removal and don't send to dispatcher."""
113134 task = self ._pending_removals .pop (name , None )
114135 if task :
115136 task .cancel ()
116- _LOGGER .info (f "Cancelled pending removal for { name } by { source } ." )
137+ _LOGGER .info ("Cancelled pending removal for %s by %s. " , name , source )
117138
118139 @callback
119140 @bind_hass
120- def dispatch (self ,signal_name , * args ):
141+ def dispatch (self , signal_name , * args ):
142+ """Send signal to dispatcher."""
121143 async_dispatcher_send (self ._hass , signal_name , * args )
122144
145+
123146class PowersensorDiscoveryService :
124- def __init__ (self , hass : HomeAssistant , service_type : str = "_powersensor._tcp.local." ):
147+ """A zeroconf service that handles the discovery of plugs."""
148+
149+ def __init__ (
150+ self , hass : HomeAssistant , service_type : str = "_powersensor._tcp.local."
151+ ) -> None :
152+ """Constructor for zeroconf service that handles the discovery of plugs."""
125153 self ._hass = hass
126154 self .service_type = service_type
127155
128- self .zc : Optional [ Zeroconf ] = None
129- self .listener : Optional [ PowersensorServiceListener ] = None
130- self .browser : Optional [ ServiceBrowser ] = None
156+ self .zc : Zeroconf | None = None
157+ self .listener : PowersensorServiceListener | None = None
158+ self .browser : ServiceBrowser | None = None
131159 self .running = False
132- self ._task : Optional [ asyncio .Task ] = None
160+ self ._task : asyncio .Task | None = None
133161
134162 async def start (self ):
135- """Start the mDNS discovery service"""
163+ """Start the mDNS discovery service. """
136164 if self .running :
137165 return
138166
@@ -147,23 +175,19 @@ async def start(self):
147175 self ._task = asyncio .create_task (self ._run ())
148176
149177 async def _run (self ):
150- """Background task that keeps the service alive"""
151- try :
178+ """Background task that keeps the service alive. """
179+ with suppress ( asyncio . CancelledError ) :
152180 while self .running :
153181 await asyncio .sleep (1 )
154- except asyncio .CancelledError :
155- pass
156182
157183 async def stop (self ):
158- """Stop the mDNS discovery service"""
184+ """Stop the mDNS discovery service. """
159185 self .running = False
160186
161187 if self ._task :
162188 self ._task .cancel ()
163- try :
189+ with suppress ( asyncio . CancelledError ) :
164190 await self ._task
165- except asyncio .CancelledError :
166- pass
167191
168192 if self .zc :
169193 # self.zc.close()
0 commit comments