mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 05:47:10 +00:00
Add group set volume
This commit is contained in:
parent
d1f0e0a70f
commit
13d9bbee52
@ -3,5 +3,6 @@
|
|||||||
ATTR_PASSWORD = "password"
|
ATTR_PASSWORD = "password"
|
||||||
ATTR_USERNAME = "username"
|
ATTR_USERNAME = "username"
|
||||||
DOMAIN = "heos"
|
DOMAIN = "heos"
|
||||||
|
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
||||||
SERVICE_SIGN_IN = "sign_in"
|
SERVICE_SIGN_IN = "sign_in"
|
||||||
SERVICE_SIGN_OUT = "sign_out"
|
SERVICE_SIGN_OUT = "sign_out"
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
|
"group_volume_set": {
|
||||||
|
"service": "mdi:volume-medium"
|
||||||
|
},
|
||||||
"sign_in": {
|
"sign_in": {
|
||||||
"service": "mdi:login"
|
"service": "mdi:login"
|
||||||
},
|
},
|
||||||
|
@ -17,10 +17,12 @@ from pyheos import (
|
|||||||
RepeatType,
|
RepeatType,
|
||||||
const as heos_const,
|
const as heos_const,
|
||||||
)
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import media_source
|
from homeassistant.components import media_source
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
ATTR_MEDIA_ENQUEUE,
|
ATTR_MEDIA_ENQUEUE,
|
||||||
|
ATTR_MEDIA_VOLUME_LEVEL,
|
||||||
BrowseMedia,
|
BrowseMedia,
|
||||||
MediaPlayerEnqueue,
|
MediaPlayerEnqueue,
|
||||||
MediaPlayerEntity,
|
MediaPlayerEntity,
|
||||||
@ -33,13 +35,17 @@ from homeassistant.components.media_player import (
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
entity_platform,
|
||||||
|
entity_registry as er,
|
||||||
|
)
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from .const import DOMAIN as HEOS_DOMAIN
|
from .const import DOMAIN as HEOS_DOMAIN, SERVICE_GROUP_VOLUME_SET
|
||||||
from .coordinator import HeosConfigEntry, HeosCoordinator
|
from .coordinator import HeosConfigEntry, HeosCoordinator
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@ -93,6 +99,13 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add media players for a config entry."""
|
"""Add media players for a config entry."""
|
||||||
|
# Register custom entity services
|
||||||
|
platform = entity_platform.async_get_current_platform()
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
|
{vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float},
|
||||||
|
"async_set_group_volume_level",
|
||||||
|
)
|
||||||
|
|
||||||
def add_entities_callback(players: Sequence[HeosPlayer]) -> None:
|
def add_entities_callback(players: Sequence[HeosPlayer]) -> None:
|
||||||
"""Add entities for each player."""
|
"""Add entities for each player."""
|
||||||
@ -346,6 +359,19 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
|||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
await self._player.set_volume(int(volume * 100))
|
await self._player.set_volume(int(volume * 100))
|
||||||
|
|
||||||
|
@catch_action_error("set group volume level")
|
||||||
|
async def async_set_group_volume_level(self, volume_level: float) -> None:
|
||||||
|
"""Set group volume level."""
|
||||||
|
if self._player.group_id is None:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=HEOS_DOMAIN,
|
||||||
|
translation_key="entity_not_grouped",
|
||||||
|
translation_placeholders={"entity_id": self.entity_id},
|
||||||
|
)
|
||||||
|
await self.coordinator.heos.set_group_volume(
|
||||||
|
self._player.group_id, int(volume_level * 100)
|
||||||
|
)
|
||||||
|
|
||||||
@catch_action_error("join players")
|
@catch_action_error("join players")
|
||||||
async def async_join_players(self, group_members: list[str]) -> None:
|
async def async_join_players(self, group_members: list[str]) -> None:
|
||||||
"""Join `group_members` as a player group with the current player."""
|
"""Join `group_members` as a player group with the current player."""
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
|
group_volume_set:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: heos
|
||||||
|
domain: media_player
|
||||||
|
fields:
|
||||||
|
volume_level:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 1
|
||||||
|
step: 0.01
|
||||||
|
|
||||||
sign_in:
|
sign_in:
|
||||||
fields:
|
fields:
|
||||||
username:
|
username:
|
||||||
|
@ -71,6 +71,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
"group_volume_set": {
|
||||||
|
"name": "Set group volume",
|
||||||
|
"description": "Sets the group's volume while preserving member volume ratios.",
|
||||||
|
"fields": {
|
||||||
|
"volume_level": {
|
||||||
|
"name": "Level",
|
||||||
|
"description": "The volume. 0 is inaudible, 1 is the maximum volume."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sign_in": {
|
"sign_in": {
|
||||||
"name": "Sign in",
|
"name": "Sign in",
|
||||||
"description": "Signs in to a HEOS account.",
|
"description": "Signs in to a HEOS account.",
|
||||||
@ -94,6 +104,9 @@
|
|||||||
"action_error": {
|
"action_error": {
|
||||||
"message": "Unable to {action}: {error}"
|
"message": "Unable to {action}: {error}"
|
||||||
},
|
},
|
||||||
|
"entity_not_grouped": {
|
||||||
|
"message": "Entity {entity_id} is not joined to a group"
|
||||||
|
},
|
||||||
"entity_not_found": {
|
"entity_not_found": {
|
||||||
"message": "Entity {entity_id} was not found"
|
"message": "Entity {entity_id} was not found"
|
||||||
},
|
},
|
||||||
|
@ -34,6 +34,7 @@ class MockHeos(Heos):
|
|||||||
self.player_set_play_state: AsyncMock = AsyncMock()
|
self.player_set_play_state: AsyncMock = AsyncMock()
|
||||||
self.player_set_volume: AsyncMock = AsyncMock()
|
self.player_set_volume: AsyncMock = AsyncMock()
|
||||||
self.set_group: AsyncMock = AsyncMock()
|
self.set_group: AsyncMock = AsyncMock()
|
||||||
|
self.set_group_volume: AsyncMock = AsyncMock()
|
||||||
self.sign_in: AsyncMock = AsyncMock()
|
self.sign_in: AsyncMock = AsyncMock()
|
||||||
self.sign_out: AsyncMock = AsyncMock()
|
self.sign_out: AsyncMock = AsyncMock()
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import pytest
|
|||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
from syrupy.filters import props
|
from syrupy.filters import props
|
||||||
|
|
||||||
from homeassistant.components.heos.const import DOMAIN
|
from homeassistant.components.heos.const import DOMAIN, SERVICE_GROUP_VOLUME_SET
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
ATTR_GROUP_MEMBERS,
|
ATTR_GROUP_MEMBERS,
|
||||||
ATTR_INPUT_SOURCE,
|
ATTR_INPUT_SOURCE,
|
||||||
@ -724,6 +724,62 @@ async def test_volume_set_error(
|
|||||||
controller.player_set_volume.assert_called_once_with(1, 100)
|
controller.player_set_volume.assert_called_once_with(1, 100)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_group_volume_set(
|
||||||
|
hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos
|
||||||
|
) -> None:
|
||||||
|
"""Test the group volume set service."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
|
{ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_LEVEL: 1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
controller.set_group_volume.assert_called_once_with(999, 100)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_group_volume_set_error(
|
||||||
|
hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos
|
||||||
|
) -> None:
|
||||||
|
"""Test the group volume set service errors."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
controller.set_group_volume.side_effect = CommandFailedError("", "Failure", 1)
|
||||||
|
with pytest.raises(
|
||||||
|
HomeAssistantError,
|
||||||
|
match=re.escape("Unable to set group volume level: Failure (1)"),
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
|
{ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_LEVEL: 1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
controller.set_group_volume.assert_called_once_with(999, 100)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_group_volume_set_not_grouped_error(
|
||||||
|
hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos
|
||||||
|
) -> None:
|
||||||
|
"""Test the group volume set service when not grouped raises error."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
player = controller.players[1]
|
||||||
|
player.group_id = None
|
||||||
|
with pytest.raises(
|
||||||
|
ServiceValidationError,
|
||||||
|
match=re.escape("Entity media_player.test_player is not joined to a group"),
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
|
{ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_LEVEL: 1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
controller.set_group_volume.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
async def test_select_favorite(
|
async def test_select_favorite(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user