11import asyncio
2- import email .parser
3- import errno
42import fcntl
53import logging
64import os
119from contextlib import contextmanager
1210
1311from yeelib .bulbs import Bulb
14-
12+ from yeelib . upnp import SSDPRequest , SimpleServiceDiscoveryProtocol
1513from .exceptions import YeelightError
1614
17- __all__ = ('search_bulbs' ,)
15+ __all__ = ('search_bulbs' , 'YeelightProtocol' )
1816
1917logger = logging .getLogger ('yeelib' )
2018
21- MCAST_IP = '239.255.255.250'
2219MCAST_PORT = 1982
23- MCAST_ADDR = MCAST_IP , MCAST_PORT
24-
25-
26- class MCAST_MSG_TYPES :
27- SEARCH = 'M-SEARCH'
28- NOTIFY = 'NOTIFY'
20+ MCAST_ADDR = SimpleServiceDiscoveryProtocol .MULTICAST_ADDRESS , MCAST_PORT
2921
3022
3123class MutableBoolean :
@@ -40,47 +32,35 @@ def set(self, value):
4032
4133@asyncio .coroutine
4234def send_search_broadcast (transport , search_interval = 30 , _running = True ):
35+ request = SSDPRequest ('M-SEARCH' , headers = [
36+ ('HOST' , '%s:%s' % MCAST_ADDR ),
37+ ('MAN' , '"ssdp:discover"' ),
38+ ('ST' , 'wifi_bulb' ),
39+ ])
4340 while _running :
44- lines = ['%s * HTTP/1.1' % MCAST_MSG_TYPES .SEARCH ]
45- lines += [
46- 'HOST: %s:%s' % MCAST_ADDR ,
47- 'MAN: "ssdp:discover"' ,
48- 'ST: wifi_bulb' ,
49- ]
50- msg = '\r \n ' .join (lines )
51- logger .debug (">>> %s" , msg )
52- transport .sendto (msg .encode (), MCAST_ADDR )
41+ request .sendto (transport , MCAST_ADDR )
5342 yield from asyncio .sleep (search_interval )
5443
5544
56- class YeelightProtocol (asyncio . DatagramProtocol ):
45+ class YeelightProtocol (SimpleServiceDiscoveryProtocol ):
5746 excluded_headers = ['DATE' , 'EXT' , 'SERVER' , 'CACHE-CONTROL' , 'LOCATION' ]
5847 location_patter = r'yeelight://(?P<ip>\d{1,3}(\.\d{1,3}){3}):(?P<port>\d+)'
5948
6049 def __init__ (self , bulbs , bulb_class = Bulb ):
6150 self .bulbs = bulbs
6251 self .bulb_class = bulb_class
6352
64- def datagram_received (self , data , addr ):
65- msg = data .decode ()
66- logger .debug ("%s:%s> %s" , addr + (msg ,))
67-
68- lines = msg .splitlines ()
69- type , addr , status = lines [0 ].split ()
70- if type == MCAST_MSG_TYPES .SEARCH :
71- return
72-
73- data = '\n ' .join (lines [1 :])
74- headers = email .parser .Parser ().parsestr (data )
75-
53+ @classmethod
54+ def header_to_kwargs (cls , headers ):
55+ headers = dict (headers )
7656 location = headers .get ('Location' )
7757 cache_control = headers .get ('Cache-Control' , 'max-age=3600' )
7858 headers = {
79- k : v for k , v in headers ._headers
80- if k .upper () not in self .excluded_headers
59+ k : v for k , v in headers .items ()
60+ if k .upper () not in cls .excluded_headers
8161 }
8262
83- match = re .match (self .location_patter , location )
63+ match = re .match (cls .location_patter , location )
8464 if match is None :
8565 raise YeelightError ('Location does not match: %s' % location )
8666 ip = match .groupdict ()['ip' ]
@@ -90,31 +70,31 @@ def datagram_received(self, data, addr):
9070
9171 kwargs = dict (ip = ip , port = port , status_refresh_interv = max_age )
9272 kwargs .update (headers )
73+ return kwargs
9374
75+ def request_received (self , request ):
76+ if request .method == 'M-SEARCH' :
77+ return
78+ self .register_bulb (** self .header_to_kwargs (request .headers ))
79+
80+ def response_received (self , response ):
81+ self .register_bulb (** self .header_to_kwargs (response .headers ))
82+
83+ def register_bulb (self , ** kwargs ):
9484 idx = kwargs ['id' ]
9585 if idx not in self .bulbs :
9686 self .bulbs [idx ] = self .bulb_class (** kwargs )
9787 else :
9888 self .bulbs [idx ].last_seen = time .time ()
9989
100- def error_received (self , exc ):
101- if exc == errno .EAGAIN or exc == errno .EWOULDBLOCK :
102- logger .error ('Error received: %s' , exc )
103- else :
104- raise YeelightError ("Unexpected connection error" ) from exc
105-
106- def connection_lost (self , exc ):
107- logger .error ("Socket closed, stop the event loop" )
108- loop = asyncio .get_event_loop ()
109- loop .stop ()
110-
11190
11291@contextmanager
11392def _unicast_socket ():
11493 with socket .socket (socket .AF_INET , socket .SOCK_DGRAM ) as ucast_socket :
11594 ucast_socket .bind (('' , MCAST_PORT ))
11695 fcntl .fcntl (ucast_socket , fcntl .F_SETFL , os .O_NONBLOCK )
117- group = socket .inet_aton (MCAST_IP )
96+ group = socket .inet_aton (
97+ SimpleServiceDiscoveryProtocol .MULTICAST_ADDRESS )
11898 mreq = struct .pack ("4sl" , group , socket .INADDR_ANY )
11999 ucast_socket .setsockopt (
120100 socket .IPPROTO_IP , socket .IP_ADD_MEMBERSHIP , mreq )
0 commit comments