Esempio n. 1
0
 class B(A):
     a = Property()
     b = Property()
     c = Property()
     _events_ = [
         'on_even_more_stuff', 'on_one_more_thing'
     ]
Esempio n. 2
0
 class A(Dispatcher):
     foo = Property()
     bar = Property()
     baz = Property()
     _events_ = [
         'on_stuff', 'on_more_stuff',
     ]
Esempio n. 3
0
class CameraParams(FakeParamBase):
    _NAME = device.CameraParams._NAME
    _prop_attrs = device.CameraParams._prop_attrs
    status = Property()
    menu_status = Property('Off')
    mode = Property()
    timecode = Property('00:00:00;00')

    def _get_now_tc(self):
        dt = datetime.datetime.now()
        microsecond = dt.microsecond / 1e6
        fr = int(microsecond * (30000 / 1001))
        tc_str = dt.strftime('%H:%M:%S')
        return f'{tc_str};{fr:02d}'

    def update_status_dict(self, data: dict):
        self.timecode = self._get_now_tc()
        super().update_status_dict(data)

    async def handle_web_button_event(self, request, payload):
        params = payload['Request']['Params']
        kind = params['Kind']
        btn = params['Button']
        if kind == 'Menu':
            if btn == 'Menu':
                if self.menu_status == 'Off':
                    self.menu_status = 'On'
                else:
                    self.menu_status = 'Off'
            elif btn == 'Cancel':
                self.menu_status = 'Off'
    class A(Dispatcher):
        test_prop = Property('default')
        name = Property('')
        something = Property()

        def __init__(self, name):
            self.name = name
            self.something = 'stuff'
Esempio n. 5
0
class TallyParams(FakeParamBase):
    _NAME = device.TallyParams._NAME
    _prop_attrs = device.TallyParams._prop_attrs

    tally_priority = Property('Camera')
    tally_status = Property('Off')

    async def handle_tally_request(self, request, payload):
        value = payload['Request']['Params']['Indication']
        self.tally_status = value
Esempio n. 6
0
 class A(Dispatcher):
     foo = Property()
     bar = Property()
     baz = Property()
     _events_ = [
         'on_stuff', 'on_more_stuff',
     ]
     def __init__(self, *args, **kwargs):
         self.foo = 1
         self.bar = 2
         self.baz = 3
Esempio n. 7
0
 class B(A):
     a = Property()
     b = Property()
     c = Property()
     _events_ = [
         'on_even_more_stuff', 'on_one_more_thing'
     ]
     def __init__(self, *args, **kwargs):
         super(B, self).__init__(*args, **kwargs)
         self.a = 1
         self.b = 2
         self.c = 3
    class A(Dispatcher):
        test_prop = Property('default')
        name = Property('')
        something = Property()

        def get_name_something(self):
            return '{} {}'.format(self.name, self.something)

        name_something = AliasProperty(get_name_something,
                                       bind=('name', 'something'))

        def __init__(self, name):
            self.name = name
            self.something = 'stuff'
Esempio n. 9
0
class SmartScopeMonitor(SmartViewMonitor):
    """A single instance of a monitor within a SmartScope device

    Attributes:
        scope_mode (str): The type of scope to display.  Choices are:
            'audio_dbfs', 'audio_dbvu', 'histogram', 'parade_rgb', 'parade_yuv',
            'video', 'vector_100', 'vector_75', 'waveform'.

    """
    scope_mode = Property()

    class PropertyChoices(SmartViewMonitor.PropertyChoices):
        scope_mode = {
            'audio_dbfs': 'AudioDbfs',
            'audio_dbvu': 'AudioDbvu',
            'histogram': 'Histogram',
            'parade_rgb': 'ParadeRGB',
            'parade_yuv': 'ParadeYUV',
            'video': 'Picture',
            'vector_100': 'Vector100',
            'vector_75': 'Vector75',
            'waveform': 'WaveformLuma',
        }
        _bind_properties = SmartViewMonitor.PropertyChoices._bind_properties + [
            'scope_mode',
        ]
Esempio n. 10
0
    class Publisher(Dispatcher):
        value = Property()
        other_value = Property()

        def __init__(self, root_node, osc_address):
            self.osc_address = osc_address
            self.random_values = get_random_values(8)
            self.msg_queue = asyncio.Queue()
            if root_node.osc_address == osc_address:
                self.osc_node = root_node
            else:
                self.osc_node = root_node.add_child(osc_address)
            self.subscribe_node = self.osc_node.add_child('_subscribe')
            self.query_node = self.osc_node.add_child('_query')
            self.list_node = self.osc_node.add_child('_list')
            for node in [
                    self.osc_node, self.subscribe_node, self.query_node,
                    self.list_node
            ]:
                node.bind(on_message_received=self.on_client_node_message)

        async def wait_for_response(self):
            msg = await self.msg_queue.get()
            self.msg_queue.task_done()
            return msg

        async def subscribe(self, server_addr):
            await self.subscribe_node.send_message(server_addr)
            msg = await self.wait_for_response()
            return msg

        async def query(self, server_addr, recursive=False):
            if recursive:
                await self.query_node.send_message(server_addr, 'recursive')
            else:
                await self.query_node.send_message(server_addr)
            msg = await self.wait_for_response()
            return msg

        def on_client_node_message(self, node, client_address, *messages):
            print('on_client_node_message: ', node, messages)
            self.msg_queue.put_nowait({
                'node': node,
                'client_address': client_address,
                'messages': messages,
            })
Esempio n. 11
0
class CameraParams(ParameterGroup):
    """Basic camera parameters
    """
    _NAME = 'camera'
    status: str | None = Property()
    """Camera status. One of
    ``['NoCard', 'Stop', 'Standby', 'Rec', 'RecPause']``
    """

    menu_status: bool = Property(False)
    """``True`` if the camera menu is open"""

    mode: str | None = Property()
    """Camera record / media mode. One of
    ``['Normal', 'Pre', 'Clip', 'Frame', 'Interval', 'Variable']``
    """

    timecode: str | None = Property()
    """The current timecode value"""

    _prop_attrs = [('status', 'Camera.Status'), ('mode', 'Camera.Mode'),
                   ('timecode', 'Camera.TC'),
                   ('menu_status', 'Camera.MenuStatus')]

    async def send_menu_button(self, value: MenuChoices):
        """Send a menu button event

        Arguments:
            value: The menu button type as a member of :class:`MenuChoices`
        """
        param = value.name.title()
        await self.device.send_web_button('Menu', param)

    def set_prop_from_api(self, prop_attr, value):
        if prop_attr == 'menu_status':
            if isinstance(value, str):
                value = 'On' in value
        super().set_prop_from_api(prop_attr, value)

    def on_prop(self, instance, value, **kwargs):
        prop = kwargs['property']
        if prop.name == 'timecode':
            return
        super().on_prop(instance, value, **kwargs)
Esempio n. 12
0
class BackendBase(Dispatcher):
    device_name = Property()
    device_model = Property()
    device_id = Property()
    device_version = Property()
    connected = Property(False)
    running = Property(False)
    prelude_parsed = Property(False)

    def __init__(self, **kwargs):
        self.device_name = kwargs.get('device_name')
        self.client = None
        self.event_loop = kwargs.get('event_loop', asyncio.get_event_loop())
        self.bind(device_id=self.on_device_id)
        if self.device_id is None:
            self.device_id = kwargs.get('device_id')

    @classmethod
    async def create_async(cls, **kwargs):
        obj = cls(**kwargs)
        await obj.connect_fut
        if not obj.connected or not obj.prelude_parsed:
            return None
        return obj

    async def connect(self):
        if self.connected:
            return self.client
        self.connected = True
        r = await self.do_connect()
        if r is False:
            self.connected = False
        else:
            if self.client is not None:
                self.client = r
        return r

    async def disconnect(self):
        if not self.connected:
            return
        await self.do_disconnect()
        self.client = None
        self.connected = False

    async def do_connect(self):
        raise NotImplementedError()

    async def do_disconnect(self):
        raise NotImplementedError()

    async def get_status(self):
        raise NotImplementedError()

    def on_device_id(self, instance, value, **kwargs):
        if value is None:
            return
        if self.device_name is None:
            self.device_name = value
        self.unbind(self.on_device_id)
Esempio n. 13
0
class IOPort(BasePort):
    inport = Property()
    outport = Property()

    async def _build_port(self):
        self.inport = InputPort(self.name)
        self.outport = OutputPort(self.name)

        await self.inport.open()
        await self.outport.open()
        return None

    async def _close_port(self):
        if self.inport is not None:
            await self.inport.close()
            self.inport = None
        if self.outport is not None:
            await self.outport.close()
            self.outport = None
Esempio n. 14
0
class VidhubSingleLabelNode(PubSubOscNode):
    value = Property()
    def __init__(self, name, parent, **kwargs):
        super().__init__(name, parent, **kwargs)
        self.index = int(name)
        self.published_property = (self, 'value')
        self.value = self.parent.vidhub_property[self.index]
        self.parent.vidhub.bind(**{self.parent.property_attr:self.on_vidhub_labels})
    def on_vidhub_labels(self, instance, value, **kwargs):
        self.value = value[self.index]
Esempio n. 15
0
class VidhubSingleCrosspointNode(PubSubOscNode):
    index = Property()
    value = Property()
    def __init__(self, name, parent, **kwargs):
        super().__init__(name, parent, **kwargs)
        self.published_property = (self, 'value')
        self.index = kwargs.get('index')
        self.value = self.parent.vidhub.crosspoints[self.index]
        self.parent.vidhub.bind(crosspoints=self.on_crosspoints)
    def on_crosspoints(self, instance, value, **kwargs):
        self.value = value[self.index]
    def on_osc_dispatcher_message(self, osc_address, client_address, *messages):
        if not len(messages):
            self.ensure_message(client_address, self.value)
        else:
            xpt = messages[0]
            asyncio.ensure_future(
                self.parent.vidhub.set_crosspoint(self.index, xpt),
                loop=self.parent.vidhub.event_loop,
            )
        super().on_osc_dispatcher_message(osc_address, client_address, *messages)
Esempio n. 16
0
class SmartViewBackendBase(BackendBase):
    num_monitors = Property()
    inverted = Property(False)
    monitors = ListProperty()
    monitor_cls = None
    device_type = 'smartview'
    _events_ = ['on_monitor_property_change']

    def __init__(self, **kwargs):
        self.bind(monitors=self._on_monitors)
        super().__init__(**kwargs)
        self.connect_fut = asyncio.ensure_future(self.connect(),
                                                 loop=self.event_loop)

    async def set_monitor_property(self, monitor, name, value):
        raise NotImplementedError()

    def get_monitor_cls(self):
        cls = self.monitor_cls
        if cls is None:
            cls = SmartViewMonitor
        return cls

    async def add_monitor(self, **kwargs):
        cls = self.get_monitor_cls()
        kwargs.setdefault('parent', self)
        kwargs.setdefault('index', len(self.monitors))
        monitor = cls(**kwargs)
        monitor.bind(on_property_change=self.on_monitor_prop)
        self.monitors.append(monitor)
        return monitor

    def on_monitor_prop(self, instance, name, value, **kwargs):
        kwargs['monitor'] = instance
        self.emit('on_monitor_property_change', self, name, value, **kwargs)

    def _on_monitors(self, *args, **kwargs):
        self.num_monitors = len(self.monitors)
Esempio n. 17
0
class DeviceConfigBase(ConfigBase):
    backend = Property()
    backend_name = Property()
    hostaddr = Property()
    hostport = Property(9990)
    device_name = Property()
    device_id = Property()
    _conf_attrs = [
        'backend_name',
        'hostaddr',
        'hostport',
        'device_name',
        'device_id',
    ]

    def __init__(self, **kwargs):
        for attr in self._conf_attrs:
            setattr(self, attr, kwargs.get(attr))
        self.backend = kwargs.get('backend')
        if self.backend is None:
            self.backend = self.build_backend(**self._get_conf_data())
        if self.backend.device_name != self.device_name:
            self.device_name = self.backend.device_name
        self.backend.bind(device_name=self.on_backend_prop_change)
        if hasattr(self.backend, 'hostport'):
            self.backend.bind(
                hostaddr=self.on_backend_prop_change,
                hostport=self.on_backend_prop_change,
            )

    @classmethod
    def from_existing(cls, backend, **kwargs):
        d = dict(
            backend=backend,
            backend_name=backend.__class__.__name__,
            hostaddr=getattr(backend, 'hostaddr', None),
            hostport=getattr(backend, 'hostport', None),
            device_name=backend.device_name,
            device_id=backend.device_id,
        )
        for key, val in d.items():
            kwargs.setdefault(key, val)
        return cls(**kwargs)

    def build_backend(self, cls=None, **kwargs):
        kwargs.setdefault('event_loop', Config.loop)
        if cls is None:
            cls = BACKENDS[self.device_type][self.backend_name]
        return cls(**kwargs)

    def on_backend_prop_change(self, instance, value, **kwargs):
        prop = kwargs.get('property')
        setattr(self, prop.name, value)
        self.emit('trigger_save')
Esempio n. 18
0
class SmartScopeMonitor(SmartViewMonitor):
    scope_mode = Property()

    class PropertyChoices(SmartViewMonitor.PropertyChoices):
        scope_mode = {
            'audio_dbfs': 'AudioDbfs',
            'audio_dbvu': 'AudioDbvu',
            'histogram': 'Histogram',
            'parade_rgb': 'ParadeRGB',
            'parade_yuv': 'ParadeYUV',
            'video': 'Picture',
            'vector_100': 'Vector100',
            'vector_75': 'Vector75',
            'waveform': 'WaveformLuma',
        }
        _bind_properties = SmartViewMonitor.PropertyChoices._bind_properties + [
            'scope_mode',
        ]
Esempio n. 19
0
class DeviceService(Dispatcher):
    published = Property(False)

    def __init__(self, device: FakeDevice, info: ServiceInfo):
        self.device = device
        self.__info = info

    @property
    def info(self):
        return self.__info

    @property
    def id(self):
        return (self.info.name, self.info.port)

    async def open(self):
        await self.device.open()

    async def close(self):
        self.published = False
        await self.device.close()
    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')
