/
/
/
1"""
2Tests for Player model final volume/mute state resolution.
3
4Covers __final_volume_level and __final_volume_muted_state, which read the
5final volume/mute exclusively from whichever control volume_control/mute_control
6resolves to (native, a protocol player, or an external player control). The
7final state is None (unknown) whenever that resolved control itself reports
8no value.
9"""
10
11from __future__ import annotations
12
13from unittest.mock import MagicMock
14
15import pytest
16from music_assistant_models.enums import PlayerFeature
17
18from music_assistant.constants import CONF_MUTE_CONTROL, CONF_VOLUME_CONTROL
19from tests.common import MockPlayer, MockProvider
20
21
22@pytest.fixture
23def mock_mass() -> MagicMock:
24 """Create a mock MusicAssistant instance."""
25 mass = MagicMock()
26 mass.closing = False
27 mass.loop = None
28 mass.config.get = MagicMock(return_value=[])
29 mass.config.get_raw_player_config_value = MagicMock(return_value=None)
30 mass.config.get_raw_core_config_value = MagicMock(return_value="GLOBAL")
31 mass.config.set = MagicMock()
32 mass.signal_event = MagicMock()
33 mass.get_providers = MagicMock(return_value=[])
34 return mass
35
36
37class TestFinalVolumeLevel:
38 """Test __final_volume_level resolution against the control volume_control resolves to."""
39
40 def test_uses_control_player_when_available(self, mock_mass: MagicMock) -> None:
41 """Final volume uses the control player's volume when it is present."""
42 mock_mass.config.get_raw_player_config_value = MagicMock(
43 side_effect=lambda _pid, key, *_a: (
44 "control_player" if key == CONF_VOLUME_CONTROL else None
45 )
46 )
47 provider = MockProvider("test", mass=mock_mass)
48 player = MockPlayer(provider, "main_player", "Main")
49 player._attr_volume_level = 30
50
51 control = MagicMock()
52 control.volume_level = 80
53 mock_mass.players.get_player = MagicMock(return_value=control)
54 mock_mass.players.get_player_control = MagicMock(return_value=None)
55 # With default min=0, max=100, scaling is identity
56 mock_mass.players.scale_volume_from_device = MagicMock(side_effect=lambda _pid, vol: vol)
57
58 player.update_state(signal_event=False)
59 assert player.state.volume_level == 80
60
61 def test_falls_back_to_native_when_control_player_missing(self, mock_mass: MagicMock) -> None:
62 """Final volume falls back to native when the control player doesn't exist."""
63 mock_mass.config.get_raw_player_config_value = MagicMock(
64 side_effect=lambda _pid, key, *_a: (
65 "missing_player" if key == CONF_VOLUME_CONTROL else None
66 )
67 )
68 provider = MockProvider("test", mass=mock_mass)
69 player = MockPlayer(provider, "main_player", "Main")
70 player._attr_volume_level = 55
71
72 mock_mass.players.get_player = MagicMock(return_value=None)
73 mock_mass.players.get_player_control = MagicMock(return_value=None)
74 # With default min=0, max=100, scaling is identity
75 mock_mass.players.scale_volume_from_device = MagicMock(side_effect=lambda _pid, vol: vol)
76
77 player.update_state(signal_event=False)
78 assert player.state.volume_level == 55
79
80 def test_reports_unknown_when_control_has_no_volume(self, mock_mass: MagicMock) -> None:
81 """Final volume is unknown when the resolved control's volume is None."""
82 mock_mass.config.get_raw_player_config_value = MagicMock(
83 side_effect=lambda _pid, key, *_a: (
84 "control_player" if key == CONF_VOLUME_CONTROL else None
85 )
86 )
87 provider = MockProvider("test", mass=mock_mass)
88 player = MockPlayer(provider, "main_player", "Main")
89 player._attr_volume_level = 42
90
91 control = MagicMock()
92 control.volume_level = None
93 mock_mass.players.get_player = MagicMock(return_value=control)
94 mock_mass.players.get_player_control = MagicMock(return_value=None)
95 # With default min=0, max=100, scaling is identity
96 mock_mass.players.scale_volume_from_device = MagicMock(side_effect=lambda _pid, vol: vol)
97
98 player.update_state(signal_event=False)
99 assert player.state.volume_level is None
100
101 def test_uses_zero_from_external_control_player(self, mock_mass: MagicMock) -> None:
102 """Volume 0 from an external (non-linked) control player is trusted as genuine."""
103 mock_mass.config.get_raw_player_config_value = MagicMock(
104 side_effect=lambda _pid, key, *_a: "amplifier" if key == CONF_VOLUME_CONTROL else None
105 )
106 provider = MockProvider("test", mass=mock_mass)
107 player = MockPlayer(provider, "main_player", "Main")
108 player._attr_volume_level = 55
109
110 control = MagicMock()
111 control.player_id = "amplifier"
112 control.volume_level = 0
113 mock_mass.players.get_player = MagicMock(return_value=control)
114 mock_mass.players.get_player_control = MagicMock(return_value=None)
115 # With default min=0, max=100, scaling is identity
116 mock_mass.players.scale_volume_from_device = MagicMock(side_effect=lambda _pid, vol: vol)
117
118 player.update_state(signal_event=False)
119 assert player.state.volume_level == 0
120
121
122class TestFinalVolumeMutedState:
123 """Test __final_volume_muted_state resolution against the control mute_control resolves to."""
124
125 def test_uses_control_player_when_available(self, mock_mass: MagicMock) -> None:
126 """Final mute state uses the control player's mute state when present."""
127 mock_mass.config.get_raw_player_config_value = MagicMock(
128 side_effect=lambda _pid, key, default=None: (
129 "control_player"
130 if key == CONF_MUTE_CONTROL
131 else 0
132 if key == "min_volume"
133 else 100
134 if key == "max_volume"
135 else default
136 ),
137 )
138 provider = MockProvider("test", mass=mock_mass)
139 player = MockPlayer(provider, "main_player", "Main")
140 player._attr_supported_features.add(PlayerFeature.VOLUME_MUTE)
141 player._attr_volume_muted = False
142
143 control = MagicMock()
144 control.volume_muted = True
145 mock_mass.players.get_player = MagicMock(return_value=control)
146 mock_mass.players.get_player_control = MagicMock(return_value=None)
147
148 player.update_state(signal_event=False)
149 assert player.state.volume_muted is True
150
151 def test_falls_back_to_native_when_control_player_missing(self, mock_mass: MagicMock) -> None:
152 """Final mute state falls back to native when the control player doesn't exist."""
153 mock_mass.config.get_raw_player_config_value = MagicMock(
154 side_effect=lambda _pid, key, default=None: (
155 "missing_player"
156 if key == CONF_MUTE_CONTROL
157 else 0
158 if key == "min_volume"
159 else 100
160 if key == "max_volume"
161 else default
162 ),
163 )
164 provider = MockProvider("test", mass=mock_mass)
165 player = MockPlayer(provider, "main_player", "Main")
166 player._attr_supported_features.add(PlayerFeature.VOLUME_MUTE)
167 player._attr_volume_muted = True
168
169 mock_mass.players.get_player = MagicMock(return_value=None)
170 mock_mass.players.get_player_control = MagicMock(return_value=None)
171
172 player.update_state(signal_event=False)
173 assert player.state.volume_muted is True
174
175 def test_reports_unknown_when_control_has_no_mute_state(self, mock_mass: MagicMock) -> None:
176 """Final mute state is unknown when the resolved control's mute state is None."""
177 mock_mass.config.get_raw_player_config_value = MagicMock(
178 side_effect=lambda _pid, key, default=None: (
179 "control_player"
180 if key == CONF_MUTE_CONTROL
181 else 0
182 if key == "min_volume"
183 else 100
184 if key == "max_volume"
185 else default
186 ),
187 )
188 provider = MockProvider("test", mass=mock_mass)
189 player = MockPlayer(provider, "main_player", "Main")
190 player._attr_supported_features.add(PlayerFeature.VOLUME_MUTE)
191 player._attr_volume_muted = False
192
193 control = MagicMock()
194 control.volume_muted = None
195 mock_mass.players.get_player = MagicMock(return_value=control)
196 mock_mass.players.get_player_control = MagicMock(return_value=None)
197
198 player.update_state(signal_event=False)
199 assert player.state.volume_muted is None
200