async def test_clients_modified_during_runtime(udp_port, udp_port0): loop = asyncio.get_event_loop() sender = UmdSender() receiver1 = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) receiver2 = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port0) add_listener1 = EventListener() add_listener2 = EventListener() tally_listener1 = EventListener() tally_listener2 = EventListener() async def drain_listeners(): async def drain(listener): while not listener.empty(): await listener.get() coros = [ drain(add_listener1), drain(add_listener2), drain(tally_listener1), drain(tally_listener2), ] await asyncio.gather(*coros) receiver1.bind_async( loop, on_tally_added=add_listener1.callback, on_tally_updated=tally_listener1.callback, ) receiver2.bind_async( loop, on_tally_added=add_listener2.callback, on_tally_updated=tally_listener2.callback, ) screen_index = 1 async with receiver1: async with receiver2: async with sender: # Add 10 tallies and add the first receiver to clients after the third one for i in range(10): t_id = (screen_index, i) sender.set_tally_text(t_id, f'Tally {i}') if i == 2: sender.clients.add(('127.0.0.1', udp_port)) # Update the tallies and add the second receiver after the third one for i in range(10): t_id = (screen_index, i) tx_tally = sender.tallies[t_id] sender.set_tally_color(t_id, 'rh|lh', 'red') if i == 2: sender.clients.add(('127.0.0.1', udp_port0)) # Wait for a full update then allow the messages to come in await asyncio.sleep(sender.tx_interval) await drain_listeners() assert receiver1.tallies == receiver2.tallies == sender.tallies
def __init__(self, config: MultiTallyConfig, hostaddr: str = UmdReceiver.DEFAULT_HOST, hostport: int = UmdReceiver.DEFAULT_PORT): super().__init__(config) self.loop = asyncio.get_event_loop() self._screen_indices = set() self._tally_keys = set() self.receiver = UmdReceiver(hostaddr=hostaddr, hostport=hostport) self.receiver.bind( on_screen_added=self._on_receiver_screen_added, on_tally_added=self._on_receiver_tally_added, on_tally_updated=self._on_receiver_tally_updated )
async def test_broadcast_display(uhs500_msg_bytes, uhs500_msg_parsed, udp_endpoint, udp_port, faker): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) displays_by_index = { disp.index: disp for disp in uhs500_msg_parsed.displays } async with receiver: # Populate the receiver's tallies and wait for them to be added transport.sendto(uhs500_msg_bytes, ('127.0.0.1', udp_port)) _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() assert len(receiver.tallies) == len(uhs500_msg_parsed.displays) receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) # Send a broadcast display message for each TallyColor with a random brightness # The control field is set so the Tally text field is unchanged for color in TallyColor: brightness = faker.pyint(max_value=3) bc_disp = Display.broadcast( control=b'foo', rh_tally=color, lh_tally=color, txt_tally=color, brightness=brightness, ) msg = Message(displays=[bc_disp]) transport.sendto(msg.build_message(), ('127.0.0.1', udp_port)) _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() # Check each of the receiver's tallies against the bc_disp values # and make sure the text didn't change for tally in receiver.tallies.values(): assert tally.index < 0xffff assert not tally.is_broadcast assert tally.rh_tally == color assert tally.txt_tally == color assert tally.lh_tally == color assert tally.control == bc_disp.control assert tally.brightness == brightness assert tally.text == displays_by_index[tally.index].text
def __init__(self): self._reading_config = False self._config_read = asyncio.Event() self._connect_lock = asyncio.Lock() super().__init__() self.receiver = UmdReceiver() self.hostaddr = self.receiver.hostaddr self.hostport = self.receiver.hostport self.receiver.bind_async(self.loop, on_tally_added=self._on_receiver_tally_added, on_tally_updated=self._on_receiver_tally_updated, ) self.bind_async(self.loop, config=self.read_config, ) self.bind(**{prop:self.update_config for prop in ['hostaddr', 'hostport']})
async def test_parse_errors(faker, udp_endpoint, udp_port): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) msgobj = Message() disp = Display(index=0, text='foo') msgobj.displays.append(disp) async with receiver: transport.sendto(msgobj.build_message(), ('127.0.0.1', udp_port)) _ = await evt_listener.get() receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) screen = receiver.screens[msgobj.screen] rx_disp = screen[disp.index] assert rx_disp is receiver.tallies[rx_disp.id] assert rx_disp == disp for i in range(100): num_bytes = faker.pyint(min_value=1, max_value=1024) bad_bytes = faker.binary(length=num_bytes) with pytest.raises(ParseError): receiver.parse_incoming(bad_bytes, ('127.0.0.1', endpoint_port)) transport.sendto(bad_bytes, ('127.0.0.1', udp_port)) disp.text = f'foo_{i}' transport.sendto(msgobj.build_message(), ('127.0.0.1', udp_port)) _ = await evt_listener.get() assert rx_disp == disp
async def test_rebind(uhs500_msg_bytes, uhs500_msg_parsed, udp_endpoint, udp_port, unused_tcp_port_factory): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) async with receiver: # Send message bytes to receiver and wait for ``on_tally_added`` events transport.sendto(uhs500_msg_bytes, ('127.0.0.1', udp_port)) _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() assert len(receiver.tallies) == len(uhs500_msg_parsed.displays) # Set up to get ``on_tally_updated`` callbacks receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) # Change bind address and trigger a change await receiver.set_hostaddr('0.0.0.0') assert receiver.hostaddr == '0.0.0.0' disp = uhs500_msg_parsed.displays[0] disp.brightness = 1 transport.sendto(uhs500_msg_parsed.build_message(), ('0.0.0.0', udp_port)) evt_args, evt_kwargs = await evt_listener.get() evt_tally = evt_args[0] assert disp == evt_tally # Change bind port and trigger a change new_port = unused_tcp_port_factory() assert new_port != udp_port await receiver.set_hostport(new_port) assert receiver.hostport == new_port disp.brightness = 2 transport.sendto(uhs500_msg_parsed.build_message(), ('0.0.0.0', new_port)) evt_args, evt_kwargs = await evt_listener.get() evt_tally = evt_args[0] assert disp == evt_tally
async def test_scontrol(faker, udp_port): loop = asyncio.get_event_loop() sender = UmdSender(clients=[('127.0.0.1', udp_port)]) receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_scontrol=evt_listener.callback) bc_listener = EventListener() receiver.broadcast_screen.bind_async(loop, on_control=bc_listener.callback) async with receiver: async with sender: for i in range(100): data_len = faker.pyint(min_value=1, max_value=1024) control_data = faker.binary(length=data_len) await sender.send_scontrol(screen_index=i, data=control_data) evt_args, evt_kwargs = await evt_listener.get() rx_screen, rx_data = evt_args assert rx_screen.index == i assert rx_data == control_data # Send broadcast await sender.send_broadcast_scontrol(data=control_data) # Wait for the broadcast screen evt_args, evt_kwargs = await bc_listener.get() rx_screen, rx_data = evt_args assert rx_screen is receiver.broadcast_screen assert rx_data == control_data # Wait for each currently existing screen num_screens = i + 1 for j in range(num_screens): evt_args, evt_kwargs = await evt_listener.get() rx_screen, rx_data = evt_args assert rx_data == control_data
async def test_scontrol(faker, udp_endpoint, udp_port): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_scontrol=evt_listener.callback) async with receiver: for i in range(100): data_len = faker.pyint(min_value=1, max_value=1024) control_data = faker.binary(length=data_len) msgobj = Message(screen=i, scontrol=control_data) transport.sendto(msgobj.build_message(), ('127.0.0.1', udp_port)) args, kwargs = await evt_listener.get() screen, rx_data = args assert screen.index == i assert rx_data == control_data
async def test_disp_control(faker, udp_port): loop = asyncio.get_event_loop() sender = UmdSender(clients=[('127.0.0.1', udp_port)]) receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) add_listener = EventListener() receiver.bind_async(loop, on_tally_added=add_listener.callback) tally_listener = EventListener() receiver.bind_async(loop, on_tally_control=tally_listener.callback) screen_index = 1 async with receiver: async with sender: for i in range(100): t_id = (screen_index, i) sender.set_tally_text(t_id, f'Tally-{i}') tx_tally = sender.tallies[t_id] evt_args, evt_kwargs = await add_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally data_len = faker.pyint(min_value=1, max_value=1024) control_data = faker.binary(length=data_len) await sender.send_tally_control(t_id, control_data) assert tx_tally.control == control_data evt_args, evt_kwargs = await tally_listener.get() _rx_tally, rx_data = evt_args assert _rx_tally is rx_tally assert rx_data == rx_tally.control == tx_tally.control == control_data t_id = (screen_index, 200) data_len = faker.pyint(min_value=1, max_value=1024) control_data = faker.binary(length=data_len) await sender.send_tally_control(t_id, control_data) tx_tally = sender.tallies[t_id] evt_args, evt_kwargs = await add_listener.get() rx_tally = evt_args[0] assert rx_tally.control == tx_tally.control == control_data assert rx_tally == tx_tally
async def test_all_off_on_close(faker, udp_port): loop = asyncio.get_event_loop() sender = UmdSender( clients=[('127.0.0.1', udp_port)], all_off_on_close=True, ) receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) add_listener = EventListener() receiver.bind_async(loop, on_tally_added=add_listener.callback) tally_listener = EventListener() receiver.bind_async(loop, on_tally_updated=tally_listener.callback) async with receiver: async with sender: for screen_index in range(10): for i in range(10): t_id = (screen_index, i) sender.set_tally_text(t_id, f'Tally-{i}') tx_tally = sender.tallies[t_id] evt_args, evt_kwargs = await add_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally for ttype in TallyType.all(): setattr(tx_tally, ttype.name, TallyColor.RED) evt_args, evt_kwargs = await tally_listener.get() assert getattr(rx_tally, ttype.name) == TallyColor.RED # Sender is closed and should have broadcast "all-off" _ = await asyncio.wait_for(tally_listener.get(), timeout=1) while not tally_listener.empty(): _ = await tally_listener.get() for rx_tally in receiver.tallies.values(): assert rx_tally.rh_tally == TallyColor.OFF assert rx_tally.txt_tally == TallyColor.OFF assert rx_tally.lh_tally == TallyColor.OFF
class UmdInput(BaseInput, namespace='umd.UmdInput', final=True): """Networked tally input using the UMDv5 protocol Arguments: config(MultiTallyConfig): The initial value for :attr:`~tallypi.baseio.BaseIO.config` hostaddr(str, optional): The local :attr:`hostaddr` to listen on. Defaults to :attr:`tslumd.receiver.UmdReceiver.DEFAULT_HOST` hostport(int, optional): The UDP :attr:`hostport` to listen on. Defaults to :attr:`tslumd.receiver.UmdReceiver.DEFAULT_PORT` """ receiver: UmdReceiver """The tslumd server """ def __init__(self, config: MultiTallyConfig, hostaddr: str = UmdReceiver.DEFAULT_HOST, hostport: int = UmdReceiver.DEFAULT_PORT): super().__init__(config) self.loop = asyncio.get_event_loop() self._screen_indices = set() self._tally_keys = set() self.receiver = UmdReceiver(hostaddr=hostaddr, hostport=hostport) self.receiver.bind( on_screen_added=self._on_receiver_screen_added, on_tally_added=self._on_receiver_tally_added, on_tally_updated=self._on_receiver_tally_updated ) @classmethod def get_init_options(cls) -> Tuple[Option]: return ( MultiTallyOption, Option( name='hostaddr', type=str, required=False, default=UmdReceiver.DEFAULT_HOST, title='Listen Address', ), Option( name='hostport', type=int, required=False, default=UmdReceiver.DEFAULT_PORT, title='Listen Port', ), ) @property def hostaddr(self) -> str: """The :attr:`~tslumd.receiver.UmdReceiver.hostaddr` of the :attr:`receiver` """ return self.receiver.hostaddr @property def hostport(self) -> int: """The :attr:`~tslumd.receiver.UmdReceiver.hostport` of the :attr:`receiver` """ return self.receiver.hostport async def open(self): if self.running: return await self.receiver.open() self.running = True async def close(self): if not self.running: return await self.receiver.close() async def set_hostaddr(self, hostaddr: str): """Set the :attr:`hostaddr` on the :attr:`receiver` """ await self.receiver.set_hostaddr(hostaddr) async def set_hostport(self, hostport: int): """Set the :attr:`hostport` on the :attr:`receiver` """ await self.receiver.set_hostport(hostport) def get_screen(self, screen_index: int) -> Optional[Screen]: if screen_index not in self._screen_indices: return None return self.receiver.screens.get(screen_index) def get_all_screens(self) -> Iterable[Screen]: for ix in self._screen_indices: yield self.receiver.screens[ix] def get_tally(self, tally_key: TallyKey) -> Optional[Tally]: if tally_key not in self._tally_keys: return None return self.receiver.tallies.get(tally_key) def get_all_tallies(self, screen_index: Optional[int] = None) -> Iterable[Tally]: if screen_index is not None: screen = self.get_screen(screen_index) tally_iter = [] if screen is not None: tally_iter = screen for tally in tally_iter: if tally.id in self._tally_keys: yield tally else: for tally_key in self._tally_keys: yield self.receiver.tallies[tally_key] def _on_receiver_screen_added(self, screen: Screen, **kwargs): if not screen.is_broadcast and self.screen_matches(screen): self._screen_indices.add(screen.index) self.emit('on_screen_added', self, screen) @logger.catch def _on_receiver_tally_added(self, tally, **kwargs): if self.tally_matches(tally): self._tally_keys.add(tally.id) self.emit('on_tally_added', self, tally) @logger.catch def _on_receiver_tally_updated(self, tally: Tally, props_changed: Set[str], **kwargs): if tally.id in self._tally_keys: self.emit('on_tally_updated', self, tally, props_changed)
async def test_with_uhs_data(uhs500_msg_bytes, uhs500_msg_parsed, udp_endpoint, udp_port): transport, protocol, endpoint_port = udp_endpoint assert udp_port != endpoint_port loop = asyncio.get_event_loop() receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) uhs_screen = uhs500_msg_parsed.screen async with receiver: # Send message bytes to receiver transport.sendto(uhs500_msg_bytes, ('127.0.0.1', udp_port)) # Wait for all ``on_tally_added`` events _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() screen = receiver.screens[uhs_screen] # Check all receiver tallies against the expected ones assert len(receiver.tallies) == len(uhs500_msg_parsed.displays) for disp in uhs500_msg_parsed.displays: assert disp.index in screen tally = screen.tallies[disp.index] assert tally.id == (uhs_screen, disp.index) assert receiver.tallies[tally.id] is tally assert disp == tally # Change each display and send the updated message to receiver # Then wait for ``on_tally_updated`` events receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) for disp in uhs500_msg_parsed.displays: tally = screen.tallies[disp.index] for tally_type in TallyType.all(): attr = tally_type.name cur_value = getattr(disp, attr) if cur_value == TallyColor.RED: new_value = TallyColor.GREEN else: new_value = TallyColor.RED setattr(disp, attr, new_value) disp.text = f'{disp.text}-foo' disp.brightness = 1 transport.sendto(uhs500_msg_parsed.build_message(), ('127.0.0.1', udp_port)) evt_args, evt_kwargs = await evt_listener.get() evt_tally = evt_args[0] assert evt_tally is tally assert disp == tally
async def test_with_uhs_data(udp_port): loop = asyncio.get_event_loop() sender = UmdSender(clients=[('127.0.0.1', udp_port)]) receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) screen_index = 1 async with receiver: async with sender: # Create initial tallies using text method for i in range(100): t_id = (screen_index, i) sender.set_tally_text(t_id, f'Tally-{i}') tx_tally = sender.tallies[t_id] screen = sender.screens[screen_index] assert screen[i] is tx_tally evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally # Create one more tally using ``set_tally_color`` t_id = (screen_index, 200) sender.set_tally_color(t_id, TallyType.lh_tally, TallyColor.GREEN) tx_tally = sender.tallies[t_id] assert screen[200] is tx_tally evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally # Allow the sender to do a full refresh. Nothing should have changed await asyncio.sleep(sender.tx_interval) assert evt_listener.empty() # Connect to ``on_tally_updated`` events receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) # Change each tally/tally_type color to red and check the received values for tx_tally in sender.tallies.values(): for tally_type in TallyType.all(): sender.set_tally_color(tx_tally.id, tally_type, TallyColor.RED) evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally is receiver.tallies[tx_tally.id] assert rx_tally == tx_tally # Change the text of the extra tally from above and check t_id = (screen_index, 200) sender.set_tally_text(t_id, 'Tally-200') tx_tally = sender.tallies[t_id] evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally # Let the sender to another full refresh await asyncio.sleep(sender.tx_interval) assert evt_listener.empty() # Change all tally/tally_type colors, but don't wait for results yet for tx_tally in sender.tallies.values(): for tally_type in TallyType.all(): sender.set_tally_color(tx_tally.id, tally_type, TallyColor.AMBER) sender.set_tally_text(tx_tally.id, f'foo-{tx_tally.index}') # Wait for updates from last loop to get to the receiver # and check the results _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() for tx_tally in sender.tallies.values(): rx_tally = receiver.tallies[tx_tally.id] assert rx_tally == tx_tally
async def test_broadcast_display(udp_port): loop = asyncio.get_event_loop() sender = UmdSender(clients=[('127.0.0.1', udp_port)]) receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) async def wait_for_receiver(): _ = await evt_listener.get() while not evt_listener.empty(): _ = await evt_listener.get() await asyncio.sleep(sender.tx_interval) assert evt_listener.empty() color_kw = { attr: TallyColor.RED for attr in ['rh_tally', 'txt_tally', 'lh_tally'] } screen_index = 1 async with receiver: async with sender: # Create initial tallies for i in range(10): t_id = (screen_index, i) tx_tally = sender.add_tally(t_id, **color_kw) screen = sender.screens[screen_index] assert screen[i] is tx_tally tx_tally.text = f'Tally-{i}' evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally == tx_tally # Connect to ``on_tally_updated`` events receiver.unbind(evt_listener) receiver.bind_async(loop, on_tally_updated=evt_listener.callback) # Send a broadcast tally for each color setting all TallyType's to it for color in TallyColor: color_kw = {k: color for k in color_kw.keys()} await sender.send_broadcast_tally(screen_index, **color_kw) await wait_for_receiver() # Check the tally colors and make sure the text values remained for rx_tally in receiver.tallies.values(): tx_tally = sender.tallies[rx_tally.id] assert rx_tally.text == tx_tally.text == f'Tally-{rx_tally.index}' assert rx_tally.rh_tally == tx_tally.rh_tally == color assert rx_tally.txt_tally == tx_tally.txt_tally == color assert rx_tally.lh_tally == tx_tally.lh_tally == color # Broadcast all colors to "OFF" and set all names to 'foo' color_kw = {k: TallyColor.OFF for k in color_kw.keys()} await sender.send_broadcast_tally(screen_index, text='foo', **color_kw) await wait_for_receiver() # Check the tally colors and text values for rx_tally in receiver.tallies.values(): tx_tally = sender.tallies[rx_tally.id] assert rx_tally.text == tx_tally.text == 'foo' assert rx_tally.rh_tally == tx_tally.rh_tally == TallyColor.OFF assert rx_tally.txt_tally == tx_tally.txt_tally == TallyColor.OFF assert rx_tally.lh_tally == tx_tally.lh_tally == TallyColor.OFF # Send broadcast tally control messages for control_data in [b'foo', b'bar', b'baz']: await sender.send_broadcast_tally(screen_index, control=control_data) await wait_for_receiver() # Check for the correct control data and ensure other values # remain unchanged for rx_tally in receiver.tallies.values(): tx_tally = sender.tallies[rx_tally.id] assert rx_tally.control == tx_tally.control == control_data assert rx_tally.text == tx_tally.text == 'foo' assert rx_tally.rh_tally == tx_tally.rh_tally == TallyColor.OFF assert rx_tally.txt_tally == tx_tally.txt_tally == TallyColor.OFF assert rx_tally.lh_tally == tx_tally.lh_tally == TallyColor.OFF # Do the same as above, but using the `sender.send_broadcast_tally_control` method # and change one tally color for control_data in [b'abc', b'def', b'ghi']: await sender.send_broadcast_tally_control( screen_index, control_data, rh_tally=TallyColor.RED) await wait_for_receiver() # Check for the correct control data and ensure other values for rx_tally in receiver.tallies.values(): tx_tally = sender.tallies[rx_tally.id] assert rx_tally.control == tx_tally.control == control_data assert rx_tally.text == tx_tally.text == 'foo' assert rx_tally.rh_tally == tx_tally.rh_tally == TallyColor.RED assert rx_tally.txt_tally == tx_tally.txt_tally == TallyColor.OFF assert rx_tally.lh_tally == tx_tally.lh_tally == TallyColor.OFF
async def test_tally_type_variations(udp_port): loop = asyncio.get_event_loop() sender = UmdSender(clients=[('127.0.0.1', udp_port)]) receiver = UmdReceiver(hostaddr='127.0.0.1', hostport=udp_port) evt_listener = EventListener() receiver.bind_async(loop, on_tally_added=evt_listener.callback) tally_listener = EventListener() receiver.bind_async(loop, on_tally_updated=tally_listener.callback) tally_type_strs = ('rh', 'txt', 'lh') tally_types = (TallyType.rh_tally, TallyType.txt_tally, TallyType.lh_tally) def get_tally_colors(tally): d = {} for tt in tally_types: d[tt] = tally[tt] return d async def wait_for_rx(tally_type): tally_types = set() if not isinstance(tally_type, TallyType): tally_type = TallyType.from_str(tally_type) if tally_type.is_iterable: for tt in tally_type: tally_types.add(tt.name) else: tally_types.add(tally_type.name) props = set() for _ in range(len(tally_types)): evt_args, evt_kwargs = await tally_listener.get() props |= evt_args[1] if props == tally_types: break assert props == tally_types screen_index = 1 async with receiver: async with sender: for i in range(10): t_id = (screen_index, i) expected = {key: TallyColor.OFF for key in tally_types} tally = None rx_tally = None for tt_str, tt in zip(tally_type_strs, tally_types): sender.set_tally_color(t_id, tt_str, TallyColor.RED) expected[tt] = TallyColor.RED tally = sender.tallies[t_id] assert get_tally_colors(tally) == expected if rx_tally is None: evt_args, evt_kwargs = await evt_listener.get() rx_tally = evt_args[0] assert rx_tally == tally else: await wait_for_rx(tt) assert get_tally_colors(rx_tally) == expected sender.set_tally_color(t_id, tt, TallyColor.GREEN) expected[tt] = TallyColor.GREEN assert get_tally_colors(tally) == expected await wait_for_rx(tt) assert get_tally_colors(rx_tally) == expected sender.set_tally_color(t_id, tt_str, 'off') expected[tt] = TallyColor.OFF assert get_tally_colors(tally) == expected await wait_for_rx(tt) assert get_tally_colors(rx_tally) == expected sender.set_tally_color(t_id, tt, 'red') expected[tt] = TallyColor.RED assert get_tally_colors(tally) == expected await wait_for_rx(tt) assert get_tally_colors(rx_tally) == expected sender.set_tally_color(t_id, 'all', 'off') expected = {key: TallyColor.OFF for key in tally_types} assert get_tally_colors(tally) == expected assert tally['all'] == TallyColor.OFF await wait_for_rx('rh|txt|lh') assert get_tally_colors(rx_tally) == expected sender.set_tally_color(t_id, 'lh|rh', 'red') expected[TallyType.rh_tally] = TallyColor.RED expected[TallyType.lh_tally] = TallyColor.RED assert get_tally_colors(tally) == expected assert tally['all'] == TallyColor.RED await wait_for_rx('lh|rh') assert get_tally_colors(rx_tally) == expected sender.set_tally_color(t_id, 'txt', 'green') expected[TallyType.txt_tally] = TallyColor.GREEN assert get_tally_colors(tally) == expected await wait_for_rx('txt') assert get_tally_colors(rx_tally) == expected assert tally['all'] == tally['lh|txt'] == tally[ 'rh|txt'] == TallyColor.AMBER
class UmdIo(Interface): """Main UMD interface """ hostaddr: str = Property('0.0.0.0') """Alias for :attr:`tslumd.receiver.UmdReceiver.hostaddr`""" hostport: int = Property(65000) """Alias for :attr:`tslumd.receiver.UmdReceiver.hostport`""" device_maps: Dict[int, DeviceMapping] = DictProperty() """A ``dict`` of :class:`~.mapper.DeviceMapping` definitions stored with their :attr:`~.mapper.DeviceMapping.device_index` as keys """ mapped_devices: Dict[int, MappedDevice] = DictProperty() """A ``dict`` of :class:`~.mapper.MappedDevice` stored with the ``device_index`` of their :attr:`~.mapper.MappedDevice.map` as keys """ def on_tally_added(self, tally: Tally): """Fired when a :class:`tslumd.tallyobj.Tally` instance is added to :attr:`tallies` """ def on_tally_updated(self, tally: Tally): """Fired when any :class:`tslumd.tallyobj.Tally` instance has been updated """ _events_ = ['on_tally_added', 'on_tally_updated'] interface_name = 'tslumd' def __init__(self): self._reading_config = False self._config_read = asyncio.Event() self._connect_lock = asyncio.Lock() super().__init__() self.receiver = UmdReceiver() self.hostaddr = self.receiver.hostaddr self.hostport = self.receiver.hostport self.receiver.bind_async(self.loop, on_tally_added=self._on_receiver_tally_added, on_tally_updated=self._on_receiver_tally_updated, ) self.bind_async(self.loop, config=self.read_config, ) self.bind(**{prop:self.update_config for prop in ['hostaddr', 'hostport']}) @property def tallies(self) -> Dict[int, Tally]: """Alias for :attr:`tslumd.receiver.UmdReceiver.tallies` """ return self.receiver.tallies async def set_engine(self, engine: 'jvconnected.engine.Engine'): if engine is self.engine: return if engine.config is not self.config: self._config_read.clear() await super().set_engine(engine) engine.bind_async( self.loop, on_device_added=self.on_engine_device_added, on_device_removed=self.on_engine_device_removed, ) async def open(self): async with self._connect_lock: if self.running: return logger.debug('UmdIo.open()') if self.config is not None: await self._config_read.wait() self.running = True await self.receiver.open() logger.success('UmdIo running') async def close(self): async with self._connect_lock: if not self.running: return logger.debug('UmdIo.close()') self.running = False await self.receiver.close() logger.success('UmdIo closed') async def set_bind_address(self, hostaddr: str, hostport: int): """Set the :attr:`hostaddr` and :attr:`hostport` and restart the server """ await self.receiver.set_bind_address(hostaddr, hostport) self.hostaddr = self.receiver.hostaddr self.hostport = self.receiver.hostport async def set_hostaddr(self, hostaddr: str): """Set the :attr:`hostaddr` and restart the server """ await self.set_bind_address(hostaddr, self.hostport) async def set_hostport(self, hostport: int): """Set the :attr:`hostport` and restart the server """ await self.set_bind_address(self.hostaddr, hostport) async def _on_receiver_tally_added(self, tally, **kwargs): for mapped_device in self.mapped_devices.values(): if mapped_device.have_tallies: continue r = mapped_device.get_tallies() if r: await mapped_device.update_device_tally() self.emit('on_tally_added', tally, **kwargs) async def _on_receiver_tally_updated(self, tally: Tally, props_changed: Set[str], **kwargs): self.emit('on_tally_updated', tally, props_changed, **kwargs) def get_device_by_index(self, ix: int) -> Optional['jvconnected.device.Device']: device = None if self.engine is not None: device_conf = self.engine.config.indexed_devices.get_by_index(ix) if device_conf is not None: device = self.engine.devices.get(device_conf.id) return device @logger.catch async def add_device_mapping(self, device_map: 'DeviceMapping'): """Add a :class:`~.mapper.DeviceMapping` definition to :attr:`device_maps` and update the :attr:`config`. An instance of :class:`~.mapper.MappedDevice` is also created and associated with its :class:`~jvconnected.device.Device` if found in the :attr:`engine`. """ ix = device_map.device_index self.device_maps[ix] = device_map mapped_device = self.mapped_devices.get(ix) if mapped_device is not None: await mapped_device.set_device(None) del self.mapped_devices[ix] device = self.get_device_by_index(ix) mapped_device = MappedDevice(map=device_map, umd_io=self) self.mapped_devices[ix] = mapped_device await mapped_device.set_device(device) self.update_config() async def remove_device_mapping(self, device_index: int): """Remove a :class:`~.mapper.DeviceMapping` and its associated :class:`~.mapper.MappedDevice` by the given device index """ if device_index not in self.device_maps: return del self.device_maps[device_index] mapped_device = self.mapped_devices.get(device_index) if mapped_device is not None: await mapped_device.set_device(None) del self.mapped_devices[device_index] self.update_config() async def on_engine_device_added(self, device, **kwargs): mapped_device = self.mapped_devices.get(device.device_index) if mapped_device is not None: await mapped_device.set_device(device) async def on_engine_device_removed(self, device, reason, **kwargs): mapped_device = self.mapped_devices.get(device.device_index) if mapped_device is not None: await mapped_device.set_device(None) def update_config(self, *args, **kwargs): """Update the :attr:`config` with current state """ if self._reading_config: return if self.config is None: return if not self._config_read.is_set(): return d = self.get_config_section() if d is None: return d['hostaddr'] = self.hostaddr d['hostport'] = self.hostport m = self.device_maps d['device_maps'] = [m[k] for k in sorted(m.keys())] @logger.catch async def read_config(self, *args, **kwargs): d = self.get_config_section() if d is None: return self._reading_config = True hostaddr = d.get('hostaddr', self.hostaddr) hostport = d.get('hostport', self.hostport) coros = [] for dev_map in d.get('device_maps', []): coros.append(self.add_device_mapping(dev_map)) await asyncio.gather(*coros) await self.set_bind_address(hostaddr, hostport) self._reading_config = False self._config_read.set()