Example #1
0
def sonos_bell():
   global sonosname
   global sonosbell
   global sonos_bell_volume
   play_reset = "no"
   
   # take snapshot of selected sonos zone
   snap = Snapshot(sonosbell)
   snap.snapshot()

   # set doorbell play volume
   sonosbell.volume = sonos_bell_volume

   # play the doorbell sound
   # MP3 shared in local network
   sonosbell.play_uri('http://192.168.0.116/doorbell.mp3')
   
   # give sonos time to start playing doorbell sound
   time.sleep(1)

   # wait for door bell sound to be finished playing
   while str(sonosbell.get_current_transport_info()[u'current_transport_state']) == "PLAYING":
      time.sleep(0.1)

   # restore state of selected sonos zone with fade = True
   snap.restore(True) 
   
   return
Example #2
0
    def play_spoken_notifications(self, notifications=None):
        notification_mp3s = []
        for notification in notifications:
            tts = gTTS(text=notification, lang=self.tts_lang)
            now = datetime.datetime.now()
            filename = now.strftime('%Y-%m-%d_%H-%M-%S-%f') + ".mp3"
            notification_mp3s.append(filename)
            tts.save(self.webserver_path + filename)

        if notification_mp3s:
            group_coordinator = None
            for zone in list(soco.discover()):
                if zone.group.coordinator.player_name == zone.player_name:
                    group_coordinator = zone

                zone_snapshot = Snapshot(zone)
                zone_snapshot.snapshot()
                time.sleep(1)
                zone.volume = int(self.volume)
                for mp3 in notification_mp3s:
                    self.log.info('Playing notification ' + mp3 + ' on ' + zone.player_name)
                    zone.play_uri(uri='http://' + Nic().get_current_address() + ':' + self.webserver_port + '/' + mp3)
                    time.sleep(self.max_notification_seconds)
                zone_snapshot.restore()

            if group_coordinator is not None:
                for zone in list(soco.discover()):
                    if zone.group.coordinator.player_name != zone.player_name:
                        print zone.player_name + ' joining ' + zone.group.coordinator.player_name
                        zone.join(group_coordinator)
        return notifications
Example #3
0
    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        from soco.snapshot import Snapshot

        self.hass = hass
        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_image_url = None
        self._media_artist = None
        self._media_album_name = None
        self._media_title = None
        self._media_radio_show = None
        self._media_next_title = None
        self._support_previous_track = False
        self._support_next_track = False
        self._support_pause = False
        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.soco_snapshot = Snapshot(self._player)
Example #4
0
 def __init__(self, hass, player):
     """Initialize the Sonos device."""
     self.hass = hass
     self.volume_increment = 5
     super(SonosDevice, self).__init__()
     self._player = player
     self.update()
     from soco.snapshot import Snapshot
     self.soco_snapshot = Snapshot(self._player)
Example #5
0
    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        from soco.snapshot import Snapshot

        self.hass = hass
        self.volume_increment = 5
        self._player = player
        self._name = None
        self.update()
        self.soco_snapshot = Snapshot(self._player)
    def snapshot(self, with_group=True):
        """Snapshot the player."""
        from soco.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
Example #7
0
    def play_snippet(self, uri, volume=-1, group_command=False, fade_in=False):
        """
        Plays a audio snippet. This will pause the current audio track , plays the snippet and after that, the previous
        track will be continued.
        :param uri: uri to be played
        :param volume: Snippet volume [-1-100]. After the snippet was played, the previous/original volume is set. If
        volume is '-1', the current volume is used. Default: -1
        :param group_command: Only affects the volume. If True, the snippet volume is set to all zone members. Default:
        False
        :raise err:
        """

        self._fade_in = fade_in

        if not self.is_coordinator:
            logger.debug(
                "forwarding play_snippet command to coordinator with uid {uid}"
                .format(uid=self.zone_coordinator.uid))
            self._zone_coordinator.play_snippet(uri, volume)
        else:
            with self._snippet_queue_lock:
                try:
                    was_empty = False
                    ''' Check if event queue is empty.
                    if yes, then add the currently played track to the end of the queue
                    '''
                    if self._snippet_queue.empty():

                        # if there is now music item in the current playlist, an exception is thrown
                        # uncritical
                        try:

                            self._saved_music_item = Snapshot(
                                device=self.soco, snapshot_queue=True)
                            self._saved_music_item.snapshot()

                            was_empty = True
                        except:
                            pass

                    # snippet is priority 1, save_music_track is 2
                    self._snippet_queue.put((1, uri, volume))
                    if was_empty:
                        self._snippet_queue.put((2, self._saved_music_item))

                except KeyError as err:  # The key have been deleted in another thread
                    del self._snippet_queue.queue[:]
                    raise err
                except Exception as err:
                    del self._snippet_queue.queue[:]
                    raise err
Example #8
0
    def get_playlist(self):
        try:
            snapshot = Snapshot(device=self.soco, snapshot_queue=True)
            snapshot.snapshot()
            f = io.BytesIO()
            pickle.dump(snapshot.queue, f, pickle.HIGHEST_PROTOCOL)
            f.seek(0)
            return definitions.MB_PLAYLIST + base64.b64encode(
                f.read()).decode('ascii')

        except Exception as err:
            raise Exception(
                "Unable to get playlist! Error: {err}".format(err=err))
        finally:
            f.close()
Example #9
0
    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        from soco.snapshot import Snapshot

        self.hass = hass
        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._media_next_title = None
        self._support_previous_track = False
        self._support_next_track = False
        self._support_pause = False
        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.soco_snapshot = Snapshot(self._player)
def play_alert(zones, alert_uri, alert_volume=60, alert_duration=0, fade_back=True):
    # Use soco.snapshot to capture current state of each zone to allow restore
    for zone in zones:
        zone.snap = Snapshot(zone)
        zone.snap.snapshot()
        print('snapshot of zone: {}'.format(zone.player_name))

    # prepare all zones for playing the alert
    for zone in zones:
        # Each Sonos group has one coordinator only these can play, pause, etc.
        if zone.is_coordinator:
            if not zone.is_playing_tv:  # can't pause TV - so don't try!
                # pause music for each coordinators if playing
                trans_state = zone.get_current_transport_info()
                if trans_state['current_transport_state'] == 'PLAYING':
                    zone.pause()

        # For every Sonos player set volume and mute for every zone
        zone.volume = alert_volume
        zone.mute = False

    # play the sound (uri) on each sonos coordinator
    print('will play: {} on all coordinators'.format(alert_uri))
    for zone in zones:
        if zone.is_coordinator:
            zone.play_uri(uri=alert_uri, title='Sonos Alert')

    # wait for alert_duration
    time.sleep(alert_duration)

    # restore each zone to previous state
    for zone in zones:
        print('restoring {}'.format(zone.player_name))
        zone.snap.restore(fade=fade_back)
Example #11
0
    def save_snapshot(self, speaker: Speaker):
        """Saves the current playback state of the passed speaker

        :param models.speaker.Speaker speaker: Speaker which playback state should be saved
        """
        soco_instance = self.get_coordinator_instance(speaker)
        soco_instance.snapshot = Snapshot(soco_instance)
        soco_instance.snapshot.snapshot()
Example #12
0
    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        from soco.snapshot import Snapshot

        self.hass = hass
        self.volume_increment = 5
        self._player = player
        self.update()
        self.soco_snapshot = Snapshot(self._player)
def speak(room):
    global sonosPlayerList, _myIp

    for place, player in sonosPlayerList.iteritems():
        if (room.lower() == 'default' and place
                == _CORE_PLACEMENT.lower()) or room.lower() == place.lower():

            # Taking a snapshot of the current state allows use to easily pause the currently playing track on the device before speaking on it and restore the music later
            snapshot = Snapshot(player)
            snapshot.snapshot()

            if isPlaying(player):
                player.pause()
                time.sleep(0.5)

            player.volume = 50
            player.play_uri(
                'x-file-cifs://{}/share/snipsTalk.wav'.format(_myIp),
                title='Snips')

            # Sonos can be sleeping. We have to loop until detecting a playing state or sometimes it might fail speaking, ending before it even started
            while player.get_current_transport_info(
            )['current_transport_state'] != 'PLAYING':
                time.sleep(0.1)

            # Once we started playing the sound, loop until it's finished
            while player.get_current_transport_info(
            )['current_transport_state'] == 'PLAYING':
                time.sleep(0.1)

            # Restore the state we were before speaking on the player
            snapshot.restore()
Example #14
0
    def snapshot(self, with_group=True):
        """Snapshot the player."""
        from soco.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
Example #15
0
def play_alert(zones,
               alert_uri,
               alert_volume=20,
               alert_duration=0,
               fade_back=False):
    """
    Demo function using soco.snapshot across multiple Sonos players.

    Args:
        zones (set): a set of SoCo objects
        alert_uri (str): uri that Sonos can play as an alert
        alert_volume (int): volume level for playing alert (0 tp 100)
        alert_duration (int): length of alert (if zero then length of track)
        fade_back (bool): on reinstating the zones fade up the sound?
    """

    # Use soco.snapshot to capture current state of each zone to allow restore
    for zone in zones:
        zone.snap = Snapshot(zone)
        zone.snap.snapshot()
        print('snapshot of zone: {}'.format(zone.player_name))

    # prepare all zones for playing the alert
    for zone in zones:
        # Each Sonos group has one coordinator only these can play, pause, etc.
        if zone.is_coordinator:
            if not zone.is_playing_tv:  # can't pause TV - so don't try!
                # pause music for each coordinators if playing
                trans_state = zone.get_current_transport_info()
                if trans_state['current_transport_state'] == 'PLAYING':
                    zone.pause()

        # For every Sonos player set volume and mute for every zone
        zone.volume = alert_volume
        zone.mute = False

    # play the sound (uri) on each sonos coordinator
    print('will play: {} on all coordinators'.format(alert_uri))
    for zone in zones:
        if zone.is_coordinator:
            zone.play_uri(uri=alert_uri, title='Sonos Alert')

    # wait for alert_duration
    time.sleep(alert_duration)

    # restore each zone to previous state
    for zone in zones:
        print('restoring {}'.format(zone.player_name))
        zone.snap.restore(fade=fade_back)
Example #16
0
    def test_sonos_restore(self, restoreMock, *args):
        """Ensuring soco methods called for sonos_restor service."""
        from soco.snapshot import Snapshot

        sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1')
        device = self.hass.data[sonos.DATA_SONOS][-1]
        device.hass = self.hass

        restoreMock.return_value = True
        device._snapshot_coordinator = mock.MagicMock()
        device._snapshot_coordinator.soco_device = SoCoMock('192.0.2.17')
        device._soco_snapshot = Snapshot(device._player)
        device.restore()
        self.assertEqual(restoreMock.call_count, 1)
        self.assertEqual(restoreMock.call_args, mock.call(False))
Example #17
0
    def doorbell(self):
        for zone in self.zones:
            zone.snap = Snapshot(zone)
            zone.snap.snapshot()


#            print('found zone')
#            print(zone.snap.volume, zone.is_coordinator, zone.is_playing_tv,  zone.player_name.encode('latin-1'))

        coordinators = [
            zone for zone in self.zones
            if zone.is_coordinator and not zone.is_playing_tv
        ]

        #        print (coordinators)

        for zone_c in coordinators:
            trans_state = zone_c.get_current_transport_info()
            #print(zone_c.player_name.encode('latin-1'), trans_state)
            if trans_state['current_transport_state'] == 'PLAYING':
                zone_c.pause()

        for zone in self.zones:
            if zone.is_playing_tv:
                zone.volume = 0
            else:
                zone.volume = 1

        waitTime = 0
        for zone_c in coordinators:
            #            print('Play doorbell', zone_c.player_name.encode('latin-1'))
            zone_c.play_uri(
                'x-file-cifs://raspberrypi/PiShare/Sounds/doorbell.mp3')
            if (waitTime == 0):
                waitTime = int(
                    zone_c.get_current_track_info()['duration'][-2:])

        time.sleep(waitTime)
        for zone in self.zones:
            if zone.is_playing_tv:
                pass
            else:
                print('restoring {}'.format(
                    zone.player_name.encode('latin-1')))
                zone.snap.restore(fade=True)
Example #18
0
def play():
    file = 'http://192.168.1.52/sounds/' + random.choice(sounds)
    try:
        sonos = by_name("Lekrum")
        if sonos.group:
            sonos = sonos.group.coordinator

        snap = Snapshot(sonos)
        snap.snapshot()
        sonos.play_uri(file)
        time.sleep(5)
        snap.restore(fade=True)
    except:
        print("Failed to play on " + str(sonos))
Example #19
0
    def __init__(self, name, init, messager):
        self.name = name
        self.pref_zone = init['children']['1']
        sonos_list = soco.discover()
        zones = {}
        self.prev_snapshot = {}
        while (len(sonos_list) > 0):
            player = sonos_list.pop()
            zones[player.player_name] = player
            snap = Snapshot(player)
            self.prev_snapshot[player.player_name] = snap
            self.prev_snapshot[player.player_name].snapshot()
            if (player.player_name == self.pref_zone):
                self.sonos = player
        self.zones = zones

        self.state = {}

        #self.children = init['children']
        self.messager = messager
        self.polling = 60
        self.last_check = time.time()
Example #20
0
    def set_playlist(self, playlist, play_on_insert):
        try:
            snapshot = Snapshot(device=self.soco, snapshot_queue=False)
            snapshot.device.stop()
            snapshot.snapshot()

            # check magic bytes
            if not playlist.startswith("#so_pl#"):
                raise Exception("This is not a valid playlist file.")

            # remove magic bytes
            playlist = playlist.lstrip(definitions.MB_PLAYLIST)

            with tempfile.TemporaryFile() as f:
                f.write(base64.b64decode(playlist))
                f.seek(0)
                snapshot.queue = pickle.load(f)
                snapshot.restore()
            if play_on_insert:
                self.set_play(1, True)
        except Exception as err:
            print(err)
            pass
Example #21
0
def on_doorbell(root_path, audio_file, volume, zone):
    global doorbell_playing

    http_path = root_path + "/" + audio_file.url
    print('on_doorbell {} {} {}'.format(audio_file.name, volume, zone))
    if is_doorbell_busy():
        print('Doorbell already playing...suppressing')
        return
    doorbell_playing = True

    snap = Snapshot(zone)
    snap.snapshot()

    # Zone does not support snapshort restore properly for soundbar
    should_ring = zone.is_coordinator and not zone.is_playing_tv

    if should_ring:
        trans_state = zone.get_current_transport_info()
        if trans_state['current_transport_state'] == 'PLAYING':
            zone.pause()
        zone.volume = volume

        print('Play doorbell on ', zone.player_name)
        zone.play_uri(uri=http_path, title="Doorbell")

        time.sleep(audio_file.length)
        print('Restoring {}'.format(zone.player_name))

        if snap.is_playing_cloud_queue:
            print(
                "Unlikely to resume playback. Cloud restore doesn't really work"
            )
        snap.restore(fade=False)
    else:
        print('Cannot play doorbell on the provided zone')
    doorbell_playing = False
