mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Remove support for cast dynamic speaker groups (#33884)
This commit is contained in:
parent
496da8823d
commit
6d3046cb42
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user