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:
parent
f18880686c
commit
02bfc68842
7 changed files with 489 additions and 59 deletions
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue