Exemple #1
0
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)
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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
Exemple #6
0
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')
Exemple #7
0
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))
Exemple #8
0
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
Exemple #9
0
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
Exemple #10
0
class Dispatcher(EventDispatcher):
    p1 = Property(10)
    p2 = Property(20)
Exemple #11
0
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)
Exemple #13
0
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
Exemple #14
0
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)