Exemplo n.º 1
0
class BMDDiscovery(Listener):
    vidhubs = DictProperty()
    smart_views = DictProperty()
    smart_scopes = DictProperty()

    def __init__(self, mainloop, service_type='_blackmagic._tcp.local.'):
        super().__init__(mainloop, service_type)

    async def add_service_info(self, info, **kwargs):
        device_cls = info.properties.get('class')
        bmd_id = info.properties.get('unique id', '').upper()
        if device_cls == 'Videohub':
            self.vidhubs[bmd_id] = info
            kwargs.update({
                'class': device_cls,
                'id': bmd_id,
                'device_type': 'vidhub'
            })
        elif info.properties.get('class') == 'SmartView':
            if 'SmartScope' in info.properties.get('name', ''):
                self.smart_scopes[bmd_id] = info
                kwargs['device_type'] = 'smartscope'
            else:
                self.smart_views[bmd_id] = info
                kwargs['device_type'] = 'smartview'
            kwargs.update({'class': device_cls, 'id': bmd_id})
        await super().add_service_info(info, **kwargs)

    async def remove_service_info(self, info, **kwargs):
        device_cls = info.properties.get('class')
        bmd_id = info.properties.get('unique id', '').upper()
        if bmd_id in self.vidhubs and device_cls == 'Videohub':
            del self.vidhubs[bmd_id]
            kwargs.update({
                'class': device_cls,
                'id': bmd_id,
                'device_type': 'vidhub'
            })
        elif bmd_id in self.smart_views and device_cls == 'SmartView':
            del self.smart_views[bmd_id]
            kwargs.update({
                'class': device_cls,
                'id': bmd_id,
                'device_type': 'smartview'
            })
        elif bmd_id in self.smart_scopes and device_cls == 'SmartView':
            del self.smart_scopes[bmd_id]
            kwargs.update({
                'class': device_cls,
                'id': bmd_id,
                'device_type': 'smartscope'
            })
        await super().remove_service_info(info, **kwargs)
Exemplo n.º 2
0
class BMDDiscovery(Listener):
    """Zeroconf listener for Blackmagic devices

    Attributes:
        vidhubs (dict): Contains discovered Videohub devices.
            This :class:`~pydispatch.properties.DictProperty` can be used to
            subscribe to changes.
        smart_views (dict): Contains discovered SmartView devices.
            This :class:`~pydispatch.properties.DictProperty` can be used to
            subscribe to changes.
        smart_scopes (dict): Contains discovered SmartScope devices.
            This :class:`~pydispatch.properties.DictProperty` can be used to
            subscribe to changes.

    """
    vidhubs = DictProperty()
    smart_views = DictProperty()
    smart_scopes = DictProperty()
    def __init__(self, mainloop, service_type='_blackmagic._tcp.local.'):
        super().__init__(mainloop, service_type)
    async def add_service_info(self, info, **kwargs):
        device_cls = info.properties.get('class')
        bmd_id = info.properties.get('unique id', '').upper()
        if device_cls == 'Videohub':
            self.vidhubs[bmd_id] = info
            kwargs.update({'class':device_cls, 'id':bmd_id, 'device_type':'vidhub'})
        elif info.properties.get('class') == 'SmartView':
            if 'SmartScope' in info.properties.get('name', ''):
                self.smart_scopes[bmd_id] = info
                kwargs['device_type'] = 'smartscope'
            else:
                self.smart_views[bmd_id] = info
                kwargs['device_type'] = 'smartview'
            kwargs.update({'class':device_cls, 'id':bmd_id})
        await super().add_service_info(info, **kwargs)
    async def remove_service_info(self, info, **kwargs):
        device_cls = info.properties.get('class')
        bmd_id = info.properties.get('unique id', '').upper()
        if bmd_id in self.vidhubs and device_cls == 'Videohub':
            del self.vidhubs[bmd_id]
            kwargs.update({'class':device_cls, 'id':bmd_id, 'device_type':'vidhub'})
        elif bmd_id in self.smart_views and device_cls == 'SmartView':
            del self.smart_views[bmd_id]
            kwargs.update({'class':device_cls, 'id':bmd_id, 'device_type':'smartview'})
        elif bmd_id in self.smart_scopes and device_cls == 'SmartView':
            del self.smart_scopes[bmd_id]
            kwargs.update({'class':device_cls, 'id':bmd_id, 'device_type':'smartscope'})
        await super().remove_service_info(info, **kwargs)
Exemplo n.º 3
0
class VidhubNode(PubSubOscNode):
    _info_properties = [
        ('device_id', 'id'),
        ('device_name', 'name'),
        ('device_model', 'model'),
        ('device_version', 'version'),
        ('num_outputs', 'num_outputs'),
        ('num_inputs', 'num_inputs'),
    ]
    device_info = DictProperty()
    def __init__(self, vidhub, use_device_id=True):
        self.vidhub = vidhub
        self.use_device_id = use_device_id
        if use_device_id:
            name = self.vidhub.device_id
        else:
            name = self.vidhub.device_name
        super().__init__(name)
        info_node = self.add_child('info', cls=PubSubOscNode)
        for vidhub_attr, name in self._info_properties:
            info_node.add_child(
                name,
                cls=VidhubInfoNode,
                published_property=(self.vidhub, vidhub_attr),
            )
        self.label_node = self.add_child('labels', cls=PubSubOscNode)
        self.label_node.add_child('input', cls=VidhubLabelNode, vidhub=vidhub)
        self.label_node.add_child('output', cls=VidhubLabelNode, vidhub=vidhub)
        self.crosspoint_node = self.add_child('crosspoints', cls=VidhubCrosspointNode, vidhub=vidhub)
        self.preset_node = self.add_child('presets', cls=VidhubPresetGroupNode, vidhub=vidhub)
Exemplo n.º 4
0
class ServiceInfo(Dispatcher):
    properties = DictProperty()
    _attrs = ['type', 'name', 'server', 'address', 'port', 'properties']

    def __init__(self, **kwargs):
        for attr in self._attrs:
            setattr(self, attr, kwargs.get(attr))

    @classmethod
    def from_zc_info(cls, info):
        kwargs = {}
        for attr in cls._attrs:
            val = getattr(info, attr)
            if attr == 'properties':
                val = convert_bytes_dict(val)
            elif attr == 'address':
                val = ipaddress.ip_address(val)
            kwargs[attr] = val
        return cls(**kwargs)

    @property
    def id(self):
        return (self.type, self.name)  #, self.address, self.port)

    def to_zc_info(self):
        kwargs = {}
        for attr in self._attrs:
            val = getattr(self, attr)
            if attr == 'properties':
                val = convert_dict_bytes(val)
            elif attr == 'address':
                if isinstance(val, ipaddress.IPv4Interface):
                    val = val.ip.packed
                elif isinstance(val, ipaddress.IPv4Address):
                    val = val.packed
                else:
                    val = ipaddress.ip_address(val).packed
            kwargs[attr] = val
        type_ = kwargs.pop('type')
        name = kwargs.pop('name')
        return zeroconf.ServiceInfo(type_, name, **kwargs)

    def update(self, other):
        if self.properties == other.properties:
            return
        self.properties = other.properties.copy()

    def __hash__(self):
        return hash(self.id)

    def __eq__(self, other):
        return self.id == other.id

    def __repr__(self):
        return '<{self.__class__.__name__}> {self}'.format(self=self)

    def __str__(self):
        return '{self.name}: {self.type} ({self.address}:{self.port}), properties={self.properties}'.format(
            self=self)
    class A(Dispatcher):
        test_prop = Property()
        test_dict = DictProperty()
        test_list = ListProperty()

        def __init__(self):
            self.received = []
            self.bind(
                test_prop=self.on_test_prop,
                test_dict=self.on_test_dict,
                test_list=self.on_test_list,
            )

        def on_test_prop(self, *args, **kwargs):
            self.received.append('test_prop')

        def on_test_dict(self, *args, **kwargs):
            self.received.append('test_dict')

        def on_test_list(self, *args, **kwargs):
            self.received.append('test_list')