Esempio n. 21
0
class Screen(Dispatcher):
    """A group of :class:`Tally` displays

    Properties:
        scontrol(bytes): Any control data received for the screen

    :Events:
        .. event:: on_tally_added(tally: Tally)

            Fired when a new :class:`Tally` instance is added to the screen

        .. event:: on_tally_update(tally: Tally, props_changed: Set[str])

            Fired when any :class:`Tally` property changes. This is a
            retransmission of :event:`Tally.on_update`

        .. event:: on_tally_control(tally: Tally, data: bytes)

            Fired when control data is received for a :class:`Tally` object.
            This is a retransmission of :event:`Tally.on_control`

        .. event:: on_control(instance: Screen, data: bytes)

            Fired when control data is received for the :class:`Screen` itself

    .. versionadded:: 0.0.3

    """
    index: int
    """The screen index from 0 to 65534 (``0xFFFE``)
    """

    tallies: Dict[int, Tally]
    """Mapping of :class:`Tally` objects within the screen using their
    :attr:`~Tally.index` as keys
    """

    scontrol = Property(b'')

    _events_ = [
        'on_tally_added', 'on_tally_update', 'on_tally_control', 'on_control',
    ]
    def __init__(self, index_: int):
        self.__index = index_
        self.tallies = {}
        self.bind(scontrol=self._on_scontrol_prop)

    @property
    def index(self) -> int:
        """The screen index from 0 to 65534 (``0xFFFE``)
        """
        return self.__index

    @property
    def is_broadcast(self) -> bool:
        """``True`` if the screen is to be "broadcast", meaning sent to all
        :attr:`screen indices<.messages.Message.screen>`.

        (if the :attr:`index` is ``0xffff``)
        """
        return self.index == 0xffff

    @classmethod
    def broadcast(cls, **kwargs) -> 'Screen':
        """Create a :attr:`broadcast <is_broadcast>` :class:`Screen`

        (with :attr:`index` set to ``0xffff``)
        """
        return cls(0xffff, **kwargs)

    def broadcast_tally(self, **kwargs) -> Tally:
        """Create a temporary :class:`Tally` using :meth:`Tally.broadcast`

        Arguments:
            **kwargs: Keyword arguments to pass to the :class:`Tally` constructor

        Note:
            The tally object is not stored in :attr:`tallies` and no event
            propagation (:event:`on_tally_added`, :event:`on_tally_update`,
            :event:`on_tally_control`) is handled by the :class:`Screen`.
        """
        return Tally.broadcast(screen=self, **kwargs)

    def add_tally(self, index_: int, **kwargs) -> Tally:
        """Create a :class:`Tally` object and add it to :attr:`tallies`

        Arguments:
            index_: The tally :attr:`~Tally.index`
            **kwargs: Keyword arguments passed to create the tally instance

        Raises:
            KeyError: If the given ``index_`` already exists
        """
        if index_ in self:
            raise KeyError(f'Tally exists for index {index_}')
        tally = Tally(index_, screen=self, **kwargs)
        self._add_tally_obj(tally)
        return tally

    def get_or_create_tally(self, index_: int) -> Tally:
        """If a :class:`Tally` object matching the given index exists, return
        it. Otherwise create one and add it to :attr:`tallies`

        This method is similar to :meth:`add_tally` and it can be used to avoid
        exception handling. It does not however take keyword arguments and
        is only intended for object creation.
        """
        if index_ in self:
            return self[index_]
        return self.add_tally(index_)

    def _add_tally_obj(self, tally: Tally):
        self.tallies[tally.index] = tally
        if tally.is_broadcast:
            tally.bind(
                on_update=self._on_broadcast_tally_updated,
                on_control=self._on_broadcast_tally_updated,
            )
        else:
            tally.bind(
                on_update=self._on_tally_updated,
                on_control=self._on_tally_control,
            )
            self.emit('on_tally_added', tally)

    def update_from_message(self, msg: 'tslumd.messages.Message'):
        """Handle an incoming :class:`~.Message`
        """
        if msg.screen != self.index and not msg.broadcast:
            return
        if msg.type == MessageType.control:
            self.scontrol = msg.scontrol
        else:
            for dmsg in msg.displays:
                self.handle_dmsg(dmsg)

    def handle_dmsg(self, dmsg: 'tslumd.messages.Display'):
        if dmsg.is_broadcast:
            for tally in self:
                tally.update_from_display(dmsg)
        else:
            if dmsg.index not in self:
                tally = Tally.from_display(dmsg, screen=self)
                self._add_tally_obj(tally)
                if dmsg.type == MessageType.control:
                    tally.emit('on_control', tally, tally.control)
            else:
                tally = self[dmsg.index]
                tally.update_from_display(dmsg)

    def _on_tally_updated(self, *args, **kwargs):
        self.emit('on_tally_update', *args, **kwargs)

    def _on_tally_control(self, *args, **kwargs):
        self.emit('on_tally_control', *args, **kwargs)

    def _on_scontrol_prop(self, instance: 'Screen', value: bytes, **kwargs):
        if not len(value):
            return
        self.emit('on_control', self, value)

    def __getitem__(self, key: int) -> Tally:
        return self.tallies[key]

    def __contains__(self, key: int) -> bool:
        return key in self.tallies

    def keys(self) -> Iterable[int]:
        yield from sorted((k for k in self.tallies.keys() if k != 0xffff))

    def values(self) -> Iterable[Tally]:
        for key in self.keys():
            yield self[key]

    def items(self) -> Iterable[Tuple[int, Tally]]:
        for key in self.keys():
            yield key, self[key]

    def __iter__(self) -> Iterable[Tally]:
        yield from self.values()

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

    def __str__(self):
        return f'{self.index}'
Esempio n. 22
0
class TelnetBackendBase(object):
    """Mix-in class for backends implementing telnet

    Attributes:
        hostaddr (str): IPv4 address of the device
        hostport (int): Port address of the device
        read_enabled (bool): Internal flag to keep the :meth:`read_loop` running
        rx_bfr (bytes): Data received from the device to be parsed
        client: Instance of :class:`vidhubcontrol.aiotelnetlib._Telnet`

    """
    hostaddr = Property()
    hostport = Property()
    def _telnet_init(self, **kwargs):
        self.read_enabled = False
        self.current_section = None
        self.ack_or_nak = None
        self.read_coro = None
        self.hostaddr = kwargs.get('hostaddr')
        self.hostport = kwargs.get('hostport', self.DEFAULT_PORT)
        self.rx_bfr = b''
    async def read_loop(self):
        while self.read_enabled:
            try:
                await self.client.wait_for_data()
            except Exception as e:
                logger.error(e)
                await self._close_client()
                self._catch_exception(e)
                return
            if not self.read_enabled:
                break
            try:
                rx_bfr = await self.client.read_very_eager()
            except Exception as e:
                logger.error(e)
                await self._close_client()
                self._catch_exception(e)
                return
            if len(rx_bfr):
                self.rx_bfr += rx_bfr
                logger.debug(self.rx_bfr.decode('UTF-8'))
                await self.parse_rx_bfr()
                self.rx_bfr = b''
    async def send_to_client(self, data):
        if not self.connected:
            c = await self.connect()
        c = self.client
        if not c:
            return
        s = '\n'.join(['---> {}'.format(line) for line in data.decode('UTF-8').splitlines()])
        logger.debug(s)
        try:
            await c.write(data)
        except Exception as e:
            logger.error(e)
            await self._close_client()
            self._catch_exception(e)
    async def do_connect(self):
        self.ack_or_nak_event = asyncio.Event()
        self.response_ready = asyncio.Event()
        self.rx_bfr = b''
        logger.debug('connecting')
        try:
            c = self.client = await aiotelnetlib.Telnet(self.hostaddr, self.hostport)
        except OSError as e:
            logger.error(e)
            self.client = None
            self._catch_exception(e)
            return False
        self.prelude_parsed = False
        self.read_enabled = True
        self.read_coro = asyncio.ensure_future(self.read_loop(), loop=self.event_loop)
        await self.wait_for_response(prelude=True)
        logger.debug('prelude parsed')
        return c
    async def _close_client(self):
        logger.info('close_client')
        self.read_enabled = False
        self.response_ready.set()
        if self.client is not None:
            try:
                await self.client.close_async()
            except Exception as e:
                logger.error(e)
            self.client = None
        self.connected = False
    async def do_disconnect(self):
        logger.debug('disconnecting')
        self.read_enabled = False
        if self.client is not None:
            await self.client.close_async()
        if self.read_coro is not None:
            await asyncio.wait([self.read_coro], loop=self.event_loop)
            self.read_coro = None
        self.client = None
        logger.debug('disconnected')
    async def wait_for_response(self, prelude=False):
        logger.debug('wait_for_response...')
        while self.read_enabled:
            await self.response_ready.wait()
            self.response_ready.clear()
            if prelude:
                if self.prelude_parsed:
                    return
                else:
                    await asyncio.sleep(.1)
            if self.ack_or_nak is not None:
                resp = self.ack_or_nak
                self.ack_or_nak_event.clear()
                logger.debug('ack_or_nak: {}'.format(resp))
                self.ack_or_nak = None
                return resp
    async def wait_for_ack_or_nak(self):
        logger.debug('wait_for_ack_or_nak...')
        await self.ack_or_nak_event.wait()
        resp = self.ack_or_nak
        self.ack_or_nak = None
        self.ack_or_nak_event.clear()
        return resp.startswith('ACK')
