diff --git a/homeassistant/components/heos/const.py b/homeassistant/components/heos/const.py index 0203def3885..e9ab51bf16e 100644 --- a/homeassistant/components/heos/const.py +++ b/homeassistant/components/heos/const.py @@ -4,5 +4,7 @@ ATTR_PASSWORD = "password" ATTR_USERNAME = "username" DOMAIN = "heos" SERVICE_GROUP_VOLUME_SET = "group_volume_set" +SERVICE_GROUP_VOLUME_DOWN = "group_volume_down" +SERVICE_GROUP_VOLUME_UP = "group_volume_up" SERVICE_SIGN_IN = "sign_in" SERVICE_SIGN_OUT = "sign_out" diff --git a/homeassistant/components/heos/icons.json b/homeassistant/components/heos/icons.json index a634701037c..d7a998b6aec 100644 --- a/homeassistant/components/heos/icons.json +++ b/homeassistant/components/heos/icons.json @@ -3,6 +3,12 @@ "group_volume_set": { "service": "mdi:volume-medium" }, + "group_volume_down": { + "service": "mdi:volume-low" + }, + "group_volume_up": { + "service": "mdi:volume-high" + }, "sign_in": { "service": "mdi:login" }, diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index a649740a933..9edc674d1cf 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -45,7 +45,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow -from .const import DOMAIN as HEOS_DOMAIN, SERVICE_GROUP_VOLUME_SET +from .const import ( + DOMAIN as HEOS_DOMAIN, + SERVICE_GROUP_VOLUME_DOWN, + SERVICE_GROUP_VOLUME_SET, + SERVICE_GROUP_VOLUME_UP, +) from .coordinator import HeosConfigEntry, HeosCoordinator PARALLEL_UPDATES = 0 @@ -106,6 +111,12 @@ async def async_setup_entry( {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float}, "async_set_group_volume_level", ) + platform.async_register_entity_service( + SERVICE_GROUP_VOLUME_DOWN, None, "async_group_volume_down" + ) + platform.async_register_entity_service( + SERVICE_GROUP_VOLUME_UP, None, "async_group_volume_up" + ) def add_entities_callback(players: Sequence[HeosPlayer]) -> None: """Add entities for each player.""" @@ -372,6 +383,28 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): self._player.group_id, int(volume_level * 100) ) + @catch_action_error("group volume down") + async def async_group_volume_down(self) -> None: + """Turn group volume down for media player.""" + 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.group_volume_down(self._player.group_id) + + @catch_action_error("group volume up") + async def async_group_volume_up(self) -> None: + """Turn group volume up for media player.""" + 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.group_volume_up(self._player.group_id) + @catch_action_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.""" diff --git a/homeassistant/components/heos/services.yaml b/homeassistant/components/heos/services.yaml index 948aeb919f4..8f3a43421f6 100644 --- a/homeassistant/components/heos/services.yaml +++ b/homeassistant/components/heos/services.yaml @@ -12,6 +12,18 @@ group_volume_set: max: 1 step: 0.01 +group_volume_down: + target: + entity: + integration: heos + domain: media_player + +group_volume_up: + target: + entity: + integration: heos + domain: media_player + sign_in: fields: username: diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index 2f3b82efc8d..cd3f0b998a1 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -81,6 +81,14 @@ } } }, + "group_volume_down": { + "name": "Turn down group volume", + "description": "Turns down the group volume." + }, + "group_volume_up": { + "name": "Turn up group volume", + "description": "Turns up the group volume." + }, "sign_in": { "name": "Sign in", "description": "Signs in to a HEOS account.", diff --git a/tests/components/heos/__init__.py b/tests/components/heos/__init__.py index bc72981d805..0b8aed91edf 100644 --- a/tests/components/heos/__init__.py +++ b/tests/components/heos/__init__.py @@ -20,6 +20,8 @@ class MockHeos(Heos): self.get_input_sources: AsyncMock = AsyncMock() self.get_playlists: AsyncMock = AsyncMock() self.get_players: AsyncMock = AsyncMock() + self.group_volume_down: AsyncMock = AsyncMock() + self.group_volume_up: AsyncMock = AsyncMock() self.load_players: AsyncMock = AsyncMock() self.play_media: AsyncMock = AsyncMock() self.play_preset_station: AsyncMock = AsyncMock() diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 5a0ed0aa7c4..3e755a29a0a 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -22,7 +22,12 @@ import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import props -from homeassistant.components.heos.const import DOMAIN, SERVICE_GROUP_VOLUME_SET +from homeassistant.components.heos.const import ( + DOMAIN, + SERVICE_GROUP_VOLUME_DOWN, + SERVICE_GROUP_VOLUME_SET, + SERVICE_GROUP_VOLUME_UP, +) from homeassistant.components.media_player import ( ATTR_GROUP_MEMBERS, ATTR_INPUT_SOURCE, @@ -780,6 +785,64 @@ async def test_group_volume_set_not_grouped_error( controller.set_group_volume.assert_not_called() +async def test_group_volume_down( + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos +) -> None: + """Test the group volume down 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_DOWN, + {ATTR_ENTITY_ID: "media_player.test_player"}, + blocking=True, + ) + controller.group_volume_down.assert_called_with(999) + + +async def test_group_volume_up( + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos +) -> None: + """Test the group volume up 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_UP, + {ATTR_ENTITY_ID: "media_player.test_player"}, + blocking=True, + ) + controller.group_volume_up.assert_called_with(999) + + +@pytest.mark.parametrize( + "service", [SERVICE_GROUP_VOLUME_DOWN, SERVICE_GROUP_VOLUME_UP] +) +async def test_group_volume_down_up_ungrouped_raises( + hass: HomeAssistant, + config_entry: MockConfigEntry, + controller: MockHeos, + service: str, +) -> None: + """Test the group volume down and up service raise if player ungrouped.""" + 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, + {ATTR_ENTITY_ID: "media_player.test_player"}, + blocking=True, + ) + controller.group_volume_down.assert_not_called() + controller.group_volume_up.assert_not_called() + + async def test_select_favorite( hass: HomeAssistant, config_entry: MockConfigEntry,