diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 2b4df23b4ccc..db7229c2e15c 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -16,7 +16,7 @@ from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send -from .const import SONOS_SPEAKER_ACTIVITY +from .const import DOMAIN, SONOS_SPEAKER_ACTIVITY from .exception import SonosUpdateError if TYPE_CHECKING: @@ -30,6 +30,8 @@ if TYPE_CHECKING: UID_PREFIX = "RINCON_" UID_POSTFIX = "01400" +UPNP_ERROR_COMMAND_FAILED = "800" + _LOGGER = logging.getLogger(__name__) type _SonosEntitiesType = ( @@ -76,8 +78,24 @@ def soco_error[_T: _SonosEntitiesType, **_P, _R]( if (target := _find_target_identifier(self, args_soco)) is None: raise RuntimeError("Unexpected use of soco_error") from err - message = f"Error calling {function} on {target}: {err}" - raise SonosUpdateError(message) from err + translation_key = "call_failed" + placeholders = { + "target": target, + "error": str(err), + } + + if error_code is not None: + translation_key = "upnp_call_failed" + placeholders["error_code"] = str(error_code) + + if str(error_code) == UPNP_ERROR_COMMAND_FAILED: + translation_key = "upnp_call_failed_music_service_unavailable" + + raise SonosUpdateError( + translation_domain=DOMAIN, + translation_key=translation_key, + translation_placeholders=placeholders, + ) from err dispatch_soco = args_soco or self.soco # type: ignore[union-attr] dispatcher_send( diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index f2e01da70fa3..44276051d9da 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -109,6 +109,9 @@ "announce_media_error": { "message": "Announcing clip {media_id} failed {response}" }, + "call_failed": { + "message": "Error on {target}: {error}" + }, "entity_not_found": { "message": "Entity {entity_id} not found." }, @@ -141,6 +144,12 @@ }, "toggle_failed": { "message": "Could not toggle {entity_id}." + }, + "upnp_call_failed": { + "message": "Error on {target} (UPnP error code {error_code}): {error}" + }, + "upnp_call_failed_music_service_unavailable": { + "message": "Error on {target} (UPnP error code {error_code}): {error}. This may indicate the selected music service is not available on the speaker." } }, "issues": { diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 4dac45065347..18db04c55ca3 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -13,6 +13,7 @@ from soco.data_structures import ( DidlPlaylistContainer, SearchResult, ) +from soco.exceptions import SoCoUPnPException from sonos_websocket.exception import SonosWebsocketError from syrupy.assertion import SnapshotAssertion @@ -326,6 +327,69 @@ async def test_play_media_library_content_error( ) +@pytest.mark.parametrize( + ("error", "translation_key", "translation_placeholders"), + [ + pytest.param( + OSError("Network down"), + "call_failed", + { + "target": "media_player.zone_a", + "error": "Network down", + }, + id="generic-error", + ), + pytest.param( + SoCoUPnPException("UPnP Error 701 received", "701", ""), + "upnp_call_failed", + { + "target": "media_player.zone_a", + "error": "UPnP Error 701 received", + "error_code": "701", + }, + id="upnp-error", + ), + pytest.param( + SoCoUPnPException("UPnP Error 800 received", "800", ""), + "upnp_call_failed_music_service_unavailable", + { + "target": "media_player.zone_a", + "error": "UPnP Error 800 received", + "error_code": "800", + }, + id="upnp-error-800-music-service-unavailable", + ), + ], +) +async def test_play_media_error_translation( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + error: Exception, + translation_key: str, + translation_placeholders: dict[str, str], +) -> None: + """Test play_media surfaces translated error details for failures.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.play_uri.side_effect = error + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "track", + ATTR_MEDIA_CONTENT_ID: _track_url, + ATTR_MEDIA_ENQUEUE: MediaPlayerEnqueue.REPLACE, + }, + blocking=True, + ) + + assert err.value.translation_key == translation_key + assert err.value.translation_placeholders == translation_placeholders + + _track_url = "S://192.168.42.100/music/iTunes/The%20Beatles/A%20Hard%20Day%2fs%I%20Should%20Have%20Known%20Better.mp3"