From 593bc866f049997f0b4cf7c3ceee29cd1468d7c9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Nov 2021 09:05:25 +0100 Subject: [PATCH] Bump pychromecast to 10.1.0 (#59719) * Prepare for pychromecast 10 * Bump pychromecast to 10.0.0 * Bump pychromecast to 10.1.0 * Update homeassistant/components/cast/discovery.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/cast/const.py | 1 - homeassistant/components/cast/discovery.py | 18 ++-- homeassistant/components/cast/helpers.py | 93 ++++++------------- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 4 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/conftest.py | 4 - tests/components/cast/test_media_player.py | 79 ++++------------ 9 files changed, 60 insertions(+), 145 deletions(-) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index 03ffdfbd15c99c..06db70b830abfc 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 a5ac4c02047850..01b00c82f64fe9 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -11,7 +11,6 @@ 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 @@ _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 remove_cast(self, uuid, service, cast_info): 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 6d021d020c4eb2..a3bf34ab3ae3ae 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -5,7 +5,7 @@ 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 dbbb7aa1417b3a..9e79e395fde919 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 cfaf884386565f..32c303dfd8bc27 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -235,6 +235,8 @@ async def async_connect_to_chromecast(self): self._cast_info.friendly_name, None, None, + self._cast_info.cast_type, + self._cast_info.manufacturer, ), ChromeCastZeroconf.get_zeroconf(), ) @@ -833,6 +835,8 @@ async def async_connect_to_chromecast(self): 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 3349956870e256..1aacb26c9f089d 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 70d9644f6a6021..9d01ed04eb5ce7 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 dce5db5116153c..4c17cf74af9e9a 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -10,10 +10,6 @@ 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 a038cb34aeab1c..6c63d3bcc2bb99 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -8,6 +8,7 @@ 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 __eq__(self, other): 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 __eq__(self, other): return ExtendedChromecastInfo( host=host, port=port, + services={"the-service"}, uuid=uuid, + model_name="Chromecast", friendly_name="Speaker", - services={"the-service"}, - is_audio_group=port != 8009, + cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, + manufacturer="Nabu Casa", ) @@ -142,6 +145,8 @@ def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: info.friendly_name, info.host, info.port, + info.cast_type, + info.manufacturer, ) discovery_callback(info.uuid, service_name) @@ -157,6 +162,8 @@ def remove_chromecast(service_name: str, info: ChromecastInfo) -> 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 @@ def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: 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,