Esempio n. 23
0
class Tally(Dispatcher):
    """A single tally object

    Properties:
        rh_tally (TallyColor): State of the :term:`right-hand tally <rh_tally>` indicator
        txt_tally (TallyColor): State of the :term:`text tally <txt_tally>` indicator
        lh_tally (TallyColor): State of the :term:`left-hand tally <lh_tally>` indicator
        brightness (int): Tally indicator brightness from 0 to 3
        text (str): Text to display
        control (bytes): Any control data received for the tally indicator
        normalized_brightness (float): The :attr:`brightness` value normalized
            as a float from ``0.0`` to ``1.0``

    :Events:
        .. event:: on_update(instance: Tally, props_changed: Set[str])

            Fired when any property changes

        .. event:: on_control(instance: Tally, data: bytes)

            Fired when control data is received for the tally indicator

    .. versionadded:: 0.0.2
        The :event:`on_control` event

    .. versionchanged:: 0.0.5
        Added container emulation
    """
    screen: Optional['Screen']
    """The parent :class:`Screen` this tally belongs to

    .. versionadded:: 0.0.3
    """
    rh_tally = Property(TallyColor.OFF)
    txt_tally = Property(TallyColor.OFF)
    lh_tally = Property(TallyColor.OFF)
    brightness = Property(3)
    normalized_brightness = Property(1.)
    text = Property('')
    control = Property(b'')
    _events_ = ['on_update', 'on_control']
    _prop_attrs = ('rh_tally', 'txt_tally', 'lh_tally', 'brightness', 'text', 'control')
    def __init__(self, index_, **kwargs):
        self.screen = kwargs.get('screen')
        self.__index = index_
        if self.screen is not None:
            self.__id = (self.screen.index, self.__index)
        else:
            self.__id = None
        self._updating_props = False
        self.update(**kwargs)
        self.bind(**{prop:self._on_prop_changed for prop in self._prop_attrs})

    @property
    def index(self) -> int:
        """Index of the tally object from 0 to 65534 (``0xfffe``)
        """
        return self.__index

    @property
    def id(self) -> TallyKey:
        """A key to uniquely identify a :class:`Tally` / :class:`Screen`
        combination.

        Tuple of (:attr:`Screen.index`, :attr:`Tally.index`)

        Raises:
            ValueError: If the :attr:`Tally.screen` is ``None``

        .. versionadded:: 0.0.3
        """
        if self.__id is None:
            raise ValueError(f'Cannot create id for Tally without a screen ({self!r})')
        return self.__id

    @property
    def is_broadcast(self) -> bool:
        """``True`` if the tally is to be "broadcast", meaning sent to all
        :attr:`display indices<.messages.Display.index>`.

        (if the :attr:`index` is ``0xffff``)

        .. versionadded:: 0.0.2
        """
        return self.index == 0xffff

    @classmethod
    def broadcast(cls, **kwargs) -> 'Tally':
        """Create a :attr:`broadcast <is_broadcast>` tally

        (with :attr:`index` set to ``0xffff``)

        .. versionadded:: 0.0.2
        """
        return cls(0xffff, **kwargs)

    @classmethod
    def from_display(cls, display: 'tslumd.Display', **kwargs) -> 'Tally':
        """Create an instance from the given :class:`~.messages.Display` object
        """
        attrs = set(cls._prop_attrs)
        if display.type.name == 'control':
            attrs.discard('text')
        else:
            attrs.discard('control')
        kw = kwargs.copy()
        kw.update({attr:getattr(display, attr) for attr in cls._prop_attrs})
        return cls(display.index, **kw)

    def set_color(self, tally_type: StrOrTallyType, color: StrOrTallyColor):
        """Set the color property (or properties) for the given TallyType

        Sets the :attr:`rh_tally`, :attr:`txt_tally` or :attr:`lh_tally`
        properties matching the :class:`~.common.TallyType` value(s).

        If the given tally_type is a combination of tally types, all of the
        matched attributes will be set to the given color.

        Arguments:
            tally_type (TallyType or str): The :class:`~.common.TallyType` member(s)
                to set. Multiple types can be specified using
                bitwise ``|`` operators.

                If the argument is a string, it should be formatted as shown in
                :meth:`.TallyType.from_str`
            color (TallyColor or str): The :class:`~.common.TallyColor` to set, or the
                name as a string


        >>> from tslumd import Tally, TallyType, TallyColor
        >>> tally = Tally(0)
        >>> tally.set_color(TallyType.rh_tally, TallyColor.RED)
        >>> tally.rh_tally
        <TallyColor.RED: 1>
        >>> tally.set_color('lh_tally', 'green')
        >>> tally.lh_tally
        <TallyColor.GREEN: 2>
        >>> tally.set_color('rh_tally|txt_tally', 'green')
        >>> tally.rh_tally
        <TallyColor.GREEN: 2>
        >>> tally.txt_tally
        <TallyColor.GREEN: 2>
        >>> tally.set_color('all', 'off')
        >>> tally.rh_tally
        <TallyColor.OFF: 0>
        >>> tally.txt_tally
        <TallyColor.OFF: 0>
        >>> tally.lh_tally
        <TallyColor.OFF: 0>

        .. versionadded:: 0.0.4

        .. versionchanged:: 0.0.5
            Allow string arguments and multiple tally_type members
        """
        self[tally_type] = color

    def get_color(self, tally_type: StrOrTallyType) -> TallyColor:
        """Get the color of the given tally_type

        If tally_type is a combination of tally types, the color returned will
        be a combination all of the matched color properties.

        Arguments:
            tally_type (TallyType or str): :class:`~.common.TallyType` member(s)
                to get the color values from.

                If the argument is a string, it should be formatted as shown in
                :meth:`.TallyType.from_str`


        >>> tally = Tally(0)
        >>> tally.get_color('rh_tally')
        <TallyColor.OFF: 0>
        >>> tally.set_color('rh_tally', 'red')
        >>> tally.get_color('rh_tally')
        <TallyColor.RED: 1>
        >>> tally.set_color('txt_tally', 'red')
        >>> tally.get_color('rh_tally|txt_tally')
        <TallyColor.RED: 1>
        >>> tally.get_color('all')
        <TallyColor.RED: 1>
        >>> tally.set_color('lh_tally', 'green')
        >>> tally.get_color('lh_tally')
        <TallyColor.GREEN: 2>
        >>> tally.get_color('all')
        <TallyColor.AMBER: 3>

        .. versionadded:: 0.0.5
        """
        return self[tally_type]

    def merge_color(self, tally_type: TallyType, color: TallyColor):
        """Merge the color property (or properties) for the given TallyType
        using the :meth:`set_color` method

        Combines the existing color value with the one provided using a bitwise
        ``|`` (or) operation

        Arguments:
            tally_type (TallyType): The :class:`~.common.TallyType` member(s)
                to merge. Multiple types can be specified using
                bitwise ``|`` operators.
            color (TallyColor): The :class:`~.common.TallyColor` to merge

        .. versionadded:: 0.0.4
        """
        for ttype in tally_type:
            cur_color = self[ttype]
            new_color = cur_color | color
            if new_color == cur_color:
                continue
            self[ttype] = new_color

    def merge(self, other: 'Tally', tally_type: Optional[TallyType] = TallyType.all_tally):
        """Merge the color(s) from another Tally instance into this one using
        the :meth:`merge_color` method

        Arguments:
            other (Tally): The Tally instance to merge with
            tally_type (TallyType, optional): The :class:`~.common.TallyType`
                member(s) to merge. Multiple types can be specified using
                bitwise ``|`` operators.
                Default is :attr:`~.common.TallyType.all_tally` (all three types)

        .. versionadded:: 0.0.4
        """
        for ttype in tally_type:
            color = other[ttype]
            self.merge_color(ttype, color)

    def update(self, **kwargs) -> Set[str]:
        """Update any known properties from the given keyword-arguments

        Returns:
            set: The property names, if any, that changed
        """
        log_updated = kwargs.pop('LOG_UPDATED', False)
        props_changed = set()
        self._updating_props = True
        for attr in self._prop_attrs:
            if attr not in kwargs:
                continue
            val = kwargs[attr]
            if attr == 'control' and val != b'':
                if self.control == val:
                    # logger.debug(f'resetting control, {val=}, {self.control=}')
                    self.control = b''
            if getattr(self, attr) == val:
                continue
            props_changed.add(attr)
            setattr(self, attr, val)
            if attr == 'brightness':
                self.normalized_brightness = val / 3
            if log_updated:
                logger.debug(f'{self!r}.{attr} = {val!r}')
        self._updating_props = False
        if 'control' in props_changed and self.control != b'':
            self.emit('on_control', self, self.control)
        if len(props_changed):
            self.emit('on_update', self, props_changed)
        return props_changed

    def update_from_display(self, display: 'tslumd.messages.Display') -> Set[str]:
        """Update this instance from the values of the given
        :class:`~.messages.Display` object

        Returns:
            set: The property names, if any, that changed
        """
        attrs = set(self._prop_attrs)
        is_control = display.type.name == 'control'
        if is_control:
            attrs.discard('text')
        else:
            attrs.discard('control')
        kw = {attr:getattr(display, attr) for attr in attrs}
        kw['LOG_UPDATED'] = True
        props_changed = self.update(**kw)
        return props_changed

    def to_dict(self) -> Dict:
        """Serialize to a :class:`dict`
        """
        d = {attr:getattr(self, attr) for attr in self._prop_attrs}
        d['index'] = self.index
        if self.screen is None:
            d['id'] = None
        else:
            d['id'] = self.id
        return d

    # def to_display(self) -> 'tslumd.messages.Display':
    #     """Create a :class:`~.messages.Display` from this instance
    #     """
    #     kw = self.to_dict()
    #     return Display(**kw)

    def _on_prop_changed(self, instance, value, **kwargs):
        if self._updating_props:
            return
        prop = kwargs['property']
        if prop.name == 'control' and value != b'':
            self.emit('on_control', self, value)
        if prop.name == 'brightness':
            self.normalized_brightness = value / 3
        self.emit('on_update', self, set([prop.name]))

    def __getitem__(self, key: StrOrTallyType) -> TallyColor:
        if not isinstance(key, TallyType):
            key = TallyType.from_str(key)
        if key.is_iterable:
            color = TallyColor.OFF
            for tt in key:
                color |= getattr(self, tt.name)
            return color
        return getattr(self, key.name)

    def __setitem__(self, key: StrOrTallyType, value: StrOrTallyColor):
        if not isinstance(key, TallyType):
            key = TallyType.from_str(key)
        if not isinstance(value, TallyColor):
            value = TallyColor.from_str(value)
        if key.is_iterable:
            for tt in key:
                setattr(self, tt.name, value)
        else:
            setattr(self, key.name, value)

    def __eq__(self, other):
        if not isinstance(other, Tally):
            return NotImplemented
        return self.to_dict() == other.to_dict()

    def __ne__(self, other):
        if not isinstance(other, Tally):
            return NotImplemented
        return self.to_dict() != other.to_dict()

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

    def __str__(self):
        if self.__id is None:
            return f'{self.index} - "{self.text}"'
        return f'{self.id} - "{self.text}"'
