Add support for HEOS groups (#32568)
* Add support for grouping HEOS media players * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Handle groups at controller level, refine tests. Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> * Fix linting issues * Update homeassistant/components/heos/media_player.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/heos/media_player.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Rename variables and improve resolving of entity_ids Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Don't patch internal methods Use the pytest fixtures which have already been defined for this. * Fix linting issues * Remove unused property * Ignore groups with unknown leader This makes sure that the group_members attribute won't contain a `None` value as a leader entity_id. * Don't call force_update_groups() from tests * Don't pass `None` player ids to HEOS API * Use signal for group manager communication * Use imports for async_dispatcher_send/async_dispatcher_connect * Raise exception when leader/player could not be resolved * Disconnect signal handlers, avoid calling async_update_groups too early * Update homeassistant/components/heos/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Andrew Sayre (he/his/him) <6730289+andrewsayre@users.noreply.github.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
0dece582e4
commit
56e93ff0ec
5 changed files with 362 additions and 42 deletions
|
@ -11,9 +11,13 @@ import voluptuous as vol
|
|||
from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
|
@ -23,8 +27,11 @@ from .const import (
|
|||
COMMAND_RETRY_ATTEMPTS,
|
||||
COMMAND_RETRY_DELAY,
|
||||
DATA_CONTROLLER_MANAGER,
|
||||
DATA_ENTITY_ID_MAP,
|
||||
DATA_GROUP_MANAGER,
|
||||
DATA_SOURCE_MANAGER,
|
||||
DOMAIN,
|
||||
SIGNAL_HEOS_PLAYER_ADDED,
|
||||
SIGNAL_HEOS_UPDATED,
|
||||
)
|
||||
|
||||
|
@ -117,15 +124,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
source_manager = SourceManager(favorites, inputs)
|
||||
source_manager.connect_update(hass, controller)
|
||||
|
||||
group_manager = GroupManager(hass, controller)
|
||||
|
||||
hass.data[DOMAIN] = {
|
||||
DATA_CONTROLLER_MANAGER: controller_manager,
|
||||
DATA_GROUP_MANAGER: group_manager,
|
||||
DATA_SOURCE_MANAGER: source_manager,
|
||||
MEDIA_PLAYER_DOMAIN: players,
|
||||
# Maps player_id to entity_id. Populated by the individual HeosMediaPlayer entities.
|
||||
DATA_ENTITY_ID_MAP: {},
|
||||
}
|
||||
|
||||
services.register(hass, controller)
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
group_manager.connect_update()
|
||||
entry.async_on_unload(group_manager.disconnect_update)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -184,7 +198,7 @@ class ControllerManager:
|
|||
if event == heos_const.EVENT_PLAYERS_CHANGED:
|
||||
self.update_ids(data[heos_const.DATA_MAPPED_IDS])
|
||||
# Update players
|
||||
self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED)
|
||||
async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED)
|
||||
|
||||
async def _heos_event(self, event):
|
||||
"""Handle connection event."""
|
||||
|
@ -196,7 +210,7 @@ class ControllerManager:
|
|||
except HeosError as ex:
|
||||
_LOGGER.error("Unable to refresh players: %s", ex)
|
||||
# Update players
|
||||
self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED)
|
||||
async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED)
|
||||
|
||||
def update_ids(self, mapped_ids: dict[int, int]):
|
||||
"""Update the IDs in the device and entity registry."""
|
||||
|
@ -223,6 +237,148 @@ class ControllerManager:
|
|||
_LOGGER.debug("Updated entity %s unique id to %s", entity_id, new_id)
|
||||
|
||||
|
||||
class GroupManager:
|
||||
"""Class that manages HEOS groups."""
|
||||
|
||||
def __init__(self, hass, controller):
|
||||
"""Init group manager."""
|
||||
self._hass = hass
|
||||
self._group_membership = {}
|
||||
self._disconnect_player_added = None
|
||||
self._initialized = False
|
||||
self.controller = controller
|
||||
|
||||
def _get_entity_id_to_player_id_map(self) -> dict:
|
||||
"""Return a dictionary which maps all HeosMediaPlayer entity_ids to player_ids."""
|
||||
return {v: k for k, v in self._hass.data[DOMAIN][DATA_ENTITY_ID_MAP].items()}
|
||||
|
||||
async def async_get_group_membership(self):
|
||||
"""Return a dictionary which contains all group members for each player as entity_ids."""
|
||||
group_info_by_entity_id = {
|
||||
player_entity_id: []
|
||||
for player_entity_id in self._get_entity_id_to_player_id_map()
|
||||
}
|
||||
|
||||
try:
|
||||
groups = await self.controller.get_groups(refresh=True)
|
||||
except HeosError as err:
|
||||
_LOGGER.error("Unable to get HEOS group info: %s", err)
|
||||
return group_info_by_entity_id
|
||||
|
||||
player_id_to_entity_id_map = self._hass.data[DOMAIN][DATA_ENTITY_ID_MAP]
|
||||
for group in groups.values():
|
||||
leader_entity_id = player_id_to_entity_id_map.get(group.leader.player_id)
|
||||
member_entity_ids = [
|
||||
player_id_to_entity_id_map[member.player_id]
|
||||
for member in group.members
|
||||
if member.player_id in player_id_to_entity_id_map
|
||||
]
|
||||
# Make sure the group leader is always the first element
|
||||
group_info = [leader_entity_id, *member_entity_ids]
|
||||
if leader_entity_id:
|
||||
group_info_by_entity_id[leader_entity_id] = group_info
|
||||
for member_entity_id in member_entity_ids:
|
||||
group_info_by_entity_id[member_entity_id] = group_info
|
||||
|
||||
return group_info_by_entity_id
|
||||
|
||||
async def async_join_players(
|
||||
self, leader_entity_id: str, member_entity_ids: list[str]
|
||||
) -> None:
|
||||
"""Create a group with `leader_entity_id` as group leader and `member_entity_ids` as member players."""
|
||||
entity_id_to_player_id_map = self._get_entity_id_to_player_id_map()
|
||||
leader_id = entity_id_to_player_id_map.get(leader_entity_id)
|
||||
if not leader_id:
|
||||
raise HomeAssistantError(
|
||||
f"The group leader {leader_entity_id} could not be resolved to a HEOS player."
|
||||
)
|
||||
member_ids = [
|
||||
entity_id_to_player_id_map[member]
|
||||
for member in member_entity_ids
|
||||
if member in entity_id_to_player_id_map
|
||||
]
|
||||
|
||||
try:
|
||||
await self.controller.create_group(leader_id, member_ids)
|
||||
except HeosError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to group %s with %s: %s",
|
||||
leader_entity_id,
|
||||
member_entity_ids,
|
||||
err,
|
||||
)
|
||||
|
||||
async def async_unjoin_player(self, player_entity_id: str):
|
||||
"""Remove `player_entity_id` from any group."""
|
||||
player_id = self._get_entity_id_to_player_id_map().get(player_entity_id)
|
||||
if not player_id:
|
||||
raise HomeAssistantError(
|
||||
f"The player {player_entity_id} could not be resolved to a HEOS player."
|
||||
)
|
||||
|
||||
try:
|
||||
await self.controller.create_group(player_id, [])
|
||||
except HeosError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to ungroup %s: %s",
|
||||
player_entity_id,
|
||||
err,
|
||||
)
|
||||
|
||||
async def async_update_groups(self, event, data=None):
|
||||
"""Update the group membership from the controller."""
|
||||
if event in (
|
||||
heos_const.EVENT_GROUPS_CHANGED,
|
||||
heos_const.EVENT_CONNECTED,
|
||||
SIGNAL_HEOS_PLAYER_ADDED,
|
||||
):
|
||||
groups = await self.async_get_group_membership()
|
||||
if groups:
|
||||
self._group_membership = groups
|
||||
_LOGGER.debug("Groups updated due to change event")
|
||||
# Let players know to update
|
||||
async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED)
|
||||
else:
|
||||
_LOGGER.debug("Groups empty")
|
||||
|
||||
def connect_update(self):
|
||||
"""Connect listener for when groups change and signal player update."""
|
||||
self.controller.dispatcher.connect(
|
||||
heos_const.SIGNAL_CONTROLLER_EVENT, self.async_update_groups
|
||||
)
|
||||
self.controller.dispatcher.connect(
|
||||
heos_const.SIGNAL_HEOS_EVENT, self.async_update_groups
|
||||
)
|
||||
|
||||
# When adding a new HEOS player we need to update the groups.
|
||||
async def _async_handle_player_added():
|
||||
# Avoid calling async_update_groups when `DATA_ENTITY_ID_MAP` has not been
|
||||
# fully populated yet. This may only happen during early startup.
|
||||
if (
|
||||
len(self._hass.data[DOMAIN][MEDIA_PLAYER_DOMAIN])
|
||||
<= len(self._hass.data[DOMAIN][DATA_ENTITY_ID_MAP])
|
||||
and not self._initialized
|
||||
):
|
||||
self._initialized = True
|
||||
await self.async_update_groups(SIGNAL_HEOS_PLAYER_ADDED)
|
||||
|
||||
self._disconnect_player_added = async_dispatcher_connect(
|
||||
self._hass, SIGNAL_HEOS_PLAYER_ADDED, _async_handle_player_added
|
||||
)
|
||||
|
||||
@callback
|
||||
def disconnect_update(self):
|
||||
"""Disconnect the listeners."""
|
||||
if self._disconnect_player_added:
|
||||
self._disconnect_player_added()
|
||||
self._disconnect_player_added = None
|
||||
|
||||
@property
|
||||
def group_membership(self):
|
||||
"""Provide access to group members for player entities."""
|
||||
return self._group_membership
|
||||
|
||||
|
||||
class SourceManager:
|
||||
"""Class that manages sources for players."""
|
||||
|
||||
|
@ -341,7 +497,7 @@ class SourceManager:
|
|||
self.source_list = self._build_source_list()
|
||||
_LOGGER.debug("Sources updated due to changed event")
|
||||
# Let players know to update
|
||||
hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED)
|
||||
async_dispatcher_send(hass, SIGNAL_HEOS_UPDATED)
|
||||
|
||||
controller.dispatcher.connect(
|
||||
heos_const.SIGNAL_CONTROLLER_EVENT, update_sources
|
||||
|
|
|
@ -5,9 +5,12 @@ ATTR_USERNAME = "username"
|
|||
COMMAND_RETRY_ATTEMPTS = 2
|
||||
COMMAND_RETRY_DELAY = 1
|
||||
DATA_CONTROLLER_MANAGER = "controller"
|
||||
DATA_ENTITY_ID_MAP = "entity_id_map"
|
||||
DATA_GROUP_MANAGER = "group_manager"
|
||||
DATA_SOURCE_MANAGER = "source_manager"
|
||||
DATA_DISCOVERED_HOSTS = "heos_discovered_hosts"
|
||||
DOMAIN = "heos"
|
||||
SERVICE_SIGN_IN = "sign_in"
|
||||
SERVICE_SIGN_OUT = "sign_out"
|
||||
SIGNAL_HEOS_PLAYER_ADDED = "heos_player_added"
|
||||
SIGNAL_HEOS_UPDATED = "heos_updated"
|
||||
|
|
|
@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import (
|
|||
MEDIA_TYPE_PLAYLIST,
|
||||
MEDIA_TYPE_URL,
|
||||
SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_GROUPING,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
|
@ -30,10 +31,21 @@ from homeassistant.components.media_player.const import (
|
|||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import DATA_SOURCE_MANAGER, DOMAIN as HEOS_DOMAIN, SIGNAL_HEOS_UPDATED
|
||||
from .const import (
|
||||
DATA_ENTITY_ID_MAP,
|
||||
DATA_GROUP_MANAGER,
|
||||
DATA_SOURCE_MANAGER,
|
||||
DOMAIN as HEOS_DOMAIN,
|
||||
SIGNAL_HEOS_PLAYER_ADDED,
|
||||
SIGNAL_HEOS_UPDATED,
|
||||
)
|
||||
|
||||
BASE_SUPPORTED_FEATURES = (
|
||||
SUPPORT_VOLUME_MUTE
|
||||
|
@ -43,6 +55,7 @@ BASE_SUPPORTED_FEATURES = (
|
|||
| SUPPORT_SHUFFLE_SET
|
||||
| SUPPORT_SELECT_SOURCE
|
||||
| SUPPORT_PLAY_MEDIA
|
||||
| SUPPORT_GROUPING
|
||||
)
|
||||
|
||||
PLAY_STATE_TO_STATE = {
|
||||
|
@ -97,6 +110,7 @@ class HeosMediaPlayer(MediaPlayerEntity):
|
|||
self._signals = []
|
||||
self._supported_features = BASE_SUPPORTED_FEATURES
|
||||
self._source_manager = None
|
||||
self._group_manager = None
|
||||
|
||||
async def _player_update(self, player_id, event):
|
||||
"""Handle player attribute updated."""
|
||||
|
@ -120,16 +134,24 @@ class HeosMediaPlayer(MediaPlayerEntity):
|
|||
)
|
||||
# Update state when heos changes
|
||||
self._signals.append(
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_HEOS_UPDATED, self._heos_updated
|
||||
)
|
||||
async_dispatcher_connect(self.hass, SIGNAL_HEOS_UPDATED, self._heos_updated)
|
||||
)
|
||||
# Register this player's entity_id so it can be resolved by the group manager
|
||||
self.hass.data[HEOS_DOMAIN][DATA_ENTITY_ID_MAP][
|
||||
self._player.player_id
|
||||
] = self.entity_id
|
||||
async_dispatcher_send(self.hass, SIGNAL_HEOS_PLAYER_ADDED)
|
||||
|
||||
@log_command_error("clear playlist")
|
||||
async def async_clear_playlist(self):
|
||||
"""Clear players playlist."""
|
||||
await self._player.clear_queue()
|
||||
|
||||
@log_command_error("join_players")
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
"""Join `group_members` as a player group with the current player."""
|
||||
await self._group_manager.async_join_players(self.entity_id, group_members)
|
||||
|
||||
@log_command_error("pause")
|
||||
async def async_media_pause(self):
|
||||
"""Send pause command."""
|
||||
|
@ -238,9 +260,17 @@ class HeosMediaPlayer(MediaPlayerEntity):
|
|||
current_support = [CONTROL_TO_SUPPORT[control] for control in controls]
|
||||
self._supported_features = reduce(ior, current_support, BASE_SUPPORTED_FEATURES)
|
||||
|
||||
if self._group_manager is None:
|
||||
self._group_manager = self.hass.data[HEOS_DOMAIN][DATA_GROUP_MANAGER]
|
||||
|
||||
if self._source_manager is None:
|
||||
self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER]
|
||||
|
||||
@log_command_error("unjoin_player")
|
||||
async def async_unjoin_player(self):
|
||||
"""Remove this player from any group."""
|
||||
await self._group_manager.async_unjoin_player(self.entity_id)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect the device when removed."""
|
||||
for signal_remove in self._signals:
|
||||
|
@ -274,6 +304,11 @@ class HeosMediaPlayer(MediaPlayerEntity):
|
|||
"media_type": self._player.now_playing_media.type,
|
||||
}
|
||||
|
||||
@property
|
||||
def group_members(self) -> list[str]:
|
||||
"""List of players which are grouped together."""
|
||||
return self._group_manager.group_membership.get(self.entity_id, [])
|
||||
|
||||
@property
|
||||
def is_volume_muted(self) -> bool:
|
||||
"""Boolean if volume is currently muted."""
|
||||
|
|
|
@ -4,7 +4,15 @@ from __future__ import annotations
|
|||
from typing import Sequence
|
||||
from unittest.mock import Mock, patch as patch
|
||||
|
||||
from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const
|
||||
from pyheos import (
|
||||
Dispatcher,
|
||||
Heos,
|
||||
HeosGroup,
|
||||
HeosPlayer,
|
||||
HeosSource,
|
||||
InputSource,
|
||||
const,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import ssdp
|
||||
|
@ -24,7 +32,7 @@ def config_entry_fixture():
|
|||
|
||||
@pytest.fixture(name="controller")
|
||||
def controller_fixture(
|
||||
players, favorites, input_sources, playlists, change_data, dispatcher
|
||||
players, favorites, input_sources, playlists, change_data, dispatcher, group
|
||||
):
|
||||
"""Create a mock Heos controller fixture."""
|
||||
mock_heos = Mock(Heos)
|
||||
|
@ -40,6 +48,8 @@ def controller_fixture(
|
|||
mock_heos.is_signed_in = True
|
||||
mock_heos.signed_in_username = "user@user.com"
|
||||
mock_heos.connection_state = const.STATE_CONNECTED
|
||||
mock_heos.get_groups.return_value = group
|
||||
mock_heos.create_group.return_value = None
|
||||
mock = Mock(return_value=mock_heos)
|
||||
|
||||
with patch("homeassistant.components.heos.Heos", new=mock), patch(
|
||||
|
@ -56,35 +66,51 @@ def config_fixture():
|
|||
|
||||
@pytest.fixture(name="players")
|
||||
def player_fixture(quick_selects):
|
||||
"""Create a mock HeosPlayer."""
|
||||
player = Mock(HeosPlayer)
|
||||
player.player_id = 1
|
||||
player.name = "Test Player"
|
||||
player.model = "Test Model"
|
||||
player.version = "1.0.0"
|
||||
player.is_muted = False
|
||||
player.available = True
|
||||
player.state = const.PLAY_STATE_STOP
|
||||
player.ip_address = "127.0.0.1"
|
||||
player.network = "wired"
|
||||
player.shuffle = False
|
||||
player.repeat = const.REPEAT_OFF
|
||||
player.volume = 25
|
||||
player.now_playing_media.supported_controls = const.CONTROLS_ALL
|
||||
player.now_playing_media.album_id = 1
|
||||
player.now_playing_media.queue_id = 1
|
||||
player.now_playing_media.source_id = 1
|
||||
player.now_playing_media.station = "Station Name"
|
||||
player.now_playing_media.type = "Station"
|
||||
player.now_playing_media.album = "Album"
|
||||
player.now_playing_media.artist = "Artist"
|
||||
player.now_playing_media.media_id = "1"
|
||||
player.now_playing_media.duration = None
|
||||
player.now_playing_media.current_position = None
|
||||
player.now_playing_media.image_url = "http://"
|
||||
player.now_playing_media.song = "Song"
|
||||
player.get_quick_selects.return_value = quick_selects
|
||||
return {player.player_id: player}
|
||||
"""Create two mock HeosPlayers."""
|
||||
players = {}
|
||||
for i in (1, 2):
|
||||
player = Mock(HeosPlayer)
|
||||
player.player_id = i
|
||||
if i > 1:
|
||||
player.name = f"Test Player {i}"
|
||||
else:
|
||||
player.name = "Test Player"
|
||||
player.model = "Test Model"
|
||||
player.version = "1.0.0"
|
||||
player.is_muted = False
|
||||
player.available = True
|
||||
player.state = const.PLAY_STATE_STOP
|
||||
player.ip_address = f"127.0.0.{i}"
|
||||
player.network = "wired"
|
||||
player.shuffle = False
|
||||
player.repeat = const.REPEAT_OFF
|
||||
player.volume = 25
|
||||
player.now_playing_media.supported_controls = const.CONTROLS_ALL
|
||||
player.now_playing_media.album_id = 1
|
||||
player.now_playing_media.queue_id = 1
|
||||
player.now_playing_media.source_id = 1
|
||||
player.now_playing_media.station = "Station Name"
|
||||
player.now_playing_media.type = "Station"
|
||||
player.now_playing_media.album = "Album"
|
||||
player.now_playing_media.artist = "Artist"
|
||||
player.now_playing_media.media_id = "1"
|
||||
player.now_playing_media.duration = None
|
||||
player.now_playing_media.current_position = None
|
||||
player.now_playing_media.image_url = "http://"
|
||||
player.now_playing_media.song = "Song"
|
||||
player.get_quick_selects.return_value = quick_selects
|
||||
players[player.player_id] = player
|
||||
return players
|
||||
|
||||
|
||||
@pytest.fixture(name="group")
|
||||
def group_fixture(players):
|
||||
"""Create a HEOS group consisting of two players."""
|
||||
group = Mock(HeosGroup)
|
||||
group.leader = players[1]
|
||||
group.members = [players[2]]
|
||||
group.group_id = 999
|
||||
return {group.group_id: group}
|
||||
|
||||
|
||||
@pytest.fixture(name="favorites")
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import asyncio
|
||||
|
||||
from pyheos import CommandFailedError, const
|
||||
from pyheos.error import HeosError
|
||||
|
||||
from homeassistant.components.heos import media_player
|
||||
from homeassistant.components.heos.const import (
|
||||
|
@ -10,6 +11,7 @@ from homeassistant.components.heos.const import (
|
|||
SIGNAL_HEOS_UPDATED,
|
||||
)
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_GROUP_MEMBERS,
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_ALBUM_NAME,
|
||||
|
@ -29,8 +31,10 @@ from homeassistant.components.media_player.const import (
|
|||
MEDIA_TYPE_PLAYLIST,
|
||||
MEDIA_TYPE_URL,
|
||||
SERVICE_CLEAR_PLAYLIST,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
SERVICE_UNJOIN,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
|
@ -264,10 +268,10 @@ async def test_updates_from_players_changed_new_ids(
|
|||
await event.wait()
|
||||
|
||||
# Assert device registry identifiers were updated
|
||||
assert len(device_registry.devices) == 1
|
||||
assert len(device_registry.devices) == 2
|
||||
assert device_registry.async_get_device({(DOMAIN, 101)})
|
||||
# Assert entity registry unique id was updated
|
||||
assert len(entity_registry.entities) == 1
|
||||
assert len(entity_registry.entities) == 2
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "101")
|
||||
== "media_player.test_player"
|
||||
|
@ -805,3 +809,99 @@ async def test_play_media_invalid_type(hass, config_entry, config, controller, c
|
|||
blocking=True,
|
||||
)
|
||||
assert "Unable to play media: Unsupported media type 'Other'" in caplog.text
|
||||
|
||||
|
||||
async def test_media_player_join_group(hass, config_entry, config, controller, caplog):
|
||||
"""Test grouping of media players through the join service."""
|
||||
await setup_platform(hass, config_entry, config)
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_JOIN,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_GROUP_MEMBERS: ["media_player.test_player_2"],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
controller.create_group.assert_called_once_with(
|
||||
1,
|
||||
[
|
||||
2,
|
||||
],
|
||||
)
|
||||
assert "Failed to group media_player.test_player with" not in caplog.text
|
||||
|
||||
controller.create_group.side_effect = HeosError("error")
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_JOIN,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_GROUP_MEMBERS: ["media_player.test_player_2"],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert "Failed to group media_player.test_player with" in caplog.text
|
||||
|
||||
|
||||
async def test_media_player_group_members(
|
||||
hass, config_entry, config, controller, caplog
|
||||
):
|
||||
"""Test group_members attribute."""
|
||||
await setup_platform(hass, config_entry, config)
|
||||
await hass.async_block_till_done()
|
||||
player_entity = hass.states.get("media_player.test_player")
|
||||
assert player_entity.attributes[ATTR_GROUP_MEMBERS] == [
|
||||
"media_player.test_player",
|
||||
"media_player.test_player_2",
|
||||
]
|
||||
controller.get_groups.assert_called_once()
|
||||
assert "Unable to get HEOS group info" not in caplog.text
|
||||
|
||||
|
||||
async def test_media_player_group_members_error(
|
||||
hass, config_entry, config, controller, caplog
|
||||
):
|
||||
"""Test error in HEOS API."""
|
||||
controller.get_groups.side_effect = HeosError("error")
|
||||
await setup_platform(hass, config_entry, config)
|
||||
await hass.async_block_till_done()
|
||||
assert "Unable to get HEOS group info" in caplog.text
|
||||
player_entity = hass.states.get("media_player.test_player")
|
||||
assert player_entity.attributes[ATTR_GROUP_MEMBERS] == []
|
||||
|
||||
|
||||
async def test_media_player_unjoin_group(
|
||||
hass, config_entry, config, controller, caplog
|
||||
):
|
||||
"""Test ungrouping of media players through the join service."""
|
||||
await setup_platform(hass, config_entry, config)
|
||||
player = controller.players[1]
|
||||
|
||||
player.heos.dispatcher.send(
|
||||
const.SIGNAL_PLAYER_EVENT,
|
||||
player.player_id,
|
||||
const.EVENT_PLAYER_STATE_CHANGED,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_UNJOIN,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
controller.create_group.assert_called_once_with(1, [])
|
||||
assert "Failed to ungroup media_player.test_player" not in caplog.text
|
||||
|
||||
controller.create_group.side_effect = HeosError("error")
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_UNJOIN,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert "Failed to ungroup media_player.test_player" in caplog.text
|
||||
|
|
Loading…
Add table
Reference in a new issue