Exemplo n.º 6
0
class ServiceInfo(Dispatcher):
    """Container for Zeroconf service information

    Closely related to :class:`zeroconf.ServiceInfo`

    Attributes:
        type (str): Fully qualified service type
        name (str): Fully qualified service name
        server (str): Fully qualified name for service host
            (defaults to :attr:`name`)
        address (:class:`ipaddress.IPv4Address`): The service ip address
        port (int): The service port
        properties (dict): Custom properties for the service

    """
    properties = DictProperty()
    _attrs = ['type', 'name', 'server', 'address', 'port', 'properties']
    def __init__(self, **kwargs):
        for attr in self._attrs:
            setattr(self, attr, kwargs.get(attr))
    @classmethod
    def from_zc_info(cls, info):
        """Creates an instance from a :class:`zeroconf.ServiceInfo` object

        Arguments:
            info (:class:`zeroconf.ServiceInfo`):

        Returns:
            An instance of :class:`ServiceInfo`

        """
        kwargs = {}
        for attr in cls._attrs:
            val = getattr(info, attr)
            if attr == 'properties':
                val = convert_bytes_dict(val)
            elif attr == 'address':
                val = ipaddress.ip_address(val)
            kwargs[attr] = val
        return cls(**kwargs)
    @property
    def id(self):
        """Unique id for the service as a ``tuple`` of (:attr:`type`, :attr:`name`)
        """
        return (self.type, self.name)#, self.address, self.port)
    def to_zc_info(self):
        """Creates a copy as an instance of :class:`zeroconf.ServiceInfo`
        """
        kwargs = {}
        for attr in self._attrs:
            val = getattr(self, attr)
            if attr == 'properties':
                val = convert_dict_bytes(val)
            elif attr == 'address':
                if isinstance(val, ipaddress.IPv4Interface):
                    val = val.ip.packed
                elif isinstance(val, ipaddress.IPv4Address):
                    val = val.packed
                else:
                    val = ipaddress.ip_address(val).packed
            kwargs[attr] = val
        type_ = kwargs.pop('type')
        name = kwargs.pop('name')
        return zeroconf.ServiceInfo(type_, name, **kwargs)
    def update(self, other):
        """Updates the :attr:`properties` from another :class:`ServiceInfo` instance
        """
        if self.properties == other.properties:
            return
        self.properties = other.properties.copy()
    def __hash__(self):
        return hash(self.id)
    def __eq__(self, other):
        return self.id == other.id
    def __repr__(self):
        return '<{self.__class__.__name__}> {self}'.format(self=self)
    def __str__(self):
        return '{self.name}: {self.type} ({self.address}:{self.port}), properties={self.properties}'.format(self=self)
