Add repeat feature to HEOS media player (#136180)

This commit is contained in:
Andrew Sayre 2025-01-22 05:25:56 -06:00 committed by GitHub
parent a3cc68754f
commit f4d6cb45e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 4 deletions

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine from collections.abc import Awaitable, Callable, Coroutine
from functools import reduce, wraps from functools import reduce, wraps
import logging
from operator import ior from operator import ior
from typing import Any from typing import Any
@ -14,6 +13,7 @@ from pyheos import (
HeosError, HeosError,
HeosPlayer, HeosPlayer,
PlayState, PlayState,
RepeatType,
const as heos_const, const as heos_const,
) )
@ -26,6 +26,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.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -48,7 +49,6 @@ BASE_SUPPORTED_FEATURES = (
| MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.SHUFFLE_SET
| MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.GROUPING
@ -78,7 +78,12 @@ HA_HEOS_ENQUEUE_MAP = {
MediaPlayerEnqueue.PLAY: AddCriteriaType.PLAY_NOW, MediaPlayerEnqueue.PLAY: AddCriteriaType.PLAY_NOW,
} }
_LOGGER = logging.getLogger(__name__) HEOS_HA_REPEAT_TYPE_MAP = {
RepeatType.OFF: RepeatMode.OFF,
RepeatType.ON_ALL: RepeatMode.ALL,
RepeatType.ON_ONE: RepeatMode.ONE,
}
HA_HEOS_REPEAT_TYPE_MAP = {v: k for k, v in HEOS_HA_REPEAT_TYPE_MAP.items()}
async def async_setup_entry( async def async_setup_entry(
@ -293,6 +298,13 @@ class HeosMediaPlayer(MediaPlayerEntity):
"""Select input source.""" """Select input source."""
await self._source_manager.play_source(source, self._player) await self._source_manager.play_source(source, self._player)
@catch_action_error("set repeat")
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set repeat mode."""
await self._player.set_play_mode(
HA_HEOS_REPEAT_TYPE_MAP[repeat], self._player.shuffle
)
@catch_action_error("set shuffle") @catch_action_error("set shuffle")
async def async_set_shuffle(self, shuffle: bool) -> None: async def async_set_shuffle(self, shuffle: bool) -> None:
"""Enable/disable shuffle mode.""" """Enable/disable shuffle mode."""
@ -305,11 +317,17 @@ class HeosMediaPlayer(MediaPlayerEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update supported features of the player.""" """Update supported features of the player."""
self._attr_repeat = HEOS_HA_REPEAT_TYPE_MAP[self._player.repeat]
controls = self._player.now_playing_media.supported_controls controls = self._player.now_playing_media.supported_controls
current_support = [CONTROL_TO_SUPPORT[control] for control in controls] current_support = [CONTROL_TO_SUPPORT[control] for control in controls]
self._attr_supported_features = reduce( self._attr_supported_features = reduce(
ior, current_support, BASE_SUPPORTED_FEATURES ior, current_support, BASE_SUPPORTED_FEATURES
) )
if self.support_next_track and self.support_previous_track:
self._attr_supported_features |= (
MediaPlayerEntityFeature.REPEAT_SET
| MediaPlayerEntityFeature.SHUFFLE_SET
)
@catch_action_error("unjoin player") @catch_action_error("unjoin player")
async def async_unjoin_player(self) -> None: async def async_unjoin_player(self) -> None:

View File

@ -19,13 +19,14 @@
'media_station': 'Station Name', 'media_station': 'Station Name',
'media_title': 'Song', 'media_title': 'Song',
'media_type': 'Station', 'media_type': 'Station',
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False, 'shuffle': False,
'source_list': list([ 'source_list': list([
"Today's Hits Radio", "Today's Hits Radio",
'Classical MPR (Classical Music)', 'Classical MPR (Classical Music)',
'HEOS Drive - Line In 1', 'HEOS Drive - Line In 1',
]), ]),
'supported_features': <MediaPlayerEntityFeature: 2817597>, 'supported_features': <MediaPlayerEntityFeature: 3079741>,
'volume_level': 0.25, 'volume_level': 0.25,
}), }),
'entity_id': 'media_player.test_player', 'entity_id': 'media_player.test_player',

View File

@ -11,6 +11,7 @@ from pyheos import (
MediaItem, MediaItem,
PlayerUpdateResult, PlayerUpdateResult,
PlayState, PlayState,
RepeatType,
SignalHeosEvent, SignalHeosEvent,
SignalType, SignalType,
const, const,
@ -30,6 +31,7 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_POSITION_UPDATED_AT,
ATTR_MEDIA_REPEAT,
ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_SHUFFLE,
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_VOLUME_MUTED,
@ -40,6 +42,7 @@ from homeassistant.components.media_player import (
SERVICE_SELECT_SOURCE, SERVICE_SELECT_SOURCE,
SERVICE_UNJOIN, SERVICE_UNJOIN,
MediaType, MediaType,
RepeatMode,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -48,6 +51,7 @@ from homeassistant.const import (
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_STOP, SERVICE_MEDIA_STOP,
SERVICE_REPEAT_SET,
SERVICE_SHUFFLE_SET, SERVICE_SHUFFLE_SET,
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET, SERVICE_VOLUME_SET,
@ -563,6 +567,46 @@ async def test_shuffle_set_error(
player.set_play_mode.assert_called_once_with(player.repeat, True) player.set_play_mode.assert_called_once_with(player.repeat, True)
async def test_repeat_set(
hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos
) -> None:
"""Test the repeat set service."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
player = controller.players[1]
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_REPEAT_SET,
{ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_REPEAT: RepeatMode.ONE},
blocking=True,
)
player.set_play_mode.assert_called_once_with(RepeatType.ON_ONE, player.shuffle)
async def test_repeat_set_error(
hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos
) -> None:
"""Test the repeat set service raises error."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
player = controller.players[1]
player.set_play_mode.side_effect = CommandFailedError(None, "Failure", 1)
with pytest.raises(
HomeAssistantError,
match=re.escape("Unable to set repeat: Failure (1)"),
):
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_REPEAT_SET,
{
ATTR_ENTITY_ID: "media_player.test_player",
ATTR_MEDIA_REPEAT: RepeatMode.ALL,
},
blocking=True,
)
player.set_play_mode.assert_called_once_with(RepeatType.ON_ALL, player.shuffle)
async def test_volume_set( async def test_volume_set(
hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos
) -> None: ) -> None: