/
/
/
1"""Tests for the state the DLNA player reads from its device."""
2
3from __future__ import annotations
4
5from datetime import UTC, datetime
6from typing import Any
7from unittest.mock import MagicMock
8
9import pytest
10from async_upnp_client.profiles.dlna import TransportState
11
12from music_assistant.providers.dlna.player import DLNAPlayer
13from tests.common import MockProvider
14
15REPORTED_AT = datetime(2026, 8, 7, 12, 0, 0, tzinfo=UTC)
16STALE_POSITION = 120.0
17STALE_REPORTED_AT = 1000.0
18
19
20async def _updated_player(**device_state: Any) -> DLNAPlayer:
21 """
22 Run a state update on a player that already knows a position, and return it.
23
24 :param device_state: Attributes to override on the fully reporting mocked device.
25 """
26 provider = MockProvider("dlna", instance_id="dlna_test")
27 provider.mass.streams.base_url = "http://192.168.1.2:8097"
28
29 device = MagicMock()
30 device.profile_device.available = True
31 device.name = "Living Room Renderer"
32 device.volume_level = 0.5
33 device.is_volume_muted = False
34 device.transport_state = TransportState.PLAYING
35 device.current_track_uri = "http://192.168.1.10/stream.mp3"
36 device.media_title = "Test Title"
37 device.media_artist = "Test Artist"
38 device.media_album_name = "Test Album"
39 device.media_image_url = "http://192.168.1.10/cover.jpg"
40 device.media_duration = 240
41 device.media_position = 42
42 device.media_position_updated_at = REPORTED_AT
43 for name, value in device_state.items():
44 setattr(device, name, value)
45
46 player = DLNAPlayer(
47 provider, # type: ignore[arg-type]
48 "uuid:dlna-player",
49 "http://192.168.1.10/description.xml",
50 device=device,
51 )
52 player._attr_elapsed_time = STALE_POSITION
53 player._attr_elapsed_time_last_updated = STALE_REPORTED_AT
54
55 await player.set_dynamic_attributes()
56 return player
57
58
59async def test_zero_position_replaces_the_previous_one() -> None:
60 """A device restarting a track reports 0, which must not be read as 'unknown'."""
61 player = await _updated_player(media_position=0)
62
63 assert player.elapsed_time == 0.0
64 assert player.elapsed_time_last_updated == REPORTED_AT.timestamp()
65
66
67async def test_known_position_is_applied() -> None:
68 """A position reported by the device is adopted together with its timestamp."""
69 player = await _updated_player(media_position=42)
70
71 assert player.elapsed_time == 42.0
72 assert player.elapsed_time_last_updated == REPORTED_AT.timestamp()
73
74
75async def test_missing_position_keeps_the_previous_one() -> None:
76 """A device that reports no position at all leaves the known position alone."""
77 player = await _updated_player(media_position=None)
78
79 assert player.elapsed_time == STALE_POSITION
80 assert player.elapsed_time_last_updated == STALE_REPORTED_AT
81
82
83@pytest.mark.parametrize(("reported", "expected"), [(0.5, 50), (0.0, 0), (1.0, 100)])
84async def test_volume_level_is_scaled_to_percent(reported: float, expected: int) -> None:
85 """The device reports volume as a 0..1 fraction, the player as a percentage."""
86 player = await _updated_player(volume_level=reported)
87
88 assert player.volume_level == expected
89
90
91async def test_unknown_volume_level_stays_unknown() -> None:
92 """A device that does not report its volume must not read as volume 0."""
93 player = await _updated_player(volume_level=None)
94
95 assert player.volume_level is None
96
97
98@pytest.mark.parametrize("reported", [True, False])
99async def test_mute_state_is_applied(reported: bool) -> None:
100 """A mute state reported by the device is adopted as-is."""
101 player = await _updated_player(is_volume_muted=reported)
102
103 assert player.volume_muted is reported
104
105
106async def test_unknown_mute_state_stays_unknown() -> None:
107 """A device that does not report its mute state must not read as unmuted."""
108 player = await _updated_player(is_volume_muted=None)
109
110 assert player.volume_muted is None
111