1
0
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:
peteS-UK
2025-10-23 14:13:22 +01:00
committed by GitHub
parent 21ab630380
commit 3019744035
4 changed files with 220 additions and 39 deletions

View File

@@ -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},
)

View File

@@ -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

View File

@@ -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}."
}
}
}

View 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