Remove support for cast dynamic speaker groups (#33884)

This commit is contained in:
Erik Montnemery 2020-04-10 19:36:57 +02:00 committed by GitHub
parent 496da8823d
commit 6d3046cb42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 5 additions and 348 deletions

View File

@ -23,7 +23,6 @@ class ChromecastInfo:
) # always convert UUID to string if not None
model_name = attr.ib(type=str, default="")
friendly_name = attr.ib(type=Optional[str], default=None)
is_dynamic_group = attr.ib(type=Optional[bool], default=None)
@property
def is_audio_group(self) -> bool:
@ -33,19 +32,7 @@ class ChromecastInfo:
@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
)
return all(attr.astuple(self))
@property
def host_port(self) -> Tuple[str, int]:
@ -70,8 +57,6 @@ class ChromecastInfo:
return self
if self.is_audio_group:
is_dynamic_group = False
return ChromecastInfo(
service=self.service,
host=self.host,
@ -79,7 +64,6 @@ class ChromecastInfo:
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.
@ -99,14 +83,6 @@ class ChromecastInfo:
model_name=self.model_name,
)
def same_dynamic_group(self, other: "ChromecastInfo") -> bool:
"""Test chromecast info is same dynamic group."""
return (
self.is_audio_group
and other.is_dynamic_group
and self.friendly_name == other.friendly_name
)
class ChromeCastZeroconf:
"""Class to hold a zeroconf instance."""
@ -190,45 +166,3 @@ class CastStatusListener:
else:
self._mz_mgr.deregister_listener(self._uuid, self)
self._valid = False
class DynamicGroupCastStatusListener:
"""Helper class to handle pychromecast status callbacks.
Necessary because a CastDevice entity can create a new socket client
and therefore callbacks from multiple chromecast connections can
potentially arrive. This class allows invalidating past chromecast objects.
"""
def __init__(self, cast_device, chromecast, mz_mgr):
"""Initialize the status listener."""
self._cast_device = cast_device
self._uuid = chromecast.uuid
self._valid = True
self._mz_mgr = mz_mgr
chromecast.register_status_listener(self)
chromecast.socket_client.media_controller.register_status_listener(self)
chromecast.register_connection_listener(self)
self._mz_mgr.add_multizone(chromecast)
def new_cast_status(self, cast_status):
"""Handle reception of a new CastStatus."""
def new_media_status(self, media_status):
"""Handle reception of a new MediaStatus."""
if self._valid:
self._cast_device.new_dynamic_group_media_status(media_status)
def new_connection_status(self, connection_status):
"""Handle reception of a new ConnectionStatus."""
if self._valid:
self._cast_device.new_dynamic_group_connection_status(connection_status)
def invalidate(self):
"""Invalidate this status listener.
All following callbacks won't be forwarded.
"""
self._mz_mgr.remove_multizone(self._uuid)
self._valid = False

View File

