Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions homeassistant/components/cast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Cast from a config entry."""
await home_assistant_cast.async_setup_ha_cast(hass, entry)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
hass.data[DOMAIN] = {}
hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}}
await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform)
return True

Expand Down Expand Up @@ -107,7 +107,7 @@ async def _register_cast_platform(
or not hasattr(platform, "async_play_media")
):
raise HomeAssistantError(f"Invalid cast platform {platform}")
hass.data[DOMAIN][integration_domain] = platform
hass.data[DOMAIN]["cast_platform"][integration_domain] = platform


async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cast/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def discover_chromecast(
_LOGGER.error("Discovered chromecast without uuid %s", info)
return

info = info.fill_out_missing_chromecast_info()
info = info.fill_out_missing_chromecast_info(hass)
_LOGGER.debug("Discovered new or updated chromecast %s", info)

dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info)
Expand Down
47 changes: 41 additions & 6 deletions homeassistant/components/cast/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
from pychromecast.const import CAST_TYPE_GROUP
from pychromecast.models import CastInfo

from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

_PLS_SECTION_PLAYLIST = "playlist"
Expand Down Expand Up @@ -47,18 +50,50 @@ def uuid(self) -> bool:
"""Return the UUID."""
return self.cast_info.uuid

def fill_out_missing_chromecast_info(self) -> ChromecastInfo:
def fill_out_missing_chromecast_info(self, hass: HomeAssistant) -> ChromecastInfo:
"""Return a new ChromecastInfo object with missing attributes filled in.

