""" homeassistant.components.media_player.sonos ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides an interface to Sonos players (via SoCo) For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.sonos/ """ import datetime import logging from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) REQUIREMENTS = ['SoCo==0.11.1'] _LOGGER = logging.getLogger(__name__) # The soco library is excessively chatty when it comes to logging and # causes a LOT of spam in the logs due to making a http connection to each # speaker every 10 seconds. Quiet it down a bit to just actual problems. _SOCO_LOGGER = logging.getLogger('soco') _SOCO_LOGGER.setLevel(logging.ERROR) _REQUESTS_LOGGER = logging.getLogger('requests') _REQUESTS_LOGGER.setLevel(logging.ERROR) SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Sonos platform. """ import soco import socket if discovery_info: add_devices([SonosDevice(hass, soco.SoCo(discovery_info))]) return True players = None hosts = config.get('hosts', None) if hosts: # Support retro compatibility with comma separated list of hosts # from config hosts = hosts.split(',') if isinstance(hosts, str) else hosts players = [] for host in hosts: players.append(soco.SoCo(socket.gethostbyname(host))) if not players: players = soco.discover(interface_addr=config.get('interface_addr', None)) if not players: _LOGGER.warning('No Sonos speakers found.') return False add_devices(SonosDevice(hass, p) for p in players) _LOGGER.info('Added %s Sonos speakers', len(players)) return True def only_if_coordinator(func): """ If used as decorator, avoid calling the decorated method if player is not a coordinator. If not, a grouped speaker (not in coordinator role) will throw soco.exceptions.SoCoSlaveException """ def wrapper(*args, **kwargs): """ Decorator wrapper """ if args[0].is_coordinator: return func(*args, **kwargs) else: _LOGGER.debug('Ignore command "%s" for Sonos device "%s" ' '(not coordinator)', func.__name__, args[0].name) return wrapper # pylint: disable=too-many-instance-attributes, too-many-public-methods # pylint: disable=abstract-method class SonosDevice(MediaPlayerDevice): """ Represents a Sonos device. """ # pylint: disable=too-many-arguments def __init__(self, hass, player): self.hass = hass super(SonosDevice, self).__init__() self._player = player self.update() @property def should_poll(self): return True def update_sonos(self, now): """ Updates state, called by track_utc_time_change. """ self.update_ha_state(True) @property def name(self): """ Returns the name of the device. """ return self._name @property def unique_id(self): """ Returns a unique id. """ return "{}.{}".format(self.__class__, self._player.uid) @property def state(self): """ Returns 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): """ Returns 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)', '') self._status = self._player.get_current_transport_info().get( 'current_transport_state') self._trackinfo = self._player.get_current_track_info() @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 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): """ Flags of media commands that are supported. """ return SUPPORT_SONOS @only_if_coordinator def turn_off(self): """ Turn off media player. """ self._player.pause() @only_if_coordinator def volume_up(self): """ Volume up media player. """ self._player.volume += 1 @only_if_coordinator def volume_down(self): """ Volume down media player. """ self._player.volume -= 1 @only_if_coordinator def set_volume_level(self, volume): """ Set volume level, range 0..1. """ self._player.volume = str(int(volume * 100)) @only_if_coordinator def mute_volume(self, mute): """ Mute (true) or unmute (false) media player. """ self._player.mute = mute @only_if_coordinator def media_play(self): """ Send paly 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()