Esempio n. 24
0
class VidHubView(SofiDataId):
    selected_output = Property(0)
    vidhub = Property()
    edit_enable = Property(False)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.input_buttons = None
        self.output_buttons = None
        self.preset_buttons = None
        self.widget = Container(attrs=self.get_data_id_attr())
        self.edit_widget = InlineTextEdit(app=self.app)
        self.edit_widget.bind(
            value=self.on_edit_widget_value,
            hidden=self.on_edit_widget_hidden,
        )
        self.edit_vidhub_btn = Button(text='Rename', ident='edit_vidhub_btn')
        self.connection_icon = None
        self.connection_btn = None
        self.bind(
            vidhub=self.on_vidhub,
            edit_enable=self.on_edit_enable,
        )
        self.vidhub = kwargs.get('vidhub')

    def on_vidhub(self, instance, value, **kwargs):
        old = kwargs.get('old')
        if old is not None:
            old.unbind(self)
        for attr in ['input_buttons', 'output_buttons', 'preset_buttons']:
            obj = getattr(self, attr)
            if obj is not None:
                obj.remove()
            setattr(self, attr, None)
        del self.widget._children[:]
        self.edit_enable = False
        self.connection_icon = None
        self.connection_btn = None
        if self.vidhub is not None:
            self.vidhub.bind(
                device_name=self.on_vidhub_device_name,
                connected=self.on_vidhub_connected,
            )
            self.build_view()

    def on_vidhub_connected(self, instance, value, **kwargs):
        if self.connection_icon is None:
            return
        states = {
            True: 'glyphicon glyphicon-ok-circle',
            False: 'glyphicon glyphicon-ban-circle'
        }
        self.connection_icon.cl = states[value]

        states = {True: 'btn btn-success', False: 'btn btn-warning'}
        self.connection_btn._children[
            0] = 'Connect ' if not value else 'Disconnect '

        selector = '#{}'.format(self.connection_btn.ident)
        self.app.removeclass(selector, states[not value])
        self.app.addclass(selector, states[value])
        self.app.replace(
            selector, ''.join([str(c) for c in self.connection_btn._children]))

    def on_vidhub_device_name(self, instance, value, **kwargs):
        self.app.replace('#vidhub_device_name', '<h1>{}</h1>'.format(value))

    def build_view(self):
        self.input_buttons = InputButtons(vidhub_view=self,
                                          vidhub=self.vidhub,
                                          app=self.app)
        self.output_buttons = OutputButtons(vidhub_view=self,
                                            vidhub=self.vidhub,
                                            app=self.app)
        self.preset_buttons = PresetButtons(vidhub=self.vidhub, app=self.app)

        row = Row()
        col = Column(count=4)
        h = PageHeader(text=str(self.vidhub.device_id),
                       ident='vidhub_device_name')
        col.addelement(h)
        row.addelement(col)

        col = Column(count=4)
        col.addelement(self.edit_widget.widget)
        col.addelement(self.edit_vidhub_btn)
        row.addelement(col)

        col = Column(count=4, ident='connection_container')
        if self.vidhub.connected:
            cl = 'glyphicon glyphicon-ok-circle'
        else:
            cl = 'glyphicon glyphicon-ban-circle'
        ico = self.connection_icon = Span(cl=cl,
                                          ident='connection_icon',
                                          attrs={'aria-hidden': 'true'})
        btn = self.connection_btn = Button(
            severity='warning' if not self.vidhub.connected else 'success',
            ident='connection_btn',
        )
        btn._parent = col
        btn._children.append(
            'Connect ' if not self.vidhub.connected else 'Disconnect ')
        btn.addelement(ico)
        col.addelement(btn)
        row.addelement(col)
        self.widget.addelement(row)

        row = Row()
        col = Column(count=12)
        row.addelement(col)
        col.addelement(self.input_buttons.widget)
        self.widget.addelement(row)
        row = Row()
        col = Column(count=12)
        row.addelement(col)
        col.addelement(self.output_buttons.widget)
        self.widget.addelement(row)
        row = Row()
        col = Column(count=12)
        row.addelement(col)
        col.addelement(self.preset_buttons.widget)
        self.widget.addelement(row)
        if self.app.loaded:
            self.app.replace(self.get_selector(), str(self.widget))

    def on_edit_enable(self, instance, value, **kwargs):
        selector = '#{}'.format(self.edit_vidhub_btn.ident)
        if value:
            self.app.removeclass(selector, 'btn-default')
            self.app.addclass(selector, 'btn-primary')
            self.edit_widget.initial = self.vidhub.device_name
            self.edit_widget.hidden = False
        else:
            self.app.removeclass(selector, 'btn-primary')
            self.app.addclass(selector, 'btn-default')
            self.edit_widget.hidden = True

    def on_edit_widget_hidden(self, instance, value, **kwargs):
        if value:
            self.edit_enable = False

    def on_edit_widget_value(self, instance, value, **kwargs):
        if not self.edit_enable:
            return
        if self.vidhub is None:
            return
        self.vidhub.device_name = value
        self.edit_enable = False

    async def on_click(self, e):
        if self.edit_enable:
            await self.edit_widget.on_btn_click(e)
        ident = e['event_object']['target'].get('id')
        if ident == self.edit_vidhub_btn.ident and self.vidhub is not None:
            self.edit_enable = not self.edit_enable
            return
        if self.connection_btn is not None and ident == self.connection_btn.ident:
            if self.vidhub is None:
                return
            if self.vidhub.connected:
                await self.vidhub.disconnect()
            else:
                await self.vidhub.connect()
            return
        data_id = e['event_object']['target'].get('data-sofi-id')
        if data_id is None:
            return
        logger.info(data_id)
        for attr in ['input_buttons', 'output_buttons', 'preset_buttons']:
            obj = getattr(self, attr)
            if obj is None:
                return
            await obj.on_click(data_id)