Example #22
0
class SonosDevice(MediaPlayerDevice):
    """Representation of a Sonos device."""

    def __init__(self, player):
        """Initialize the Sonos device."""
        self._receives_events = False
        self._volume_increment = 5
        self._unique_id = player.uid
        self._player = player
        self._model = None
        self._player_volume = None
        self._player_muted = None
        self._play_mode = None
        self._name = None
        self._coordinator = None
        self._sonos_group = None
        self._status = 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._night_sound = None
        self._speech_enhance = None
        self._source_name = None
        self._available = True
        self._favorites = None
        self._soco_snapshot = None
        self._snapshot_group = None

        self._set_basic_information()

    @asyncio.coroutine
    def async_added_to_hass(self):
        """Subscribe sonos events."""
        self.hass.data[DATA_SONOS].devices.append(self)
        self.hass.async_add_job(self._subscribe_to_player_events)

    @property
    def unique_id(self):
        """Return a unique ID."""
        return self._unique_id

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    @soco_coordinator
    def state(self):
        """Return the state of the device."""
        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 _check_available(self):
        """Check that we can still connect to the player."""
        try:
            sock = socket.create_connection(
                address=(self.soco.ip_address, 1443), timeout=3)
            sock.close()
            return True
        except socket.error:
            return False

    def _set_basic_information(self):
        """Set initial device information."""
        speaker_info = self.soco.get_speaker_info(True)
        self._name = speaker_info['zone_name']
        self._model = speaker_info['model_name']
        self._play_mode = self.soco.play_mode

        self.update_volume()

        self._favorites = []
        # SoCo 0.14 raises a generic Exception on invalid xml in favorites.
        # Filter those out now so our list is safe to use.
        # pylint: disable=broad-except
        try:
            for fav in self.soco.music_library.get_sonos_favorites():
                try:
                    if fav.reference.get_uri():
                        self._favorites.append(fav)
                except Exception:
                    _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title)
        except Exception:
            _LOGGER.debug("Ignoring invalid favorite list")

    def _radio_artwork(self, url):
        """Return the private URL with artwork for a radio stream."""
        if url not in ('', 'NOT_IMPLEMENTED', None):
            if url.find('tts_proxy') > 0:
                # If the content is a tts don't try to fetch an image from it.
                return None
            url = 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
                host=self.soco.ip_address,
                port=1400,
                uri=urllib.parse.quote(url, safe='')
            )
        return url

    def _subscribe_to_player_events(self):
        """Add event subscriptions."""
        self._receives_events = False

        # New player available, build the current group topology
        for device in self.hass.data[DATA_SONOS].devices:
            device.update_groups()

        player = self.soco

        queue = _ProcessSonosEventQueue(self.update_media)
        player.avTransport.subscribe(auto_renew=True, event_queue=queue)

        queue = _ProcessSonosEventQueue(self.update_volume)
        player.renderingControl.subscribe(auto_renew=True, event_queue=queue)

        queue = _ProcessSonosEventQueue(self.update_groups)
        player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue)

    def update(self):
        """Retrieve latest state."""
        available = self._check_available()
        if self._available != available:
            self._available = available
            if available:
                self._set_basic_information()
                self._subscribe_to_player_events()
            else:
                self._player_volume = None
                self._player_muted = None
                self._status = 'OFF'
                self._coordinator = 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._source_name = None
        elif available and not self._receives_events:
            self.update_groups()
            self.update_volume()
            if self.is_coordinator:
                self.update_media()

    def update_media(self, event=None):
        """Update information about currently playing media."""
        transport_info = self.soco.get_current_transport_info()
        new_status = transport_info.get('current_transport_state')

        # Ignore transitions, we should get the target state soon
        if new_status == 'TRANSITIONING':
            return

        self._play_mode = self.soco.play_mode

        if self.soco.is_playing_tv:
            self.update_media_linein(SOURCE_TV)
        elif self.soco.is_playing_line_in:
            self.update_media_linein(SOURCE_LINEIN)
        else:
            track_info = self.soco.get_current_track_info()

            if _is_radio_uri(track_info['uri']):
                variables = event and event.variables
                self.update_media_radio(variables, track_info)
            else:
                update_position = (new_status != self._status)
                self.update_media_music(update_position, track_info)

        self._status = new_status

        self.schedule_update_ha_state()

        # Also update slaves
        for entity in self.hass.data[DATA_SONOS].devices:
            coordinator = entity.coordinator
            if coordinator and coordinator.unique_id == self.unique_id:
                entity.schedule_update_ha_state()

    def update_media_linein(self, source):
        """Update state when playing from line-in/tv."""
        self._media_duration = None
        self._media_position = None
        self._media_position_updated_at = None

        self._media_image_url = None

        self._media_artist = source
        self._media_album_name = None
        self._media_title = None

        self._source_name = source

    def update_media_radio(self, variables, track_info):
        """Update state when streaming radio."""
        self._media_duration = None
        self._media_position = None
        self._media_position_updated_at = None

        media_info = self.soco.avTransport.GetMediaInfo([('InstanceID', 0)])
        self._media_image_url = self._radio_artwork(media_info['CurrentURI'])

        self._media_artist = track_info.get('artist')
        self._media_album_name = None
        self._media_title = track_info.get('title')

        if self._media_artist and self._media_title:
            # artist and album name are in the data, concatenate
            # that do display as artist.
            # "Information" field in the sonos pc app
            self._media_artist = '{artist} - {title}'.format(
                artist=self._media_artist,
                title=self._media_title
            )
        elif variables:
            # "On Now" field in the sonos pc app
            current_track_metadata = variables.get('current_track_meta_data')
            if current_track_metadata:
                self._media_artist = \
                    current_track_metadata.radio_show.split(',')[0]

        # For radio streams we set the radio station name as the title.
        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):
                self._media_title = md_title

        if self._media_artist and self._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
            trim = '{title} - '.format(title=self._media_title)
            chars = min(len(self._media_artist), len(trim))

            if self._media_artist[:chars].upper() == trim[:chars].upper():
                self._media_artist = self._media_artist[chars:]

        # Check if currently playing radio station is in favorites
        self._source_name = None
        for fav in self._favorites:
            if fav.reference.get_uri() == media_info['CurrentURI']:
                self._source_name = fav.title

    def update_media_music(self, update_media_position, track_info):
        """Update state when playing music tracks."""
        self._media_duration = _timespan_secs(track_info.get('duration'))

        position_info = self.soco.avTransport.GetPositionInfo(
            [('InstanceID', 0),
             ('Channel', 'Master')]
        )
        rel_time = _timespan_secs(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 jumped?
        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:
            self._media_position = rel_time
            self._media_position_updated_at = utcnow()

        self._media_image_url = track_info.get('album_art')

        self._media_artist = track_info.get('artist')
        self._media_album_name = track_info.get('album')
        self._media_title = track_info.get('title')

        self._source_name = None

    def update_volume(self, event=None):
        """Update information about currently volume settings."""
        if event:
            variables = event.variables

            if 'volume' in variables:
                self._player_volume = int(variables['volume']['Master'])

            if 'mute' in variables:
                self._player_muted = (variables['mute']['Master'] == '1')

            if 'night_mode' in variables:
                self._night_sound = (variables['night_mode'] == '1')

            if 'dialog_level' in variables:
                self._speech_enhance = (variables['dialog_level'] == '1')

            self.schedule_update_ha_state()
        else:
            self._player_volume = self.soco.volume
            self._player_muted = self.soco.mute
            self._night_sound = self.soco.night_mode
            self._speech_enhance = self.soco.dialog_mode

    def update_groups(self, event=None):
        """Process a zone group topology event coming from a player."""
        if event:
            self._receives_events = True

            if not hasattr(event, 'zone_player_uui_ds_in_group'):
                return

        with self.hass.data[DATA_SONOS].topology_lock:
            group = event and event.zone_player_uui_ds_in_group
            if group:
                # New group information is pushed
                coordinator_uid, *slave_uids = group.split(',')
            elif self.soco.group:
                # Use SoCo cache for existing topology
                coordinator_uid = self.soco.group.coordinator.uid
                slave_uids = [p.uid for p in self.soco.group.members
                              if p.uid != coordinator_uid]
            else:
                # Not yet in the cache, this can happen when a speaker boots
                coordinator_uid = self.unique_id
                slave_uids = []

            if self.unique_id == coordinator_uid:
                sonos_group = []
                for uid in (coordinator_uid, *slave_uids):
                    entity = _get_entity_from_soco_uid(self.hass, uid)
                    if entity:
                        sonos_group.append(entity.entity_id)

                self._coordinator = None
                self._sonos_group = sonos_group
                self.schedule_update_ha_state()

                for slave_uid in slave_uids:
                    slave = _get_entity_from_soco_uid(self.hass, slave_uid)
                    if slave:
                        # pylint: disable=protected-access
                        slave._coordinator = self
                        slave._sonos_group = sonos_group
                        slave.schedule_update_ha_state()

    @property
    def volume_level(self):
        """Volume level of the media player (0..1)."""
        return self._player_volume / 100

    @property
    def is_volume_muted(self):
        """Return true if volume is muted."""
        return self._player_muted

    @property
    @soco_coordinator
    def shuffle(self):
        """Shuffling state."""
        return 'SHUFFLE' in self._play_mode

    @property
    def media_content_type(self):
        """Content type of current playing media."""
        return MEDIA_TYPE_MUSIC

    @property
    @soco_coordinator
    def media_duration(self):
        """Duration of current playing media in seconds."""
        return self._media_duration

    @property
    @soco_coordinator
    def media_position(self):
        """Position of current playing media in seconds."""
        return self._media_position

    @property
    @soco_coordinator
    def media_position_updated_at(self):
        """When was the position of the current playing media valid."""
        return self._media_position_updated_at

    @property
    @soco_coordinator
    def media_image_url(self):
        """Image url of current playing media."""
        return self._media_image_url or None

    @property
    @soco_coordinator
    def media_artist(self):
        """Artist of current playing media, music track only."""
        return self._media_artist

    @property
    @soco_coordinator
    def media_album_name(self):
        """Album name of current playing media, music track only."""
        return self._media_album_name

    @property
    @soco_coordinator
    def media_title(self):
        """Title of current playing media."""
        return self._media_title

    @property
    @soco_coordinator
    def source(self):
        """Name of the current input source."""
        return self._source_name

    @property
    @soco_coordinator
    def supported_features(self):
        """Flag media player features that are supported."""
        return SUPPORT_SONOS

    @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.soco.volume = str(int(volume * 100))

    @soco_error()
    @soco_coordinator
    def set_shuffle(self, shuffle):
        """Enable/Disable shuffle mode."""
        self.soco.play_mode = 'SHUFFLE_NOREPEAT' if shuffle else 'NORMAL'

    @soco_error()
    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        self.soco.mute = mute

    @soco_error()
    @soco_coordinator
    def select_source(self, source):
        """Select input source."""
        if source == SOURCE_LINEIN:
            self.soco.switch_to_line_in()
        elif source == SOURCE_TV:
            self.soco.switch_to_tv()
        else:
            fav = [fav for fav in self._favorites
                   if fav.title == source]
            if len(fav) == 1:
                src = fav.pop()
                uri = src.reference.get_uri()
                if _is_radio_uri(uri):
                    # SoCo 0.14 fails to XML escape the title parameter
                    from xml.sax.saxutils import escape
                    self.soco.play_uri(uri, title=escape(source))
                else:
                    self.soco.clear_queue()
                    self.soco.add_to_queue(src.reference)
                    self.soco.play_from_queue(0)

    @property
    @soco_coordinator
    def source_list(self):
        """List of available input sources."""
        sources = [fav.title for fav in self._favorites]

        model = self._model.upper()
        if 'PLAY:5' in model or 'CONNECT' in model:
            sources += [SOURCE_LINEIN]
        elif 'PLAYBAR' in model:
            sources += [SOURCE_LINEIN, SOURCE_TV]

        return sources

    @soco_error()
    def turn_on(self):
        """Turn the media player on."""
        self.media_play()

    @soco_error()
    def turn_off(self):
        """Turn off media player."""
        self.media_stop()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_play(self):
        """Send play command."""
        self.soco.play()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_stop(self):
        """Send stop command."""
        self.soco.stop()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_pause(self):
        """Send pause command."""
        self.soco.pause()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_next_track(self):
        """Send next track command."""
        self.soco.next()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_previous_track(self):
        """Send next track command."""
        self.soco.previous()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_seek(self, position):
        """Send seek command."""
        self.soco.seek(str(datetime.timedelta(seconds=int(position))))

    @soco_error()
    @soco_coordinator
    def clear_playlist(self):
        """Clear players playlist."""
        self.soco.clear_queue()

    @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.soco.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.soco.play_uri(media_id)

    @soco_error()
    def join(self, slaves):
        """Form a group with other players."""
        if self._coordinator:
            self.unjoin()

        for slave in slaves:
            if slave.unique_id != self.unique_id:
                slave.soco.join(self.soco)
                # pylint: disable=protected-access
                slave._coordinator = self

    @soco_error()
    def unjoin(self):
        """Unjoin the player from a group."""
        self.soco.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.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 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.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):
        """Set the timer on the player."""
        self.soco.set_sleep_timer(sleep_time)

    @soco_error()
    @soco_coordinator
    def clear_sleep_timer(self):
        """Clear the timer on the player."""
        self.soco.set_sleep_timer(None)

    @soco_error()
    @soco_coordinator
    def set_alarm(self, **data):
        """Set the alarm clock on the player."""
        from soco import alarms
        alarm = None
        for one_alarm in alarms.get_alarms(self.soco):
            # pylint: disable=protected-access
            if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]):
                alarm = one_alarm
        if alarm is None:
            _LOGGER.warning("did not find alarm with id %s",
                            data[ATTR_ALARM_ID])
            return
        if ATTR_TIME in data:
            alarm.start_time = data[ATTR_TIME]
        if ATTR_VOLUME in data:
            alarm.volume = int(data[ATTR_VOLUME] * 100)
        if ATTR_ENABLED in data:
            alarm.enabled = data[ATTR_ENABLED]
        if ATTR_INCLUDE_LINKED_ZONES in data:
            alarm.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
        alarm.save()

    @soco_error()
    def set_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_SONOS_GROUP: self._sonos_group}

        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
