Add support for announce to Squeezebox media player (#129460)

* initial

* Add support for announce: true to media player

* Move play_announcement to _player

* update snapshot

* conftest update

* remove conftest update

* Update conftest.py

* Test Updates

* Updates post moving functions to library

* test fixes

* Review updates

* Snapshot update

* rebase updates

* Merge updates

* Review updates

* Review updates
pull/136885/head
peteS-UK 2025-02-18 17:22:19 +00:00 committed by GitHub
parent a45fb57595
commit d1f0e0a70f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 182 additions and 3 deletions

View File

@ -36,3 +36,5 @@ CONF_BROWSE_LIMIT = "browse_limit"
CONF_VOLUME_STEP = "volume_step"
DEFAULT_BROWSE_LIMIT = 1000
DEFAULT_VOLUME_STEP = 5
ATTR_ANNOUNCE_VOLUME = "announce_volume"
ATTR_ANNOUNCE_TIMEOUT = "announce_timeout"

View File

@ -14,6 +14,7 @@ import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_EXTRA,
BrowseError,
BrowseMedia,
MediaPlayerEnqueue,
@ -52,6 +53,8 @@ from .browse_media import (
media_source_content_filter,
)
from .const import (
ATTR_ANNOUNCE_TIMEOUT,
ATTR_ANNOUNCE_VOLUME,
CONF_BROWSE_LIMIT,
CONF_VOLUME_STEP,
DEFAULT_BROWSE_LIMIT,
@ -157,6 +160,26 @@ async def async_setup_entry(
entry.async_on_unload(async_at_start(hass, start_server_discovery))
def get_announce_volume(extra: dict) -> float | None:
"""Get announce volume from extra service data."""
if ATTR_ANNOUNCE_VOLUME not in extra:
return None
announce_volume = float(extra[ATTR_ANNOUNCE_VOLUME])
if not (0 < announce_volume <= 1):
raise ValueError
return announce_volume * 100
def get_announce_timeout(extra: dict) -> int | None:
"""Get announce volume from extra service data."""
if ATTR_ANNOUNCE_TIMEOUT not in extra:
return None
announce_timeout = int(extra[ATTR_ANNOUNCE_TIMEOUT])
if announce_timeout < 1:
raise ValueError
return announce_timeout
class SqueezeBoxMediaPlayerEntity(
CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator], MediaPlayerEntity
):
@ -184,6 +207,7 @@ class SqueezeBoxMediaPlayerEntity(
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.GROUPING
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
)
_attr_has_entity_name = True
_attr_name = None
@ -437,7 +461,11 @@ class SqueezeBoxMediaPlayerEntity(
await self.coordinator.async_refresh()
async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
self,
media_type: MediaType | str,
media_id: str,
announce: bool | None = None,
**kwargs: Any,
) -> None:
"""Send the play_media command to the media player."""
index = None
@ -460,6 +488,32 @@ class SqueezeBoxMediaPlayerEntity(
)
media_id = play_item.url
if announce:
if media_type not in MediaType.MUSIC:
raise ServiceValidationError(
"Announcements must have media type of 'music'. Playlists are not supported"
)
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
cmd = "announce"
try:
announce_volume = get_announce_volume(extra)
except ValueError:
raise ServiceValidationError(
f"{ATTR_ANNOUNCE_VOLUME} must be a number greater than 0 and less than or equal to 1"
) from None
else:
self._player.set_announce_volume(announce_volume)
try:
announce_timeout = get_announce_timeout(extra)
except ValueError:
raise ServiceValidationError(
f"{ATTR_ANNOUNCE_TIMEOUT} must be a whole number greater than 0"
) from None
else:
self._player.set_announce_timeout(announce_timeout)
if media_type in MediaType.MUSIC:
if not media_id.startswith(SQUEEZEBOX_SOURCE_STRINGS):
# do not process special squeezebox "source" media ids

View File

@ -120,6 +120,11 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry:
return config_entry
async def mock_async_play_announcement(media_id: str) -> bool:
"""Mock the announcement."""
return True
async def mock_async_browse(
media_type: MediaType, limit: int, browse_id: tuple | None = None
) -> dict | None:
@ -222,6 +227,11 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock:
mock_player.generate_image_url_from_track_id = MagicMock(
return_value="http://lms.internal:9000/html/images/favorites.png"
)
mock_player.set_announce_volume = MagicMock(return_value=True)
mock_player.set_announce_timeout = MagicMock(return_value=True)
mock_player.async_play_announcement = AsyncMock(
side_effect=mock_async_play_announcement
)
mock_player.name = TEST_PLAYER_NAME
mock_player.player_id = uuid
mock_player.mode = "stop"

View File

@ -65,7 +65,7 @@
'original_name': None,
'platform': 'squeezebox',
'previous_unique_id': None,
'supported_features': <MediaPlayerEntityFeature: 3078079>,
'supported_features': <MediaPlayerEntityFeature: 4126655>,
'translation_key': None,
'unique_id': 'aa:bb:cc:dd:ee:ff',
'unit_of_measurement': None,
@ -88,7 +88,7 @@
}),
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'supported_features': <MediaPlayerEntityFeature: 3078079>,
'supported_features': <MediaPlayerEntityFeature: 4126655>,
'volume_level': 0.01,
}),
'context': <ANY>,