Esempio n. 25
0
 class Sender(Dispatcher):
     value = Property()
     _events_ = ['on_test']
Esempio n. 26
0
class BasePort(Dispatcher):
    """Async wrapper for :any:`mido.ports`

    Arguments:
        name (str): The port name

    Attributes:
        stopped (asyncio.Event):
    """
    MAX_QUEUE = 100
    name: str = Property()
    """The port name"""

    running: bool = Property(False)
    """Current run state"""

    EXECUTOR: ClassVar['concurrent.futures.ThreadPoolExecutor'] = None
    """A :class:`concurrent.futures.ThreadPoolExecutor` to use in the
    :meth:`run_in_executor` method for all instances of all :class:`BasePort` subclasses
    """
    def __init__(self, name: str):
        self.name = name
        self.loop = asyncio.get_event_loop()
        # self.queue = asyncio.Queue(self.MAX_QUEUE)
        # self.running = asyncio.Event()
        self.stopped = asyncio.Event()
        self.port = None

    @staticmethod
    def get_executor() -> 'concurrent.futures.ThreadPoolExecutor':
        """Get or create the :attr:`EXECUTOR` instance to use in the
        :meth:`run_in_executor` method
        """
        exec = BasePort.EXECUTOR
        if exec is None:
            exec = BasePort.EXECUTOR = ThreadPoolExecutor(1)
        return exec

    async def run_in_executor(self, fn: Callable) -> Any:
        """Call the given function in the :attr:`EXECUTOR` instance using
        :meth:`asyncio.loop.run_in_executor` and return the result

        This method is used to create and manipulate all :mod:`mido` ports to
        avoid blocking, threaded operations
        """
        exec = self.get_executor()
        return await self.loop.run_in_executor(exec, fn)

    async def open(self) -> bool:
        """Open the midi port

        Returns:
            bool: ``True`` if the port was successfully opened

        """
        if self.running:
            return False
        self.running = True
        self.port = await self._build_port()
        # if port is not None:
        #     self.name = self.port.name
        logger.debug(f'{self}.port: {self.port}')
        logger.success(f'{self!r} running')
        return True

    async def close(self):
        """Close the midi port
        """
        if not self.running:
            return False
        self.running = False
        await self._close_port()
        self.stopped.set()
        logger.success(f'{self!r} closed')
        return True

    async def __aenter__(self):
        await self.open()
        return self

    async def __aexit__(self, *args):
        await self.close()

    async def _build_port(self):
        raise NotImplementedError

    async def _close_port(self):
        raise NotImplementedError

    def __repr__(self):
        return f'<{self.__class__.__name__}: "{self}">'

    def __str__(self):
        return self.name
