diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index ee2f9e6b4dc..4a02cf2676f 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -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 diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index d57c730a9f8..55743c4f843 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -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()