text: """
  Support to interface with Sonos players (via SoCo).
  
  For more details about this platform, please refer to the documentation at
  https://home-assistant.io/components/media_player.sonos/
  """
  import asyncio
  import datetime
  import functools as ft
  import logging
  from os import path
  import socket
  import urllib
  
  import voluptuous as vol
  
  from homeassistant.components.media_player import (
      ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
      SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
      SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
      SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP,
      SUPPORT_PLAY, SUPPORT_SHUFFLE_SET)
  from homeassistant.const import (
      STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
      CONF_HOSTS, ATTR_TIME)
  from homeassistant.config import load_yaml_config_file
  import homeassistant.helpers.config_validation as cv
  from homeassistant.util.dt import utcnow
  
  REQUIREMENTS = ['SoCo==0.13']
  
  _LOGGER = logging.getLogger(__name__)
  
  # The soco library is excessively chatty when it comes to logging and
  # causes a LOT of spam in the logs due to making a http connection to each
  # speaker every 10 seconds. Quiet it down a bit to just actual problems.
  _SOCO_LOGGER = logging.getLogger('soco')
  _SOCO_LOGGER.setLevel(logging.ERROR)
  _SOCO_SERVICES_LOGGER = logging.getLogger('soco.services')
  _REQUESTS_LOGGER = logging.getLogger('requests')
  _REQUESTS_LOGGER.setLevel(logging.ERROR)
  
  SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\
      SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
      SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\
      SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET
  
  SERVICE_JOIN = 'sonos_join'
  SERVICE_UNJOIN = 'sonos_unjoin'
  SERVICE_SNAPSHOT = 'sonos_snapshot'
  SERVICE_RESTORE = 'sonos_restore'
  SERVICE_SET_TIMER = 'sonos_set_sleep_timer'
  SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
  SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
  SERVICE_SET_OPTION = 'sonos_set_option'
  
  DATA_SONOS = 'sonos'
  
  SUPPORT_SOURCE_LINEIN = 'Line-in'
  SUPPORT_SOURCE_TV = 'TV'
  
  CONF_ADVERTISE_ADDR = 'advertise_addr'
  CONF_INTERFACE_ADDR = 'interface_addr'
  
  # Service call validation schemas
  ATTR_SLEEP_TIME = 'sleep_time'
  ATTR_ALARM_ID = 'alarm_id'
  ATTR_VOLUME = 'volume'
  ATTR_ENABLED = 'enabled'
  ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones'
  ATTR_MASTER = 'master'
  ATTR_WITH_GROUP = 'with_group'
  ATTR_NIGHT_SOUND = 'night_sound'
  ATTR_SPEECH_ENHANCE = 'speech_enhance'
  
  ATTR_IS_COORDINATOR = 'is_coordinator'
  
  UPNP_ERRORS_TO_IGNORE = ['701']
  
  PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
      vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
      vol.Optional(CONF_INTERFACE_ADDR): cv.string,
      vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]),
  })
  
  SONOS_SCHEMA = vol.Schema({
      vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
  })
  
  SONOS_JOIN_SCHEMA = SONOS_SCHEMA.extend({
      vol.Required(ATTR_MASTER): cv.entity_id,
  })
  
  SONOS_STATES_SCHEMA = SONOS_SCHEMA.extend({
      vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean,
  })
  
  SONOS_SET_TIMER_SCHEMA = SONOS_SCHEMA.extend({
      vol.Required(ATTR_SLEEP_TIME):
          vol.All(vol.Coerce(int), vol.Range(min=0, max=86399))
  })
  
  SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({
      vol.Required(ATTR_ALARM_ID): cv.positive_int,
      vol.Optional(ATTR_TIME): cv.time,
      vol.Optional(ATTR_VOLUME): cv.small_float,
      vol.Optional(ATTR_ENABLED): cv.boolean,
      vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean,
  })
  
  SONOS_SET_OPTION_SCHEMA = SONOS_SCHEMA.extend({
      vol.Optional(ATTR_NIGHT_SOUND): cv.boolean,
      vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean,
  })
  
  
  def setup_platform(hass, config, add_devices, discovery_info=None):
      """Set up the Sonos platform."""
      import soco
  
      if DATA_SONOS not in hass.data:
          hass.data[DATA_SONOS] = []
  
      advertise_addr = config.get(CONF_ADVERTISE_ADDR, None)
      if advertise_addr:
          soco.config.EVENT_ADVERTISE_IP = advertise_addr
  
      if discovery_info:
          player = soco.SoCo(discovery_info.get('host'))
  
          # if device already exists by config
          if player.uid in [x.unique_id for x in hass.data[DATA_SONOS]]:
              return
  
          if player.is_visible:
              device = SonosDevice(player)
              add_devices([device], True)
              hass.data[DATA_SONOS].append(device)
              if len(hass.data[DATA_SONOS]) > 1:
                  return
      else:
          players = None
          hosts = config.get(CONF_HOSTS, None)
          if hosts:
              # Support retro compatibility with comma separated list of hosts
              # from config
              hosts = hosts[0] if len(hosts) == 1 else hosts
              hosts = hosts.split(',') if isinstance(hosts, str) else hosts
              players = []
              for host in hosts:
                  try:
                      players.append(soco.SoCo(socket.gethostbyname(host)))
                  except OSError:
                      _LOGGER.warning("Failed to initialize '%s'", host)
  
          if not players:
              players = soco.discover(
                  interface_addr=config.get(CONF_INTERFACE_ADDR))
  
          if not players:
              _LOGGER.warning("No Sonos speakers found")
              return
  
          # Add coordinators first so they can be queried by slaves
          coordinators = [SonosDevice(p) for p in players if p.is_coordinator]
          slaves = [SonosDevice(p) for p in players if not p.is_coordinator]
          hass.data[DATA_SONOS] = coordinators + slaves
          if coordinators:
              add_devices(coordinators, True)
          if slaves:
              add_devices(slaves, True)
          _LOGGER.info("Added %s Sonos speakers", len(players))
  
      descriptions = load_yaml_config_file(
          path.join(path.dirname(__file__), 'services.yaml'))
  
      def service_handle(service):
          """Handle for services."""
          entity_ids = service.data.get('entity_id')
  
          if entity_ids:
              devices = [device for device in hass.data[DATA_SONOS]
                         if device.entity_id in entity_ids]
          else:
              devices = hass.data[DATA_SONOS]
  
          for device in devices:
              if service.service == SERVICE_JOIN:
                  if device.entity_id != service.data[ATTR_MASTER]:
                      device.join(service.data[ATTR_MASTER])
              elif service.service == SERVICE_UNJOIN:
                  device.unjoin()
              elif service.service == SERVICE_SNAPSHOT:
                  device.snapshot(service.data[ATTR_WITH_GROUP])
              elif service.service == SERVICE_RESTORE:
                  device.restore(service.data[ATTR_WITH_GROUP])
              elif service.service == SERVICE_SET_TIMER:
                  device.set_sleep_timer(service.data[ATTR_SLEEP_TIME])
              elif service.service == SERVICE_CLEAR_TIMER:
                  device.clear_sleep_timer()
              elif service.service == SERVICE_UPDATE_ALARM:
                  device.update_alarm(**service.data)
              elif service.service == SERVICE_SET_OPTION:
                  device.update_option(**service.data)
  
              device.schedule_update_ha_state(True)
  
      hass.services.register(
          DOMAIN, SERVICE_JOIN, service_handle,
          descriptions.get(SERVICE_JOIN), schema=SONOS_JOIN_SCHEMA)
  
      hass.services.register(
          DOMAIN, SERVICE_UNJOIN, service_handle,
          descriptions.get(SERVICE_UNJOIN), schema=SONOS_SCHEMA)
  
      hass.services.register(
          DOMAIN, SERVICE_SNAPSHOT, service_handle,
          descriptions.get(SERVICE_SNAPSHOT), schema=SONOS_STATES_SCHEMA)
  
      hass.services.register(
          DOMAIN, SERVICE_RESTORE, service_handle,
          descriptions.get(SERVICE_RESTORE), schema=SONOS_STATES_SCHEMA)
  
      hass.services.register(
          DOMAIN, SERVICE_SET_TIMER, service_handle,
          descriptions.get(SERVICE_SET_TIMER), schema=SONOS_SET_TIMER_SCHEMA)
  
      hass.services.register(
          DOMAIN, SERVICE_CLEAR_TIMER, service_handle,
          descriptions.get(SERVICE_CLEAR_TIMER), schema=SONOS_SCHEMA)
  
      hass.services.register(
          DOMAIN, SERVICE_UPDATE_ALARM, service_handle,
          descriptions.get(SERVICE_UPDATE_ALARM),
          schema=SONOS_UPDATE_ALARM_SCHEMA)
  
      hass.services.register(
          DOMAIN, SERVICE_SET_OPTION, service_handle,
          descriptions.get(SERVICE_SET_OPTION),
          schema=SONOS_SET_OPTION_SCHEMA)
  
  
  def _parse_timespan(timespan):
      """Parse a time-span into number of seconds."""
      if timespan in ('', 'NOT_IMPLEMENTED', None):
          return None
  
      return sum(60 ** x[0] * int(x[1]) for x in enumerate(
          reversed(timespan.split(':'))))
  
  
  class _ProcessSonosEventQueue():
      """Queue like object for dispatching sonos events."""
  
      def __init__(self, sonos_device):
          self._sonos_device = sonos_device
  
      def put(self, item, block=True, timeout=None):
          """Queue up event for processing."""
          # Instead of putting events on a queue, dispatch them to the event
          # processing method.
          self._sonos_device.process_sonos_event(item)
  
  
  def _get_entity_from_soco(hass, soco):
      """Return SonosDevice from SoCo."""
      for device in hass.data[DATA_SONOS]:
          if soco == device.soco:
              return device
      raise ValueError("No entity for SoCo device")
  
  
  def soco_error(funct):
      """Catch soco exceptions."""
      @ft.wraps(funct)
      def wrapper(*args, **kwargs):
          """Wrap for all soco exception."""
          from soco.exceptions import SoCoException
          try:
              return funct(*args, **kwargs)
          except SoCoException as err:
              _LOGGER.error("Error on %s with %s", funct.__name__, err)
      return wrapper
  
  
  def soco_filter_upnperror(errorcodes=None):
      """Filter out specified UPnP errors from logs."""
      def decorator(funct):
          """Decorator function."""
          @ft.wraps(funct)
          def wrapper(*args, **kwargs):
              """Wrap for all soco UPnP exception."""
              from soco.exceptions import SoCoUPnPException
  
              # Temporarily disable SoCo logging because it will log the
              # UPnP exception otherwise
              _SOCO_SERVICES_LOGGER.disabled = True
  
              try:
                  return funct(*args, **kwargs)
              except SoCoUPnPException as err:
                  if err.error_code in errorcodes:
                      pass
                  else:
                      raise
              finally:
                  _SOCO_SERVICES_LOGGER.disabled = False
  
          return wrapper
      return decorator
  
  
  def soco_coordinator(funct):
      """Call function on coordinator."""
      @ft.wraps(funct)
      def wrapper(device, *args, **kwargs):
          """Wrap for call to coordinator."""
          if device.is_coordinator:
              return funct(device, *args, **kwargs)
          return funct(device.coordinator, *args, **kwargs)
  
      return wrapper
  
  
  class SonosDevice(MediaPlayerDevice):
      """Representation of a Sonos device."""
  
      def __init__(self, player):
          """Initialize the Sonos device."""
          self.volume_increment = 5
          self._unique_id = player.uid
          self._player = player
          self._player_volume = None
          self._player_volume_muted = None
          self._speaker_info = None
          self._name = None
          self._status = None
          self._coordinator = None
          self._media_content_id = None
          self._media_duration = None
          self._media_position = None
          self._media_position_updated_at = None
          self._media_image_url = None
          self._media_artist = None
          self._media_album_name = None
          self._media_title = None
          self._media_radio_show = None
          self._available = True
          self._support_previous_track = False
          self._support_next_track = False
          self._support_play = False
          self._support_shuffle_set = True
          self._support_stop = False
          self._support_pause = False
          self._night_sound = None
          self._speech_enhance = None
          self._current_track_uri = None
          self._current_track_is_radio_stream = False
          self._queue = None
          self._last_avtransport_event = None
          self._is_playing_line_in = None
          self._is_playing_tv = None
          self._favorite_sources = None
          self._source_name = None
          self._soco_snapshot = None
          self._snapshot_group = None
  
      @asyncio.coroutine
      def async_added_to_hass(self):
          """Subscribe sonos events."""
          self.hass.async_add_job(self._subscribe_to_player_events)
  
      @property
      def should_poll(self):
          """Return the polling state."""
          return True
  
      @property
      def unique_id(self):
          """Return an unique ID."""
          return self._unique_id
  
      @property
      def name(self):
          """Return the name of the device."""
          return self._name
  
      @property
      def state(self):
          """Return the state of the device."""
          if self._coordinator:
              return self._coordinator.state
          if self._status in ('PAUSED_PLAYBACK', 'STOPPED'):
              return STATE_PAUSED
          if self._status in ('PLAYING', 'TRANSITIONING'):
              return STATE_PLAYING
          if self._status == 'OFF':
              return STATE_OFF
          return STATE_IDLE
  
      @property
      def is_coordinator(self):
          """Return true if player is a coordinator."""
          return self._coordinator is None
  
      @property
      def soco(self):
          """Return soco device."""
          return self._player
  
      @property
      def coordinator(self):
          """Return coordinator of this player."""
          return self._coordinator
  
      @property
      def available(self) -> bool:
          """Return True if entity is available."""
          return self._available
  
      def _is_available(self):
          try:
              sock = socket.create_connection(
                  address=(self._player.ip_address, 1443), timeout=3)
              sock.close()
              return True
          except socket.error:
              return False
  
      # pylint: disable=invalid-name
      def _subscribe_to_player_events(self):
          if self._queue is None:
              self._queue = _ProcessSonosEventQueue(self)
              self._player.avTransport.subscribe(
                  auto_renew=True,
                  event_queue=self._queue)
              self._player.renderingControl.subscribe(
                  auto_renew=True,
                  event_queue=self._queue)
  
      def update(self):
          """Retrieve latest state."""
          if self._speaker_info is None:
              self._speaker_info = self._player.get_speaker_info(True)
              self._name = self._speaker_info['zone_name'].replace(
                  ' (R)', '').replace(' (L)', '')
              self._favorite_sources = \
                  self._player.get_sonos_favorites()['favorites']
  
          if self._last_avtransport_event:
              self._available = True
          else:
              self._available = self._is_available()
  
          if not self._available:
              self._player_volume = None
              self._player_volume_muted = None
              self._status = 'OFF'
              self._coordinator = None
              self._media_content_id = None
              self._media_duration = None
              self._media_position = None
              self._media_position_updated_at = None
              self._media_image_url = None
              self._media_artist = None
              self._media_album_name = None
              self._media_title = None
              self._media_radio_show = None
              self._current_track_uri = None
              self._current_track_is_radio_stream = False
              self._support_previous_track = False
              self._support_next_track = False
              self._support_play = False
              self._support_shuffle_set = False
              self._support_stop = False
              self._support_pause = False
              self._night_sound = None
              self._speech_enhance = None
              self._is_playing_tv = False
              self._is_playing_line_in = False
              self._source_name = None
              self._last_avtransport_event = None
              return
  
          # set group coordinator
          if self._player.is_coordinator:
              self._coordinator = None
          else:
              try:
                  self._coordinator = _get_entity_from_soco(
                      self.hass, self._player.group.coordinator)
  
                  # protect for loop
                  if not self._coordinator.is_coordinator:
                      # pylint: disable=protected-access
                      self._coordinator._coordinator = None
              except ValueError:
                  self._coordinator = None
  
          track_info = None
          if self._last_avtransport_event:
              variables = self._last_avtransport_event.variables
              current_track_metadata = variables.get(
                  'current_track_meta_data', {}
              )
  
              self._status = variables.get('transport_state')
  
              if current_track_metadata:
                  # no need to ask speaker for information we already have
                  current_track_metadata = current_track_metadata.__dict__
  
                  track_info = {
                      'uri': variables.get('current_track_uri'),
                      'artist': current_track_metadata.get('creator'),
                      'album': current_track_metadata.get('album'),
                      'title': current_track_metadata.get('title'),
                      'playlist_position': variables.get('current_track'),
                      'duration': variables.get('current_track_duration')
                  }
          else:
              self._player_volume = self._player.volume
              self._player_volume_muted = self._player.mute
              transport_info = self._player.get_current_transport_info()
              self._status = transport_info.get('current_transport_state')
  
          if not track_info:
              track_info = self._player.get_current_track_info()
  
          if self._coordinator:
              self._last_avtransport_event = None
              return
  
          is_playing_tv = self._player.is_playing_tv
          is_playing_line_in = self._player.is_playing_line_in
  
          media_info = self._player.avTransport.GetMediaInfo(
              [('InstanceID', 0)]
          )
  
          current_media_uri = media_info['CurrentURI']
          media_artist = track_info.get('artist')
          media_album_name = track_info.get('album')
          media_title = track_info.get('title')
          media_image_url = track_info.get('album_art', None)
  
          media_position = None
          media_position_updated_at = None
          source_name = None
  
          night_sound = self._player.night_mode
          speech_enhance = self._player.dialog_mode
  
          is_radio_stream = \
              current_media_uri.startswith('x-sonosapi-stream:') or \
              current_media_uri.startswith('x-rincon-mp3radio:')
  
          if is_playing_tv or is_playing_line_in:
              # playing from line-in/tv.
  
              support_previous_track = False
              support_next_track = False
              support_play = False
              support_stop = True
              support_pause = False
              support_shuffle_set = False
  
              if is_playing_tv:
                  media_artist = SUPPORT_SOURCE_TV
              else:
                  media_artist = SUPPORT_SOURCE_LINEIN
  
              source_name = media_artist
  
              media_album_name = None
              media_title = None
              media_image_url = None
  
          elif is_radio_stream:
              media_image_url = self._format_media_image_url(
                  media_image_url,
                  current_media_uri
              )
              support_previous_track = False
              support_next_track = False
              support_play = True
              support_stop = True
              support_pause = False
              support_shuffle_set = False
  
              source_name = 'Radio'
              # Check if currently playing radio station is in favorites
              favc = [fav for fav in self._favorite_sources
                      if fav['uri'] == current_media_uri]
              if len(favc) == 1:
                  src = favc.pop()
                  source_name = src['title']
  
              # for radio streams we set the radio station name as the
              # title.
              if media_artist and media_title:
                  # artist and album name are in the data, concatenate
                  # that do display as artist.
                  # "Information" field in the sonos pc app
  
                  media_artist = '{artist} - {title}'.format(
                      artist=media_artist,
                      title=media_title
                  )
              else:
                  # "On Now" field in the sonos pc app
                  media_artist = self._media_radio_show
  
              current_uri_metadata = media_info["CurrentURIMetaData"]
              if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None):
  
                  # currently soco does not have an API for this
                  import soco
                  current_uri_metadata = soco.xml.XML.fromstring(
                      soco.utils.really_utf8(current_uri_metadata))
  
                  md_title = current_uri_metadata.findtext(
                      './/{http://purl.org/dc/elements/1.1/}title')
  
                  if md_title not in ('', 'NOT_IMPLEMENTED', None):
                      media_title = md_title
  
              if media_artist and media_title:
                  # some radio stations put their name into the artist
                  # name, e.g.:
                  #   media_title = "Station"
                  #   media_artist = "Station - Artist - Title"
                  # detect this case and trim from the front of
                  # media_artist for cosmetics
                  str_to_trim = '{title} - '.format(
                      title=media_title
                  )
                  chars = min(len(media_artist), len(str_to_trim))
  
                  if media_artist[:chars].upper() == str_to_trim[:chars].upper():
                      media_artist = media_artist[chars:]
  
          else:
              # not a radio stream
              media_image_url = self._format_media_image_url(
                  media_image_url,
                  track_info['uri']
              )
              support_previous_track = True
              support_next_track = True
              support_play = True
              support_stop = True
              support_pause = True
              support_shuffle_set = True
  
              position_info = self._player.avTransport.GetPositionInfo(
                  [('InstanceID', 0),
                   ('Channel', 'Master')]
              )
              rel_time = _parse_timespan(
                  position_info.get("RelTime")
              )
  
              # player no longer reports position?
              update_media_position = rel_time is None and \
                  self._media_position is not None
  
              # player started reporting position?
              update_media_position |= rel_time is not None and \
                  self._media_position is None
  
              # position changed?
              if rel_time is not None and self._media_position is not None:
  
                  time_diff = utcnow() - self._media_position_updated_at
                  time_diff = time_diff.total_seconds()
  
                  calculated_position = self._media_position + time_diff
  
                  update_media_position = \
                      abs(calculated_position - rel_time) > 1.5
  
              if update_media_position and self.state == STATE_PLAYING:
                  media_position = rel_time
                  media_position_updated_at = utcnow()
              else:
                  # don't update media_position (don't want unneeded
                  # state transitions)
                  media_position = self._media_position
                  media_position_updated_at = self._media_position_updated_at
  
              playlist_position = track_info.get('playlist_position')
              if playlist_position in ('', 'NOT_IMPLEMENTED', None):
                  playlist_position = None
              else:
                  playlist_position = int(playlist_position)
  
              playlist_size = media_info.get('NrTracks')
              if playlist_size in ('', 'NOT_IMPLEMENTED', None):
                  playlist_size = None
              else:
                  playlist_size = int(playlist_size)
  
              if playlist_position is not None and playlist_size is not None:
  
                  if playlist_position <= 1:
                      support_previous_track = False
  
                  if playlist_position == playlist_size:
                      support_next_track = False
  
          self._media_content_id = track_info.get('title')
          self._media_duration = _parse_timespan(
              track_info.get('duration')
          )
          self._media_position = media_position
          self._media_position_updated_at = media_position_updated_at
          self._media_image_url = media_image_url
          self._media_artist = media_artist
          self._media_album_name = media_album_name
          self._media_title = media_title
          self._current_track_uri = track_info['uri']
          self._current_track_is_radio_stream = is_radio_stream
          self._support_previous_track = support_previous_track
          self._support_next_track = support_next_track
          self._support_play = support_play
          self._support_shuffle_set = support_shuffle_set
          self._support_stop = support_stop
          self._support_pause = support_pause
          self._night_sound = night_sound
          self._speech_enhance = speech_enhance
          self._is_playing_tv = is_playing_tv
          self._is_playing_line_in = is_playing_line_in
          self._source_name = source_name
          self._last_avtransport_event = None
  
      def _format_media_image_url(self, url, fallback_uri):
          if url in ('', 'NOT_IMPLEMENTED', None):
              if fallback_uri in ('', 'NOT_IMPLEMENTED', None):
                  return None
              if fallback_uri.find('tts_proxy') > 0:
                  # If the content is a tts don't try to fetch an image from it.
                  return None
              return 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
                  host=self._player.ip_address,
                  port=1400,
                  uri=urllib.parse.quote(fallback_uri)
              )
          return url
  
      def process_sonos_event(self, event):
          """Process a service event coming from the speaker."""
          next_track_image_url = None
          if event.service == self._player.avTransport:
              self._last_avtransport_event = event
  
              self._media_radio_show = None
              if self._current_track_is_radio_stream:
                  current_track_metadata = event.variables.get(
                      'current_track_meta_data'
                  )
                  if current_track_metadata:
                      self._media_radio_show = \
                          current_track_metadata.radio_show.split(',')[0]
  
              next_track_uri = event.variables.get('next_track_uri')
              if next_track_uri:
                  next_track_image_url = self._format_media_image_url(
                      None,
                      next_track_uri
                  )
  
          elif event.service == self._player.renderingControl:
              if 'volume' in event.variables:
                  self._player_volume = int(
                      event.variables['volume'].get('Master')
                  )
  
              if 'mute' in event.variables:
                  self._player_volume_muted = \
                      event.variables['mute'].get('Master') == '1'
  
          self.schedule_update_ha_state(True)
  
          if next_track_image_url:
              self.preload_media_image_url(next_track_image_url)
  
      @property
      def volume_level(self):
          """Volume level of the media player (0..1)."""
          return self._player_volume / 100.0
  
      @property
      def is_volume_muted(self):
          """Return true if volume is muted."""
          return self._player_volume_muted
  
      @property
      def shuffle(self):
          """Shuffling state."""
          return True if self._player.play_mode == 'SHUFFLE' else False
  
      @property
      def media_content_id(self):
          """Content ID of current playing media."""
          if self._coordinator:
              return self._coordinator.media_content_id
  
          return self._media_content_id
  
      @property
      def media_content_type(self):
          """Content type of current playing media."""
          return MEDIA_TYPE_MUSIC
  
      @property
      def media_duration(self):
          """Duration of current playing media in seconds."""
          if self._coordinator:
              return self._coordinator.media_duration
  
          return self._media_duration
  
      @property
      def media_position(self):
          """Position of current playing media in seconds."""
          if self._coordinator:
              return self._coordinator.media_position
  
          return self._media_position
  
      @property
      def media_position_updated_at(self):
          """When was the position of the current playing media valid.
  
          Returns value from homeassistant.util.dt.utcnow().
          """
          if self._coordinator:
              return self._coordinator.media_position_updated_at
  
          return self._media_position_updated_at
  
      @property
      def media_image_url(self):
          """Image url of current playing media."""
          if self._coordinator:
              return self._coordinator.media_image_url
  
          return self._media_image_url
  
      @property
      def media_artist(self):
          """Artist of current playing media, music track only."""
          if self._coordinator:
              return self._coordinator.media_artist
  
          return self._media_artist
  
      @property
      def media_album_name(self):
          """Album name of current playing media, music track only."""
          if self._coordinator:
              return self._coordinator.media_album_name
  
          return self._media_album_name
  
      @property
      def media_title(self):
          """Title of current playing media."""
          if self._coordinator:
              return self._coordinator.media_title
  
          return self._media_title
  
      @property
      def night_sound(self):
          """Get status of Night Sound."""
          return self._night_sound
  
      @property
      def speech_enhance(self):
          """Get status of Speech Enhancement."""
          return self._speech_enhance
  
      @property
      def supported_features(self):
          """Flag media player features that are supported."""
          if self._coordinator:
              return self._coordinator.supported_features
  
          supported = SUPPORT_SONOS
  
          if not self._support_previous_track:
              supported = supported ^ SUPPORT_PREVIOUS_TRACK
  
          if not self._support_next_track:
              supported = supported ^ SUPPORT_NEXT_TRACK
  
          if not self._support_play:
              supported = supported ^ SUPPORT_PLAY
          if not self._support_shuffle_set:
              supported = supported ^ SUPPORT_SHUFFLE_SET
          if not self._support_stop:
              supported = supported ^ SUPPORT_STOP
  
          if not self._support_pause:
              supported = supported ^ SUPPORT_PAUSE
  
          return supported
  
      @soco_error
      def volume_up(self):
          """Volume up media player."""
          self._player.volume += self.volume_increment
  
      @soco_error
      def volume_down(self):
          """Volume down media player."""
          self._player.volume -= self.volume_increment
  
      @soco_error
      def set_volume_level(self, volume):
          """Set volume level, range 0..1."""
          self._player.volume = str(int(volume * 100))
  
      @soco_error
      def set_shuffle(self, shuffle):
          """Enable/Disable shuffle mode."""
          self._player.play_mode = 'SHUFFLE' if shuffle else 'NORMAL'
  
      @soco_error
      def mute_volume(self, mute):
          """Mute (true) or unmute (false) media player."""
          self._player.mute = mute
  
      @soco_error
      @soco_coordinator
      def select_source(self, source):
          """Select input source."""
          if source == SUPPORT_SOURCE_LINEIN:
              self._source_name = SUPPORT_SOURCE_LINEIN
              self._player.switch_to_line_in()
          elif source == SUPPORT_SOURCE_TV:
              self._source_name = SUPPORT_SOURCE_TV
              self._player.switch_to_tv()
          else:
              fav = [fav for fav in self._favorite_sources
                     if fav['title'] == source]
              if len(fav) == 1:
                  src = fav.pop()
                  self._source_name = src['title']
  
                  if ('object.container.playlistContainer' in src['meta'] or
                          'object.container.album.musicAlbum' in src['meta']):
                      self._replace_queue_with_playlist(src)
                      self._player.play_from_queue(0)
                  else:
                      self._player.play_uri(src['uri'], src['meta'],
                                            src['title'])
  
      def _replace_queue_with_playlist(self, src):
          """Replace queue with playlist represented by src.
  
          Playlists can't be played directly with the self._player.play_uri
          API as they are actually composed of multiple URLs. Until soco has
          support for playing a playlist, we'll need to parse the playlist item
          and replace the current queue in order to play it.
          """
          import soco
          import xml.etree.ElementTree as ET
  
          root = ET.fromstring(src['meta'])
          namespaces = {'item':
                        'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/',
                        'desc': 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/'}
          desc = root.find('item:item', namespaces).find('desc:desc',
                                                         namespaces).text
  
          res = [soco.data_structures.DidlResource(uri=src['uri'],
                                                   protocol_info="DUMMY")]
          didl = soco.data_structures.DidlItem(title="DUMMY",
                                               parent_id="DUMMY",
                                               item_id=src['uri'],
                                               desc=desc,
                                               resources=res)
  
          self._player.stop()
          self._player.clear_queue()
          self._player.add_to_queue(didl)
  
      @property
      def source_list(self):
          """List of available input sources."""
          if self._coordinator:
              return self._coordinator.source_list
  
          model_name = self._speaker_info['model_name']
          sources = []
  
          if self._favorite_sources:
              for fav in self._favorite_sources:
                  sources.append(fav['title'])
  
          if 'PLAY:5' in model_name:
              sources += [SUPPORT_SOURCE_LINEIN]
          elif 'PLAYBAR' in model_name:
              sources += [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV]
          return sources
  
      @property
      def source(self):
          """Name of the current input source."""
          if self._coordinator:
              return self._coordinator.source
  
          return self._source_name
  
      @soco_error
      def turn_off(self):
          """Turn off media player."""
          if self._support_stop:
              self.media_stop()
  
      @soco_error
      @soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
      @soco_coordinator
      def media_play(self):
          """Send play command."""
          self._player.play()
  
      @soco_error
      @soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
      @soco_coordinator
      def media_stop(self):
          """Send stop command."""
          self._player.stop()
  
      @soco_error
      @soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
      @soco_coordinator
      def media_pause(self):
          """Send pause command."""
          self._player.pause()
  
      @soco_error
      @soco_coordinator
      def media_next_track(self):
          """Send next track command."""
          self._player.next()
  
      @soco_error
      @soco_coordinator
      def media_previous_track(self):
          """Send next track command."""
          self._player.previous()
  
      @soco_error
      @soco_coordinator
      def media_seek(self, position):
          """Send seek command."""
          self._player.seek(str(datetime.timedelta(seconds=int(position))))
  
      @soco_error
      @soco_coordinator
      def clear_playlist(self):
          """Clear players playlist."""
          self._player.clear_queue()
  
      @soco_error
      def turn_on(self):
          """Turn the media player on."""
          if self.support_play:
              self.media_play()
  
      @soco_error
      @soco_coordinator
      def play_media(self, media_type, media_id, **kwargs):
          """
          Send the play_media command to the media player.
  
          If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
          """
          if kwargs.get(ATTR_MEDIA_ENQUEUE):
              from soco.exceptions import SoCoUPnPException
              try:
                  self._player.add_uri_to_queue(media_id)
              except SoCoUPnPException:
                  _LOGGER.error('Error parsing media uri "%s", '
                                "please check it's a valid media resource "
                                'supported by Sonos', media_id)
          else:
              self._player.play_uri(media_id)
  
      @soco_error
      def join(self, master):
          """Join the player to a group."""
          coord = [device for device in self.hass.data[DATA_SONOS]
                   if device.entity_id == master]
  
          if coord and master != self.entity_id:
              coord = coord[0]
              if coord.soco.group.coordinator != coord.soco:
                  coord.soco.unjoin()
              self._player.join(coord.soco)
              self._coordinator = coord
          else:
              _LOGGER.error("Master not found %s", master)
  
      @soco_error
      def unjoin(self):
          """Unjoin the player from a group."""
          self._player.unjoin()
          self._coordinator = None
  
      @soco_error
      def snapshot(self, with_group=True):
          """Snapshot the player."""
          from soco.snapshot import Snapshot
  
          self._soco_snapshot = Snapshot(self._player)
          self._soco_snapshot.snapshot()
  
          if with_group:
              self._snapshot_group = self._player.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 soco.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._player.group
  
              ##
              # Master have not change, update group
              if old.coordinator == actual.coordinator:
                  if self._player 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._player.join(old.coordinator)
                  return
  
              ##
              # restore old master, update group
              old.coordinator.unjoin()
              coordinator = _get_entity_from_soco(self.hass, old.coordinator)
              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):
          """Set the timer on the player."""
          self._player.set_sleep_timer(sleep_time)
  
      @soco_error
      @soco_coordinator
      def clear_sleep_timer(self):
          """Clear the timer on the player."""
          self._player.set_sleep_timer(None)
  
      @soco_error
      @soco_coordinator
      def update_alarm(self, **data):
          """Set the alarm clock on the player."""
          from soco import alarms
          a = None
          for alarm in alarms.get_alarms(self.soco):
              # pylint: disable=protected-access
              if alarm._alarm_id == str(data[ATTR_ALARM_ID]):
                  a = alarm
          if a is None:
              _LOGGER.warning("did not find alarm with id %s",
                              data[ATTR_ALARM_ID])
              return
          if ATTR_TIME in data:
              a.start_time = data[ATTR_TIME]
          if ATTR_VOLUME in data:
              a.volume = int(data[ATTR_VOLUME] * 100)
          if ATTR_ENABLED in data:
              a.enabled = data[ATTR_ENABLED]
          if ATTR_INCLUDE_LINKED_ZONES in data:
              a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
          a.save()
  
      @soco_error
      def update_option(self, **data):
          """Modify playback options."""
          if ATTR_NIGHT_SOUND in data and self.night_sound is not None:
              self.soco.night_mode = data[ATTR_NIGHT_SOUND]
  
          if ATTR_SPEECH_ENHANCE in data and self.speech_enhance is not None:
              self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE]
  
      @property
      def device_state_attributes(self):
          """Return device specific state attributes."""
          attributes = {ATTR_IS_COORDINATOR: self.is_coordinator}
  
          if self.night_sound is not None:
              attributes[ATTR_NIGHT_SOUND] = self.night_sound
  
          if self.speech_enhance is not None:
              attributes[ATTR_SPEECH_ENHANCE] = self.speech_enhance
  
          return attributes
  