hass-core/homeassistant/components/media_player/sonos.py
Alexander Fortin 124a9b7a81 ADD only_if_coordinator decorator to sonos
Currently we interact with players regardless of thir coordinator
role, hence soco.exceptions.SoCoSlaveException are thrown. The use
of the decorator for each interactive method should address this
2016-02-27 00:40:34 +01:00

251 lines
7.5 KiB
Python

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