From b025f17becdfa4b3b0bd8446705d90d57303e395 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 11 Nov 2021 11:19:09 +0100 Subject: [PATCH 1/4] Prepare for pychromecast 10 --- homeassistant/components/cast/const.py | 1 - homeassistant/components/cast/discovery.py | 16 ++-- homeassistant/components/cast/helpers.py | 93 ++++++------------- homeassistant/components/cast/media_player.py | 4 + tests/components/cast/conftest.py | 4 - tests/components/cast/test_media_player.py | 79 ++++------------ 6 files changed, 55 insertions(+), 142 deletions(-) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index 03ffdfbd15c99..06db70b830abf 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 a5ac4c0204785..8f3d01a36fdbf 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,16 @@ _LOGGER = logging.getLogger(__name__) -def discover_chromecast(hass: HomeAssistant, device_info): +def discover_chromecast(hass: HomeAssistant, cast_info: pychromecast.models.CastInfo): """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 +78,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 6d021d020c4eb..a3bf34ab3ae3a 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/media_player.py b/homeassistant/components/cast/media_player.py index 310431bb48832..2dc6f9e5072fb 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -239,6 +239,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(), ) @@ -836,6 +838,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/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index dce5db5116153..4c17cf74af9e9 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 3bb9cbf23169b..35648e81b98a0 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, From 2217e8cdcfe9509d3cf982a23e3d07f0d150bcbd Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 15 Nov 2021 14:58:10 +0100 Subject: [PATCH 2/4] Bump pychromecast to 10.0.0 --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index dbbb7aa1417b3..e2c52d84457bf 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.0.0"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 025c34b17672b..fc5ec7272082b 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.0.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c7854a817fd6..55cbd503ecbfb 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.0.0 # homeassistant.components.climacell pyclimacell==0.18.2 From 4252b85d8f7c73ab725018f3533e62346e70769b Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 15 Nov 2021 20:47:15 +0100 Subject: [PATCH 3/4] Bump pychromecast to 10.1.0 --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index e2c52d84457bf..9e79e395fde91 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==10.0.0"], + "requirements": ["pychromecast==10.1.0"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index fc5ec7272082b..65544b5ac4960 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==10.0.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 55cbd503ecbfb..080a7b2a729ab 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==10.0.0 +pychromecast==10.1.0 # homeassistant.components.climacell pyclimacell==0.18.2 From ed75df55dfe5f909bf8b56accd14a58b0b4a4e7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 16 Nov 2021 15:14:56 +0100 Subject: [PATCH 4/4] Update homeassistant/components/cast/discovery.py Co-authored-by: Martin Hjelmare --- homeassistant/components/cast/discovery.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 8f3d01a36fdbf..01b00c82f64fe 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -20,7 +20,9 @@ _LOGGER = logging.getLogger(__name__) -def discover_chromecast(hass: HomeAssistant, cast_info: pychromecast.models.CastInfo): +def discover_chromecast( + hass: HomeAssistant, cast_info: pychromecast.models.CastInfo +) -> None: """Discover a Chromecast.""" info = ChromecastInfo(