class SonosDevice(MediaPlayerDevice):
    """Representation of a Sonos device."""
    def __init__(self, player):
        """Initialize the Sonos device."""
        self._receives_events = False
        self._volume_increment = 5
        self._unique_id = player.uid
        self._player = player
        self._model = None
        self._player_volume = None
        self._player_muted = None
        self._play_mode = None
        self._name = None
        self._coordinator = None
        self._sonos_group = None
        self._status = 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._night_sound = None
        self._speech_enhance = None
        self._source_name = None
        self._available = True
        self._favorites = None
        self._soco_snapshot = None
        self._snapshot_group = None

        self._set_basic_information()

    @asyncio.coroutine
    def async_added_to_hass(self):
        """Subscribe sonos events."""
        self.hass.data[DATA_SONOS].devices.append(self)
        self.hass.async_add_job(self._subscribe_to_player_events)

    @property
    def unique_id(self):
        """Return a unique ID."""
        return self._unique_id

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    @soco_coordinator
    def state(self):
        """Return the state of the device."""
        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 _check_available(self):
        """Check that we can still connect to the player."""
        try:
            sock = socket.create_connection(address=(self.soco.ip_address,
                                                     1443),
                                            timeout=3)
            sock.close()
            return True
        except socket.error:
            return False

    def _set_basic_information(self):
        """Set initial device information."""
        speaker_info = self.soco.get_speaker_info(True)
        self._name = speaker_info['zone_name']
        self._model = speaker_info['model_name']
        self._play_mode = self.soco.play_mode

        self.update_volume()

        self._favorites = []
        # SoCo 0.14 raises a generic Exception on invalid xml in favorites.
        # Filter those out now so our list is safe to use.
        # pylint: disable=broad-except
        try:
            for fav in self.soco.music_library.get_sonos_favorites():
                try:
                    if fav.reference.get_uri():
                        self._favorites.append(fav)
                except Exception:
                    _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title)
        except Exception:
            _LOGGER.debug("Ignoring invalid favorite list")

    def _radio_artwork(self, url):
        """Return the private URL with artwork for a radio stream."""
        if url not in ('', 'NOT_IMPLEMENTED', None):
            if url.find('tts_proxy') > 0:
                # If the content is a tts don't try to fetch an image from it.
                return None
            url = 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
                host=self.soco.ip_address,
                port=1400,
                uri=urllib.parse.quote(url, safe=''))
        return url

    def _subscribe_to_player_events(self):
        """Add event subscriptions."""
        self._receives_events = False

        # New player available, build the current group topology
        for device in self.hass.data[DATA_SONOS].devices:
            device.update_groups()

        player = self.soco

        queue = _ProcessSonosEventQueue(self.update_media)
        player.avTransport.subscribe(auto_renew=True, event_queue=queue)

        queue = _ProcessSonosEventQueue(self.update_volume)
        player.renderingControl.subscribe(auto_renew=True, event_queue=queue)

        queue = _ProcessSonosEventQueue(self.update_groups)
        player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue)

    def update(self):
        """Retrieve latest state."""
        available = self._check_available()
        if self._available != available:
            self._available = available
            if available:
                self._set_basic_information()
                self._subscribe_to_player_events()
            else:
                self._player_volume = None
                self._player_muted = None
                self._status = 'OFF'
                self._coordinator = 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._source_name = None
        elif available and not self._receives_events:
            self.update_groups()
            self.update_volume()
            if self.is_coordinator:
                self.update_media()

    def update_media(self, event=None):
        """Update information about currently playing media."""
        transport_info = self.soco.get_current_transport_info()
        new_status = transport_info.get('current_transport_state')

        # Ignore transitions, we should get the target state soon
        if new_status == 'TRANSITIONING':
            return

        self._play_mode = self.soco.play_mode

        if self.soco.is_playing_tv:
            self.update_media_linein(SOURCE_TV)
        elif self.soco.is_playing_line_in:
            self.update_media_linein(SOURCE_LINEIN)
        else:
            track_info = self.soco.get_current_track_info()

            if _is_radio_uri(track_info['uri']):
                variables = event and event.variables
                self.update_media_radio(variables, track_info)
            else:
                update_position = (new_status != self._status)
                self.update_media_music(update_position, track_info)

        self._status = new_status

        self.schedule_update_ha_state()

        # Also update slaves
        for entity in self.hass.data[DATA_SONOS].devices:
            coordinator = entity.coordinator
            if coordinator and coordinator.unique_id == self.unique_id:
                entity.schedule_update_ha_state()

    def update_media_linein(self, source):
        """Update state when playing from line-in/tv."""
        self._media_duration = None
        self._media_position = None
        self._media_position_updated_at = None

        self._media_image_url = None

        self._media_artist = source
        self._media_album_name = None
        self._media_title = None

        self._source_name = source

    def update_media_radio(self, variables, track_info):
        """Update state when streaming radio."""
        self._media_duration = None
        self._media_position = None
        self._media_position_updated_at = None

        media_info = self.soco.avTransport.GetMediaInfo([('InstanceID', 0)])
        self._media_image_url = self._radio_artwork(media_info['CurrentURI'])

        self._media_artist = track_info.get('artist')
        self._media_album_name = None
        self._media_title = track_info.get('title')

        if self._media_artist and self._media_title:
            # artist and album name are in the data, concatenate
            # that do display as artist.
            # "Information" field in the sonos pc app
            self._media_artist = '{artist} - {title}'.format(
                artist=self._media_artist, title=self._media_title)
        elif variables:
            # "On Now" field in the sonos pc app
            current_track_metadata = variables.get('current_track_meta_data')
            if current_track_metadata:
                self._media_artist = \
                    current_track_metadata.radio_show.split(',')[0]

        # For radio streams we set the radio station name as the title.
        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):
                self._media_title = md_title

        if self._media_artist and self._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
            trim = '{title} - '.format(title=self._media_title)
            chars = min(len(self._media_artist), len(trim))

            if self._media_artist[:chars].upper() == trim[:chars].upper():
                self._media_artist = self._media_artist[chars:]

        # Check if currently playing radio station is in favorites
        self._source_name = None
        for fav in self._favorites:
            if fav.reference.get_uri() == media_info['CurrentURI']:
                self._source_name = fav.title

    def update_media_music(self, update_media_position, track_info):
        """Update state when playing music tracks."""
        self._media_duration = _timespan_secs(track_info.get('duration'))

        position_info = self.soco.avTransport.GetPositionInfo([
            ('InstanceID', 0), ('Channel', 'Master')
        ])
        rel_time = _timespan_secs(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 jumped?
        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:
            self._media_position = rel_time
            self._media_position_updated_at = utcnow()

        self._media_image_url = track_info.get('album_art')

        self._media_artist = track_info.get('artist')
        self._media_album_name = track_info.get('album')
        self._media_title = track_info.get('title')

        self._source_name = None

    def update_volume(self, event=None):
        """Update information about currently volume settings."""
        if event:
            variables = event.variables

            if 'volume' in variables:
                self._player_volume = int(variables['volume']['Master'])

            if 'mute' in variables:
                self._player_muted = (variables['mute']['Master'] == '1')

            if 'night_mode' in variables:
                self._night_sound = (variables['night_mode'] == '1')

            if 'dialog_level' in variables:
                self._speech_enhance = (variables['dialog_level'] == '1')

            self.schedule_update_ha_state()
        else:
            self._player_volume = self.soco.volume
            self._player_muted = self.soco.mute
            self._night_sound = self.soco.night_mode
            self._speech_enhance = self.soco.dialog_mode

    def update_groups(self, event=None):
        """Process a zone group topology event coming from a player."""
        if event:
            self._receives_events = True

            if not hasattr(event, 'zone_player_uui_ds_in_group'):
                return

        with self.hass.data[DATA_SONOS].topology_lock:
            group = event and event.zone_player_uui_ds_in_group
            if group:
                # New group information is pushed
                coordinator_uid, *slave_uids = group.split(',')
            elif self.soco.group:
                # Use SoCo cache for existing topology
                coordinator_uid = self.soco.group.coordinator.uid
                slave_uids = [
                    p.uid for p in self.soco.group.members
                    if p.uid != coordinator_uid
                ]
            else:
                # Not yet in the cache, this can happen when a speaker boots
                coordinator_uid = self.unique_id
                slave_uids = []

            if self.unique_id == coordinator_uid:
                sonos_group = []
                for uid in (coordinator_uid, *slave_uids):
                    entity = _get_entity_from_soco_uid(self.hass, uid)
                    if entity:
                        sonos_group.append(entity.entity_id)

                self._coordinator = None
                self._sonos_group = sonos_group
                self.schedule_update_ha_state()

                for slave_uid in slave_uids:
                    slave = _get_entity_from_soco_uid(self.hass, slave_uid)
                    if slave:
                        # pylint: disable=protected-access
                        slave._coordinator = self
                        slave._sonos_group = sonos_group
                        slave.schedule_update_ha_state()

    @property
    def volume_level(self):
        """Volume level of the media player (0..1)."""
        return self._player_volume / 100

    @property
    def is_volume_muted(self):
        """Return true if volume is muted."""
        return self._player_muted

    @property
    @soco_coordinator
    def shuffle(self):
        """Shuffling state."""
        return 'SHUFFLE' in self._play_mode

    @property
    def media_content_type(self):
        """Content type of current playing media."""
        return MEDIA_TYPE_MUSIC

    @property
    @soco_coordinator
    def media_duration(self):
        """Duration of current playing media in seconds."""
        return self._media_duration

    @property
    @soco_coordinator
    def media_position(self):
        """Position of current playing media in seconds."""
        return self._media_position

    @property
    @soco_coordinator
    def media_position_updated_at(self):
        """When was the position of the current playing media valid."""
        return self._media_position_updated_at

    @property
    @soco_coordinator
    def media_image_url(self):
        """Image url of current playing media."""
        return self._media_image_url or None

    @property
    @soco_coordinator
    def media_artist(self):
        """Artist of current playing media, music track only."""
        return self._media_artist

    @property
    @soco_coordinator
    def media_album_name(self):
        """Album name of current playing media, music track only."""
        return self._media_album_name

    @property
    @soco_coordinator
    def media_title(self):
        """Title of current playing media."""
        return self._media_title

    @property
    @soco_coordinator
    def source(self):
        """Name of the current input source."""
        return self._source_name

    @property
    @soco_coordinator
    def supported_features(self):
        """Flag media player features that are supported."""
        return SUPPORT_SONOS

    @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.soco.volume = str(int(volume * 100))

    @soco_error()
    @soco_coordinator
    def set_shuffle(self, shuffle):
        """Enable/Disable shuffle mode."""
        self.soco.play_mode = 'SHUFFLE_NOREPEAT' if shuffle else 'NORMAL'

    @soco_error()
    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        self.soco.mute = mute

    @soco_error()
    @soco_coordinator
    def select_source(self, source):
        """Select input source."""
        if source == SOURCE_LINEIN:
            self.soco.switch_to_line_in()
        elif source == SOURCE_TV:
            self.soco.switch_to_tv()
        else:
            fav = [fav for fav in self._favorites if fav.title == source]
            if len(fav) == 1:
                src = fav.pop()
                uri = src.reference.get_uri()
                if _is_radio_uri(uri):
                    # SoCo 0.14 fails to XML escape the title parameter
                    from xml.sax.saxutils import escape
                    self.soco.play_uri(uri, title=escape(source))
                else:
                    self.soco.clear_queue()
                    self.soco.add_to_queue(src.reference)
                    self.soco.play_from_queue(0)

    @property
    @soco_coordinator
    def source_list(self):
        """List of available input sources."""
        sources = [fav.title for fav in self._favorites]

        if 'PLAY:5' in self._model or 'CONNECT' in self._model:
            sources += [SOURCE_LINEIN]
        elif 'PLAYBAR' in self._model:
            sources += [SOURCE_LINEIN, SOURCE_TV]

        return sources

    @soco_error()
    def turn_on(self):
        """Turn the media player on."""
        self.media_play()

    @soco_error()
    def turn_off(self):
        """Turn off media player."""
        self.media_stop()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_play(self):
        """Send play command."""
        self.soco.play()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_stop(self):
        """Send stop command."""
        self.soco.stop()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_pause(self):
        """Send pause command."""
        self.soco.pause()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_next_track(self):
        """Send next track command."""
        self.soco.next()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_previous_track(self):
        """Send next track command."""
        self.soco.previous()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_seek(self, position):
        """Send seek command."""
        self.soco.seek(str(datetime.timedelta(seconds=int(position))))

    @soco_error()
    @soco_coordinator
    def clear_playlist(self):
        """Clear players playlist."""
        self.soco.clear_queue()

    @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.soco.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.soco.play_uri(media_id)

    @soco_error()
    def join(self, slaves):
        """Form a group with other players."""
        if self._coordinator:
            self.unjoin()

        for slave in slaves:
            if slave.unique_id != self.unique_id:
                slave.soco.join(self.soco)
                # pylint: disable=protected-access
                slave._coordinator = self

    @soco_error()
    def unjoin(self):
        """Unjoin the player from a group."""
        self.soco.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.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 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.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):
        """Set the timer on the player."""
        self.soco.set_sleep_timer(sleep_time)

    @soco_error()
    @soco_coordinator
    def clear_sleep_timer(self):
        """Clear the timer on the player."""
        self.soco.set_sleep_timer(None)

    @soco_error()
    @soco_coordinator
    def set_alarm(self, **data):
        """Set the alarm clock on the player."""
        from soco import alarms
        alarm = None
        for one_alarm in alarms.get_alarms(self.soco):
            # pylint: disable=protected-access
            if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]):
                alarm = one_alarm
        if alarm is None:
            _LOGGER.warning("did not find alarm with id %s",
                            data[ATTR_ALARM_ID])
            return
        if ATTR_TIME in data:
            alarm.start_time = data[ATTR_TIME]
        if ATTR_VOLUME in data:
            alarm.volume = int(data[ATTR_VOLUME] * 100)
        if ATTR_ENABLED in data:
            alarm.enabled = data[ATTR_ENABLED]
        if ATTR_INCLUDE_LINKED_ZONES in data:
            alarm.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
        alarm.save()

    @soco_error()
    def set_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_SONOS_GROUP: self._sonos_group}

        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
Example #24
0
import soco
from soco.snapshot import Snapshot

# something to play on a Sonos player to start (a radio station)
start_uri = 'x-sonosapi-stream:s2846?sid=254&flags=32'

# alert sound to interrupt the above (a poem) - use amy file Sonos can play
alert_uri = 'https://ia800504.us.archive.org/21/items/PoemsInEnglish/tygerblake.mp3'

# choose device
device = soco.SoCo('192.168.1.68')  # <--change IP to one of your Sonos devices

# start playing something on this device(a radio station)
print('playing a radio station')
device.play_uri(start_uri, title='test radio station')
time.sleep(10)  # pause to ensure radio station playing

# take snapshot of current state
snap = Snapshot(device)  # 1) create a Snapshot class for this device
snap.snapshot()          # 2) take a snapshot of this device's status

# Do something that changes what's playing on this device
print('playing alert')
device.volume += 10                             # increase volume
device.play_uri(alert_uri, title='my alert')    # play an alert sound
time.sleep(10)                                  # wait for a bit !

