/
/
/
1"""Tests for WiiM player provider."""
2
3from unittest.mock import AsyncMock, MagicMock
4
5import pytest
6from music_assistant_models.enums import PlaybackState, PlayerFeature
7from wiim import PlayingStatus
8from wiim.exceptions import WiimDeviceException, WiimRequestException
9
10from music_assistant.models.player import PlayerMedia
11from music_assistant.providers.wiim.constants import PLAYER_ID_PREFIX, SOURCE_NETWORK
12from music_assistant.providers.wiim.player import SDK_TO_MA_STATE, WiimPlayer
13
14
15@pytest.fixture
16def mock_wiim_device() -> MagicMock:
17 """Create a mock WiimDevice."""
18 device = MagicMock()
19 device.name = "Test WiiM Pro"
20 device.udn = "uuid:test-wiim-001"
21 device.available = True
22 device.volume = 50
23 device.is_muted = False
24 device.playing_status = None
25 device.play_mode = None
26 device.current_media = None
27 device.model_name = "WiiM Pro"
28 device.manufacturer = "Linkplay"
29 device.firmware_version = "4.8.1"
30 device.ip_address = "192.168.1.100"
31 device.supports_http_api = True
32 device.supported_input_modes = ("Network", "Bluetooth", "Line In", "Optical In")
33 device.async_play = AsyncMock()
34 device.async_pause = AsyncMock()
35 device.async_stop = AsyncMock()
36 device.async_set_volume = AsyncMock()
37 device.async_set_mute = AsyncMock()
38 device.async_set_play_mode = AsyncMock()
39 device.sync_device_duration_and_position = AsyncMock()
40 device.disconnect = AsyncMock()
41 device.ensure_subscriptions = AsyncMock()
42 device.general_event_callback = None
43 device.rendering_control_event_callback = None
44 device.av_transport_event_callback = None
45 device.play_queue_event_callback = None
46 return device
47
48
49@pytest.fixture
50def mock_controller() -> MagicMock:
51 """Create a mock WiimController."""
52 controller = MagicMock()
53 snapshot = MagicMock()
54 snapshot.role = "standalone"
55 snapshot.leader_udn = "uuid:test-wiim-001"
56 snapshot.member_udns = ("uuid:test-wiim-001",)
57 controller.get_group_snapshot.return_value = snapshot
58 controller.get_group_members.return_value = []
59 controller.get_device.return_value = MagicMock()
60 controller.async_join_group = AsyncMock()
61 controller.async_ungroup_device = AsyncMock()
62 return controller
63
64
65@pytest.fixture
66def mock_provider(mock_controller: MagicMock) -> MagicMock:
67 """Create a mock WiimProvider."""
68 provider = MagicMock()
69 provider.wiim_controller = mock_controller
70 provider.instance_id = "wiim_test"
71 provider.domain = "wiim"
72 provider.manifest = MagicMock()
73 provider.manifest.domain = "wiim"
74 provider.mass = MagicMock()
75 provider.mass.players = MagicMock()
76
77 config = MagicMock()
78 config.name = None
79 config.default_name = "Test WiiM Pro"
80 config.enabled = True
81 config.player_type = None
82 config.get_value = MagicMock(return_value=None)
83 provider.mass.config.get_base_player_config.return_value = config
84 return provider
85
86
87class TestSDKStateMapping:
88 """Test SDK to MA state mapping."""
89
90 def test_playing_maps_to_playing(self) -> None:
91 """PLAYING should map to PlaybackState.PLAYING."""
92 assert SDK_TO_MA_STATE[PlayingStatus.PLAYING] == PlaybackState.PLAYING
93
94 def test_paused_maps_to_paused(self) -> None:
95 """PAUSED should map to PlaybackState.PAUSED."""
96 assert SDK_TO_MA_STATE[PlayingStatus.PAUSED] == PlaybackState.PAUSED
97
98 def test_stopped_maps_to_idle(self) -> None:
99 """STOPPED should map to PlaybackState.IDLE."""
100 assert SDK_TO_MA_STATE[PlayingStatus.STOPPED] == PlaybackState.IDLE
101
102 def test_loading_maps_to_playing(self) -> None:
103 """LOADING should map to PlaybackState.PLAYING."""
104 assert SDK_TO_MA_STATE[PlayingStatus.LOADING] == PlaybackState.PLAYING
105
106 def test_all_sdk_states_mapped(self) -> None:
107 """All non-UNKNOWN SDK states should have a mapping."""
108 for status in PlayingStatus:
109 if status != PlayingStatus.UNKNOWN:
110 assert status in SDK_TO_MA_STATE, f"{status} not mapped"
111
112
113class TestFalsePlayingFilter:
114 """A uri-less PLAYING report in network mode must not become PLAYING state."""
115
116 def _make_player(self, provider: MagicMock, device: MagicMock) -> WiimPlayer:
117 player = WiimPlayer(provider=provider, player_id="uuid:test", device=device)
118 player.update_state = MagicMock() # type: ignore[misc,method-assign]
119 return player
120
121 def test_false_playing_ack_is_suppressed(
122 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
123 ) -> None:
124 """
125 The transient PLAYING ack without media loaded must keep the previous state.
126
127 The device acks (group) transport commands with a short false PLAYING
128 report before any track is loaded; propagating it causes a
129 PLAYING->IDLE->PLAYING flicker downstream.
130 """
131 mock_wiim_device.play_mode = SOURCE_NETWORK
132 mock_wiim_device.current_media = None
133 mock_wiim_device.playing_status = PlayingStatus.PLAYING
134 player = self._make_player(mock_provider, mock_wiim_device)
135 player._attr_playback_state = PlaybackState.IDLE
136
137 player._update_ma_state_from_sdk_cache()
138
139 assert player._attr_playback_state == PlaybackState.IDLE
140
141 def test_loading_without_uri_is_suppressed(
142 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
143 ) -> None:
144 """LOADING maps to PLAYING and gets the same uri-less filter."""
145 mock_wiim_device.play_mode = SOURCE_NETWORK
146 mock_wiim_device.current_media = None
147 mock_wiim_device.playing_status = PlayingStatus.LOADING
148 player = self._make_player(mock_provider, mock_wiim_device)
149 player._attr_playback_state = PlaybackState.IDLE
150
151 player._update_ma_state_from_sdk_cache()
152
153 assert player._attr_playback_state == PlaybackState.IDLE
154
155 def test_playing_kept_when_uri_drops_mid_playback(
156 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
157 ) -> None:
158 """The filter keeps the previous state; it never forces a playing player idle."""
159 mock_wiim_device.play_mode = SOURCE_NETWORK
160 mock_wiim_device.current_media = None
161 mock_wiim_device.playing_status = PlayingStatus.PLAYING
162 player = self._make_player(mock_provider, mock_wiim_device)
163 player._attr_playback_state = PlaybackState.PLAYING
164
165 player._update_ma_state_from_sdk_cache()
166
167 assert player._attr_playback_state == PlaybackState.PLAYING
168
169 def test_playing_accepted_once_uri_present(
170 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
171 ) -> None:
172 """A PLAYING report with media loaded is a real start and passes through."""
173 media = MagicMock()
174 media.uri = "http://192.168.1.80:8097/single/abc/queue/item/uuid:test.flac"
175 mock_wiim_device.play_mode = SOURCE_NETWORK
176 mock_wiim_device.current_media = media
177 mock_wiim_device.playing_status = PlayingStatus.PLAYING
178 player = self._make_player(mock_provider, mock_wiim_device)
179 player._attr_playback_state = PlaybackState.IDLE
180
181 player._update_ma_state_from_sdk_cache()
182
183 assert player._attr_playback_state == PlaybackState.PLAYING
184
185 def test_external_input_playing_without_uri_accepted(
186 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
187 ) -> None:
188 """External inputs legitimately play without a URI and must not be filtered."""
189 mock_wiim_device.play_mode = "Line In"
190 mock_wiim_device.current_media = None
191 mock_wiim_device.playing_status = PlayingStatus.PLAYING
192 player = self._make_player(mock_provider, mock_wiim_device)
193 player._attr_playback_state = PlaybackState.IDLE
194
195 player._update_ma_state_from_sdk_cache()
196
197 assert player._attr_playback_state == PlaybackState.PLAYING
198
199 def test_unknown_play_mode_trusts_device(
200 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
201 ) -> None:
202 """Without a known play mode the device report is trusted (no suppression)."""
203 mock_wiim_device.play_mode = None
204 mock_wiim_device.current_media = None
205 mock_wiim_device.playing_status = PlayingStatus.PLAYING
206 player = self._make_player(mock_provider, mock_wiim_device)
207 player._attr_playback_state = PlaybackState.IDLE
208
209 player._update_ma_state_from_sdk_cache()
210
211 assert player._attr_playback_state == PlaybackState.PLAYING
212
213 def test_stopped_report_unaffected(
214 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
215 ) -> None:
216 """The filter only guards PLAYING-mapped reports; STOPPED passes through."""
217 mock_wiim_device.play_mode = SOURCE_NETWORK
218 mock_wiim_device.current_media = None
219 mock_wiim_device.playing_status = PlayingStatus.STOPPED
220 player = self._make_player(mock_provider, mock_wiim_device)
221 player._attr_playback_state = PlaybackState.PLAYING
222
223 player._update_ma_state_from_sdk_cache()
224
225 assert player._attr_playback_state == PlaybackState.IDLE
226
227
228class TestSupportedFeatures:
229 """Test that required features are declared."""
230
231 def test_play_media_in_features(
232 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
233 ) -> None:
234 """PLAY_MEDIA should be in supported features."""
235 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
236 assert PlayerFeature.PLAY_MEDIA in player._attr_supported_features
237
238 def test_volume_features(self, mock_provider: MagicMock, mock_wiim_device: MagicMock) -> None:
239 """VOLUME_SET and VOLUME_MUTE should be in supported features."""
240 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
241 assert PlayerFeature.VOLUME_SET in player._attr_supported_features
242 assert PlayerFeature.VOLUME_MUTE in player._attr_supported_features
243
244 def test_select_source_in_features(
245 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
246 ) -> None:
247 """SELECT_SOURCE should be in supported features."""
248 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
249 assert PlayerFeature.SELECT_SOURCE in player._attr_supported_features
250
251
252class TestGroupMembers:
253 """Test group member state handling."""
254
255 def test_unmanaged_group_members_are_ignored(
256 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
257 ) -> None:
258 """Only group members registered in Music Assistant should be reported."""
259 managed_udn = "uuid:test-wiim-002"
260 unmanaged_udn = "uuid:test-linkplay-001"
261 leader_player_id = f"{PLAYER_ID_PREFIX}{mock_wiim_device.udn}"
262 managed_player_id = f"{PLAYER_ID_PREFIX}{managed_udn}"
263 snapshot = mock_provider.wiim_controller.get_group_snapshot.return_value
264 snapshot.role = "leader"
265 snapshot.member_udns = (mock_wiim_device.udn, managed_udn, unmanaged_udn)
266 mock_provider.wiim_controller.get_group_members.side_effect = ValueError(
267 f"Device {unmanaged_udn} is not managed by the controller"
268 )
269 mock_provider.mass.players.get_player.side_effect = {managed_player_id: MagicMock()}.get
270 player = WiimPlayer(
271 provider=mock_provider,
272 player_id=leader_player_id,
273 device=mock_wiim_device,
274 )
275 player.update_state = MagicMock() # type: ignore[misc,method-assign]
276
277 player._update_ma_state_from_sdk_cache()
278
279 assert player._attr_group_members == [leader_player_id, managed_player_id]
280 mock_provider.wiim_controller.get_group_members.assert_not_called()
281
282 def test_only_unmanaged_group_members_reports_no_group(
283 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
284 ) -> None:
285 """A group without any MA-managed followers should not be reported."""
286 snapshot = mock_provider.wiim_controller.get_group_snapshot.return_value
287 snapshot.role = "leader"
288 snapshot.member_udns = (mock_wiim_device.udn, "uuid:test-linkplay-001")
289 mock_provider.mass.players.get_player.return_value = None
290 player = WiimPlayer(
291 provider=mock_provider,
292 player_id=f"{PLAYER_ID_PREFIX}{mock_wiim_device.udn}",
293 device=mock_wiim_device,
294 )
295 player.update_state = MagicMock() # type: ignore[misc,method-assign]
296
297 player._update_ma_state_from_sdk_cache()
298
299 assert player._attr_group_members == []
300
301
302class TestSourceList:
303 """Test dynamic source list construction."""
304
305 @pytest.mark.asyncio
306 async def test_setup_adds_device_input_modes(
307 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
308 ) -> None:
309 """setup() should add sources for device-supported input modes."""
310 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
311 await player.setup()
312 source_ids = [s.id for s in player._attr_source_list]
313 assert "bluetooth" in source_ids
314 assert "line_in" in source_ids
315 assert "optical" in source_ids
316
317 @pytest.mark.asyncio
318 async def test_setup_adds_passive_sources(
319 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
320 ) -> None:
321 """setup() should add passive sources (AirPlay, Spotify)."""
322 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
323 await player.setup()
324 source_ids = [s.id for s in player._attr_source_list]
325 assert "airplay" in source_ids
326 assert "spotify" in source_ids
327
328 @pytest.mark.asyncio
329 async def test_setup_skips_unknown_input_modes(
330 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
331 ) -> None:
332 """setup() should skip input modes not in INPUT_MODE_SOURCES."""
333 mock_wiim_device.supported_input_modes = ("Network", "FutureMode")
334 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
335 await player.setup()
336 source_ids = [s.id for s in player._attr_source_list]
337 assert "futuremode" not in source_ids
338
339
340class TestErrorHandling:
341 """Test that command errors mark device unavailable."""
342
343 @pytest.mark.asyncio
344 async def test_play_error_refreshes_state(
345 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
346 ) -> None:
347 """Play command error should refresh state without marking unavailable."""
348 mock_wiim_device.async_play.side_effect = WiimRequestException("timeout")
349 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
350 player.update_state = MagicMock() # type: ignore[misc,method-assign]
351 await player.play()
352 assert player._attr_available is True
353 player.update_state.assert_called()
354
355 @pytest.mark.skip(
356 reason="volume_set inlines the HTTP fix from wiim PR#18 and no longer calls "
357 "async_set_volume; re-enable when wiim-sdk 0.1.5+ has been released"
358 )
359 @pytest.mark.asyncio
360 async def test_volume_set_error_refreshes_state(
361 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
362 ) -> None:
363 """Volume set error should refresh state without marking unavailable."""
364 mock_wiim_device.async_set_volume.side_effect = WiimDeviceException("disconnected")
365 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
366 player.update_state = MagicMock() # type: ignore[misc,method-assign]
367 await player.volume_set(50)
368 assert player._attr_available is True
369 player.update_state.assert_called()
370
371 @pytest.mark.asyncio
372 async def test_stop_error_refreshes_state(
373 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
374 ) -> None:
375 """Stop command error should refresh state without marking unavailable."""
376 mock_wiim_device.async_stop.side_effect = WiimRequestException("connection lost")
377 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
378 player.update_state = MagicMock() # type: ignore[misc,method-assign]
379 await player.stop()
380 assert player._attr_available is True
381 player.update_state.assert_called()
382
383 @pytest.mark.asyncio
384 async def test_pause_error_refreshes_state(
385 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
386 ) -> None:
387 """Pause command error should refresh state without marking unavailable."""
388 mock_wiim_device.async_pause.side_effect = WiimDeviceException("timeout")
389 player = WiimPlayer(provider=mock_provider, player_id="uuid:test", device=mock_wiim_device)
390 player.update_state = MagicMock() # type: ignore[misc,method-assign]
391 await player.pause()
392 assert player._attr_available is True
393 player.update_state.assert_called()
394
395
396class TestStalePositionOnNewStream:
397 """A new stream handed to the device must not inherit the previous position."""
398
399 def _make_player(self, provider: MagicMock, device: MagicMock) -> WiimPlayer:
400 player = WiimPlayer(provider=provider, player_id="uuid:test", device=device)
401 player.update_state = MagicMock() # type: ignore[misc,method-assign]
402 return player
403
404 @pytest.mark.asyncio
405 async def test_play_media_resets_elapsed_time(
406 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
407 ) -> None:
408 """play_media() must clear the stale elapsed_time anchor from prior content."""
409 stream_url = "http://192.168.1.80:8097/single/abc/queue/item/uuid:test.flac"
410 mock_provider.mass.streams.resolve_stream_url = AsyncMock(return_value=stream_url)
411 player = self._make_player(mock_provider, mock_wiim_device)
412 player._attr_elapsed_time = 273
413 player._attr_elapsed_time_last_updated = 1000.0
414
415 await player.play_media(PlayerMedia(uri="library://track/1", title="Some Track"))
416
417 assert player._attr_elapsed_time == 0
418 assert player._attr_elapsed_time_last_updated is not None
419 assert player._attr_elapsed_time_last_updated > 1000.0
420
421 @pytest.mark.asyncio
422 async def test_play_media_then_sync_position_end_to_end(
423 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
424 ) -> None:
425 """
426 The device still reports the previous AirPlay content after play_media.
427
428 Its metadata makes MA rebuild _attr_current_media from the device's own
429 uri, so the position guard must not key off _attr_current_media.
430 """
431 stream_url = "http://192.168.1.80:8097/single/abc/queue/item/uuid:test.flac"
432 mock_provider.mass.streams.resolve_stream_url = AsyncMock(return_value=stream_url)
433
434 device_media = MagicMock()
435 device_media.uri = "wiimu_airplay"
436 device_media.title = "Previous Song"
437 device_media.artist = "Previous Artist"
438 device_media.album = "Previous Album"
439 device_media.position = 273
440 mock_wiim_device.play_mode = SOURCE_NETWORK
441 mock_wiim_device.current_media = device_media
442 mock_wiim_device.playing_status = PlayingStatus.PLAYING
443
444 player = self._make_player(mock_provider, mock_wiim_device)
445 player._attr_elapsed_time = 273
446
447 await player.play_media(PlayerMedia(uri="library://track/1", title="New Track"))
448 assert player._attr_elapsed_time == 0
449
450 await player._sync_position()
451
452 assert player._attr_elapsed_time == 0
453
454 @pytest.mark.asyncio
455 async def test_failed_play_media_releases_the_guard(
456 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
457 ) -> None:
458 """A device that never took our stream must not stay guarded against its own position."""
459 stream_url = "http://192.168.1.80:8097/single/abc/queue/item/uuid:test.flac"
460 mock_provider.mass.streams.resolve_stream_url = AsyncMock(return_value=stream_url)
461 mock_wiim_device.async_play = AsyncMock(side_effect=WiimDeviceException("boom"))
462 player = self._make_player(mock_provider, mock_wiim_device)
463
464 await player.play_media(PlayerMedia(uri="library://track/1", title="New Track"))
465
466 device_media = MagicMock()
467 device_media.uri = "wiimu_airplay"
468 device_media.position = 42
469 mock_wiim_device.current_media = device_media
470
471 await player._sync_position()
472
473 assert player._attr_elapsed_time == 42
474
475 @pytest.mark.asyncio
476 async def test_sync_position_ignores_position_reported_without_uri(
477 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
478 ) -> None:
479 """The device clears its uri mid-handover while still reporting the old position."""
480 stream_url = "http://192.168.1.80:8097/single/abc/queue/item/uuid:test.flac"
481 mock_provider.mass.streams.resolve_stream_url = AsyncMock(return_value=stream_url)
482
483 device_media = MagicMock()
484 device_media.uri = None
485 device_media.title = None
486 device_media.artist = None
487 device_media.album = None
488 device_media.position = 273
489 mock_wiim_device.play_mode = SOURCE_NETWORK
490 mock_wiim_device.current_media = device_media
491
492 player = self._make_player(mock_provider, mock_wiim_device)
493
494 await player.play_media(PlayerMedia(uri="library://track/1", title="New Track"))
495 await player._sync_position()
496
497 assert player._attr_elapsed_time == 0
498
499 @pytest.mark.asyncio
500 async def test_sync_position_ignores_foreign_uri(
501 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
502 ) -> None:
503 """The device's position is rejected while it hasn't loaded MA's stream uri."""
504 stream_uri = "http://192.168.1.80:8097/single/abc/queue/item/uuid:test.flac"
505 player = self._make_player(mock_provider, mock_wiim_device)
506 player._ma_stream_uri = stream_uri
507 player._attr_elapsed_time = 0
508 player._attr_elapsed_time_last_updated = 1000.0
509
510 device_media = MagicMock()
511 device_media.uri = "wiimu_airplay"
512 device_media.position = 273
513 mock_wiim_device.current_media = device_media
514
515 await player._sync_position()
516
517 assert player._attr_elapsed_time == 0
518 assert player._attr_elapsed_time_last_updated == 1000.0
519
520 @pytest.mark.asyncio
521 async def test_sync_position_accepts_matching_uri_and_clears_guard(
522 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
523 ) -> None:
524 """Once the device reports MA's own uri, its position is trusted and the guard lifts."""
525 stream_uri = "http://192.168.1.80:8097/single/abc/queue/item/uuid:test.flac"
526 mock_provider.mass.streams.resolve_stream_url = AsyncMock(return_value=stream_uri)
527 player = self._make_player(mock_provider, mock_wiim_device)
528
529 device_media = MagicMock()
530 device_media.uri = stream_uri
531 device_media.position = 12
532 mock_wiim_device.current_media = device_media
533
534 await player.play_media(PlayerMedia(uri="library://track/1", title="New Track"))
535 player._attr_elapsed_time_last_updated = 1000.0
536
537 await player._sync_position()
538
539 assert player._attr_elapsed_time == 12
540 assert player._attr_elapsed_time_last_updated > 1000.0
541 assert player._ma_stream_uri is None
542
543 # A later switch to an external source must not be blocked by a stale guard.
544 device_media.uri = "wiimu_airplay"
545 device_media.position = 55
546 await player._sync_position()
547
548 assert player._attr_elapsed_time == 55
549
550 @pytest.mark.asyncio
551 async def test_sync_position_accepts_device_when_ma_not_driving_playback(
552 self, mock_provider: MagicMock, mock_wiim_device: MagicMock
553 ) -> None:
554 """With no stream handed over by MA (external source), the device's position is authoritative."""
555 player = self._make_player(mock_provider, mock_wiim_device)
556 player._ma_stream_uri = None
557 player._attr_elapsed_time = 0
558 player._attr_elapsed_time_last_updated = 1000.0
559
560 device_media = MagicMock()
561 device_media.uri = "wiimu_airplay"
562 device_media.position = 42
563 mock_wiim_device.current_media = device_media
564
565 await player._sync_position()
566
567 assert player._attr_elapsed_time == 42
568 assert player._attr_elapsed_time_last_updated > 1000.0
569