Uses blocking HTTP / HTTPS.
"""
cast_info = self.cast_info
if self.cast_info.cast_type is None or self.cast_info.manufacturer is None:
# Manufacturer and cast type is not available in mDNS data, get it over http
cast_info = dial.get_cast_type(
cast_info,
zconf=ChromeCastZeroconf.get_zeroconf(),
)
unknown_models = hass.data[DOMAIN]["unknown_models"]
if self.cast_info.model_name not in unknown_models:
# Manufacturer and cast type is not available in mDNS data, get it over http
cast_info = dial.get_cast_type(
cast_info,
zconf=ChromeCastZeroconf.get_zeroconf(),
)
unknown_models[self.cast_info.model_name] = (
cast_info.cast_type,
cast_info.manufacturer,
)

report_issue = (
"create a bug report at "
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
"+label%3A%22integration%3A+cast%22"
)

_LOGGER.info(
"Fetched cast details for unknown model '%s' manufacturer: '%s', type: '%s'. Please %s",
cast_info.model_name,
cast_info.manufacturer,
cast_info.cast_type,
report_issue,
)
else:
cast_type, manufacturer = unknown_models[self.cast_info.model_name]
cast_info = CastInfo(
cast_info.services,
cast_info.uuid,
cast_info.model_name,
cast_info.friendly_name,
cast_info.host,
cast_info.port,
cast_type,
manufacturer,
)

if not self.is_audio_group or self.is_dynamic_group is not None:
# We have all information, no need to check HTTP API.
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cast/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==12.0.0"],
"requirements": ["pychromecast==12.1.0"],
"after_dependencies": [
"cloud",
"http",
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/cast/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ async def _async_root_payload(self, content_filter):
"""Generate root node."""
children = []
# Add media browsers
for platform in self.hass.data[CAST_DOMAIN].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
children.extend(
await platform.async_get_media_browser_root_object(
self.hass, self._chromecast.cast_type
Expand Down Expand Up @@ -587,7 +587,7 @@ def audio_content_filter(item):
if media_content_id is None:
return await self._async_root_payload(content_filter)

for platform in self.hass.data[CAST_DOMAIN].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
browse_media = await platform.async_browse_media(
self.hass,
media_content_type,
Expand Down Expand Up @@ -646,7 +646,7 @@ async def async_play_media(self, media_type, media_id, **kwargs):
return

# Try the cast platforms
for platform in self.hass.data[CAST_DOMAIN].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
result = await platform.async_play_media(
self.hass, self.entity_id, self._chromecast, media_type, media_id
)
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1402,7 +1402,7 @@ pycfdns==1.2.2
pychannels==1.0.0

# homeassistant.components.cast
pychromecast==12.0.0
pychromecast==12.1.0

# homeassistant.components.pocketcasts
pycketcasts==1.0.0
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,7 @@ pybotvac==0.0.23
pycfdns==1.2.2

# homeassistant.components.cast
pychromecast==12.0.0
pychromecast==12.1.0

# homeassistant.components.climacell
pyclimacell==0.18.2
Expand Down
11 changes: 11 additions & 0 deletions tests/components/cast/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ def get_multizone_status_mock():
return mock


@pytest.fixture()
def get_cast_type_mock():
"""Mock pychromecast dial."""
mock = MagicMock(spec_set=pychromecast.dial.get_cast_type)
return mock


@pytest.fixture()
def castbrowser_mock():
"""Mock pychromecast CastBrowser."""
Expand Down Expand Up @@ -43,6 +50,7 @@ def cast_mock(
mz_mock,
quick_play_mock,
castbrowser_mock,
get_cast_type_mock,
get_chromecast_mock,
get_multizone_status_mock,
):
Expand All @@ -52,6 +60,9 @@ def cast_mock(
with patch(
"homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser",
castbrowser_mock,
), patch(
"homeassistant.components.cast.helpers.dial.get_cast_type",
get_cast_type_mock,
), patch(
"homeassistant.components.cast.helpers.dial.get_multizone_status",
get_multizone_status_mock,
Expand Down
109 changes: 105 additions & 4 deletions tests/components/cast/test_media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
pychromecast.const.SERVICE_TYPE_MDNS, "the-service"
)

UNDEFINED = object()


def get_fake_chromecast(info: ChromecastInfo):
"""Generate a Fake Chromecast object with the specified arguments."""
Expand All @@ -74,24 +76,37 @@ def get_fake_chromecast(info: ChromecastInfo):


def get_fake_chromecast_info(
host="192.168.178.42", port=8009, service=None, uuid: UUID | None = FakeUUID
*,
host="192.168.178.42",
port=8009,
service=None,
uuid: UUID | None = FakeUUID,
cast_type=UNDEFINED,
manufacturer=UNDEFINED,
model_name=UNDEFINED,
):
"""Generate a Fake ChromecastInfo with the specified arguments."""

if service is None:
service = pychromecast.discovery.ServiceInfo(
pychromecast.const.SERVICE_TYPE_HOST, (host, port)
)
if cast_type is UNDEFINED:
cast_type = CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST
if manufacturer is UNDEFINED:
manufacturer = "Nabu Casa"
if model_name is UNDEFINED:
model_name = "Chromecast"
return ChromecastInfo(
cast_info=pychromecast.models.CastInfo(
services={service},
uuid=uuid,
model_name="Chromecast",
model_name=model_name,
friendly_name="Speaker",
host=host,
port=port,
cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST,
manufacturer="Nabu Casa",
cast_type=cast_type,
manufacturer=manufacturer,
)
)

Expand Down Expand Up @@ -342,6 +357,92 @@ async def test_internal_discovery_callback_fill_out_group(
get_multizone_status_mock.assert_called_once()


async def test_internal_discovery_callback_fill_out_cast_type_manufacturer(
hass, get_cast_type_mock, caplog
):
"""Test internal discovery automatically filling out information."""
discover_cast, _, _ = await async_setup_cast_internal_discovery(hass)
info = get_fake_chromecast_info(
host="host1",
port=8009,
service=FAKE_MDNS_SERVICE,
cast_type=None,
manufacturer=None,
)
info2 = get_fake_chromecast_info(
host="host1",
port=8009,
service=FAKE_MDNS_SERVICE,
cast_type=None,
manufacturer=None,
model_name="Model 101",
)
zconf = get_fake_zconf(host="host1", port=8009)
full_info = attr.evolve(
info,
cast_info=pychromecast.discovery.CastInfo(
services=info.cast_info.services,
uuid=FakeUUID,
model_name="Chromecast",
friendly_name="Speaker",
host=info.cast_info.host,
port=info.cast_info.port,
cast_type="audio",
manufacturer="TrollTech",
),
is_dynamic_group=None,
)
full_info2 = attr.evolve(
info2,
cast_info=pychromecast.discovery.CastInfo(
services=info.cast_info.services,
uuid=FakeUUID,
model_name="Model 101",
friendly_name="Speaker",
host=info.cast_info.host,
port=info.cast_info.port,
cast_type="cast",
manufacturer="Cyberdyne Systems",
),
is_dynamic_group=None,
)

get_cast_type_mock.assert_not_called()
get_cast_type_mock.return_value = full_info.cast_info

with patch(
"homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf",
return_value=zconf,
):
signal = MagicMock()

async_dispatcher_connect(hass, "cast_discovered", signal)
discover_cast(FAKE_MDNS_SERVICE, info)
await hass.async_block_till_done()

# when called with incomplete info, it should use HTTP to get missing
get_cast_type_mock.assert_called_once()
assert get_cast_type_mock.call_count == 1
discover = signal.mock_calls[0][1][0]
assert discover == full_info
assert "Fetched cast details for unknown model 'Chromecast'" in caplog.text

# Call again, the model name should be fetched from cache
discover_cast(FAKE_MDNS_SERVICE, info)
await hass.async_block_till_done()
assert get_cast_type_mock.call_count == 1 # No additional calls
discover = signal.mock_calls[1][1][0]
assert discover == full_info

# Call for another model, need to call HTTP again
get_cast_type_mock.return_value = full_info2.cast_info
discover_cast(FAKE_MDNS_SERVICE, info2)
await hass.async_block_till_done()
assert get_cast_type_mock.call_count == 2
discover = signal.mock_calls[2][1][0]
assert discover == full_info2


async def test_stop_discovery_called_on_stop(hass, castbrowser_mock):
"""Test pychromecast.stop_discovery called on shutdown."""
# start_discovery should be called with empty config
Expand Down