async def test_content_item_update_calls_active_listener( psm, protocol, listener): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) await protocol.inject(msg) assert listener.call_count == 1 update_item = set_path(messages.create(pb.UPDATE_CONTENT_ITEM_MESSAGE)) item = update_item.inner().contentItems.add() await protocol.inject(update_item) assert listener.call_count == 2 msg = messages.create(pb.SET_NOW_PLAYING_CLIENT_MESSAGE) client = msg.inner().client client.bundleIdentifier = CLIENT_ID_1 await protocol.inject(msg) assert listener.call_count == 3 now_playing = set_path(messages.create(pb.SET_NOW_PLAYING_PLAYER_MESSAGE)) await protocol.inject(now_playing) assert listener.call_count == 4 await protocol.inject(update_item) assert listener.call_count == 5
def handle_send_hid_event(self, message, inner): # These corresponds to the bytes mapping to pressed key (see # send_hid_event in pyatv/mrp/messages.py) start = inner.hidEventData[43:49] use_page, usage, down_press = struct.unpack(">HHH", start) if down_press == 1: self.state.outstanding_keypresses.add((use_page, usage)) self.send(messages.create(0, identifier=message.identifier)) elif down_press == 0: if (use_page, usage) in self.state.outstanding_keypresses: if (_convert_key_press(use_page, usage) == "select" and self.state.last_button_pressed == "home"): self.state.powered_on = False self._send_device_info(update=True) self.state.last_button_pressed = _convert_key_press( use_page, usage) self.state.outstanding_keypresses.remove((use_page, usage)) _LOGGER.debug("Pressed button: %s", self.state.last_button_pressed) self.send(messages.create(0, identifier=message.identifier)) else: _LOGGER.error("Missing key down for %d,%d", use_page, usage) else: _LOGGER.error("Invalid key press state: %d", down_press)
def handle_send_hid_event(self, message, inner): # These corresponds to the bytes mapping to pressed key (see # send_hid_event in pyatv/mrp/messages.py) start = inner.hidEventData[43:49] use_page, usage, down_press = struct.unpack('>HHH', start) if down_press == 1: self.outstanding_keypresses.add((use_page, usage)) self.send(messages.create(0, identifier=message.identifier)) elif down_press == 0: if (use_page, usage) in self.outstanding_keypresses: if ( _convert_key_press(use_page, usage) == 'select' and self.last_button_pressed == 'home' ): msg = messages.device_information( 'pyatv', message.identifier, 0) self.send(msg) self.last_button_pressed = _convert_key_press(use_page, usage) self.outstanding_keypresses.remove((use_page, usage)) _LOGGER.debug('Pressed button: %s', self.last_button_pressed) self.send(messages.create(0, identifier=message.identifier)) else: _LOGGER.error('Missing key down for %d,%d', use_page, usage) else: _LOGGER.error('Invalid key press state: %d', down_press)
async def test_set_now_playing_player_for_active_client( psm, protocol, listener): msg = messages.create(pb.SET_NOW_PLAYING_CLIENT_MESSAGE) client = msg.inner().client client.bundleIdentifier = CLIENT_ID_1 await protocol.inject(msg) msg = set_path(messages.create(pb.SET_NOW_PLAYING_PLAYER_MESSAGE)) await protocol.inject(msg) assert listener.call_count == 2 assert psm.playing.identifier == PLAYER_ID_1 assert psm.playing.display_name == PLAYER_NAME_1
async def test_set_state_calls_active_listener(psm, protocol, listener): set_state = set_path(messages.create(pb.SET_STATE_MESSAGE)) await protocol.inject(set_state) assert listener.call_count == 1 now_playing = set_path(messages.create(pb.SET_NOW_PLAYING_PLAYER_MESSAGE)) await protocol.inject(now_playing) assert listener.call_count == 2 await protocol.inject(set_state) assert listener.call_count == 3
async def test_remove_active_player(psm, protocol, listener): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) await protocol.inject(msg) msg = set_path(messages.create(pb.SET_NOW_PLAYING_PLAYER_MESSAGE)) await protocol.inject(msg) assert psm.playing.identifier == PLAYER_ID_1 remove = set_path(messages.create(pb.REMOVE_PLAYER_MESSAGE)) await protocol.inject(remove) assert listener.call_count == 3 assert not psm.playing.is_valid
async def test_content_item_update(psm, protocol): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) msg = add_metadata_item(msg, identifier="id", title="item", playCount=123) await protocol.inject(msg) msg = set_path(messages.create(pb.UPDATE_CONTENT_ITEM_MESSAGE)) item = msg.inner().contentItems.add() item.identifier = "id" item.metadata.title = "new title" item.metadata.playCount = 1111 await protocol.inject(msg) player = psm.get_player(msg.inner().playerPath) assert player.metadata_field("title") == "new title" assert player.metadata_field("playCount") == 1111
async def test_metadata_single_item(psm, protocol): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) msg = add_metadata_item(msg, title="item") await protocol.inject(msg) player = psm.get_player(msg.inner().playerPath) assert player.metadata.title == "item"
def _set_state_message(metadata, identifier): # Most things are hardcoded here for simplicity. Will change that # as time goes by and more dynamic content is needed. set_state = messages.create(protobuf.SET_STATE_MESSAGE) inner = set_state.inner() inner.playbackState = metadata.playback_state inner.displayName = "Fake Player" for command in metadata.supported_commands: item = inner.supportedCommands.supportedCommands.add() item.command = command item.enabled = True if metadata.repeat and metadata.repeat != const.RepeatState.Off: cmd = inner.supportedCommands.supportedCommands.add() cmd.command = protobuf.CommandInfo_pb2.ChangeRepeatMode cmd.repeatMode = _REPEAT_LOOKUP[metadata.repeat] if metadata.shuffle: cmd = inner.supportedCommands.supportedCommands.add() cmd.command = protobuf.CommandInfo_pb2.ChangeShuffleMode cmd.shuffleMode = metadata.shuffle queue = inner.playbackQueue queue.location = 0 _fill_item(queue.contentItems.add(), metadata) client = inner.playerPath.client client.processIdentifier = 123 client.bundleIdentifier = identifier return set_state
async def heartbeat_loop(protocol): """Periodically send heartbeat messages to device.""" _LOGGER.debug("Starting heartbeat loop") count = 0 attempts = 0 message = messages.create(protobuf.GENERIC_MESSAGE) while True: try: # Re-attempts are made with no initial delay to more quickly # recover a failed heartbeat (if possible) if attempts == 0: await asyncio.sleep(HEARTBEAT_INTERVAL) _LOGGER.debug("Sending periodic heartbeat %d", count) await protocol.send_and_receive(message) _LOGGER.debug("Got heartbeat %d", count) except asyncio.CancelledError: break except Exception: attempts += 1 if attempts > HEARTBEAT_RETRIES: _LOGGER.error("heartbeat %d failed after %d tries", count, attempts) protocol.connection.close() break _LOGGER.debug("heartbeat %d failed", count) else: attempts = 0 finally: count += 1 _LOGGER.debug("Stopping heartbeat loop at %d", count)
def update_client(self, display_name, identifier): msg = messages.create(protobuf.UPDATE_CLIENT_MESSAGE) client = msg.inner().client client.bundleIdentifier = identifier if display_name is not None: client.displayName = display_name self._send(msg)
def set_active_player(self, identifier): if identifier not in self.states: raise Exception('invalid player: %s', identifier) now_playing = messages.create(protobuf.SET_NOW_PLAYING_CLIENT_MESSAGE) client = now_playing.inner().client client.bundleIdentifier = identifier self._send(now_playing)
async def test_get_metadata_field(psm, protocol): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) msg = add_metadata_item(msg, title="item", playCount=123) await protocol.inject(msg) player = psm.get_player(msg.inner().playerPath) assert player.metadata_field("title") == "item" assert player.metadata_field("playCount") == 123
async def test_set_now_playing_player(psm, protocol, listener): msg = set_path(messages.create(pb.SET_NOW_PLAYING_PLAYER_MESSAGE)) await protocol.inject(msg) assert listener.call_count == 1 assert psm.playing.identifier == PLAYER_ID_1 assert psm.playing.display_name == PLAYER_NAME_1
def handle_generic(self, message, inner): # Generic message is used by pyatv for heartbeats self.state.heartbeat_count += 1 _LOGGER.debug("Received heartbeat (total count: %d)", self.state.heartbeat_count) self.send_to_client( messages.create(protobuf.ProtocolMessage.UNKNOWN_MESSAGE, identifier=message.identifier))
def handle_get_keyboard_session_message(self, message): _LOGGER.debug('Get keyboard session') # This message has a lot more fields, but pyatv currently # not use them so ignore for now resp = messages.create(protobuf.KEYBOARD_MESSAGE) resp.identifier = message.identifier self._send(resp)
async def test_update_client(psm, protocol, listener): msg = messages.create(pb.SET_NOW_PLAYING_CLIENT_MESSAGE) client = msg.inner().client client.bundleIdentifier = CLIENT_ID_1 await protocol.inject(msg) assert listener.call_count == 1 assert psm.client.display_name is None update = messages.create(pb.UPDATE_CLIENT_MESSAGE) client = update.inner().client client.bundleIdentifier = CLIENT_ID_1 client.displayName = CLIENT_NAME_1 await protocol.inject(update) assert listener.call_count == 2 assert psm.client.display_name == CLIENT_NAME_1
async def test_default_player_when_only_client_set(psm, protocol, listener): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) await protocol.inject(msg) msg = set_path( messages.create(pb.SET_STATE_MESSAGE), player_id="MediaRemote-DefaultPlayer", player_name="Default Name", ) await protocol.inject(msg) msg = messages.create(pb.SET_NOW_PLAYING_CLIENT_MESSAGE) client = msg.inner().client client.bundleIdentifier = CLIENT_ID_1 await protocol.inject(msg) assert psm.playing.identifier == "MediaRemote-DefaultPlayer" assert psm.playing.display_name == "Default Name"
async def test_set_now_playing_player_when_no_client(psm, protocol, listener): msg = set_path(messages.create(pb.SET_NOW_PLAYING_PLAYER_MESSAGE)) await protocol.inject(msg) assert listener.call_count == 0 assert not psm.playing.identifier assert not psm.playing.display_name
async def test_playback_state_seeking(psm, protocol): set_state = set_path(messages.create(pb.SET_STATE_MESSAGE)) set_state.inner().playbackState = pb.PlaybackState.Playing msg = add_metadata_item(set_state, playbackRate=2.0) await protocol.inject(msg) player = psm.get_player(msg.inner().playerPath) assert player.playback_state == pb.PlaybackState.Seeking
def default_supported_commands(self, commands): msg = messages.create(protobuf.SET_DEFAULT_SUPPORTED_COMMANDS_MESSAGE) supported_commands = msg.inner().supportedCommands.supportedCommands for command in commands: item = supported_commands.add() item.command = command item.enabled = True msg.inner().playerPath.client.bundleIdentifier = PLAYER_IDENTIFIER self._send(msg)
async def test_remove_client_if_belongs_to_active_player( psm, protocol, listener): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) await protocol.inject(msg) msg = set_path(messages.create(pb.SET_NOW_PLAYING_PLAYER_MESSAGE)) await protocol.inject(msg) msg = messages.create(pb.SET_NOW_PLAYING_CLIENT_MESSAGE) client = msg.inner().client client.bundleIdentifier = CLIENT_ID_1 await protocol.inject(msg) remove = set_path(messages.create(pb.REMOVE_PLAYER_MESSAGE)) await protocol.inject(remove) assert psm.client is None assert listener.call_count == 4
async def test_set_now_playing_client(psm, protocol, listener): msg = messages.create(pb.SET_NOW_PLAYING_CLIENT_MESSAGE) client = msg.inner().client client.bundleIdentifier = CLIENT_ID_1 await protocol.inject(msg) assert listener.call_count == 1 assert psm.client.bundle_identifier == CLIENT_ID_1
async def test_get_command_info(psm, protocol): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) info = msg.inner().supportedCommands.supportedCommands.add() info.command = pb.CommandInfo_pb2.Pause await protocol.inject(msg) player = psm.get_player(msg.inner().playerPath) assert player.command_info(pb.CommandInfo_pb2.Play) is None assert player.command_info(pb.CommandInfo_pb2.Pause) is not None
def handle_playback_queue_request(self, message, inner): setstate = messages.create(protobuf.SET_STATE_MESSAGE, identifier=message.identifier) queue = setstate.inner().playbackQueue queue.location = 0 item = queue.contentItems.add() item.artworkData = self.state.states[self.state.active_player].artwork item.artworkDataWidth = 456 item.artworkDataHeight = 789 self.send(setstate)
async def test_remove_not_active_client(psm, protocol, listener): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) await protocol.inject(msg) msg = messages.create(pb.SET_NOW_PLAYING_CLIENT_MESSAGE) client = msg.inner().client client.bundleIdentifier = CLIENT_ID_1 await protocol.inject(msg) assert listener.call_count == 2 assert psm.client.bundle_identifier == CLIENT_ID_1 remove = messages.create(pb.REMOVE_CLIENT_MESSAGE) client = remove.inner().client client.bundleIdentifier = CLIENT_ID_2 await protocol.inject(remove) assert listener.call_count == 2 assert psm.client.bundle_identifier == CLIENT_ID_1
async def test_get_client_and_player(psm, protocol): msg = set_path(messages.create(pb.SET_STATE_MESSAGE)) await protocol.inject(msg) player = psm.get_player(msg.inner().playerPath) assert player.identifier == PLAYER_ID_1 assert player.display_name == PLAYER_NAME_1 client = psm.get_client(msg.inner().playerPath.client) assert client.bundle_identifier == CLIENT_ID_1 assert client.display_name == CLIENT_NAME_1
def item_update(self, metadata, identifier): msg = messages.create(protobuf.UPDATE_CONTENT_ITEM_MESSAGE) inner = msg.inner() _fill_item(inner.contentItems.add(), metadata) client = inner.playerPath.client client.processIdentifier = 123 client.bundleIdentifier = identifier self.send(msg)
def handle_send_hid_event(self, message, inner): outstanding = self.state.outstanding_keypresses # These corresponds to the bytes mapping to pressed key (see # send_hid_event in pyatv/mrp/messages.py) start = inner.hidEventData[43:49] use_page, usage, down_press = struct.unpack(">HHH", start) if down_press == 1: outstanding[(use_page, usage)] = stub_sleep() self.send_to_client( messages.create(0, identifier=message.identifier)) elif down_press == 0: if (use_page, usage) in outstanding: pressed_key = _KEY_LOOKUP.get((use_page, usage)) if not pressed_key: raise Exception( f"unsupported key: use_page={use_page}, usage={usage}") if pressed_key == "select" and self.state.last_button_pressed == "home": self.state.powered_on = False self._send_device_info(update=True) time_diff = stub_sleep() - outstanding[(use_page, usage)] if time_diff > 0.5: self.state.last_button_action = const.InputAction.Hold elif self.state.last_button_pressed == pressed_key: # NB: Will report double tap for >= 3 clicks (fix when needed) self.state.last_button_action = const.InputAction.DoubleTap else: self.state.last_button_action = const.InputAction.SingleTap self.state.last_button_pressed = pressed_key del outstanding[(use_page, usage)] _LOGGER.debug("Pressed button: %s", self.state.last_button_pressed) self.send_to_client( messages.create(0, identifier=message.identifier)) else: _LOGGER.error("Missing key down for %d,%d", use_page, usage) else: _LOGGER.error("Invalid key press state: %d", down_press)