# Restore previous state of Sonos (with slow fade up)
print('reinstating how it was before....')
snap.restore(fade=True)
Example #25
0
class SonosSpeaker():
    tts_local_mode = False
    local_folder = ''
    remote_folder = ''

    @classmethod
    def set_tts(self,
                local_folder,
                remote_folder,
                quota,
                tts_local_mode=False):
        SonosSpeaker.tts_local_mode = tts_local_mode
        SonosSpeaker.local_folder = local_folder
        SonosSpeaker.remote_folder = remote_folder
        SonosSpeaker.quota = quota

    def __init__(self, soco):
        self._tts_local_mode = SonosSpeaker.tts_local_mode
        self._fade_in = False
        self._saved_music_item = None
        self._zone_members = NotifyList()
        self._zone_members.register_callback(self.zone_member_changed)
        self._dirty_properties = []
        self._soco = soco
        self._uid = self.soco.uid.lower()
        self._alarms = ''
        self._mute = 0
        self._track_uri = ''
        self._track_duration = "00:00:00"
        self._track_position = "00:00:00"
        self._streamtype = ''
        self._stop = 0
        self._play = 0
        self._pause = 0
        self._radio_station = ''
        self._radio_show = ''
        self._track_album_art = ''
        self._track_title = ''
        self._track_artist = ''
        self._led = 1
        self._max_volume = -1
        self._playlist_position = 0
        self._model = ''
        self._status = True
        self._metadata = ''
        self._sub_av_transport = None
        self._sub_rendering_control = None
        self._sub_zone_group = None
        self._sub_alarm = None
        self._properties_hash = None
        self._zone_coordinator = None
        self._additional_zone_members = ''
        self._snippet_queue = queue.PriorityQueue(10)
        self._snippet_queue_lock = threading.Lock()

        self._volume = self.soco.volume
        self._bass = self.soco.bass
        self._treble = self.soco.treble
        self._loudness = self.soco.loudness
        self._playmode = self.soco.play_mode
        self._ip = self.soco.ip_address
        self._zone_icon = self.soco.speaker_info['zone_icon']
        self._zone_name = soco.speaker_info['zone_name']
        self._serial_number = self.soco.speaker_info['serial_number']
        self._software_version = self.soco.speaker_info['software_version']
        self._hardware_version = self.soco.speaker_info['hardware_version']
        self._mac_address = self.soco.speaker_info['mac_address']

        self._snippet_event_thread = threading.Thread(
            target=self.process_snippets)
        self._snippet_event_thread.daemon = True
        self._snippet_event_thread.start()

        self.dirty_all()

    # ## SoCo instance ##################################################################################################

    @property
    def soco(self):
        """
        Returns the SoCo instance associated to the current Sonos speaker.
        :return: SoCo instance
        """
        return self._soco

    ### TTS Local Mode #################################################################################################

    @property
    def tts_local_mode(self):
        """
        Is tts local mode available? If False, streaming mode is assumed (some disadvantages, see Broker docu.
        :return: tts_local_mode
        :rtype : bool
        """
        return self._tts_local_mode

    # ## MODEL ##########################################################################################################

    @property
    def model(self):
        """
        Return the model name of the speaker.
        :return: model name
        :rtype : string
        """
        return self._model

    @model.setter
    def model(self, value):
        if self._model == value:
            return
        self._model = value
        self.dirty_property('model')

    # ## METADATA #######################################################################################################

    @property
    def metadata(self):
        return self._metadata

    @metadata.setter
    def metadata(self, value):
        if self._metadata == value:
            return
        self._metadata = value

    # ## zone_coordinator #######################################################################################

    @property
    def zone_coordinator(self):
        return self._zone_coordinator

    @property
    def is_coordinator(self):
        if self == self.zone_coordinator:
            return True
        return False

    # ## EVENTS #########################################################################################################

    @property
    def sub_av_transport(self):
        return self._sub_av_transport

    @property
    def sub_rendering_control(self):
        return self._sub_rendering_control

    @property
    def sub_zone_group(self):
        return self._sub_zone_group

    @property
    def sub_alarm(self):
        return self._sub_alarm

    # ## SERIAL #########################################################################################################

    @property
    def serial_number(self):
        return self._serial_number

    # ## SOFTWARE VERSION ###############################################################################################

    @property
    def software_version(self):
        return self._software_version

    # ## HARDWARE VERSION ###############################################################################################

    @property
    def hardware_version(self):
        return self._hardware_version

    ### MAC ADDRESS ####################################################################################################

    @property
    def mac_address(self):
        return self._mac_address

    ### LED ############################################################################################################

    def get_led(self):
        return self._led

    def set_led(self, value, trigger_action=False, group_command=False):
        if trigger_action:
            if group_command:
                for speaker in self._zone_members:
                    speaker.set_led(value,
                                    trigger_action=True,
                                    group_command=False)
            self.soco.status_light = value
        if value == self._led:
            return
        self._led = value
        self.dirty_property('led')

    ### BASS ###########################################################################################################

    def get_bass(self):
        return self._bass

    def set_bass(self, value, trigger_action=False, group_command=False):
        bass = int(value)
        if trigger_action:
            if group_command:
                for speaker in self._zone_members:
                    speaker.set_bass(bass,
                                     trigger_action=True,
                                     group_command=False)
            self.soco.bass = bass
        if self._bass == bass:
            return
        self._bass = value
        self.dirty_property('bass')

    ### TREBLE #########################################################################################################

    def get_treble(self):
        return self._treble

    def set_treble(self, value, trigger_action=False, group_command=False):
        treble = int(value)
        if trigger_action:
            if group_command:
                for speaker in self._zone_members:
                    speaker.set_treble(treble,
                                       trigger_action=True,
                                       group_command=False)
            self.soco.treble = treble
        if self._treble == treble:
            return
        self._treble = treble
        self.dirty_property('treble')

    ### LOUDNESS #######################################################################################################

    def get_loudness(self):
        return int(self._loudness)

    def set_loudness(self, value, trigger_action=False, group_command=False):
        loudness = int(value)
        if trigger_action:
            if group_command:
                for speaker in self._zone_members:
                    speaker.set_loudness(loudness,
                                         trigger_action=True,
                                         group_command=False)
            self.soco.loudness = loudness
        if self._loudness == loudness:
            return
        self._loudness = value
        self.dirty_property('loudness')

    ### PLAYMODE #######################################################################################################

    def get_playmode(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding playmode getter to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.playmode
        return self._playmode.lower()

    def set_playmode(self, value, trigger_action=False):
        if trigger_action:
            if not self.is_coordinator:
                logger.debug(
                    "forwarding playmode setter to coordinator with uid {uid}".
                    format(uid=self.zone_coordinator.uid))
                self.zone_coordinator.set_playmode(value, trigger_action)
            else:
                self.soco.play_mode = value
        if self._playmode == value:
            return
        self._playmode = value
        self.dirty_property('playmode')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('playmode')

    ### ZONE NAME ######################################################################################################

    @property
    def zone_name(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding zone_name getter to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.zone_name
        return self._zone_name

    ### ZONE ICON ######################################################################################################

    @property
    def zone_icon(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding zone_icon getter to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.zone_icon
        return self._zone_icon

    ### ZONE MEMBERS ###################################################################################################

    @property
    def zone_members(self):
        return self._zone_members

    def zone_member_changed(self):
        self.dirty_property('additional_zone_members')

    @property
    def additional_zone_members(self):
        """
        Returns all zone members (current speaker NOT included) as string, delimited by ','
        :return:
        """
        members = ','.join(str(speaker.uid) for speaker in self.zone_members)
        if not members:
            members = ''
        return members

    ### IP #############################################################################################################

    @property
    def ip(self):
        return self._ip

    ### VOLUME #########################################################################################################

    def get_volume(self):
        return self._volume

    def set_volume(self, volume, trigger_action=False, group_command=False):
        volume = int(volume)
        if trigger_action:
            if group_command:
                for speaker in self._zone_members:
                    speaker.set_volume(volume,
                                       trigger_action=True,
                                       group_command=False)
            utils.check_volume_range(volume)
            if utils.check_max_volume_exceeded(volume, self.max_volume):
                volume = self.max_volume
            self.soco.volume = volume
        if self._volume == volume:
            return
        self._volume = volume
        self.dirty_property('volume')

    ### VOLUME UP#######################################################################################################

    def volume_up(self, group_command=False):
        """
        volume + 2 is the default sonos speaker behaviour, if the volume-up button was pressed
        :param group_command: if True, the volume for all group members is increased by 2
        """
        self._volume_up()
        if group_command:
            for speaker in self.zone_members:
                speaker._volume_up()

    def _volume_up(self):
        vol = self.volume
        vol += 2
        if vol > 100:
            vol = 100
        self.set_volume(vol, trigger_action=True)

    ### VOLUME DOWN ####################################################################################################

    def volume_down(self, group_command=False):
        """
        volume - 2 is the default sonos speaker behaviour, if the volume-down button was pressed
        :param group_command: if True, the volume for all group members is decreased by 2
        """

        self._volume_down()
        if group_command:
            for speaker in self.zone_members:
                speaker._volume_down()

    def _volume_down(self):
        vol = self.volume
        vol -= 2
        if vol < 0:
            vol = 0
        self.set_volume(vol, trigger_action=True)

    ### MAX VOLUME #####################################################################################################

    def get_maxvolume(self):
        """
        Getter function for property max_volume.
        :return: None
        """

        return self._max_volume

    def set_maxvolume(self, value, group_command=False):
        """
        Setter function for property max_volume.
        :param value: max_volume as an integer between -1 and 100. If value == -1, no maximum value is assumed.
        :param group_command: If True, the maximum volume for all group members is set.
        """

        self._set_maxvolume(value)
        if group_command:
            for speaker in self.zone_members:
                speaker._set_maxvolume(value)

    def _set_maxvolume(self, value):

        m_volume = int(value)
        if m_volume is not -1:
            self._max_volume = m_volume
            if utils.check_volume_range(self._max_volume):
                if self.volume > self._max_volume:
                    self.set_volume(self._max_volume, trigger_action=True)
        else:
            self._max_volume = m_volume
        self.dirty_property('max_volume')

    ### UID ############################################################################################################

    @property
    def uid(self):
        return self._uid.lower()

    ### MUTE ###########################################################################################################

    def get_mute(self):
        return self._mute

    def set_mute(self, value, trigger_action=False, group_command=False):
        """
        By default, mute is not a group command, but the sonos app handles the mute command as a group command.
        :param value: mute value [0/1]
        :param trigger_action: triggers a soco action. Otherwise just a property setter
        :param group_command: Acts as a group command, all members in a group will be muted. False by default.
        """
        mute = int(value)
        if trigger_action:
            if group_command:
                for speaker in self._zone_members:
                    speaker.set_mute(mute,
                                     trigger_action=True,
                                     group_command=False)
            self.soco.mute = mute
        if self._mute == value:
            return
        self._mute = value
        self.dirty_property('mute')

    ### TRACK_URI ######################################################################################################

    @property
    def track_uri(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding track_uri getter to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.track_uri
        return self._track_uri

    @track_uri.setter
    def track_uri(self, value):
        if self._track_uri == value:
            return
        self._track_uri = value
        self.dirty_property('track_uri')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('track_uri')

    ### TRACK DURATION #################################################################################################

    @property
    def track_duration(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding track_duration getter to coordinator with uid {uid}"
                .format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.track_duration
        if not self._track_duration:
            return "00:00:00"
        return self._track_duration

    @track_duration.setter
    def track_duration(self, value):
        if self._track_duration == value:
            return
        self.dirty_property('track_duration')
        self._track_duration = value

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('track_duration')

    ### TRACK POSITION #################################################################################################

    def get_trackposition(self, force_refresh=False):
        """
        Gets the current track position.
        :param force_refresh:
        :return: You have to poll this value manually. There is no sonos event for a track position change.
        """
        if not self.is_coordinator:
            logger.debug(
                "forwarding track_position getter to coordinator with uid {uid}"
                .format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.get_trackposition(force_refresh=True)

        if force_refresh:
            track_info = self.soco.get_current_track_info()
            self.track_position = track_info['position']
        if not self._track_position:
            return "00:00:00"
        return self._track_position

    def set_trackposition(self, value, trigger_action=False):
        """
        Sets the track position.
        :param  value: track position to set (format: HH:MM:ss)
        :param  trigger_action: If True, the value is passed to SoCo and triggers a seek command. If False, the
                behavior of this function is more or less like a property
        :rtype : None
        """
        if trigger_action:
            if not self.is_coordinator:
                logger.debug(
                    "forwarding trackposition setter to coordinator with uid {uid}"
                    .format(uid=self.zone_coordinator.uid))
                self.zone_coordinator.set_trackposition(value, trigger_action)
            else:
                self.soco.seek(value)
        if self._track_position == value:
            return
        self._track_position = value
        self.dirty_property('track_position')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('track_position')

    ### PLAYLIST POSITION ##############################################################################################

    @property
    def playlist_position(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding playlist_position getter to coordinator with uid {uid}"
                .format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.playlist_position
        return self._playlist_position

    @playlist_position.setter
    def playlist_position(self, value):
        if self._playlist_position == value:
            return
        self._playlist_position = value
        self.dirty_property('playlist_position')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('playlist_position')

    ### STREAMTYPE #####################################################################################################

    @property
    def streamtype(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding streamtype getter to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.streamtype
        return self._streamtype

    @streamtype.setter
    def streamtype(self, value):
        if self._streamtype == value:
            return
        self._streamtype = value
        self.dirty_property('streamtype')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('streamtype')

    ### STOP ###########################################################################################################

    def get_stop(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding stop getter to coordinator with uid {uid}".format(
                    uid=self.zone_coordinator.uid))
            return self.zone_coordinator.stop
        return self._stop

    def set_stop(self, value, trigger_action=False):
        stop = int(value)
        if trigger_action:
            if not self.is_coordinator:
                logger.debug(
                    "forwarding stop setter to coordinator with uid {uid}".
                    format(uid=self.zone_coordinator.uid))
                self.zone_coordinator.set_stop(stop, trigger_action=True)
            else:
                if stop:
                    self.soco.stop()
                else:
                    self.soco.play()

        if self._stop == stop:
            return

        self._stop = stop
        self._play = int(not self._stop)
        self._pause = 0
        self.dirty_property('stop', 'play', 'pause')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('pause', 'play', 'stop')

    ### PLAY ###########################################################################################################

    def get_play(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding play getter to coordinator with uid {uid}".format(
                    uid=self.zone_coordinator.uid))
            return self.zone_coordinator.play
        return self._play

    def set_play(self, value, trigger_action=False):
        play = int(value)
        if trigger_action:
            if not self.is_coordinator:
                logger.debug(
                    "forwarding play setter to coordinator with uid {uid}".
                    format(uid=self.zone_coordinator.uid))
                self.zone_coordinator.set_play(play, trigger_action=True)
            else:
                if play:
                    self.soco.play()
                else:
                    self.soco.pause()

        if self._play == play:
            return

        self._play = play
        self._pause = 0
        self._stop = int(not self._play)
        self.dirty_property('pause', 'play', 'stop')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('pause', 'play', 'stop')

    ### PAUSE ##########################################################################################################

    def get_pause(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding pause getter to coordinator with uid {uid}".format(
                    uid=self.zone_coordinator.uid))
            return self.zone_coordinator.pause
        return self._pause

    def set_pause(self, value, trigger_action=False):
        pause = int(value)
        if trigger_action:
            if not self.is_coordinator:
                logger.debug(
                    "forwarding pause setter to coordinator with uid {uid}".
                    format(uid=self.zone_coordinator.uid))
                self.zone_coordinator.set_pause(pause, trigger_action=True)
            else:
                if pause:
                    self.soco.pause()
                else:
                    self.soco.play()

        if self._pause == pause:
            return

        self._pause = pause
        self._play = int(not self._pause)
        self._stop = 0
        self.dirty_property('pause', 'play', 'stop')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('pause', 'play', 'stop')

    ### RADIO STATION ##################################################################################################

    @property
    def radio_station(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding radio_station getter to coordinator with uid {uid}"
                .format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.radio_station
        return self._radio_station

    @radio_station.setter
    def radio_station(self, value):
        if self._radio_station == value:
            return
        self._radio_station = value
        self.dirty_property('radio_station')

        #dirty properties for all zone members, if coordinator
        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('radio_station')

    ### RADIO SHOW #####################################################################################################

    @property
    def radio_show(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding radio_show getter to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.radio_show
        return self._radio_show

    @radio_show.setter
    def radio_show(self, value):
        if self._radio_show == value:
            return
        self._radio_show = value
        self.dirty_property('radio_show')

        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('radio_show')

    ### TRACK ALBUM ART ################################################################################################

    @property
    def track_album_art(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding track_album_art getter to coordinator with uid {uid}"
                .format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.track_album_art
        return self._track_album_art

    @track_album_art.setter
    def track_album_art(self, value):
        if self._track_album_art == value:
            return
        self._track_album_art = value
        self.dirty_property('track_album_art')

        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('track_album_art')

    ### TRACK TITLE ####################################################################################################

    @property
    def track_title(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding track_title getter to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.track_title
        if not self._track_title:
            return ''
        return self._track_title

    @track_title.setter
    def track_title(self, value):
        if self._track_title == value:
            return
        self._track_title = value
        self.dirty_property('track_title')

        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('track_title')

    ### TRACK ARTIST ###################################################################################################

    @property
    def track_artist(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding track_artist getter to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            return self.zone_coordinator.track_artist
        if not self._track_artist:
            return ''
        return self._track_artist

    @track_artist.setter
    def track_artist(self, value):
        if self._track_artist == value:
            return
        self._track_artist = value
        self.dirty_property('track_artist')

        if self.is_coordinator:
            for speaker in self._zone_members:
                speaker.dirty_property('track_artist')

    ### NEXT ###########################################################################################################

    def next(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding next command to coordinator with uid {uid}".format(
                    uid=self.zone_coordinator.uid))
            self.zone_coordinator.next()
        else:
            self.soco.next()

    ### PREVIOUS #######################################################################################################

    def previous(self):
        if not self.is_coordinator:
            logger.debug(
                "forwarding previous command to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            self.zone_coordinator.previous()
        else:
            self.soco.previous()

    ### PARTYMODE ######################################################################################################

    def partymode(self):
        """
        Joins all speakers to the current speaker group.
        :rtype : None
        """

        self.soco.partymode()

    ### JOIN ###########################################################################################################

    def join(self, join_uid):
        """
        Joins a Sonos speaker to another speaker / group.
        :param join_uid: A uid of any speaker of the group, the speaker has to join.
        :raise Exception: No master speaker was found
        """

        try:
            if not sonos_speakers[join_uid].is_coordinator:
                speaker = [
                    speaker
                    for speaker in sonos_speakers[join_uid].zone_members
                    if speaker.is_coordinator is True
                ][0]
            else:
                speaker = sonos_speakers[join_uid]
            self.soco.join(speaker.soco)

        except Exception:
            raise Exception(
                'No master speaker found for uid \'{uid}\'!'.format(
                    uid=join_uid))

    ### UNJOIN #########################################################################################################

    def unjoin(self):
        """
        Unjoins the current speaker from a group.
        """

        self.soco.unjoin()

    ### CURRENT STATE ##################################################################################################

    def current_state(self, group_command=False):
        """
        Refreshs all values for the current speaker. All values will be send to the connected clients.
        :param group_command: Refreshs the status for all additional zone members
        """

        self.dirty_all()
        if group_command:
            for speaker in self._zone_members:
                speaker.current_state(group_command=False)

    def dirty_music_metadata(self):
        """
        Small helper function to make the music metadata properties 'dirty' after a speaker was joined or un-joined
        to or from a group.
        """

        self.dirty_property(
            'track_title',
            'track_position',
            'track_album_art',
            'track_artist',
            'track_uri',
            'track_duration',
            'stop',
            'play',
            'pause',
            'mute',
            'radio_station',
            'radio_show',
            'playlist_position',
            'streamtype',
            'playmode',
            'zone_icon',
            'zone_name',
            'playmode',
        )

    def dirty_all(self):

        self.dirty_music_metadata()

        self.dirty_property('tts_local_mode', 'ip', 'mac_address',
                            'software_version', 'hardware_version',
                            'serial_number', 'led', 'volume', 'max_volume',
                            'mute', 'additional_zone_members', 'status',
                            'model', 'bass', 'treble', 'loudness', 'alarms',
                            'is_coordinator')

    @property
    def alarms(self):
        return self._alarms

    @alarms.setter
    def alarms(self, value):
        if value != self._alarms:
            self._alarms = value
            self.dirty_property('alarms')

    @property
    def status(self):
        return self._status

    @status.setter
    def status(self, value):
        # status == 0 -> speaker offline:
        self._status = value

        if self._status == 0:
            self._tts_local_mode = False
            self._streamtype = ''
            self._volume = 0
            self._bass = 0
            self._treble = 0
            self._loudness = 0
            self._additional_zone_members = ''
            self._mute = False
            self._led = True
            self._stop = False
            self._play = False
            self._pause = False
            self._track_title = ''
            self._track_artist = ''
            self._track_duration = "00:00:00"
            self._track_position = "00:00:00"
            self._playlist_position = 0
            self._track_uri = ''
            self._track_album_art = ''
            self._radio_show = ''
            self._radio_station = ''
            self._max_volume = -1
            self._zone_name = ''
            self._zone_coordinator = ''
            self._zone_icon = ''
            self._playmode = ''
            self._alarms = ''

    def play_uri(self, uri, metadata=None):
        """
        Plays a song from a given uri
        :param uri: uri to be played
        :param metadata: ATTENTION - currently not working due to a bug in SoCo framework
        :return: True, if the song is played.
        """

        if not self.is_coordinator:
            logger.debug(
                "forwarding play_uri command to coordinator with uid {uid}".
                format(uid=self.zone_coordinator.uid))
            self.zone_coordinator.play_uri(uri, metadata)
        else:
            return self.soco.play_uri(uri)

    def play_snippet(self, uri, volume=-1, group_command=False, fade_in=False):
        """
        Plays a audio snippet. This will pause the current audio track , plays the snippet and after that, the previous
        track will be continued.
        :param uri: uri to be played
        :param volume: Snippet volume [-1-100]. After the snippet was played, the previous/original volume is set. If
        volume is '-1', the current volume is used. Default: -1
        :param group_command: Only affects the volume. If True, the snippet volume is set to all zone members. Default:
        False
        :raise err:
        """

        self._fade_in = fade_in

        if not self.is_coordinator:
            logger.debug(
                "forwarding play_snippet command to coordinator with uid {uid}"
                .format(uid=self.zone_coordinator.uid))
            self._zone_coordinator.play_snippet(uri, volume)
        else:
            with self._snippet_queue_lock:
                try:
                    was_empty = False
                    ''' Check if event queue is empty.
                    if yes, then add the currently played track to the end of the queue
                    '''
                    if self._snippet_queue.empty():

                        # if there is now music item in the current playlist, an exception is thrown
                        # uncritical
                        try:

                            self._saved_music_item = Snapshot(
                                device=self.soco, snapshot_queue=True)
                            self._saved_music_item.snapshot()

                            was_empty = True
                        except:
                            pass

                    # snippet is priority 1, save_music_track is 2
                    self._snippet_queue.put((1, uri, volume))
                    if was_empty:
                        self._snippet_queue.put((2, self._saved_music_item))

                except KeyError as err:  # The key have been deleted in another thread
                    del self._snippet_queue.queue[:]
                    raise err
                except Exception as err:
                    del self._snippet_queue.queue[:]
                    raise err

    def _play_saved_music_item(self):
        try:
            self._saved_music_item.restore(fade=self._fade_in)

            if self._fade_in:
                for member in self.zone_members:
                    vol_to_ramp = member.soco.volume
                    member.soco.volume = 0
                    member.soco.renderingControl.RampToVolume([
                        ('InstanceID', 0), ('Channel', 'Master'),
                        ('RampType', 'SLEEP_TIMER_RAMP_TYPE'),
                        ('DesiredVolume', vol_to_ramp),
                        ('ResetVolumeAfter', False), ('ProgramURI', '')
                    ])

        except SoCoUPnPException as err:
            # maybe illegal seek target here, this is not critical
            logger.warning(err)
            return

    def _play_snippet(self, uri, volume=-1, group_command=False):
        try:
            if volume == -1:
                volume = self.volume

            logger.debug('Playing snippet \'{uri}\'. Volume: {volume}'.format(
                uri=uri, volume=volume))
            if self.volume != volume:
                self.set_volume(volume,
                                trigger_action=True,
                                group_command=group_command)

            self.play_uri(uri, '')
            time.sleep(1)
            h, m, s = self.track_duration.split(":")
            seconds = int(h) * 3600 + int(m) * 60 + int(s) + 1
            logger.debug(
                'Estimated snippet length: {seconds}'.format(seconds=seconds))

            # maximum snippet length is 60 sec
            if seconds > 60:
                seconds = 60
            if seconds < 2:
                seconds = 10

            logger.debug(
                'Waiting {seconds} seconds until snippet has finished playing.'
                .format(seconds=seconds))
            time.sleep(seconds)
        except Exception as err:
            logger.error(
                "Could not play snippet with uri '{uri}'. Exception: {err}".
                format(uri=uri, err=err))
            return

    def play_tts(self,
                 tts,
                 volume,
                 language='en',
                 group_command=False,
                 force_stream_mode=False,
                 fade_in=False):
        if (not self._tts_local_mode) or force_stream_mode:
            logger.warning(
                'Google TTS local mode disabled, using radio stream mode!')
            url = "x-rincon-mp3radio://translate.google.com/" \
                  "translate_tts?ie=UTF-8&tl={lang}&q={message}".format(lang=language,
                                                                        message=urllib.request.quote(tts))
        else:
            # we do not need any code here to get the zone coordinator.
            # The play_snippet function does the necessary work.
            filename = utils.save_google_tts(SonosSpeaker.local_folder, tts,
                                             language, SonosSpeaker.quota)
            if SonosSpeaker.local_folder.endswith('/'):
                SonosSpeaker.local_folder = SonosSpeaker.local_folder[:-1]
            url = '{}/{}'.format(SonosSpeaker.remote_folder, filename)

        self.play_snippet(url, volume, group_command, fade_in)

    def set_add_to_queue(self, uri):
        self.soco.add_to_queue(uri)

    def send(self):
        self._send()
        '''
        we need to trigger all zone members, because slave members never trigger events
        '''
        for speaker in self._zone_members:
            if len(speaker._dirty_properties) > 0:
                speaker._send()

    def _send(self):
        dirty_values = {}
        for prop in self._dirty_properties:
            value = getattr(self, prop)
            dirty_values[prop] = value
        if len(dirty_values) == 0:
            return
        '''
        always add the uid
        '''
        dirty_values['uid'] = self.uid

        data = json.dumps(self,
                          default=lambda o: dirty_values,
                          sort_keys=True,
                          ensure_ascii=False,
                          indent=4,
                          separators=(',', ': '))
        udp_broker.UdpBroker.udp_send(data)
        '''
        empty list
        '''
        del self._dirty_properties[:]

    def event_unsubscribe(self):
        """
        Unsubcribes the Broker from the event queue.
        """

        try:
            if self.sub_zone_group is not None:
                self.sub_zone_group.unsubscribe()
            if self.sub_av_transport is not None:
                self.sub_av_transport.unsubscribe()
            if self.sub_rendering_control is not None:
                self.sub_rendering_control.unsubscribe()
        except Exception as err:
            logger.exception(err)

    def event_subscription(self, event_queue):
        """
        Subscribes the Broker to all necessary Sonos speaker events
        :param event_queue:
        """

        try:
            if self.sub_zone_group is None or self.sub_zone_group.time_left == 0:
                logger.debug(
                    'renewing topology event for {uid}'.format(uid=self.uid))
                self._sub_zone_group = self.soco.zoneGroupTopology.subscribe(
                    definitions.SUBSCRIPTION_TIMEOUT, True, event_queue)

            if self.sub_av_transport is None or self.sub_av_transport.time_left == 0:
                logger.debug('renewing av-transport event for {uid}'.format(
                    uid=self.uid))
                self._sub_av_transport = self.soco.avTransport.subscribe(
                    definitions.SUBSCRIPTION_TIMEOUT, True, event_queue)

            if self.sub_rendering_control is None or self.sub_rendering_control.time_left == 0:
                logger.debug(
                    'renewing rendering event for {uid}'.format(uid=self.uid))
                self._sub_rendering_control = self.soco.renderingControl.subscribe(
                    definitions.SUBSCRIPTION_TIMEOUT, True, event_queue)

            if self.sub_alarm is None or self.sub_alarm.time_left == 0:
                logger.debug(
                    'renewing alarm event for {uid}'.format(uid=self.uid))
                self._sub_alarm = self.soco.alarmClock.subscribe(
                    definitions.SUBSCRIPTION_TIMEOUT, True, event_queue)

        except Exception as err:
            logger.exception(err)

    def get_alarms(self):
        """
        Gets all alarms for the speaker
        :return:
        """
        try:
            values = get_alarms(self.soco)
        except:
            return {}
        alarm_dict = {}
        for alarm in values:
            if alarm.zone.uid.lower() != self.uid.lower():
                continue
            dict = SonosSpeaker.alarm_to_dict(alarm)
            alarm_dict[alarm._alarm_id] = dict
        self.alarms = alarm_dict

    @staticmethod
    def alarm_to_dict(alarm):
        return {
            'Enabled': alarm.enabled,
            'Duration': str(alarm.duration),
            'PlayMode': alarm.play_mode,
            'Volume': alarm.volume,
            'Recurrence': alarm.recurrence,
            'StartTime': str(alarm.start_time),
            'IncludedLinkZones': alarm.include_linked_zones
            #'ProgramUri': alarm.program_uri,
            #'ProgramMetadata': alarm.program_metadata,
            #'Uid': alarm.zone.uid
        }

    def dirty_property(self, *args):
        for arg in args:
            if arg not in self._dirty_properties:
                self._dirty_properties.append(arg)

    def set_zone_coordinator(self):
        soco = next(member for member in self.soco.group.members
                    if member.is_coordinator is True)
        if not soco:
            '''
            current instance is the coordinator
            '''
            self._zone_coordinator = None
            self._snippet_event_thread.join()
            self._snippet_event_thread = None

        self._zone_coordinator = sonos_speakers[soco.uid.lower()]
        self.dirty_property('is_coordinator')

    def set_group_members(self):
        del self.zone_members[:]
        for member in self.soco.group.members:
            member_uid = member.uid.lower()
            if member_uid != self.uid:
                self.zone_members.append(sonos_speakers[member_uid])

    def process_snippets(self):
        while True:
            try:
                event = self._snippet_queue.get()
                if isinstance(event[1], Snapshot):
                    self._play_saved_music_item()
                else:
                    self._play_snippet(event[1], event[2])
                self._snippet_queue.task_done()
            except queue.Empty:
                pass
            except KeyboardInterrupt:
                break

    def get_playlist(self):
        try:
            snapshot = Snapshot(device=self.soco, snapshot_queue=True)
            snapshot.snapshot()
            f = io.BytesIO()
            pickle.dump(snapshot.queue, f, pickle.HIGHEST_PROTOCOL)
            f.seek(0)
            return definitions.MB_PLAYLIST + base64.b64encode(
                f.read()).decode('ascii')

        except Exception as err:
            raise Exception(
                "Unable to get playlist! Error: {err}".format(err=err))
        finally:
            f.close()

    def set_playlist(self, playlist, play_on_insert):
        try:
            snapshot = Snapshot(device=self.soco, snapshot_queue=False)
            snapshot.device.stop()
            snapshot.snapshot()

            # check magic bytes
            if not playlist.startswith("#so_pl#"):
                raise Exception("This is not a valid playlist file.")

            # remove magic bytes
            playlist = playlist.lstrip(definitions.MB_PLAYLIST)

            with tempfile.TemporaryFile() as f:
                f.write(base64.b64decode(playlist))
                f.seek(0)
                snapshot.queue = pickle.load(f)
                snapshot.restore()
            if play_on_insert:
                self.set_play(1, True)
        except Exception as err:
            print(err)
            pass

    led = property(get_led, set_led)
    bass = property(get_bass, set_bass)
    treble = property(get_treble, set_treble)
    loudness = property(get_loudness, set_loudness)
    volume = property(get_volume, set_volume)
    mute = property(get_mute, set_mute)
    playmode = property(get_playmode, set_playmode)
    stop = property(get_stop, set_stop)
    play = property(get_play, set_play)
    pause = property(get_pause, set_pause)
    max_volume = property(get_maxvolume, set_maxvolume)
    track_position = property(get_trackposition, set_trackposition)
Example #26
0
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(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_play(self):
        """Send play command."""
        self._player.play()

    @soco_error(UPNP_ERRORS_TO_IGNORE)
    @soco_coordinator
    def media_stop(self):
        """Send stop command."""
        self._player.stop()

    @soco_error(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
Example #27
0
class SonosDevice(MediaPlayerDevice):
    """Representation of a Sonos device."""

    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        from soco.snapshot import Snapshot

        self.hass = hass
        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._media_next_title = None
        self._support_previous_track = False
        self._support_next_track = False
        self._support_pause = False
        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.soco_snapshot = Snapshot(self._player)

    @property
    def should_poll(self):
        """Polling needed."""
        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

    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)

    # pylint: disable=too-many-branches, too-many-statements
    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)', '')

        if self._last_avtransport_event:
            is_available = True
        else:
            is_available = self._is_available()

        if is_available:

            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 track_info['uri'].startswith('x-rincon:'):
                # this speaker is a slave, find the coordinator
                # the uri of the track is 'x-rincon:{coordinator-id}'
                coordinator_id = track_info['uri'][9:]
                coordinators = [device for device in DEVICES
                                if device.unique_id == coordinator_id]
                self._coordinator = coordinators[0] if coordinators else None
            else:
                self._coordinator = None

            if not self._coordinator:

                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_position = None
                media_position_updated_at = None

                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_pause = False

                    if is_playing_tv:
                        media_artist = SUPPORT_SOURCE_TV
                    else:
                        media_artist = SUPPORT_SOURCE_LINEIN

                    media_album_name = None
                    media_title = None
                    media_image_url = None

                elif is_radio_stream:
                    media_image_url = self._format_media_image_url(
                        current_media_uri
                    )
                    support_previous_track = False
                    support_next_track = False
                    support_pause = False

                    # 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(
                        track_info['uri']
                    )
                    support_previous_track = True
                    support_next_track = True
                    support_pause = 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:
                        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_pause = support_pause
                self._is_playing_tv = is_playing_tv
                self._is_playing_line_in = is_playing_line_in

                # update state of the whole group
                # pylint: disable=protected-access
                for device in [x for x in DEVICES if x._coordinator == self]:
                    if device.entity_id is not self.entity_id:
                        self.hass.add_job(device.async_update_ha_state)

                if self._queue is None and self.entity_id is not None:
                    self._subscribe_to_player_events()
        else:
            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._media_next_title = 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_pause = False
            self._is_playing_tv = False
            self._is_playing_line_in = False

        self._last_avtransport_event = None

    def _format_media_image_url(self, uri):
        return 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
            host=self._player.ip_address,
            port=1400,
            uri=urllib.parse.quote(uri)
        )

    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(
                    next_track_uri
                )

            next_track_metadata = event.variables.get('next_track_meta_data')
            if next_track_metadata:
                next_track = '{title} - {creator}'.format(
                    title=next_track_metadata.title,
                    creator=next_track_metadata.creator
                )
                if next_track != self._media_next_title:
                    self._media_next_title = next_track
            else:
                self._media_next_title = None

        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.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 media_content_id(self):
        """Content ID of current playing media."""
        if self._coordinator:
            return self._coordinator.media_content_id
        else:
            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
        else:
            return self._media_duration

    @property
    def media_position(self):
        """Position of current playing media in seconds."""
        if self._coordinator:
            return self._coordinator.media_position
        else:
            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
        else:
            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
        else:
            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
        else:
            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
        else:
            return self._media_album_name

    @property
    def media_title(self):
        """Title of current playing media."""
        if self._coordinator:
            return self._coordinator.media_title
        else:
            return self._media_title

    @property
    def supported_media_commands(self):
        """Flag of media commands that are supported."""
        if self._coordinator:
            return self._coordinator.supported_media_commands

        supported = SUPPORT_SONOS

        if not self.source_list:
            # some devices do not allow source selection
            supported = supported ^ SUPPORT_SELECT_SOURCE

        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_pause:
            supported = supported ^ SUPPORT_PAUSE

        return supported

    def volume_up(self):
        """Volume up media player."""
        self._player.volume += self.volume_increment

    def volume_down(self):
        """Volume down media player."""
        self._player.volume -= self.volume_increment

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        self._player.volume = str(int(volume * 100))

    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        self._player.mute = mute

    def select_source(self, source):
        """Select input source."""
        if source == SUPPORT_SOURCE_LINEIN:
            self._player.switch_to_line_in()
        elif source == SUPPORT_SOURCE_TV:
            self._player.switch_to_tv()

    @property
    def source_list(self):
        """List of available input sources."""
        model_name = self._speaker_info['model_name']

        if 'PLAY:5' in model_name:
            return [SUPPORT_SOURCE_LINEIN]
        elif 'PLAYBAR' in model_name:
            return [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV]

    @property
    def source(self):
        """Name of the current input source."""
        if self._coordinator:
            return self._coordinator.source
        else:
            if self._is_playing_line_in:
                return SUPPORT_SOURCE_LINEIN
            elif self._is_playing_tv:
                return SUPPORT_SOURCE_TV

        return None

    def turn_off(self):
        """Turn off media player."""
        self.media_pause()

    def media_play(self):
        """Send play command."""
        if self._coordinator:
            self._coordinator.media_play()
        else:
            self._player.play()

    def media_pause(self):
        """Send pause command."""
        if self._coordinator:
            self._coordinator.media_pause()
        else:
            self._player.pause()

    def media_next_track(self):
        """Send next track command."""
        if self._coordinator:
            self._coordinator.media_next_track()
        else:
            self._player.next()

    def media_previous_track(self):
        """Send next track command."""
        if self._coordinator:
            self._coordinator.media_previous_track()
        else:
            self._player.previous()

    def media_seek(self, position):
        """Send seek command."""
        if self._coordinator:
            self._coordinator.media_seek(position)
        else:
            self._player.seek(str(datetime.timedelta(seconds=int(position))))

    def clear_playlist(self):
        """Clear players playlist."""
        if self._coordinator:
            self._coordinator.clear_playlist()
        else:
            self._player.clear_queue()

    def turn_on(self):
        """Turn the media player on."""
        self.media_play()

    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 self._coordinator:
            self._coordinator.play_media(media_type, media_id, **kwargs)
        else:
            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)

    def group_players(self):
        """Group all players under this coordinator."""
        if self._coordinator:
            self._coordinator.group_players()
        else:
            self._player.partymode()

    @only_if_coordinator
    def unjoin(self):
        """Unjoin the player from a group."""
        self._player.unjoin()

    @only_if_coordinator
    def snapshot(self):
        """Snapshot the player."""
        self.soco_snapshot.snapshot()

    @only_if_coordinator
    def restore(self):
        """Restore snapshot for the player."""
        self.soco_snapshot.restore(True)

    @only_if_coordinator
    def set_sleep_timer(self, sleep_time):
        """Set the timer on the player."""
        self._player.set_sleep_timer(sleep_time)

    @only_if_coordinator
    def clear_sleep_timer(self):
        """Clear the timer on the player."""
        self._player.set_sleep_timer(None)
Example #28
0
    def say(self, message):
        log("Speech: Message to say is: %s" % message)
        # Start by checking to see if the message is valid
        if not self.checkIfValidMessage(message):
            return

        xbmc.executebuiltin("ActivateWindow(busydialog)")
        try:
            # Need to subscribe to transport events, this is so that we know
            # when a given track has finished, and so we can stop it, if
            # we do not stop it, then it will repeat the text for a second time
            sub = self.device.avTransport.subscribe()

            # Take a snapshot of the current sonos device state, we will want
            # to roll back to this when we are done
            log("Speech: Taking snapshot")
            snap = Snapshot(self.device)
            snap.snapshot()

            # Get the URI and play it
            trans_URI = self._get_uri(message)
            log("Speech: Playing URI %s" % trans_URI)
            self.device.play_uri(trans_URI, title=__addon__.getLocalizedString(32105))

            # The maximum number of seconds that we will wait for the message to
            # complete playing
            duration = 200
            while duration > 0:
                # Check to see if the system is shutting down
                if xbmc.abortRequested:
                    break
                try:
                    eventItem = sub.events.get(timeout=0.1)

                    # Now get the details of an event if there is one there
                    if eventItem is not None:
                        # Check to see if there is a message saying that it is waiting
                        # to restart the audio stream.  This happens because it is
                        # being treated like a radio stream, so Sonos things when the
                        # end of the mp3 file playing is reached that there has been
                        # a connection error and needs to reconnect. If left to itself
                        # it would play the mp3 file again
                        if hasattr(eventItem, 'restart_pending') and (eventItem.restart_pending is not None):
                            # About to try and restart, so stop looping and stop the
                            # track before it starts again
                            if eventItem.restart_pending == '1':
                                log("Speech: Detected restart attempt")
                                break
                except Empty:
                    pass
                # Wait another 10th of a second for the speech to stop playing
                duration = duration - 1
                xbmc.sleep(100)

            log("Speech: Stopping speech")
            # Stop the stream playing
            self.device.stop()

            log("Speech: Restoring snapshot")
            try:
                # We no longer want to  receive messages
                sub.unsubscribe()
            except:
                log("Sonos: Failed to unsubscribe: %s" % traceback.format_exc(), xbmc.LOGERROR)
            try:
                # Make sure the thread is stopped even if unsubscribe failed
                event_listener.stop()
            except:
                log("Sonos: Failed to stop event listener: %s" % traceback.format_exc(), xbmc.LOGERROR)
            del sub
            # Restore the sonos device back to it's previous state
            snap.restore()
            del snap
        except:
            log("Speech: %s" % traceback.format_exc(), xbmc.LOGERROR)
            xbmc.executebuiltin("Dialog.Close(busydialog)")
            raise

        xbmc.executebuiltin("Dialog.Close(busydialog)")
Example #29
0
# -*- coding: utf-8 -*-
"""
Test for #224
"""

import time
import soco
from soco.snapshot import Snapshot

# alert = "x-file-cifs://DoorPi/DoorPiPublic/doorbell/sounds/Tinkle5sec.mp3"
alert = 'http://archive.org/download/TenD2005-07-16.flac16/TenD2005-07-16t10Wonderboy_64kb.mp3'

device = soco.SoCo('192.168.1.73')

#take snapshot of current state
snap = Snapshot(device)
is_coord = snap.snapshot

# print 'playlist_pos:', snap.playlist_position
# print 'track_pos:', snap.track_position
# #print 'stream_uri:', snap.stream_uri
# #print 'meta:', snap.metadata
# print 'media uri:', snap.media_uri
# #print 'media meta:', snap.media_metadata
# print 'Transport state:', snap.transport_state

# Do something here that changes what's playing etc.
device.volume = 20
if is_coord:
    device.play_uri(uri=alert)
time.sleep(4)
Example #30
0
    def say(self, message):
        log("Speech: Message to say is: %s" % message)
        # Start by checking to see if the message is valid
        if not self.checkIfValidMessage(message):
            return

        xbmc.executebuiltin("ActivateWindow(busydialog)")
        try:
            # Need to subscribe to transport events, this is so that we know
            # when a given track has finished, and so we can stop it, if
            # we do not stop it, then it will repeat the text for a second time
            sub = self.device.avTransport.subscribe()

            # Take a snapshot of the current sonos device state, we will want
            # to roll back to this when we are done
            log("Speech: Taking snapshot")
            snap = Snapshot(self.device)
            snap.snapshot()

            # Get the URI and play it
            trans_URI = self._get_uri(message)
            log("Speech: Playing URI %s" % trans_URI)
            self.device.play_uri(trans_URI,
                                 title=ADDON.getLocalizedString(32105))

            # The maximum number of seconds that we will wait for the message to
            # complete playing
            duration = 200
            while duration > 0:
                # Check to see if the system is shutting down
                if xbmc.abortRequested:
                    break
                try:
                    eventItem = sub.events.get(timeout=0.1)

                    # Now get the details of an event if there is one there
                    if eventItem is not None:
                        # Check to see if there is a message saying that it is waiting
                        # to restart the audio stream.  This happens because it is
                        # being treated like a radio stream, so Sonos things when the
                        # end of the mp3 file playing is reached that there has been
                        # a connection error and needs to reconnect. If left to itself
                        # it would play the mp3 file again
                        if hasattr(eventItem, 'restart_pending') and (
                                eventItem.restart_pending is not None):
                            # About to try and restart, so stop looping and stop the
                            # track before it starts again
                            if eventItem.restart_pending == '1':
                                log("Speech: Detected restart attempt")
                                break
                except Empty:
                    pass
                # Wait another 10th of a second for the speech to stop playing
                duration = duration - 1
                xbmc.sleep(100)

            log("Speech: Stopping speech")
            # Stop the stream playing
            self.device.stop()

            log("Speech: Restoring snapshot")
            try:
                # We no longer want to  receive messages
                sub.unsubscribe()
            except:
                log(
                    "Sonos: Failed to unsubscribe: %s" %
                    traceback.format_exc(), xbmc.LOGERROR)
            try:
                # Make sure the thread is stopped even if unsubscribe failed
                event_listener.stop()
            except:
                log(
                    "Sonos: Failed to stop event listener: %s" %
                    traceback.format_exc(), xbmc.LOGERROR)
            del sub
            # Restore the sonos device back to it's previous state
            snap.restore()
            del snap
        except:
            log("Speech: %s" % traceback.format_exc(), xbmc.LOGERROR)
            xbmc.executebuiltin("Dialog.Close(busydialog)")
            raise

        xbmc.executebuiltin("Dialog.Close(busydialog)")
Example #31
0
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._media_next_title = None
        self._support_previous_track = False
        self._support_next_track = False
        self._support_stop = False
        self._support_pause = False
        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):
        """Polling needed."""
        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

    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:
            is_available = True
        else:
            is_available = self._is_available()

        if not is_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._media_next_title = 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_stop = False
            self._support_pause = False
            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

        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_stop = False
            support_pause = 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_stop = False
            support_pause = 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_stop = True
            support_pause = 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_stop = support_stop
        self._support_pause = support_pause
        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
            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
                )

            next_track_metadata = event.variables.get('next_track_meta_data')
            if next_track_metadata:
                next_track = '{title} - {creator}'.format(
                    title=next_track_metadata.title,
                    creator=next_track_metadata.creator
                )
                if next_track != self._media_next_title:
                    self._media_next_title = next_track
            else:
                self._media_next_title = None

        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 media_content_id(self):
        """Content ID of current playing media."""
        if self._coordinator:
            return self._coordinator.media_content_id
        else:
            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
        else:
            return self._media_duration

    @property
    def media_position(self):
        """Position of current playing media in seconds."""
        if self._coordinator:
            return self._coordinator.media_position
        else:
            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
        else:
            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
        else:
            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
        else:
            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
        else:
            return self._media_album_name

    @property
    def media_title(self):
        """Title of current playing media."""
        if self._coordinator:
            return self._coordinator.media_title
        else:
            return self._media_title

    @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_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 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']
                self._player.play_uri(src['uri'], src['meta'], src['title'])

    @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
        else:
            return self._source_name

    @soco_error
    def turn_off(self):
        """Turn off media player."""
        self.media_pause()

    @soco_error
    @soco_coordinator
    def media_play(self):
        """Send play command."""
        self._player.play()

    @soco_error
    @soco_coordinator
    def media_stop(self):
        """Send stop command."""
        self._player.stop()

    @soco_error
    @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."""
        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 allready 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)

    @property
    def device_state_attributes(self):
        """Return device specific state attributes."""
        return {ATTR_IS_COORDINATOR: self.is_coordinator}
Example #32
0
    def say(self, message):
        # Need to subscribe to transport events, this is so that we know
        # when a given track has finished, and so we can stop it, if
        # we do not stop it, then it will repeat the text for a second time
        sub = self.device.avTransport.subscribe()

        # fade out
        #prefade_volume = self.device.volume
        #for v in range(prefade_volume):
        #    self.device.volume -= 1
        #    time.sleep(0.25)

        # Take a snapshot of the current sonos device state, we will want
        # to roll back to this when we are done
        snap = Snapshot(self.device)
        snap.snapshot()

        msg = cgi.escape(message)
        payload = {
            'ie': 'UTF-8',
            'q': message,
            'tl': 'en',
            'total': 1,
            'idx': 0,
            'client': 't',
            'textlen': len(message),
            'tk': Token().calculate_token(message)
        }
        #trans_URL = "x-rincon-mp3radio://translate.google.com/translate_tts?tl=en&q=%s" % msg
        trans_URL = "x-rincon-mp3radio://translate.google.com/translate_tts?" + urlencode(
            payload)
        print trans_URL
        #from IPython import embed
        #embed()
        self.device.play_uri(trans_URL, title="Speech")

        #self.device.volume = prefade_volume

        impatience = time.time()
        patience = time.time() + 20
        while patience > impatience:
            try:
                event = sub.events.get(timeout=0.5)
                print event.variables
                if 'restart_pending' not in event.variables:
                    continue
                restart_pending = event.variables['restart_pending']
                # About to try and restart, so stop looping and stop the
                # track before it starts again
                if restart_pending == '1':
                    break
            except Empty:
                pass
            # Wait another second for the speech to stop playing
            time.sleep(1)
            impatience = time.time()

        time.sleep(0)
        # Stop the stream playing
        self.device.stop()
        # Restore the sonos device back to it's previous state
        snap.restore()

        # fade back in
        #for v in range(prefade_volume):
        #    self.device.volume += 1
        #    time.sleep(0.25)

        # We no longer want to  receive messages
        sub.unsubscribe()
        event_listener.stop()
Example #33
0
def take_snapshot():
    print("Taking snapshot for backup purposes...")
    snap = Snapshot(speaker)
    return snap.snapshot()
Example #34
0
class SonosDevice(MediaPlayerDevice):
    """Representation of a Sonos device."""

    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        from soco.snapshot import Snapshot

        self.hass = hass
        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_image_url = None
        self._media_artist = None
        self._media_album_name = None
        self._media_title = None
        self._media_radio_show = None
        self._media_next_title = None
        self._support_previous_track = False
        self._support_next_track = False
        self._support_pause = False
        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.soco_snapshot = Snapshot(self._player)

    @property
    def should_poll(self):
        """Polling needed."""
        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

    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)

    # pylint: disable=too-many-branches, too-many-statements
    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)', '')

        if self._last_avtransport_event:
            is_available = True
        else:
            is_available = self._is_available()

        if is_available:

            self._is_playing_tv = self._player.is_playing_tv
            self._is_playing_line_in = self._player.is_playing_line_in

            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 track_info['uri'].startswith('x-rincon:'):
                # this speaker is a slave, find the coordinator
                # the uri of the track is 'x-rincon:{coordinator-id}'
                coordinator_id = track_info['uri'][9:]
                coordinators = [device for device in DEVICES
                                if device.unique_id == coordinator_id]
                self._coordinator = coordinators[0] if coordinators else None
            else:
                self._coordinator = None

            if not self._coordinator:
                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')

                is_radio_stream = \
                    current_media_uri.startswith('x-sonosapi-stream:') or \
                    current_media_uri.startswith('x-rincon-mp3radio:')

                if is_radio_stream:
                    is_radio_stream = True
                    media_image_url = self._format_media_image_url(
                        current_media_uri
                    )
                    support_previous_track = False
                    support_next_track = False
                    support_pause = False

                    # 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(
                        track_info['uri']
                    )
                    support_previous_track = True
                    support_next_track = True
                    support_pause = True

                    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_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_pause = support_pause

                # update state of the whole group
                # pylint: disable=protected-access
                for device in [x for x in DEVICES if x._coordinator == self]:
                    if device.entity_id is not self.entity_id:
                        self.hass.add_job(device.async_update_ha_state)

                if self._queue is None:
                    self._subscribe_to_player_events()
        else:
            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_image_url = None
            self._media_artist = None
            self._media_album_name = None
            self._media_title = None
            self._media_radio_show = None
            self._media_next_title = 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_pause = False
            self._is_playing_tv = False
            self._is_playing_line_in = False

        self._last_avtransport_event = None

    def _format_media_image_url(self, uri):
        return 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
            host=self._player.ip_address,
            port=1400,
            uri=urllib.parse.quote(uri)
        )

    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(
                    next_track_uri
                )

            next_track_metadata = event.variables.get('next_track_meta_data')
            if next_track_metadata:
                next_track = '{title} - {creator}'.format(
                    title=next_track_metadata.title,
                    creator=next_track_metadata.creator
                )
                if next_track != self._media_next_title:
                    self._media_next_title = next_track
            else:
                self._media_next_title = None

        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.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 media_content_id(self):
        """Content ID of current playing media."""
        if self._coordinator:
            return self._coordinator.media_content_id
        else:
            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
        else:
            return self._media_duration

    @property
    def media_image_url(self):
        """Image url of current playing media."""
        if self._coordinator:
            return self._coordinator.media_image_url
        else:
            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
        else:
            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
        else:
            return self._media_album_name

    @property
    def media_title(self):
        """Title of current playing media."""
        if self._coordinator:
            return self._coordinator.media_title
        else:
            return self._media_title

    @property
    def supported_media_commands(self):
        """Flag of media commands that are supported."""
        if self._coordinator:
            return self._coordinator.supported_media_commands

        supported = SUPPORT_SONOS

        if not self.source_list:
            # some devices do not allow source selection
            supported = supported ^ SUPPORT_SELECT_SOURCE

        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_pause:
            supported = supported ^ SUPPORT_PAUSE

        return supported

    def volume_up(self):
        """Volume up media player."""
        self._player.volume += self.volume_increment

    def volume_down(self):
        """Volume down media player."""
        self._player.volume -= self.volume_increment

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        self._player.volume = str(int(volume * 100))

    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        self._player.mute = mute

    def select_source(self, source):
        """Select input source."""
        if source == SUPPORT_SOURCE_LINEIN:
            self._player.switch_to_line_in()
        elif source == SUPPORT_SOURCE_TV:
            self._player.switch_to_tv()

    @property
    def source_list(self):
        """List of available input sources."""
        model_name = self._speaker_info['model_name']

        if 'PLAY:5' in model_name:
            return [SUPPORT_SOURCE_LINEIN]
        elif 'PLAYBAR' in model_name:
            return [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV]

    @property
    def source(self):
        """Name of the current input source."""
        if self._is_playing_line_in:
            return SUPPORT_SOURCE_LINEIN
        if self._is_playing_tv:
            return SUPPORT_SOURCE_TV

        return None

    def turn_off(self):
        """Turn off media player."""
        self.media_pause()

    def media_play(self):
        """Send play command."""
        if self._coordinator:
            self._coordinator.media_play()
        else:
            self._player.play()

    def media_pause(self):
        """Send pause command."""
        if self._coordinator:
            self._coordinator.media_pause()
        else:
            self._player.pause()

    def media_next_track(self):
        """Send next track command."""
        if self._coordinator:
            self._coordinator.media_next_track()
        else:
            self._player.next()

    def media_previous_track(self):
        """Send next track command."""
        if self._coordinator:
            self._coordinator.media_previous_track()
        else:
            self._player.previous()

    def media_seek(self, position):
        """Send seek command."""
        if self._coordinator:
            self._coordinator.media_seek(position)
        else:
            self._player.seek(str(datetime.timedelta(seconds=int(position))))

    def clear_playlist(self):
        """Clear players playlist."""
        if self._coordinator:
            self._coordinator.clear_playlist()
        else:
            self._player.clear_queue()

    def turn_on(self):
        """Turn the media player on."""
        self.media_play()

    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 self._coordinator:
            self._coordinator.play_media(media_type, media_id, **kwargs)
        else:
            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)

    def group_players(self):
        """Group all players under this coordinator."""
        if self._coordinator:
            self._coordinator.group_players()
        else:
            self._player.partymode()

    @only_if_coordinator
    def unjoin(self):
        """Unjoin the player from a group."""
        self._player.unjoin()

    @only_if_coordinator
    def snapshot(self):
        """Snapshot the player."""
        self.soco_snapshot.snapshot()

    @only_if_coordinator
    def restore(self):
        """Restore snapshot for the player."""
        self.soco_snapshot.restore(True)

    @only_if_coordinator
    def set_sleep_timer(self, sleep_time):
        """Set the timer on the player."""
        self._player.set_sleep_timer(sleep_time)

    @only_if_coordinator
    def clear_sleep_timer(self):
        """Clear the timer on the player."""
        self._player.set_sleep_timer(None)
Example #35
0
import soco
from soco.snapshot import Snapshot

# something to play on a Sonos player to start (a radio station)
start_uri = "x-sonosapi-stream:s2846?sid=254&amp;flags=32"

# alert sound to interrupt the above (a poem) - use amy file Sonos can play
alert_uri = "https://ia800504.us.archive.org/21/items/PoemsInEnglish/tygerblake.mp3"

# choose device
device = soco.SoCo("192.168.1.68")  # <--change IP to one of your Sonos devices

# start playing something on this device(a radio station)
print("playing a radio station")
device.play_uri(start_uri, title="test radio station")
time.sleep(10)  # pause to ensure radio station playing

# take snapshot of current state
snap = Snapshot(device)  # 1) create a Snapshot class for this device
snap.snapshot()  # 2) take a snapshot of this device's status

# Do something that changes what's playing on this device
print("playing alert")
device.volume += 10  # increase volume
device.play_uri(alert_uri, title="my alert")  # play an alert sound
time.sleep(10)  # wait for a bit !

# Restore previous state of Sonos (with slow fade up)
print("reinstating how it was before....")
snap.restore(fade=True)
Example #36
0
    def say(self, message):
        # Need to subscribe to transport events, this is so that we know
        # when a given track has finished, and so we can stop it, if
        # we do not stop it, then it will repeat the text for a second time
        sub = self.device.avTransport.subscribe()

        # fade out
        #prefade_volume = self.device.volume
        #for v in range(prefade_volume):
        #    self.device.volume -= 1
        #    time.sleep(0.25)

        # Take a snapshot of the current sonos device state, we will want
        # to roll back to this when we are done
        snap = Snapshot(self.device)
        snap.snapshot()

        msg = cgi.escape(message)
        payload = { 'ie' : 'UTF-8',
                   'q' : message,
                   'tl' : 'en',
                   'total' : 1,
                   'idx' : 0,
                   'client' : 't',
                   'textlen' : len(message),
                   'tk' : Token().calculate_token(message)}
        #trans_URL = "x-rincon-mp3radio://translate.google.com/translate_tts?tl=en&q=%s" % msg
        trans_URL = "x-rincon-mp3radio://translate.google.com/translate_tts?" + urlencode(payload)
        print trans_URL
        #from IPython import embed
        #embed()
        self.device.play_uri(trans_URL, title="Speech")

        #self.device.volume = prefade_volume

        impatience = time.time()
        patience = time.time() + 20
        while patience > impatience:
            try:
                event = sub.events.get(timeout=0.5)
                print event.variables
                if 'restart_pending' not in event.variables:
                    continue
                restart_pending = event.variables['restart_pending']
                # About to try and restart, so stop looping and stop the
                # track before it starts again
                if restart_pending == '1':
                    break
            except Empty:
                pass
            # Wait another second for the speech to stop playing
            time.sleep(1)
            impatience = time.time()

        time.sleep(0)
        # Stop the stream playing
        self.device.stop()
        # Restore the sonos device back to it's previous state
        snap.restore()

        # fade back in
        #for v in range(prefade_volume):
        #    self.device.volume += 1
        #    time.sleep(0.25)

        # We no longer want to  receive messages
        sub.unsubscribe()
        event_listener.stop()
Example #37
0
class SonosDevice(MediaPlayerDevice):
    """Representation of a Sonos device."""
    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        from soco.snapshot import Snapshot

        self.hass = hass
        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._media_next_title = None
        self._support_previous_track = False
        self._support_next_track = False
        self._support_stop = False
        self._support_pause = False
        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 = Snapshot(self._player)
        self._snapshot_group = None

    @property
    def should_poll(self):
        """Polling needed."""
        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_device(self):
        """Return soco device."""
        return self._player

    @property
    def coordinator(self):
        """Return coordinator of this player."""
        return self._coordinator

    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)

    # pylint: disable=too-many-branches, too-many-statements
    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)', '')

        if self._last_avtransport_event:
            is_available = True
        else:
            is_available = self._is_available()

        if is_available:

            # 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 not self._coordinator:

                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

                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_stop = False
                    support_pause = 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_stop = False
                    support_pause = False

                    source_name = 'Radio'
                    # Check if currently playing radio station is in favorites
                    favs = self._player.get_sonos_favorites()['favorites']
                    favc = [
                        fav for fav in favs 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_stop = True
                    support_pause = 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_stop = support_stop
                self._support_pause = support_pause
                self._is_playing_tv = is_playing_tv
                self._is_playing_line_in = is_playing_line_in
                self._source_name = source_name

                # update state of the whole group
                for device in [
                        x for x in self.hass.data[DATA_SONOS]
                        if x.coordinator == self
                ]:
                    if device.entity_id is not self.entity_id:
                        self.schedule_update_ha_state()

                if self._queue is None and self.entity_id is not None:
                    self._subscribe_to_player_events()
            favs = self._player.get_sonos_favorites().get('favorites', [])
            self._favorite_sources = [fav['title'] for fav in favs]
        else:
            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._media_next_title = 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_stop = False
            self._support_pause = False
            self._is_playing_tv = False
            self._is_playing_line_in = False
            self._favorite_sources = None
            self._source_name = None

        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
            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)

            next_track_metadata = event.variables.get('next_track_meta_data')
            if next_track_metadata:
                next_track = '{title} - {creator}'.format(
                    title=next_track_metadata.title,
                    creator=next_track_metadata.creator)
                if next_track != self._media_next_title:
                    self._media_next_title = next_track
            else:
                self._media_next_title = None

        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 media_content_id(self):
        """Content ID of current playing media."""
        if self._coordinator:
            return self._coordinator.media_content_id
        else:
            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
        else:
            return self._media_duration

    @property
    def media_position(self):
        """Position of current playing media in seconds."""
        if self._coordinator:
            return self._coordinator.media_position
        else:
            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
        else:
            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
        else:
            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
        else:
            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
        else:
            return self._media_album_name

    @property
    def media_title(self):
        """Title of current playing media."""
        if self._coordinator:
            return self._coordinator.media_title
        else:
            return self._media_title

    @property
    def supported_media_commands(self):
        """Flag of media commands that are supported."""
        if self._coordinator:
            return self._coordinator.supported_media_commands

        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_stop:
            supported = supported ^ SUPPORT_STOP

        if not self._support_pause:
            supported = supported ^ SUPPORT_PAUSE

        return supported

    def volume_up(self):
        """Volume up media player."""
        self._player.volume += self.volume_increment

    def volume_down(self):
        """Volume down media player."""
        self._player.volume -= self.volume_increment

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        self._player.volume = str(int(volume * 100))

    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        self._player.mute = mute

    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:
            favorites = self._player.get_sonos_favorites()['favorites']
            fav = [fav for fav in favorites if fav['title'] == source]
            if len(fav) == 1:
                src = fav.pop()
                self._source_name = src['title']
                self._player.play_uri(src['uri'], src['meta'], src['title'])

    @property
    def source_list(self):
        """List of available input sources."""
        model_name = self._speaker_info['model_name']

        sources = self._favorite_sources.copy()

        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
        else:
            return self._source_name

    def turn_off(self):
        """Turn off media player."""
        self.media_pause()

    def media_play(self):
        """Send play command."""
        if self._coordinator:
            self._coordinator.media_play()
        else:
            self._player.play()

    def media_stop(self):
        """Send stop command."""
        if self._coordinator:
            self._coordinator.media_stop()
        else:
            self._player.stop()

    def media_pause(self):
        """Send pause command."""
        if self._coordinator:
            self._coordinator.media_pause()
        else:
            self._player.pause()

    def media_next_track(self):
        """Send next track command."""
        if self._coordinator:
            self._coordinator.media_next_track()
        else:
            self._player.next()

    def media_previous_track(self):
        """Send next track command."""
        if self._coordinator:
            self._coordinator.media_previous_track()
        else:
            self._player.previous()

    def media_seek(self, position):
        """Send seek command."""
        if self._coordinator:
            self._coordinator.media_seek(position)
        else:
            self._player.seek(str(datetime.timedelta(seconds=int(position))))

    def clear_playlist(self):
        """Clear players playlist."""
        if self._coordinator:
            self._coordinator.clear_playlist()
        else:
            self._player.clear_queue()

    def turn_on(self):
        """Turn the media player on."""
        self.media_play()

    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 self._coordinator:
            self._coordinator.play_media(media_type, media_id, **kwargs)
        else:
            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)

    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_device.group.coordinator != coord.soco_device:
                coord.soco_device.unjoin()
            self._player.join(coord.soco_device)
            self._coordinator = coord
        else:
            _LOGGER.error("Master not found %s", master)

    def unjoin(self):
        """Unjoin the player from a group."""
        self._player.unjoin()
        self._coordinator = None

    def snapshot(self, with_group=True):
        """Snapshot the 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

    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, 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 allready 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)

    def set_sleep_timer(self, sleep_time):
        """Set the timer on the player."""
        if self._coordinator:
            self._coordinator.set_sleep_timer(sleep_time)
        else:
            self._player.set_sleep_timer(sleep_time)

    def clear_sleep_timer(self):
        """Clear the timer on the player."""
        if self._coordinator:
            self._coordinator.set_sleep_timer(None)
        else:
            self._player.set_sleep_timer(None)

    @property
    def device_state_attributes(self):
        """Return device specific state attributes."""
        return {ATTR_IS_COORDINATOR: self.is_coordinator}
Example #38
0
class SonosDevice(MediaPlayerDevice):
    """Representation of a Sonos device."""

    # pylint: disable=too-many-arguments
    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        self.hass = hass
        self.volume_increment = 5
        super(SonosDevice, self).__init__()
        self._player = player
        self.update()
        from soco.snapshot import Snapshot
        self.soco_snapshot = Snapshot(self._player)

    @property
    def should_poll(self):
        """Polling needed."""
        return True

    def update_sonos(self, now):
        """Update state, called by track_utc_time_change."""
        self.update_ha_state(True)

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def unique_id(self):
        """Return a unique ID."""
        return "{}.{}".format(self.__class__, self._player.uid)

    @property
    def state(self):
        """Return the state of the device."""
        if self._status == 'PAUSED_PLAYBACK':
            return STATE_PAUSED
        if self._status == 'PLAYING':
            return STATE_PLAYING
        if self._status == 'STOPPED':
            return STATE_IDLE
        return STATE_UNKNOWN

    @property
    def is_coordinator(self):
        """Return true if player is a coordinator."""
        return self._player.is_coordinator

    def update(self):
        """Retrieve latest state."""
        self._name = self._player.get_speaker_info()['zone_name'].replace(
            ' (R)', '').replace(' (L)', '')

        if self.available:
            self._status = self._player.get_current_transport_info().get(
                'current_transport_state')
            self._trackinfo = self._player.get_current_track_info()
        else:
            self._status = STATE_OFF
            self._trackinfo = {}

    @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.mute

    @property
    def media_content_id(self):
        """Content ID of current playing media."""
        return self._trackinfo.get('title', None)

    @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."""
        dur = self._trackinfo.get('duration', '0:00')

        # If the speaker is playing from the "line-in" source, getting
        # track metadata can return NOT_IMPLEMENTED, which breaks the
        # volume logic below
        if dur == 'NOT_IMPLEMENTED':
            return None

        return sum(60**x[0] * int(x[1])
                   for x in enumerate(reversed(dur.split(':'))))

    @property
    def media_image_url(self):
        """Image url of current playing media."""
        if 'album_art' in self._trackinfo:
            return self._trackinfo['album_art']

    @property
    def media_title(self):
        """Title of current playing media."""
        if 'artist' in self._trackinfo and 'title' in self._trackinfo:
            return '{artist} - {title}'.format(
                artist=self._trackinfo['artist'],
                title=self._trackinfo['title'])
        if 'title' in self._status:
            return self._trackinfo['title']

    @property
    def supported_media_commands(self):
        """Flag of media commands that are supported."""
        return SUPPORT_SONOS

    def volume_up(self):
        """Volume up media player."""
        self._player.volume += self.volume_increment

    def volume_down(self):
        """Volume down media player."""
        self._player.volume -= self.volume_increment

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        self._player.volume = str(int(volume * 100))

    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        self._player.mute = mute

    @only_if_coordinator
    def turn_off(self):
        """Turn off media player."""
        self._player.pause()

    @only_if_coordinator
    def media_play(self):
        """Send play command."""
        self._player.play()

    @only_if_coordinator
    def media_pause(self):
        """Send pause command."""
        self._player.pause()

    @only_if_coordinator
    def media_next_track(self):
        """Send next track command."""
        self._player.next()

    @only_if_coordinator
    def media_previous_track(self):
        """Send next track command."""
        self._player.previous()

    @only_if_coordinator
    def media_seek(self, position):
        """Send seek command."""
        self._player.seek(str(datetime.timedelta(seconds=int(position))))

    @only_if_coordinator
    def turn_on(self):
        """Turn the media player on."""
        self._player.play()

    @only_if_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)

    @only_if_coordinator
    def group_players(self):
        """Group all players under this coordinator."""
        self._player.partymode()

    @only_if_coordinator
    def unjoin(self):
        """Unjoin the player from a group."""
        self._player.unjoin()

    @only_if_coordinator
    def snapshot(self):
        """Snapshot the player."""
        self.soco_snapshot.snapshot()

    @only_if_coordinator
    def restore(self):
        """Restore snapshot for the player."""
        self.soco_snapshot.restore(True)

    @property
    def available(self):
        """Return True if player is reachable, False otherwise."""
        try:
            sock = socket.create_connection(address=(self._player.ip_address,
                                                     1443),
                                            timeout=3)
            sock.close()
            return True
        except socket.error:
            return False
Example #39
0
class SonosDevice(MediaPlayerDevice):
    """Representation of a Sonos device."""

    # pylint: disable=too-many-arguments
    def __init__(self, hass, player):
        """Initialize the Sonos device."""
        from soco.snapshot import Snapshot

        self.hass = hass
        self.volume_increment = 5
        self._player = player
        self.update()
        self.soco_snapshot = Snapshot(self._player)

    @property
    def should_poll(self):
        """Polling needed."""
        return True

    def update_sonos(self, now):
        """Update state, called by track_utc_time_change."""
        self.update_ha_state(True)

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def unique_id(self):
        """Return a unique ID."""
        return "{}.{}".format(self.__class__, self._player.uid)

    @property
    def state(self):
        """Return the state of the device."""
        if self._status == 'PAUSED_PLAYBACK':
            return STATE_PAUSED
        if self._status == 'PLAYING':
            return STATE_PLAYING
        if self._status == 'STOPPED':
            return STATE_IDLE
        return STATE_UNKNOWN

    @property
    def is_coordinator(self):
        """Return true if player is a coordinator."""
        return self._player.is_coordinator

    def update(self):
        """Retrieve latest state."""
        self._name = self._player.get_speaker_info()['zone_name'].replace(
            ' (R)', '').replace(' (L)', '')

        if self.available:
            self._status = self._player.get_current_transport_info().get(
                'current_transport_state')
            self._trackinfo = self._player.get_current_track_info()
        else:
            self._status = STATE_OFF
            self._trackinfo = {}

    @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.mute

    @property
    def media_content_id(self):
        """Content ID of current playing media."""
        return self._trackinfo.get('title', None)

    @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."""
        dur = self._trackinfo.get('duration', '0:00')

        # If the speaker is playing from the "line-in" source, getting
        # track metadata can return NOT_IMPLEMENTED, which breaks the
        # volume logic below
        if dur == 'NOT_IMPLEMENTED':
            return None

        return sum(60 ** x[0] * int(x[1]) for x in
                   enumerate(reversed(dur.split(':'))))

    @property
    def media_image_url(self):
        """Image url of current playing media."""
        if 'album_art' in self._trackinfo:
            return self._trackinfo['album_art']

    @property
    def media_title(self):
        """Title of current playing media."""
        if self._player.is_playing_line_in:
            return SUPPORT_SOURCE_LINEIN
        if self._player.is_playing_tv:
            return SUPPORT_SOURCE_TV
        if 'artist' in self._trackinfo and 'title' in self._trackinfo:
            return '{artist} - {title}'.format(
                artist=self._trackinfo['artist'],
                title=self._trackinfo['title']
            )
        if 'title' in self._status:
            return self._trackinfo['title']

    @property
    def supported_media_commands(self):
        """Flag of media commands that are supported."""
        return SUPPORT_SONOS

    def volume_up(self):
        """Volume up media player."""
        self._player.volume += self.volume_increment

    def volume_down(self):
        """Volume down media player."""
        self._player.volume -= self.volume_increment

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        self._player.volume = str(int(volume * 100))

    def mute_volume(self, mute):
        """Mute (true) or unmute (false) media player."""
        self._player.mute = mute

    def select_source(self, source):
        """Select input source."""
        if source == SUPPORT_SOURCE_LINEIN:
            self._player.switch_to_line_in()
        elif source == SUPPORT_SOURCE_TV:
            self._player.switch_to_tv()

    @property
    def source_list(self):
        """List of available input sources."""
        source = []

        # generate list of supported device
        source.append(SUPPORT_SOURCE_LINEIN)
        source.append(SUPPORT_SOURCE_TV)
        source.append(SUPPORT_SOURCE_RADIO)

        return source

    @property
    def source(self):
        """Name of the current input source."""
        if self._player.is_playing_line_in:
            return SUPPORT_SOURCE_LINEIN
        if self._player.is_playing_tv:
            return SUPPORT_SOURCE_TV
        if self._player.is_playing_radio:
            return SUPPORT_SOURCE_RADIO
        return None

    @only_if_coordinator
    def turn_off(self):
        """Turn off media player."""
        self._player.pause()

    @only_if_coordinator
    def media_play(self):
        """Send play command."""
        self._player.play()

    @only_if_coordinator
    def media_pause(self):
        """Send pause command."""
        self._player.pause()

    @only_if_coordinator
    def media_next_track(self):
        """Send next track command."""
        self._player.next()

    @only_if_coordinator
    def media_previous_track(self):
        """Send next track command."""
        self._player.previous()

    @only_if_coordinator
    def media_seek(self, position):
        """Send seek command."""
        self._player.seek(str(datetime.timedelta(seconds=int(position))))

    @only_if_coordinator
    def clear_playlist(self):
        """Clear players playlist."""
        self._player.clear_queue()

    @only_if_coordinator
    def turn_on(self):
        """Turn the media player on."""
        self._player.play()

    @only_if_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)

    @only_if_coordinator
    def group_players(self):
        """Group all players under this coordinator."""
        self._player.partymode()

    @only_if_coordinator
    def unjoin(self):
        """Unjoin the player from a group."""
        self._player.unjoin()

    @only_if_coordinator
    def snapshot(self):
        """Snapshot the player."""
        self.soco_snapshot.snapshot()

    @only_if_coordinator
    def restore(self):
        """Restore snapshot for the player."""
        self.soco_snapshot.restore(True)

    @property
    def available(self):
        """Return True if player is reachable, False otherwise."""
        try:
            sock = socket.create_connection(
                address=(self._player.ip_address, 1443),
                timeout=3)
            sock.close()
            return True
        except socket.error:
            return False
Example #40
0
 def _snap(self):
     for zone in self.zones:
         zone.snap = Snapshot(zone)
         zone.snap.snapshot()
    zones = soco.discover()
    if zones == None:
        print "No Sonos players discovered on this network."
    else:
        for zone in zones:
            if zone.player_name == target_name:
                return zone

    raise UndiscoverablePlayerError(target_name)



sonos_player_name = "Kitchen"
try:
    player = discover_target_sonos_player(sonos_player_name)
except UndiscoverablePlayerError as e:
    print "Can't find the target Sonos player " + e.value
    sys.exit(0)

sonos = soco.SoCo(player.ip_address)
snap = Snapshot(sonos)    # create snapshot class
snap.snapshot()    # take a snapshot of current state

if snap.media_uri == "":
    print "No media uri found - is the Sonos player playing something?"
else:
    print 'media_uri: ', snap.media_uri
sys.exit(0)

if __name__ == "__main__":
    main()