From b7ba5963c508a657d05b0f5bd1da2abaa651c8ae Mon Sep 17 00:00:00 2001 From: thiccaxe <51303986+thiccaxe@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:50:35 -0700 Subject: [PATCH 1/3] companion skip implementation --- pyatv/const.py | 3 +++ pyatv/core/facade.py | 9 +++++++++ pyatv/interface.py | 9 +++++++++ pyatv/protocols/companion/__init__.py | 7 +++++++ tests/fake_device/companion.py | 7 ++++++- tests/protocols/companion/test_companion_functional.py | 6 ++++++ 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/pyatv/const.py b/pyatv/const.py index d0d840910..5fec0391d 100644 --- a/pyatv/const.py +++ b/pyatv/const.py @@ -312,6 +312,9 @@ class FeatureName(Enum): SkipBackward = 37 """Skip backwards a time interval.""" + Skip = 63 + """Skip backwards or forwards a specified time interval.""" + SetPosition = 19 """Seek to position.""" diff --git a/pyatv/core/facade.py b/pyatv/core/facade.py index e223d02eb..c07c12aed 100644 --- a/pyatv/core/facade.py +++ b/pyatv/core/facade.py @@ -161,6 +161,15 @@ async def skip_backward(self) -> None: """ return await self.relay("skip_backward")() + @shield.guard + async def skip(self, time_delta: float) -> None: + """Skip backwards or forwards a time_delta, measured in seconds. + + If the delta is positive, skip forwards. + If the delta is negative, skip backwards. + """ + return await self.relay("skip")(time_delta) + @shield.guard async def set_position(self, pos: int) -> None: """Seek in the current playing media.""" diff --git a/pyatv/interface.py b/pyatv/interface.py index 4affd4bb9..3aa6f1129 100644 --- a/pyatv/interface.py +++ b/pyatv/interface.py @@ -421,6 +421,15 @@ async def skip_backward(self) -> None: """ raise exceptions.NotSupportedError() + @feature(63, "Skip", "Skip forwards of backwards a time interval.") + async def skip(self, time_delta: float) -> None: + """Skip backwards or forwards a time_delta, measured in seconds. + + If the delta is positive, skip forwards. + If the delta is negative, skip backwards. + """ + raise exceptions.NotSupportedError() + @feature(19, "SetPosition", "Seek to position.") async def set_position(self, pos: int) -> None: """Seek in the current playing media.""" diff --git a/pyatv/protocols/companion/__init__.py b/pyatv/protocols/companion/__init__.py index ad7e5f58d..1bcdbcd6c 100644 --- a/pyatv/protocols/companion/__init__.py +++ b/pyatv/protocols/companion/__init__.py @@ -106,6 +106,8 @@ class MediaControlFlags(IntFlag): FeatureName.SetVolume: MediaControlFlags.Volume, FeatureName.SkipForward: MediaControlFlags.SkipForward, FeatureName.SkipBackward: MediaControlFlags.SkipBackward, + # skip is present if either is active, though typically both or none are present + FeatureName.Skip: MediaControlFlags.SkipBackward | MediaControlFlags.SkipForward } SUPPORTED_FEATURES = set( @@ -334,6 +336,11 @@ async def previous(self) -> None: """Press key previous.""" await self.api.mediacontrol_command(MediaControlCommand.PreviousTrack) + async def skip(self, time_delta: float) -> None: + await self.api.mediacontrol_command( + MediaControlCommand.SkipBy, {"_skpS": time_delta} + ) + async def channel_up(self) -> None: """Select next channel.""" await self._press_button(HidCommand.ChannelIncrement) diff --git a/tests/fake_device/companion.py b/tests/fake_device/companion.py index 1fa97cbac..84df288ea 100644 --- a/tests/fake_device/companion.py +++ b/tests/fake_device/companion.py @@ -22,6 +22,7 @@ DEVICE_NAME = "Fake Companion ATV" INITIAL_VOLUME = 10.0 +INITIAL_DURATION = 10.0 VOLUME_STEP = 5.0 INITIAL_RTI_TEXT = "Fake Companion Keyboard Text" @@ -54,6 +55,7 @@ MediaControlCommand.NextTrack: "next", MediaControlCommand.PreviousTrack: "previous", MediaControlCommand.SetVolume: "set_volume", + MediaControlCommand.SkipBy: "skip", } @@ -88,6 +90,7 @@ def __init__(self): self.media_control_flags: int = MediaControlFlags.Volume self.interests: Set[str] = set() self.volume: float = INITIAL_VOLUME + self.duration: float = INITIAL_DURATION self.rti_clients: List[FakeCompanionService] = [] self._rti_focus_state: KeyboardFocusState = KeyboardFocusState.Focused self.rti_text: Optional[str] = INITIAL_RTI_TEXT @@ -395,8 +398,10 @@ def handle__mcc(self, message): if mcc == MediaControlCommand.SetVolume: # Make sure we send response before triggering event with volume update self.loop.call_soon(self.volume_changed(message["_c"]["_vol"] * 100.0)) - if mcc == MediaControlCommand.GetVolume: + elif mcc == MediaControlCommand.GetVolume: args["_vol"] = self.state.volume / 100.0 + elif mcc == MediaControlCommand.SkipBy: + self.state.duration += message["_c"]["_skpS"] elif mcc in MEDIA_CONTROL_MAP: _LOGGER.debug("Activated Media Control Command %s", mcc) self.state.latest_button = MEDIA_CONTROL_MAP[mcc] diff --git a/tests/protocols/companion/test_companion_functional.py b/tests/protocols/companion/test_companion_functional.py index afd87a933..6b02ccb67 100644 --- a/tests/protocols/companion/test_companion_functional.py +++ b/tests/protocols/companion/test_companion_functional.py @@ -16,6 +16,7 @@ from tests.fake_device.companion import ( INITIAL_RTI_TEXT, INITIAL_VOLUME, + INITIAL_DURATION, VOLUME_STEP, CompanionServiceFlags, ) @@ -210,6 +211,11 @@ async def test_remote_control_buttons(companion_client, companion_state, button) await getattr(companion_client.remote_control, button)() assert companion_state.latest_button == button +async def test_remote_control_skip(companion_client, companion_state): + await companion_client.remote_control.skip(10.0) + await until( + lambda: math.isclose(companion_state.duration, INITIAL_DURATION + 10.0) + ) async def test_audio_set_volume(companion_client, companion_state, companion_usecase): await until(lambda: companion_client.audio.volume, INITIAL_VOLUME) From 63c3ba8c9fdae5d59ac0a603f36a19a8279da123 Mon Sep 17 00:00:00 2001 From: thiccaxe <51303986+thiccaxe@users.noreply.github.com> Date: Tue, 30 Jul 2024 23:51:26 -0700 Subject: [PATCH 2/3] precise skip in companion, mrp, dmap --- pyatv/const.py | 3 -- pyatv/core/facade.py | 25 ++++++--------- pyatv/interface.py | 21 ++++-------- pyatv/protocols/companion/__init__.py | 18 ++++++++--- pyatv/protocols/dmap/__init__.py | 10 +++--- pyatv/protocols/mrp/__init__.py | 19 +++++++---- tests/fake_device/companion.py | 2 +- .../companion/test_companion_functional.py | 32 +++++++++++++++++-- tests/protocols/dmap/test_dmap_functional.py | 10 ++++++ tests/protocols/mrp/test_mrp_functional.py | 7 ++++ 10 files changed, 95 insertions(+), 52 deletions(-) diff --git a/pyatv/const.py b/pyatv/const.py index 7f3f9db08..6e2b5d5c6 100644 --- a/pyatv/const.py +++ b/pyatv/const.py @@ -312,9 +312,6 @@ class FeatureName(Enum): SkipBackward = 37 """Skip backwards a time interval.""" - Skip = 63 - """Skip backwards or forwards a specified time interval.""" - SetPosition = 19 """Seek to position.""" diff --git a/pyatv/core/facade.py b/pyatv/core/facade.py index fe50197cb..4be00ccac 100644 --- a/pyatv/core/facade.py +++ b/pyatv/core/facade.py @@ -146,29 +146,22 @@ async def wakeup(self) -> None: return await self.relay("wakeup")() @shield.guard - async def skip_forward(self) -> None: + async def skip_forward(self, time_interval: float = 0.0) -> None: """Skip forward a time interval. - Skip interval is typically 15-30s, but is decided by the app. + If time_interval is not positive or not present, a default or app-chosen + time interval is used, which is typically 10, 15, 30, etc. seconds. """ - return await self.relay("skip_forward")() + return await self.relay("skip_forward")(time_interval) @shield.guard - async def skip_backward(self) -> None: - """Skip backwards a time interval. + async def skip_backward(self, time_interval: float = 0.0) -> None: + """Skip backward a time interval. - Skip interval is typically 15-30s, but is decided by the app. + If time_interval is not positive or not present, a default or app-chosen + time interval is used, which is typically 10, 15, 30, etc. seconds. """ - return await self.relay("skip_backward")() - - @shield.guard - async def skip(self, time_delta: float) -> None: - """Skip backwards or forwards a time_delta, measured in seconds. - - If the delta is positive, skip forwards. - If the delta is negative, skip backwards. - """ - return await self.relay("skip")(time_delta) + return await self.relay("skip_backward")(time_interval) @shield.guard async def set_position(self, pos: int) -> None: diff --git a/pyatv/interface.py b/pyatv/interface.py index 8517dcb12..336bf4a1e 100644 --- a/pyatv/interface.py +++ b/pyatv/interface.py @@ -407,27 +407,20 @@ async def wakeup(self) -> None: "SkipForward", "Skip forward a time interval.", ) - async def skip_forward(self) -> None: + async def skip_forward(self, time_interval: float = 0.0) -> None: """Skip forward a time interval. - Skip interval is typically 15-30s, but is decided by the app. + If time_interval is not positive or not present, a default or app-chosen + time interval is used, which is typically 10, 15, 30, etc. seconds. """ raise exceptions.NotSupportedError() @feature(37, "SkipBackward", "Skip backwards a time interval.") - async def skip_backward(self) -> None: - """Skip backwards a time interval. + async def skip_backward(self, time_interval: float = 0.0) -> None: + """Skip backward a time interval. - Skip interval is typically 15-30s, but is decided by the app. - """ - raise exceptions.NotSupportedError() - - @feature(63, "Skip", "Skip forwards of backwards a time interval.") - async def skip(self, time_delta: float) -> None: - """Skip backwards or forwards a time_delta, measured in seconds. - - If the delta is positive, skip forwards. - If the delta is negative, skip backwards. + If time_interval is not positive or not present, a default or app-chosen + time interval is used, which is typically 10, 15, 30, etc. seconds. """ raise exceptions.NotSupportedError() diff --git a/pyatv/protocols/companion/__init__.py b/pyatv/protocols/companion/__init__.py index 0b714809a..71f355ea2 100644 --- a/pyatv/protocols/companion/__init__.py +++ b/pyatv/protocols/companion/__init__.py @@ -77,6 +77,8 @@ # that pyatv supports). PAIRING_WITH_PIN_SUPPORTED_MASK = 0x4000 +_DEFAULT_SKIP_TIME = 10 + # pylint: disable=invalid-name @@ -108,8 +110,6 @@ class MediaControlFlags(IntFlag): FeatureName.SetVolume: MediaControlFlags.Volume, FeatureName.SkipForward: MediaControlFlags.SkipForward, FeatureName.SkipBackward: MediaControlFlags.SkipBackward, - # skip is present if either is active, though typically both or none are present - FeatureName.Skip: MediaControlFlags.SkipBackward | MediaControlFlags.SkipForward } SUPPORTED_FEATURES = set( @@ -341,9 +341,19 @@ async def previous(self) -> None: """Press key previous.""" await self.api.mediacontrol_command(MediaControlCommand.PreviousTrack) - async def skip(self, time_delta: float) -> None: + async def skip_forward(self, time_interval: float = 0.0) -> None: + await self.api.mediacontrol_command( + MediaControlCommand.SkipBy, + {"_skpS": float(time_interval if time_interval > 0 else _DEFAULT_SKIP_TIME)} + ) + + async def skip_backward(self, time_interval: float = 0.0) -> None: + # float cast: opack fails with negative integers await self.api.mediacontrol_command( - MediaControlCommand.SkipBy, {"_skpS": time_delta} + MediaControlCommand.SkipBy, + { "_skpS": + float(-time_interval if time_interval > 0 else -_DEFAULT_SKIP_TIME) + } ) async def channel_up(self) -> None: diff --git a/pyatv/protocols/dmap/__init__.py b/pyatv/protocols/dmap/__init__.py index ca15e7025..20e9be978 100644 --- a/pyatv/protocols/dmap/__init__.py +++ b/pyatv/protocols/dmap/__init__.py @@ -353,23 +353,25 @@ async def volume_down(self) -> None: """Press key volume down.""" await self.apple_tv.ctrl_int_cmd("volumedown") - async def skip_forward(self) -> None: + async def skip_forward(self, time_interval: float = 0.0) -> None: """Skip forward a time interval. Skip interval is typically 15-30s, but is decided by the app. """ current_position = (await self.apple_tv.playstatus()).position if current_position: - await self.set_position(current_position + _DEFAULT_SKIP_TIME) + await self.set_position(current_position + + (int(time_interval) if time_interval > 0 else _DEFAULT_SKIP_TIME)) - async def skip_backward(self) -> None: + async def skip_backward(self, time_interval: float = 0.0) -> None: """Skip backwards a time interval. Skip interval is typically 15-30s, but is decided by the app. """ current_position = (await self.apple_tv.playstatus()).position if current_position: - await self.set_position(current_position - _DEFAULT_SKIP_TIME) + await self.set_position(current_position - + (int(time_interval) if time_interval > 0 else _DEFAULT_SKIP_TIME)) async def set_position(self, pos: int) -> None: """Seek in the current playing media.""" diff --git a/pyatv/protocols/mrp/__init__.py b/pyatv/protocols/mrp/__init__.py index 4323e9763..685d2dea9 100644 --- a/pyatv/protocols/mrp/__init__.py +++ b/pyatv/protocols/mrp/__init__.py @@ -71,6 +71,8 @@ _LOGGER = logging.getLogger(__name__) +_DEFAULT_SKIP_TIME = 15 + # Source: https://github.com/Daij-Djan/DDHidLib/blob/master/usb_hid_usages.txt _KEY_LOOKUP = { # name: [usage_page, usage] @@ -429,28 +431,31 @@ async def wakeup(self) -> None: """Wake up the device.""" await _send_hid_key(self.protocol, "wakeup", InputAction.SingleTap) - async def skip_forward(self) -> None: + async def skip_forward(self, time_interval: float = 0.0) -> None: """Skip forward a time interval. Skip interval is typically 15-30s, but is decided by the app. """ - await self._skip_command(CommandInfo_pb2.SkipForward) + await self._skip_command(CommandInfo_pb2.SkipForward, time_interval) - async def skip_backward(self) -> None: + async def skip_backward(self, time_interval: float = 0.0) -> None: """Skip backwards a time interval. Skip interval is typically 15-30s, but is decided by the app. """ - await self._skip_command(CommandInfo_pb2.SkipBackward) + await self._skip_command(CommandInfo_pb2.SkipBackward, time_interval) - async def _skip_command(self, command) -> None: + async def _skip_command(self, command, time_interval: float) -> None: info = self.psm.playing.command_info(command) + skip_interval: int + if time_interval > 0: + skip_interval = int(time_interval) # Pick the first preferred interval for simplicity - if info and info.preferredIntervals: + elif info and info.preferredIntervals: skip_interval = info.preferredIntervals[0] else: - skip_interval = 15 # Default value + skip_interval = _DEFAULT_SKIP_TIME # Default value await self._send_command(command, skipInterval=skip_interval) diff --git a/tests/fake_device/companion.py b/tests/fake_device/companion.py index 2bdbdd348..fe6fda057 100644 --- a/tests/fake_device/companion.py +++ b/tests/fake_device/companion.py @@ -460,7 +460,7 @@ def handle__mcc(self, message): elif mcc == MediaControlCommand.GetVolume: args["_vol"] = self.state.volume / 100.0 elif mcc == MediaControlCommand.SkipBy: - self.state.duration += message["_c"]["_skpS"] + self.state.duration = max(0, self.state.duration + message["_c"]["_skpS"]) elif mcc in MEDIA_CONTROL_MAP: _LOGGER.debug("Activated Media Control Command %s", mcc) self.state.latest_button = MEDIA_CONTROL_MAP[mcc] diff --git a/tests/protocols/companion/test_companion_functional.py b/tests/protocols/companion/test_companion_functional.py index 4f0b3701a..250c68b85 100644 --- a/tests/protocols/companion/test_companion_functional.py +++ b/tests/protocols/companion/test_companion_functional.py @@ -217,10 +217,36 @@ async def test_remote_control_buttons(companion_client, companion_state, button) await getattr(companion_client.remote_control, button)() assert companion_state.latest_button == button -async def test_remote_control_skip(companion_client, companion_state): - await companion_client.remote_control.skip(10.0) +async def test_remote_control_skip_forward_backward(companion_client, companion_state): + duration = companion_state.duration + await companion_client.remote_control.skip_forward() await until( - lambda: math.isclose(companion_state.duration, INITIAL_DURATION + 10.0) + lambda: math.isclose(companion_state.duration, duration + 10) + ) + + duration = companion_state.duration + await companion_client.remote_control.skip_backward() + await until( + lambda: math.isclose(companion_state.duration, duration - 10) + ) + + duration = companion_state.duration + await companion_client.remote_control.skip_forward(10.5) + await until( + lambda: math.isclose(companion_state.duration, duration + 10.5) + ) + + duration = companion_state.duration + await companion_client.remote_control.skip_backward(7.3) + await until( + lambda: math.isclose(companion_state.duration, duration - 7.3) + ) + + # "From beginning" + duration = companion_state.duration + await companion_client.remote_control.skip_backward(9999999.0) + await until( + lambda: math.isclose(companion_state.duration, 0) ) async def test_audio_set_volume(companion_client, companion_state, companion_usecase): diff --git a/tests/protocols/dmap/test_dmap_functional.py b/tests/protocols/dmap/test_dmap_functional.py index 821309536..49d82f9c6 100644 --- a/tests/protocols/dmap/test_dmap_functional.py +++ b/tests/protocols/dmap/test_dmap_functional.py @@ -270,6 +270,16 @@ async def test_skip_forward_backward(self): await self.atv.remote_control.skip_backward() metadata = await self.playing() self.assertEqual(metadata.position, prev_position - SKIP_TIME) + prev_position = metadata.position + + await self.atv.remote_control.skip_forward(13.0) + metadata = await self.playing() + self.assertEqual(metadata.position, prev_position + 13) + prev_position = metadata.position + + await self.atv.remote_control.skip_backward(11.0) + metadata = await self.playing() + self.assertEqual(metadata.position, prev_position - 11) async def test_reset_revision_if_push_updates_fail(self): """Test that revision is reset when an error occurs during push update. diff --git a/tests/protocols/mrp/test_mrp_functional.py b/tests/protocols/mrp/test_mrp_functional.py index 0c2b35932..9c36cec63 100644 --- a/tests/protocols/mrp/test_mrp_functional.py +++ b/tests/protocols/mrp/test_mrp_functional.py @@ -474,6 +474,13 @@ async def test_skip_forward_backward(self): self.usecase.change_metadata(title="dummy4") metadata = await self.playing(title="dummy4") self.assertEqual(metadata.position, prev_position - 8) + prev_position = metadata.position + + # Test specified skip time + await self.atv.remote_control.skip_forward(17) + self.usecase.change_metadata(title="dummy5") + metadata = await self.playing(title="dummy5") + self.assertEqual(metadata.position, prev_position + 17) async def test_button_play_pause(self): self.usecase.example_video(supported_commands=[CommandInfo_pb2.TogglePlayPause]) From 82d51e52c91a5a1e04b97de11430498b4e563f08 Mon Sep 17 00:00:00 2001 From: thiccaxe <51303986+thiccaxe@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:17:15 -0700 Subject: [PATCH 3/3] fix styling for 2462 --- pyatv/protocols/companion/__init__.py | 17 +++++++++---- pyatv/protocols/dmap/__init__.py | 12 ++++++---- .../companion/test_companion_functional.py | 24 +++++++------------ 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/pyatv/protocols/companion/__init__.py b/pyatv/protocols/companion/__init__.py index 71f355ea2..7d48dda76 100644 --- a/pyatv/protocols/companion/__init__.py +++ b/pyatv/protocols/companion/__init__.py @@ -77,6 +77,7 @@ # that pyatv supports). PAIRING_WITH_PIN_SUPPORTED_MASK = 0x4000 +# As seen in the TV Remote App _DEFAULT_SKIP_TIME = 10 # pylint: disable=invalid-name @@ -342,18 +343,26 @@ async def previous(self) -> None: await self.api.mediacontrol_command(MediaControlCommand.PreviousTrack) async def skip_forward(self, time_interval: float = 0.0) -> None: + """Skip forward a time interval.""" await self.api.mediacontrol_command( MediaControlCommand.SkipBy, - {"_skpS": float(time_interval if time_interval > 0 else _DEFAULT_SKIP_TIME)} + { + "_skpS": float( + time_interval if time_interval > 0 else _DEFAULT_SKIP_TIME + ) + }, ) async def skip_backward(self, time_interval: float = 0.0) -> None: + """Skip forward a time interval.""" # float cast: opack fails with negative integers await self.api.mediacontrol_command( MediaControlCommand.SkipBy, - { "_skpS": - float(-time_interval if time_interval > 0 else -_DEFAULT_SKIP_TIME) - } + { + "_skpS": float( + -time_interval if time_interval > 0 else -_DEFAULT_SKIP_TIME + ) + }, ) async def channel_up(self) -> None: diff --git a/pyatv/protocols/dmap/__init__.py b/pyatv/protocols/dmap/__init__.py index 20e9be978..662a975b3 100644 --- a/pyatv/protocols/dmap/__init__.py +++ b/pyatv/protocols/dmap/__init__.py @@ -360,8 +360,10 @@ async def skip_forward(self, time_interval: float = 0.0) -> None: """ current_position = (await self.apple_tv.playstatus()).position if current_position: - await self.set_position(current_position + - (int(time_interval) if time_interval > 0 else _DEFAULT_SKIP_TIME)) + await self.set_position( + current_position + + (int(time_interval) if time_interval > 0 else _DEFAULT_SKIP_TIME) + ) async def skip_backward(self, time_interval: float = 0.0) -> None: """Skip backwards a time interval. @@ -370,8 +372,10 @@ async def skip_backward(self, time_interval: float = 0.0) -> None: """ current_position = (await self.apple_tv.playstatus()).position if current_position: - await self.set_position(current_position - - (int(time_interval) if time_interval > 0 else _DEFAULT_SKIP_TIME)) + await self.set_position( + current_position + - (int(time_interval) if time_interval > 0 else _DEFAULT_SKIP_TIME) + ) async def set_position(self, pos: int) -> None: """Seek in the current playing media.""" diff --git a/tests/protocols/companion/test_companion_functional.py b/tests/protocols/companion/test_companion_functional.py index 250c68b85..e4c55c63e 100644 --- a/tests/protocols/companion/test_companion_functional.py +++ b/tests/protocols/companion/test_companion_functional.py @@ -20,9 +20,9 @@ from pyatv.protocols.companion.api import SystemStatus from tests.fake_device.companion import ( + INITIAL_DURATION, INITIAL_RTI_TEXT, INITIAL_VOLUME, - INITIAL_DURATION, VOLUME_STEP, CompanionServiceFlags, ) @@ -217,37 +217,29 @@ async def test_remote_control_buttons(companion_client, companion_state, button) await getattr(companion_client.remote_control, button)() assert companion_state.latest_button == button + async def test_remote_control_skip_forward_backward(companion_client, companion_state): duration = companion_state.duration await companion_client.remote_control.skip_forward() - await until( - lambda: math.isclose(companion_state.duration, duration + 10) - ) + await until(lambda: math.isclose(companion_state.duration, duration + 10)) duration = companion_state.duration await companion_client.remote_control.skip_backward() - await until( - lambda: math.isclose(companion_state.duration, duration - 10) - ) + await until(lambda: math.isclose(companion_state.duration, duration - 10)) duration = companion_state.duration await companion_client.remote_control.skip_forward(10.5) - await until( - lambda: math.isclose(companion_state.duration, duration + 10.5) - ) + await until(lambda: math.isclose(companion_state.duration, duration + 10.5)) duration = companion_state.duration await companion_client.remote_control.skip_backward(7.3) - await until( - lambda: math.isclose(companion_state.duration, duration - 7.3) - ) + await until(lambda: math.isclose(companion_state.duration, duration - 7.3)) # "From beginning" duration = companion_state.duration await companion_client.remote_control.skip_backward(9999999.0) - await until( - lambda: math.isclose(companion_state.duration, 0) - ) + await until(lambda: math.isclose(companion_state.duration, 0)) + async def test_audio_set_volume(companion_client, companion_state, companion_usecase): await until(lambda: companion_client.audio.volume, INITIAL_VOLUME)