mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Fix Sonos snapshot/restore (#21411)
This commit is contained in:
parent
4e9d0ebc63
commit
095a0d19d1
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user