diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index 03ffdfbd15c..06db70b830a 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -1,7 +1,6 @@ """Consts for Cast integration.""" DOMAIN = "cast" -DEFAULT_PORT = 8009 # Stores a threading.Lock that is held by the internal pychromecast discovery. INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running" diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index a5ac4c02047..01b00c82f64 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -11,7 +11,6 @@ from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( CAST_BROWSER_KEY, CONF_KNOWN_HOSTS, - DEFAULT_PORT, INTERNAL_DISCOVERY_RUNNING_KEY, SIGNAL_CAST_DISCOVERED, SIGNAL_CAST_REMOVED, @@ -21,15 +20,18 @@ from .helpers import ChromecastInfo, ChromeCastZeroconf _LOGGER = logging.getLogger(__name__) -def discover_chromecast(hass: HomeAssistant, device_info): +def discover_chromecast( + hass: HomeAssistant, cast_info: pychromecast.models.CastInfo +) -> None: """Discover a Chromecast.""" info = ChromecastInfo( - services=device_info.services, - uuid=device_info.uuid, - model_name=device_info.model_name, - friendly_name=device_info.friendly_name, - is_audio_group=device_info.port != DEFAULT_PORT, + services=cast_info.services, + uuid=cast_info.uuid, + model_name=cast_info.model_name, + friendly_name=cast_info.friendly_name, + cast_type=cast_info.cast_type, + manufacturer=cast_info.manufacturer, ) if info.uuid is None: @@ -78,6 +80,8 @@ def setup_internal_discovery(hass: HomeAssistant, config_entry) -> None: uuid=cast_info.uuid, model_name=cast_info.model_name, friendly_name=cast_info.friendly_name, + cast_type=cast_info.cast_type, + manufacturer=cast_info.manufacturer, ), ) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 6d021d020c4..a3bf34ab3ae 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -5,7 +5,7 @@ from typing import Optional import attr from pychromecast import dial -from pychromecast.const import CAST_MANUFACTURERS +from pychromecast.const import CAST_TYPE_GROUP @attr.s(slots=True, frozen=True) @@ -16,89 +16,48 @@ class ChromecastInfo: """ services: set | None = attr.ib() - uuid: str | None = 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: str | None = attr.ib(default=None) - is_audio_group = attr.ib(type=Optional[bool], default=False) + uuid: str = attr.ib(converter=attr.converters.optional(str)) + model_name: str = attr.ib() + friendly_name: str = attr.ib() + cast_type: str = attr.ib() + manufacturer: str = attr.ib() is_dynamic_group = attr.ib(type=Optional[bool], default=None) @property - 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 | None: - """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 is_audio_group(self) -> bool: + """Return if the cast is an audio group.""" + return self.cast_type == CAST_TYPE_GROUP 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: + if not self.is_audio_group or self.is_dynamic_group is not None: # 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( - None, - 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, - uuid=self.uuid, - friendly_name=self.friendly_name, - model_name=self.model_name, - is_audio_group=True, - is_dynamic_group=is_dynamic_group, - ) - - # Fill out some missing information (friendly_name, uuid) via HTTP dial. - http_device_status = dial.get_device_status( - None, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf() + is_dynamic_group = False + http_group_status = None + http_group_status = dial.get_multizone_status( + None, + services=self.services, + zconf=ChromeCastZeroconf.get_zeroconf(), ) - if http_device_status is None: - # HTTP dial didn't give us any new information. - return self + 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, - 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), + uuid=self.uuid, + friendly_name=self.friendly_name, + model_name=self.model_name, + cast_type=self.cast_type, + manufacturer=self.manufacturer, + is_dynamic_group=is_dynamic_group, ) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index dbbb7aa1417..9e79e395fde 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.4.0"], + "requirements": ["pychromecast==10.1.0"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index cfaf8843865..32c303dfd8b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -235,6 +235,8 @@ class CastDevice(MediaPlayerEntity): self._cast_info.friendly_name, None, None, + self._cast_info.cast_type, + self._cast_info.manufacturer, ), ChromeCastZeroconf.get_zeroconf(), ) @@ -833,6 +835,8 @@ class DynamicCastGroup: self._cast_info.friendly_name, None, None, + self._cast_info.cast_type, + self._cast_info.manufacturer, ), ChromeCastZeroconf.get_zeroconf(), ) diff --git a/requirements_all.txt b/requirements_all.txt index 3349956870e..1aacb26c9f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1390,7 +1390,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.4.0 +pychromecast==10.1.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70d9644f6a6..9d01ed04eb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -838,7 +838,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==9.4.0 +pychromecast==10.1.0 # homeassistant.components.climacell pyclimacell==0.18.2 diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index dce5db51161..4c17cf74af9 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -10,10 +10,6 @@ import pytest 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 diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index a038cb34aea..6c63d3bcc2b 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -8,6 +8,7 @@ from uuid import UUID import attr import pychromecast +from pychromecast.const import CAST_TYPE_CHROMECAST, CAST_TYPE_GROUP import pytest from homeassistant.components import media_player, tts @@ -70,10 +71,10 @@ def get_fake_chromecast_info( ChromecastInfo( services=self.services, uuid=self.uuid, - manufacturer=self.manufacturer, model_name=self.model_name, friendly_name=self.friendly_name, - is_audio_group=self.is_audio_group, + cast_type=self.cast_type, + manufacturer=self.manufacturer, is_dynamic_group=self.is_dynamic_group, ) == other @@ -83,10 +84,12 @@ def get_fake_chromecast_info( return ExtendedChromecastInfo( host=host, port=port, - uuid=uuid, - friendly_name="Speaker", services={"the-service"}, - is_audio_group=port != 8009, + uuid=uuid, + model_name="Chromecast", + friendly_name="Speaker", + cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, + manufacturer="Nabu Casa", ) @@ -142,6 +145,8 @@ async def async_setup_cast_internal_discovery(hass, config=None): info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ) discovery_callback(info.uuid, service_name) @@ -157,6 +162,8 @@ async def async_setup_cast_internal_discovery(hass, config=None): info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ), ) @@ -195,6 +202,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ) discovery_callback(info.uuid, service_name) @@ -211,6 +220,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ) discovery_callback(info.uuid, service_name) @@ -245,64 +256,6 @@ async def test_start_discovery_called_once(hass, castbrowser_mock): assert castbrowser_mock.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) @@ -337,7 +290,7 @@ async def test_internal_discovery_callback_fill_out_group(hass): zconf = get_fake_zconf(host="host1", port=12345) full_info = attr.evolve( info, - model_name="", + model_name="Chromecast", friendly_name="Speaker", uuid=FakeUUID, is_dynamic_group=False,