Exemplo n.º 7
0
class Listener(Dispatcher):
    """An async zeroconf service listener

    Allows async communication with :class:`zeroconf.Zeroconf` through
    :meth:`asyncio.AbstractEventLoop.run_in_executor` calls.

    Arguments:
        mainloop (:class:`asyncio.BaseEventLoop`): asyncio event loop instance
        service_type (str): The fully qualified service type name to subscribe to

    Attributes:
        services (dict): All services currently discovered as instances of
            :class:`ServiceInfo`. Stored using :attr:`ServiceInfo.id` as keys
        message_queue (:class:`asyncio.Queue`): Used to communicate actions and
            events with instances of :class:`Message`
        published_services (dict): Stores services that have been published
            using :meth:`publish_service` as :class:`ServiceInfo` instances.

    """
    _events_ = ['service_added', 'service_removed']
    services = DictProperty()
    def __init__(self, mainloop, service_type):
        self.mainloop = mainloop
        self.service_type = service_type
        self.running = False
        self.stopped = asyncio.Event()
        self.message_queue = asyncio.Queue()
        self.zeroconf = None
        self.published_services = {}
    async def start(self):
        """Starts the service listener

        Runs :class:`zeroconf.Zeroconf` in an :class:`~concurrent.futures.Executor`
        instance through `asyncio.AbstractEventLoop.run_in_executor`
        (see :meth:`run_zeroconf`).

        """
        await self.mainloop.run_in_executor(None, self.run_zeroconf)
        self.running = True
        self.run_future = asyncio.ensure_future(self.run(), loop=self.mainloop)
    async def run(self):
        """Main loop for communicating with :class:`zeroconf.Zeroconf`

        Waits for messages on the :attr:`message_queue` and processes them.
        The loop will exit if an object placed on the queue is not an instance
        of :class:`Message`.

        When the loop exits, the :class:`zeroconf.Zeroconf` instance will be
        closed.

        """
        while self.running:
            msg = await self.message_queue.get()
            self.message_queue.task_done()
            if not isinstance(msg, Message):
                self.running = False
                break
            elif isinstance(msg, AddedMessage):
                if msg.info.id in self.services:
                    self.services[msg.info.id].update(msg.info)
                else:
                    await self.add_service_info(msg.info)
            elif isinstance(msg, RemovedMessage):
                if msg.info.id in self.services:
                    await self.remove_service_info(msg.info)
            elif isinstance(msg, PublishMessage):
                if not ZEROCONF_AVAILABLE:
                    continue
                zc_info = msg.info.to_zc_info()
                await self.mainloop.run_in_executor(
                    None, self.zeroconf.register_service,
                    zc_info, msg.ttl,
                )
            elif isinstance(msg, UnPublishMessage):
                zc_info = msg.info.to_zc_info()
                await self.mainloop.run_in_executor(
                    None, self.zeroconf.unregister_service, zc_info,
                )
        await self.mainloop.run_in_executor(None, self.stop_zeroconf)
        self.stopped.set()
    async def stop(self):
        """Stops the loop in :meth:`run`
        """
        if not self.running:
            return
        self.message_queue.put_nowait(None)
        await self.stopped.wait()
    def run_zeroconf(self):
        """Starts :class:`zeroconf.Zeroconf` and :class:`zeroconf.ServiceBrowser` instances

        This is meant to be called inside of an :class:`concurrent.futures.Executor`
        and not used directly.

        """
        if not ZEROCONF_AVAILABLE:
            return
        self.zeroconf = zeroconf.Zeroconf()
        self.zeroconf.listener = self
        self.browser = zeroconf.ServiceBrowser(self.zeroconf, self.service_type, self)
    def stop_zeroconf(self):
        """Closes the :class:`zeroconf.Zeroconf` instance

        This is meant to be called inside of an :class:`concurrent.futures.Executor`
        and not used directly.

        """
        if self.zeroconf is None:
            return
        self.zeroconf.close()
    async def add_message(self, msg):
        """Adds a message to the :attr:`message_queue`

        Arguments:
            msg (:class:`Message`): Message to send

        """
        await self.message_queue.put(msg)
    async def add_service_info(self, info, **kwargs):
        self.services[info.id] = info
        self.emit('service_added', info, **kwargs)
    async def remove_service_info(self, info, **kwargs):
        del self.services[info.id]
        self.emit('service_removed', info, **kwargs)
    def remove_service(self, zc, type_, name):
        info = ServiceInfo(type=type_, name=name)
        msg = RemovedMessage(info)
        asyncio.run_coroutine_threadsafe(self.add_message(msg), loop=self.mainloop)
    def add_service(self, zc, type_, name):
        info = zc.get_service_info(type_, name)
        info = ServiceInfo.from_zc_info(info)
        msg = AddedMessage(info)
        asyncio.run_coroutine_threadsafe(self.add_message(msg), loop=self.mainloop)
    async def get_local_ifaces(self, refresh=False):
        ifaces = getattr(self, '_local_ifaces', None)
        if ifaces is not None and not refresh:
            return ifaces
        ifaces = self._local_ifaces = [iface for iface_name, iface in find_ip_addresses()]
        return ifaces
    async def get_local_hostname(self):
        name = getattr(self, '_local_hostname', None)
        if name is not None:
            return name
        name = None
        for iface in await self.get_local_ifaces():
            _name, srv = await self.mainloop.getnameinfo((str(iface.ip), 80))
            if _name is not None and _name != 'localhost':
                name = _name
                break
        if name is None:
            name = 'localhost'
        self._local_hostname = name
        return name
    async def publish_service(self, type_, port, name=None, addresses=None,
                              properties=None, ttl=PUBLISH_TTL):
        """Publishes a service on the network

        Arguments:
            type_ (str): Fully qualified service type
            port (int): The service port
            name (str, optional): Fully qualified service name. If not provided,
                this will be generated from the ``type_`` and the hostname
                detected by :meth:`get_local_hostname`
            addresses (optional): If provided, an ``iterable`` of IP addresses
                to publish. Can be :class:`ipaddress.IPv4Address` or any type
                that can be parsed by :func:`ipaddress.ip_address`
            properties (dict, optional): Custom properties for the service
            ttl (int, optional): The TTL value to publish.
                Defaults to :const:`PUBLISH_TTL`

        """
        hostname = await self.get_local_hostname()
        if name is None:
            name = '.'.join([hostname, type_])
        if addresses is None:
            addresses = await self.get_local_ifaces()
        if properties is None:
            properties = {}
        info_kwargs = {
            'type':type_,
            'port':port,
            'name':name,
            'properties':properties,
        }
        for addr in addresses:
            if not isinstance(addr, ipaddress.IPv4Address):
                addr = ipaddress.IPv4Address(addr)
            info_kwargs['address'] = addr
            info = ServiceInfo(**info_kwargs)
            if info.id not in self.published_services:
                self.published_services[info.id] = {}
            if info.address in self.published_services[info.id]:
                continue
            self.published_services[info.id][info.address] = info
            msg = PublishMessage(info, ttl)
            asyncio.run_coroutine_threadsafe(self.add_message(msg), loop=self.mainloop)
    async def unpublish_service(self, type_, port, name=None, addresses=None, properties=None):
        """Removes a service published through :meth:`publish_service`

        Arguments:
            type_ (str): Fully qualified service type
            port (int): The service port
            name (str, optional): Fully qualified service name. If not provided,
                this will be generated from the ``type_`` and the hostname
                detected by :meth:`get_local_hostname`
            addresses (optional): If provided, an ``iterable`` of IP addresses
                to unpublish. Can be :class:`ipaddress.IPv4Address` or any type
                that can be parsed by :func:`ipaddress.ip_address`
            properties (dict, optional): Custom properties for the service

        """
        hostname = await self.get_local_hostname()
        if name is None:
            name = '.'.join([hostname, type_])
        if addresses is None:
            addresses = await self.get_local_ifaces()
        if properties is None:
            properties = {}
        info_kwargs = {
            'type':type_,
            'port':port,
            'name':name,
            'properties':properties,
        }
        for addr in addresses:
            if not isinstance(addr, ipaddress.IPv4Address):
                addr = ipaddress.IPv4Address(addr)
            info_kwargs['address'] = addr
            info = ServiceInfo(**info_kwargs)
            if info.id not in self.published_services:
                continue
            if info.address not in self.published_services[info.id]:
                continue
            del self.published_services[info.id][info.address]
            msg = PublishMessage(info)
            asyncio.run_coroutine_threadsafe(self.add_message(msg), loop=self.mainloop)
Exemplo n.º 8
0
class Listener(Dispatcher):
    _events_ = ['service_added', 'service_removed']
    services = DictProperty()

    def __init__(self, mainloop, service_type):
        self.mainloop = mainloop
        self.service_type = service_type
        self.running = False
        self.stopped = asyncio.Event()
        self.message_queue = asyncio.Queue()
        self.zeroconf = None
        self.published_services = {}

    async def start(self):
        await self.mainloop.run_in_executor(None, self.run_zeroconf)
        self.running = True
        self.run_future = asyncio.ensure_future(self.run(), loop=self.mainloop)

    async def run(self):
        while self.running:
            msg = await self.message_queue.get()
            self.message_queue.task_done()
            if not isinstance(msg, Message):
                self.running = False
                break
            elif isinstance(msg, AddedMessage):
                if msg.info.id in self.services:
                    self.services[msg.info.id].update(msg.info)
                else:
                    await self.add_service_info(msg.info)
            elif isinstance(msg, RemovedMessage):
                if msg.info.id in self.services:
                    await self.remove_service_info(msg.info)
            elif isinstance(msg, PublishMessage):
                if not ZEROCONF_AVAILABLE:
                    continue
                zc_info = msg.info.to_zc_info()
                await self.mainloop.run_in_executor(
                    None,
                    self.zeroconf.register_service,
                    zc_info,
                    msg.ttl,
                )
        await self.mainloop.run_in_executor(None, self.stop_zeroconf)
        self.stopped.set()

    async def stop(self):
        if not self.running:
            return
        self.message_queue.put_nowait(None)
        await self.stopped.wait()

    def run_zeroconf(self):
        if not ZEROCONF_AVAILABLE:
            return
        self.zeroconf = zeroconf.Zeroconf()
        self.zeroconf.listener = self
        self.browser = zeroconf.ServiceBrowser(self.zeroconf,
                                               self.service_type, self)

    def stop_zeroconf(self):
        if self.zeroconf is None:
            return
        self.zeroconf.close()

    async def add_message(self, msg):
        await self.message_queue.put(msg)

    async def add_service_info(self, info, **kwargs):
        self.services[info.id] = info
        self.emit('service_added', info, **kwargs)

    async def remove_service_info(self, info, **kwargs):
        del self.services[info.id]
        self.emit('service_removed', info, **kwargs)

    def remove_service(self, zc, type_, name):
        info = ServiceInfo(type=type_, name=name)
        msg = RemovedMessage(info)
        asyncio.run_coroutine_threadsafe(self.add_message(msg),
                                         loop=self.mainloop)

    def add_service(self, zc, type_, name):
        info = zc.get_service_info(type_, name)
        info = ServiceInfo.from_zc_info(info)
        msg = AddedMessage(info)
        asyncio.run_coroutine_threadsafe(self.add_message(msg),
                                         loop=self.mainloop)

    async def get_local_ifaces(self, refresh=False):
        ifaces = getattr(self, '_local_ifaces', None)
        if ifaces is not None and not refresh:
            return ifaces
        ifaces = self._local_ifaces = [
            iface for iface_name, iface in find_ip_addresses()
        ]
        return ifaces

    async def get_local_hostname(self):
        name = getattr(self, '_local_hostname', None)
        if name is not None:
            return name
        name = None
        for iface in await self.get_local_ifaces():
            _name, srv = await self.mainloop.getnameinfo((str(iface.ip), 80))
            if _name is not None and _name != 'localhost':
                name = _name
                break
        if name is None:
            name = 'localhost'
        self._local_hostname = name
        return name

    async def publish_service(self,
                              type_,
                              port,
                              name=None,
                              addresses=None,
                              properties=None,
                              ttl=PUBLISH_TTL):
        hostname = await self.get_local_hostname()
        if name is None:
            name = '.'.join([hostname, type_])
        if addresses is None:
            addresses = await self.get_local_ifaces()
        if properties is None:
            properties = {}
        info_kwargs = {
            'type': type_,
            'port': port,
            'name': name,
            'properties': properties,
        }
        for addr in addresses:
            if not isinstance(addr, ipaddress.IPv4Address):
                addr = ipaddress.IPv4Address(addr)
            info_kwargs['address'] = addr
            info = ServiceInfo(**info_kwargs)
            if info.id not in self.published_services:
                self.published_services[info.id] = {}
            if info.address in self.published_services[info.id]:
                continue
            self.published_services[info.id][info.address] = info
            msg = PublishMessage(info, ttl)
            asyncio.run_coroutine_threadsafe(self.add_message(msg),
                                             loop=self.mainloop)
