33import binascii
44import logging
55import socket
6+ import threading
67import time
78
89_LOGGER = logging .getLogger (__name__ )
1516TIMEOUT = 1.0
1617DISCOVERY_TIMEOUT = 1.0
1718
19+ # Timeout after which to renew device subscriptions
20+ SUBSCRIPTION_TIMEOUT = 60
21+
1822# Packet constants.
1923MAGIC = b'\x68 \x64 '
2024DISCOVERY = b'\x00 \x06 \x71 \x61 '
2832ON = b'\x01 '
2933OFF = b'\x00 '
3034
31- # Timeout after which to renew device subscriptions
32- SUBSCRIPTION_TIMEOUT = 60
35+ # Socket
36+ _SOCKET = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
37+
38+ # Buffer
39+ _BUFFER = {}
40+
41+
42+ def _listen ():
43+ """ Listen on socket. """
44+ while True :
45+ data , addr = _SOCKET .recvfrom (1024 )
46+ _BUFFER [addr [0 ]] = data
47+
48+
49+ def _setup ():
50+ """ Set up module.
51+
52+ Open a UDP socket, and listen in a thread.
53+ """
54+ for opt in [socket .SO_BROADCAST , socket .SO_REUSEADDR , socket .SO_REUSEPORT ]:
55+ _SOCKET .setsockopt (socket .SOL_SOCKET , opt , 1 )
56+ _SOCKET .bind (('' , PORT ))
57+ udp = threading .Thread (target = _listen , daemon = True )
58+ udp .start ()
59+
60+
61+ def discover (timeout = DISCOVERY_TIMEOUT ):
62+ """ Discover devices on the local network.
63+
64+ :param timeout: Optional timeout in seconds.
65+ :returns: Set of discovered host addresses.
66+ """
67+ hosts = set ()
68+ payload = MAGIC + DISCOVERY
69+ for _ in range (RETRIES ):
70+ _SOCKET .sendto (bytearray (payload ), ('255.255.255.255' , PORT ))
71+ start = time .time ()
72+ while time .time () < start + timeout :
73+ for host , data in _BUFFER .copy ().items ():
74+ if _is_discovery_response (data ):
75+ if host not in hosts :
76+ _LOGGER .debug ("Discovered device at %s" , host )
77+ hosts .add (host )
78+ return hosts
3379
3480
3581def _is_discovery_response (data ):
@@ -74,13 +120,7 @@ def __init__(self, host):
74120 :param host: IP or hostname of device.
75121 """
76122 self .host = host
77- self ._socket = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
78- for opt in [socket .SO_BROADCAST , socket .SO_REUSEADDR ,
79- socket .SO_REUSEPORT ]:
80- self ._socket .setsockopt (socket .SOL_SOCKET , opt , 1 )
81- self ._socket .bind (('' , PORT ))
82123 (self ._mac , self ._mac_reversed ) = self ._discover_mac ()
83-
84124 self ._subscribe ()
85125
86126 @property
@@ -144,6 +184,10 @@ def _subscribe(self):
144184 "No status could be found for {}" .format (self .host ))
145185
146186 def _subscription_is_recent (self ):
187+ """ Check if subscription occurred recently.
188+
189+ :returns: Yes (True) or no (False)
190+ """
147191 return self .last_subscribed > time .time () - SUBSCRIPTION_TIMEOUT
148192
149193 def _control (self , state ):
@@ -219,24 +263,22 @@ def _udp_transact(self, payload, handler, *args,
219263 :param broadcast: Send a broadcast instead.
220264 :param timeout: Timeout in seconds.
221265 """
266+ if self .host in _BUFFER :
267+ del _BUFFER [self .host ]
222268 host = self .host
223269 if broadcast :
224270 host = '255.255.255.255'
225271 retval = None
226- self ._socket .settimeout (timeout )
227272 for _ in range (RETRIES ):
228- self ._socket .sendto (bytearray (payload ), (host , PORT ))
229- while True :
230- try :
231- data , addr = self ._socket .recvfrom (1024 )
232- # From the right device?
233- if addr [0 ] == self .host :
234- retval = handler (data , * args )
235- # Return as soon as a response is received
236- if retval :
237- return retval
238- except socket .timeout :
239- break
273+ _SOCKET .sendto (bytearray (payload ), (host , PORT ))
274+ start = time .time ()
275+ while time .time () < start + timeout :
276+ data = _BUFFER .get (self .host , None )
277+ if data :
278+ retval = handler (data , * args )
279+ # Return as soon as a response is received
280+ if retval :
281+ return retval
240282
241283 def _turn_on (self ):
242284 """ Turn on the device. """
@@ -245,3 +287,6 @@ def _turn_on(self):
245287 def _turn_off (self ):
246288 """ Turn off the device. """
247289 self ._control (OFF )
290+
291+
292+ _setup ()
0 commit comments