@ -56,12 +56,7 @@ from .const import (
SIGNAL_HASS_CAST_SHOW_VIEW,
)
from .discovery import setup_internal_discovery
from .helpers import (
CastStatusListener,
ChromecastInfo,
ChromeCastZeroconf,
DynamicGroupCastStatusListener,
)
from .helpers import CastStatusListener, ChromecastInfo, ChromeCastZeroconf
_LOGGER = logging.getLogger(__name__)
@ -101,10 +96,6 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo):
return CastDevice(info)
# Found a cast with UUID
if info.is_dynamic_group:
# This is a dynamic group, do not add it.
return None
added_casts = hass.data[ADDED_CAST_DEVICES_KEY]
if info.uuid in added_casts:
# Already added this one, the entity will take care of moved hosts
@ -201,19 +192,11 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = None
self.media_status = None
self.media_status_received = None
self._dynamic_group_cast_info: ChromecastInfo = None
self._dynamic_group_cast: Optional[pychromecast.Chromecast] = None
self.dynamic_group_media_status = None
self.dynamic_group_media_status_received = None
self.mz_media_status = {}
self.mz_media_status_received = {}
self.mz_mgr = None
self._available = False
self._dynamic_group_available = False
self._status_listener: Optional[CastStatusListener] = None
self._dynamic_group_status_listener: Optional[
DynamicGroupCastStatusListener
] = None
self._hass_cast_controller: Optional[HomeAssistantController] = None
self._add_remove_handler = None
@ -232,20 +215,6 @@ class CastDevice(MediaPlayerDevice):
self.hass.async_create_task(
async_create_catching_coro(self.async_set_cast_info(self._cast_info))
)
for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]:
if self._cast_info.same_dynamic_group(info):
_LOGGER.debug(
"[%s %s (%s:%s)] Found dynamic group: %s",
self.entity_id,
self._cast_info.friendly_name,
self._cast_info.host,
self._cast_info.port,
info,
)
self.hass.async_create_task(
async_create_catching_coro(self.async_set_dynamic_group(info))
)
break
self._cast_view_remove_handler = async_dispatcher_connect(
self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view
@ -358,69 +327,6 @@ class CastDevice(MediaPlayerDevice):
self.services,
)
async def async_set_dynamic_group(self, cast_info):
"""Set the cast information and set up the chromecast object."""
_LOGGER.debug(
"[%s %s (%s:%s)] Connecting to dynamic group by host %s",
self.entity_id,
self._cast_info.friendly_name,
self._cast_info.host,
self._cast_info.port,
cast_info,
)
await self.async_del_dynamic_group()
self._dynamic_group_cast_info = cast_info
# pylint: disable=protected-access
chromecast = await self.hass.async_add_executor_job(
pychromecast._get_chromecast_from_host,
(
cast_info.host,
cast_info.port,
cast_info.uuid,
cast_info.model_name,
cast_info.friendly_name,
),
)
self._dynamic_group_cast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager()
mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY]
self._dynamic_group_status_listener = DynamicGroupCastStatusListener(
self, chromecast, mz_mgr
)
self._dynamic_group_available = False
self.dynamic_group_media_status = chromecast.media_controller.status
self._dynamic_group_cast.start()
self.async_write_ha_state()
async def async_del_dynamic_group(self):
"""Remove the dynamic group."""
cast_info = self._dynamic_group_cast_info
_LOGGER.debug(
"[%s %s (%s:%s)] Remove dynamic group: %s",
self.entity_id,
self._cast_info.friendly_name,
self._cast_info.host,
self._cast_info.port,
cast_info.service if cast_info else None,
)
self._dynamic_group_available = False
self._dynamic_group_cast_info = None
if self._dynamic_group_cast is not None:
await self.hass.async_add_executor_job(self._dynamic_group_cast.disconnect)
self._dynamic_group_invalidate()
self.async_write_ha_state()
async def _async_disconnect(self):
"""Disconnect Chromecast object if it is set."""
if self._chromecast is None:
@ -437,8 +343,6 @@ class CastDevice(MediaPlayerDevice):
self.async_write_ha_state()
await self.hass.async_add_executor_job(self._chromecast.disconnect)
if self._dynamic_group_cast is not None:
await self.hass.async_add_executor_job(self._dynamic_group_cast.disconnect)
self._invalidate()
@ -458,15 +362,6 @@ class CastDevice(MediaPlayerDevice):
self._status_listener.invalidate()
self._status_listener = None
def _dynamic_group_invalidate(self):
"""Invalidate some attributes."""
self._dynamic_group_cast = None
self.dynamic_group_media_status = None
self.dynamic_group_media_status_received = None
if self._dynamic_group_status_listener is not None:
self._dynamic_group_status_listener.invalidate()
self._dynamic_group_status_listener = None
# ========== Callbacks ==========
def new_cast_status(self, cast_status):
"""Handle updates of the cast status."""
@ -515,44 +410,6 @@ class CastDevice(MediaPlayerDevice):
self._available = new_available
self.schedule_update_ha_state()
def new_dynamic_group_media_status(self, media_status):
"""Handle updates of the media status."""
self.dynamic_group_media_status = media_status
self.dynamic_group_media_status_received = dt_util.utcnow()
self.schedule_update_ha_state()
def new_dynamic_group_connection_status(self, connection_status):
"""Handle updates of connection status."""
_LOGGER.debug(
"[%s %s (%s:%s)] Received dynamic group connection status: %s",
self.entity_id,
self._cast_info.friendly_name,
self._cast_info.host,
self._cast_info.port,
connection_status.status,
)
if connection_status.status == CONNECTION_STATUS_DISCONNECTED:
self._dynamic_group_available = False
self._dynamic_group_invalidate()
self.schedule_update_ha_state()
return
new_available = connection_status.status == CONNECTION_STATUS_CONNECTED
if new_available != self._dynamic_group_available:
# Connection status callbacks happen often when disconnected.
# Only update state when availability changed to put less pressure
# on state machine.
_LOGGER.debug(
"[%s %s (%s:%s)] Dynamic group availability changed: %s",
self.entity_id,
self._cast_info.friendly_name,
self._cast_info.host,
self._cast_info.port,
connection_status.status,
)
self._dynamic_group_available = new_available
self.schedule_update_ha_state()
def multizone_new_media_status(self, group_uuid, media_status):
"""Handle updates of audio group media status."""
_LOGGER.debug(
@ -573,18 +430,11 @@ class CastDevice(MediaPlayerDevice):
"""
Return media status.
First try from our own cast, then dynamic groups and finally
groups which our cast is a member in.
First try from our own cast, then groups which our cast is a member in.
"""
media_status = self.media_status
media_controller = self._chromecast.media_controller
if (
media_status is None or media_status.player_state == "UNKNOWN"
) and self._dynamic_group_cast is not None:
media_status = self.dynamic_group_media_status
media_controller = self._dynamic_group_cast.media_controller
if media_status is None or media_status.player_state == "UNKNOWN":
groups = self.mz_media_status
for k, val in groups.items():
@ -652,7 +502,7 @@ class CastDevice(MediaPlayerDevice):
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL."""
# We do not want this to be forwarded to a group / dynamic group
# We do not want this to be forwarded to a group
self._chromecast.media_controller.play_media(media_id, media_type)
# ========== Properties ==========
@ -685,18 +535,11 @@ class CastDevice(MediaPlayerDevice):
"""
Return media status.
First try from our own cast, then dynamic groups and finally
groups which our cast is a member in.
First try from our own cast, then groups which our cast is a member in.
"""
media_status = self.media_status
media_status_received = self.media_status_received
if (
media_status is None or media_status.player_state == "UNKNOWN"
) and self._dynamic_group_cast is not None:
media_status = self.dynamic_group_media_status
media_status_received = self.dynamic_group_media_status_received
if media_status is None or media_status.player_state == "UNKNOWN":
groups = self.mz_media_status
for k, val in groups.items():
@ -887,11 +730,6 @@ class CastDevice(MediaPlayerDevice):
# We can't handle empty UUIDs
return
if self._cast_info.same_dynamic_group(discover):
_LOGGER.debug("Discovered matching dynamic group: %s", discover)
await self.async_set_dynamic_group(discover)
return
if self._cast_info.uuid != discover.uuid:
# Discovered is not our device.
return
@ -915,14 +753,6 @@ class CastDevice(MediaPlayerDevice):
# We can't handle empty UUIDs
return
if (
self._dynamic_group_cast_info is not None
and self._dynamic_group_cast_info.uuid == discover.uuid
):
_LOGGER.debug("Removed matching dynamic group: %s", discover)
await self.async_del_dynamic_group()
return
if self._cast_info.uuid != discover.uuid:
# Removed is not our device.
return

View File

@ -435,58 +435,6 @@ async def test_group_media_states(hass: HomeAssistantType):
assert state.state == "playing"
async def test_dynamic_group_media_states(hass: HomeAssistantType):
"""Test media states are read from group if entity has no state."""
info = get_fake_chromecast_info()
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,
):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
entity.schedule_update_ha_state()
await hass.async_block_till_done()
state = hass.states.get("media_player.speaker")
assert state is not None
assert state.name == "Speaker"
assert state.state == "unknown"
assert entity.unique_id == full_info.uuid
group_media_status = MagicMock(images=None)
player_media_status = MagicMock(images=None)
# Player has no state, dynamic group is playing -> Should report 'playing'
entity._dynamic_group_cast = MagicMock()
group_media_status.player_is_playing = True
entity.new_dynamic_group_media_status(group_media_status)
await hass.async_block_till_done()
state = hass.states.get("media_player.speaker")
assert state.state == "playing"
# Player is paused, dynamic group is playing -> Should report 'paused'
player_media_status.player_is_playing = False
player_media_status.player_is_paused = True
entity.new_media_status(player_media_status)
await hass.async_block_till_done()
await hass.async_block_till_done()
state = hass.states.get("media_player.speaker")
assert state.state == "paused"
# Player is in unknown state, dynamic group is playing -> Should report
# 'playing'
player_media_status.player_state = "UNKNOWN"
entity.new_media_status(player_media_status)
await hass.async_block_till_done()
state = hass.states.get("media_player.speaker")
assert state.state == "playing"
async def test_group_media_control(hass: HomeAssistantType):
"""Test media states are read from group if entity has no state."""
info = get_fake_chromecast_info()
@ -543,61 +491,6 @@ async def test_group_media_control(hass: HomeAssistantType):
assert chromecast.media_controller.play_media.called
async def test_dynamic_group_media_control(hass: HomeAssistantType):
"""Test media states are read from group if entity has no state."""
info = get_fake_chromecast_info()
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,
):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
entity.schedule_update_ha_state()
entity._dynamic_group_cast = MagicMock()
await hass.async_block_till_done()
state = hass.states.get("media_player.speaker")
assert state is not None
assert state.name == "Speaker"
assert state.state == "unknown"
assert entity.unique_id == full_info.uuid
group_media_status = MagicMock(images=None)
player_media_status = MagicMock(images=None)
# Player has no state, dynamic group is playing -> Should forward
group_media_status.player_is_playing = True
entity.new_dynamic_group_media_status(group_media_status)
entity.media_previous_track()
assert entity._dynamic_group_cast.media_controller.queue_prev.called
assert not chromecast.media_controller.queue_prev.called
# Player is paused, dynamic group is playing -> Should not forward
player_media_status.player_is_playing = False
player_media_status.player_is_paused = True
entity.new_media_status(player_media_status)
entity.media_next_track()
assert not entity._dynamic_group_cast.media_controller.queue_next.called
assert chromecast.media_controller.queue_next.called
# Player is in unknown state, dynamic group is playing -> Should forward
player_media_status.player_state = "UNKNOWN"
entity.new_media_status(player_media_status)
entity.media_seek(None)
assert entity._dynamic_group_cast.media_controller.seek.called
assert not chromecast.media_controller.seek.called
# Verify play_media is not forwarded
entity.play_media(None, None)
assert not entity._dynamic_group_cast.media_controller.play_media.called
assert chromecast.media_controller.play_media.called
async def test_disconnect_on_stop(hass: HomeAssistantType):
"""Test cast device disconnects socket on stop."""
info = get_fake_chromecast_info()