Exemplo n.º 9
0
class Preset(Dispatcher):
    name = Property()
    index = Property()
    crosspoints = DictProperty()
    active = Property(False)
    _events_ = ['on_preset_stored']

    def __init__(self, **kwargs):
        self.backend = kwargs.get('backend')
        self.index = kwargs.get('index')
        name = kwargs.get('name')
        if name is None:
            name = 'Preset {}'.format(self.index + 1)
        self.name = name
        self.crosspoints = kwargs.get('crosspoints', {})
        if self.backend.connected and self.backend.prelude_parsed:
            self.check_active()
        else:
            self.backend.bind(prelude_parsed=self.on_backend_ready)
        self.backend.bind(crosspoints=self.on_backend_crosspoints)
        self.bind(crosspoints=self.on_preset_crosspoints)

    async def store(self, outputs_to_store=None, clear_current=True):
        if outputs_to_store is None:
            outputs_to_store = range(self.backend.num_outputs)
        if clear_current:
            self.crosspoints = {}
        async with self.emission_lock('crosspoints'):
            for out_idx in outputs_to_store:
                self.crosspoints[out_idx] = self.backend.crosspoints[out_idx]
            self.active = True
        self.emit('on_preset_stored', preset=self)

    async def recall(self):
        if not len(self.crosspoints):
            return
        args = [(i, v) for i, v in self.crosspoints.items()]
        await self.backend.set_crosspoints(*args)

    def check_active(self):
        if not len(self.crosspoints):
            self.active = False
            return
        for out_idx, in_idx in self.crosspoints.items():
            in_idx = self.crosspoints[out_idx]
            if self.backend.crosspoints[out_idx] != in_idx:
                self.active = False
                return
        self.active = True

    def on_backend_ready(self, instance, value, **kwargs):
        if not value:
            return
        self.backend.unbind(self.on_backend_ready)
        self.check_active()

    def on_backend_crosspoints(self, instance, value, **kwargs):
        if not self.backend.prelude_parsed:
            return
        self.check_active()

    def on_preset_crosspoints(self, instance, value, **kwargs):
        if not len(self.crosspoints) or not self.backend.prelude_parsed:
            return
        self.check_active()
Exemplo n.º 10
0
 class A(Dispatcher):
     test_dict = DictProperty(copy_on_change=True)
     test_list = ListProperty(copy_on_change=True)
     no_cp_dict = DictProperty()
     no_cp_list = ListProperty()
Exemplo n.º 11
0
 class A(Dispatcher):
     test_dict = DictProperty({'defaultkey': 'defaultval'})
     test_list = ListProperty(['defaultitem'])
Exemplo n.º 12
0
 class A(Dispatcher):
     test_prop = Property()
     test_dict = DictProperty()
     test_list = ListProperty()
Exemplo n.º 13
0
 class A(Dispatcher):
     test_dict = DictProperty({'a': 1, 'b': 2, 'c': 3, 'd': 4})
