core/homeassistant/components/media_player/sonos.py

276 lines
8.3 KiB
Python
Raw Normal View History

2015-09-11 22:32:47 +00:00
"""
2016-03-08 09:34:33 +00:00
Support to interface with Sonos players (via SoCo).
2015-09-11 22:32:47 +00:00
2015-10-23 16:39:50 +00:00
For more details about this platform, please refer to the documentation at
2015-11-09 12:12:18 +00:00
https://home-assistant.io/components/media_player.sonos/
2015-09-11 22:32:47 +00:00
"""
import datetime
2016-02-19 05:27:50 +00:00
import logging
import socket
2015-09-11 22:32:47 +00:00
from homeassistant.components.media_player import (
2016-02-19 05:27:50 +00:00
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
2016-03-26 05:57:14 +00:00
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
2015-09-11 22:32:47 +00:00
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
REQUIREMENTS = ['SoCo==0.11.1']
2015-09-11 22:32:47 +00:00
_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)
2015-09-11 22:32:47 +00:00
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
2016-03-26 05:57:14 +00:00
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
SUPPORT_SEEK
2015-09-11 22:32:47 +00:00
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
2016-03-08 09:34:33 +00:00
"""Setup the Sonos platform."""
2015-09-11 22:32:47 +00:00
import soco
2015-11-30 08:55:36 +00:00
if discovery_info:
player = soco.SoCo(discovery_info)
if player.is_visible:
add_devices([SonosDevice(hass, player)])
return True
return False
2015-11-30 08:55:36 +00:00
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))
2015-11-30 08:55:36 +00:00
if not players:
2015-11-30 08:55:36 +00:00
_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))
2015-09-11 22:32:47 +00:00
2015-09-11 22:52:31 +00:00
return True
2015-09-11 22:32:47 +00:00
def only_if_coordinator(func):
2016-03-08 09:34:33 +00:00
"""Decorator for coordinator.
2016-03-08 09:34:33 +00:00
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):
2016-03-08 09:34:33 +00:00
"""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
2015-10-23 16:39:50 +00:00
# pylint: disable=too-many-instance-attributes, too-many-public-methods
# pylint: disable=abstract-method
2015-09-11 22:32:47 +00:00
class SonosDevice(MediaPlayerDevice):
2016-03-08 09:34:33 +00:00
"""Representation of a Sonos device."""
2015-09-11 22:32:47 +00:00
# pylint: disable=too-many-arguments
def __init__(self, hass, player):
2016-03-08 09:34:33 +00:00
"""Initialize the Sonos device."""
self.hass = hass
self.volume_increment = 5
2015-09-11 22:32:47 +00:00
super(SonosDevice, self).__init__()
self._player = player
self.update()
@property
def should_poll(self):
2016-03-08 09:34:33 +00:00
"""No polling needed."""
return True
2015-09-11 22:32:47 +00:00
def update_sonos(self, now):
2016-03-08 09:34:33 +00:00
"""Update state, called by track_utc_time_change."""
self.update_ha_state(True)
2015-09-11 22:32:47 +00:00
@property
def name(self):
2016-03-08 09:34:33 +00:00
"""Return the name of the device."""
2015-09-11 22:32:47 +00:00
return self._name
@property
def unique_id(self):
2016-03-08 09:34:33 +00:00
"""Return a unique ID."""
return "{}.{}".format(self.__class__, self._player.uid)
2015-09-11 22:32:47 +00:00
@property
def state(self):
2016-03-08 09:34:33 +00:00
"""Return the state of the device."""
2015-09-11 22:32:47 +00:00
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):
2016-03-08 09:34:33 +00:00
"""Return true if player is a coordinator."""
return self._player.is_coordinator
2015-09-11 22:32:47 +00:00
def update(self):
2016-03-08 09:34:33 +00:00
"""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 = {}
2015-09-11 22:32:47 +00:00
@property
def volume_level(self):
2016-03-08 09:34:33 +00:00
"""Volume level of the media player (0..1)."""
return self._player.volume / 100.0
2015-09-11 22:32:47 +00:00
@property
def is_volume_muted(self):
2016-03-08 09:34:33 +00:00
"""Return true if volume is muted."""
2015-09-11 22:32:47 +00:00
return self._player.mute
@property
def media_content_id(self):
2016-03-08 09:34:33 +00:00
"""Content ID of current playing media."""
2015-09-11 22:32:47 +00:00
return self._trackinfo.get('title', None)
@property
def media_content_type(self):
2016-03-08 09:34:33 +00:00
"""Content type of current playing media."""
2015-09-11 22:32:47 +00:00
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
2016-03-08 09:34:33 +00:00
"""Duration of current playing media in seconds."""
2015-09-11 22:32:47 +00:00
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
2015-09-11 22:32:47 +00:00
return sum(60 ** x[0] * int(x[1]) for x in
enumerate(reversed(dur.split(':'))))
@property
def media_image_url(self):
2016-03-08 09:34:33 +00:00
"""Image url of current playing media."""
2015-09-11 22:32:47 +00:00
if 'album_art' in self._trackinfo:
return self._trackinfo['album_art']
@property
def media_title(self):
2016-03-08 09:34:33 +00:00
"""Title of current playing media."""
2015-09-11 22:32:47 +00:00
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):
2016-03-08 09:34:33 +00:00
"""Flag of media commands that are supported."""
2015-09-11 22:32:47 +00:00
return SUPPORT_SONOS
def volume_up(self):
2016-03-08 09:34:33 +00:00
"""Volume up media player."""
self._player.volume += self.volume_increment
2015-09-11 22:32:47 +00:00
def volume_down(self):
2016-03-08 09:34:33 +00:00
"""Volume down media player."""
self._player.volume -= self.volume_increment
2015-09-11 22:32:47 +00:00
def set_volume_level(self, volume):
2016-03-08 09:34:33 +00:00
"""Set volume level, range 0..1."""
2015-09-11 22:32:47 +00:00
self._player.volume = str(int(volume * 100))
def mute_volume(self, mute):
2016-03-08 09:34:33 +00:00
"""Mute (true) or unmute (false) media player."""
2015-09-11 22:32:47 +00:00
self._player.mute = mute
@only_if_coordinator
def turn_off(self):
"""Turn off media player."""
self._player.pause()
@only_if_coordinator
2015-09-11 22:32:47 +00:00
def media_play(self):
2016-03-26 05:57:28 +00:00
"""Send play command."""
2015-09-11 22:32:47 +00:00
self._player.play()
@only_if_coordinator
2015-09-11 22:32:47 +00:00
def media_pause(self):
2016-03-08 09:34:33 +00:00
"""Send pause command."""
2015-09-11 22:32:47 +00:00
self._player.pause()
@only_if_coordinator
2015-09-11 22:32:47 +00:00
def media_next_track(self):
2016-03-08 09:34:33 +00:00
"""Send next track command."""
2015-09-11 22:32:47 +00:00
self._player.next()
@only_if_coordinator
2015-09-11 22:32:47 +00:00
def media_previous_track(self):
2016-03-08 09:34:33 +00:00
"""Send next track command."""
2015-09-11 22:32:47 +00:00
self._player.previous()
@only_if_coordinator
2015-09-11 22:32:47 +00:00
def media_seek(self, position):
2016-03-08 09:34:33 +00:00
"""Send seek command."""
2015-09-11 22:32:47 +00:00
self._player.seek(str(datetime.timedelta(seconds=int(position))))
@only_if_coordinator
2015-09-11 22:32:47 +00:00
def turn_on(self):
2016-03-08 09:34:33 +00:00
"""Turn the media player on."""
2015-09-11 22:32:47 +00:00
self._player.play()
2016-03-26 05:57:14 +00:00
@only_if_coordinator
def play_media(self, media_type, media_id):
"""Send the play_media command to the media player."""
self._player.play_uri(media_id)
@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