diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index 80a693c50135..4a67f5758172 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -204,7 +204,26 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): async def sync_media_state(self) -> None: """Sync media state.""" - await self.api.sync_media_state() + try: + await self.api.sync_media_state() + except CannotAuthenticate as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_auth", + translation_placeholders={"error": repr(err)}, + ) from err + except (CannotConnect, TimeoutError) as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="cannot_connect_with_error", + translation_placeholders={"error": repr(err)}, + ) from err + except (CannotRetrieveData, ValueError) as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="cannot_retrieve_data_with_error", + translation_placeholders={"error": repr(err)}, + ) from err async def media_state_event_handler( self, media_state: dict[str, AmazonMediaState] diff --git a/homeassistant/components/alexa_devices/media_player.py b/homeassistant/components/alexa_devices/media_player.py index e222ab02820c..fe94b98b1184 100644 --- a/homeassistant/components/alexa_devices/media_player.py +++ b/homeassistant/components/alexa_devices/media_player.py @@ -156,9 +156,11 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity): @property def is_volume_muted(self) -> bool | None: """Return True if the volume is muted.""" - if not self.volume_state: + if not self.volume_state or self.volume_state.volume is None: return None - return self.volume_state.volume == 0 + # is_muted is True when Alexa has muted the device + # volume == 0 is where we have muted by setting volume to 0 + return self.volume_state.is_muted or self.volume_state.volume == 0 @property def media_title(self) -> str | None: @@ -259,12 +261,20 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity): return if mute: self._prev_volume = self.volume_state.volume - target_volume = 0 - else: - if self._prev_volume is None: - return - target_volume = self._prev_volume + await self.async_set_volume_level(0) + return + + if self.volume_state.is_muted and self._prev_volume is None: + # is muted by Alexa which we can see but not control + # when muted this way, volume is still set + # changing volume will unmute + # if HA set volume to 0 then Alexa muted we just default to 30% + self._prev_volume = self.volume_state.volume or 30 + if self._prev_volume is None: + return + target_volume = self._prev_volume await self.async_set_volume_level(target_volume / 100) + self._prev_volume = None @alexa_api_call async def _send_media_command(self, command: AmazonMediaControls) -> None: diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index 0ec611a8d62c..fcb8ab13b9c5 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -125,6 +125,9 @@ }, "invalid_sound_value": { "message": "Invalid sound {sound} specified" + }, + "unknown_exception": { + "message": "Unknown error occurred: {error}" } }, "selector": { diff --git a/tests/components/alexa_devices/test_coordinator.py b/tests/components/alexa_devices/test_coordinator.py index 73854f0de76d..427d38e3a4b2 100644 --- a/tests/components/alexa_devices/test_coordinator.py +++ b/tests/components/alexa_devices/test_coordinator.py @@ -126,3 +126,48 @@ async def test_sync_history_state_error( await hass.async_block_till_done() assert mock_config_entry.state is expected_state + + +@pytest.mark.parametrize( + ("side_effect", "expected_state"), + [ + pytest.param( + CannotAuthenticate, + ConfigEntryState.SETUP_ERROR, + id="cannot_authenticate", + ), + pytest.param( + CannotConnect, + ConfigEntryState.SETUP_RETRY, + id="cannot_connect", + ), + pytest.param( + TimeoutError, + ConfigEntryState.SETUP_RETRY, + id="timeout_error", + ), + pytest.param( + CannotRetrieveData, + ConfigEntryState.SETUP_RETRY, + id="cannot_retrieve_data", + ), + pytest.param( + ValueError, + ConfigEntryState.SETUP_RETRY, + id="value_error", + ), + ], +) +async def test_sync_media_state_auth_failed( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: type[Exception], + expected_state: ConfigEntryState, +) -> None: + """Test setup fails with ConfigEntryAuthFailed when sync_media_state raises CannotAuthenticate.""" + mock_amazon_devices_client.sync_media_state.side_effect = side_effect + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is expected_state diff --git a/tests/components/alexa_devices/test_media_player.py b/tests/components/alexa_devices/test_media_player.py index 8d1399514e73..c470eeafa473 100644 --- a/tests/components/alexa_devices/test_media_player.py +++ b/tests/components/alexa_devices/test_media_player.py @@ -700,3 +700,28 @@ async def test_unmute_volume_without_prev_volume_returns_early( ) mock_amazon_devices_client.set_device_volume.assert_not_awaited() + + +async def test_unmute_volume_when_alexa_muted_restores_current_volume( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Unmute restores current volume when device was muted directly by Alexa.""" + await _setup_media_player_platform(hass, mock_config_entry) + + await _push_volume_state( + mock_amazon_devices_client, + volume_state=AmazonVolumeState(volume=30, is_muted=True), + ) + await hass.async_block_till_done() + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: False}, + blocking=True, + ) + + mock_amazon_devices_client.set_device_volume.assert_awaited_once() + assert mock_amazon_devices_client.set_device_volume.call_args.args[1] == 30