Exemplo n.º 14
0
class Config(ConfigBase):
    DEFAULT_FILENAME = '~/vidhubcontrol.json'
    USE_DISCOVERY = True
    vidhubs = DictProperty()
    smartviews = DictProperty()
    smartscopes = DictProperty()
    _conf_attrs = ['vidhubs', 'smartscopes', 'smartviews']
    _device_type_map = {
        'vidhub': {
            'prop': 'vidhubs'
        },
        'smartview': {
            'prop': 'smartviews'
        },
        'smartscope': {
            'prop': 'smartscopes'
        },
    }
    loop = None

    def __init__(self, **kwargs):
        self.running = asyncio.Event()
        self.stopped = asyncio.Event()
        self.filename = kwargs.get('filename', self.DEFAULT_FILENAME)
        if self.loop is None:
            Config.loop = kwargs.get('loop', asyncio.get_event_loop())
        for key, d in self._device_type_map.items():
            items = kwargs.get(d['prop'], {})
            prop = getattr(self, d['prop'])
            for item_data in items.values():
                obj = d['cls'](**item_data)
                device_id = obj.device_id
                if device_id is None:
                    device_id = str(id(obj.backend))
                prop[device_id] = obj
                obj.backend.bind(device_id=self.on_backend_device_id)
                obj.bind(trigger_save=self.on_device_trigger_save)
        self.discovery_listener = None
        self.discovery_lock = asyncio.Lock()
        self._start_fut = None
        if self.USE_DISCOVERY:
            fut = asyncio.ensure_future(self.start(), loop=self.loop)
        else:
            fut = None
        self._start_fut = fut

    async def start(self):
        if not self.USE_DISCOVERY:
            self._start_fut = None
            self.running.set()
            return
        if self.discovery_listener is not None:
            await self.running.wait()
            return
        self.discovery_listener = BMDDiscovery(self.loop)
        self.discovery_listener.bind(
            service_added=self.on_discovery_service_added, )
        await self.discovery_listener.start()
        self._start_fut = None
        self.running.set()

    async def stop(self):
        self.running.clear()
        if self.discovery_listener is None:
            return
        await self.discovery_listener.stop()
        self.discovery_listener = None
        for vidhub in self.vidhubs.values():
            await vidhub.backend.disconnect()
        for smartview in self.smartviews.values():
            await smartview.backend.disconnect()
        for smartscope in self.smartscopes.values():
            await smartscope.backend.disconnect()
        self.stopped.set()
        Config.loop = None

    def build_backend(self, device_type, backend_name, **kwargs):
        prop = getattr(self, self._device_type_map[device_type]['prop'])
        for obj in prop.values():
            if obj.backend_name != backend_name:
                continue
            if kwargs.get('device_id') is not None and kwargs[
                    'device_id'] == obj.device_id:
                return obj.backend
            if kwargs.get('hostaddr'
                          ) is not None and kwargs['hostaddr'] == obj.hostaddr:
                return obj.backend
        cls = BACKENDS[device_type][backend_name]
        kwargs['event_loop'] = self.loop
        backend = cls(**kwargs)
        self.add_device(backend)
        return backend

    def add_vidhub(self, backend):
        return self.add_device(backend)

    def add_smartview(self, backend):
        return self.add_device(backend)

    def add_smartscope(self, backend):
        return self.add_device(backend)

    def add_device(self, backend):
        device_type = backend.device_type
        device_id = backend.device_id
        if device_id is None:
            device_id = str(id(backend))
        cls = self._device_type_map[device_type]['cls']
        prop = getattr(self, self._device_type_map[device_type]['prop'])
        obj = cls.from_existing(backend)
        prop[device_id] = obj
        obj.bind(trigger_save=self.on_device_trigger_save)
        backend.bind(device_id=self.on_backend_device_id)
        self.save()

    def on_backend_device_id(self, backend, value, **kwargs):
        if value is None:
            return
        prop = getattr(self,
                       self._device_type_map[backend.device_type]['prop'])
        obj = prop[str(id(backend))]
        obj.device_id = value
        del prop[str(id(backend))]
        if value in prop:
            self.save()
            return
        prop[value] = obj
        self.save()

    async def add_discovered_device(self, device_type, info, device_id):
        async with self.discovery_lock:
            prop = getattr(self, self._device_type_map[device_type]['prop'])
            cls = None
            for key, _cls in BACKENDS[device_type].items():
                if 'Telnet' in key:
                    cls = _cls
                    break
            if device_id in prop:
                return
            addr = str(info.address)
            backend = await cls.create_async(
                hostaddr=addr,
                hostport=int(info.port),
                event_loop=self.loop,
            )
            if backend is None:
                return
            if backend.device_id != device_id:
                await backend.disconnect()
                return
            self.add_device(backend)

    def on_discovery_service_added(self, info, **kwargs):
        if kwargs.get('class') not in ['Videohub', 'SmartView']:
            return
        device_type = kwargs.get('device_type')
        device_id = kwargs.get('id')
        if device_id is None:
            return
        prop = getattr(self, self._device_type_map[device_type]['prop'])
        if device_id in prop:
            return
        asyncio.ensure_future(
            self.add_discovered_device(device_type, info, device_id))

    def on_device_trigger_save(self, *args, **kwargs):
        self.save()

    def save(self, filename=None):
        if filename is not None:
            self.filename = filename
        else:
            filename = self.filename
        filename = os.path.expanduser(filename)
        data = self._get_conf_data()
        if not os.path.exists(os.path.dirname(filename)):
            os.makedirs(os.path.dirname(filename))
        s = jsonfactory.dumps(data, indent=4)
        with open(filename, 'w') as f:
            f.write(s)

    @classmethod
    def load(cls, filename=None, **kwargs):
        if filename is None:
            filename = cls.DEFAULT_FILENAME
        kwargs['filename'] = filename
        filename = os.path.expanduser(filename)
        if os.path.exists(filename):
            with open(filename, 'r') as f:
                s = f.read()
            kwargs.update(jsonfactory.loads(s))
        return cls(**kwargs)
Exemplo n.º 15
0
class Preset(Dispatcher):
    """Stores and recalls routing information

    Attributes:
        name: The name of the preset.
            This is a :class:`pydispatch.Property`
        index (int): The index of the preset as it is stored in the
            :attr:`~BackendBase.presets` container.
        crosspoints (dict): The crosspoints that this preset has stored.
            This is a :class:`~pydispatch.properties.DictProperty`
        active (bool): A flag indicating whether all of the crosspoints stored
            in this preset are currently active on the switcher.
            This is a :class:`pydispatch.Property`

    Events:
        on_preset_stored: Dispatched after the preset stores its state.

    """
    name = Property()
    index = Property()
    crosspoints = DictProperty()
    active = Property(False)
    _events_ = ['on_preset_stored']

    def __init__(self, **kwargs):
        self.backend = kwargs.get('backend')
        self.index = kwargs.get('index')
        name = kwargs.get('name')
        if name is None:
            name = 'Preset {}'.format(self.index + 1)
        self.name = name
        self.crosspoints = kwargs.get('crosspoints', {})
        if self.backend.connected and self.backend.prelude_parsed:
            self.check_active()
        else:
            self.backend.bind(prelude_parsed=self.on_backend_ready)
        self.backend.bind(crosspoints=self.on_backend_crosspoints)
        self.bind(crosspoints=self.on_preset_crosspoints)

    async def store(self, outputs_to_store=None, clear_current=True):
        if outputs_to_store is None:
            outputs_to_store = range(self.backend.num_outputs)
        if clear_current:
            self.crosspoints = {}
        async with self.emission_lock('crosspoints'):
            for out_idx in outputs_to_store:
                self.crosspoints[out_idx] = self.backend.crosspoints[out_idx]
            self.active = True
        self.emit('on_preset_stored', preset=self)

    async def recall(self):
        if not len(self.crosspoints):
            return
        args = [(i, v) for i, v in self.crosspoints.items()]
        await self.backend.set_crosspoints(*args)

    def check_active(self):
        if not len(self.crosspoints):
            self.active = False
            return
        for out_idx, in_idx in self.crosspoints.items():
            in_idx = self.crosspoints[out_idx]
            if self.backend.crosspoints[out_idx] != in_idx:
                self.active = False
                return
        self.active = True

    def on_backend_ready(self, instance, value, **kwargs):
        if not value:
            return
        self.backend.unbind(self.on_backend_ready)
        self.check_active()

    def on_backend_crosspoints(self, instance, value, **kwargs):
        if not self.backend.prelude_parsed:
            return
        self.check_active()

    def on_preset_crosspoints(self, instance, value, **kwargs):
        if not len(self.crosspoints) or not self.backend.prelude_parsed:
            return
        self.check_active()
