diff --git a/homeassistant/components/russound_rio/quality_scale.yaml b/homeassistant/components/russound_rio/quality_scale.yaml index 63693ee6259..6edf439cae6 100644 --- a/homeassistant/components/russound_rio/quality_scale.yaml +++ b/homeassistant/components/russound_rio/quality_scale.yaml @@ -38,7 +38,7 @@ rules: comment: | This integration does not require authentication. parallel-updates: done - test-coverage: todo + test-coverage: done integration-owner: done docs-installation-parameters: todo docs-configuration-parameters: diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index 3321d4160b9..b9e6e89812a 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -47,27 +47,54 @@ def mock_russound_client() -> Generator[AsyncMock]: ), ): client = mock_client.return_value - zones = { - int(k): ZoneControlSurface.from_dict(v) - for k, v in load_json_object_fixture("get_zones.json", DOMAIN).items() + controller_zones = { + int(controller_id): { + int(zone_id): ZoneControlSurface.from_dict(zone) + for zone_id, zone in v["zones"].items() + } + for controller_id, v in load_json_object_fixture("get_zones.json", DOMAIN)[ + "controllers" + ].items() } client.sources = { int(k): Source.from_dict(v) for k, v in load_json_object_fixture("get_sources.json", DOMAIN).items() } client.state = load_json_object_fixture("get_state.json", DOMAIN) - for k, v in zones.items(): - v.device_str = zone_device_str(1, k) - v.fetch_current_source = Mock( - side_effect=lambda current_source=v.current_source: client.sources.get( - int(current_source) + for controller_id, zones in controller_zones.items(): + for zone_id, zone in zones.items(): + zone.device_str = zone_device_str(controller_id, zone_id) + zone.fetch_current_source = Mock( + side_effect=lambda current_source=zone.current_source: client.sources.get( + int(current_source) + ) ) - ) + zone.volume_up = AsyncMock() + zone.volume_down = AsyncMock() + zone.set_volume = AsyncMock() + zone.zone_on = AsyncMock() + zone.zone_off = AsyncMock() + zone.select_source = AsyncMock() client.controllers = { 1: Controller( - 1, "MCA-C5", client, controller_device_str(1), HARDWARE_MAC, None, zones - ) + 1, + MODEL, + client, + controller_device_str(1), + HARDWARE_MAC, + None, + controller_zones[1], + ), + 2: Controller( + 2, + MODEL, + client, + controller_device_str(2), + None, + None, + controller_zones[2], + ), } client.connection_handler = RussoundTcpConnectionHandler( MOCK_CONFIG[CONF_HOST], MOCK_CONFIG[CONF_PORT] diff --git a/tests/components/russound_rio/const.py b/tests/components/russound_rio/const.py index 18f75838525..8269e825e33 100644 --- a/tests/components/russound_rio/const.py +++ b/tests/components/russound_rio/const.py @@ -1,7 +1,5 @@ """Constants for russound_rio tests.""" -from collections import namedtuple - from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT @@ -19,9 +17,6 @@ MOCK_RECONFIGURATION_CONFIG = { CONF_PORT: 9622, } -_CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"]) # noqa: PYI024 -MOCK_CONTROLLERS = {1: _CONTROLLER(mac_address=HARDWARE_MAC, controller_type=MODEL)} - DEVICE_NAME = "mca_c5" NAME_ZONE_1 = "backyard" ENTITY_ID_ZONE_1 = f"{MP_DOMAIN}.{DEVICE_NAME}_{NAME_ZONE_1}" diff --git a/tests/components/russound_rio/fixtures/get_zones.json b/tests/components/russound_rio/fixtures/get_zones.json index 396310339b3..e1077944593 100644 --- a/tests/components/russound_rio/fixtures/get_zones.json +++ b/tests/components/russound_rio/fixtures/get_zones.json @@ -1,22 +1,38 @@ { - "1": { - "name": "Backyard", - "volume": "10", - "status": "ON", - "enabled": "True", - "current_source": "1" - }, - "2": { - "name": "Kitchen", - "volume": "50", - "status": "OFF", - "enabled": "True", - "current_source": "2" - }, - "3": { - "name": "Bedroom", - "volume": "10", - "status": "OFF", - "enabled": "False" + "controllers": { + "1": { + "zones": { + "1": { + "name": "Backyard", + "volume": "10", + "status": "ON", + "enabled": "True", + "current_source": "1" + }, + "2": { + "name": "Kitchen", + "volume": "50", + "status": "OFF", + "enabled": "True", + "current_source": "2" + }, + "3": { + "name": "Bedroom", + "volume": "10", + "status": "OFF", + "enabled": "False" + } + } + }, + "2": { + "zones": { + "9": { + "name": "Living Room", + "volume": "10", + "status": "OFF", + "enabled": "True" + } + } + } } } diff --git a/tests/components/russound_rio/test_media_player.py b/tests/components/russound_rio/test_media_player.py index c740ec4f39e..1ff87ee8b0e 100644 --- a/tests/components/russound_rio/test_media_player.py +++ b/tests/components/russound_rio/test_media_player.py @@ -2,10 +2,23 @@ from unittest.mock import AsyncMock +from aiorussound.exceptions import CommandError from aiorussound.models import PlayStatus import pytest +from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_VOLUME_LEVEL, + DOMAIN as MP_DOMAIN, + SERVICE_SELECT_SOURCE, +) from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, STATE_BUFFERING, STATE_IDLE, STATE_OFF, @@ -14,6 +27,7 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from . import mock_state_update, setup_integration from .const import ENTITY_ID_ZONE_1 @@ -50,3 +64,115 @@ async def test_entity_state( state = hass.states.get(ENTITY_ID_ZONE_1) assert state.state == media_player_state + + +async def test_media_volume( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test volume service.""" + await setup_integration(hass, mock_config_entry) + + # Test volume up + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_UP, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].volume_up.assert_called_once() + + # Test volume down + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_DOWN, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].volume_down.assert_called_once() + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_LEVEL: 0.30}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].set_volume.assert_called_once_with( + "15" + ) + + +@pytest.mark.parametrize( + ("source_name", "source_id"), + [ + ("Aux", 1), + ("Spotify", 2), + ], +) +async def test_source_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, + source_name: str, + source_id: int, +) -> None: + """Test source service.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_INPUT_SOURCE: source_name}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].select_source.assert_called_once_with( + source_id + ) + + +async def test_invalid_source_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test source service with invalid source ID.""" + await setup_integration(hass, mock_config_entry) + + mock_russound_client.controllers[1].zones[ + 1 + ].select_source.side_effect = CommandError + + with pytest.raises( + HomeAssistantError, + match="Error executing async_select_source on entity media_player.mca_c5_backyard", + ): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_INPUT_SOURCE: "Aux"}, + blocking=True, + ) + + +async def test_power_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test power service.""" + await setup_integration(hass, mock_config_entry) + + data = {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1} + + await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + mock_russound_client.controllers[1].zones[1].zone_on.assert_called_once() + + await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + + mock_russound_client.controllers[1].zones[1].zone_off.assert_called_once()