Wait for Sonos regrouping in service calls (#22006)

This commit is contained in:
Anders Melchiorsen 2019-03-13 20:51:41 +01:00 committed by Paulus Schoutsen
parent fe5e4b5b9b
commit de2c7a9567

View File

@ -6,6 +6,7 @@ import socket
import asyncio import asyncio
import urllib import urllib
import async_timeout
import requests import requests
import voluptuous as vol import voluptuous as vol
@ -115,7 +116,7 @@ class SonosData:
"""Initialize the data.""" """Initialize the data."""
self.uids = set() self.uids = set()
self.entities = [] self.entities = []
self.topology_lock = asyncio.Lock(loop=hass.loop) self.topology_condition = asyncio.Condition(loop=hass.loop)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
@ -364,7 +365,6 @@ class SonosEntity(MediaPlayerDevice):
self._favorites = None self._favorites = None
self._soco_snapshot = None self._soco_snapshot = None
self._snapshot_group = None self._snapshot_group = None
self._restore_pending = False
self._set_basic_information() self._set_basic_information()
@ -752,15 +752,14 @@ class SonosEntity(MediaPlayerDevice):
async def _async_handle_group_event(event): async def _async_handle_group_event(event):
"""Get async lock and handle event.""" """Get async lock and handle event."""
async with self.hass.data[DATA_SONOS].topology_lock: async with self.hass.data[DATA_SONOS].topology_condition:
group = await _async_extract_group(event) group = await _async_extract_group(event)
if self.unique_id == group[0]: if self.unique_id == group[0]:
if self._restore_pending:
await self.hass.async_add_executor_job(self.restore)
_async_regroup(group) _async_regroup(group)
self.hass.data[DATA_SONOS].topology_condition.notify_all()
if event: if event:
self._receives_events = True self._receives_events = True
@ -988,18 +987,26 @@ class SonosEntity(MediaPlayerDevice):
"""Form a group with other players.""" """Form a group with other players."""
if self._coordinator: if self._coordinator:
self.unjoin() self.unjoin()
group = [self]
else:
group = self._sonos_group.copy()
for slave in slaves: for slave in slaves:
if slave.unique_id != self.unique_id: if slave.unique_id != self.unique_id:
slave.soco.join(self.soco) slave.soco.join(self.soco)
# pylint: disable=protected-access # pylint: disable=protected-access
slave._coordinator = self slave._coordinator = self
if slave not in group:
group.append(slave)
return group
@staticmethod @staticmethod
async def join_multi(hass, master, entities): async def join_multi(hass, master, entities):
"""Form a group with other players.""" """Form a group with other players."""
async with hass.data[DATA_SONOS].topology_lock: async with hass.data[DATA_SONOS].topology_condition:
await hass.async_add_executor_job(master.join, entities) group = await hass.async_add_executor_job(master.join, entities)
await SonosEntity.wait_for_groups(hass, [group])
@soco_error() @soco_error()
def unjoin(self): def unjoin(self):
@ -1019,8 +1026,9 @@ class SonosEntity(MediaPlayerDevice):
for entity in slaves + coordinators: for entity in slaves + coordinators:
entity.unjoin() entity.unjoin()
async with hass.data[DATA_SONOS].topology_lock: async with hass.data[DATA_SONOS].topology_condition:
await hass.async_add_executor_job(_unjoin_all, entities) await hass.async_add_executor_job(_unjoin_all, entities)
await SonosEntity.wait_for_groups(hass, [[e] for e in entities])
@soco_error() @soco_error()
def snapshot(self, with_group): def snapshot(self, with_group):
@ -1050,7 +1058,7 @@ class SonosEntity(MediaPlayerDevice):
for entity in list(entities): for entity in list(entities):
entities.update(entity._sonos_group) entities.update(entity._sonos_group)
async with hass.data[DATA_SONOS].topology_lock: async with hass.data[DATA_SONOS].topology_condition:
await hass.async_add_executor_job(_snapshot_all, entities) await hass.async_add_executor_job(_snapshot_all, entities)
@soco_error() @soco_error()
@ -1060,7 +1068,6 @@ class SonosEntity(MediaPlayerDevice):
try: try:
# pylint: disable=protected-access # pylint: disable=protected-access
self.soco._zgs_cache.clear()
self._soco_snapshot.restore() self._soco_snapshot.restore()
except (TypeError, AttributeError, SoCoException) as ex: except (TypeError, AttributeError, SoCoException) as ex:
# Can happen if restoring a coordinator onto a current slave # Can happen if restoring a coordinator onto a current slave
@ -1068,20 +1075,20 @@ class SonosEntity(MediaPlayerDevice):
self._soco_snapshot = None self._soco_snapshot = None
self._snapshot_group = None self._snapshot_group = None
self._restore_pending = False
@staticmethod @staticmethod
async def restore_multi(hass, entities, with_group): async def restore_multi(hass, entities, with_group):
"""Restore snapshots for all the entities.""" """Restore snapshots for all the entities."""
# pylint: disable=protected-access # pylint: disable=protected-access
def _restore_all(entities): def _restore_groups(entities, with_group):
"""Sync helper.""" """Pause all current coordinators and restore groups."""
# Pause all current coordinators
for entity in (e for e in entities if e.is_coordinator): for entity in (e for e in entities if e.is_coordinator):
if entity.state == STATE_PLAYING: if entity.state == STATE_PLAYING:
entity.media_pause() entity.media_pause()
groups = []
if with_group: if with_group:
# Unjoin slaves first to prevent inheritance of queues # Unjoin slaves first to prevent inheritance of queues
for entity in [e for e in entities if not e.is_coordinator]: for entity in [e for e in entities if not e.is_coordinator]:
@ -1092,19 +1099,17 @@ class SonosEntity(MediaPlayerDevice):
for entity in (e for e in entities if e._snapshot_group): for entity in (e for e in entities if e._snapshot_group):
if entity._snapshot_group[0] == entity: if entity._snapshot_group[0] == entity:
entity.join(entity._snapshot_group) entity.join(entity._snapshot_group)
groups.append(entity._snapshot_group.copy())
# Restore slaves return groups
def _restore_players(entities):
"""Restore state of all players."""
for entity in (e for e in entities if not e.is_coordinator): for entity in (e for e in entities if not e.is_coordinator):
entity.restore() entity.restore()
# Restore coordinators (or delay if moving from slave)
for entity in (e for e in entities if e.is_coordinator): for entity in (e for e in entities if e.is_coordinator):
if entity._sonos_group[0] == entity: entity.restore()
# Was already coordinator
entity.restore()
else:
# Await coordinator role
entity._restore_pending = True
# Find all affected players # Find all affected players
entities = set(e for e in entities if e._soco_snapshot) entities = set(e for e in entities if e._soco_snapshot)
@ -1112,8 +1117,44 @@ class SonosEntity(MediaPlayerDevice):
for entity in [e for e in entities if e._snapshot_group]: for entity in [e for e in entities if e._snapshot_group]:
entities.update(entity._snapshot_group) entities.update(entity._snapshot_group)
async with hass.data[DATA_SONOS].topology_lock: async with hass.data[DATA_SONOS].topology_condition:
await hass.async_add_executor_job(_restore_all, entities) groups = await hass.async_add_executor_job(
_restore_groups, entities, with_group)
await SonosEntity.wait_for_groups(hass, groups)
await hass.async_add_executor_job(_restore_players, entities)
@staticmethod
async def wait_for_groups(hass, groups):
"""Wait until all groups are present, or timeout."""
# pylint: disable=protected-access
def _test_groups(groups):
"""Return whether all groups exist now."""
for group in groups:
coordinator = group[0]
# Test that coordinator is coordinating
current_group = coordinator._sonos_group
if coordinator != current_group[0]:
return False
# Test that slaves match
if set(group[1:]) != set(current_group[1:]):
return False
return True
try:
with async_timeout.timeout(5):
while not _test_groups(groups):
await hass.data[DATA_SONOS].topology_condition.wait()
except asyncio.TimeoutError:
_LOGGER.warning("Timeout waiting for target groups %s", groups)
for entity in hass.data[DATA_SONOS].entities:
entity.soco._zgs_cache.clear()
@soco_error() @soco_error()
@soco_coordinator @soco_coordinator