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
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
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()
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))
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
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
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
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
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)")
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}
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}
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)
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()
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)
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
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
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)
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)
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) #restore with fade = True snap.restore(True)
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
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)")
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)
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()