View File

@ -10,9 +10,11 @@ from syrupy import SnapshotAssertion
from homeassistant.components.media_player import (
ATTR_GROUP_MEMBERS,
ATTR_MEDIA_ANNOUNCE,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_EXTRA,
ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT,
ATTR_MEDIA_REPEAT,
@ -31,6 +33,8 @@ from homeassistant.components.media_player import (
RepeatMode,
)
from homeassistant.components.squeezebox.const import (
ATTR_ANNOUNCE_TIMEOUT,
ATTR_ANNOUNCE_VOLUME,
DISCOVERY_INTERVAL,
DOMAIN,
PLAYER_UPDATE_INTERVAL,
@ -436,6 +440,115 @@ async def test_squeezebox_play(
configured_player.async_play.assert_called_once()
async def test_squeezebox_play_media_with_announce(
hass: HomeAssistant, configured_player: MagicMock
) -> None:
"""Test play service call with announce."""
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: "media_player.test_player",
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
ATTR_MEDIA_ANNOUNCE: True,
},
blocking=True,
)
configured_player.async_load_url.assert_called_once_with(
FAKE_VALID_ITEM_ID, "announce"
)
@pytest.mark.parametrize(
"announce_volume",
["0.2", 0.2],
)
async def test_squeezebox_play_media_with_announce_volume(
hass: HomeAssistant, configured_player: MagicMock, announce_volume: str | int
) -> None:
"""Test play service call with announce."""
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: "media_player.test_player",
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
ATTR_MEDIA_ANNOUNCE: True,
ATTR_MEDIA_EXTRA: {ATTR_ANNOUNCE_VOLUME: announce_volume},
},
blocking=True,
)
configured_player.set_announce_volume.assert_called_once_with(20)
configured_player.async_load_url.assert_called_once_with(
FAKE_VALID_ITEM_ID, "announce"
)
@pytest.mark.parametrize("announce_volume", ["1.1", 1.1, "text", "-1", -1, 0, "0"])
async def test_squeezebox_play_media_with_announce_volume_invalid(
hass: HomeAssistant, configured_player: MagicMock, announce_volume: str | int
) -> None:
"""Test play service call with announce and volume zero."""
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: "media_player.test_player",
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
ATTR_MEDIA_ANNOUNCE: True,
ATTR_MEDIA_EXTRA: {ATTR_ANNOUNCE_VOLUME: announce_volume},
},
blocking=True,
)
@pytest.mark.parametrize("announce_timeout", ["-1", "text", -1, 0, "0"])
async def test_squeezebox_play_media_with_announce_timeout_invalid(
hass: HomeAssistant, configured_player: MagicMock, announce_timeout: str | int
) -> None:
"""Test play service call with announce and invalid timeout."""
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: "media_player.test_player",
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
ATTR_MEDIA_ANNOUNCE: True,
ATTR_MEDIA_EXTRA: {ATTR_ANNOUNCE_TIMEOUT: announce_timeout},
},
blocking=True,
)
@pytest.mark.parametrize("announce_timeout", ["100", 100])
async def test_squeezebox_play_media_with_announce_timeout(
hass: HomeAssistant, configured_player: MagicMock, announce_timeout: str | int
) -> None:
"""Test play service call with announce."""
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: "media_player.test_player",
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
ATTR_MEDIA_ANNOUNCE: True,
ATTR_MEDIA_EXTRA: {ATTR_ANNOUNCE_TIMEOUT: announce_timeout},
},
blocking=True,
)
configured_player.set_announce_timeout.assert_called_once_with(100)
configured_player.async_load_url.assert_called_once_with(
FAKE_VALID_ITEM_ID, "announce"
)
async def test_squeezebox_play_pause(
hass: HomeAssistant, configured_player: MagicMock
) -> None: