mirror of
https://github.com/home-assistant/core.git
synced 2025-12-24 21:06:19 +00:00
Add exception handling for library calls in Squeezebox (#154946)
This commit is contained in:
@@ -15,6 +15,7 @@ from . import SqueezeboxConfigEntry
|
||||
from .const import SIGNAL_PLAYER_DISCOVERED
|
||||
from .coordinator import SqueezeBoxPlayerUpdateCoordinator
|
||||
from .entity import SqueezeboxEntity
|
||||
from .util import safe_library_call
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -157,4 +158,10 @@ class SqueezeboxButtonEntity(SqueezeboxEntity, ButtonEntity):
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Execute the button action."""
|
||||
await self._player.async_query("button", self.entity_description.press_action)
|
||||
await safe_library_call(
|
||||
self._player.async_query,
|
||||
"button",
|
||||
self.entity_description.press_action,
|
||||
translation_key="press_failed",
|
||||
translation_placeholders={"action": self.entity_description.press_action},
|
||||
)
|
||||
|
||||
@@ -70,6 +70,7 @@ from .const import (
|
||||
)
|
||||
from .coordinator import SqueezeBoxPlayerUpdateCoordinator
|
||||
from .entity import SqueezeboxEntity
|
||||
from .util import safe_library_call
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import SqueezeboxConfigEntry
|
||||
@@ -433,58 +434,98 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off media player."""
|
||||
await self._player.async_set_power(False)
|
||||
await safe_library_call(
|
||||
self._player.async_set_power, False, translation_key="turn_off_failed"
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
volume_percent = str(round(volume * 100))
|
||||
await self._player.async_set_volume(volume_percent)
|
||||
await safe_library_call(
|
||||
self._player.async_set_volume,
|
||||
volume_percent,
|
||||
translation_key="set_volume_failed",
|
||||
translation_placeholders={"volume": volume_percent},
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
await self._player.async_set_muting(mute)
|
||||
await safe_library_call(
|
||||
self._player.async_set_muting,
|
||||
mute,
|
||||
translation_key="set_mute_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command to media player."""
|
||||
await self._player.async_stop()
|
||||
await safe_library_call(
|
||||
self._player.async_stop,
|
||||
translation_key="stop_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Send pause command to media player."""
|
||||
await self._player.async_toggle_pause()
|
||||
"""Send pause/play toggle command to media player."""
|
||||
await safe_library_call(
|
||||
self._player.async_toggle_pause,
|
||||
translation_key="play_pause_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command to media player."""
|
||||
await self._player.async_play()
|
||||
await safe_library_call(
|
||||
self._player.async_play,
|
||||
translation_key="play_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command to media player."""
|
||||
await self._player.async_pause()
|
||||
await safe_library_call(
|
||||
self._player.async_pause,
|
||||
translation_key="pause_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
await self._player.async_index("+1")
|
||||
await safe_library_call(
|
||||
self._player.async_index,
|
||||
"+1",
|
||||
translation_key="next_track_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
await self._player.async_index("-1")
|
||||
"""Send previous track command."""
|
||||
await safe_library_call(
|
||||
self._player.async_index,
|
||||
"-1",
|
||||
translation_key="previous_track_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_media_seek(self, position: float) -> None:
|
||||
"""Send seek command."""
|
||||
await self._player.async_time(position)
|
||||
await safe_library_call(
|
||||
self._player.async_time,
|
||||
position,
|
||||
translation_key="seek_failed",
|
||||
translation_placeholders={"position": position},
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the media player on."""
|
||||
await self._player.async_set_power(True)
|
||||
await safe_library_call(
|
||||
self._player.async_set_power,
|
||||
True,
|
||||
translation_key="turn_on_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_play_media(
|
||||
@@ -523,9 +564,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_announce_media_type",
|
||||
translation_placeholders={
|
||||
"media_type": str(media_type),
|
||||
},
|
||||
translation_placeholders={"media_type": str(media_type)},
|
||||
)
|
||||
|
||||
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
|
||||
@@ -536,9 +575,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_announce_volume",
|
||||
translation_placeholders={
|
||||
"announce_volume": ATTR_ANNOUNCE_VOLUME,
|
||||
},
|
||||
translation_placeholders={"announce_volume": ATTR_ANNOUNCE_VOLUME},
|
||||
) from None
|
||||
else:
|
||||
self._player.set_announce_volume(announce_volume)
|
||||
@@ -550,7 +587,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_announce_timeout",
|
||||
translation_placeholders={
|
||||
"announce_timeout": ATTR_ANNOUNCE_TIMEOUT,
|
||||
"announce_timeout": ATTR_ANNOUNCE_TIMEOUT
|
||||
},
|
||||
) from None
|
||||
else:
|
||||
@@ -558,15 +595,19 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
|
||||
if media_type in MediaType.MUSIC:
|
||||
if not media_id.startswith(SQUEEZEBOX_SOURCE_STRINGS):
|
||||
# do not process special squeezebox "source" media ids
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
|
||||
await self._player.async_load_url(media_id, cmd)
|
||||
await safe_library_call(
|
||||
self._player.async_load_url,
|
||||
media_id,
|
||||
cmd,
|
||||
translation_key="load_url_failed",
|
||||
translation_placeholders={"media_id": media_id, "cmd": cmd},
|
||||
)
|
||||
return
|
||||
|
||||
if media_type == MediaType.PLAYLIST:
|
||||
try:
|
||||
# a saved playlist by number
|
||||
payload = {
|
||||
"search_id": media_id,
|
||||
"search_type": MediaType.PLAYLIST,
|
||||
@@ -575,7 +616,6 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
self._player, payload, self.browse_limit, self._browse_data
|
||||
)
|
||||
except BrowseError:
|
||||
# a list of urls
|
||||
content = json.loads(media_id)
|
||||
playlist = content["urls"]
|
||||
index = content["index"]
|
||||
@@ -587,12 +627,19 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
playlist = await generate_playlist(
|
||||
self._player, payload, self.browse_limit, self._browse_data
|
||||
)
|
||||
|
||||
_LOGGER.debug("Generated playlist: %s", playlist)
|
||||
|
||||
await self._player.async_load_playlist(playlist, cmd)
|
||||
await safe_library_call(
|
||||
self._player.async_load_playlist,
|
||||
playlist,
|
||||
cmd,
|
||||
translation_key="load_playlist_failed",
|
||||
translation_placeholders={"cmd": cmd},
|
||||
)
|
||||
|
||||
if index is not None:
|
||||
await self._player.async_index(index)
|
||||
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_search_media(
|
||||
@@ -672,18 +719,29 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
else:
|
||||
repeat_mode = "none"
|
||||
|
||||
await self._player.async_set_repeat(repeat_mode)
|
||||
await safe_library_call(
|
||||
self._player.async_set_repeat,
|
||||
repeat_mode,
|
||||
translation_key="set_repeat_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_set_shuffle(self, shuffle: bool) -> None:
|
||||
"""Enable/disable shuffle mode."""
|
||||
"""Enable or disable shuffle mode."""
|
||||
shuffle_mode = "song" if shuffle else "none"
|
||||
await self._player.async_set_shuffle(shuffle_mode)
|
||||
await safe_library_call(
|
||||
self._player.async_set_shuffle,
|
||||
shuffle_mode,
|
||||
translation_key="set_shuffle_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_clear_playlist(self) -> None:
|
||||
"""Send the media player the command for clear playlist."""
|
||||
await self._player.async_clear_playlist()
|
||||
"""Send the media player the command to clear the playlist."""
|
||||
await safe_library_call(
|
||||
self._player.async_clear_playlist,
|
||||
translation_key="clear_playlist_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_call_method(
|
||||
@@ -692,12 +750,18 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
"""Call Squeezebox JSON/RPC method.
|
||||
|
||||
Additional parameters are added to the command to form the list of
|
||||
positional parameters (p0, p1..., pN) passed to JSON/RPC server.
|
||||
positional parameters (p0, p1..., pN) passed to JSON/RPC server.
|
||||
"""
|
||||
all_params = [command]
|
||||
if parameters:
|
||||
all_params.extend(parameters)
|
||||
await self._player.async_query(*all_params)
|
||||
|
||||
await safe_library_call(
|
||||
self._player.async_query,
|
||||
*all_params,
|
||||
translation_key="call_method_failed",
|
||||
translation_placeholders={"command": command},
|
||||
)
|
||||
|
||||
async def async_call_query(
|
||||
self, command: str, parameters: list[str] | None = None
|
||||
@@ -705,12 +769,18 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
"""Call Squeezebox JSON/RPC method where we care about the result.
|
||||
|
||||
Additional parameters are added to the command to form the list of
|
||||
positional parameters (p0, p1..., pN) passed to JSON/RPC server.
|
||||
positional parameters (p0, p1..., pN) passed to JSON/RPC server.
|
||||
"""
|
||||
all_params = [command]
|
||||
if parameters:
|
||||
all_params.extend(parameters)
|
||||
self._query_result = await self._player.async_query(*all_params)
|
||||
|
||||
self._query_result = await safe_library_call(
|
||||
self._player.async_query,
|
||||
*all_params,
|
||||
translation_key="call_query_failed",
|
||||
translation_placeholders={"command": command},
|
||||
)
|
||||
_LOGGER.debug("call_query got result %s", self._query_result)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -744,7 +814,10 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_unjoin_player(self) -> None:
|
||||
"""Unsync this Squeezebox player."""
|
||||
await self._player.async_unsync()
|
||||
await safe_library_call(
|
||||
self._player.async_unsync,
|
||||
translation_key="unjoin_failed",
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
def get_synthetic_id_and_cache_url(self, url: str) -> str:
|
||||
@@ -808,14 +881,19 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
image_url = self._synthetic_media_browser_thumbnail_items.get(
|
||||
media_image_id
|
||||
)
|
||||
|
||||
if image_url is None:
|
||||
_LOGGER.debug("Synthetic ID %s not found in cache", media_image_id)
|
||||
return (None, None)
|
||||
else:
|
||||
image_url = self._player.generate_image_url_from_track_id(media_image_id)
|
||||
image_url = await safe_library_call(
|
||||
self._player.generate_image_url_from_track_id,
|
||||
media_image_id,
|
||||
translation_key="generate_image_url_failed",
|
||||
translation_placeholders={"track_id": media_image_id},
|
||||
)
|
||||
|
||||
result = await self._async_fetch_image(image_url)
|
||||
if result == (None, None):
|
||||
_LOGGER.debug("Error retrieving proxied album art from %s", image_url)
|
||||
|
||||
return result
|
||||
|
||||
@@ -207,6 +207,69 @@
|
||||
},
|
||||
"invalid_search_media_content_type": {
|
||||
"message": "If specified, Media content type must be one of {media_content_type}"
|
||||
},
|
||||
"turn_on_failed": {
|
||||
"message": "Failed to turn on the player."
|
||||
},
|
||||
"turn_off_failed": {
|
||||
"message": "Failed to turn off the player."
|
||||
},
|
||||
"set_shuffle_failed": {
|
||||
"message": "Failed to set shuffle mode."
|
||||
},
|
||||
"set_volume_failed": {
|
||||
"message": "Failed to set volume to {volume}%."
|
||||
},
|
||||
"set_mute_failed": {
|
||||
"message": "Failed to mute/unmute the player."
|
||||
},
|
||||
"stop_failed": {
|
||||
"message": "Failed to stop playback."
|
||||
},
|
||||
"play_pause_failed": {
|
||||
"message": "Failed to toggle play/pause."
|
||||
},
|
||||
"play_failed": {
|
||||
"message": "Failed to start playback."
|
||||
},
|
||||
"pause_failed": {
|
||||
"message": "Failed to pause playback."
|
||||
},
|
||||
"next_track_failed": {
|
||||
"message": "Failed to skip to the next track."
|
||||
},
|
||||
"previous_track_failed": {
|
||||
"message": "Failed to return to the previous track."
|
||||
},
|
||||
"seek_failed": {
|
||||
"message": "Failed to seek to position {position} seconds."
|
||||
},
|
||||
"set_repeat_failed": {
|
||||
"message": "Failed to set repeat mode."
|
||||
},
|
||||
"clear_playlist_failed": {
|
||||
"message": "Failed to clear the playlist."
|
||||
},
|
||||
"call_method_failed": {
|
||||
"message": "Failed to call method {command}."
|
||||
},
|
||||
"call_query_failed": {
|
||||
"message": "Failed to query method {command}."
|
||||
},
|
||||
"unjoin_failed": {
|
||||
"message": "Failed to unsync the player."
|
||||
},
|
||||
"press_failed": {
|
||||
"message": "Failed to execute button action {action}."
|
||||
},
|
||||
"load_url_failed": {
|
||||
"message": "Failed to load media URL {media_id} with command {cmd}."
|
||||
},
|
||||
"load_playlist_failed": {
|
||||
"message": "Failed to load playlist with command {cmd}."
|
||||
},
|
||||
"generate_image_url_failed": {
|
||||
"message": "Failed to generate image URL for track ID {track_id}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
homeassistant/components/squeezebox/util.py
Normal file
33
homeassistant/components/squeezebox/util.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Utility functions for Squeezebox integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def safe_library_call(
|
||||
method: Callable[..., Awaitable[Any]],
|
||||
*args: Any,
|
||||
translation_key: str,
|
||||
translation_placeholders: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Call a player method safely and raise HomeAssistantError on failure."""
|
||||
try:
|
||||
result = await method(*args, **kwargs)
|
||||
except ValueError:
|
||||
result = None
|
||||
|
||||
if result is False or result is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=translation_placeholders,
|
||||
)
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user