Support dynamic Google Cast groups (#44484)

* Re-add support for dynamic groups

* Add tests

* Add support for manufacturer

* Refactor support for dynamic groups

* Bump pychromecast to 7.7.0

* Bump pychromecast to 7.7.1

* Tweak tests

* Apply review suggestion
This commit is contained in:
Erik Montnemery 2021-01-06 09:23:18 +01:00 committed by GitHub
parent f18880686c
commit 02bfc68842
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 489 additions and 59 deletions

View file

@ -25,6 +25,7 @@ def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo):
_LOGGER.error("Discovered chromecast without uuid %s", info)
return
info = info.fill_out_missing_chromecast_info()
if info.uuid in hass.data[KNOWN_CHROMECAST_INFO_KEY]:
_LOGGER.debug("Discovered update for known chromecast %s", info)
else:

View file

@ -1,7 +1,8 @@
"""Helpers to deal with Cast devices."""
from typing import Optional, Tuple
from typing import Optional
import attr
from pychromecast import dial
from pychromecast.const import CAST_MANUFACTURERS
from .const import DEFAULT_PORT
@ -20,8 +21,10 @@ class ChromecastInfo:
uuid: Optional[str] = attr.ib(
converter=attr.converters.optional(str), default=None
) # always convert UUID to string if not None
_manufacturer = attr.ib(type=Optional[str], default=None)
model_name: str = attr.ib(default="")
friendly_name: Optional[str] = attr.ib(default=None)
is_dynamic_group = attr.ib(type=Optional[bool], default=None)
@property
def is_audio_group(self) -> bool:
@ -29,17 +32,84 @@ class ChromecastInfo:
return self.port != DEFAULT_PORT
@property
def host_port(self) -> Tuple[str, int]:
"""Return the host+port tuple."""
return self.host, self.port
def is_information_complete(self) -> bool:
"""Return if all information is filled out."""
want_dynamic_group = self.is_audio_group
have_dynamic_group = self.is_dynamic_group is not None
have_all_except_dynamic_group = all(
attr.astuple(
self,
filter=attr.filters.exclude(
attr.fields(ChromecastInfo).is_dynamic_group
),
)
)
return have_all_except_dynamic_group and (
not want_dynamic_group or have_dynamic_group
)
@property
def manufacturer(self) -> str:
"""Return the manufacturer."""
if self._manufacturer:
return self._manufacturer
if not self.model_name:
return None
return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.")
def fill_out_missing_chromecast_info(self) -> "ChromecastInfo":
"""Return a new ChromecastInfo object with missing attributes filled in.
Uses blocking HTTP / HTTPS.
"""
if self.is_information_complete:
# We have all information, no need to check HTTP API.
return self
# Fill out missing group information via HTTP API.
if self.is_audio_group:
is_dynamic_group = False
http_group_status = None
if self.uuid:
http_group_status = dial.get_multizone_status(
self.host,
services=self.services,
zconf=ChromeCastZeroconf.get_zeroconf(),
)
if http_group_status is not None:
is_dynamic_group = any(
str(g.uuid) == self.uuid
for g in http_group_status.dynamic_groups
)
return ChromecastInfo(
services=self.services,
host=self.host,
port=self.port,
uuid=self.uuid,
friendly_name=self.friendly_name,
model_name=self.model_name,
is_dynamic_group=is_dynamic_group,
)
# Fill out some missing information (friendly_name, uuid) via HTTP dial.
http_device_status = dial.get_device_status(
self.host, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf()
)
if http_device_status is None:
# HTTP dial didn't give us any new information.
return self
return ChromecastInfo(
services=self.services,
host=self.host,
port=self.port,
uuid=(self.uuid or http_device_status.uuid),
friendly_name=(self.friendly_name or http_device_status.friendly_name),
manufacturer=(self.manufacturer or http_device_status.manufacturer),
model_name=(self.model_name or http_device_status.model_name),
)
class ChromeCastZeroconf:
"""Class to hold a zeroconf instance."""
@ -65,19 +135,22 @@ class CastStatusListener:
potentially arrive. This class allows invalidating past chromecast objects.
"""
def __init__(self, cast_device, chromecast, mz_mgr):
def __init__(self, cast_device, chromecast, mz_mgr, mz_only=False):
"""Initialize the status listener."""
self._cast_device = cast_device
self._uuid = chromecast.uuid
self._valid = True
self._mz_mgr = mz_mgr
if cast_device._cast_info.is_audio_group:
self._mz_mgr.add_multizone(chromecast)
if mz_only:
return
chromecast.register_status_listener(self)
chromecast.socket_client.media_controller.register_status_listener(self)
chromecast.register_connection_listener(self)
if cast_device._cast_info.is_audio_group:
self._mz_mgr.add_multizone(chromecast)
else:
if not cast_device._cast_info.is_audio_group:
self._mz_mgr.register_listener(chromecast.uuid, self)
def new_cast_status(self, cast_status):

View file

@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==7.6.0"],
"requirements": ["pychromecast==7.7.1"],
"after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"]

View file

@ -63,6 +63,7 @@ from .const import (
DOMAIN as CAST_DOMAIN,
KNOWN_CHROMECAST_INFO_KEY,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
)
from .discovery import setup_internal_discovery
@ -115,6 +116,13 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo):
return None
# -> New cast device
added_casts.add(info.uuid)
if info.is_dynamic_group:
# This is a dynamic group, do not add it but connect to the service.
group = DynamicCastGroup(hass, info)
group.async_setup()
return None
return CastDevice(info)
@ -206,8 +214,9 @@ class CastDevice(MediaPlayerEntity):
self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered
)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop)
self.async_set_cast_info(self._cast_info)
self.hass.async_create_task(
async_create_catching_coro(self.async_set_cast_info(self._cast_info))
async_create_catching_coro(self.async_connect_to_chromecast())
)
self._cast_view_remove_handler = async_dispatcher_connect(
@ -228,15 +237,13 @@ class CastDevice(MediaPlayerEntity):
self._cast_view_remove_handler()
self._cast_view_remove_handler = None
async def async_set_cast_info(self, cast_info):
"""Set the cast information and set up the chromecast object."""
def async_set_cast_info(self, cast_info):
"""Set the cast information."""
self._cast_info = cast_info
if self._chromecast is not None:
# Only setup the chromecast once, added elements to services
# will automatically be picked up.
return
async def async_connect_to_chromecast(self):
"""Set up the chromecast object."""
_LOGGER.debug(
"[%s %s] Connecting to cast device by service %s",
@ -248,9 +255,9 @@ class CastDevice(MediaPlayerEntity):
pychromecast.get_chromecast_from_service,
(
self.services,
cast_info.uuid,
cast_info.model_name,
cast_info.friendly_name,
self._cast_info.uuid,
self._cast_info.model_name,
self._cast_info.friendly_name,
None,
None,
),
@ -777,16 +784,12 @@ class CastDevice(MediaPlayerEntity):
async def _async_cast_discovered(self, discover: ChromecastInfo):
"""Handle discovery of new Chromecast."""
if self._cast_info.uuid is None:
# We can't handle empty UUIDs
return
if self._cast_info.uuid != discover.uuid:
# Discovered is not our device.
return
_LOGGER.debug("Discovered chromecast with same UUID: %s", discover)
await self.async_set_cast_info(discover)
self.async_set_cast_info(discover)
async def _async_stop(self, event):
"""Disconnect socket on Home Assistant stop."""
@ -808,3 +811,131 @@ class CastDevice(MediaPlayerEntity):
self._chromecast.register_handler(controller)
self._hass_cast_controller.show_lovelace_view(view_path, url_path)
class DynamicCastGroup:
"""Representation of a Cast device on the network - for dynamic cast groups."""
def __init__(self, hass, cast_info: ChromecastInfo):
"""Initialize the cast device."""
self.hass = hass
self._cast_info = cast_info
self.services = cast_info.services
self._chromecast: Optional[pychromecast.Chromecast] = None
self.mz_mgr = None
self._status_listener: Optional[CastStatusListener] = None
self._add_remove_handler = None
self._del_remove_handler = None
def async_setup(self):
"""Create chromecast object."""
self._add_remove_handler = async_dispatcher_connect(
self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered
)
self._del_remove_handler = async_dispatcher_connect(
self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed
)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop)
self.async_set_cast_info(self._cast_info)
self.hass.async_create_task(
async_create_catching_coro(self.async_connect_to_chromecast())
)
async def async_tear_down(self) -> None:
"""Disconnect Chromecast object."""
await self._async_disconnect()
if self._cast_info.uuid is not None:
# Remove the entity from the added casts so that it can dynamically
# be re-added again.
self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid)
if self._add_remove_handler:
self._add_remove_handler()
self._add_remove_handler = None
if self._del_remove_handler:
self._del_remove_handler()
self._del_remove_handler = None
def async_set_cast_info(self, cast_info):
"""Set the cast information and set up the chromecast object."""
self._cast_info = cast_info
async def async_connect_to_chromecast(self):
"""Set the cast information and set up the chromecast object."""
_LOGGER.debug(
"[%s %s] Connecting to cast device by service %s",
"Dynamic group",
self._cast_info.friendly_name,
self.services,
)
chromecast = await self.hass.async_add_executor_job(
pychromecast.get_chromecast_from_service,
(
self.services,
self._cast_info.uuid,
self._cast_info.model_name,
self._cast_info.friendly_name,
None,
None,
),
ChromeCastZeroconf.get_zeroconf(),
)
self._chromecast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager()
self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY]
self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr, True)
self._chromecast.start()
async def _async_disconnect(self):
"""Disconnect Chromecast object if it is set."""
if self._chromecast is None:
# Can't disconnect if not connected.
return
_LOGGER.debug(
"[%s %s] Disconnecting from chromecast socket",
"Dynamic group",
self._cast_info.friendly_name,
)
await self.hass.async_add_executor_job(self._chromecast.disconnect)
self._invalidate()
def _invalidate(self):
"""Invalidate some attributes."""
self._chromecast = None
self.mz_mgr = None
if self._status_listener is not None:
self._status_listener.invalidate()
self._status_listener = None
async def _async_cast_discovered(self, discover: ChromecastInfo):
"""Handle discovery of new Chromecast."""
if self._cast_info.uuid != discover.uuid:
# Discovered is not our device.
return
_LOGGER.debug("Discovered dynamic group with same UUID: %s", discover)
self.async_set_cast_info(discover)
async def _async_cast_removed(self, discover: ChromecastInfo):
"""Handle removal of Chromecast."""
if self._cast_info.uuid != discover.uuid:
# Removed is not our device.
return
if not discover.services:
# Clean up the dynamic group
_LOGGER.debug("Clean up dynamic group: %s", discover)
await self.async_tear_down()
async def _async_stop(self, event):
"""Disconnect socket on Home Assistant stop."""
await self._async_disconnect()

View file

@ -1304,7 +1304,7 @@ pycfdns==1.2.1
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==7.6.0
pychromecast==7.7.1
# homeassistant.components.pocketcasts
pycketcasts==1.0.0

View file

@ -664,7 +664,7 @@ pybotvac==0.0.19
pycfdns==1.2.1
# homeassistant.components.cast
pychromecast==7.6.0
pychromecast==7.7.1
# homeassistant.components.coolmaster
pycoolmasternet-async==0.1.2

View file

@ -14,6 +14,7 @@ from homeassistant.components.cast.media_player import ChromecastInfo
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
@ -21,12 +22,32 @@ 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 mz_mock():
"""Mock pychromecast MultizoneManager."""
return MagicMock()
@pytest.fixture()
def pycast_mock():
"""Mock pychromecast."""
pycast_mock = MagicMock()
pycast_mock.start_discovery.return_value = (None, Mock())
return pycast_mock
@pytest.fixture()
def quick_play_mock():
"""Mock pychromecast quick_play."""
@ -34,20 +55,14 @@ def quick_play_mock():
@pytest.fixture(autouse=True)
def cast_mock(mz_mock, quick_play_mock):
def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock):
"""Mock pychromecast."""
pycast_mock = MagicMock()
pycast_mock.start_discovery.return_value = (None, Mock())
dial_mock = MagicMock(name="XXX")
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"
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,
@ -130,6 +145,7 @@ async def async_setup_cast_internal_discovery(hass, config=None):
assert start_discovery.call_count == 1
discovery_callback = cast_listener.call_args[0][0]
remove_callback = cast_listener.call_args[0][1]
def discover_chromecast(service_name: str, info: ChromecastInfo) -> None:
"""Discover a chromecast device."""
@ -141,7 +157,15 @@ async def async_setup_cast_internal_discovery(hass, config=None):
)
discovery_callback(info.uuid, service_name)
return discover_chromecast, add_entities
def remove_chromecast(service_name: str, info: ChromecastInfo) -> None:
"""Remove a chromecast device."""
remove_callback(
info.uuid,
service_name,
(set(), info.uuid, info.model_name, info.friendly_name),
)
return discover_chromecast, remove_chromecast, add_entities
async def async_setup_media_player_cast(hass: HomeAssistantType, info: ChromecastInfo):
@ -183,7 +207,18 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas
await hass.async_block_till_done()
await hass.async_block_till_done()
assert get_chromecast.call_count == 1
return chromecast
def discover_chromecast(service_name: str, info: ChromecastInfo) -> None:
"""Discover a chromecast device."""
listener.services[info.uuid] = (
{service_name},
info.uuid,
info.model_name,
info.friendly_name,
)
discovery_callback(info.uuid, service_name)
return chromecast, discover_chromecast
def get_status_callbacks(chromecast_mock, mz_mock=None):
@ -219,6 +254,123 @@ async def test_start_discovery_called_once(hass):
assert start_discovery.call_count == 1
async def test_internal_discovery_callback_fill_out(hass):
"""Test internal discovery automatically filling out information."""
discover_cast, _, _ = await async_setup_cast_internal_discovery(hass)
info = get_fake_chromecast_info(host="host1")
zconf = get_fake_zconf(host="host1", port=8009)
full_info = attr.evolve(
info,
model_name="google home",
friendly_name="Speaker",
uuid=FakeUUID,
manufacturer="Nabu Casa",
)
with patch(
"homeassistant.components.cast.helpers.dial.get_device_status",
return_value=full_info,
), patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf,
):
signal = MagicMock()
async_dispatcher_connect(hass, "cast_discovered", signal)
discover_cast("the-service", info)
await hass.async_block_till_done()
# when called with incomplete info, it should use HTTP to get missing
discover = signal.mock_calls[0][1][0]
assert discover == full_info
async def test_internal_discovery_callback_fill_out_default_manufacturer(hass):
"""Test internal discovery automatically filling out information."""
discover_cast, _, _ = await async_setup_cast_internal_discovery(hass)
info = get_fake_chromecast_info(host="host1")
zconf = get_fake_zconf(host="host1", port=8009)
full_info = attr.evolve(
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
with patch(
"homeassistant.components.cast.helpers.dial.get_device_status",
return_value=full_info,
), patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf,
):
signal = MagicMock()
async_dispatcher_connect(hass, "cast_discovered", signal)
discover_cast("the-service", info)
await hass.async_block_till_done()
# when called with incomplete info, it should use HTTP to get missing
discover = signal.mock_calls[0][1][0]
assert discover == attr.evolve(full_info, manufacturer="Google Inc.")
async def test_internal_discovery_callback_fill_out_fail(hass):
"""Test internal discovery automatically filling out information."""
discover_cast, _, _ = await async_setup_cast_internal_discovery(hass)
info = get_fake_chromecast_info(host="host1")
zconf = get_fake_zconf(host="host1", port=8009)
full_info = (
info # attr.evolve(info, model_name="", friendly_name="Speaker", uuid=FakeUUID)
)
with patch(
"homeassistant.components.cast.helpers.dial.get_device_status",
return_value=None,
), patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf,
):
signal = MagicMock()
async_dispatcher_connect(hass, "cast_discovered", signal)
discover_cast("the-service", info)
await hass.async_block_till_done()
# when called with incomplete info, it should use HTTP to get missing
discover = signal.mock_calls[0][1][0]
assert discover == full_info
# assert 1 == 2
async def test_internal_discovery_callback_fill_out_group(hass):
"""Test internal discovery automatically filling out information."""
discover_cast, _, _ = await async_setup_cast_internal_discovery(hass)
info = get_fake_chromecast_info(host="host1", port=12345)
zconf = get_fake_zconf(host="host1", port=12345)
full_info = attr.evolve(
info,
model_name="",
friendly_name="Speaker",
uuid=FakeUUID,
is_dynamic_group=False,
)
with patch(
"homeassistant.components.cast.helpers.dial.get_device_status",
return_value=full_info,
), patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf,
):
signal = MagicMock()
async_dispatcher_connect(hass, "cast_discovered", signal)
discover_cast("the-service", info)
await hass.async_block_till_done()
# when called with incomplete info, it should use HTTP to get missing
discover = signal.mock_calls[0][1][0]
assert discover == full_info
async def test_stop_discovery_called_on_stop(hass):
"""Test pychromecast.stop_discovery called on shutdown."""
browser = MagicMock(zc={})
@ -272,7 +424,7 @@ async def test_replay_past_chromecasts(hass):
zconf_1 = get_fake_zconf(host="host1", port=8009)
zconf_2 = get_fake_zconf(host="host2", port=8009)
discover_cast, add_dev1 = await async_setup_cast_internal_discovery(
discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(
hass, config={"uuid": FakeUUID}
)
@ -308,7 +460,7 @@ async def test_manual_cast_chromecasts_uuid(hass):
zconf_2 = get_fake_zconf(host="host_2")
# Manual configuration of media player with host "configured_host"
discover_cast, add_dev1 = await async_setup_cast_internal_discovery(
discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(
hass, config={"uuid": FakeUUID}
)
with patch(
@ -338,7 +490,7 @@ async def test_auto_cast_chromecasts(hass):
zconf_2 = get_fake_zconf(host="other_host")
# Manual configuration of media player with host "configured_host"
discover_cast, add_dev1 = await async_setup_cast_internal_discovery(hass)
discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(hass)
with patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf_1,
@ -358,6 +510,79 @@ async def test_auto_cast_chromecasts(hass):
assert add_dev1.call_count == 2
async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog):
"""Test dynamic group does not create device or entity."""
cast_1 = get_fake_chromecast_info(host="host_1", port=23456, uuid=FakeUUID)
cast_2 = get_fake_chromecast_info(host="host_2", port=34567, uuid=FakeUUID2)
zconf_1 = get_fake_zconf(host="host_1", port=23456)
zconf_2 = get_fake_zconf(host="host_2", port=34567)
reg = await hass.helpers.entity_registry.async_get_registry()
# Fake dynamic group info
tmp1 = MagicMock()
tmp1.uuid = FakeUUID
tmp2 = MagicMock()
tmp2.uuid = FakeUUID2
dial_mock.get_multizone_status.return_value.dynamic_groups = [tmp1, tmp2]
pycast_mock.get_chromecast_from_service.assert_not_called()
discover_cast, remove_cast, add_dev1 = await async_setup_cast_internal_discovery(
hass
)
# Discover cast service
with patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf_1,
):
discover_cast("service", cast_1)
await hass.async_block_till_done()
await hass.async_block_till_done() # having tasks that add jobs
pycast_mock.get_chromecast_from_service.assert_called()
pycast_mock.get_chromecast_from_service.reset_mock()
assert add_dev1.call_count == 0
assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None
# Discover other dynamic group cast service
with patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf_2,
):
discover_cast("service", cast_2)
await hass.async_block_till_done()
await hass.async_block_till_done() # having tasks that add jobs
pycast_mock.get_chromecast_from_service.assert_called()
pycast_mock.get_chromecast_from_service.reset_mock()
assert add_dev1.call_count == 0
assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None
# Get update for cast service
with patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf_1,
):
discover_cast("service", cast_1)
await hass.async_block_till_done()
await hass.async_block_till_done() # having tasks that add jobs
pycast_mock.get_chromecast_from_service.assert_not_called()
assert add_dev1.call_count == 0
assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None
# Remove cast service
assert "Disconnecting from chromecast" not in caplog.text
with patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf_1,
):
remove_cast("service", cast_1)
await hass.async_block_till_done()
await hass.async_block_till_done() # having tasks that add jobs
assert "Disconnecting from chromecast" in caplog.text
async def test_update_cast_chromecasts(hass):
"""Test discovery of same UUID twice only adds one cast."""
cast_1 = get_fake_chromecast_info(host="old_host")
@ -366,7 +591,7 @@ async def test_update_cast_chromecasts(hass):
zconf_2 = get_fake_zconf(host="new_host")
# Manual configuration of media player with host "configured_host"
discover_cast, add_dev1 = await async_setup_cast_internal_discovery(hass)
discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(hass)
with patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
@ -392,7 +617,7 @@ async def test_entity_availability(hass: HomeAssistantType):
entity_id = "media_player.speaker"
info = get_fake_chromecast_info()
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, _ = get_status_callbacks(chromecast)
state = hass.states.get(entity_id)
@ -423,7 +648,7 @@ async def test_entity_cast_status(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
cast_status_cb, conn_status_cb, _ = get_status_callbacks(chromecast)
connection_status = MagicMock()
@ -466,7 +691,7 @@ async def test_entity_play_media(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, _ = get_status_callbacks(chromecast)
connection_status = MagicMock()
@ -495,7 +720,7 @@ async def test_entity_play_media_cast(hass: HomeAssistantType, quick_play_mock):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, _ = get_status_callbacks(chromecast)
connection_status = MagicMock()
@ -528,7 +753,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, _ = get_status_callbacks(chromecast)
connection_status = MagicMock()
@ -575,7 +800,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistantType):
info = get_fake_chromecast_info()
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, _ = get_status_callbacks(chromecast)
connection_status = MagicMock()
@ -601,7 +826,7 @@ async def test_entity_media_content_type(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, media_status_cb = get_status_callbacks(chromecast)
connection_status = MagicMock()
@ -655,7 +880,7 @@ async def test_entity_control(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, media_status_cb = get_status_callbacks(chromecast)
connection_status = MagicMock()
@ -738,7 +963,7 @@ async def test_entity_media_states(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, media_status_cb = get_status_callbacks(chromecast)
connection_status = MagicMock()
@ -797,7 +1022,7 @@ async def test_group_media_states(hass, mz_mock):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, media_status_cb, group_media_status_cb = get_status_callbacks(
chromecast, mz_mock
)
@ -841,7 +1066,7 @@ async def test_group_media_states(hass, mz_mock):
async def test_group_media_control(hass, mz_mock):
"""Test media states are read from group if entity has no state."""
"""Test media controls are handled by group if entity has no state."""
entity_id = "media_player.speaker"
reg = await hass.helpers.entity_registry.async_get_registry()
@ -850,7 +1075,7 @@ async def test_group_media_control(hass, mz_mock):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, conn_status_cb, media_status_cb, group_media_status_cb = get_status_callbacks(
chromecast, mz_mock
@ -904,7 +1129,7 @@ async def test_group_media_control(hass, mz_mock):
async def test_failed_cast_on_idle(hass, caplog):
"""Test no warning when unless player went idle with reason "ERROR"."""
info = get_fake_chromecast_info()
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, _, media_status_cb = get_status_callbacks(chromecast)
media_status = MagicMock(images=None)
@ -939,7 +1164,7 @@ async def test_failed_cast_other_url(hass, caplog):
)
info = get_fake_chromecast_info()
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, _, media_status_cb = get_status_callbacks(chromecast)
media_status = MagicMock(images=None)
@ -962,7 +1187,7 @@ async def test_failed_cast_internal_url(hass, caplog):
)
info = get_fake_chromecast_info()
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, _, media_status_cb = get_status_callbacks(chromecast)
media_status = MagicMock(images=None)
@ -990,7 +1215,7 @@ async def test_failed_cast_external_url(hass, caplog):
)
info = get_fake_chromecast_info()
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, _, media_status_cb = get_status_callbacks(chromecast)
media_status = MagicMock(images=None)
@ -1014,7 +1239,7 @@ async def test_failed_cast_tts_base_url(hass, caplog):
)
info = get_fake_chromecast_info()
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
_, _, media_status_cb = get_status_callbacks(chromecast)
media_status = MagicMock(images=None)
@ -1032,7 +1257,7 @@ async def test_disconnect_on_stop(hass: HomeAssistantType):
"""Test cast device disconnects socket on stop."""
info = get_fake_chromecast_info()
chromecast = await async_setup_media_player_cast(hass, info)
chromecast, _ = await async_setup_media_player_cast(hass, info)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()