From aceb1b39ba4ed15dd96404ab03fbd360e5a2691c Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Sat, 28 Dec 2024 03:22:13 -0500 Subject: [PATCH] Add mute support to Russound RIO (#134118) --- .../components/russound_rio/const.py | 7 --- .../components/russound_rio/media_player.py | 23 ++++++-- tests/components/russound_rio/conftest.py | 3 + .../russound_rio/test_media_player.py | 56 +++++++++++++++++++ 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/russound_rio/const.py b/homeassistant/components/russound_rio/const.py index a142ba8641d..9647c419da0 100644 --- a/homeassistant/components/russound_rio/const.py +++ b/homeassistant/components/russound_rio/const.py @@ -3,9 +3,6 @@ import asyncio from aiorussound import CommandError -from aiorussound.const import FeatureFlag - -from homeassistant.components.media_player import MediaPlayerEntityFeature DOMAIN = "russound_rio" @@ -15,7 +12,3 @@ RUSSOUND_RIO_EXCEPTIONS = ( TimeoutError, asyncio.CancelledError, ) - -MP_FEATURES_BY_FLAG = { - FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON: MediaPlayerEntityFeature.VOLUME_MUTE -} diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index b7ad4f5d89b..62981262e32 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -22,7 +22,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RussoundConfigEntry -from .const import MP_FEATURES_BY_FLAG from .entity import RussoundBaseEntity, command _LOGGER = logging.getLogger(__name__) @@ -54,6 +53,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE @@ -69,9 +69,6 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): self._sources = sources self._attr_name = _zone.name self._attr_unique_id = f"{self._primary_mac_address}-{_zone.device_str}" - for flag, feature in MP_FEATURES_BY_FLAG.items(): - if flag in self._client.supported_features: - self._attr_supported_features |= feature @property def _zone(self) -> ZoneControlSurface: @@ -150,6 +147,11 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): """ return self._zone.volume / 50.0 + @property + def is_volume_muted(self) -> bool: + """Return whether zone is muted.""" + return self._zone.is_mute + @command async def async_turn_off(self) -> None: """Turn off the zone.""" @@ -184,3 +186,16 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): async def async_volume_down(self) -> None: """Step the volume down.""" await self._zone.volume_down() + + @command + async def async_mute_volume(self, mute: bool) -> None: + """Mute the media player.""" + if FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON in self._client.supported_features: + if mute: + await self._zone.mute() + else: + await self._zone.unmute() + return + + if mute != self.is_volume_muted: + await self._zone.toggle_mute() diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index b9e6e89812a..bf6884e09fb 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -75,6 +75,9 @@ def mock_russound_client() -> Generator[AsyncMock]: zone.zone_on = AsyncMock() zone.zone_off = AsyncMock() zone.select_source = AsyncMock() + zone.mute = AsyncMock() + zone.unmute = AsyncMock() + zone.toggle_mute = AsyncMock() client.controllers = { 1: Controller( diff --git a/tests/components/russound_rio/test_media_player.py b/tests/components/russound_rio/test_media_player.py index 1ff87ee8b0e..5a6420da000 100644 --- a/tests/components/russound_rio/test_media_player.py +++ b/tests/components/russound_rio/test_media_player.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock +from aiorussound.const import FeatureFlag from aiorussound.exceptions import CommandError from aiorussound.models import PlayStatus import pytest @@ -9,6 +10,7 @@ import pytest from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MP_DOMAIN, SERVICE_SELECT_SOURCE, ) @@ -17,6 +19,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_BUFFERING, @@ -106,6 +109,59 @@ async def test_media_volume( ) +async def test_volume_mute( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test mute service.""" + await setup_integration(hass, mock_config_entry) + + # Test mute (w/ toggle mute support) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_MUTED: True}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].toggle_mute.assert_called_once() + mock_russound_client.controllers[1].zones[1].toggle_mute.reset_mock() + + mock_russound_client.controllers[1].zones[1].is_mute = True + + # Test mute when already muted (w/ toggle mute support) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_MUTED: True}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].toggle_mute.assert_not_called() + mock_russound_client.supported_features = [FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON] + + # Test mute (w/ dedicated commands) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_MUTED: True}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].mute.assert_called_once() + + # Test unmute (w/ dedicated commands) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_MUTED: False}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].unmute.assert_called_once() + + @pytest.mark.parametrize( ("source_name", "source_id"), [