Fix Sonos snapshot/restore (#21411)

This commit is contained in:
Anders Melchiorsen 2019-02-25 22:03:15 +01:00 committed by Paulus Schoutsen
parent 4e9d0ebc63
commit 095a0d19d1
2 changed files with 116 additions and 109 deletions

View File

@ -195,35 +195,30 @@ def _setup_platform(hass, config, add_entities, discovery_info):
if entity_ids:
entities = [e for e in entities if e.entity_id in entity_ids]
if service.service == SERVICE_JOIN:
master = [e for e in hass.data[DATA_SONOS].entities
if e.entity_id == service.data[ATTR_MASTER]]
if master:
with hass.data[DATA_SONOS].topology_lock:
master[0].join(entities)
return
if service.service == SERVICE_UNJOIN:
with hass.data[DATA_SONOS].topology_lock:
for entity in entities:
entity.unjoin()
return
for entity in entities:
with hass.data[DATA_SONOS].topology_lock:
if service.service == SERVICE_SNAPSHOT:
entity.snapshot(service.data[ATTR_WITH_GROUP])
snapshot(entities, service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_RESTORE:
entity.restore(service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_SET_TIMER:
entity.set_sleep_timer(service.data[ATTR_SLEEP_TIME])
elif service.service == SERVICE_CLEAR_TIMER:
entity.clear_sleep_timer()
elif service.service == SERVICE_UPDATE_ALARM:
entity.set_alarm(**service.data)
elif service.service == SERVICE_SET_OPTION:
entity.set_option(**service.data)
restore(entities, service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_JOIN:
master = [e for e in hass.data[DATA_SONOS].entities
if e.entity_id == service.data[ATTR_MASTER]]
if master:
master[0].join(entities)
else:
for entity in entities:
if service.service == SERVICE_UNJOIN:
entity.unjoin()
elif service.service == SERVICE_SET_TIMER:
entity.set_sleep_timer(service.data[ATTR_SLEEP_TIME])
elif service.service == SERVICE_CLEAR_TIMER:
entity.clear_sleep_timer()
elif service.service == SERVICE_UPDATE_ALARM:
entity.set_alarm(**service.data)
elif service.service == SERVICE_SET_OPTION:
entity.set_option(**service.data)
entity.schedule_update_ha_state(True)
entity.schedule_update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_JOIN, service_handle,
@ -346,7 +341,7 @@ class SonosEntity(MediaPlayerDevice):
self._shuffle = None
self._name = None
self._coordinator = None
self._sonos_group = None
self._sonos_group = [self]
self._status = None
self._media_duration = None
self._media_position = None
@ -375,6 +370,10 @@ class SonosEntity(MediaPlayerDevice):
"""Return a unique ID."""
return self._unique_id
def __hash__(self):
"""Return a hash of self."""
return hash(self.unique_id)
@property
def name(self):
"""Return the name of the entity."""
@ -729,7 +728,7 @@ class SonosEntity(MediaPlayerDevice):
for uid in (coordinator_uid, *slave_uids):
entity = _get_entity_from_soco_uid(self.hass, uid)
if entity:
sonos_group.append(entity.entity_id)
sonos_group.append(entity)
self._coordinator = None
self._sonos_group = sonos_group
@ -975,72 +974,6 @@ class SonosEntity(MediaPlayerDevice):
self.soco.unjoin()
self._coordinator = None
@soco_error()
def snapshot(self, with_group=True):
"""Snapshot the player."""
from pysonos.snapshot import Snapshot
self._soco_snapshot = Snapshot(self.soco)
self._soco_snapshot.snapshot()
if with_group:
self._snapshot_group = self.soco.group
if self._coordinator:
self._coordinator.snapshot(False)
else:
self._snapshot_group = None
@soco_error()
def restore(self, with_group=True):
"""Restore snapshot for the player."""
from pysonos.exceptions import SoCoException
try:
# need catch exception if a coordinator is going to slave.
# this state will recover with group part.
self._soco_snapshot.restore(False)
except (TypeError, AttributeError, SoCoException):
_LOGGER.debug("Error on restore %s", self.entity_id)
# restore groups
if with_group and self._snapshot_group:
old = self._snapshot_group
actual = self.soco.group
##
# Master have not change, update group
if old.coordinator == actual.coordinator:
if self.soco is not old.coordinator:
# restore state of the groups
self._coordinator.restore(False)
remove = actual.members - old.members
add = old.members - actual.members
# remove new members
for soco_dev in list(remove):
soco_dev.unjoin()
# add old members
for soco_dev in list(add):
soco_dev.join(old.coordinator)
return
##
# old is already master, rejoin
if old.coordinator.group.coordinator == old.coordinator:
self.soco.join(old.coordinator)
return
##
# restore old master, update group
old.coordinator.unjoin()
coordinator = _get_entity_from_soco_uid(
self.hass, old.coordinator.uid)
coordinator.restore(False)
for s_dev in list(old.members):
if s_dev != old.coordinator:
s_dev.join(old.coordinator)
@soco_error()
@soco_coordinator
def set_sleep_timer(self, sleep_time):
@ -1089,7 +1022,9 @@ class SonosEntity(MediaPlayerDevice):
@property
def device_state_attributes(self):
"""Return entity specific state attributes."""
attributes = {ATTR_SONOS_GROUP: self._sonos_group}
attributes = {
ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group],
}
if self._night_sound is not None:
attributes[ATTR_NIGHT_SOUND] = self._night_sound
@ -1098,3 +1033,62 @@ class SonosEntity(MediaPlayerDevice):
attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance
return attributes
@soco_error()
def snapshot(entities, with_group):
"""Snapshot all the entities and optionally their groups."""
# pylint: disable=protected-access
from pysonos.snapshot import Snapshot
# Find all affected players
entities = set(entities)
if with_group:
for entity in list(entities):
entities.update(entity._sonos_group)
for entity in entities:
entity._soco_snapshot = Snapshot(entity.soco)
entity._soco_snapshot.snapshot()
if with_group:
entity._snapshot_group = entity._sonos_group.copy()
else:
entity._snapshot_group = None
@soco_error()
def restore(entities, with_group):
"""Restore snapshots for all the entities."""
# pylint: disable=protected-access
from pysonos.exceptions import SoCoException
# Find all affected players
entities = set(e for e in entities if e._soco_snapshot)
if with_group:
for entity in [e for e in entities if e._snapshot_group]:
entities.update(entity._snapshot_group)
# Pause all current coordinators
for entity in (e for e in entities if e.is_coordinator):
if entity.state == STATE_PLAYING:
entity.media_pause()
# Bring back the original group topology and clear pysonos cache
if with_group:
for entity in (e for e in entities if e._snapshot_group):
if entity._snapshot_group[0] == entity:
entity.join(entity._snapshot_group)
entity.soco._zgs_cache.clear()
# Restore slaves, then coordinators
slaves = [e for e in entities if not e.is_coordinator]
coordinators = [e for e in entities if e.is_coordinator]
for entity in slaves + coordinators:
try:
entity._soco_snapshot.restore()
except (TypeError, AttributeError, SoCoException) as ex:
# Can happen if restoring a coordinator onto a current slave
_LOGGER.warning("Error on restore %s: %s", entity.entity_id, ex)
entity._soco_snapshot = None
entity._snapshot_group = None

View File

@ -49,6 +49,14 @@ class MusicLibraryMock():
return []
class CacheMock():
"""Mock class for the _zgs_cache property on pysonos.SoCo object."""
def clear(self):
"""Clear cache."""
pass
class SoCoMock():
"""Mock class for the pysonos.SoCo object."""
@ -63,6 +71,7 @@ class SoCoMock():
self.dialog_mode = False
self.music_library = MusicLibraryMock()
self.avTransport = AvTransportMock()
self._zgs_cache = CacheMock()
def get_sonos_favorites(self):
"""Get favorites list from sonos."""
@ -126,7 +135,7 @@ def add_entities_factory(hass):
"""Add entities factory."""
def add_entities(entities, update_befor_add=False):
"""Fake add entity."""
hass.data[sonos.DATA_SONOS].entities = entities
hass.data[sonos.DATA_SONOS].entities = list(entities)
return add_entities
@ -162,7 +171,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
'host': '192.0.2.1'
})
entities = list(self.hass.data[sonos.DATA_SONOS].entities)
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 1
assert entities[0].name == 'Kitchen'
@ -242,7 +251,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
def test_ensure_setup_sonos_discovery(self, *args):
"""Test a single device using the autodiscovery provided by Sonos."""
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass))
entities = list(self.hass.data[sonos.DATA_SONOS].entities)
entities = self.hass.data[sonos.DATA_SONOS].entities
assert len(entities) == 1
assert entities[0].name == 'Kitchen'
@ -254,7 +263,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entity = self.hass.data[sonos.DATA_SONOS].entities[-1]
entity.hass = self.hass
entity.set_sleep_timer(30)
@ -268,7 +277,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entity = self.hass.data[sonos.DATA_SONOS].entities[-1]
entity.hass = self.hass
entity.set_sleep_timer(None)
@ -282,7 +291,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entity = self.hass.data[sonos.DATA_SONOS].entities[-1]
entity.hass = self.hass
alarm1 = alarms.Alarm(pysonos_mock)
alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False,
@ -312,11 +321,14 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entities = self.hass.data[sonos.DATA_SONOS].entities
entity = entities[-1]
entity.hass = self.hass
snapshotMock.return_value = True
entity.snapshot()
entity.soco.group = mock.MagicMock()
entity.soco.group.members = [e.soco for e in entities]
sonos.snapshot(entities, True)
assert snapshotMock.call_count == 1
assert snapshotMock.call_args == mock.call()
@ -330,13 +342,14 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), {
'host': '192.0.2.1'
})
entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1]
entities = self.hass.data[sonos.DATA_SONOS].entities
entity = entities[-1]
entity.hass = self.hass
restoreMock.return_value = True
entity._snapshot_coordinator = mock.MagicMock()
entity._snapshot_coordinator.soco_entity = SoCoMock('192.0.2.17')
entity._soco_snapshot = Snapshot(entity._player)
entity.restore()
entity._snapshot_group = mock.MagicMock()
entity._snapshot_group.members = [e.soco for e in entities]
entity._soco_snapshot = Snapshot(entity.soco)
sonos.restore(entities, True)
assert restoreMock.call_count == 1
assert restoreMock.call_args == mock.call(False)
assert restoreMock.call_args == mock.call()