class B(A): a = Property() b = Property() c = Property() _events_ = [ 'on_even_more_stuff', 'on_one_more_thing' ]
class A(Dispatcher): foo = Property() bar = Property() baz = Property() _events_ = [ 'on_stuff', 'on_more_stuff', ]
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'
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
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
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'
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', ]
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, })
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)
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)
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
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]
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)
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)
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')
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', ]
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')
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}'
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')
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}"'
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)
class Sender(Dispatcher): value = Property() _events_ = ['on_test']
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
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
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)
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)
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')