1+ import asyncio
2+ from typing import Optional
3+
4+ from homeassistant .core import HomeAssistant
5+ from zeroconf import ServiceBrowser , ServiceListener , Zeroconf
6+ from homeassistant .helpers .dispatcher import async_dispatcher_send
7+ import homeassistant .components .zeroconf
8+
9+ from .const import DOMAIN
10+
11+ import logging
12+ _LOGGER = logging .getLogger (__name__ )
13+
14+
15+ class PowersensorServiceListener (ServiceListener ):
16+ def __init__ (self , hass : HomeAssistant , debounce_timeout : float = 60 ):
17+ self ._hass = hass
18+ self ._plugs = {}
19+ self ._discoveries = {}
20+ self ._pending_removals = {}
21+ self ._debounce_seconds = debounce_timeout
22+
23+ def add_service (self , zc , type_ , name ):
24+ self .cancel_any_pending_removal (name , "request to add" )
25+ info = self .__add_plug (zc , type_ , name )
26+ if info :
27+ asyncio .run_coroutine_threadsafe (
28+ self ._async_service_add (self ._plugs [name ]),
29+ self ._hass .loop
30+ )
31+
32+ async def _async_service_add (self , * args ):
33+ async_dispatcher_send (self ._hass , f"{ DOMAIN } _zeroconf_add_plug" , * args )
34+
35+
36+ async def _async_delayed_remove (self , name ):
37+ """Actually process the removal after delay."""
38+ try :
39+ await asyncio .sleep (self ._debounce_seconds )
40+ _LOGGER .info (f"Request to remove service { name } still pending after timeout. Processing remove request..." )
41+ if name in self ._plugs :
42+ data = self ._plugs [name ].copy ()
43+ del self ._plugs [name ]
44+ else :
45+ data = None
46+ asyncio .run_coroutine_threadsafe (
47+ self ._async_service_remove (name , data ),
48+ self ._hass .loop
49+ )
50+ except asyncio .CancelledError :
51+ # Task was cancelled because service came back
52+ _LOGGER .info (f"Request to remove service { name } was canceled by request to update or add plug." )
53+ raise
54+ finally :
55+ # Either way were done with this task
56+ self ._pending_removals .pop (name , None )
57+
58+
59+ def remove_service (self , zc , type_ , name ):
60+ if name in self ._pending_removals :
61+ # removal for this service is already pending
62+ return
63+
64+ _LOGGER .info (f"Scheduling removal for { name } " )
65+ self ._pending_removals [name ] = asyncio .run_coroutine_threadsafe (
66+ self ._async_delayed_remove (name ),
67+ self ._hass .loop
68+ )
69+
70+ async def _async_service_remove (self , * args ):
71+ async_dispatcher_send (self ._hass , f"{ DOMAIN } _zeroconf_remove_plug" , * args )
72+
73+ def update_service (self , zc , type_ , name ):
74+ self .cancel_any_pending_removal (name , "request to update" )
75+ info = self .__add_plug (zc , type_ , name )
76+ if info :
77+ asyncio .run_coroutine_threadsafe (
78+ self ._async_service_update ( self ._plugs [name ]),
79+ self ._hass .loop
80+ )
81+
82+ async def _async_service_update (self , * args ):
83+ # remove from pending tasks if update received
84+ async_dispatcher_send (self ._hass , f"{ DOMAIN } _zeroconf_update_plug" , * args )
85+
86+ async def _async_get_service_info (self , zc , type_ , name ):
87+ try :
88+ info = await zc .async_get_service_info (type_ , name , timeout = 3000 )
89+ self ._discoveries [name ] = info
90+ except Exception as e :
91+ _LOGGER .error (f"Error retrieving info for { name } " )
92+
93+
94+ def __add_plug (self , zc , type_ , name ):
95+ info = zc .get_service_info (type_ , name )
96+
97+ if info :
98+ self ._plugs [name ] = {'type' : type_ ,
99+ 'name' : name ,
100+ 'addresses' : ['.' .join (str (b ) for b in addr ) for addr in info .addresses ],
101+ 'port' : info .port ,
102+ 'server' : info .server ,
103+ 'properties' : info .properties
104+ }
105+ return info
106+
107+ def cancel_any_pending_removal (self , name , source ):
108+ task = self ._pending_removals .pop (name , None )
109+ if task :
110+ task .cancel ()
111+ _LOGGER .info (f"Cancelled pending removal for { name } by { source } ." )
112+
113+ class PowersensorDiscoveryService :
114+ def __init__ (self , hass : HomeAssistant , service_type : str = "_powersensor._tcp.local." ):
115+ self ._hass = hass
116+ self .service_type = service_type
117+
118+ self .zc : Optional [Zeroconf ] = None
119+ self .listener : Optional [PowersensorServiceListener ] = None
120+ self .browser : Optional [ServiceBrowser ] = None
121+ self .running = False
122+ self ._task : Optional [asyncio .Task ] = None
123+
124+ async def start (self ):
125+ """Start the mDNS discovery service"""
126+ if self .running :
127+ return
128+
129+ self .running = True
130+ self .zc = await homeassistant .components .zeroconf .async_get_instance (self ._hass )
131+ self .listener = PowersensorServiceListener (self ._hass )
132+
133+ # Create browser
134+ self .browser = ServiceBrowser (self .zc , self .service_type , self .listener )
135+
136+ # Start the background task
137+ self ._task = asyncio .create_task (self ._run ())
138+
139+ async def _run (self ):
140+ """Background task that keeps the service alive"""
141+ try :
142+ while self .running :
143+ await asyncio .sleep (1 )
144+ except asyncio .CancelledError :
145+ pass
146+
147+ async def stop (self ):
148+ """Stop the mDNS discovery service"""
149+ self .running = False
150+
151+ if self ._task :
152+ self ._task .cancel ()
153+ try :
154+ await self ._task
155+ except asyncio .CancelledError :
156+ pass
157+
158+ if self .zc :
159+ # self.zc.close()
160+ self .zc = None
161+
162+ self .browser = None
163+ self .listener = None
0 commit comments