From 96cc17b462c107117382a30bbc30ede246e723ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Mar 2021 00:18:18 +0100 Subject: [PATCH] Add support for a list of known hosts to Google Cast (#47232) --- homeassistant/components/cast/config_flow.py | 149 +++++++++++++++--- homeassistant/components/cast/const.py | 4 + homeassistant/components/cast/discovery.py | 17 +- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 8 +- homeassistant/components/cast/strings.json | 26 ++- .../components/cast/translations/en.json | 24 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/conftest.py | 76 +++++++++ tests/components/cast/test_init.py | 140 +++++++++++++++- tests/components/cast/test_media_player.py | 69 +------- 12 files changed, 417 insertions(+), 102 deletions(-) create mode 100644 tests/components/cast/conftest.py diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index e00048a7589..4a4426a5db1 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -1,35 +1,136 @@ """Config flow for Cast.""" -import functools - -from pychromecast.discovery import discover_chromecasts, stop_discovery +import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import zeroconf -from homeassistant.helpers import config_entry_flow +from homeassistant.helpers import config_validation as cv -from .const import DOMAIN -from .helpers import ChromeCastZeroconf +from .const import CONF_KNOWN_HOSTS, DOMAIN + +KNOWN_HOSTS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string])) -async def _async_has_devices(hass): - """ - Return if there are devices that can be discovered. +class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" - This function will be called if no devices are already found through the zeroconf - integration. - """ + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH - zeroconf_instance = ChromeCastZeroconf.get_zeroconf() - if zeroconf_instance is None: - zeroconf_instance = await zeroconf.async_get_instance(hass) + def __init__(self): + """Initialize flow.""" + self._known_hosts = None - casts, browser = await hass.async_add_executor_job( - functools.partial(discover_chromecasts, zeroconf_instance=zeroconf_instance) - ) - stop_discovery(browser) - return casts + @staticmethod + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return CastOptionsFlowHandler(config_entry) + + async def async_step_import(self, import_data=None): + """Import data.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + data = {CONF_KNOWN_HOSTS: self._known_hosts} + return self.async_create_entry(title="Google Cast", data=data) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_config() + + async def async_step_zeroconf(self, discovery_info): + """Handle a flow initialized by zeroconf discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(DOMAIN) + + return await self.async_step_confirm() + + async def async_step_config(self, user_input=None): + """Confirm the setup.""" + errors = {} + data = {CONF_KNOWN_HOSTS: self._known_hosts} + + if user_input is not None: + bad_hosts = False + known_hosts = user_input[CONF_KNOWN_HOSTS] + known_hosts = [x.strip() for x in known_hosts.split(",") if x.strip()] + try: + known_hosts = KNOWN_HOSTS_SCHEMA(known_hosts) + except vol.Invalid: + errors["base"] = "invalid_known_hosts" + bad_hosts = True + else: + data[CONF_KNOWN_HOSTS] = known_hosts + if not bad_hosts: + return self.async_create_entry(title="Google Cast", data=data) + + fields = {} + fields[vol.Optional(CONF_KNOWN_HOSTS, default="")] = str + + return self.async_show_form( + step_id="config", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_confirm(self, user_input=None): + """Confirm the setup.""" + + data = {CONF_KNOWN_HOSTS: self._known_hosts} + + if user_input is not None: + return self.async_create_entry(title="Google Cast", data=data) + + return self.async_show_form(step_id="confirm") -config_entry_flow.register_discovery_flow( - DOMAIN, "Google Cast", _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH -) +class CastOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Google Cast options.""" + + def __init__(self, config_entry): + """Initialize MQTT options flow.""" + self.config_entry = config_entry + self.broker_config = {} + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the Cast options.""" + return await self.async_step_options() + + async def async_step_options(self, user_input=None): + """Manage the MQTT options.""" + errors = {} + current_config = self.config_entry.data + if user_input is not None: + bad_hosts = False + + known_hosts = user_input.get(CONF_KNOWN_HOSTS, "") + known_hosts = [x.strip() for x in known_hosts.split(",") if x.strip()] + try: + known_hosts = KNOWN_HOSTS_SCHEMA(known_hosts) + except vol.Invalid: + errors["base"] = "invalid_known_hosts" + bad_hosts = True + if not bad_hosts: + updated_config = {} + updated_config[CONF_KNOWN_HOSTS] = known_hosts + self.hass.config_entries.async_update_entry( + self.config_entry, data=updated_config + ) + return self.async_create_entry(title="", data=None) + + fields = {} + known_hosts_string = "" + if current_config.get(CONF_KNOWN_HOSTS): + known_hosts_string = ",".join(current_config.get(CONF_KNOWN_HOSTS)) + fields[ + vol.Optional( + "known_hosts", description={"suggested_value": known_hosts_string} + ) + ] = str + + return self.async_show_form( + step_id="options", + data_schema=vol.Schema(fields), + errors=errors, + ) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index c6164484dbb..993315b5518 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -13,6 +13,8 @@ KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts" ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices" # Stores an audio group manager. CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager" +# Store a CastBrowser +CAST_BROWSER_KEY = "cast_browser" # Dispatcher signal fired with a ChromecastInfo every time we discover a new # Chromecast or receive it through configuration @@ -24,3 +26,5 @@ SIGNAL_CAST_REMOVED = "cast_removed" # Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view. SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view" + +CONF_KNOWN_HOSTS = "known_hosts" diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 81048b35a97..fcae28b5bfe 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -9,6 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + CAST_BROWSER_KEY, + CONF_KNOWN_HOSTS, DEFAULT_PORT, INTERNAL_DISCOVERY_RUNNING_KEY, KNOWN_CHROMECAST_INFO_KEY, @@ -52,7 +54,7 @@ def _remove_chromecast(hass: HomeAssistant, info: ChromecastInfo): dispatcher_send(hass, SIGNAL_CAST_REMOVED, info) -def setup_internal_discovery(hass: HomeAssistant) -> None: +def setup_internal_discovery(hass: HomeAssistant, config_entry) -> None: """Set up the pychromecast internal discovery.""" if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() @@ -86,8 +88,11 @@ def setup_internal_discovery(hass: HomeAssistant) -> None: _LOGGER.debug("Starting internal pychromecast discovery") browser = pychromecast.discovery.CastBrowser( - CastListener(), ChromeCastZeroconf.get_zeroconf() + CastListener(), + ChromeCastZeroconf.get_zeroconf(), + config_entry.data.get(CONF_KNOWN_HOSTS), ) + hass.data[CAST_BROWSER_KEY] = browser browser.start_discovery() def stop_discovery(event): @@ -97,3 +102,11 @@ def setup_internal_discovery(hass: HomeAssistant) -> None: hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) + + config_entry.add_update_listener(config_entry_updated) + + +async def config_entry_updated(hass, config_entry): + """Handle config entry being updated.""" + browser = hass.data[CAST_BROWSER_KEY] + browser.host_browser.update_hosts(config_entry.data.get(CONF_KNOWN_HOSTS)) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index ac728b4ec45..0c9d0dfc4a5 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.0.0"], + "requirements": ["pychromecast==9.1.1"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 235c7ab4479..fbb08437830 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -134,7 +134,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # no pending task done, _ = await asyncio.wait( [ - _async_setup_platform(hass, ENTITY_SCHEMA(cfg), async_add_entities) + _async_setup_platform( + hass, ENTITY_SCHEMA(cfg), async_add_entities, config_entry + ) for cfg in config ] ) @@ -146,7 +148,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities + hass: HomeAssistantType, config: ConfigType, async_add_entities, config_entry ): """Set up the cast platform.""" # Import CEC IGNORE attributes @@ -177,7 +179,7 @@ async def _async_setup_platform( async_cast_discovered(chromecast) ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass)) - hass.async_add_executor_job(setup_internal_discovery, hass) + hass.async_add_executor_job(setup_internal_discovery, hass, config_entry) class CastDevice(MediaPlayerEntity): diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json index ad8f0f41ae7..7cd07518db8 100644 --- a/homeassistant/components/cast/strings.json +++ b/homeassistant/components/cast/strings.json @@ -3,11 +3,33 @@ "step": { "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" + }, + "config": { + "title": "Google Cast", + "description": "Please enter the Google Cast configuration.", + "data": { + "known_hosts": "Optional list of known hosts if mDNS discovery is not working." + } } }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "error": { + "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." + } + }, + "options": { + "step": { + "options": { + "description": "Please enter the Google Cast configuration.", + "data": { + "known_hosts": "Optional list of known hosts if mDNS discovery is not working." + } + } + }, + "error": { + "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." } } } diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json index f05becffed3..9ef2f10cda2 100644 --- a/homeassistant/components/cast/translations/en.json +++ b/homeassistant/components/cast/translations/en.json @@ -1,13 +1,35 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, + "error": { + "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." + }, "step": { + "config": { + "data": { + "known_hosts": "Optional list of known hosts if mDNS discovery is not working." + }, + "description": "Please enter the Google Cast configuration.", + "title": "Google Cast" + }, "confirm": { "description": "Do you want to start set up?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." + }, + "step": { + "options": { + "data": { + "known_hosts": "Optional list of known hosts if mDNS discovery is not working." + }, + "description": "Please enter the Google Cast configuration." + } + } } } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 5bd96e37683..761c47e34da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1302,7 +1302,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.0.0 +pychromecast==9.1.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 510bb91b265..9f84817843c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -685,7 +685,7 @@ pybotvac==0.0.20 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==9.0.0 +pychromecast==9.1.1 # homeassistant.components.climacell pyclimacell==0.14.0 diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py new file mode 100644 index 00000000000..875d831afa9 --- /dev/null +++ b/tests/components/cast/conftest.py @@ -0,0 +1,76 @@ +"""Test fixtures for the cast integration.""" +# pylint: disable=protected-access +from unittest.mock import AsyncMock, MagicMock, patch + +import pychromecast +import pytest + + +@pytest.fixture() +def dial_mock(): + """Mock pychromecast dial.""" + dial_mock = MagicMock() + dial_mock.get_device_status.return_value.uuid = "fake_uuid" + dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer" + dial_mock.get_device_status.return_value.model_name = "fake_model_name" + dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name" + dial_mock.get_multizone_status.return_value.dynamic_groups = [] + return dial_mock + + +@pytest.fixture() +def castbrowser_mock(): + """Mock pychromecast CastBrowser.""" + return MagicMock() + + +@pytest.fixture() +def castbrowser_constructor_mock(): + """Mock pychromecast CastBrowser constructor.""" + return MagicMock() + + +@pytest.fixture() +def mz_mock(): + """Mock pychromecast MultizoneManager.""" + return MagicMock() + + +@pytest.fixture() +def pycast_mock(castbrowser_mock, castbrowser_constructor_mock): + """Mock pychromecast.""" + pycast_mock = MagicMock() + pycast_mock.discovery.CastBrowser = castbrowser_constructor_mock + pycast_mock.discovery.CastBrowser.return_value = castbrowser_mock + pycast_mock.discovery.AbstractCastListener = ( + pychromecast.discovery.AbstractCastListener + ) + return pycast_mock + + +@pytest.fixture() +def quick_play_mock(): + """Mock pychromecast quick_play.""" + return MagicMock() + + +@pytest.fixture(autouse=True) +def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock): + """Mock pychromecast.""" + with patch( + "homeassistant.components.cast.media_player.pychromecast", pycast_mock + ), patch( + "homeassistant.components.cast.discovery.pychromecast", pycast_mock + ), patch( + "homeassistant.components.cast.helpers.dial", dial_mock + ), patch( + "homeassistant.components.cast.media_player.MultizoneManager", + return_value=mz_mock, + ), patch( + "homeassistant.components.cast.media_player.zeroconf.async_get_instance", + AsyncMock(), + ), patch( + "homeassistant.components.cast.media_player.quick_play", + quick_play_mock, + ): + yield diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index d364256b703..77268e7de97 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,11 +1,14 @@ """Tests for the Cast config flow.""" +from unittest.mock import ANY, patch -from unittest.mock import patch +import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + async def test_creating_entry_sets_up_media_player(hass): """Test setting up Cast loads the media player.""" @@ -54,3 +57,138 @@ async def test_not_configuring_cast_not_creates_entry(hass): await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 0 + + +@pytest.mark.parametrize("source", ["import", "user", "zeroconf"]) +async def test_single_instance(hass, source): + """Test we only allow a single config flow.""" + MockConfigEntry(domain="cast").add_to_hass(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + "cast", context={"source": source} + ) + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" + + +async def test_user_setup(hass, mqtt_mock): + """Test we can finish a config flow.""" + result = await hass.config_entries.flow.async_init( + "cast", context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + users = await hass.auth.async_get_users() + assert len(users) == 1 + assert result["type"] == "create_entry" + assert result["result"].data == { + "known_hosts": [], + "user_id": users[0].id, # Home Assistant cast user + } + + +async def test_user_setup_options(hass, mqtt_mock): + """Test we can finish a config flow.""" + result = await hass.config_entries.flow.async_init( + "cast", context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "} + ) + + users = await hass.auth.async_get_users() + assert len(users) == 1 + assert result["type"] == "create_entry" + assert result["result"].data == { + "known_hosts": ["192.168.0.1", "192.168.0.2"], + "user_id": users[0].id, # Home Assistant cast user + } + + +async def test_zeroconf_setup(hass): + """Test we can finish a config flow through zeroconf.""" + result = await hass.config_entries.flow.async_init( + "cast", context={"source": "zeroconf"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + users = await hass.auth.async_get_users() + assert len(users) == 1 + assert result["type"] == "create_entry" + assert result["result"].data == { + "known_hosts": None, + "user_id": users[0].id, # Home Assistant cast user + } + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + + +async def test_option_flow(hass): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain="cast", data={"known_hosts": ["192.168.0.10", "192.168.0.11"]} + ) + config_entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + data_schema = result["data_schema"].schema + assert get_suggested(data_schema, "known_hosts") == "192.168.0.10,192.168.0.11" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"known_hosts": "192.168.0.1, , 192.168.0.2 "}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] is None + assert config_entry.data == {"known_hosts": ["192.168.0.1", "192.168.0.2"]} + + +async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): + """Test known hosts is passed to pychromecasts.""" + result = await hass.config_entries.flow.async_init( + "cast", context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + config_entry = hass.config_entries.async_entries("cast")[0] + + assert castbrowser_mock.start_discovery.call_count == 1 + castbrowser_constructor_mock.assert_called_once_with( + ANY, ANY, ["192.168.0.1", "192.168.0.2"] + ) + castbrowser_mock.reset_mock() + castbrowser_constructor_mock.reset_mock() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"known_hosts": "192.168.0.11, 192.168.0.12"}, + ) + + await hass.async_block_till_done() + + castbrowser_mock.start_discovery.assert_not_called() + castbrowser_constructor_mock.assert_not_called() + castbrowser_mock.host_browser.update_hosts.assert_called_once_with( + ["192.168.0.11", "192.168.0.12"] + ) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 51c49484c50..27f817b6771 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import json from typing import Optional -from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch +from unittest.mock import ANY, MagicMock, Mock, patch from uuid import UUID import attr @@ -35,70 +35,6 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, assert_setup_component from tests.components.media_player import common - -@pytest.fixture() -def dial_mock(): - """Mock pychromecast dial.""" - dial_mock = MagicMock() - dial_mock.get_device_status.return_value.uuid = "fake_uuid" - dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer" - dial_mock.get_device_status.return_value.model_name = "fake_model_name" - dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name" - dial_mock.get_multizone_status.return_value.dynamic_groups = [] - return dial_mock - - -@pytest.fixture() -def castbrowser_mock(): - """Mock pychromecast CastBrowser.""" - return MagicMock() - - -@pytest.fixture() -def mz_mock(): - """Mock pychromecast MultizoneManager.""" - return MagicMock() - - -@pytest.fixture() -def pycast_mock(castbrowser_mock): - """Mock pychromecast.""" - pycast_mock = MagicMock() - pycast_mock.discovery.CastBrowser.return_value = castbrowser_mock - pycast_mock.discovery.AbstractCastListener = ( - pychromecast.discovery.AbstractCastListener - ) - return pycast_mock - - -@pytest.fixture() -def quick_play_mock(): - """Mock pychromecast quick_play.""" - return MagicMock() - - -@pytest.fixture(autouse=True) -def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock): - """Mock pychromecast.""" - with patch( - "homeassistant.components.cast.media_player.pychromecast", pycast_mock - ), patch( - "homeassistant.components.cast.discovery.pychromecast", pycast_mock - ), patch( - "homeassistant.components.cast.helpers.dial", dial_mock - ), patch( - "homeassistant.components.cast.media_player.MultizoneManager", - return_value=mz_mock, - ), patch( - "homeassistant.components.cast.media_player.zeroconf.async_get_instance", - AsyncMock(), - ), patch( - "homeassistant.components.cast.media_player.quick_play", - quick_play_mock, - ): - yield - - # pylint: disable=invalid-name FakeUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e2") FakeUUID2 = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e4") @@ -482,7 +418,8 @@ async def test_replay_past_chromecasts(hass): assert add_dev1.call_count == 1 add_dev2 = Mock() - await cast._async_setup_platform(hass, {"host": "host2"}, add_dev2) + entry = hass.config_entries.async_entries("cast")[0] + await cast._async_setup_platform(hass, {"host": "host2"}, add_dev2, entry) await hass.async_block_till_done() assert add_dev2.call_count == 1