mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add repeat media controls to Bang & Olufsen (#128170)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
2da0a91a36
commit
66ca424d3a
@ -7,7 +7,11 @@ from typing import Final
|
|||||||
|
|
||||||
from mozart_api.models import Source, SourceArray, SourceTypeEnum
|
from mozart_api.models import Source, SourceArray, SourceTypeEnum
|
||||||
|
|
||||||
from homeassistant.components.media_player import MediaPlayerState, MediaType
|
from homeassistant.components.media_player import (
|
||||||
|
MediaPlayerState,
|
||||||
|
MediaType,
|
||||||
|
RepeatMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenSource:
|
class BangOlufsenSource:
|
||||||
@ -36,6 +40,17 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
|||||||
"unknown": MediaPlayerState.IDLE,
|
"unknown": MediaPlayerState.IDLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Dict used for translating Home Assistant settings to device repeat settings.
|
||||||
|
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
|
||||||
|
RepeatMode.ALL: "all",
|
||||||
|
RepeatMode.ONE: "track",
|
||||||
|
RepeatMode.OFF: "none",
|
||||||
|
}
|
||||||
|
# Dict used for translating device repeat settings to Home Assistant settings.
|
||||||
|
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
|
||||||
|
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Media types for play_media
|
# Media types for play_media
|
||||||
class BangOlufsenMediaType(StrEnum):
|
class BangOlufsenMediaType(StrEnum):
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
import contextlib
|
||||||
|
from datetime import timedelta
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
|
from aiohttp import ClientConnectorError
|
||||||
from mozart_api import __version__ as MOZART_API_VERSION
|
from mozart_api import __version__ as MOZART_API_VERSION
|
||||||
from mozart_api.exceptions import ApiException
|
from mozart_api.exceptions import ApiException
|
||||||
from mozart_api.models import (
|
from mozart_api.models import (
|
||||||
@ -22,6 +25,7 @@ from mozart_api.models import (
|
|||||||
PlaybackProgress,
|
PlaybackProgress,
|
||||||
PlayQueueItem,
|
PlayQueueItem,
|
||||||
PlayQueueItemType,
|
PlayQueueItemType,
|
||||||
|
PlayQueueSettings,
|
||||||
RenderingState,
|
RenderingState,
|
||||||
SceneProperties,
|
SceneProperties,
|
||||||
SoftwareUpdateState,
|
SoftwareUpdateState,
|
||||||
@ -44,6 +48,7 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
MediaType,
|
MediaType,
|
||||||
|
RepeatMode,
|
||||||
async_process_play_media_url,
|
async_process_play_media_url,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -58,6 +63,8 @@ from homeassistant.util.dt import utcnow
|
|||||||
|
|
||||||
from . import BangOlufsenConfigEntry
|
from . import BangOlufsenConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
|
BANG_OLUFSEN_REPEAT_FROM_HA,
|
||||||
|
BANG_OLUFSEN_REPEAT_TO_HA,
|
||||||
BANG_OLUFSEN_STATES,
|
BANG_OLUFSEN_STATES,
|
||||||
CONF_BEOLINK_JID,
|
CONF_BEOLINK_JID,
|
||||||
CONNECTION_STATUS,
|
CONNECTION_STATUS,
|
||||||
@ -72,6 +79,8 @@ from .const import (
|
|||||||
from .entity import BangOlufsenEntity
|
from .entity import BangOlufsenEntity
|
||||||
from .util import get_serial_number_from_jid
|
from .util import get_serial_number_from_jid
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
BANG_OLUFSEN_FEATURES = (
|
BANG_OLUFSEN_FEATURES = (
|
||||||
@ -84,6 +93,7 @@ BANG_OLUFSEN_FEATURES = (
|
|||||||
| MediaPlayerEntityFeature.PLAY
|
| MediaPlayerEntityFeature.PLAY
|
||||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||||
|
| MediaPlayerEntityFeature.REPEAT_SET
|
||||||
| MediaPlayerEntityFeature.SEEK
|
| MediaPlayerEntityFeature.SEEK
|
||||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||||
| MediaPlayerEntityFeature.STOP
|
| MediaPlayerEntityFeature.STOP
|
||||||
@ -131,6 +141,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
|||||||
serial_number=self._unique_id,
|
serial_number=self._unique_id,
|
||||||
)
|
)
|
||||||
self._attr_unique_id = self._unique_id
|
self._attr_unique_id = self._unique_id
|
||||||
|
self._attr_should_poll = True
|
||||||
|
|
||||||
# Misc. variables.
|
# Misc. variables.
|
||||||
self._audio_sources: dict[str, str] = {}
|
self._audio_sources: dict[str, str] = {}
|
||||||
@ -220,6 +231,16 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
|||||||
|
|
||||||
await self._async_update_sound_modes()
|
await self._async_update_sound_modes()
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Update queue settings."""
|
||||||
|
# The WebSocket event listener is the main handler for connection state.
|
||||||
|
# The polling updates do therefore not set the device as available or unavailable
|
||||||
|
with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
|
||||||
|
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
|
||||||
|
|
||||||
|
if queue_settings.repeat is not None:
|
||||||
|
self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
|
||||||
|
|
||||||
async def _async_update_sources(self) -> None:
|
async def _async_update_sources(self) -> None:
|
||||||
"""Get sources for the specific product."""
|
"""Get sources for the specific product."""
|
||||||
|
|
||||||
@ -630,6 +651,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
|||||||
"""Clear the current playback queue."""
|
"""Clear the current playback queue."""
|
||||||
await self._client.post_clear_queue()
|
await self._client.post_clear_queue()
|
||||||
|
|
||||||
|
async def async_set_repeat(self, repeat: RepeatMode) -> None:
|
||||||
|
"""Set playback queues to repeat."""
|
||||||
|
await self._client.set_settings_queue(
|
||||||
|
play_queue_settings=PlayQueueSettings(
|
||||||
|
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def async_select_source(self, source: str) -> None:
|
async def async_select_source(self, source: str) -> None:
|
||||||
"""Select an input source."""
|
"""Select an input source."""
|
||||||
if source not in self._sources.values():
|
if source not in self._sources.values():
|
||||||
|
@ -15,6 +15,7 @@ from mozart_api.models import (
|
|||||||
PlaybackContentMetadata,
|
PlaybackContentMetadata,
|
||||||
PlaybackProgress,
|
PlaybackProgress,
|
||||||
PlaybackState,
|
PlaybackState,
|
||||||
|
PlayQueueSettings,
|
||||||
ProductState,
|
ProductState,
|
||||||
RemoteMenuItem,
|
RemoteMenuItem,
|
||||||
RenderingState,
|
RenderingState,
|
||||||
@ -315,6 +316,12 @@ def mock_mozart_client() -> Generator[AsyncMock]:
|
|||||||
href="",
|
href="",
|
||||||
id=123,
|
id=123,
|
||||||
)
|
)
|
||||||
|
client.get_settings_queue = AsyncMock()
|
||||||
|
client.get_settings_queue.return_value = PlayQueueSettings(
|
||||||
|
repeat="none",
|
||||||
|
shuffle=False,
|
||||||
|
)
|
||||||
|
|
||||||
client.post_standby = AsyncMock()
|
client.post_standby = AsyncMock()
|
||||||
client.set_current_volume_level = AsyncMock()
|
client.set_current_volume_level = AsyncMock()
|
||||||
client.set_volume_mute = AsyncMock()
|
client.set_volume_mute = AsyncMock()
|
||||||
@ -336,6 +343,7 @@ def mock_mozart_client() -> Generator[AsyncMock]:
|
|||||||
client.post_beolink_allstandby = AsyncMock()
|
client.post_beolink_allstandby = AsyncMock()
|
||||||
client.join_latest_beolink_experience = AsyncMock()
|
client.join_latest_beolink_experience = AsyncMock()
|
||||||
client.activate_listening_mode = AsyncMock()
|
client.activate_listening_mode = AsyncMock()
|
||||||
|
client.set_settings_queue = AsyncMock()
|
||||||
|
|
||||||
# Non-REST API client methods
|
# Non-REST API client methods
|
||||||
client.check_device_connection = AsyncMock()
|
client.check_device_connection = AsyncMock()
|
||||||
|
@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, patch
|
|||||||
from mozart_api.models import (
|
from mozart_api.models import (
|
||||||
BeolinkLeader,
|
BeolinkLeader,
|
||||||
PlaybackContentMetadata,
|
PlaybackContentMetadata,
|
||||||
|
PlayQueueSettings,
|
||||||
RenderingState,
|
RenderingState,
|
||||||
Source,
|
Source,
|
||||||
WebsocketNotificationTag,
|
WebsocketNotificationTag,
|
||||||
@ -14,6 +15,7 @@ from mozart_api.models import (
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.bang_olufsen.const import (
|
from homeassistant.components.bang_olufsen.const import (
|
||||||
|
BANG_OLUFSEN_REPEAT_FROM_HA,
|
||||||
BANG_OLUFSEN_STATES,
|
BANG_OLUFSEN_STATES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
BangOlufsenSource,
|
BangOlufsenSource,
|
||||||
@ -32,6 +34,7 @@ from homeassistant.components.media_player import (
|
|||||||
ATTR_MEDIA_EXTRA,
|
ATTR_MEDIA_EXTRA,
|
||||||
ATTR_MEDIA_POSITION,
|
ATTR_MEDIA_POSITION,
|
||||||
ATTR_MEDIA_POSITION_UPDATED_AT,
|
ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||||
|
ATTR_MEDIA_REPEAT,
|
||||||
ATTR_MEDIA_SEEK_POSITION,
|
ATTR_MEDIA_SEEK_POSITION,
|
||||||
ATTR_MEDIA_TITLE,
|
ATTR_MEDIA_TITLE,
|
||||||
ATTR_MEDIA_TRACK,
|
ATTR_MEDIA_TRACK,
|
||||||
@ -54,8 +57,9 @@ from homeassistant.components.media_player import (
|
|||||||
SERVICE_VOLUME_SET,
|
SERVICE_VOLUME_SET,
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
MediaType,
|
MediaType,
|
||||||
|
RepeatMode,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_ENTITY_ID
|
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_REPEAT_SET
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -1421,3 +1425,52 @@ async def test_async_unjoin_player(
|
|||||||
)
|
)
|
||||||
|
|
||||||
mock_mozart_client.post_beolink_leave.assert_called_once()
|
mock_mozart_client.post_beolink_leave.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("repeat"),
|
||||||
|
[
|
||||||
|
# Repeat all
|
||||||
|
(RepeatMode.ALL),
|
||||||
|
# Repeat track
|
||||||
|
(RepeatMode.ONE),
|
||||||
|
# Repeat none
|
||||||
|
(RepeatMode.OFF),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_async_set_repeat(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_mozart_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
repeat: RepeatMode,
|
||||||
|
) -> None:
|
||||||
|
"""Test async_set_repeat."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
|
||||||
|
assert ATTR_MEDIA_REPEAT not in states.attributes
|
||||||
|
|
||||||
|
# Set the return value of the repeat endpoint to match service call
|
||||||
|
mock_mozart_client.get_settings_queue.return_value = PlayQueueSettings(
|
||||||
|
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
MEDIA_PLAYER_DOMAIN,
|
||||||
|
SERVICE_REPEAT_SET,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
|
||||||
|
ATTR_MEDIA_REPEAT: repeat,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_mozart_client.set_settings_queue.assert_called_once_with(
|
||||||
|
play_queue_settings=PlayQueueSettings(
|
||||||
|
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test the BANG_OLUFSEN_REPEAT_TO_HA dict by checking property value
|
||||||
|
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
|
||||||
|
assert states.attributes[ATTR_MEDIA_REPEAT] == repeat
|
||||||
|
Loading…
x
Reference in New Issue
Block a user