Skip to content

Commit ed1d357

Browse files
committed
Refactor bulb discovery
* Allow for all kinds of missing connection and os errors
1 parent 2956e12 commit ed1d357

File tree

3 files changed

+41
-44
lines changed

3 files changed

+41
-44
lines changed

README.rst

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ Example:
3030
@asyncio.coroutine
3131
def turn_all_lights_on(bulbs):
3232
while True:
33-
print(bulbs)
3433
for b in bulbs.values():
3534
asyncio.Task(b.send_command("set_power",
3635
["off", "sudden", 40]))
@@ -39,12 +38,12 @@ Example:
3938
4039
def main():
4140
loop = asyncio.get_event_loop()
42-
with search_bulbs() as bulbs:
43-
loop.create_task(turn_all_lights_on(bulbs))
44-
try:
45-
loop.run_forever()
46-
except KeyboardInterrupt:
47-
loop.stop()
41+
bulbs = loop.run_until_complete(search_bulbs())
42+
loop.create_task(turn_all_lights_on(bulbs))
43+
try:
44+
loop.run_forever()
45+
except KeyboardInterrupt:
46+
loop.stop()
4847
4948
5049
if __name__ == '__main__':

tests/test_discover.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ def test_wrong_location(self):
3838

3939

4040
def test_search_bulbs():
41+
asyncio.Task(search_bulbs())
4142
loop = asyncio.get_event_loop()
42-
with search_bulbs():
43-
loop.run_until_complete(asyncio.sleep(1))
43+
loop.run_until_complete(asyncio.sleep(1))

yeelib/discover.py

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@
66
import socket
77
import struct
88
import time
9-
from contextlib import contextmanager
109

1110
from ssdp import SSDPRequest, SimpleServiceDiscoveryProtocol
1211

13-
from .exceptions import YeelightError
1412
from .devices import Bulb
13+
from .exceptions import YeelightError
1514

1615
__all__ = ('search_bulbs', 'YeelightProtocol', 'bulbs')
1716

1817
logger = logging.getLogger('yeelib')
1918

20-
2119
bulbs = {}
2220

2321
MCAST_PORT = 1982
@@ -31,16 +29,34 @@ async def send_search_broadcast(transport, search_interval=30):
3129
('ST', 'wifi_bulb'),
3230
])
3331
while True:
34-
request.sendto(transport, MCAST_ADDR)
32+
try:
33+
request.sendto(transport, MCAST_ADDR)
34+
except OSError:
35+
logger.exception("Connection error")
3536
await asyncio.sleep(search_interval)
3637

3738

3839
class YeelightProtocol(SimpleServiceDiscoveryProtocol):
3940
excluded_headers = ['DATE', 'EXT', 'SERVER', 'CACHE-CONTROL', 'LOCATION']
4041
location_patter = r'yeelight://(?P<ip>\d{1,3}(\.\d{1,3}){3}):(?P<port>\d+)'
4142

42-
def __init__(self, bulb_class=Bulb):
43+
def __init__(self, bulb_class=Bulb, loop=None):
4344
self.bulb_class = bulb_class
45+
self.loop = loop or asyncio.get_event_loop()
46+
47+
def connection_made(self, transport):
48+
ucast_socket = transport.get_extra_info('socket')
49+
try:
50+
ucast_socket.bind(('', MCAST_PORT))
51+
fcntl.fcntl(ucast_socket, fcntl.F_SETFL, os.O_NONBLOCK)
52+
group = socket.inet_aton(
53+
SimpleServiceDiscoveryProtocol.MULTICAST_ADDRESS)
54+
mreq = struct.pack("4sl", group, socket.INADDR_ANY)
55+
ucast_socket.setsockopt(
56+
socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
57+
except socket.error as e:
58+
ucast_socket.close()
59+
self.connection_lost(exc=e)
4460

4561
@classmethod
4662
def header_to_kwargs(cls, headers):
@@ -79,39 +95,21 @@ def register_bulb(self, **kwargs):
7995
else:
8096
bulbs[idx].last_seen = time.time()
8197

98+
def connection_lost(self, exc):
99+
logger.exception("connection error")
82100

83-
def open_unicast_socket():
84-
ucast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
85-
ucast_socket.bind(('', MCAST_PORT))
86-
fcntl.fcntl(ucast_socket, fcntl.F_SETFL, os.O_NONBLOCK)
87-
group = socket.inet_aton(
88-
SimpleServiceDiscoveryProtocol.MULTICAST_ADDRESS)
89-
mreq = struct.pack("4sl", group, socket.INADDR_ANY)
90-
ucast_socket.setsockopt(
91-
socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
92-
return ucast_socket
101+
async def _restart():
102+
await asyncio.sleep(10)
103+
await search_bulbs(self.bulb_class, self.loop)
93104

105+
asyncio.Task(_restart())
94106

95-
@contextmanager
96-
def search_bulbs(bulb_class=Bulb, loop=None):
107+
108+
async def search_bulbs(bulb_class=Bulb, loop=None):
97109
if loop is None:
98110
loop = asyncio.get_event_loop()
99-
multicast_connection = loop.create_datagram_endpoint(
100-
lambda: YeelightProtocol(bulb_class),
101-
family=socket.AF_INET)
102-
mcast_transport, _ = loop.run_until_complete(multicast_connection)
103-
loop.create_task(send_search_broadcast(mcast_transport))
104-
105-
ucast_socket = open_unicast_socket()
106111
unicast_connection = loop.create_datagram_endpoint(
107-
lambda: YeelightProtocol(bulb_class),
108-
sock=ucast_socket)
109-
ucast_transport, _ = loop.run_until_complete(unicast_connection)
110-
try:
111-
yield bulbs
112-
except BaseException:
113-
pass
114-
finally:
115-
ucast_transport.close()
116-
mcast_transport.close()
117-
ucast_socket.close()
112+
lambda: YeelightProtocol(bulb_class), family=socket.AF_INET)
113+
ucast_transport, _ = await unicast_connection
114+
loop.create_task(send_search_broadcast(ucast_transport))
115+
return bulbs

0 commit comments

Comments
 (0)