Exemplo n.º 16
0
class OscInterface(Dispatcher):
    DEFAULT_HOSTPORT = 9000
    osc_dispatcher = Property()
    root_node = Property()
    iface_name = Property()
    hostport = Property(DEFAULT_HOSTPORT)
    hostiface = Property()
    config = Property()
    vidhubs = DictProperty(copy_on_change=True)
    vidhubs_by_name = DictProperty()
    def __init__(self, **kwargs):
        self.event_loop = kwargs.get('event_loop', asyncio.get_event_loop())
        self.bind(config=self.on_config)
        self.config = kwargs.get('config')
        self.iface_name = kwargs.get('iface_name')
        self.hostport = kwargs.get('hostport', 9000)
        hostaddr = kwargs.get('hostaddr')
        if self.iface_name is not None:
            for iface_name, iface in find_ip_addresses(self.hostiface):
                self.hostiface = iface
                break
        if self.hostiface is None:
            exclude_loopback=True
            if hostaddr is not None:
                hostaddr = ipaddress.ip_address(hostaddr)
                exclude_loopback=False
            for iface_name, iface in find_ip_addresses(exclude_loopback=exclude_loopback):
                if hostaddr is not None:
                    if hostaddr not in iface.network:
                        continue
                self.hostiface = iface
                self.iface_name = iface_name
        self.osc_dispatcher = OscDispatcher()
        self.server = None
        self.root_node = OscNode(
            'vidhubcontrol',
            osc_dispatcher=self.osc_dispatcher,
            event_loop=self.event_loop,
        )
        vidhub_node = self.root_node.add_child('vidhubs')
        vidhub_node.add_child(
            'by-id',
            cls=PubSubOscNode,
            published_property=(self, 'vidhubs'),
        )
        vidhub_node.add_child(
            'by-name',
            cls=PubSubOscNode,
            published_property=(self, 'vidhubs_by_name'),
        )
        # self.root_node.add_child('vidhubs/_update')
        # subscribe_node = self.root_node.add_child('vidhubs/_subscribe')
        # query_node = self.root_node.add_child('vidhubs/_query')
        # query_node.bind(on_message_received=self.on_vidhub_query_message)
    async def add_vidhub(self, vidhub):
        await vidhub.connect_fut
        if vidhub.device_id in self.vidhubs:
            return
        node = VidhubNode(vidhub, use_device_id=True)
        self.root_node.find('vidhubs/by-id').add_child('', node)
        node.osc_dispatcher = self.osc_dispatcher
        node = VidhubNode(vidhub, use_device_id=False)
        self.root_node.find('vidhubs/by-name').add_child('', node)
        node.osc_dispatcher = self.osc_dispatcher
        self.vidhubs[vidhub.device_id] = vidhub
        self.vidhubs_by_name[vidhub.device_name] = vidhub
        vidhub.bind(device_name=self.on_vidhub_name)
    async def start(self):
        if self.server is not None:
            await self.server.stop()
        if self.config is not None:
            if not self.config.running.is_set():
                await self.config.start()
            if self.config.USE_DISCOVERY:
                await self.publish_zeroconf_service()
        addr = (str(self.hostiface.ip), self.hostport)
        self.server = OSCUDPServer(addr, self.osc_dispatcher)
        await self.server.start()
    async def stop(self):
        if self.server is not None:
            await self.server.stop()
        self.server = None
    async def publish_zeroconf_service(self):
        await self.config.discovery_listener.publish_service(
            '_osc._udp.local.', self.hostport, properties={
                'txtvers':'1',
                'version':'1.1',
                'types':'ifsbrTF',
            }
        )
    def on_vidhub_name(self, instance, value, **kwargs):
        old = kwargs.get('old')
        with self.emission_lock('vidhubs_by_name'):
            del self.vidhubs_by_name[old]
            self.vidhubs_by_name[value] = instance
    def on_config(self, instance, config, **kwargs):
        if config is None:
            return
        self.update_config_vidhubs()
        config.bind(vidhubs=self.update_config_vidhubs)
    def update_config_vidhubs(self, *args, **kwargs):
        for vidhub_conf in self.config.vidhubs.values():
            if vidhub_conf.device_id is None:
                continue
            vidhub = vidhub_conf.backend
            asyncio.ensure_future(self.add_vidhub(vidhub), loop=self.event_loop)
Exemplo n.º 17
0
class OscNode(Dispatcher):
    name = Property()
    osc_address = Property()
    parent = Property()
    children = DictProperty()
    osc_dispatcher = Property()
    _events_ = ['on_message_received', 'on_tree_message_received']

    def __init__(self, name, parent=None, **kwargs):
        self.name = name
        self.bind(
            parent=self.on_parent,
            osc_dispatcher=self.on_osc_dispatcher,
        )
        self.parent = parent
        if self.parent is None:
            self.osc_address = self.build_osc_address()
            self.event_loop = kwargs.get('event_loop',
                                         asyncio.get_event_loop())
        else:
            self.event_loop = self.parent.event_loop
        osc_dispatcher = kwargs.get('osc_dispatcher')
        if osc_dispatcher is None:
            if self.parent is not None:
                osc_dispatcher = self.parent.osc_dispatcher
        self.osc_dispatcher = osc_dispatcher
        for ch_name, ckwargs in kwargs.get('children', {}).items():
            self.add_child(ch_name, **ckwargs)

    @classmethod
    def create_from_address(cls, osc_address, parent=None, **kwargs):
        name = osc_address.split('/')[0]
        child_address = '/'.join(osc_address.split('/')[1:])
        if len(child_address):
            if 'children' not in kwargs:
                kwargs['children'] = {}
            kwargs['children'][child_address] = {}
        root = cls(name, parent, **kwargs)
        return root, root.find(child_address)

    @property
    def root(self):
        p = self.parent
        if p is None:
            return self
        return p.root

    def find(self, osc_address):
        if osc_address.startswith('/'):
            return self.root.find(osc_address.lstrip('/'))
        if '/' not in osc_address:
            return self.children.get(osc_address)
        name = osc_address.split('/')[0]
        child = self.children.get(name)
        if child is not None:
            return child.find('/'.join(osc_address.split('/')[1:]))

    def build_osc_address(self, to_parent=None):
        path = self.name
        p = self.parent
        if p is to_parent:
            if to_parent is not None:
                return path
            else:
                return '/{}'.format(path)
        if to_parent is not None:
            return '/'.join([p.build_osc_address(to_parent), self.name])
        if p.osc_address is None:
            return None
        return '/'.join([p.osc_address, self.name])

    def add_child(self, name, node=None, cls=None, **kwargs):
        if cls is None:
            cls = OscNode
        if isinstance(node, OscNode):
            node.parent = self
            node.osc_dispatcher = self.osc_dispatcher
            child = tail = node
        elif '/' in name:
            if name.startswith('/'):
                return self.root.add_child(name.lstrip('/'), node, cls,
                                           **kwargs)
            tail = self.find(name)
            if tail is not None:
                return tail
            if name.split('/')[0] in self.children:
                child = self.children[name.split('/')[0]]
                name = '/'.join(name.split('/')[1:])
                tail = child.add_child(name, cls=cls, **kwargs)
            else:
                child, tail = self.create_from_address(name, self, **kwargs)
        else:
            if name in self.children:
                return self.children[name]
            child = cls(name, self, **kwargs)
            tail = child
        child.bind(on_tree_message_received=self.on_child_message_received)
        self.children[child.name] = child
        return tail

    def on_parent(self, instance, value, **kwargs):
        old = kwargs.get('old')
        if old is not None:
            old.unbind(self)
        self.osc_address = self.build_osc_address()
        if self.parent is not None:
            self.parent.bind(osc_address=self.on_parent_osc_address)

    def on_parent_osc_address(self, instance, value, **kwargs):
        if value is None:
            self.osc_address = None
        else:
            self.osc_address = self.build_osc_address()

    def on_osc_dispatcher(self, instance, obj, **kwargs):
        if obj is None:
            return
        obj.map(self.osc_address, self.on_osc_dispatcher_message)
        for child in self:
            child.osc_dispatcher = obj

    def ensure_message(self, client_address, *args, **kwargs):
        asyncio.ensure_future(
            self.send_message(client_address, *args, **kwargs),
            loop=self.event_loop,
        )

    async def send_message(self, client_address, *args, **kwargs):
        await self.osc_dispatcher.send_message(self, client_address, *args,
                                               **kwargs)

    def on_osc_dispatcher_message(self, osc_address, client_address,
                                  *messages):
        self.emit('on_message_received', self, client_address, *messages)
        self.emit('on_tree_message_received', self, client_address, *messages)

    def on_child_message_received(self, node, client_address, *messages):
        self.emit('on_tree_message_received', node, client_address, *messages)

    def __iter__(self):
        yield from self.children.values()

    def walk(self):
        yield self
        for child in self:
            yield from child.walk()

    def __repr__(self):
        return '<{self.__class__.__name__}>: {self}'.format(self=self)

    def __str__(self):
        return str(self.osc_address)
