class SSDPServer(object): """ Implementation of a SSDP server. The notify_received and search_received methods are called when the appropriate type of datagram is received by the server. """ msg_already_started = 'tried to start() SSDPServer when already started' msg_already_stopped = 'tried to stop() SSDPServer when already stopped' def __init__(self, server_name, xml_description_filename, max_age=1800, receive_notify=True, http_version="1.1", search_type="sspd:all", additional_headers={}): """ Constructor for the SSDPServer class. @param server_name: server name @param xml_description_filename: XML description filename @param max_age: max age parameter, default 1800. @param receive_notify: if False, ignores notify messages @type server_name: string @type xml_description_filename: @type max_age: integer @type receive_notify: boolean """ self.server_name = server_name self.xml_description_filename = xml_description_filename self.max_age = max_age self.receive_notify = receive_notify self.running = False self.http_version = http_version self.known_device = {} self.advertised = {} self._callbacks = {} self.additional_headers = additional_headers self.search_type = search_type self.udp_transport = UDPTransport() self.udp_listener = UDPListener(SSDP_ADDR, SSDP_PORT, data_callback=self._datagram_received) self.renew_loop = LoopingCall(self._renew_notifications) self.renew_loop.start(0.8 * self.max_age, now=True) def is_running(self): """ Returns True if the SSDPServer is running, False otherwise. """ return self.running def start(self): """ Starts the SSDPServer. """ if not self.is_running(): self.udp_listener.start() self.running = True else: log.warning(self.msg_already_started) def stop(self): """ Sends bye bye notifications and stops the SSDPServer. """ if self.is_running(): # Avoid racing conditions own_temp = self.advertised.copy() for usn in own_temp: self._do_byebye(usn) self.renew_loop.stop() self.udp_listener.stop() self.running = False else: log.warning(self.msg_already_stopped) def destroy(self): """ Destroys the SSDPServer. """ if self.is_running(): self.stop() self.renew_loop.destroy() self.udp_listener.destroy() self._cleanup() def clear_device_list(self): """ Clears the device list. """ self.known_device.clear() def discovered_device_failed(self, dev): """ Device could not be fully built, so forget it. """ usn = dev['USN'] if usn in self.known_device: self.known_device.pop(usn) def is_known_device(self, usn): """ Returns if the device with the passed usn is already known. @param usn: device's usn @type usn: string @return: True if it is known @rtype: boolean """ return usn in self.known_device def subscribe(self, name, callback): """ Subscribes a callback for an event. @param name: name of the event. May be "new_device_event" or "removed_device_event" @param callback: callback @type name: string @type callback: callable """ self._callbacks.setdefault(name, []).append(callback) def unsubscribe(self, name, callback): """ Unsubscribes a callback for an event. @param name: name of the event @param callback: callback @type name: string @type callback: callable """ callbacks = self._callbacks.get(name, []) [callbacks.remove(c) for c in callbacks] self._callbacks[name] = callbacks def announce_device(self): """ Announces the device. """ [self._do_notify(usn) for usn in self.advertised] def register_device(self, device): """ Registers a device on the SSDP server. @param device: device to be registered @type device: Device """ self._register_device(device) if device.is_root_device(): [self._register_device(d) for d in device.devices.values()] # Messaging def _datagram_received(self, data, (host, port)): """ Handles a received multicast datagram. @param data: raw data @param host: datagram source host @param port: datagram source port @type data: string @type host: string @type port: integer """ try: header, payload = data.split('\r\n\r\n') except ValueError, err: log.error('Error while receiving datagram packet: %s', str(err)) return
class SSDPServer(object): """ Implementation of a SSDP server. The notify_received and search_received methods are called when the appropriate type of datagram is received by the server. """ msg_already_started = 'tried to start() SSDPServer when already started' msg_already_stopped = 'tried to stop() SSDPServer when already stopped' def __init__(self, server_name, xml_description_filename, max_age=1800, receive_notify=True, udp_listener=''): """ Constructor for the SSDPServer class. @param server_name: server name @param xml_description_filename: XML description filename @param max_age: max age parameter, default 1800. @param receive_notify: if False, ignores notify messages @type server_name: string @type xml_description_filename: @type max_age: integer @type receive_notify: boolean """ self.server_name = server_name self.xml_description_filename = xml_description_filename self.max_age = max_age log.debug("max_age: %s", max_age) self.receive_notify = receive_notify self.running = False self.known_device = {} self.advertised = {} self._callbacks = {} self.udp_transport = UDPTransport() if udp_listener == '': self.udp_listener = UDPListener( SSDP_ADDR, SSDP_PORT, data_callback=self._datagram_received) else: self.udp_listener = None udp_listener.subscribe(self) self.renew_loop = LoopingCall(self._renew_notifications) self.renew_loop.start(0.8 * self.max_age, now=True) def is_running(self): """ Returns True if the SSDPServer is running, False otherwise. """ return self.running def start(self): """ Starts the SSDPServer. """ if not self.is_running(): if self.udp_listener != None: self.udp_listener.start() self.running = True else: log.warning(self.msg_already_started) def stop(self): """ Sends bye bye notifications and stops the SSDPServer. """ if self.is_running(): # Avoid racing conditions own_temp = self.advertised.copy() for usn in own_temp: self._do_byebye(usn) self.renew_loop.stop() if self.udp_listener != None: self.udp_listener.stop() self.running = False else: log.warning(self.msg_already_stopped) def destroy(self): """ Destroys the SSDPServer. """ if self.is_running(): self.stop() self.renew_loop.destroy() if self.udp_listener != None: self.udp_listener.destroy() self._cleanup() def clear_device_list(self): """ Clears the device list. """ self.known_device.clear() def discovered_device_failed(self, dev): """ Device could not be fully built, so forget it. """ usn = dev['USN'] if usn in self.known_device: self.known_device.pop(usn) def is_known_device(self, usn): """ Returns if the device with the passed usn is already known. @param usn: device's usn @type usn: string @return: True if it is known @rtype: boolean """ return usn in self.known_device def subscribe(self, name, callback): """ Subscribes a callback for an event. @param name: name of the event. May be "new_device_event" or "removed_device_event" @param callback: callback @type name: string @type callback: callable """ self._callbacks.setdefault(name, []).append(callback) def unsubscribe(self, name, callback): """ Unsubscribes a callback for an event. @param name: name of the event @param callback: callback @type name: string @type callback: callable """ callbacks = self._callbacks.get(name, []) [callbacks.remove(c) for c in callbacks] self._callbacks[name] = callbacks def announce_device(self): """ Announces the device. """ log.debug("announce_device") [self._do_notify(usn) for usn in self.advertised] def register_device(self, device): """ Registers a device on the SSDP server. @param device: device to be registered @type device: Device """ self._register_device(device) if device.is_root_device(): [self._register_device(d) for d in device.devices.values()] # Messaging def _datagram_received(self, data, (host, port)): """ Handles a received multicast datagram. @param data: raw data @param host: datagram source host @param port: datagram source port @type data: string @type host: string @type port: integer """ log.debug("SSDP._datagram_received host: %s, port: %s\ndata: %s", host, port, data) try: header, payload = data.split('\r\n\r\n') except ValueError, err: log.error('Error while receiving datagram packet: %s', str(err)) return
class MSearch(object): """ Represents a MSearch. Contains some control functions for starting and stopping the search. While running, search will be repeated in regular intervals specified at construction or passed to the start() method. """ msg_already_started = 'tried to start() MSearch when already started' msg_already_stopped = 'tried to stop() MSearch when already stopped' def __init__(self, ssdp, start=True, interval=DEFAULT_SEARCH_TIME, ssdp_addr='239.255.255.250', ssdp_port=1900): """ Constructor for the MSearch class. @param ssdp: ssdp server instance that will receive new device events and subscriptions @param start: if True starts the search when constructed @param interval: interval between searchs @param ssdp_addr: ssdp address for listening (UDP) @param ssdp_port: ssdp port for listening (UDP) @type ssdp: SSDPServer @type start: boolean @type interval: float @type ssdp_addr: string @type ssdp_port integer """ self.ssdp = ssdp self.ssdp_addr = ssdp_addr self.ssdp_port = ssdp_port self.udp_transport = UDPTransport() self.listen_udp = UDPListener(ssdp_addr, data_callback=self._datagram_received, shared_socket=self.udp_transport.socket) self.loopcall = LoopingCall(self.double_discover) if start: self.start(interval) def is_running(self): """ Returns True if the search is running (it's being repeated in the interval given). @rtype: boolean """ return self.loopcall.is_running() def start(self, interval=DEFAULT_SEARCH_TIME, search_type=DEFAULT_SEARCH_TYPE): """ Starts the search. @param interval: interval between searchs. Default is 600.0 seconds @param search_type: type of the search, default is "ssdp:all" @type interval: float @type search_type: string """ if not self.is_running(): self.listen_udp.start() self.loopcall._args = (search_type, ) self.loopcall.start(interval, now=True) log.debug('MSearch started') else: log.warning(self.msg_already_started) def stop(self): """ Stops the search. """ if self.is_running(): log.debug('MSearch stopped') self.listen_udp.stop() self.loopcall.stop() else: log.warning(self.msg_already_stopped) def destroy(self): """ Destroys and quits MSearch. """ if self.is_running(): self.stop() self.listen_udp.destroy() self.loopcall.destroy() self._cleanup() def double_discover(self, search_type=DEFAULT_SEARCH_TYPE): """ Sends a MSearch imediatelly. Each call to this method will yield a MSearch message, that is, it won't repeat automatically. """ log.info("Doing double discover for %s", search_type) self.discover(search_type) self.discover(search_type) def discover(self, type="ssdp:all"): """ Mounts and sends the discover message (MSearch). @param type: search type @type type: string """ req = ['M-SEARCH * HTTP/1.1', 'HOST: %s:%d' % (self.ssdp_addr, self.ssdp_port), 'MAN: "ssdp:discover"', 'MX: 5', 'ST: ' + type, '', ''] req = '\r\n'.join(req) self.udp_transport.send_data(req, self.ssdp_addr, self.ssdp_port) def _datagram_received(self, data, (host, port)): """ Callback for the UDPListener when messages arrive. @param data: raw data received @param host: host where data came from @param port: port where data came from @type data: string @type host: string @type port: integer """ cmd, headers = parse_http_response(data) if cmd[0] == 'HTTP/1.1' and cmd[1] == '200': if self.ssdp != None: if not self.ssdp.is_known_device(headers['usn']): log.debug('Received MSearch answer %s,%s from %s:%s', headers['usn'], headers['st'], host, port) self.ssdp._register(headers['usn'], headers['st'], headers['location'], headers['server'], headers['cache-control'])
class MSearch(object): """ Represents a MSearch. Contains some control functions for starting and stopping the search. While running, search will be repeated in regular intervals specified at construction or passed to the start() method. """ msg_already_started = 'tried to start() MSearch when already started' msg_already_stopped = 'tried to stop() MSearch when already stopped' def __init__(self, ssdp, start=True, interval=DEFAULT_SEARCH_TIME, ssdp_addr=DEFAULT_SSDP_ADDR, ssdp_port=1900): """ Constructor for the MSearch class. @param ssdp: ssdp server instance that will receive new device events and subscriptions @param start: if True starts the search when constructed @param interval: interval between searchs @param ssdp_addr: ssdp address for listening (UDP) @param ssdp_port: ssdp port for listening (UDP) @type ssdp: SSDPServer @type start: boolean @type interval: float @type ssdp_addr: string @type ssdp_port integer """ self.ssdp = ssdp self.ssdp_addr = ssdp_addr self.ssdp_port = ssdp_port self.udp_transport = UDPTransport() self.listen_udp = UDPListener(ssdp_addr, data_callback=self._datagram_received, shared_socket=self.udp_transport.socket) self.loopcall = LoopingCall(self.double_discover) if start: self.start(interval) def is_running(self): """ Returns True if the search is running (it's being repeated in the interval given). @rtype: boolean """ return self.loopcall.is_running() def start(self, interval=DEFAULT_SEARCH_TIME, search_type=DEFAULT_SEARCH_TYPE, http_version="1.1", man='"ssdp:discover"', mx=1, additionals={}): """ Starts the search. @param interval: interval between searchs. Default is 600.0 seconds @param search_type: type of the search, default is "ssdp:all" @param http_version: http version for m-search (default is 1.1) @param man: man field for m-search (default is ssdp:discover) @param mx: mx field for m-search (default is 1) @param additionals: dict containing additional field to be appended in the end of the m-search message (default is a empty dictionary) @type interval: float @type search_type: string @type http_version: string @type man: string @type mx: int @type additionals: dict """ if not self.is_running(): self.ssdp.search_type = search_type self.listen_udp.start() self.loopcall._args = ( search_type, http_version, man, mx, additionals, ) self.loopcall.start(interval, now=True) log.debug('MSearch started') else: log.warning(self.msg_already_started) def stop(self): """ Stops the search. """ if self.is_running(): log.debug('MSearch stopped') self.listen_udp.stop() self.loopcall.stop() else: log.warning(self.msg_already_stopped) def destroy(self): """ Destroys and quits MSearch. """ if self.is_running(): self.stop() self.listen_udp.destroy() self.loopcall.destroy() self._cleanup() def double_discover(self, search_type=DEFAULT_SEARCH_TYPE, http_version="1.1", man='"ssdp:discover"', mx=1, additionals={}): """ Sends a MSearch imediatelly. Each call to this method will yield a MSearch message, that is, it won't repeat automatically. """ log.info("Doing double discover for %s, HTTP_VERSION=%s, MAN=%s, \ MX=%d, additionals=%s" % (search_type, http_version, man, mx, additionals)) self.discover(search_type, http_version, man, mx, additionals) def discover(self, search_type=DEFAULT_SEARCH_TYPE, http_version="1.1", man='"ssdp:discover"', mx=1, additionals={}): """ Builds and sends the discover message (MSearch). @param type: search type @type type: string """ if (mx > 120): mx = 120 elif (mx < 1): mx = 1 req = [ 'M-SEARCH * HTTP/%s' % http_version, 'HOST: %s:%d' % (self.ssdp_addr, self.ssdp_port), 'MAN: %s' % man, 'MX: %s' % mx, 'ST: %s' % search_type ] append = req.append [append('%s: %s' % (k, v)) for k, v in additionals.items()] append('') append('') req = '\r\n'.join(req) self.udp_transport.send_data(req, self.ssdp_addr, self.ssdp_port) def _datagram_received(self, data, (host, port)): """ Callback for the UDPListener when messages arrive. @param data: raw data received @param host: host where data came from @param port: port where data came from @type data: string @type host: string @type port: integer """ cmd, headers = parse_http_response(data) if cmd[0].startswith('HTTP/1.') and cmd[1] == '200': if self.ssdp != None: if not self.ssdp.is_known_device(headers['usn']): log.debug('Received MSearch answer %s,%s from %s:%s', headers['usn'], headers['st'], host, port) default_fields_name = [ "usn", "st", "location", "server", "cache-control", "ext" ] default_header = {} for field in default_fields_name: default_header[field] = headers.pop(field, "") self.ssdp.register(default_header['usn'], default_header['st'], default_header['location'], default_header['server'], default_header['cache-control'], "remote", headers)
class MSearch(object): """ Represents a MSearch. Contains some control functions for starting and stopping the search. While running, search will be repeated in regular intervals specified at construction or passed to the start() method. """ msg_already_started = 'tried to start() MSearch when already started' msg_already_stopped = 'tried to stop() MSearch when already stopped' def __init__(self, ssdp, start=True, interval=DEFAULT_SEARCH_TIME, ssdp_addr='239.255.255.250', ssdp_port=1900): """ Constructor for the MSearch class. @param ssdp: ssdp server instance that will receive new device events and subscriptions @param start: if True starts the search when constructed @param interval: interval between searchs @param ssdp_addr: ssdp address for listening (UDP) @param ssdp_port: ssdp port for listening (UDP) @type ssdp: SSDPServer @type start: boolean @type interval: float @type ssdp_addr: string @type ssdp_port integer """ self.ssdp = ssdp self.ssdp_addr = ssdp_addr self.ssdp_port = ssdp_port self.search_type = DEFAULT_SEARCH_TYPE self.udp_transport = UDPTransport() # self.listen_udp = UDPListener(ssdp_addr, ssdp_port, self.listen_udp = UDPListener(ssdp_addr, 2149, # WMP is not picked up if 1900 is used for source data_callback=self._datagram_received, shared_socket=self.udp_transport.socket) self.loopcall = LoopingCall(self.double_discover) if start: self.start(interval) def is_running(self): """ Returns True if the search is running (it's being repeated in the interval given). @rtype: boolean """ return self.loopcall.is_running() def start(self, interval=DEFAULT_SEARCH_TIME, search_type=DEFAULT_SEARCH_TYPE): """ Starts the search. @param interval: interval between searchs. Default is 600.0 seconds @param search_type: type of the search, default is "ssdp:all" @type interval: float @type search_type: string """ # interval = 30.0 if not self.is_running(): self.search_type = search_type self.listen_udp.start() # print ">>>>>>>>> interval: " + str(interval) self.loopcall.start(interval, now=True) log.debug('MSearch started') else: log.warning(self.msg_already_started) def stop(self): """ Stops the search. """ if self.is_running(): log.debug('MSearch stopped') self.listen_udp.stop() self.loopcall.stop() else: log.warning(self.msg_already_stopped) def destroy(self): """ Destroys and quits MSearch. """ if self.is_running(): self.stop() self.listen_udp.destroy() self.loopcall.destroy() self._cleanup() def double_discover(self, search_type=DEFAULT_SEARCH_TYPE): """ Sends a MSearch imediatelly. Each call to this method will yield a MSearch message, that is, it won't repeat automatically. """ # print "<<<<<<<<< start double discover >>>>>>>>>" self.discover(search_type) self.discover(search_type) # print "<<<<<<<<< end double discover >>>>>>>>>" def discover(self, type="ssdp:all"): # def discover(self, type="upnp:rootdevice"): """ Mounts and sends the discover message (MSearch). @param type: search type @type type: string """ # type = "urn:schemas-upnp-org:device:MediaServer:1" type = "upnp:rootdevice" # req = ['M-SEARCH * HTTP/1.1', # 'HOST: %s:%d' % (self.ssdp_addr, self.ssdp_port), # 'MAN: "ssdp:discover"', # 'MX: 5', # 'ST: ' + type, '', ''] # req = '\r\n'.join(req) req = ['M-SEARCH * HTTP/1.1', 'HOST:%s:%d' % (self.ssdp_addr, self.ssdp_port), 'MAN:"ssdp:discover"', # 'Host:%s:%d' % (self.ssdp_addr, self.ssdp_port), # 'Man:"ssdp:discover"', 'MX:5', 'ST:' + type, '', '', ''] req = '\r\n'.join(req) self.udp_transport.send_data(req, self.ssdp_addr, self.ssdp_port) def _datagram_received(self, data, (host, port)): """ Callback for the UDPListener when messages arrive. @param data: raw data received @param host: host where data came from @param port: port where data came from @type data: string @type host: string @type port: integer """ # print "datagram_received start" cmd, headers = parse_http_response(data) if cmd[0] == 'HTTP/1.1' and cmd[1] == '200': if self.ssdp != None: if not self.ssdp.is_known_device(headers['usn']): log.debug('Received MSearch answer %s,%s from %s:%s', headers['usn'], headers['st'], host, port) # print "_datagram_received _register" # print "_datagram_received headers: " + str(headers) self.ssdp._register(headers['usn'], headers['st'], headers['location'], headers['server'], headers['cache-control'])
class MSearch(object): """ Represents a MSearch. Contains some control functions for starting and stopping the search. While running, search will be repeated in regular intervals specified at construction or passed to the start() method. """ msg_already_started = 'tried to start() MSearch when already started' msg_already_stopped = 'tried to stop() MSearch when already stopped' def __init__(self, ssdp, start=True, interval=DEFAULT_SEARCH_TIME, ssdp_addr=DEFAULT_SSDP_ADDR, ssdp_port=1900): """ Constructor for the MSearch class. @param ssdp: ssdp server instance that will receive new device events and subscriptions @param start: if True starts the search when constructed @param interval: interval between searchs @param ssdp_addr: ssdp address for listening (UDP) @param ssdp_port: ssdp port for listening (UDP) @type ssdp: SSDPServer @type start: boolean @type interval: float @type ssdp_addr: string @type ssdp_port integer """ self.ssdp = ssdp self.ssdp_addr = ssdp_addr self.ssdp_port = ssdp_port self.udp_transport = UDPTransport() self.listen_udp = UDPListener(ssdp_addr, data_callback=self._datagram_received, shared_socket=self.udp_transport.socket) self.loopcall = LoopingCall(self.double_discover) if start: self.start(interval) def is_running(self): """ Returns True if the search is running (it's being repeated in the interval given). @rtype: boolean """ return self.loopcall.is_running() def start(self, interval=DEFAULT_SEARCH_TIME, search_type=DEFAULT_SEARCH_TYPE, http_version="1.1", man='"ssdp:discover"', mx=1, additionals={}): """ Starts the search. @param interval: interval between searchs. Default is 600.0 seconds @param search_type: type of the search, default is "ssdp:all" @param http_version: http version for m-search (default is 1.1) @param man: man field for m-search (default is ssdp:discover) @param mx: mx field for m-search (default is 1) @param additionals: dict containing additional field to be appended in the end of the m-search message (default is a empty dictionary) @type interval: float @type search_type: string @type http_version: string @type man: string @type mx: int @type additionals: dict """ if not self.is_running(): self.ssdp.search_type = search_type self.listen_udp.start() self.loopcall._args = (search_type, http_version, man, mx, additionals, ) self.loopcall.start(interval, now=True) log.debug('MSearch started') else: log.warning(self.msg_already_started) def stop(self): """ Stops the search. """ if self.is_running(): log.debug('MSearch stopped') self.listen_udp.stop() self.loopcall.stop() else: log.warning(self.msg_already_stopped) def destroy(self): """ Destroys and quits MSearch. """ if self.is_running(): self.stop() self.listen_udp.destroy() self.loopcall.destroy() self._cleanup() def double_discover(self, search_type=DEFAULT_SEARCH_TYPE, http_version="1.1", man='"ssdp:discover"', mx=1, additionals={}): """ Sends a MSearch imediatelly. Each call to this method will yield a MSearch message, that is, it won't repeat automatically. """ log.info("Doing double discover for %s, HTTP_VERSION=%s, MAN=%s, \ MX=%d, additionals=%s" % (search_type, http_version, man, mx, additionals)) self.discover(search_type, http_version, man, mx, additionals) def discover(self, search_type=DEFAULT_SEARCH_TYPE, http_version="1.1", man='"ssdp:discover"', mx=1, additionals={}): """ Builds and sends the discover message (MSearch). @param type: search type @type type: string """ if (mx > 120): mx = 120 elif (mx < 1): mx = 1 req = ['M-SEARCH * HTTP/%s' % http_version, 'HOST: %s:%d' % (self.ssdp_addr, self.ssdp_port), 'MAN: %s' % man, 'MX: %s' % mx, 'ST: %s' % search_type] append = req.append [append('%s: %s' % (k, v)) for k, v in additionals.items()] append('') append('') req = '\r\n'.join(req) self.udp_transport.send_data(req, self.ssdp_addr, self.ssdp_port) def _datagram_received(self, data, (host, port)): """ Callback for the UDPListener when messages arrive. @param data: raw data received @param host: host where data came from @param port: port where data came from @type data: string @type host: string @type port: integer """ cmd, headers = parse_http_response(data) if cmd[0].startswith('HTTP/1.') and cmd[1] == '200': if self.ssdp != None: if not self.ssdp.is_known_device(headers['usn']): log.debug('Received MSearch answer %s,%s from %s:%s', headers['usn'], headers['st'], host, port) default_fields_name = ["usn", "st", "location", "server", "cache-control", "ext"] default_header = {} for field in default_fields_name: default_header[field] = headers.pop(field, "") self.ssdp.register(default_header['usn'], default_header['st'], default_header['location'], default_header['server'], default_header['cache-control'], "remote", headers)