From bc7c5fbc860cea539e44fd3e4c2a5b20caa354a5 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:44:57 -0600 Subject: [PATCH] Fix typing errors in HEOS tests (#136795) * Correct typing errors of mocked heos * Fix player related typing issues * Sort mocks --- tests/components/heos/__init__.py | 57 +++ tests/components/heos/conftest.py | 98 +++--- .../heos/snapshots/test_diagnostics.ambr | 11 +- tests/components/heos/test_config_flow.py | 52 +-- tests/components/heos/test_diagnostics.py | 10 +- tests/components/heos/test_init.py | 47 ++- tests/components/heos/test_media_player.py | 324 +++++++++--------- tests/components/heos/test_services.py | 14 +- 8 files changed, 343 insertions(+), 270 deletions(-) diff --git a/tests/components/heos/__init__.py b/tests/components/heos/__init__.py index 3a774529c69..cf0d10790b7 100644 --- a/tests/components/heos/__init__.py +++ b/tests/components/heos/__init__.py @@ -1 +1,58 @@ """Tests for the Heos component.""" + +from unittest.mock import AsyncMock + +from pyheos import Heos, HeosGroup, HeosOptions, HeosPlayer + + +class MockHeos(Heos): + """Defines a mocked HEOS API.""" + + def __init__(self, options: HeosOptions) -> None: + """Initialize the mock.""" + super().__init__(options) + # Overwrite the methods with async mocks, changing type + self.add_to_queue: AsyncMock = AsyncMock() + self.connect: AsyncMock = AsyncMock() + self.disconnect: AsyncMock = AsyncMock() + self.get_favorites: AsyncMock = AsyncMock() + self.get_groups: AsyncMock = AsyncMock() + self.get_input_sources: AsyncMock = AsyncMock() + self.get_playlists: AsyncMock = AsyncMock() + self.get_players: AsyncMock = AsyncMock() + self.load_players: AsyncMock = AsyncMock() + self.play_media: AsyncMock = AsyncMock() + self.play_preset_station: AsyncMock = AsyncMock() + self.play_url: AsyncMock = AsyncMock() + self.player_clear_queue: AsyncMock = AsyncMock() + self.player_get_quick_selects: AsyncMock = AsyncMock() + self.player_play_next: AsyncMock = AsyncMock() + self.player_play_previous: AsyncMock = AsyncMock() + self.player_play_quick_select: AsyncMock = AsyncMock() + self.player_set_mute: AsyncMock = AsyncMock() + self.player_set_play_mode: AsyncMock = AsyncMock() + self.player_set_play_state: AsyncMock = AsyncMock() + self.player_set_volume: AsyncMock = AsyncMock() + self.set_group: AsyncMock = AsyncMock() + self.sign_in: AsyncMock = AsyncMock() + self.sign_out: AsyncMock = AsyncMock() + + def mock_set_players(self, players: dict[int, HeosPlayer]) -> None: + """Set the players on the mock instance.""" + for player in players.values(): + player.heos = self + self._players = players + self._players_loaded = bool(players) + self.get_players.return_value = players + + def mock_set_groups(self, groups: dict[int, HeosGroup]) -> None: + """Set the groups on the mock instance.""" + for group in groups.values(): + group.heos = self + self._groups = groups + self._groups_loaded = bool(groups) + self.get_groups.return_value = groups + + def mock_set_signed_in_username(self, signed_in_username: str | None) -> None: + """Set the signed in status on the mock instance.""" + self._signed_in_username = signed_in_username diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 5312b8295ed..5ec809b10e9 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -2,11 +2,10 @@ from __future__ import annotations -from collections.abc import AsyncIterator -from unittest.mock import AsyncMock, Mock, patch +from collections.abc import Iterator +from unittest.mock import Mock, patch from pyheos import ( - Heos, HeosGroup, HeosHost, HeosNowPlayingMedia, @@ -38,6 +37,8 @@ from homeassistant.helpers.service_info.ssdp import ( SsdpServiceInfo, ) +from . import MockHeos + from tests.common import MockConfigEntry @@ -64,6 +65,17 @@ def config_entry_options_fixture() -> MockConfigEntry: ) +@pytest.fixture(name="new_mock", autouse=True) +def new_heos_mock_fixture(controller: MockHeos) -> Iterator[Mock]: + """Patch the Heos class to return the mock instance.""" + new_mock = Mock(return_value=controller) + with ( + patch("homeassistant.components.heos.coordinator.Heos", new=new_mock), + patch("homeassistant.components.heos.config_flow.Heos", new=new_mock), + ): + yield new_mock + + @pytest_asyncio.fixture(name="controller", autouse=True) async def controller_fixture( players: dict[int, HeosPlayer], @@ -72,49 +84,38 @@ async def controller_fixture( playlists: list[MediaItem], change_data: PlayerUpdateResult, group: dict[int, HeosGroup], -) -> AsyncIterator[Heos]: + quick_selects: dict[int, str], +) -> MockHeos: """Create a mock Heos controller fixture.""" - mock_heos = Heos(HeosOptions(host="127.0.0.1")) - for player in players.values(): - player.heos = mock_heos - mock_heos.connect = AsyncMock() - mock_heos.disconnect = AsyncMock() - mock_heos.sign_in = AsyncMock() - mock_heos.sign_out = AsyncMock() - mock_heos.get_players = AsyncMock(return_value=players) - mock_heos._players = players - mock_heos.get_favorites = AsyncMock(return_value=favorites) - mock_heos.get_input_sources = AsyncMock(return_value=input_sources) - mock_heos.get_playlists = AsyncMock(return_value=playlists) - mock_heos.load_players = AsyncMock(return_value=change_data) - mock_heos._signed_in_username = "user@user.com" - mock_heos.get_groups = AsyncMock(return_value=group) - mock_heos._groups = group - mock_heos.set_group = AsyncMock(return_value=None) - new_mock = Mock(return_value=mock_heos) - mock_heos.new_mock = new_mock - with ( - patch("homeassistant.components.heos.coordinator.Heos", new=new_mock), - patch("homeassistant.components.heos.config_flow.Heos", new=new_mock), - ): - yield mock_heos + + mock_heos = MockHeos(HeosOptions(host="127.0.0.1")) + mock_heos.mock_set_signed_in_username("user@user.com") + mock_heos.mock_set_players(players) + mock_heos.mock_set_groups(group) + mock_heos.get_favorites.return_value = favorites + mock_heos.get_input_sources.return_value = input_sources + mock_heos.get_playlists.return_value = playlists + mock_heos.load_players.return_value = change_data + mock_heos.player_get_quick_selects.return_value = quick_selects + return mock_heos @pytest.fixture(name="system") -def system_info_fixture() -> dict[str, str]: +def system_info_fixture() -> HeosSystem: """Create a system info fixture.""" + main_host = HeosHost( + "Test Player", + "HEOS Drive HS2", + "123456", + "1.0.0", + "127.0.0.1", + NetworkType.WIRED, + ) return HeosSystem( "user@user.com", - "127.0.0.1", + main_host, hosts=[ - HeosHost( - "Test Player", - "HEOS Drive HS2", - "123456", - "1.0.0", - "127.0.0.1", - NetworkType.WIRED, - ), + main_host, HeosHost( "Test Player 2", "Speaker", @@ -128,7 +129,7 @@ def system_info_fixture() -> dict[str, str]: @pytest.fixture(name="players") -def players_fixture(quick_selects: dict[int, str]) -> dict[int, HeosPlayer]: +def players_fixture() -> dict[int, HeosPlayer]: """Create two mock HeosPlayers.""" players = {} for i in (1, 2): @@ -148,7 +149,6 @@ def players_fixture(quick_selects: dict[int, str]) -> dict[int, HeosPlayer]: shuffle=False, repeat=RepeatType.OFF, volume=25, - heos=None, ) player.now_playing_media = HeosNowPlayingMedia( type=MediaType.STATION, @@ -162,24 +162,6 @@ def players_fixture(quick_selects: dict[int, str]) -> dict[int, HeosPlayer]: queue_id=1, source_id=10, ) - player.add_to_queue = AsyncMock() - player.clear_queue = AsyncMock() - player.get_quick_selects = AsyncMock(return_value=quick_selects) - player.mute = AsyncMock() - player.pause = AsyncMock() - player.play = AsyncMock() - player.play_media = AsyncMock() - player.play_next = AsyncMock() - player.play_previous = AsyncMock() - player.play_preset_station = AsyncMock() - player.play_quick_select = AsyncMock() - player.play_url = AsyncMock() - player.set_mute = AsyncMock() - player.set_play_mode = AsyncMock() - player.set_quick_select = AsyncMock() - player.set_volume = AsyncMock() - player.stop = AsyncMock() - player.unmute = AsyncMock() players[player.player_id] = player return players diff --git a/tests/components/heos/snapshots/test_diagnostics.ambr b/tests/components/heos/snapshots/test_diagnostics.ambr index 6de0a645f17..1df2d172142 100644 --- a/tests/components/heos/snapshots/test_diagnostics.ambr +++ b/tests/components/heos/snapshots/test_diagnostics.ambr @@ -98,8 +98,15 @@ 'Speaker - Line In 1', ]), 'system': dict({ - 'connected_to_preferred_host': False, - 'host': '127.0.0.1', + 'connected_to_preferred_host': True, + 'host': dict({ + 'ip_address': '127.0.0.1', + 'model': 'HEOS Drive HS2', + 'name': 'Test Player', + 'network': 'wired', + 'serial': '**REDACTED**', + 'version': '1.0.0', + }), 'hosts': list([ dict({ 'ip_address': '127.0.0.1', diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 39ede354496..cbc32526958 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,6 +1,8 @@ """Tests for the Heos config flow module.""" -from pyheos import CommandAuthenticationError, CommandFailedError, Heos, HeosError +from typing import Any + +from pyheos import CommandAuthenticationError, CommandFailedError, HeosError import pytest from homeassistant.components.heos.const import DOMAIN @@ -10,6 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo +from . import MockHeos + from tests.common import MockConfigEntry @@ -38,7 +42,7 @@ async def test_no_host_shows_form(hass: HomeAssistant) -> None: async def test_cannot_connect_shows_error_form( - hass: HomeAssistant, controller: Heos + hass: HomeAssistant, controller: MockHeos ) -> None: """Test form is shown with error when cannot connect.""" controller.connect.side_effect = HeosError() @@ -47,13 +51,15 @@ async def test_cannot_connect_shows_error_form( ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"][CONF_HOST] == "cannot_connect" + errors = result["errors"] + assert errors is not None + assert errors[CONF_HOST] == "cannot_connect" assert controller.connect.call_count == 1 assert controller.disconnect.call_count == 1 async def test_create_entry_when_host_valid( - hass: HomeAssistant, controller: Heos + hass: HomeAssistant, controller: MockHeos ) -> None: """Test result type is create entry when host is valid.""" data = {CONF_HOST: "127.0.0.1"} @@ -70,7 +76,7 @@ async def test_create_entry_when_host_valid( async def test_create_entry_when_friendly_name_valid( - hass: HomeAssistant, controller: Heos + hass: HomeAssistant, controller: MockHeos ) -> None: """Test result type is create entry when friendly name is valid.""" hass.data[DOMAIN] = {"Office (127.0.0.1)": "127.0.0.1"} @@ -131,7 +137,7 @@ async def test_discovery_flow_aborts_already_setup( async def test_reconfigure_validates_and_updates_config( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test reconfigure validates host and successfully updates.""" config_entry.add_to_hass(hass) @@ -139,9 +145,9 @@ async def test_reconfigure_validates_and_updates_config( assert config_entry.data[CONF_HOST] == "127.0.0.1" # Test reconfigure initially shows form with current host value. - host = next( - key.default() for key in result["data_schema"].schema if key == CONF_HOST - ) + schema = result["data_schema"] + assert schema is not None + host = next(key.default() for key in schema.schema if key == CONF_HOST) assert host == "127.0.0.1" assert result["errors"] == {} assert result["step_id"] == "reconfigure" @@ -161,7 +167,7 @@ async def test_reconfigure_validates_and_updates_config( async def test_reconfigure_cannot_connect_recovers( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test reconfigure cannot connect and recovers.""" controller.connect.side_effect = HeosError() @@ -176,11 +182,13 @@ async def test_reconfigure_cannot_connect_recovers( assert controller.connect.call_count == 1 assert controller.disconnect.call_count == 1 - host = next( - key.default() for key in result["data_schema"].schema if key == CONF_HOST - ) + schema = result["data_schema"] + assert schema is not None + host = next(key.default() for key in schema.schema if key == CONF_HOST) assert host == "127.0.0.2" - assert result["errors"][CONF_HOST] == "cannot_connect" + errors = result["errors"] + assert errors is not None + assert errors[CONF_HOST] == "cannot_connect" assert result["step_id"] == "reconfigure" assert result["type"] is FlowResultType.FORM @@ -214,7 +222,7 @@ async def test_reconfigure_cannot_connect_recovers( async def test_options_flow_signs_in( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, error: HeosError, expected_error_key: str, ) -> None: @@ -255,7 +263,7 @@ async def test_options_flow_signs_in( async def test_options_flow_signs_out( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test options flow signs-out when credentials cleared.""" config_entry.add_to_hass(hass) @@ -268,7 +276,7 @@ async def test_options_flow_signs_out( assert result["type"] is FlowResultType.FORM # Fail to sign-out, show error - user_input = {} + user_input: dict[str, Any] = {} controller.sign_out.side_effect = HeosError() result = await hass.config_entries.options.async_configure( result["flow_id"], user_input @@ -301,7 +309,7 @@ async def test_options_flow_signs_out( async def test_options_flow_missing_one_param_recovers( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, user_input: dict[str, str], expected_errors: dict[str, str], ) -> None: @@ -350,7 +358,7 @@ async def test_options_flow_missing_one_param_recovers( async def test_reauth_signs_in_aborts( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, error: HeosError, expected_error_key: str, ) -> None: @@ -391,7 +399,7 @@ async def test_reauth_signs_in_aborts( async def test_reauth_signs_out( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test reauth flow signs-out when credentials cleared and aborts.""" config_entry.add_to_hass(hass) @@ -404,7 +412,7 @@ async def test_reauth_signs_out( assert result["type"] is FlowResultType.FORM # Fail to sign-out, show error - user_input = {} + user_input: dict[str, Any] = {} controller.sign_out.side_effect = HeosError() result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input @@ -439,7 +447,7 @@ async def test_reauth_signs_out( async def test_reauth_flow_missing_one_param_recovers( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, user_input: dict[str, str], expected_errors: dict[str, str], ) -> None: diff --git a/tests/components/heos/test_diagnostics.py b/tests/components/heos/test_diagnostics.py index d6fb8e1a8fe..2a7deccfb33 100644 --- a/tests/components/heos/test_diagnostics.py +++ b/tests/components/heos/test_diagnostics.py @@ -2,15 +2,17 @@ from unittest import mock -from pyheos import Heos, HeosSystem +from pyheos import HeosSystem import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.heos.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from . import MockHeos + from tests.common import MockConfigEntry from tests.components.diagnostics import ( get_diagnostics_for_config_entry, @@ -23,7 +25,7 @@ async def test_config_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, system: HeosSystem, snapshot: SnapshotAssertion, ) -> None: @@ -77,7 +79,7 @@ async def test_device_diagnostics( assert await hass.config_entries.async_setup(config_entry.entry_id) device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, "1")}) - + assert device is not None diagnostics = await get_diagnostics_for_device( hass, hass_client, config_entry, device ) diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 4c5eee67e2c..27dea82dcf2 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -1,8 +1,9 @@ """Tests for the init module.""" from typing import cast +from unittest.mock import Mock -from pyheos import Heos, HeosError, HeosOptions, SignalHeosEvent, SignalType +from pyheos import HeosError, HeosOptions, SignalHeosEvent, SignalType import pytest from homeassistant.components.heos.const import DOMAIN @@ -11,13 +12,15 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from . import MockHeos + from tests.common import MockConfigEntry async def test_async_setup_entry_loads_platforms( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, ) -> None: """Test load connects to heos, retrieves players, and loads platforms.""" config_entry.add_to_hass(hass) @@ -32,7 +35,10 @@ async def test_async_setup_entry_loads_platforms( async def test_async_setup_entry_with_options_loads_platforms( - hass: HomeAssistant, config_entry_options: MockConfigEntry, controller: Heos + hass: HomeAssistant, + config_entry_options: MockConfigEntry, + controller: MockHeos, + new_mock: Mock, ) -> None: """Test load connects to heos with options, retrieves players, and loads platforms.""" config_entry_options.add_to_hass(hass) @@ -40,8 +46,9 @@ async def test_async_setup_entry_with_options_loads_platforms( # Assert options passed and methods called assert config_entry_options.state is ConfigEntryState.LOADED - options = cast(HeosOptions, controller.new_mock.call_args[0][0]) + options = cast(HeosOptions, new_mock.call_args[0][0]) assert options.host == config_entry_options.data[CONF_HOST] + assert options.credentials is not None assert options.credentials.username == config_entry_options.options[CONF_USERNAME] assert options.credentials.password == config_entry_options.options[CONF_PASSWORD] assert controller.connect.call_count == 1 @@ -54,14 +61,14 @@ async def test_async_setup_entry_with_options_loads_platforms( async def test_async_setup_entry_auth_failure_starts_reauth( hass: HomeAssistant, config_entry_options: MockConfigEntry, - controller: Heos, + controller: MockHeos, ) -> None: """Test load with auth failure starts reauth, loads platforms.""" config_entry_options.add_to_hass(hass) # Simulates what happens when the controller can't sign-in during connection async def connect_send_auth_failure() -> None: - controller._signed_in_username = None + controller.mock_set_signed_in_username(None) await controller.dispatcher.wait_send( SignalType.HEOS_EVENT, SignalHeosEvent.USER_CREDENTIALS_INVALID ) @@ -76,19 +83,19 @@ async def test_async_setup_entry_auth_failure_starts_reauth( controller.disconnect.assert_not_called() assert config_entry_options.state is ConfigEntryState.LOADED assert any( - config_entry_options.async_get_active_flows(hass, sources=[SOURCE_REAUTH]) + config_entry_options.async_get_active_flows(hass, sources={SOURCE_REAUTH}) ) async def test_async_setup_entry_not_signed_in_loads_platforms( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, caplog: pytest.LogCaptureFixture, ) -> None: """Test setup does not retrieve favorites when not logged in.""" config_entry.add_to_hass(hass) - controller._signed_in_username = None + controller.mock_set_signed_in_username(None) assert await hass.config_entries.async_setup(config_entry.entry_id) assert controller.connect.call_count == 1 assert controller.get_players.call_count == 1 @@ -102,7 +109,7 @@ async def test_async_setup_entry_not_signed_in_loads_platforms( async def test_async_setup_entry_connect_failure( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Connection failure raises ConfigEntryNotReady.""" config_entry.add_to_hass(hass) @@ -114,7 +121,7 @@ async def test_async_setup_entry_connect_failure( async def test_async_setup_entry_player_failure( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Failure to retrieve players raises ConfigEntryNotReady.""" config_entry.add_to_hass(hass) @@ -126,7 +133,7 @@ async def test_async_setup_entry_player_failure( async def test_async_setup_entry_favorites_failure( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Failure to retrieve favorites loads.""" config_entry.add_to_hass(hass) @@ -136,7 +143,7 @@ async def test_async_setup_entry_favorites_failure( async def test_async_setup_entry_inputs_failure( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Failure to retrieve inputs loads.""" config_entry.add_to_hass(hass) @@ -146,7 +153,7 @@ async def test_async_setup_entry_inputs_failure( async def test_unload_entry( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test entries are unloaded correctly.""" config_entry.add_to_hass(hass) @@ -164,12 +171,14 @@ async def test_device_info( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) device = device_registry.async_get_device({(DOMAIN, "1")}) + assert device is not None assert device.manufacturer == "HEOS" assert device.model == "Drive HS2" assert device.name == "Test Player" assert device.serial_number == "123456" assert device.sw_version == "1.0.0" device = device_registry.async_get_device({(DOMAIN, "2")}) + assert device is not None assert device.manufacturer == "HEOS" assert device.model == "Speaker" @@ -183,12 +192,14 @@ async def test_device_id_migration( config_entry.add_to_hass(hass) # Create a device with a legacy identifier device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, 1)} + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, 1)}, # type: ignore[arg-type] ) device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={("Other", 1)} + config_entry_id=config_entry.entry_id, + identifiers={("Other", 1)}, # type: ignore[arg-type] ) assert await hass.config_entries.async_setup(config_entry.entry_id) - assert device_registry.async_get_device({("Other", 1)}) is not None - assert device_registry.async_get_device({(DOMAIN, 1)}) is None + assert device_registry.async_get_device({("Other", 1)}) is not None # type: ignore[arg-type] + assert device_registry.async_get_device({(DOMAIN, 1)}) is None # type: ignore[arg-type] assert device_registry.async_get_device({(DOMAIN, "1")}) is not None diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 8fc63bbc7ad..3768462eada 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -8,7 +8,6 @@ from freezegun.api import FrozenDateTimeFactory from pyheos import ( AddCriteriaType, CommandFailedError, - Heos, HeosError, MediaItem, MediaType as HeosMediaType, @@ -66,6 +65,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er +from . import MockHeos + from tests.common import MockConfigEntry, async_fire_time_changed @@ -88,7 +89,7 @@ async def test_state_attributes( async def test_updates_from_signals( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Tests dispatched signals update player.""" config_entry.add_to_hass(hass) @@ -97,33 +98,36 @@ async def test_updates_from_signals( # Test player does not update for other players player.state = PlayState.PLAY - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.PLAYER_EVENT, 2, const.EVENT_PLAYER_STATE_CHANGED ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.state == STATE_IDLE # Test player_update standard events player.state = PlayState.PLAY - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.state == STATE_PLAYING # Test player_update progress events player.now_playing_media.duration = 360000 player.now_playing_media.current_position = 1000 - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_NOW_PLAYING_PROGRESS, ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] is not None assert state.attributes[ATTR_MEDIA_DURATION] == 360 assert state.attributes[ATTR_MEDIA_POSITION] == 1 @@ -132,7 +136,7 @@ async def test_updates_from_signals( async def test_updates_from_connection_event( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, caplog: pytest.LogCaptureFixture, ) -> None: """Tests player updates from connection event after connection failure.""" @@ -142,34 +146,37 @@ async def test_updates_from_connection_event( # Connected player.available = True - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.state == STATE_IDLE assert controller.load_players.call_count == 1 # Disconnected controller.load_players.reset_mock() player.available = False - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.HEOS_EVENT, SignalHeosEvent.DISCONNECTED ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.state == STATE_UNAVAILABLE assert controller.load_players.call_count == 0 # Connected handles refresh failure controller.load_players.reset_mock() - controller.load_players.side_effect = CommandFailedError(None, "Failure", 1) + controller.load_players.side_effect = CommandFailedError("", "Failure", 1) player.available = True - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.state == STATE_IDLE assert controller.load_players.call_count == 1 assert "Unable to refresh players" in caplog.text @@ -180,7 +187,7 @@ async def test_updates_from_connection_event_new_player_ids( entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, change_data_mapped_ids: PlayerUpdateResult, ) -> None: """Test player ids changed after reconnection updates ids.""" @@ -208,16 +215,15 @@ async def test_updates_from_connection_event_new_player_ids( async def test_updates_from_sources_updated( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, freezer: FrozenDateTimeFactory, ) -> None: """Tests player updates from changes in sources list.""" config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] controller.get_input_sources.return_value = [] - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.CONTROLLER_EVENT, const.EVENT_SOURCES_CHANGED, {} ) freezer.tick(timedelta(seconds=1)) @@ -225,6 +231,7 @@ async def test_updates_from_sources_updated( await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.attributes[ATTR_INPUT_SOURCE_LIST] == [ "Today's Hits Radio", "Classical MPR (Classical Music)", @@ -234,7 +241,7 @@ async def test_updates_from_sources_updated( async def test_updates_from_players_changed( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, change_data: PlayerUpdateResult, ) -> None: """Test player updates from changes to available players.""" @@ -242,13 +249,17 @@ async def test_updates_from_players_changed( assert await hass.config_entries.async_setup(config_entry.entry_id) player = controller.players[1] - assert hass.states.get("media_player.test_player").state == STATE_IDLE + state = hass.states.get("media_player.test_player") + assert state is not None + assert state.state == STATE_IDLE player.state = PlayState.PLAY - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, change_data ) await hass.async_block_till_done() - assert hass.states.get("media_player.test_player").state == STATE_PLAYING + state = hass.states.get("media_player.test_player") + assert state is not None + assert state.state == STATE_PLAYING async def test_updates_from_players_changed_new_ids( @@ -256,13 +267,12 @@ async def test_updates_from_players_changed_new_ids( entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, change_data_mapped_ids: PlayerUpdateResult, ) -> None: """Test player updates from changes to available players.""" config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] # Assert device registry matches current id assert device_registry.async_get_device(identifiers={(DOMAIN, "1")}) @@ -272,7 +282,7 @@ async def test_updates_from_players_changed_new_ids( == "media_player.test_player" ) - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, change_data_mapped_ids, @@ -293,16 +303,15 @@ async def test_updates_from_players_changed_new_ids( async def test_updates_from_user_changed( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, freezer: FrozenDateTimeFactory, ) -> None: """Tests player updates from changes in user.""" config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - controller._signed_in_username = None - await player.heos.dispatcher.wait_send( + controller.mock_set_signed_in_username(None) + await controller.dispatcher.wait_send( SignalType.CONTROLLER_EVENT, const.EVENT_USER_CHANGED, None ) freezer.tick(timedelta(seconds=1)) @@ -310,6 +319,7 @@ async def test_updates_from_user_changed( await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.attributes[ATTR_INPUT_SOURCE_LIST] == [ "HEOS Drive - Line In 1", "Speaker - Line In 1", @@ -317,22 +327,28 @@ async def test_updates_from_user_changed( async def test_updates_from_groups_changed( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test player updates from changes to groups.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # Assert current state - assert hass.states.get("media_player.test_player").attributes[ - ATTR_GROUP_MEMBERS - ] == ["media_player.test_player", "media_player.test_player_2"] - assert hass.states.get("media_player.test_player_2").attributes[ - ATTR_GROUP_MEMBERS - ] == ["media_player.test_player", "media_player.test_player_2"] + state = hass.states.get("media_player.test_player") + assert state is not None + assert state.attributes[ATTR_GROUP_MEMBERS] == [ + "media_player.test_player", + "media_player.test_player_2", + ] + state = hass.states.get("media_player.test_player_2") + assert state is not None + assert state.attributes[ATTR_GROUP_MEMBERS] == [ + "media_player.test_player", + "media_player.test_player_2", + ] # Clear group information - controller._groups = {} + controller.mock_set_groups({}) for player in controller.players.values(): player.group_id = None await controller.dispatcher.wait_send( @@ -341,40 +357,37 @@ async def test_updates_from_groups_changed( await hass.async_block_till_done() # Assert groups changed - assert ( - hass.states.get("media_player.test_player").attributes[ATTR_GROUP_MEMBERS] - is None - ) - assert ( - hass.states.get("media_player.test_player_2").attributes[ATTR_GROUP_MEMBERS] - is None - ) + state = hass.states.get("media_player.test_player") + assert state is not None + assert state.attributes[ATTR_GROUP_MEMBERS] is None + + state = hass.states.get("media_player.test_player_2") + assert state is not None + assert state.attributes[ATTR_GROUP_MEMBERS] is None async def test_clear_playlist( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the clear playlist service.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_CLEAR_PLAYLIST, {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.clear_queue.call_count == 1 + assert controller.player_clear_queue.call_count == 1 async def test_clear_playlist_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test error raised when clear playlist fails.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.clear_queue.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_clear_queue.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to clear playlist: Failure (1)") ): @@ -384,33 +397,31 @@ async def test_clear_playlist_error( {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.clear_queue.call_count == 1 + assert controller.player_clear_queue.call_count == 1 async def test_pause( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the pause service.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.pause.call_count == 1 + assert controller.player_set_play_state.call_count == 1 async def test_pause_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the pause service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.pause.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_set_play_state.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to pause: Failure (1)") ): @@ -420,33 +431,31 @@ async def test_pause_error( {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.pause.call_count == 1 + assert controller.player_set_play_state.call_count == 1 async def test_play( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the play service.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.play.call_count == 1 + assert controller.player_set_play_state.call_count == 1 async def test_play_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the play service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.play.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_set_play_state.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to play: Failure (1)") ): @@ -456,33 +465,31 @@ async def test_play_error( {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.play.call_count == 1 + assert controller.player_set_play_state.call_count == 1 async def test_previous_track( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the previous track service.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.play_previous.call_count == 1 + assert controller.player_play_previous.call_count == 1 async def test_previous_track_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the previous track service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.play_previous.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_play_previous.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to move to previous track: Failure (1)"), @@ -493,33 +500,31 @@ async def test_previous_track_error( {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.play_previous.call_count == 1 + assert controller.player_play_previous.call_count == 1 async def test_next_track( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the next track service.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.play_next.call_count == 1 + assert controller.player_play_next.call_count == 1 async def test_next_track_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the next track service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.play_next.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_play_next.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to move to next track: Failure (1)"), @@ -530,33 +535,31 @@ async def test_next_track_error( {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.play_next.call_count == 1 + assert controller.player_play_next.call_count == 1 async def test_stop( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the stop service.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_STOP, {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.stop.call_count == 1 + assert controller.player_set_play_state.call_count == 1 async def test_stop_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the stop service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.stop.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_set_play_state.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to stop: Failure (1)"), @@ -567,33 +570,31 @@ async def test_stop_error( {ATTR_ENTITY_ID: "media_player.test_player"}, blocking=True, ) - assert player.stop.call_count == 1 + assert controller.player_set_play_state.call_count == 1 async def test_volume_mute( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the volume mute service.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, {ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_MUTED: True}, blocking=True, ) - assert player.set_mute.call_count == 1 + assert controller.player_set_mute.call_count == 1 async def test_volume_mute_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the volume mute service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.set_mute.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_set_mute.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to set mute: Failure (1)"), @@ -604,11 +605,11 @@ async def test_volume_mute_error( {ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_MUTED: True}, blocking=True, ) - assert player.set_mute.call_count == 1 + assert controller.player_set_mute.call_count == 1 async def test_shuffle_set( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the shuffle set service.""" config_entry.add_to_hass(hass) @@ -620,17 +621,17 @@ async def test_shuffle_set( {ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_SHUFFLE: True}, blocking=True, ) - player.set_play_mode.assert_called_once_with(player.repeat, True) + controller.player_set_play_mode.assert_called_once_with(1, player.repeat, True) async def test_shuffle_set_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the shuffle set service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) player = controller.players[1] - player.set_play_mode.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_set_play_mode.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to set shuffle: Failure (1)"), @@ -641,11 +642,11 @@ async def test_shuffle_set_error( {ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_SHUFFLE: True}, blocking=True, ) - player.set_play_mode.assert_called_once_with(player.repeat, True) + controller.player_set_play_mode.assert_called_once_with(1, player.repeat, True) async def test_repeat_set( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the repeat set service.""" config_entry.add_to_hass(hass) @@ -657,17 +658,19 @@ async def test_repeat_set( {ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_REPEAT: RepeatMode.ONE}, blocking=True, ) - player.set_play_mode.assert_called_once_with(RepeatType.ON_ONE, player.shuffle) + controller.player_set_play_mode.assert_called_once_with( + 1, RepeatType.ON_ONE, player.shuffle + ) async def test_repeat_set_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the repeat set service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) player = controller.players[1] - player.set_play_mode.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_set_play_mode.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to set repeat: Failure (1)"), @@ -681,33 +684,33 @@ async def test_repeat_set_error( }, blocking=True, ) - player.set_play_mode.assert_called_once_with(RepeatType.ON_ALL, player.shuffle) + controller.player_set_play_mode.assert_called_once_with( + 1, RepeatType.ON_ALL, player.shuffle + ) async def test_volume_set( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the volume set service.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_LEVEL: 1}, blocking=True, ) - player.set_volume.assert_called_once_with(100) + controller.player_set_volume.assert_called_once_with(1, 100) async def test_volume_set_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the volume set service raises error.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.set_volume.side_effect = CommandFailedError(None, "Failure", 1) + controller.player_set_volume.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to set volume level: Failure (1)"), @@ -718,13 +721,13 @@ async def test_volume_set_error( {ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_LEVEL: 1}, blocking=True, ) - player.set_volume.assert_called_once_with(100) + controller.player_set_volume.assert_called_once_with(1, 100) async def test_select_favorite( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, favorites: dict[int, MediaItem], ) -> None: """Tests selecting a music service favorite and state.""" @@ -739,22 +742,23 @@ async def test_select_favorite( {ATTR_ENTITY_ID: "media_player.test_player", ATTR_INPUT_SOURCE: favorite.name}, blocking=True, ) - player.play_preset_station.assert_called_once_with(1) + controller.play_preset_station.assert_called_once_with(1, 1) # Test state is matched by station name player.now_playing_media.type = HeosMediaType.STATION player.now_playing_media.station = favorite.name - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.attributes[ATTR_INPUT_SOURCE] == favorite.name async def test_select_radio_favorite( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, favorites: dict[int, MediaItem], ) -> None: """Tests selecting a radio favorite and state.""" @@ -769,32 +773,32 @@ async def test_select_radio_favorite( {ATTR_ENTITY_ID: "media_player.test_player", ATTR_INPUT_SOURCE: favorite.name}, blocking=True, ) - player.play_preset_station.assert_called_once_with(2) + controller.play_preset_station.assert_called_once_with(1, 2) # Test state is matched by album id player.now_playing_media.type = HeosMediaType.STATION player.now_playing_media.station = "Classical" player.now_playing_media.album_id = favorite.media_id - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.attributes[ATTR_INPUT_SOURCE] == favorite.name async def test_select_radio_favorite_command_error( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, favorites: dict[int, MediaItem], ) -> None: """Tests command error raises when playing favorite.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] # Test set radio preset favorite = favorites[2] - player.play_preset_station.side_effect = CommandFailedError(None, "Failure", 1) + controller.play_preset_station.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to select source: Failure (1)"), @@ -808,7 +812,7 @@ async def test_select_radio_favorite_command_error( }, blocking=True, ) - player.play_preset_station.assert_called_once_with(2) + controller.play_preset_station.assert_called_once_with(1, 2) @pytest.mark.parametrize( @@ -821,7 +825,7 @@ async def test_select_radio_favorite_command_error( async def test_select_input_source( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, input_sources: list[MediaItem], source_name: str, station: str, @@ -840,21 +844,24 @@ async def test_select_input_source( }, blocking=True, ) - input_sources = next( + input_source = next( input_sources for input_sources in input_sources if input_sources.name == source_name ) - player.play_media.assert_called_once_with(input_sources) + controller.play_media.assert_called_once_with( + 1, input_source, AddCriteriaType.PLAY_NOW + ) # Update the now_playing_media to reflect play_media player.now_playing_media.source_id = const.MUSIC_SOURCE_AUX_INPUT player.now_playing_media.station = station player.now_playing_media.media_id = const.INPUT_AUX_IN_1 - await player.heos.dispatcher.wait_send( + await controller.dispatcher.wait_send( SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED ) await hass.async_block_till_done() state = hass.states.get("media_player.test_player") + assert state is not None assert state.attributes[ATTR_INPUT_SOURCE] == source_name @@ -879,15 +886,14 @@ async def test_select_input_unknown_raises( async def test_select_input_command_error( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, input_sources: list[MediaItem], ) -> None: """Tests selecting an unknown input.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] input_source = input_sources[0] - player.play_media.side_effect = CommandFailedError(None, "Failure", 1) + controller.play_media.side_effect = CommandFailedError("", "Failure", 1) with pytest.raises( HomeAssistantError, match=re.escape("Unable to select source: Failure (1)"), @@ -901,7 +907,9 @@ async def test_select_input_command_error( }, blocking=True, ) - player.play_media.assert_called_once_with(input_source) + controller.play_media.assert_called_once_with( + 1, input_source, AddCriteriaType.PLAY_NOW + ) async def test_unload_config_entry( @@ -911,20 +919,21 @@ async def test_unload_config_entry( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get("media_player.test_player").state == STATE_UNAVAILABLE + state = hass.states.get("media_player.test_player") + assert state is not None + assert state.state == STATE_UNAVAILABLE @pytest.mark.parametrize("media_type", [MediaType.URL, MediaType.MUSIC]) async def test_play_media( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, media_type: MediaType, ) -> None: """Test the play media service with type url.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] url = "http://news/podcast.mp3" await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -936,21 +945,20 @@ async def test_play_media( }, blocking=True, ) - player.play_url.assert_called_once_with(url) + controller.play_url.assert_called_once_with(1, url) @pytest.mark.parametrize("media_type", [MediaType.URL, MediaType.MUSIC]) async def test_play_media_error( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, media_type: MediaType, ) -> None: """Test the play media service with type url error raises.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] - player.play_url.side_effect = CommandFailedError(None, "Failure", 1) + controller.play_url.side_effect = CommandFailedError("", "Failure", 1) url = "http://news/podcast.mp3" with pytest.raises( HomeAssistantError, @@ -966,7 +974,7 @@ async def test_play_media_error( }, blocking=True, ) - player.play_url.assert_called_once_with(url) + controller.play_url.assert_called_once_with(1, url) @pytest.mark.parametrize( @@ -975,14 +983,13 @@ async def test_play_media_error( async def test_play_media_quick_select( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, content_id: str, expected_index: int, ) -> None: """Test the play media service with type quick_select.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -993,16 +1000,15 @@ async def test_play_media_quick_select( }, blocking=True, ) - player.play_quick_select.assert_called_once_with(expected_index) + controller.player_play_quick_select.assert_called_once_with(1, expected_index) async def test_play_media_quick_select_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the play media service with invalid quick_select raises.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] with pytest.raises( HomeAssistantError, match=re.escape("Unable to play media: Invalid quick select 'Invalid'"), @@ -1017,7 +1023,7 @@ async def test_play_media_quick_select_error( }, blocking=True, ) - assert player.play_quick_select.call_count == 0 + assert controller.player_play_quick_select.call_count == 0 @pytest.mark.parametrize( @@ -1031,7 +1037,7 @@ async def test_play_media_quick_select_error( async def test_play_media_playlist( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, playlists: list[MediaItem], enqueue: Any, criteria: AddCriteriaType, @@ -1039,7 +1045,6 @@ async def test_play_media_playlist( """Test the play media service with type playlist.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] playlist = playlists[0] service_data = { ATTR_ENTITY_ID: "media_player.test_player", @@ -1054,16 +1059,15 @@ async def test_play_media_playlist( service_data, blocking=True, ) - player.play_media.assert_called_once_with(playlist, criteria) + controller.play_media.assert_called_once_with(1, playlist, criteria) async def test_play_media_playlist_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the play media service with an invalid playlist name.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] with pytest.raises( HomeAssistantError, match=re.escape("Unable to play media: Invalid playlist 'Invalid'"), @@ -1078,7 +1082,7 @@ async def test_play_media_playlist_error( }, blocking=True, ) - assert player.add_to_queue.call_count == 0 + assert controller.add_to_queue.call_count == 0 @pytest.mark.parametrize( @@ -1087,14 +1091,13 @@ async def test_play_media_playlist_error( async def test_play_media_favorite( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, content_id: str, expected_index: int, ) -> None: """Test the play media service with type favorite.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -1105,16 +1108,15 @@ async def test_play_media_favorite( }, blocking=True, ) - player.play_preset_station.assert_called_once_with(expected_index) + controller.play_preset_station.assert_called_once_with(1, expected_index) async def test_play_media_favorite_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the play media service with an invalid favorite raises.""" config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - player = controller.players[1] with pytest.raises( HomeAssistantError, match=re.escape("Unable to play media: Invalid favorite 'Invalid'"), @@ -1129,7 +1131,7 @@ async def test_play_media_favorite_error( }, blocking=True, ) - assert player.play_preset_station.call_count == 0 + assert controller.play_preset_station.call_count == 0 async def test_play_media_invalid_type( @@ -1165,7 +1167,7 @@ async def test_play_media_invalid_type( async def test_media_player_join_group( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, members: list[str], expected: tuple[int, list[int]], ) -> None: @@ -1185,7 +1187,7 @@ async def test_media_player_join_group( async def test_media_player_join_group_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test grouping of media players through the join service raises error.""" config_entry.add_to_hass(hass) @@ -1209,13 +1211,14 @@ async def test_media_player_join_group_error( async def test_media_player_group_members( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, caplog: pytest.LogCaptureFixture, ) -> None: """Test group_members attribute.""" config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) player_entity = hass.states.get("media_player.test_player") + assert player_entity is not None assert player_entity.attributes[ATTR_GROUP_MEMBERS] == [ "media_player.test_player", "media_player.test_player_2", @@ -1227,16 +1230,17 @@ async def test_media_player_group_members( async def test_media_player_group_members_error( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, caplog: pytest.LogCaptureFixture, ) -> None: """Test error in HEOS API.""" + controller.mock_set_groups({}) controller.get_groups.side_effect = HeosError("error") - controller._groups = {} config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) assert "Unable to retrieve groups" in caplog.text player_entity = hass.states.get("media_player.test_player") + assert player_entity is not None assert player_entity.attributes[ATTR_GROUP_MEMBERS] is None @@ -1247,7 +1251,7 @@ async def test_media_player_group_members_error( async def test_media_player_unjoin_group( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, entity_id: str, expected_args: list[int], ) -> None: @@ -1266,7 +1270,7 @@ async def test_media_player_unjoin_group( async def test_media_player_unjoin_group_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test ungrouping of media players through the unjoin service error raises.""" config_entry.add_to_hass(hass) @@ -1289,7 +1293,7 @@ async def test_media_player_unjoin_group_error( async def test_media_player_group_fails_when_entity_removed( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, entity_registry: er.EntityRegistry, ) -> None: """Test grouping fails when entity removed.""" @@ -1316,7 +1320,7 @@ async def test_media_player_group_fails_when_entity_removed( async def test_media_player_group_fails_wrong_integration( hass: HomeAssistant, config_entry: MockConfigEntry, - controller: Heos, + controller: MockHeos, entity_registry: er.EntityRegistry, ) -> None: """Test grouping fails when trying to join from the wrong integration.""" diff --git a/tests/components/heos/test_services.py b/tests/components/heos/test_services.py index 8eda26d2b3d..151571ceb50 100644 --- a/tests/components/heos/test_services.py +++ b/tests/components/heos/test_services.py @@ -1,6 +1,6 @@ """Tests for the services module.""" -from pyheos import CommandAuthenticationError, Heos, HeosError +from pyheos import CommandAuthenticationError, HeosError import pytest from homeassistant.components.heos.const import ( @@ -13,11 +13,13 @@ from homeassistant.components.heos.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from . import MockHeos + from tests.common import MockConfigEntry async def test_sign_in( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the sign-in service.""" config_entry.add_to_hass(hass) @@ -34,7 +36,7 @@ async def test_sign_in( async def test_sign_in_failed( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test sign-in service logs error when not connected.""" config_entry.add_to_hass(hass) @@ -56,7 +58,7 @@ async def test_sign_in_failed( async def test_sign_in_unknown_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test sign-in service logs error for failure.""" config_entry.add_to_hass(hass) @@ -93,7 +95,7 @@ async def test_sign_in_not_loaded_raises( async def test_sign_out( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the sign-out service.""" config_entry.add_to_hass(hass) @@ -117,7 +119,7 @@ async def test_sign_out_not_loaded_raises( async def test_sign_out_unknown_error( - hass: HomeAssistant, config_entry: MockConfigEntry, controller: Heos + hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: """Test the sign-out service.""" config_entry.add_to_hass(hass)