Add support for a list of known hosts to Google Cast (#47232)

This commit is contained in:
Erik Montnemery 2021-03-02 00:18:18 +01:00 committed by GitHub
parent dd9e926689
commit 96cc17b462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 417 additions and 102 deletions

View File

@ -1,35 +1,136 @@
"""Config flow for Cast.""" """Config flow for Cast."""
import functools import voluptuous as vol
from pychromecast.discovery import discover_chromecasts, stop_discovery
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_entry_flow
from .const import DOMAIN from .const import CONF_KNOWN_HOSTS, DOMAIN
from .helpers import ChromeCastZeroconf
KNOWN_HOSTS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
async def _async_has_devices(hass): class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
""" """Handle a config flow."""
Return if there are devices that can be discovered.
This function will be called if no devices are already found through the zeroconf VERSION = 1
integration. CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
"""
zeroconf_instance = ChromeCastZeroconf.get_zeroconf() def __init__(self):
if zeroconf_instance is None: """Initialize flow."""
zeroconf_instance = await zeroconf.async_get_instance(hass) self._known_hosts = None
casts, browser = await hass.async_add_executor_job( @staticmethod
functools.partial(discover_chromecasts, zeroconf_instance=zeroconf_instance) def async_get_options_flow(config_entry):
) """Get the options flow for this handler."""
stop_discovery(browser) return CastOptionsFlowHandler(config_entry)
return casts
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( class CastOptionsFlowHandler(config_entries.OptionsFlow):
DOMAIN, "Google Cast", _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH """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,
)

View File

@ -13,6 +13,8 @@ KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts"
ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices" ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices"
# Stores an audio group manager. # Stores an audio group manager.
CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_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 # Dispatcher signal fired with a ChromecastInfo every time we discover a new
# Chromecast or receive it through configuration # 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. # Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view.
SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view" SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view"
CONF_KNOWN_HOSTS = "known_hosts"

View File

@ -9,6 +9,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from .const import ( from .const import (
CAST_BROWSER_KEY,
CONF_KNOWN_HOSTS,
DEFAULT_PORT, DEFAULT_PORT,
INTERNAL_DISCOVERY_RUNNING_KEY, INTERNAL_DISCOVERY_RUNNING_KEY,
KNOWN_CHROMECAST_INFO_KEY, KNOWN_CHROMECAST_INFO_KEY,
@ -52,7 +54,7 @@ def _remove_chromecast(hass: HomeAssistant, info: ChromecastInfo):
dispatcher_send(hass, SIGNAL_CAST_REMOVED, info) 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.""" """Set up the pychromecast internal discovery."""
if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data:
hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() 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") _LOGGER.debug("Starting internal pychromecast discovery")
browser = pychromecast.discovery.CastBrowser( 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() browser.start_discovery()
def stop_discovery(event): def stop_discovery(event):
@ -97,3 +102,11 @@ def setup_internal_discovery(hass: HomeAssistant) -> None:
hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) 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))

View File

@ -3,7 +3,7 @@
"name": "Google Cast", "name": "Google Cast",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast", "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"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"],
"zeroconf": ["_googlecast._tcp.local."], "zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"] "codeowners": ["@emontnemery"]

View File

@ -134,7 +134,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
# no pending task # no pending task
done, _ = await asyncio.wait( 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 for cfg in config
] ]
) )
@ -146,7 +148,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_platform( 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.""" """Set up the cast platform."""
# Import CEC IGNORE attributes # Import CEC IGNORE attributes
@ -177,7 +179,7 @@ async def _async_setup_platform(
async_cast_discovered(chromecast) async_cast_discovered(chromecast)
ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass)) 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): class CastDevice(MediaPlayerEntity):

View File

@ -3,11 +3,33 @@
"step": { "step": {
"confirm": { "confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]" "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": { "abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" },
"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."
} }
} }
} }

View File

@ -1,13 +1,35 @@
{ {
"config": { "config": {
"abort": { "abort": {
"no_devices_found": "No devices found on the network",
"single_instance_allowed": "Already configured. Only a single configuration possible." "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": { "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": { "confirm": {
"description": "Do you want to start set up?" "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."
}
}
} }
} }

View File

@ -1302,7 +1302,7 @@ pycfdns==1.2.1
pychannels==1.0.0 pychannels==1.0.0
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==9.0.0 pychromecast==9.1.1
# homeassistant.components.pocketcasts # homeassistant.components.pocketcasts
pycketcasts==1.0.0 pycketcasts==1.0.0

View File

@ -685,7 +685,7 @@ pybotvac==0.0.20
pycfdns==1.2.1 pycfdns==1.2.1
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==9.0.0 pychromecast==9.1.1
# homeassistant.components.climacell # homeassistant.components.climacell
pyclimacell==0.14.0 pyclimacell==0.14.0

View File

@ -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

View File

@ -1,11 +1,14 @@
"""Tests for the Cast config flow.""" """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 import config_entries, data_entry_flow
from homeassistant.components import cast from homeassistant.components import cast
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def test_creating_entry_sets_up_media_player(hass): async def test_creating_entry_sets_up_media_player(hass):
"""Test setting up Cast loads the media player.""" """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() await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 0 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"]
)

View File

@ -2,7 +2,7 @@
# pylint: disable=protected-access # pylint: disable=protected-access
import json import json
from typing import Optional 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 from uuid import UUID
import attr import attr
@ -35,70 +35,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, assert_setup_component from tests.common import MockConfigEntry, assert_setup_component
from tests.components.media_player import common 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 # pylint: disable=invalid-name
FakeUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e2") FakeUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e2")
FakeUUID2 = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e4") FakeUUID2 = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e4")
@ -482,7 +418,8 @@ async def test_replay_past_chromecasts(hass):
assert add_dev1.call_count == 1 assert add_dev1.call_count == 1
add_dev2 = Mock() 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() await hass.async_block_till_done()
assert add_dev2.call_count == 1 assert add_dev2.call_count == 1