# pylint: disable=too-many-instance-attributes
# Disable while we have Python 2.x compatability
# pylint: disable=useless-object-inheritance
"""Functionality to support saving and restoring the current Sonos state.
This is useful for scenarios such as when you want to switch to radio
or an announcement and then back again to what was playing previously.
Warning:
Sonos has introduced control via Amazon Alexa. A new cloud queue is
created and at present there appears no way to restart this
queue from snapshot. Currently if a cloud queue was playing it will
not restart.
Warning:
This class is designed to be created used and destroyed. It is not
designed to be reused or long lived. The init sets up defaults for
one use.
"""
[docs]class Snapshot:
"""A snapshot of the current state.
Note:
This does not change anything to do with the configuration
such as which group the speaker is in, just settings that impact
what is playing, or how it is played.
List of sources that may be playing using root of media_uri:
| ``x-rincon-queue``: playing from Queue
| ``x-sonosapi-stream``: playing a stream (eg radio)
| ``x-file-cifs``: playing file
| ``x-rincon``: slave zone (only change volume etc. rest from
coordinator)
"""
def __init__(self, device, snapshot_queue=False):
"""
Args:
device (SoCo): The device to snapshot
snapshot_queue (bool): Whether the queue should be snapshotted.
Defaults to `False`.
Warning:
It is strongly advised that you do not snapshot the queue unless
you really need to as it takes a very long time to restore large
queues as it is done one track at a time.
"""
# The device that will be snapshotted
self.device = device
# The values that will be stored
# For all zones:
self.media_uri = None
self.is_coordinator = False
self.is_playing_queue = False
self.is_playing_cloud_queue = False
self.volume = None
self.mute = None
self.bass = None
self.treble = None
self.loudness = None
# For coordinator zone playing from Queue:
self.play_mode = None
self.cross_fade = None
self.playlist_position = 0
self.track_position = None
# For coordinator zone playing a Stream:
self.media_metadata = None
# For all coordinator zones
self.transport_state = None
self.queue = None
# Only set the queue as a list if we are going to save it
if snapshot_queue:
self.queue = []
[docs] def snapshot(self):
"""Record and store the current state of a device.
Returns:
bool: `True` if the device is a coordinator, `False` otherwise.
Useful for determining whether playing an alert on a device
will ungroup it.
"""
# get if device coordinator (or slave) True (or False)
self.is_coordinator = self.device.is_coordinator
# Get information about the currently playing media
media_info = self.device.avTransport.GetMediaInfo([("InstanceID", 0)])
self.media_uri = media_info["CurrentURI"]
# Extract source from media uri - below some media URI value examples:
# 'x-rincon-queue:RINCON_000E5859E49601400#0'
# - playing a local queue always #0 for local queue)
#
# 'x-rincon-queue:RINCON_000E5859E49601400#6'
# - playing a cloud queue where #x changes with each queue)
#
# -'x-rincon:RINCON_000E5859E49601400'
# - a slave player pointing to coordinator player
if self.media_uri.split(":")[0] == "x-rincon-queue":
# The pylint error below is a false positive, see about removing it
# in the future
# pylint: disable=simplifiable-if-statement
if self.media_uri.split("#")[1] == "0":
# playing local queue
self.is_playing_queue = True
else:
# playing cloud queue - started from Alexa
self.is_playing_cloud_queue = True
# Save the volume, mute and other sound settings
self.volume = self.device.volume
self.mute = self.device.mute
self.bass = self.device.bass
self.treble = self.device.treble
self.loudness = self.device.loudness
# get details required for what's playing:
if self.is_playing_queue:
# playing from queue - save repeat, random, cross fade, track, etc.
self.play_mode = self.device.play_mode
self.cross_fade = self.device.cross_fade
# Get information about the currently playing track
track_info = self.device.get_current_track_info()
if track_info is not None:
position = track_info["playlist_position"]
if position != "":
# save as integer
self.playlist_position = int(position)
self.track_position = track_info["position"]
else:
# playing from a stream - save media metadata
self.media_metadata = media_info["CurrentURIMetaData"]
# Work out what the playing state is - if a coordinator
if self.is_coordinator:
transport_info = self.device.get_current_transport_info()
if transport_info is not None:
self.transport_state = transport_info["current_transport_state"]
# Save of the current queue if we need to
self._save_queue()
# return if device is a coordinator (helps usage)
return self.is_coordinator
# pylint: disable=too-many-branches
[docs] def restore(self, fade=False):
"""Restore the state of a device to that which was previously saved.
For coordinator devices restore everything. For slave devices
only restore volume etc., not transport info (transport info
comes from the slave's coordinator).
Args:
fade (bool): Whether volume should be faded up on restore.
"""
try:
if self.is_coordinator:
self._restore_coordinator()
finally:
self._restore_volume(fade)
# Now everything is set, see if we need to be playing, stopped
# or paused ( only for coordinators)
if self.is_coordinator:
if self.transport_state == "PLAYING":
self.device.play()
elif self.transport_state == "STOPPED":
self.device.stop()
def _restore_coordinator(self):
"""Do the coordinator-only part of the restore."""
# Start by ensuring that the speaker is paused as we don't want
# things all rolling back when we are changing them, as this could
# include things like audio
transport_info = self.device.get_current_transport_info()
if transport_info is not None:
if transport_info["current_transport_state"] == "PLAYING":
self.device.pause()
# Check if the queue should be restored
self._restore_queue()
# Reinstate what was playing
if self.is_playing_queue and self.playlist_position > 0:
# was playing from playlist
if self.playlist_position is not None:
# The position in the playlist returned by
# get_current_track_info starts at 1, but when
# playing from playlist, the index starts at 0
# if position > 0:
self.playlist_position -= 1
self.device.play_from_queue(self.playlist_position, False)
if self.track_position is not None:
if self.track_position != "":
self.device.seek(self.track_position)
# reinstate track, position, play mode, cross fade
# Need to make sure there is a proper track selected first
self.device.play_mode = self.play_mode
self.device.cross_fade = self.cross_fade
elif self.is_playing_cloud_queue:
# was playing a cloud queue started by Alexa
# No way yet to re-start this so prevent it throwing an error!
pass
else:
# was playing a stream (radio station, file, or nothing)
# reinstate uri and meta data
if self.media_uri != "":
self.device.play_uri(self.media_uri, self.media_metadata, start=False)
def _restore_volume(self, fade):
"""Reinstate volume.
Args:
fade (bool): Whether volume should be faded up on restore.
"""
self.device.mute = self.mute
# Can only change volume on device with fixed volume set to False
# otherwise get uPnP error, so check first. Before issuing a network
# command to check, fixed volume always has volume set to 100.
# So only checked fixed volume if volume is 100.
if self.volume == 100:
fixed_vol = self.device.fixed_volume
else:
fixed_vol = False
# now set volume if not fixed
if not fixed_vol:
self.device.bass = self.bass
self.device.treble = self.treble
self.device.loudness = self.loudness
if fade:
# if fade requested in restore
# set volume to 0 then fade up to saved volume (non blocking)
self.device.volume = 0
self.device.ramp_to_volume(self.volume)
else:
# set volume
self.device.volume = self.volume
def _save_queue(self):
"""Save the current state of the queue."""
if self.queue is not None:
# Maximum batch is 486, anything larger will still only
# return 486
batch_size = 400
total = 0
num_return = batch_size
# Need to get all the tracks in batches, but Only get the next
# batch if all the items requested were in the last batch
while num_return == batch_size:
queue_items = self.device.get_queue(total, batch_size)
# Check how many entries were returned
num_return = len(queue_items)
# Make sure the queue is not empty
if num_return > 0:
self.queue.append(queue_items)
# Update the total that have been processed
total = total + num_return
def _restore_queue(self):
"""Restore the previous state of the queue.
Note:
The restore currently adds the items back into the queue
using the URI, for items the Sonos system already knows about
this is OK, but for other items, they may be missing some of
their metadata as it will not be automatically picked up.
"""
if self.queue is not None:
# Clear the queue so that it can be reset
self.device.clear_queue()
# Now loop around all the queue entries adding them
for queue_group in self.queue:
for queue_item in queue_group:
self.device.add_uri_to_queue(queue_item.uri)
def __enter__(self):
self.snapshot()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.restore()