Add media seek for sources other than Deezer for Bang & Olufsen (#128661)

* Add seeking for sources other than Deezer

* Add is_seekable attribute to fallback sources and BangOlufsenSource
Add testing

* Update comment

* Use support flags instead of raising errors when seeking on incompatible source
This commit is contained in:
Markus Jacobsen 2024-10-25 23:34:39 +02:00 committed by GitHub
parent dbb80dd6c0
commit 6c365fffde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 69 additions and 26 deletions

View File

@ -17,14 +17,46 @@ from homeassistant.components.media_player import (
class BangOlufsenSource: class BangOlufsenSource:
"""Class used for associating device source ids with friendly names. May not include all sources.""" """Class used for associating device source ids with friendly names. May not include all sources."""
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer") URI_STREAMER: Final[Source] = Source(
BLUETOOTH: Final[Source] = Source(name="Bluetooth", id="bluetooth") name="Audio Streamer",
CHROMECAST: Final[Source] = Source(name="Chromecast built-in", id="chromeCast") id="uriStreamer",
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn") is_seekable=False,
SPDIF: Final[Source] = Source(name="Optical", id="spdif") )
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio") BLUETOOTH: Final[Source] = Source(
DEEZER: Final[Source] = Source(name="Deezer", id="deezer") name="Bluetooth",
TIDAL: Final[Source] = Source(name="Tidal", id="tidal") id="bluetooth",
is_seekable=False,
)
CHROMECAST: Final[Source] = Source(
name="Chromecast built-in",
id="chromeCast",
is_seekable=False,
)
LINE_IN: Final[Source] = Source(
name="Line-In",
id="lineIn",
is_seekable=False,
)
SPDIF: Final[Source] = Source(
name="Optical",
id="spdif",
is_seekable=False,
)
NET_RADIO: Final[Source] = Source(
name="B&O Radio",
id="netRadio",
is_seekable=False,
)
DEEZER: Final[Source] = Source(
name="Deezer",
id="deezer",
is_seekable=True,
)
TIDAL: Final[Source] = Source(
name="Tidal",
id="tidal",
is_seekable=True,
)
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = { BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
@ -162,6 +194,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=False, is_playable=False,
name="Audio Streamer", name="Audio Streamer",
type=SourceTypeEnum(value="uriStreamer"), type=SourceTypeEnum(value="uriStreamer"),
is_seekable=False,
), ),
Source( Source(
id="bluetooth", id="bluetooth",
@ -169,6 +202,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=False, is_playable=False,
name="Bluetooth", name="Bluetooth",
type=SourceTypeEnum(value="bluetooth"), type=SourceTypeEnum(value="bluetooth"),
is_seekable=False,
), ),
Source( Source(
id="spotify", id="spotify",
@ -176,6 +210,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=False, is_playable=False,
name="Spotify Connect", name="Spotify Connect",
type=SourceTypeEnum(value="spotify"), type=SourceTypeEnum(value="spotify"),
is_seekable=True,
), ),
Source( Source(
id="lineIn", id="lineIn",
@ -183,6 +218,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="Line-In", name="Line-In",
type=SourceTypeEnum(value="lineIn"), type=SourceTypeEnum(value="lineIn"),
is_seekable=False,
), ),
Source( Source(
id="spdif", id="spdif",
@ -190,6 +226,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="Optical", name="Optical",
type=SourceTypeEnum(value="spdif"), type=SourceTypeEnum(value="spdif"),
is_seekable=False,
), ),
Source( Source(
id="netRadio", id="netRadio",
@ -197,6 +234,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="B&O Radio", name="B&O Radio",
type=SourceTypeEnum(value="netRadio"), type=SourceTypeEnum(value="netRadio"),
is_seekable=False,
), ),
Source( Source(
id="deezer", id="deezer",
@ -204,6 +242,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="Deezer", name="Deezer",
type=SourceTypeEnum(value="deezer"), type=SourceTypeEnum(value="deezer"),
is_seekable=True,
), ),
Source( Source(
id="tidalConnect", id="tidalConnect",
@ -211,6 +250,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="Tidal Connect", name="Tidal Connect",
type=SourceTypeEnum(value="tidalConnect"), type=SourceTypeEnum(value="tidalConnect"),
is_seekable=True,
), ),
] ]
) )

View File

@ -94,7 +94,6 @@ BANG_OLUFSEN_FEATURES = (
| MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.REPEAT_SET | MediaPlayerEntityFeature.REPEAT_SET
| MediaPlayerEntityFeature.SEEK
| MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_OFF
@ -124,7 +123,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
_attr_icon = "mdi:speaker-wireless" _attr_icon = "mdi:speaker-wireless"
_attr_name = None _attr_name = None
_attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_supported_features = BANG_OLUFSEN_FEATURES
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None: def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
"""Initialize the media player.""" """Initialize the media player."""
@ -485,6 +483,17 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self.async_write_ha_state() self.async_write_ha_state()
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
features = BANG_OLUFSEN_FEATURES
# Add seeking if supported by the current source
if self._source_change.is_seekable is True:
features |= MediaPlayerEntityFeature.SEEK
return features
@property @property
def state(self) -> MediaPlayerState: def state(self) -> MediaPlayerState:
"""Return the current state of the media player.""" """Return the current state of the media player."""
@ -631,17 +640,12 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
async def async_media_seek(self, position: float) -> None: async def async_media_seek(self, position: float) -> None:
"""Seek to position in ms.""" """Seek to position in ms."""
if self._source_change.id == BangOlufsenSource.DEEZER.id:
await self._client.seek_to_position(position_ms=int(position * 1000)) await self._client.seek_to_position(position_ms=int(position * 1000))
# Try to prevent the playback progress from bouncing in the UI. # Try to prevent the playback progress from bouncing in the UI.
self._attr_media_position_updated_at = utcnow() self._attr_media_position_updated_at = utcnow()
self._playback_progress = PlaybackProgress(progress=int(position)) self._playback_progress = PlaybackProgress(progress=int(position))
self.async_write_ha_state() self.async_write_ha_state()
else:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="non_deezer_seeking"
)
async def async_media_previous_track(self) -> None: async def async_media_previous_track(self) -> None:
"""Send the previous track command.""" """Send the previous track command."""

View File

@ -29,9 +29,6 @@
"m3u_invalid_format": { "m3u_invalid_format": {
"message": "Media sources with the .m3u extension are not supported." "message": "Media sources with the .m3u extension are not supported."
}, },
"non_deezer_seeking": {
"message": "Seeking is currently only supported when using Deezer"
},
"invalid_source": { "invalid_source": {
"message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}" "message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}"
}, },

View File

@ -673,10 +673,12 @@ async def test_async_media_next_track(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("source", "expected_result", "seek_called_times"), ("source", "expected_result", "seek_called_times"),
[ [
# Deezer source, seek expected # Seekable source, seek expected
(BangOlufsenSource.DEEZER, does_not_raise(), 1), (BangOlufsenSource.DEEZER, does_not_raise(), 1),
# Non deezer source, seek shouldn't work # Non seekable source, seek shouldn't work
(BangOlufsenSource.TIDAL, pytest.raises(HomeAssistantError), 0), (BangOlufsenSource.LINE_IN, pytest.raises(HomeAssistantError), 0),
# Malformed source, seek shouldn't work
(Source(), pytest.raises(HomeAssistantError), 0),
], ],
) )
async def test_async_media_seek( async def test_async_media_seek(