Esempio n. 27
0
class Engine(Dispatcher):
    """Top level component to handle config, discovery and device control
    """

    devices: tp.Dict[str, Device] = DictProperty()
    """Mapping of :class:`~.device.Device` instances using their
    :attr:`~.device.Device.id` as keys
    """

    discovered_devices = DictProperty()
    running = Property(False)
    auto_add_devices = Property(True)
    """If ``True``, devices will be added automatically
    when discovered on the network. Otherwise, they must be added manually
    using :meth:`add_device_from_conf`
    """

    midi_io = Property()
    interfaces: tp.Dict[
        str, 'jvconnected.interfaces.base.Interface'] = DictProperty()
    """Container for :class:`~.interfaces.base.Interface` instances
    """

    _events_ = [
        'on_config_device_added',
        'on_device_discovered',
        'on_device_added',
        'on_device_removed',
    ]
    config: Config
    """The :class:`~.config.Config` instance"""

    discovery: Discovery
    """The :class:`~.discovery.Discovery` instance"""

    connection_status: tp.Dict[str, ReconnectStatus]
    """Mapping of :class:`ReconnectStatus` instances using the associated
    :attr:`device_id <.config.DeviceConfig.id>` as keys
    """
    def on_config_device_added(self, conf_device: DeviceConfig):
        """Fired when an instance of :class:`~.config.DeviceConfig` is added
        """

    def on_device_discovered(self, conf_device: DeviceConfig):
        """Fired when a device is detected on the network. An instance of
        :class:`~.config.DeviceConfig` is found (or created)
        and passed as the argument
        """

    def on_device_added(self, device: Device):
        """Fired when an instance of :class:`~.device.Device` is
        added to :attr:`devices`
        """

    def on_device_removed(self, device: Device, reason: RemovalReason):
        """Fired when an instance of :class:`~.device.Device` is removed

        Arguments:
            device: The device that was removed
            reason: Reason for removal
        """

    _device_reconnect_timeout = 5
    _device_reconnect_max_attempts = 100

    def __init__(self, **kwargs):
        self.auto_add_devices = kwargs.get('auto_add_devices', True)
        self.loop = asyncio.get_event_loop()
        self.config = Config()
        self.discovery = Discovery()
        self.device_reconnect_queue = asyncio.Queue()
        self._device_reconnect_main_task = None
        self._run_pending = False
        self.connection_status = {}
        for name, cls in interfaces.registry:
            obj = cls()
            self.interfaces[name] = obj
            if name == 'midi':
                self.midi_io = obj
        interfaces.registry.bind_async(
            self.loop,
            interface_added=self.on_interface_registered,
        )

    async def on_interface_registered(self, name, cls, **kwargs):
        if name not in self.interfaces:
            obj = cls()
            self.interfaces[name] = obj
            await obj.set_engine(self)

    def run_forever(self):
        """Convenience method to open and run until interrupted
        """
        self.loop.run_until_complete(self.open())
        try:
            self.loop.run_forever()
        except KeyboardInterrupt:
            self.loop.run_until_complete(self.close())
        finally:
            self.loop.run_until_complete(self.close())

    async def open(self):
        """Open all communication methods
        """
        if self.running:
            return
        self._run_pending = True
        t = asyncio.create_task(self._reconnect_devices())
        self._device_reconnect_main_task = t
        for obj in self.interfaces.values():
            await obj.set_engine(self)
        self.config.bind_async(
            self.loop,
            on_device_added=self._on_config_device_added,
        )
        self.discovery.bind_async(
            self.loop,
            on_service_added=self.on_discovery_service_added,
            on_service_updated=self.on_discovery_service_updated,
            on_service_removed=self.on_discovery_service_removed,
        )
        self.running = True
        self._run_pending = False
        await self.add_always_connected_devices()
        await self.discovery.open()
        logger.success('Engine open')

    async def add_always_connected_devices(self):
        """Create and open any devices with
        :attr:`~jvconnected.config.DeviceConfig.always_connect` set to True
        """
        coros = []
        for device_conf in self.config.devices.values():
            if not device_conf.always_connect:
                continue
            assert device_conf.id not in self.discovered_devices
            info = device_conf.build_service_info()
            coros.append(self.on_discovery_service_added(info.name, info=info))
        if len(coros):
            await asyncio.sleep(.01)
            await asyncio.gather(*coros)
            await asyncio.sleep(.01)

    async def close(self):
        """Close the discovery engine and any running device clients
        """
        if not self.running:
            return
        self.running = False
        self.discovery.unbind(self)
        await self.discovery.close()

        t = self._device_reconnect_main_task
        self._device_reconnect_main_task = None
        await self.device_reconnect_queue.put(None)
        await t
        for status in self.connection_status.values():
            t = status.task
            if t is None or t.done():
                continue
            t.cancel()
            try:
                await t
            except asyncio.CancelledError:
                pass
        self.connection_status.clear()

        for conf_device in self.discovered_devices.values():
            conf_device.active = False
            conf_device.online = False

        await asyncio.sleep(0)

        async def close_device(device):
            try:
                await device.close()
            finally:
                del self.devices[device.id]
                self.emit('on_device_removed', device, RemovalReason.SHUTDOWN)

        coros = []
        for device in self.devices.values():
            coros.append(close_device(device))
        await asyncio.gather(*coros)
        logger.success('Engine closed')

    async def add_device_from_conf(
            self, device_conf: 'jvconnected.config.DeviceConfig'):
        """Add a client :class:`~jvconnected.device.Device` instance from the given
        :class:`~jvconnected.config.DeviceConfig` and attempt to connect.

        If auth information is incorrect or does not exist, display the error
        and remove the newly added device.

        """
        status = self.connection_status.get(device_conf.id)
        if status is None:
            status = ReconnectStatus(device_id=device_conf.id)
            self.connection_status[device_conf.id] = status
        if status.state == ConnectionState.ATTEMPTING:
            task = status.task
            if task is not None and not task.done():
                await task
                if status.state == ConnectionState.CONNECTED:
                    return

        status.state = ConnectionState.ATTEMPTING
        logger.debug(f'add_device_from_conf: {device_conf}')
        device = Device(
            device_conf.hostaddr,
            device_conf.auth_user,
            device_conf.auth_pass,
            device_conf.id,
            device_conf.hostport,
        )
        device.device_index = device_conf.device_index
        self.devices[device_conf.id] = device
        self.emit('on_device_added', device)
        async with status:
            try:
                await device.open()
            except ClientError as exc:
                await asyncio.sleep(0)
                await self.on_device_client_error(device,
                                                  exc,
                                                  skip_status_lock=True)
                return
            status.state = ConnectionState.CONNECTED
            status.reason = RemovalReason.UNKNOWN
            status.num_attempts = 0
            device_conf.active = True
        device.bind_async(self.loop,
                          on_client_error=self.on_device_client_error)

    @logger.catch
    async def on_device_client_error(self, device, exc, **kwargs):
        skip_status_lock = kwargs.get('skip_status_lock', False)
        if not self.running:
            return
        if isinstance(exc, ClientNetworkError):
            reason = RemovalReason.TIMEOUT
        elif isinstance(exc, ClientAuthError):
            reason = RemovalReason.AUTH
            logger.warning(f'Authentication failed for device_id: {device.id}')
        else:
            reason = RemovalReason.UNKNOWN
        # logger.debug(f'device client error: device={device}, reason={reason}, exc={exc}')
        device_conf = self.discovered_devices[device.id]
        device_conf.active = False
        status = self.connection_status[device.id]

        async def handle_state():
            try:
                await device.close()
            finally:
                status.state = ConnectionState.FAILED
                if device.id in self.devices:
                    del self.devices[device.id]
                if reason == RemovalReason.TIMEOUT and status.reason != RemovalReason.OFFLINE:
                    await self.device_reconnect_queue.put((device.id, reason))

        if skip_status_lock:
            await handle_state()
        else:
            async with status:
                await handle_state()

        self.emit('on_device_removed', device, reason)

    @logger.catch
    async def on_discovery_service_added(self, name, **kwargs):
        logger.debug(f'on_discovery_service_added: {name}, {kwargs}')
        info = kwargs['info']
        device_id = DeviceConfig.get_id_for_service_info(info)
        device_conf = self.discovered_devices.get(device_id)

        if device_id in self.config.devices:
            if device_conf is not None:
                dev = self.config.add_device(device_conf)
                assert dev is device_conf
                device_conf.update_from_service_info(info)
            else:
                device_conf = self.config.add_discovered_device(info)
                self.discovered_devices[device_id] = device_conf
        elif device_conf is None:
            device_conf = self.add_discovered_device(info)

        device_conf.online = True
        self.emit('on_device_discovered', device_conf)
        if self.auto_add_devices:
            if device_conf.id not in self.devices:
                await self.add_device_from_conf(device_conf)

    async def on_discovery_service_updated(self, name, **kwargs):
        logger.debug(f'on_discovery_service_updated: "{name}", {kwargs}')
        info = kwargs['info']
        old = kwargs['old']
        device_id = DeviceConfig.get_id_for_service_info(old)
        status = self.connection_status.get(device_id)
        if status.task is not None and not status.task.done():
            await status.task
        await self.on_discovery_service_removed(name, info=old)
        await self.on_discovery_service_added(name, info=info)

    async def on_discovery_service_removed(self, name, **kwargs):
        logger.debug(f'on_discovery_service_removed: {name}, {kwargs}')
        info = kwargs['info']
        device_id = DeviceConfig.get_id_for_service_info(info)
        device_conf = self.discovered_devices.get(device_id)
        if device_conf is not None:
            device_conf.online = False
            if device_conf.always_connect:
                return
            device_conf.active = False
        status = self.connection_status[device_id]
        async with status:
            status.state = ConnectionState.FAILED
            status.reason = RemovalReason.OFFLINE
            device = self.devices.get(device_id)
            if device is not None:
                try:
                    await device.close()
                finally:
                    del self.devices[device_id]
        self.emit('on_device_removed', device, RemovalReason.OFFLINE)

    def add_discovered_device(self,
                              info: 'zeroconf.ServiceInfo') -> DeviceConfig:
        """Create a :class:`~jvconnected.config.DeviceConfig` and add it to
        :attr:`discovered_devices`
        """
        device_id = DeviceConfig.get_id_for_service_info(info)
        if device_id in self.discovered_devices:
            device_conf = self.discovered_devices[device_id]
        else:
            device_conf = DeviceConfig.from_service_info(info)
            self.discovered_devices[device_conf.id] = device_conf
        return device_conf

    @logger.catch
    async def _reconnect_devices(self):
        q = self.device_reconnect_queue

        async def do_reconnect(status: ReconnectStatus):
            status.state = ConnectionState.SLEEPING
            await asyncio.sleep(self._device_reconnect_timeout)
            async with status:
                if status.state != ConnectionState.SLEEPING:
                    return
                if not self.running:
                    return
                disco_conf = self.discovered_devices.get(status.device_id)
                if disco_conf is None:
                    return
                if not disco_conf.online:
                    return
                logger.debug(f'reconnect to {disco_conf}')
                status.num_attempts += 1
            await self.add_device_from_conf(disco_conf)

        while self.running or self._run_pending:
            item = await q.get()
            if item is None or not self.running:
                q.task_done()
                break
            device_id, reason = item
            status = self.connection_status[device_id]
            valid = True
            async with status:
                if status.state != ConnectionState.FAILED:
                    valid = False
                elif status.num_attempts >= self._device_reconnect_max_attempts:
                    logger.debug(f'max attempts reached for "{device_id}"')
                    valid = False
                elif status.task is not None and not status.task.done():
                    logger.error(f'Active reconnect task exists for {status}')
                    valid = False
                elif reason == RemovalReason.TIMEOUT and status.reason == RemovalReason.OFFLINE:
                    valid = False

                if valid:
                    status.reason = reason
                    status.state = ConnectionState.SCHEDULING
                    logger.debug(
                        f'scheduling reconnect for {device_id}, num_attempts={status.num_attempts}'
                    )
                    status.task = asyncio.create_task(do_reconnect(status))
            q.task_done()

    async def _on_config_device_added(self, conf_device, **kwargs):
        conf_device.bind(device_index=self._on_config_device_index_changed)
        self.emit('on_config_device_added', conf_device)

    def _on_config_device_index_changed(self, instance, value, **kwargs):
        device_id = instance.id
        device = self.devices.get(device_id)
        if device is None:
            return
        device.device_index = value
Esempio n. 28
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)
Esempio n. 29
0
class PubSubOscNode(OscNode):
    # spec: tuple of (instance, property_name)
    published_property = Property()

    def __init__(self, name, parent=None, **kwargs):
        super().__init__(name, parent, **kwargs)
        self._subscriber_lock = asyncio.Lock()
        self.subscribers = set()
        subscribe_node = self.add_child('_subscribe')
        query_node = self.add_child('_query')
        list_node = self.add_child('_list')
        subscribe_node.bind(on_message_received=self.on_subscribe_node_message)
        query_node.bind(on_message_received=self.on_query_node_message)
        list_node.bind(on_message_received=self.on_list_node_message)
        self.bind(published_property=self.on_published_property)
        self.published_property = kwargs.get('published_property')

    def on_subscribe_node_message(self, node, client_address, *messages):
        if len(messages) == 1 and not messages[0]:
            remove = True
        else:
            remove = False
        asyncio.ensure_future(
            self._add_or_remove_subscriber(client_address, remove),
            loop=self.event_loop,
        )

    async def _add_or_remove_subscriber(self, client_address, remove):
        async with self._subscriber_lock:
            if remove:
                self.subscribers.discard(client_address)
            else:
                self.subscribers.add(client_address)
        node = self.find('_subscribe')
        await node.send_message(client_address)

    async def _send_to_subscribers(self, *messages):
        async with self._subscriber_lock:
            for client_address in self.subscribers:
                await self.send_message(client_address, *messages)

    def update_subscribers(self, *messages):
        asyncio.ensure_future(self._send_to_subscribers(*messages),
                              loop=self.event_loop)

    def on_query_node_message(self, node, client_address, *messages):
        recursive = False
        if len(messages) and isinstance(messages[0], str):
            recursive = 'recursive' in messages[0].lower()
        if recursive:
            for node in self.walk():
                if not isinstance(node, PubSubOscNode):
                    continue
                try:
                    response = node.get_query_response()
                except NotImplementedError:
                    continue
                node.ensure_message(client_address, *response)
        else:
            try:
                response = self.get_query_response()
            except NotImplementedError:
                response = None
            self.ensure_message(client_address, *response)

    def get_query_response(self):
        prop = self.published_property
        if prop is not None:
            inst, prop = prop
            value = getattr(inst, prop)
            if isinstance(value, dict):
                value = value.keys()
            elif not isinstance(value, (list, tuple, set)):
                value = [value]
            return value
        raise NotImplementedError()

    def on_list_node_message(self, node, client_address, *messages):
        recursive = False
        if len(messages) and isinstance(messages[0], str):
            recursive = 'recursive' in messages[0].lower()
        if recursive:
            child_iter = self.walk()
        else:
            child_iter = self.children.values()
        child_iter = (n for n in child_iter
                      if n.name not in ('_query', '_subscribe', '_list'))
        addrs = [
            n.build_osc_address(to_parent=self) for n in child_iter
            if n is not self
        ]
        node = self.find('_list')
        node.ensure_message(client_address, *addrs)

    def on_published_property(self, instance, value, **kwargs):
        old = kwargs.get('old')
        if old is not None:
            old_inst, old_prop = old
            old_inst.unbind(self.on_published_property_change)
        if value is None:
            return
        inst, prop = value
        inst.bind(**{prop: self.on_published_property_change})

    def on_published_property_change(self, instance, value, **kwargs):
        if isinstance(value, list):
            args = value
        elif isinstance(value, dict):
            args = value.keys()
        else:
            args = [value]
        self.update_subscribers(*args)
Esempio n. 30
0
class InlineTextEdit(SofiDataId):
    label_text = Property()
    initial = Property()
    value = Property()
    input_type = Property('text')
    hidden = Property(True)
    registered = Property(False)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.label_text = kwargs.get('label', '')
        self.initial = kwargs.get('initial', '')
        self.hidden = kwargs.get('hidden', True)
        self.input_type = kwargs.get('input_type', 'text')
        if self.hidden:
            cl = 'panel hidden'
        else:
            cl = 'panel'
        self.widget = Div(cl=cl, attrs=self.get_data_id_attr())
        body = Div(cl='panel-body')

        grp = Div(cl='input-group')
        self.label_widget = Span(
            text=self.label_text,
            cl='input-group-addon',
            attrs=self.get_data_id_attr('label'),
        )
        attrs = self.get_data_id_attr('input')
        attrs['value'] = self.initial
        self.input_widget = Input(
            inputtype=self.input_type,
            attrs=attrs,
        )
        grp.addelement(self.label_widget)
        grp.addelement(self.input_widget)
        body.addelement(grp)

        btngrp = ButtonGroup()
        self.ok_btn = Button(text='Ok',
                             cl='text-edit-btn',
                             severity='primary',
                             attrs=self.get_data_id_attr('ok'))
        self.cancel_btn = Button(text='Cancel',
                                 cl='text-edit-btn',
                                 attrs=self.get_data_id_attr('cancel'))
        btngrp.addelement(self.ok_btn)
        btngrp.addelement(self.cancel_btn)
        body.addelement(btngrp)

        self.widget.addelement(body)

        self.bind(
            label_text=self.on_label_text,
            initial=self.on_initial,
            hidden=self.on_hidden,
        )
        self.app.register('load', self.on_app_load)

    async def on_app_load(self, *args):
        if self.registered:
            return
        self.registered = True
        self.app.register('click', self.on_btn_click, '.text-edit-btn')

    async def on_btn_click(self, e):
        if isinstance(e, dict):
            data_id = e['event_object']['target'].get('data-sofi-id')
        else:
            data_id = e
        if not data_id:
            return
        data_id = data_id.split('_')
        if data_id[0] != self.get_data_id():
            return
        selector = self.get_selector(obj=self.input_widget)
        if data_id[1] == 'ok':
            self.value = await self.app.get_property(selector, 'value')
        elif data_id[1] == 'cancel':
            self.input_widget.attrs['value'] = self.initial
            self.app.property(selector, 'value', self.initial)
        self.hidden = True

    def on_label_text(self, instance, value, **kwargs):
        selector = self.get_selector(obj=self.label_widget)
        self.app.text(selector, value)

    def on_initial(self, instance, value, **kwargs):
        if self.input_widget.attrs['value'] == value:
            return
        self.input_widget.attrs['value'] = value
        selector = self.get_selector(obj=self.input_widget)
        self.app.attr(selector, 'value', value)

    def on_hidden(self, instance, value, **kwargs):
        if value:
            self.app.addclass(self.get_selector(), 'hidden')
        else:
            self.app.removeclass(self.get_selector(), 'hidden')