Exemplo n.º 18
0
class Config(ConfigBase):
    """Config store for devices

    Handles storage of device connection information and any user-defined values
    for the backends defined in the :doc:`backends module <backends>`. Data is stored
    in JSON format.

    During :meth:`start`, all previously stored devices will be loaded and begin
    communication. Devices are also discovered using `Zeroconf`_ through the
    :doc:`discovery module <discovery>`.

    Since each device has a unique id, network address changes (due to DHCP, etc)
    are handled appropriately.

    The configuration data is stored when:

    * A device is added or removed
    * A change is detected for a device's network address
    * Any user-defined device value changes (device name, presets, etc)

    The recommended method to start ``Config`` is through the :meth:`load_async`
    method.

    Example:
        .. code-block:: python

            import asyncio
            from vidhubcontrol.config import Config

            loop = asyncio.get_event_loop()
            conf = loop.run_until_complete(Config.load_async(loop=loop))

    Keyword Arguments:
        filename (:obj:`str`, optional): Filename to load/save config data to.
            If not given, defaults to :attr:`DEFAULT_FILENAME`
        loop: The :class:`EventLoop <asyncio.BaseEventLoop>` to use. If not
            given, the value from :func:`asyncio.get_event_loop` will be used.
        auto_start (bool): If ``True`` (default), the :meth:`start` method will
            be added to the asyncio event loop on initialization.

    Attributes:
        vidhubs (dict): A :class:`~pydispatch.properties.DictProperty` of
            :class:`VidhubConfig` instances using
            :attr:`~DeviceConfigBase.device_id` as keys
        smartviews (dict): A :class:`~pydispatch.properties.DictProperty` of
            :class:`SmartViewConfig` instances using
            :attr:`~DeviceConfigBase.device_id` as keys
        smartscopes (dict): A :class:`~pydispatch.properties.DictProperty` of
            :class:`SmartScopeConfig` instances using
            :attr:`~DeviceConfigBase.device_id` as keys

    .. autoattribute:: DEFAULT_FILENAME

    .. _Zeroconf: https://en.wikipedia.org/wiki/Zero-configuration_networking

    """
    DEFAULT_FILENAME = '~/vidhubcontrol.json'
    USE_DISCOVERY = True
    vidhubs = DictProperty()
    smartviews = DictProperty()
    smartscopes = DictProperty()
    _conf_attrs = ['vidhubs', 'smartscopes', 'smartviews']
    _device_type_map = {
        'vidhub':{'prop':'vidhubs'},
        'smartview':{'prop':'smartviews'},
        'smartscope':{'prop':'smartscopes'},
    }
    loop = None
    def __init__(self, **kwargs):
        self.start_kwargs = kwargs.copy()
        auto_start = kwargs.get('auto_start', True)
        self.starting = asyncio.Event()
        self.running = asyncio.Event()
        self.stopped = asyncio.Event()
        self.filename = kwargs.get('filename', self.DEFAULT_FILENAME)
        if 'loop' in kwargs:
            Config.loop = kwargs['loop']
        elif Config.loop is None:
            Config.loop = asyncio.get_event_loop()
        self.discovery_listener = None
        self.discovery_lock = asyncio.Lock()
        if auto_start:
            self._start_fut = asyncio.ensure_future(self.start(**kwargs), loop=self.loop)
        else:
            async def _start_fut(config):
                await config.running.wait()
            self._start_fut = asyncio.ensure_future(_start_fut(self), loop=self.loop)
    def id_for_device(self, device):
        if not isinstance(device, DeviceConfigBase):
            prop = getattr(self, self._device_type_map[device.device_type]['prop'])
            obj = None
            for _obj in prop.values():
                if _obj.backend is device:
                    obj = _obj
                    break
            if obj is None:
                raise Exception('Could not find device {!r}'.format(device))
            else:
                device = obj
        if device.device_id is not None:
            return device.device_id
        return str(id(device))
    async def _initialize_backends(self, **kwargs):
        """Creates and initializes device backends

        Keyword Arguments:
            vidhubs (dict): A ``dict`` containing the necessary data (as values)
                to create an instance of :class:`VidhubConfig`
            smartviews (dict): A ``dict`` containing the necessary data (as values)
                to create an instance of :class:`SmartViewConfig`
            smartscopes (dict): A ``dict`` containing the necessary data (as values)
                to create an instance of :class:`SmartScopeConfig`

        Note:
            All config object instances are created using the
            :meth:`DeviceConfigBase.create` classmethod.

        """
        async def _init_backend(prop, cls, **okwargs):
            okwargs['config'] = self
            obj = await cls.create(**okwargs)
            device_id = obj.device_id
            if device_id is None:
                device_id = self.id_for_device(obj)
            prop[device_id] = obj
            obj.bind(
                device_id=self.on_backend_device_id,
                trigger_save=self.on_device_trigger_save,
            )
        tasks = []
        for key, d in self._device_type_map.items():
            items = kwargs.get(d['prop'], {})
            prop = getattr(self, d['prop'])
            for item_data in items.values():
                okwargs = item_data.copy()
                task = _init_backend(prop, d['cls'], **okwargs)
                tasks.append(task)
        if len(tasks):
            await asyncio.wait(tasks)
    async def start(self, **kwargs):
        """Starts the device backends and discovery routines

        Keyword arguments passed to the initialization will be used here,
        but can be overridden in this method. They will also be passed to
        :meth:`_initialize_backends`.

        """
        if self.starting.is_set():
            await self.running.wait()
            return
        if self.running.is_set():
            return
        self.starting.set()

        self.start_kwargs.update(kwargs)
        kwargs = self.start_kwargs

        await self._initialize_backends(**kwargs)
        if not self.USE_DISCOVERY:
            self.starting.clear()
            self.running.set()
            return
        if self.discovery_listener is not None:
           await self.running.wait()
           return
        self.discovery_listener = BMDDiscovery(self.loop)
        self.discovery_listener.bind(
            service_added=self.on_discovery_service_added,
        )
        await self.discovery_listener.start()
        self.starting.clear()
        self.running.set()
    async def stop(self):
        """Stops all device backends and discovery routines
        """
        self.running.clear()
        if self.discovery_listener is None:
            return
        await self.discovery_listener.stop()
        self.discovery_listener = None
        for vidhub in self.vidhubs.values():
            await vidhub.backend.disconnect()
        for smartview in self.smartviews.values():
            await smartview.backend.disconnect()
        for smartscope in self.smartscopes.values():
            await smartscope.backend.disconnect()
        self.stopped.set()
        Config.loop = None
    async def build_backend(self, device_type, backend_name, **kwargs):
        """Creates a "backend" instance

        The supplied keyword arguments are used to create the instance object
        which will be created using its
        :meth:`~vidhubcontrol.backends.base.BackendBase.create` classmethod.

        The appropriate subclass of :class:`DeviceConfigBase` will be created
        and stored to the config using :meth:`add_device`.

        Arguments:
            device_type (str): Device type to create. Choices are "vidhub",
                "smartview", "smartscope"
            backend_name (str): The class name of the backend as found in
                :doc:`backends`

        Returns:
            An instance of a :class:`vidhubcontrol.backends.base.BackendBase`
            subclass

        """
        prop = getattr(self, self._device_type_map[device_type]['prop'])
        for obj in prop.values():
            if obj.backend_name != backend_name:
                continue
            if kwargs.get('device_id') is not None and kwargs['device_id'] == obj.device_id:
                return obj.backend
            if kwargs.get('hostaddr') is not None and kwargs['hostaddr'] == obj.hostaddr:
                return obj.backend
        cls = BACKENDS[device_type][backend_name]
        kwargs['event_loop'] = self.loop
        backend = await cls.create_async(**kwargs)
        await self.add_device(backend)
        return backend
    async def add_vidhub(self, backend):
        return await self.add_device(backend)
    async def add_smartview(self, backend):
        return await self.add_device(backend)
    async def add_smartscope(self, backend):
        return await self.add_device(backend)
    async def add_device(self, backend):
        """Adds a "backend" instance to the config

        A subclass of :class:`DeviceConfigBase` will be either created or updated
        from the given backend instance.

        If the ``device_id`` exists in the config, the
        :attr:`DeviceConfigBase.backend` value of the matching
        :class:`DeviceConfigBase` instance will be set to the given ``backend``.
        Otherwise, a new :class:`DeviceConfigBase` instance will be created using
        the :meth:`DeviceConfigBase.from_existing` classmethod.

        Arguments:
            backend: An instance of one of the subclasses of
                :class:`vidhubcontrol.backends.base.BackendBase` found in
                :doc:`backends`

        """
        device_type = backend.device_type
        cls = self._device_type_map[device_type]['cls']
        prop = getattr(self, self._device_type_map[device_type]['prop'])
        if backend.device_id is not None and backend.device_id in prop:
            obj = prop[backend.device_id]
            obj.backend = backend
        else:
            obj = await cls.from_existing(backend, config=self)
        if obj.device_id is None:
            obj.device_id = self.id_for_device(obj)
        prop[obj.device_id] = obj
        obj.bind(
            trigger_save=self.on_device_trigger_save,
            device_id=self.on_backend_device_id,
        )
        self.save()
    def on_backend_device_id(self, backend, value, **kwargs):
        if value is None:
            return
        old = kwargs.get('old')
        prop = getattr(self, self._device_type_map[backend.device_type]['prop'])
        if old in prop:
            del prop[old]
        if value in prop:
            self.save()
            return
        prop[value] = backend
        self.save()
    async def add_discovered_device(self, device_type, info, device_id):
        async with self.discovery_lock:
            prop = getattr(self, self._device_type_map[device_type]['prop'])
            cls = None
            for key, _cls in BACKENDS[device_type].items():
                if 'Telnet' in key:
                    cls = _cls
                    break
            hostaddr = str(info.address)
            hostport = int(info.port)
            if device_id in prop:
                obj = prop[device_id]
                if obj.hostaddr != hostaddr or obj.hostport != hostport:
                    await obj.reset_hostaddr(hostaddr, hostport)
                return
            backend = await cls.create_async(
                hostaddr=hostaddr,
                hostport=hostport,
                event_loop=self.loop,
            )
            if backend is None:
                return
            if backend.device_id != device_id:
                await backend.disconnect()
                return
            await self.add_device(backend)
    def on_discovery_service_added(self, info, **kwargs):
        if kwargs.get('class') not in ['Videohub', 'SmartView']:
            return
        device_type = kwargs.get('device_type')
        device_id = kwargs.get('id')
        if device_id is None:
            return
        prop = getattr(self, self._device_type_map[device_type]['prop'])
        if device_id in prop:
            obj = prop[device_id]
            if obj.backend is not None and obj.backend.connected:
                return
        asyncio.run_coroutine_threadsafe(self.add_discovered_device(device_type, info, device_id), loop=self.loop)
    def on_device_trigger_save(self, *args, **kwargs):
        self.save()
    def save(self, filename=None):
        """Saves the config data to the given filename

        Arguments:
            filename (:obj:`str`, optional): The filename to write config data to.
                If not supplied, the current :attr:`filename` is used.

        Notes:
            If the ``filename`` argument is provided, it will replace the
            existing :attr:`filename` value.

        """
        if filename is not None:
            self.filename = filename
        else:
            filename = self.filename
        filename = os.path.expanduser(filename)
        data = self._get_conf_data()
        if not os.path.exists(os.path.dirname(filename)):
            os.makedirs(os.path.dirname(filename))
        s = jsonfactory.dumps(data, indent=4)
        with open(filename, 'w') as f:
            f.write(s)
    @classmethod
    def _prepare_load_params(cls, filename=None, **kwargs):
        if filename is None:
            filename = cls.DEFAULT_FILENAME
        kwargs['filename'] = filename
        filename = os.path.expanduser(filename)
        if os.path.exists(filename):
            with open(filename, 'r') as f:
                s = f.read()
            kwargs.update(jsonfactory.loads(s))
        return kwargs
    @classmethod
    def load(cls, filename=None, **kwargs):
        """Creates a Config instance, loading data from the given filename

        Arguments:
            filename (:obj:`str`, optional): The filename to read config data
                from, defaults to :const:`Config.DEFAULT_FILENAME`

        Returns:
            A :class:`Config` instance

        """
        kwargs = cls._prepare_load_params(filename, **kwargs)
        return cls(**kwargs)
    @classmethod
    async def load_async(cls, filename=None, **kwargs):
        """Creates a Config instance, loading data from the given filename

        This coroutine method creates the ``Config`` instance and will ``await``
        all start-up coroutines and futures before returning.

        Arguments:
            filename (:obj:`str`, optional): The filename to read config data
                from, defaults to :attr:`DEFAULT_FILENAME`

        Returns:
            A :class:`Config` instance

        """
        kwargs = cls._prepare_load_params(filename, **kwargs)
        kwargs['auto_start'] = False
        config = cls(**kwargs)
        await config.start()
        await config._start_fut
        return config