class SettingsFile(EventDispatcher): last_login = Property(None) color = Property('red') def __init__(self, filepath): super(SettingsFile, self).__init__() self.filepath = filepath self.number_of_file_updates = 0 # Bind the properties to the function that updates the file self.bind(last_login=self.update_settings_file, color=self.update_settings_file) def on_last_login(self, inst, last_login): # Default handler for the last_login property print 'last login was %s' % last_login def on_color(self, inst, color): # Default handler for the color property print 'color has been set to %s' % color def update_settings_file(self, *args): # Update the file with the latest settings. print 'Updating settings file.' self.number_of_file_updates += 1 with open(self.filepath, 'w') as _f: settings = {'last_login': self.last_login, 'color': self.color} json.dump(settings, _f)
class InternetGatewayDeviceClient(EventDispatcher, log.LogAble): ''' .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * The emitted events changed: - Coherence.UPnP.DeviceClient.detection_completed => device_client_detection_completed * Changed class variable :attr:`detection_completed` to benefit from the EventDispatcher's properties ''' logCategory = 'igd_client' detection_completed = Property(False) ''' To know whenever the device detection has completed. Defaults to `False` and it will be set automatically to `True` by the class method :meth:`embedded_device_notified`. ''' def __init__(self, device): log.LogAble.__init__(self) EventDispatcher.__init__(self) self.register_event('device_client_detection_completed', ) self.device = device self.device.bind(embedded_device_client_detection_completed=self. embedded_device_notified) # noqa self.device_type = self.device.get_friendly_device_type() self.version = int(self.device.get_device_type_version()) self.icons = device.icons self.wan_device = None try: wan_device = self.device.get_embedded_device_by_type( 'WANDevice')[0] self.wan_device = WANDeviceClient(wan_device) except Exception as e: self.warning(f'Embedded WANDevice device not available, device not' f' implemented properly according to the UPnP' f' specification [error: {e}]') raise self.info(f'InternetGatewayDevice {device.get_friendly_name()}') def remove(self): self.info('removal of InternetGatewayDeviceClient started') if self.wan_device is not None: self.wan_device.remove() def embedded_device_notified(self, device): self.info(f'EmbeddedDevice {device} sent notification') if self.detection_completed: return self.detection_completed = True self.dispatch_event('device_client_detection_completed', client=self, udn=self.device.udn)
class DeviceQuery(EventDispatcher): ''' .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * Changed class variable :attr:`fired` to benefit from the EventDispatcher's properties ''' fired = Property(False) def __init__(self, type, pattern, callback, timeout=0, oneshot=True): EventDispatcher.__init__(self) self.type = type self.pattern = pattern self.callback = callback self.timeout = timeout self.oneshot = oneshot if self.type == 'uuid' and self.pattern.startswith('uuid:'): self.pattern = self.pattern[5:] if isinstance(self.callback, str): # print(f'DeviceQuery: register event {self.callback}') self.register_event(self.callback) def fire(self, device): if callable(self.callback): self.callback(device) elif isinstance(self.callback, str): self.dispatch_event(self.callback, device=device) self.fired = True def check(self, device): if self.fired and self.oneshot: return if self.type == 'host' and device.host == self.pattern: self.fire(device) elif (self.type == 'friendly_name' and device.friendly_name == self.pattern): self.fire(device) elif self.type == 'uuid' and device.get_uuid() == self.pattern: self.fire(device)
class WANDeviceClient(EventDispatcher, log.LogAble): ''' .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * The emitted events changed: - Coherence.UPnP.EmbeddedDeviceClient.detection_completed => embedded_device_client_detection_completed * Changed some class variable to benefit from the EventDispatcher's properties: - :attr:`embedded_device_detection_completed` - :attr:`service_detection_completed` ''' logCategory = 'wan_device_client' embedded_device_detection_completed = Property(False) ''' To know whenever the embedded device detection has completed. Defaults to `False` and it will be set automatically to `True` by the class method :meth:`embedded_device_notified`. ''' service_detection_completed = Property(False) ''' To know whenever the service detection has completed. Defaults to `False` and it will be set automatically to `True` by the class method :meth:`service_notified`. ''' def __init__(self, device): log.LogAble.__init__(self) EventDispatcher.__init__(self) self.register_event('embedded_device_client_detection_completed') self.device = device self.device.bind( embedded_device_client_detection_completed=self. embedded_device_notified, # noqa service_notified=self.service_notified, ) self.device_type = self.device.get_friendly_device_type() self.version = int(self.device.get_device_type_version()) self.icons = device.icons self.wan_connection_device = None self.wan_common_interface_connection = None try: wan_connection_device = self.device.get_embedded_device_by_type( 'WANConnectionDevice')[0] self.wan_connection_device = WANConnectionDeviceClient( wan_connection_device) except Exception as er: self.warning( f'Embedded WANConnectionDevice device not available, device ' + f'not implemented properly according to the UPnP ' + f'specification [ERROR: {er}]') raise for service in self.device.get_services(): if service.get_type() in [ 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1' ]: self.wan_common_interface_connection = WANCommonInterfaceConfigClient( # noqa: E501 service) self.info(f'WANDevice {device.get_friendly_name()}') def remove(self): self.info('removal of WANDeviceClient started') if self.wan_common_interface_connection is not None: self.wan_common_interface_connection.remove() if self.wan_connection_device is not None: self.wan_connection_device.remove() def embedded_device_notified(self, device): self.info(f'EmbeddedDevice {device} sent notification') if self.embedded_device_detection_completed: return self.embedded_device_detection_completed = True if (self.embedded_device_detection_completed is True and self.service_detection_completed is True): self.dispatch_event('embedded_device_client_detection_completed', self) def service_notified(self, service): self.info(f'Service {service} sent notification') if self.service_detection_completed: return if self.wan_common_interface_connection is not None: if not hasattr( self.wan_common_interface_connection.service, 'last_time_updated', ): return if (self.wan_common_interface_connection.service.last_time_updated is None): return self.service_detection_completed = True if (self.embedded_device_detection_completed is True and self.service_detection_completed is True): self.dispatch_event('embedded_device_client_detection_completed', self)
class Dispatcher(EventDispatcher): listp = ListProperty([1, 2, 3]) prop = Property(1) dictp = DictProperty({1: 'asd', 2: 'qwe'}) setp = SetProperty(set(range(5))) unitp = UnitProperty(1.0, 'm') stringp = StringProperty('test') limitp = LimitProperty(5, min=0, max=10) def __init__(self): super(Dispatcher, self).__init__() self.bind(listp=self.callback, prop=self.callback, dictp=self.callback) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_setter_prop(self): prev_value = self.prop for i in range(INNER_LOOP): self.prop = prev_value = PropertyTest.create_different_value( prev_value) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_setter_listp(self): prev_value = self.listp for i in range(INNER_LOOP): self.listp = prev_value = ListPropertyTest.create_different_value( prev_value) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_setter_dictprop(self): prev_value = self.dictp for i in range(INNER_LOOP): self.dictp = prev_value = DictPropertyTest.create_different_value( prev_value) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_setter_setprop(self): prev_value = self.setp for i in range(INNER_LOOP): self.setp = prev_value = SetPropertyTest.create_different_value( prev_value) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_setter_stringprop(self): prev_value = self.stringp for i in range(INNER_LOOP): self.stringp = prev_value = StringPropertyTest.create_different_value( prev_value) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_setter_limitprop(self): prev_value = self.limitp for i in range(INNER_LOOP): self.limitp = prev_value = LimitPropertyTest.create_different_value( prev_value) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_setter_unitprop(self): prev_value = self.unitp for i in range(INNER_LOOP): self.unitp = prev_value = UnitPropertyTest.create_different_value( prev_value) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_dispatch(self): dispatch = self.dispatch for i in range(INNER_LOOP): dispatch('prop', self, self.prop) dispatch('dictp', self, self.dictp) dispatch('listp', self, self.listp) dispatch('setp', self, self.setp) dispatch('stringp', self, self.stringp) dispatch('limitp', self, self.limitp) dispatch('unitp', self, self.unitp) @BenchmarkedFunction(classname='Dispatcher', timeit_repeat=TIMEIT_REPEAT, timeit_number=TIMEIT_NUMBER) def run_getter(self): for i in range(INNER_LOOP): prop = self.prop listprop = self.listp dictprop = self.dictp def callback(self, inst, number): pass
class BasicDeviceMixin(EventDispatcher): ''' This is used as a base class for the following classes: - :class:`~coherence.upnp.devices.media_renderer.MediaRenderer` - :class:`~coherence.upnp.devices.media_server.MediaServer` It contains some methods that will help us to initialize the backend (:meth:`on_backend`, :meth:`init_complete` and :meth:`init_failed`). There is no need to call those methods, because it will be automatically triggered based on the backend status. .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * Changed class variable :attr:`backend` to benefit from the EventDispatcher's properties ''' backend = Property(None) '''The device's backend. When this variable is filled it will automatically trigger the method :meth:`on_backend`. ''' def __init__(self, coherence, backend, **kwargs): EventDispatcher.__init__(self) self.coherence = coherence if not hasattr(self, 'version'): self.version = int( kwargs.get('version', self.coherence.config.get('version', 2))) try: self.uuid = str(kwargs['uuid']) if not self.uuid.startswith('uuid:'): self.uuid = 'uuid:' + self.uuid except KeyError: from coherence.upnp.core.uuid import UUID self.uuid = UUID() urlbase = str(self.coherence.urlbase) if urlbase[-1] != '/': urlbase += '/' self.urlbase = urlbase + str(self.uuid)[5:] kwargs['urlbase'] = self.urlbase self.icons = kwargs.get('iconlist', kwargs.get('icons', [])) if len(self.icons) == 0: if 'icon' in kwargs: if isinstance(kwargs['icon'], dict): self.icons.append(kwargs['icon']) else: self.icons = kwargs['icon'] reactor.callLater(0.2, self.fire, backend, **kwargs) def on_backend(self, *arsg): ''' This function is automatically triggered whenever the :attr:`backend` class variable changes. Here we connect the backend initialization with the device. .. versionadded:: 0.9.0 ''' if self.backend is None: return if self.backend.init_completed: self.init_complete(self.backend) self.backend.bind( backend_init_completed=self.init_complete, backend_init_failed=self.init_failed, ) def init_complete(self, backend): # This must be overwritten in subclass pass def init_failed(self, backend, msg): if self.backend != backend: return self.warning(f'backend not installed, {self.device_type} ' f'activation aborted - {msg.getErrorMessage()}') self.debug(msg) try: del self.coherence.active_backends[str(self.uuid)] except KeyError: pass def register(self): s = self.coherence.ssdp_server uuid = str(self.uuid) host = self.coherence.hostname self.msg(f'{self.device_type} register') # we need to do this after the children are there, # since we send notifies s.register('local', f'{uuid}::upnp:rootdevice', 'upnp:rootdevice', self.coherence.urlbase + uuid[5:] + '/' + f'description-{self.version:d}.xml', host=host) s.register('local', uuid, uuid, self.coherence.urlbase + uuid[5:] + '/' + f'description-{self.version:d}.xml', host=host) version = self.version while version > 0: if version == self.version: silent = False else: silent = True s.register( 'local', f'{uuid}::urn:schemas-upnp-org:device:{self.device_type}:{version:d}', # noqa f'urn:schemas-upnp-org:device:{self.device_type}:{version:d}', self.coherence.urlbase + uuid[5:] + '/' + f'description-{version:d}.xml', silent=silent, host=host) version -= 1 for service in self._services: device_version = self.version service_version = self.version if hasattr(service, 'version'): service_version = service.version silent = False while service_version > 0: try: namespace = service.namespace except AttributeError: namespace = 'schemas-upnp-org' device_description_tmpl = f'description-{device_version:d}.xml' if hasattr(service, 'device_description_tmpl'): device_description_tmpl = service.device_description_tmpl s.register( 'local', f'{uuid}::urn:{namespace}:service:{service.id}:{service_version:d}', # noqa f'urn:{namespace}:service:{service.id}:{service_version:d}', # noqa self.coherence.urlbase + uuid[5:] + '/' + device_description_tmpl, silent=silent, host=host) silent = True service_version -= 1 device_version -= 1 def unregister(self): if self.backend is not None and hasattr(self.backend, 'release'): self.backend.release() if not hasattr(self, '_services'): ''' seems we never made it to actually completing that device ''' return for service in self._services: try: service.check_subscribers_loop.stop() except Exception as e1: ms = f'BasicDeviceMixin.unregister: {e1}' if hasattr(self, 'warning'): self.warning(ms) else: print('WARNING: ', ms) if hasattr(service, 'check_moderated_loop') and \ service.check_moderated_loop is not None: try: service.check_moderated_loop.stop() except Exception as e2: ms = f'BasicDeviceMixin.unregister: {e2}' if hasattr(self, 'warning'): self.warning(ms) else: print('WARNING: ', ms) if hasattr(service, 'release'): service.release() if hasattr(service, '_release'): service._release() s = self.coherence.ssdp_server uuid = str(self.uuid) self.coherence.remove_web_resource(uuid[5:]) version = self.version while version > 0: s.doByebye( f'{uuid}::urn:schemas-upnp-org:device:{self.device_type}:{version:d}' ) # noqa for service in self._services: if hasattr(service, 'version') and service.version < version: continue try: namespace = service.namespace except AttributeError: namespace = 'schemas-upnp-org' s.doByebye( f'{uuid}::urn:{namespace}:service:{service.id}:{version:d}' ) version -= 1 s.doByebye(uuid) s.doByebye(f'{uuid}::upnp:rootdevice')
class MediaRendererClient(EventDispatcher, log.LogAble): ''' .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * The emitted events changed: - Coherence.UPnP.DeviceClient.detection_completed => device_client_detection_completed * Changed class variable :attr:`detection_completed` to benefit from the EventDispatcher's properties ''' logCategory = 'mr_client' detection_completed = Property(False) ''' To know whenever the device detection has completed. Defaults to *False* and it will be set automatically to `True` by the class method :meth:`service_notified`. ''' def __init__(self, device): log.LogAble.__init__(self) EventDispatcher.__init__(self) self.register_event('device_client_detection_completed', ) self.device = device self.device.bind(device_service_notified=self.service_notified) self.device_type = self.device.get_friendly_device_type() self.version = int(self.device.get_device_type_version()) self.icons = device.icons self.rendering_control = None self.connection_manager = None self.av_transport = None for service in self.device.get_services(): if service.get_type() in [ 'urn:schemas-upnp-org:service:RenderingControl:1', 'urn:schemas-upnp-org:service:RenderingControl:2' ]: self.rendering_control = RenderingControlClient(service) if service.get_type() in [ 'urn:schemas-upnp-org:service:ConnectionManager:1', 'urn:schemas-upnp-org:service:ConnectionManager:2' ]: self.connection_manager = ConnectionManagerClient(service) if service.get_type() in [ 'urn:schemas-upnp-org:service:AVTransport:1', 'urn:schemas-upnp-org:service:AVTransport:2' ]: self.av_transport = AVTransportClient(service) if service.detection_completed: self.service_notified(service) self.info(f'MediaRenderer {device.get_friendly_name()}') if self.rendering_control: self.info('RenderingControl available') ''' actions = self.rendering_control.service.get_actions() print actions for action in actions: print 'Action:', action for arg in actions[action].get_arguments_list(): print ' ', arg ''' # self.rendering_control.list_presets() # self.rendering_control.get_mute() # self.rendering_control.get_volume() # self.rendering_control.set_mute(desired_mute=1) else: self.warning( 'RenderingControl not available, device not implemented' ' properly according to the UPnP specification') return if self.connection_manager: self.info('ConnectionManager available') # self.connection_manager.get_protocol_info() else: self.warning( 'ConnectionManager not available, device not implemented' ' properly according to the UPnP specification') return if self.av_transport: self.info('AVTransport (optional) available') # self.av_transport.service.subscribe_for_variable( # 'LastChange', 0, self.state_variable_change) # self.av_transport.service.subscribe_for_variable( # 'TransportState', 0, self.state_variable_change) # self.av_transport.service.subscribe_for_variable( # 'CurrentTransportActions', 0, self.state_variable_change) # self.av_transport.get_transport_info() # self.av_transport.get_current_transport_actions() # def __del__(self): # # print('MediaRendererClient deleted') # pass def remove(self): self.info('removal of MediaRendererClient started') if self.rendering_control is not None: self.rendering_control.remove() if self.connection_manager is not None: self.connection_manager.remove() if self.av_transport is not None: self.av_transport.remove() # del self def service_notified(self, service): self.info(f'Service {service} sent notification') if self.detection_completed: return if self.rendering_control is not None: if not hasattr(self.rendering_control.service, 'last_time_updated'): return if self.rendering_control.service.last_time_updated is None: return if self.connection_manager is not None: if not hasattr(self.connection_manager.service, 'last_time_updated'): return if self.connection_manager.service.last_time_updated is None: return if self.av_transport is not None: if not hasattr(self.av_transport.service, 'last_time_updated'): return if self.av_transport.service.last_time_updated is None: return self.detection_completed = True self.dispatch_event('device_client_detection_completed', client=self, udn=self.device.udn) def state_variable_change(self, variable): self.info('%(name)r changed from %(old_value)r to %(value)r', vars(variable))
class RootDevice(Device): ''' Description for a root device. .. versionchanged:: 0.9.0 * Migrated from louie/dispatcher to EventDispatcher * The emitted events changed: - Coherence.UPnP.RootDevice.detection_completed => root_device_detection_completed - Coherence.UPnP.RootDevice.removed => root_device_removed ''' root_detection_completed = Property(False) ''' To know whenever the root device detection has completed. Defaults to `False` and it will be set automatically to `True` by the class method :meth:`device_detect`. ''' def __init__(self, infos): self.usn = infos['USN'] self.udn = infos.get('UDN', '') self.server = infos['SERVER'] self.st = infos['ST'] self.location = infos['LOCATION'] self.manifestation = infos['MANIFESTATION'] self.host = infos['HOST'] Device.__init__(self, None) self.register_event( 'root_device_detection_completed', 'root_device_removed' ) self.bind(detection_completed=self.device_detect) # we need to handle root device completion # these events could be our self or our children. self.parse_description() self.debug(f'RootDevice initialized: {self.location}') def __repr__(self): return ( f'rootdevice {self.friendly_name} {self.udn} {self.st} ' f'{self.host}, manifestation {self.manifestation}' ) def remove(self, *args): result = Device.remove(self, *args) self.dispatch_event('root_device_removed', self, usn=self.get_usn()) return result def get_usn(self): return self.usn def get_st(self): return self.st def get_location(self): return ( self.location if isinstance(self.location, bytes) else self.location.encode('ascii') if self.location else None ) def get_upnp_version(self): return self.upnp_version def get_urlbase(self): return ( self.urlbase if isinstance(self.urlbase, bytes) else self.urlbase.encode('ascii') if self.urlbase else None ) def get_host(self): return self.host def is_local(self): if self.manifestation == 'local': return True return False def is_remote(self): if self.manifestation != 'local': return True return False def device_detect(self, *args, **kwargs): ''' This method is automatically triggered whenever the property of the base class :attr:`Device.detection_completed` is set to `True`. Here we perform some more operations, before the :class:`RootDevice` emits an event notifying that the root device detection has completed. ''' self.debug(f'device_detect {kwargs}') self.debug(f'root_detection_completed {self.root_detection_completed}') if self.root_detection_completed: return # our self is not complete yet self.debug(f'detection_completed {self.detection_completed}') if not self.detection_completed: return # now check child devices. self.debug(f'self.devices {self.devices}') for d in self.devices: self.debug(f'check device {d.detection_completed} {d}') if not d.detection_completed: return # now must be done, so notify root done self.root_detection_completed = True self.info( f'rootdevice {self.friendly_name} {self.st} {self.host} ' + f'initialized, manifestation {self.manifestation}' ) self.dispatch_event('root_device_detection_completed', device=self) def add_device(self, device): self.debug(f'RootDevice add_device {device}') self.devices.append(device) def get_devices(self): self.debug(f'RootDevice get_devices: {self.devices}') return self.devices def parse_description(self): def gotPage(x): self.debug(f'got device description from {self.location}') self.debug(f'data is {x}') data, headers = x xml_data = None try: xml_data = etree.fromstring(data) except Exception: self.warning( f'Invalid device description received from {self.location}' ) import traceback self.debug(traceback.format_exc()) if xml_data is not None: tree = xml_data major = tree.findtext(f'./{{{ns}}}specVersion/{{{ns}}}major') minor = tree.findtext(f'./{{{ns}}}specVersion/{{{ns}}}minor') try: self.upnp_version = '.'.join((major, minor)) except Exception: self.upnp_version = 'n/a' try: self.urlbase = tree.findtext(f'./{{{ns}}}URLBase') except Exception: import traceback self.debug(traceback.format_exc()) d = tree.find(f'./{{{ns}}}device') if d is not None: self.parse_device(d) # root device self.debug(f'device parsed successfully {self.location}') def gotError(failure, url): self.warning(f'error getting device description from {url}') self.info(failure) try: utils.getPage(self.location).addCallbacks( gotPage, gotError, None, None, [self.location], None ) except Exception as e: self.error(f'Error on parsing device description: {e}') def make_fullyqualified(self, url): '''Be aware that this function returns a byte string''' self.info(f'make_fullyqualified: {url} [{type(url)}]') if isinstance(url, str): url = url.encode('ascii') if url.startswith(b'http://'): return url from urllib.parse import urljoin base = self.get_urlbase() if isinstance(base, str): base = base.encode('ascii') if base is not None: if base[-1] != b'/': base += b'/' r = urljoin(base, url) else: loc = self.get_location() if isinstance(loc, str): loc = loc.encode('ascii') r = urljoin(loc, url) return r
class Device(EventDispatcher, log.LogAble): ''' Represents a UPnP's device, but this is not a root device, it's the base class used for any device. See :class:`RootDevice` if you want a root device. .. versionchanged:: 0.9.0 * Migrated from louie/dispatcher to EventDispatcher * The emitted events changed: - Coherence.UPnP.Device.detection_completed => device_detection_completed - Coherence.UPnP.Device.remove_client => device_remove_client * New events: device_service_notified, device_got_client * Changes some class variables to benefit from the EventDispatcher's properties: - :attr:`client` - :attr:`devices` - :attr:`services` - :attr:`client` - :attr:`detection_completed` ''' logCategory = 'device' client = Property(None) ''' Defined by :class:`~coherence.upnp.devices.controlpoint.ControlPoint`. It should be one of: - Initialized instance of a class :class:`~coherence.upnp.devices.media_server_client.MediaServerClient` - Initialized instance of a class :class:`~coherence.upnp.devices.media_renderer_client.MediaRendererClient` - Initialized instance of a class :class:`~coherence.upnp.devices.internet_gateway_device_client.InternetGatewayDeviceClient` Whenever a client is set an event will be sent notifying it by :meth:`on_client`. ''' # noqa icons = ListProperty([]) '''A list of the device icons.''' devices = ListProperty([]) '''A list of the device devices.''' services = ListProperty([]) '''A list of the device services.''' detection_completed = Property(False) ''' To know whenever the device detection has completed. Defaults to `False` and it will be set automatically to `True` by the class method :meth:`receiver`. ''' def __init__(self, parent=None, udn=None): log.LogAble.__init__(self) EventDispatcher.__init__(self) self.register_event( 'device_detection_completed', 'device_remove_client', 'device_service_notified', 'device_got_client', ) self.parent = parent self.udn = udn # self.uid = self.usn[:-len(self.st)-2] self.friendly_name = '' self.device_type = '' self.upnp_version = 'n/a' self.friendly_device_type = '[unknown]' self.device_type_version = 0 def __repr__(self): return ( f'embedded device {self.friendly_name} ' + f'{self.device_type}, parent {self.parent}' ) # def __del__(self): # # print('Device removal completed') # pass def as_dict(self): d = { 'device_type': self.get_device_type(), 'friendly_name': self.get_friendly_name(), 'udn': self.get_id(), 'services': [x.as_dict() for x in self.services], } icons = [] for icon in self.icons: icons.append( { 'mimetype': icon['mimetype'], 'url': icon['url'], 'height': icon['height'], 'width': icon['width'], 'depth': icon['depth'], } ) d['icons'] = icons return d def remove(self, *args): self.info(f'removal of {self.friendly_name} {self.udn}') while len(self.devices) > 0: device = self.devices.pop() self.debug(f'try to remove {device}') device.remove() while len(self.services) > 0: service = self.services.pop() self.debug(f'try to remove {service}') service.remove() if self.client is not None: self.dispatch_event('device_remove_client', self.udn, self.client) self.client = None # del self return True def receiver(self, *args, **kwargs): if self.detection_completed: return for s in self.services: if not s.detection_completed: return self.dispatch_event('device_service_notified', service=s) if self.udn is None: return self.detection_completed = True if self.parent is not None: self.info( f'embedded device {self.friendly_name} ' + f'{self.device_type} initialized, parent {self.parent}' ) self.dispatch_event('device_detection_completed', None, device=self) if self.parent is not None: self.dispatch_event( 'device_detection_completed', self.parent, device=self ) else: self.dispatch_event( 'device_detection_completed', self, device=self ) def service_detection_failed(self, device): self.remove() def get_id(self): return self.udn def get_uuid(self): return self.udn[5:] def get_embedded_devices(self): return self.devices def get_embedded_device_by_type(self, type): r = [] for device in self.devices: if type == device.friendly_device_type: r.append(device) return r def get_services(self): return self.services def get_service_by_type(self, service_type): if not isinstance(service_type, (tuple, list)): service_type = [service_type] for service in self.services: _, _, _, service_class, version = service.service_type.split(':') if service_class in service_type: return service def add_service(self, service): ''' Add a service to the device. Also we check if service already notified, and trigger the callback if needed. We also connect the device to service in case the service still not completed his detection in order that the device knows when the service has completed his detection. Args: service (object): A service which should be an initialized instance of :class:`~coherence.upnp.core.service.Service` ''' self.debug(f'add_service {service}') if service.detection_completed: self.receiver(service) service.bind( service_detection_completed=self.receiver, service_detection_failed=self.service_detection_failed, ) self.services.append(service) # fixme: This fails as Service.get_usn() is not implemented. def remove_service_with_usn(self, service_usn): for service in self.services: if service.get_usn() == service_usn: service.unbind( service_detection_completed=self.receiver, service_detection_failed=self.service_detection_failed, ) self.services.remove(service) service.remove() break def add_device(self, device): self.debug(f'Device add_device {device}') self.devices.append(device) def get_friendly_name(self): return self.friendly_name def get_device_type(self): return self.device_type def get_friendly_device_type(self): return self.friendly_device_type def get_markup_name(self): try: return self._markup_name except AttributeError: self._markup_name = ( f'{self.friendly_device_type}:{self.device_type_version} ' + f'{self.friendly_name}' ) return self._markup_name def get_device_type_version(self): return self.device_type_version def set_client(self, client): self.client = client def get_client(self): return self.client def on_client(self, *args): ''' Automatically triggered whenever a client is set or changed. Emmit an event notifying that the client has changed. .. versionadded:: 0.9.0 ''' self.dispatch_event('device_got_client', self, client=self.client) def renew_service_subscriptions(self): ''' iterate over device's services and renew subscriptions ''' self.info(f'renew service subscriptions for {self.friendly_name}') now = time.time() for service in self.services: self.info( f'check service {service.id} {service.get_sid()} ' + f'{service.get_timeout()} {now}' ) if service.get_sid() is not None: if service.get_timeout() < now: self.debug( f'wow, we lost an event subscription for ' + f'{self.friendly_name} {service.get_id()}, ' + f'maybe we need to rethink the loop time and ' + f'timeout calculation?' ) if service.get_timeout() < now + 30: service.renew_subscription() for device in self.devices: device.renew_service_subscriptions() def unsubscribe_service_subscriptions(self): '''Iterate over device's services and unsubscribe subscriptions ''' sl = [] for service in self.get_services(): if service.get_sid() is not None: sl.append(service.unsubscribe()) dl = defer.DeferredList(sl) return dl def parse_device(self, d): self.info(f'parse_device {d}') self.device_type = d.findtext(f'./{{{ns}}}deviceType') ( self.friendly_device_type, self.device_type_version, ) = self.device_type.split(':')[-2:] self.friendly_name = d.findtext(f'./{{{ns}}}friendlyName') self.udn = d.findtext(f'./{{{ns}}}UDN') self.info(f'found udn {self.udn} {self.friendly_name}') try: self.manufacturer = d.findtext(f'./{{{ns}}}manufacturer') except Exception: pass try: self.manufacturer_url = d.findtext(f'./{{{ns}}}manufacturerURL') except Exception: pass try: self.model_name = d.findtext(f'./{{{ns}}}modelName') except Exception: pass try: self.model_description = d.findtext(f'./{{{ns}}}modelDescription') except Exception: pass try: self.model_number = d.findtext(f'./{{{ns}}}modelNumber') except Exception: pass try: self.model_url = d.findtext(f'./{{{ns}}}modelURL') except Exception: pass try: self.serial_number = d.findtext(f'./{{{ns}}}serialNumber') except Exception: pass try: self.upc = d.findtext(f'./{{{ns}}}UPC') except Exception: pass try: self.presentation_url = d.findtext(f'./{{{ns}}}presentationURL') except Exception: pass try: for dlna_doc in d.findall( './{urn:schemas-dlna-org:device-1-0}X_DLNADOC' ): try: self.dlna_dc.append(dlna_doc.text) except AttributeError: self.dlna_dc = [] self.dlna_dc.append(dlna_doc.text) except Exception: pass try: for dlna_cap in d.findall( './{urn:schemas-dlna-org:device-1-0}X_DLNACAP' ): for cap in dlna_cap.text.split(','): try: self.dlna_cap.append(cap) except AttributeError: self.dlna_cap = [] self.dlna_cap.append(cap) except Exception: pass icon_list = d.find(f'./{{{ns}}}iconList') if icon_list is not None: from urllib.parse import urlparse url_base = '%s://%s' % urlparse(self.get_location())[:2] for icon in icon_list.findall(f'./{{{ns}}}icon'): try: i = {} i['mimetype'] = icon.find(f'./{{{ns}}}mimetype').text i['width'] = icon.find(f'./{{{ns}}}width').text i['height'] = icon.find(f'./{{{ns}}}height').text i['depth'] = icon.find(f'./{{{ns}}}depth').text i['realurl'] = icon.find(f'./{{{ns}}}url').text i['url'] = self.make_fullyqualified(i['realurl']).decode( 'utf-8' ) self.icons.append(i) self.debug(f'adding icon {i} for {self.friendly_name}') except Exception as e: import traceback self.debug(traceback.format_exc()) self.warning( f'device {self.friendly_name} seems to have an invalid' + f' icon description, ignoring that icon [error: {e}]' ) serviceList = d.find(f'./{{{ns}}}serviceList') if serviceList is not None: for service in serviceList.findall(f'./{{{ns}}}service'): serviceType = service.findtext(f'{{{ns}}}serviceType') serviceId = service.findtext(f'{{{ns}}}serviceId') controlUrl = service.findtext(f'{{{ns}}}controlURL') eventSubUrl = service.findtext(f'{{{ns}}}eventSubURL') presentationUrl = service.findtext(f'{{{ns}}}presentationURL') scpdUrl = service.findtext(f'{{{ns}}}SCPDURL') # check if values are somehow reasonable if len(scpdUrl) == 0: self.warning('service has no uri for its description') continue if len(eventSubUrl) == 0: self.warning('service has no uri for eventing') continue if len(controlUrl) == 0: self.warning('service has no uri for controling') continue try: self.add_service( Service( serviceType, serviceId, self.get_location(), controlUrl, eventSubUrl, presentationUrl, scpdUrl, self, ) ) except Exception as e: self.error( f'Error on adding service: {service} [ERROR: {e}]' ) # now look for all sub devices embedded_devices = d.find(f'./{{{ns}}}deviceList') if embedded_devices is not None: for d in embedded_devices.findall(f'./{{{ns}}}device'): embedded_device = Device(self) self.add_device(embedded_device) embedded_device.parse_device(d) self.receiver() def get_location(self): return self.parent.get_location() def get_usn(self): return self.parent.get_usn() def get_upnp_version(self): return self.parent.get_upnp_version() def get_urlbase(self): return self.parent.get_urlbase() def get_presentation_url(self): try: return self.make_fullyqualified(self.presentation_url) except Exception: return '' def get_parent_id(self): try: return self.parent.get_id() except Exception: return '' def make_fullyqualified(self, url): return self.parent.make_fullyqualified(url) def as_tuples(self): r = [] def append(name, attribute): try: if isinstance(attribute, tuple): if callable(attribute[0]): v1 = attribute[0]() else: v1 = getattr(self, attribute[0]) if v1 in [None, 'None']: return if callable(attribute[1]): v2 = attribute[1]() else: v2 = getattr(self, attribute[1]) if v2 in [None, 'None']: return r.append((name, (v1, v2))) return elif callable(attribute): v = attribute() else: v = getattr(self, attribute) if v not in [None, 'None']: r.append((name, v)) except Exception as e: self.error(f'Device.as_tuples: {e}') import traceback self.debug(traceback.format_exc()) try: r.append(('Location', (self.get_location(), self.get_location()))) except Exception: pass try: append('URL base', self.get_urlbase) except Exception: pass try: r.append(('UDN', self.get_id())) except Exception: pass try: r.append(('Type', self.device_type)) except Exception: pass try: r.append(('UPnP Version', self.upnp_version)) except Exception: pass try: r.append(('DLNA Device Class', ','.join(self.dlna_dc))) except Exception: pass try: r.append(('DLNA Device Capability', ','.join(self.dlna_cap))) except Exception: pass try: r.append(('Friendly Name', self.friendly_name)) except Exception: pass try: append('Manufacturer', 'manufacturer') except Exception: pass try: append( 'Manufacturer URL', ('manufacturer_url', 'manufacturer_url') ) except Exception: pass try: append('Model Description', 'model_description') except Exception: pass try: append('Model Name', 'model_name') except Exception: pass try: append('Model Number', 'model_number') except Exception: pass try: append('Model URL', ('model_url', 'model_url')) except Exception: pass try: append('Serial Number', 'serial_number') except Exception: pass try: append('UPC', 'upc') except Exception: pass try: append( 'Presentation URL', ( 'presentation_url', lambda: self.make_fullyqualified( getattr(self, 'presentation_url') ), ), ) except Exception: pass for icon in self.icons: r.append( ( 'Icon', ( icon['realurl'], self.make_fullyqualified(icon['realurl']), { 'Mimetype': icon['mimetype'], 'Width': icon['width'], 'Height': icon['height'], 'Depth': icon['depth'], }, ), ) ) return r
class Dispatcher(EventDispatcher): p1 = Property(10) p2 = Property(20)
class Coherence(EventDispatcher, log.LogAble): ''' The Main class of the Cohen3 project. The Coherence class controls all the servers initialization depending on the configuration passed. It is also capable of initialize the plugins defined in config variable or by configuration file. It supports the creation of multiple servers at once. **Example of a simple server via plugin AppleTrailersStore**:: from coherence.base import Coherence from coherence.upnp.core.uuid import UUID from twisted.internet import reactor new_uuid = UUID() coherence = Coherence( {'logmode': 'info', 'controlpoint': 'yes', 'plugin': [{'backend': 'AppleTrailersStore', 'name': 'Cohen3 Example FSStore', 'uuid': new_uuid, } ] } ) reactor.run() .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * The emitted events changed: - Coherence.UPnP.Device.detection_completed => coherence_device_detection_completed - Coherence.UPnP.Device.removed => coherence_device_removed - Coherence.UPnP.RootDevice.removed => coherence_root_device_removed * Changed some variables to benefit from the EventDispatcher's properties: - :attr:`devices` - :attr:`children` - :attr:`_callbacks` - :attr:`active_backends` - :attr:`ctrl` - :attr:`dbus` - :attr:`json` - :attr:`msearch` - :attr:`ssdp_server` - :attr:`transcoder_manager` - :attr:`web_server` ''' __instance = None # Singleton __initialized = False __incarnations = 0 __cls = None logCategory = 'coherence' devices = ListProperty([]) '''A list of the added devices.''' children = DictProperty({}) '''A dict containing the web resources.''' _callbacks = DictProperty({}) '''A dict containing the callbacks, used by the methods :meth:`subscribe` and :meth:`unsubscribe`.''' active_backends = DictProperty({}) '''A dict containing the active backends.''' # Services/Devices ctrl = Property(None) '''A coherence's instance of class :class:`~coherence.upnp.devices.control_point.ControlPoint`. This will be enabled if we request it by config dict or configuration file via keyword *controlpoint = yes*.''' dbus = Property(None) '''A coherence's instance of class :class:`~coherence.dbus_service.DBusPontoon`. This will be enabled if we request it by config dict or configuration file via keyword *use_dbus = yes*.''' json = Property(None) '''A coherence's instance of class :class:`~coherence.json_service.JsonInterface`. This will be enabled if we request it by config dict or configuration file via keyword *json = yes*.''' msearch = Property(None) '''A coherence's instance of class :class:`~coherence.upnp.core.msearch.MSearch`. This is automatically enabled when :class:`Coherence` is initialized''' ssdp_server = Property(None) '''A coherence's instance of class :class:`~coherence.upnp.core.ssdp.SSDPServer`. This is automatically enabled when :class:`Coherence` is initialized''' transcoder_manager = Property(None) '''A coherence's instance of class :class:`~coherence.transcoder.TranscoderManager`. This will be enabled if we request itby config dict or configuration file via keyword *transcoding = yes*.''' web_server = Property(None) '''A coherence's instance of class :class:`WebServer` or :class:`WebServerUi`. We can request our preference by config dict or configuration file. If we use the keyword *web-ui = yes*, then the class :class:`WebServerUi` will be used, otherwise, the enabled web server will be of class :class:`WebServer`.''' def __new__(cls, *args, **kwargs): if cls.__instance is None: cls.__instance = super(Coherence, cls).__new__(cls) cls.__instance.__initialized = False cls.__instance.__incarnations = 0 cls.__instance.__cls = cls cls.__instance.config = kwargs.get('config', {}) cls.__instance.__incarnations += 1 return cls.__instance def __init__(self, config=None): # initialize only once if self.__initialized: return self.__initialized = True # supers log.LogAble.__init__(self) EventDispatcher.__init__(self) self.register_event( 'coherence_device_detection_completed', 'coherence_device_removed', 'coherence_root_device_removed', ) self.config = config or {} self.available_plugins = None self.external_address = None self.urlbase = None self.web_server_port = int(config.get('serverport', 8080)) self.setup_logger() self.setup_hostname() if self.hostname.startswith('127.'): # use interface detection via routing table as last resort def catch_result(hostname): self.hostname = hostname self.setup_part2() d = defer.maybeDeferred(get_host_address) d.addCallback(catch_result) else: self.setup_part2() def clear(self): '''We do need this to survive multiple calls to Coherence during trial tests''' self.unbind_all() self.__cls.__instance = None @property def is_unittest(self): ''' Reads config and returns if we are testing or not via `unittest` key. ''' unittest = self.config.get('unittest', 'no') return False if unittest in {'no', False, None} else True @property def log_level(self): '''Read config and return the log level.''' try: log_level = self.config.get('logging').get('level', 'warning') except (KeyError, AttributeError): log_level = self.config.get('logmode', 'warning') return log_level.upper() @property def log_file(self): '''Read config and return the logfile or `None`.''' try: logfile = self.config.get('logging').get('logfile', None) if logfile is not None: logfile = str(logfile) except (KeyError, AttributeError, TypeError): logfile = self.config.get('logfile', None) return logfile def setup_logger(self): '''Initializes log's system based on config. .. note:: the COHEN_DEBUG environment variable overwrites all level settings in here ''' try: subsystems = self.config.get('logging')['subsystem'] if isinstance(subsystems, dict): subsystems = [subsystems] for subsystem in subsystems: try: if subsystem['active'] == 'no': continue except (KeyError, TypeError): pass self.info(f'setting log-level for subsystem ' + f'{subsystem["name"]} to {subsystem["level"]}') logging.getLogger(subsystem['name'].lower()).setLevel( subsystem['level'].upper()) except (KeyError, TypeError): subsystem_log = self.config.get('subsystem_log', {}) for subsystem, level in list(subsystem_log.items()): logging.getLogger(subsystem.lower()).setLevel(level.upper()) log.init(self.log_file, self.log_level) self.warning(f'Coherence UPnP framework version {__version__} ' + f'starting [log level: {self.log_level}]...') def setup_hostname(self): ''' Configure the `hostname`. .. note:: If something goes wrong will default to `127.0.0.1` ''' network_if = self.config.get('interface') if network_if: self.hostname = get_ip_address(f'{network_if}') else: try: self.hostname = socket.gethostbyname(socket.gethostname()) except socket.gaierror: self.warning('hostname can\'t be resolved, ' + 'maybe a system misconfiguration?') self.hostname = '127.0.0.1' self.info(f'running on host: {self.hostname}') if self.hostname.startswith('127.'): self.warning( f'detection of own ip failed, using {self.hostname} ' + f'as own address, functionality will be limited') def setup_ssdp_server(self): '''Initialize the :class:`~coherence.upnp.core.ssdp.SSDPServer`.''' try: # TODO: add ip/interface bind self.ssdp_server = SSDPServer(test=self.is_unittest) except CannotListenError as err: self.error(f'Error starting the SSDP-server: {err}') self.debug('Error starting the SSDP-server', exc_info=True) reactor.stop() return # maybe some devices are already notified, so we enforce # to create the device, if it is not already added...and # then we connect the signals for new detections. for st, usn in self.ssdp_server.root_devices: self.create_device(st, usn) self.ssdp_server.bind(new_device=self.create_device) self.ssdp_server.bind(removed_device=self.remove_device) self.ssdp_server.subscribe('new_device', self.add_device) self.ssdp_server.subscribe('removed_device', self.remove_device) def setup_web_server(self): '''Initialize the web server.''' try: # TODO: add ip/interface bind if self.config.get('web-ui', 'no') != 'yes': self.web_server = WebServer(None, self.web_server_port, self) else: self.web_server = WebServerUi(self.web_server_port, self, unittests=self.is_unittest) except CannotListenError: self.error( f'port {self.web_server_port} already in use, aborting!') reactor.stop() return self.urlbase = f'http://{self.hostname}:{self.web_server_port:d}/' self.external_address = ':'.join( (self.hostname, str(self.web_server_port))) # self.renew_service_subscription_loop = \ # task.LoopingCall(self.check_devices) # self.renew_service_subscription_loop.start(20.0, now=False) def setup_plugins(self): '''Initialize the plugins.''' try: plugins = self.config['plugin'] if isinstance(plugins, dict): plugins = [plugins] except Exception: plugins = None if plugins is None: plugins = self.config.get('plugins', None) if plugins is None: self.info('No plugin defined!') else: if isinstance(plugins, dict): for plugin, arguments in list(plugins.items()): try: if not isinstance(arguments, dict): arguments = {} self.add_plugin(plugin, **arguments) except Exception as msg: self.warning(f'Can\'t enable plugin, {plugin}: {msg}!') self.info(traceback.format_exc()) else: for plugin in plugins: try: if plugin['active'] == 'no': continue except (KeyError, TypeError): pass try: backend = plugin['backend'] arguments = copy.copy(plugin) del arguments['backend'] backend = self.add_plugin(backend, **arguments) if self.writeable_config(): if 'uuid' not in plugin: plugin['uuid'] = str(backend.uuid)[5:] self.config.save() except Exception as msg: self.warning(f'Can\'t enable plugin, {plugin}: {msg}!') self.error(traceback.format_exc()) def setup_part2(self): '''Initializes the basic and optional services/devices and the enabled plugins (backends).''' self.setup_ssdp_server() if not self.ssdp_server: raise Exception('Unable to initialize an ssdp server') self.msearch = MSearch(self.ssdp_server, test=self.is_unittest) reactor.addSystemEventTrigger( 'before', 'shutdown', self.shutdown, force=True, ) self.setup_web_server() if not self.urlbase: raise Exception('Unable to initialize an web server') self.setup_plugins() # Control Point Initialization if (self.config.get('controlpoint', 'no') == 'yes' or self.config.get('json', 'no') == 'yes'): self.ctrl = ControlPoint(self) # Json Interface Initialization if self.config.get('json', 'no') == 'yes': from coherence.json_service import JsonInterface self.json = JsonInterface(self.ctrl) # Transcoder Initialization if self.config.get('transcoding', 'no') == 'yes': from coherence.transcoder import TranscoderManager self.transcoder_manager = TranscoderManager(self) # DBus Initialization if self.config.get('use_dbus', 'no') == 'yes': try: from coherence import dbus_service if self.ctrl is None: self.ctrl = ControlPoint(self) self.ctrl.auto_client_append('InternetGatewayDevice') self.dbus = dbus_service.DBusPontoon(self.ctrl) except Exception as msg: self.warning(f'Unable to activate dbus sub-system: {msg}') self.debug(traceback.format_exc()) def add_plugin(self, plugin, **kwargs): self.info(f'adding plugin {plugin}') self.available_plugins = Plugins() # TODO clean up this exception concept try: plugin_class = self.available_plugins.get(plugin, None) if plugin_class is None: raise KeyError for device in plugin_class.implements: try: device_class = globals().get(device, None) if device_class is None: raise KeyError self.info(f'Activating {plugin} plugin as {device}...') new_backend = device_class(self, plugin_class, **kwargs) self.active_backends[str(new_backend.uuid)] = new_backend return new_backend except KeyError: self.warning(f'Can\'t enable {plugin} plugin, ' f'sub-system {device} not found!') except Exception as e1: self.exception(f'Can\'t enable {plugin} plugin for ' f'sub-system {device} [exception: {e1}]') self.debug(traceback.format_exc()) except KeyError: self.warning(f'Can\'t enable {plugin} plugin, not found!') except Exception as e2: self.warning(f'Can\'t enable {plugin} plugin, {e2}!') self.debug(traceback.format_exc()) def remove_plugin(self, plugin): '''Removes a backend from Coherence Args: plugin (object): is the object return by add_plugin or an UUID string. ''' if isinstance(plugin, str): try: plugin = self.active_backends[plugin] except KeyError: self.warning(f'no backend with the uuid {plugin} found') return '' try: del self.active_backends[str(plugin.uuid)] self.info(f'removing plugin {plugin}') plugin.unregister() return plugin.uuid except KeyError: self.warning(f'no backend with the uuid {plugin.uuid} found') return '' @staticmethod def writeable_config(): '''Do we have a new-style config file''' return False def store_plugin_config(self, uuid, items): '''Find the backend with uuid and store in its the config the key and value pair(s).''' plugins = self.config.get('plugin') if plugins is None: self.warning('storing a plugin config option is only possible' ' with the new config file format') return if isinstance(plugins, dict): plugins = [plugins] uuid = str(uuid) if uuid.startswith('uuid:'): uuid = uuid[5:] for plugin in plugins: try: if plugin['uuid'] == uuid: for k, v in list(items.items()): plugin[k] = v self.config.save() except Exception as e: self.warning(f'Coherence.store_plugin_config: {e}') else: self.info(f'storing plugin config option ' f'for {uuid} failed, plugin not found') def receiver(self, signal, *args, **kwargs): pass def shutdown(self, force=False): if self.__incarnations > 1 and not force: self.__incarnations -= 1 return if self.dbus: self.dbus.shutdown() self.dbus = None for backend in self.active_backends.values(): backend.unregister() self.active_backends = {} # send service unsubscribe messages if self.web_server is not None: if hasattr(self.web_server, 'endpoint_listen'): if self.web_server.endpoint_listen is not None: self.web_server.endpoint_listen.cancel() self.web_server.endpoint_listen = None if self.web_server.endpoint_port is not None: self.web_server.endpoint_port.stopListening() if hasattr(self.web_server, 'ws_endpoint_listen'): if self.web_server.ws_endpoint_listen is not None: self.web_server.ws_endpoint_listen.cancel() self.web_server.ws_endpoint_listen = None if self.web_server.ws_endpoint_port is not None: self.web_server.ws_endpoint_port.stopListening() try: if hasattr(self.msearch, 'double_discover_loop'): self.msearch.double_discover_loop.stop() if hasattr(self.msearch, 'port'): self.msearch.port.stopListening() if hasattr(self.ssdp_server, 'resend_notify_loop'): self.ssdp_server.resend_notify_loop.stop() if hasattr(self.ssdp_server, 'port'): self.ssdp_server.port.stopListening() # self.renew_service_subscription_loop.stop() except Exception: pass dev_l = [] for root_device in self.get_devices(): if hasattr(root_device, 'root_device_detection_completed'): root_device.unbind( root_device_detection_completed=self.add_device) for device in root_device.get_devices(): dd = device.unsubscribe_service_subscriptions() dd.addCallback(device.remove) dev_l.append(dd) rd = root_device.unsubscribe_service_subscriptions() rd.addCallback(root_device.remove) dev_l.append(rd) def homecleanup(result): # cleans up anything left over self.ssdp_server.unbind(new_device=self.create_device) self.ssdp_server.unbind(removed_device=self.remove_device) self.ssdp_server.shutdown() if self.ctrl: self.ctrl.shutdown() self.warning('Coherence UPnP framework shutdown') return result dl = defer.DeferredList(dev_l) dl.addCallback(homecleanup) return dl def check_devices(self): '''Iterate over devices and their embedded ones and renew subscriptions.''' for root_device in self.get_devices(): root_device.renew_service_subscriptions() for device in root_device.get_devices(): device.renew_service_subscriptions() def subscribe(self, name, callback): self._callbacks.setdefault(name, []).append(callback) def unsubscribe(self, name, callback): callbacks = self._callbacks.get(name, []) if callback in callbacks: callbacks.remove(callback) self._callbacks[name] = callbacks def callback(self, name, *args): for callback in self._callbacks.get(name, []): callback(*args) def get_device_by_host(self, host): found = [] for device in self.devices: if device.get_host() == host: found.append(device) return found def get_device_with_usn(self, usn): found = None for device in self.devices: if device.get_usn() == usn: found = device break return found def get_device_with_id(self, device_id): # print(f'get_device_with_id [{type(device_id)}]: {device_id}') found = None for device in self.devices: id = device.get_id() if device_id[:5] != 'uuid:': id = id[5:] if id == device_id: found = device break return found def get_devices(self): # print(f'get_devices: {self.devices}') return self.devices def get_local_devices(self): # print(f'get_local_devices: ' # f'{[d for d in self.devices if d.manifestation == "local"]}') return [d for d in self.devices if d.manifestation == 'local'] def get_nonlocal_devices(self): # print(f'get_nonlocal_devices: ' # f'{[d for d in self.devices if d.manifestation == "remote"]}') return [d for d in self.devices if d.manifestation == 'remote'] def is_device_added(self, infos): ''' Check if the device exists in our list of created :attr:`devices`. Args: infos (dict): Information about the device Returns: True if the device exists in our list of :attr:`devices`, otherwise, returns False. .. versionadded:: 0.9.0 ''' for d in self.devices: if d.st == infos['ST'] and d.usn == infos['USN']: return True return False def create_device(self, device_type, infos): if self.is_device_added(infos): self.warning( f'No need to create the device, we already added device: ' f'{infos["ST"]} with usn {infos["USN"]}...!!') return self.info(f'creating {infos["ST"]} {infos["USN"]}') if infos['ST'] == 'upnp:rootdevice': self.info(f'creating upnp:rootdevice {infos["USN"]}') root = RootDevice(infos) root.bind(root_detection_completed=self.add_device) else: self.info(f'creating device/service {infos["USN"]}') root_id = infos['USN'][:-len(infos['ST']) - 2] root = self.get_device_with_id(root_id) # TODO: must check that this is working as expected device = Device(root, udn=infos['UDN']) def add_device(self, device, *args): self.info(f'adding device {device.get_id()} {device.get_usn()} ' + f'{device.friendly_device_type}') self.devices.append(device) self.dispatch_event('coherence_device_detection_completed', device=device) def remove_device(self, device_type, infos): self.info(f'removed device {infos["ST"]} {infos["USN"]}') device = self.get_device_with_usn(infos['USN']) if device: self.dispatch_event('coherence_device_removed', infos['USN'], device.client) self.devices.remove(device) device.remove() if infos['ST'] == 'upnp:rootdevice': self.dispatch_event( 'coherence_root_device_removed', infos['USN'], device.client, ) self.callback('removed_device', infos['ST'], infos['USN']) def add_web_resource(self, name, sub): self.children[name] = sub def remove_web_resource(self, name): try: del self.children[name] except KeyError: ''' probably the backend init failed ''' pass @staticmethod def check_louie(receiver, signal, method='connect'): ''' Check if the connect or disconnect method's arguments are valid in order to automatically convert to EventDispatcher's bind The old valid signals are: - Coherence.UPnP.Device.detection_completed - Coherence.UPnP.RootDevice.detection_completed - Coherence.UPnP.Device.removed - Coherence.UPnP.RootDevice.removed .. versionadded:: 0.9.0 ''' if not callable(receiver): raise Exception('The receiver should be callable in order to use' + ' the method {method}') if not signal: raise Exception( f'We need a signal in order to use method {method}') if not (signal.startswith('Coherence.UPnP.Device.') or signal.startswith('Coherence.UPnP.RootDevice.')): raise Exception( 'We need a signal an old signal starting with: ' + '"Coherence.UPnP.Device." or "Coherence.UPnP.RootDevice."') def connect(self, receiver, signal=None, sender=None, weak=True): ''' Wrapper method around the deprecated method louie.connect. It will check if the passed signal is supported by executing the method :meth:`check_louie`. .. warning:: This will probably be removed at some point, if you use the connect method you should migrate to the new event system EventDispatcher. .. versionchanged:: 0.9.0 Added EventDispatcher's compatibility for some basic signals ''' self.check_louie(receiver, signal, 'connect') if signal.endswith('.detection_completed'): self.bind(coherence_device_detection_completed=receiver) elif signal.endswith('.Device.removed'): self.bind(coherence_device_removed=receiver) elif signal.endswith('.RootDevice.removed'): self.bind(coherence_root_device_removed=receiver) else: raise Exception( f'Unknown signal {signal}, we cannot bind that signal.') def disconnect(self, receiver, signal=None, sender=None, weak=True): ''' Wrapper method around the deprecated method louie.disconnect. It will check if the passed signal is supported by executing the method :meth:`check_louie`. .. warning:: This will probably be removed at some point, if you use the disconnect method you should migrate to the new event system EventDispatcher. .. versionchanged:: 0.9.0 Added EventDispatcher's compatibility for some basic signals ''' self.check_louie(receiver, signal, 'disconnect') if signal.endswith('.detected'): self.unbind(coherence_device_detection_completed=receiver) elif signal.endswith('.removed'): self.unbind(control_point_client_removed=receiver) else: raise Exception( f'Unknown signal {signal}, we cannot unbind that signal.')
class MediaServerClient(EventDispatcher, log.LogAble): ''' .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * The emitted events changed: - Coherence.UPnP.DeviceClient.detection_completed => device_client_detection_completed * Changed some class variable to benefit from the EventDispatcher's properties: - :attr:`detection_completed` - :attr:`content_directory` ''' logCategory = 'ms_client' detection_completed = Property(False) ''' To know whenever the device detection has completed. Defaults to `False` and it will be set automatically to `True` by the class method :meth:`service_notified`. ''' content_directory = Property(None) def __init__(self, device): log.LogAble.__init__(self) EventDispatcher.__init__(self) self.register_event('device_client_detection_completed', ) self.device = device self.device.bind(device_service_notified=self.service_notified) self.device_type = self.device.get_friendly_device_type() self.version = int(self.device.get_device_type_version()) self.icons = device.icons self.scheduled_recording = None self.connection_manager = None self.av_transport = None for service in self.device.get_services(): if service.get_type() in [ 'urn:schemas-upnp-org:service:ContentDirectory:1', 'urn:schemas-upnp-org:service:ContentDirectory:2' ]: self.content_directory = ContentDirectoryClient(service) if service.get_type() in [ 'urn:schemas-upnp-org:service:ConnectionManager:1', 'urn:schemas-upnp-org:service:ConnectionManager:2' ]: self.connection_manager = ConnectionManagerClient(service) if service.get_type() in [ 'urn:schemas-upnp-org:service:AVTransport:1', 'urn:schemas-upnp-org:service:AVTransport:2' ]: self.av_transport = AVTransportClient(service) if service.detection_completed: self.service_notified(service) self.info(f'MediaServer {device.get_friendly_name()}') if self.content_directory: self.info('ContentDirectory available') else: self.warning( 'ContentDirectory not available, device not implemented' ' properly according to the UPnP specification') return if self.connection_manager: self.info('ConnectionManager available') else: self.warning( 'ConnectionManager not available, device not implemented' ' properly according to the UPnP specification') return if self.av_transport: self.info('AVTransport (optional) available') if self.scheduled_recording: self.info('ScheduledRecording (optional) available') def remove(self): self.info('removal of MediaServerClient started') if self.content_directory is not None: self.content_directory.remove() if self.connection_manager is not None: self.connection_manager.remove() if self.av_transport is not None: self.av_transport.remove() if self.scheduled_recording is not None: self.scheduled_recording.remove() def service_notified(self, service): self.info(f'notified about {service}') if self.detection_completed: return if self.content_directory is not None: if not hasattr(self.content_directory.service, 'last_time_updated'): return if self.content_directory.service.last_time_updated is None: return if self.connection_manager is not None: if not hasattr(self.connection_manager.service, 'last_time_updated'): return if self.connection_manager.service.last_time_updated is None: return if self.av_transport is not None: if not hasattr(self.av_transport.service, 'last_time_updated'): return if self.av_transport.service.last_time_updated is None: return if self.scheduled_recording is not None: if not hasattr(self.scheduled_recording.service, 'last_time_updated'): return if self.scheduled_recording.service.last_time_updated is None: return self.detection_completed = True self.dispatch_event('device_client_detection_completed', client=self, udn=self.device.udn) self.info(f'detection_completed for {self}') def state_variable_change(self, variable, usn): self.info('%(name)r changed from %(old_value)r to %(value)r', vars(variable)) def print_results(self, results): self.info(f'results= {results}') def process_meta(self, results): for k, v in results.items(): dfr = self.content_directory.browse(k, 'BrowseMetadata') dfr.addCallback(self.print_results)
class Backend(EventDispatcher, log.LogAble, Plugin): '''In the :class:`Backend` class we initialize the very basic stuff needed to create a Backend and registers some basic events needed to be successfully detected by our server. The init method for a backend, should probably most of the time be overwritten when the init is done and send a signal to its device. We can send this signal via two methods, depending on the nature of our backend. For instance, if we want that the backend to be notified without fetching any data we could simply set the attribute :attr:`init_completed` equal to True at the end of our init method of the backend, but in some cases, we will want to send this signal after some deferred call returns a result...in that case we should process slightly differently, you can see how to do that at the end of the init method of the class :class:`~coherence.backends.models.stores.BackendBaseStore`. After that, the device will then setup, announce itself and should call to the backend's method :meth:`upnp_init`. .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * The emitted events changed: - Coherence.UPnP.Backend.init_completed => backend_init_completed * Added new event: backend_init_failed * Added new method :meth:`on_init_failed` * Moved class method `init_completed` to `on_init_completed` and added class variable :attr:`init_completed` .. note:: We can also use this init class to do whatever is necessary with the stuff we can extract from the config dict, connect maybe to an external data-source and start up the backend or if there are any UPnP service actions (Like maybe upnp_Browse for the CDS Browse action), that can't be handled by the service classes itself, or need some special adjustments for the backend, they probably will need to be defined into the method :meth:`__init__`. ''' logCategory = 'backend' implements = [] '''A list of the device classe like: ['MediaServer','MediaRenderer'] ''' init_completed = Property(False) '''To know whenever the backend init has completed. This has to be done in the actual backend, maybe it has to wait for an answer from an external data-source first...so...the backend should set this variable to `True`, then the method :meth:`on_init_completed` will be automatically triggered dispatching an event announcing that the backend has been initialized.''' def __init__(self, server, *args, **kwargs): ''' Args: server (object): This usually should be an instance of our main class :class:`~coherence.base.Coherence` (the UPnP device that's hosting our backend). *args (list): A list with extra arguments for the backend. This, must be implemented into the subclass (if needed). **kwargs (dict): An unpacked dictionary with the backend's configuration. ''' self.config = kwargs self.server = server EventDispatcher.__init__(self) log.LogAble.__init__(self) Plugin.__init__(self) self.register_event('backend_init_completed', 'backend_init_failed') def on_init_completed(self, *args, **kwargs): ''' Inform Coherence that this backend is ready for announcement. This method just accepts any form of arguments as we don't under which circumstances it is called. ''' self.dispatch_event('backend_init_completed', backend=self, **kwargs) def on_init_failed(self, *args, **kwargs): ''' Inform Coherence that this backend has failed. .. versionadded:: 0.9.0 ''' self.dispatch_event('backend_init_failed', backend=self, **kwargs) def upnp_init(self): ''' This method gets called after the device is fired, here all initializations of service related state variables should happen, as the services aren't available before that point. ''' pass
class Container(BackendItem): ''' Represents a backend item which will contains backend items inside. .. versionchanged:: 0.9.0 * Added static class variable :attr:`update_id` * Changed some variables to benefit from the EventDispatcher's properties: - :attr:`children` - :attr:`children_ids` - :attr:`children_by_external_id` - :attr:`parent` ''' update_id = Property(0) '''It represents the update id of thhe container. This should be incremented on every modification of the UPnP ContentDirectoryService Container, as we do in methods :meth:`add_child` and :meth:`remove_child`. ''' children = ListProperty([]) '''A list of the backend items.''' children_ids = DictProperty({}) '''A dictionary of the backend items by his id.''' children_by_external_id = DictProperty({}) '''A dictionary of the backend items by his external id.''' parent = Property(None) '''The parent object for this class.''' parent_id = -1 '''The id of the parent object. This will be automatically set whenever we set the attribute :attr:`parent`. ''' mimetype = 'directory' '''The mimetype variable describes the protocol info for the object. In a :class:`Container` this should be set to value `directory` or `root`. ''' def __init__(self, parent, title): BackendItem.__init__(self) self.parent = parent self.name = title self.sorted = False self.sorting_method = 'name' def on_parent(self, parent): if self.parent is not None: self.parent_id = self.parent.get_id() def register_child(self, child, external_id=None): id = self.store.append_item(child) child.url = self.store.urlbase + str(id) child.parent = self if external_id is not None: child.external_id = external_id self.children_by_external_id[external_id] = child def add_child(self, child, external_id=None, update=True): self.register_child(child, external_id) if self.children is None: self.children = [] self.children.append(child) self.sorted = False if update: self.update_id += 1 def remove_child(self, child, external_id=None, update=True): self.children.remove(child) self.store.remove_item(child) if update: self.update_id += 1 if external_id is not None: child.external_id = None del self.children_by_external_id[external_id] def get_children(self, start=0, end=0): if not self.sorted: self.children = sorted(self.children, key=attrgetter(self.sorting_method)) self.sorted = True if end != 0: return self.children[start:end] return self.children[start:] def get_child_count(self): if self.children is None: return 0 return len(self.children) def get_path(self): return self.store.urlbase + str(self.storage_id) def get_item(self): if self.item is None: self.item = DIDLLite.Container(self.storage_id, self.parent_id, self.name) self.item.childCount = len(self.children) return self.item def get_name(self): return self.name def get_id(self): return self.storage_id def get_update_id(self): return self.update_id
class WANConnectionDeviceClient(EventDispatcher, log.LogAble): ''' .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * The emitted events changed: - Coherence.UPnP.EmbeddedDeviceClient.detection_completed => embedded_device_client_detection_completed * Changed class variable :attr:`detection_completed` to benefit from the EventDispatcher's properties ''' logCategory = 'igd_client' detection_completed = Property(False) ''' To know whenever the wan device detection has completed. Defaults to `False` and it will be set automatically to `True` by the class method :meth:`service_notified`. ''' def __init__(self, device): log.LogAble.__init__(self) EventDispatcher.__init__(self) self.register_event('embedded_device_client_detection_completed') self.device = device self.device.bind(service_notified=self.service_notified) self.device_type = self.device.get_friendly_device_type() self.version = int(self.device.get_device_type_version()) self.icons = device.icons self.wan_ip_connection = None self.wan_ppp_connection = None for service in self.device.get_services(): if service.get_type() in [ 'urn:schemas-upnp-org:service:WANIPConnection:1' ]: self.wan_ip_connection = WANIPConnectionClient(service) if service.get_type() in [ 'urn:schemas-upnp-org:service:WANPPPConnection:1' ]: self.wan_ppp_connection = WANPPPConnectionClient(service) self.info(f'WANConnectionDevice {device.get_friendly_name()}') if self.wan_ip_connection: self.info('WANIPConnection service available') if self.wan_ppp_connection: self.info('WANPPPConnection service available') def remove(self): self.info('removal of WANConnectionDeviceClient started') if self.wan_ip_connection is not None: self.wan_ip_connection.remove() if self.wan_ppp_connection is not None: self.wan_ppp_connection.remove() def service_notified(self, service): self.info(f'Service {service} sent notification') if self.detection_completed: return if self.wan_ip_connection is not None: if not hasattr(self.wan_ip_connection.service, 'last_time_updated'): return if self.wan_ip_connection.service.last_time_updated is None: return if self.wan_ppp_connection is not None: if not hasattr(self.wan_ppp_connection.service, 'last_time_updated'): return if self.wan_ppp_connection.service.last_time_updated is None: return self.detection_completed = True self.dispatch_event('embedded_device_client_detection_completed', self)