2015-09-11 22:32:47 +00:00
|
|
|
"""
|
|
|
|
homeassistant.components.media_player.sonos
|
2015-10-23 16:39:50 +00:00
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2015-09-11 22:32:47 +00:00
|
|
|
Provides an interface to Sonos players (via SoCo)
|
|
|
|
|
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
|
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,
|
|
|
|
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_VOLUME_MUTE,
|
|
|
|
SUPPORT_VOLUME_SET, MediaPlayerDevice)
|
2015-09-11 22:32:47 +00:00
|
|
|
from homeassistant.const import (
|
2016-02-19 05:27:50 +00:00
|
|
|
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
|
2015-09-13 04:09:51 +00:00
|
|
|
|
|
|
|
REQUIREMENTS = ['SoCo==0.11.1']
|
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2015-09-17 03:54:43 +00:00
|
|
|
# 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 |\
|
|
|
|
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
|
2016-01-19 18:30:45 +00:00
|
|
|
import socket
|
2015-09-13 04:09:51 +00:00
|
|
|
|
2015-11-30 08:55:36 +00:00
|
|
|
if discovery_info:
|
|
|
|
add_devices([SonosDevice(hass, soco.SoCo(discovery_info))])
|
|
|
|
return True
|
|
|
|
|
2016-01-19 18:30:45 +00:00
|
|
|
players = None
|
|
|
|
hosts = config.get('hosts', None)
|
|
|
|
if hosts:
|
2016-02-18 17:57:09 +00:00
|
|
|
# Support retro compatibility with comma separated list of hosts
|
|
|
|
# from config
|
|
|
|
hosts = hosts.split(',') if isinstance(hosts, str) else hosts
|
2016-01-19 18:30:45 +00:00
|
|
|
players = []
|
2016-02-18 17:57:09 +00:00
|
|
|
for host in hosts:
|
|
|
|
players.append(soco.SoCo(socket.gethostbyname(host)))
|
2016-01-19 18:30:45 +00:00
|
|
|
|
|
|
|
if not players:
|
|
|
|
players = soco.discover(interface_addr=config.get('interface_addr',
|
|
|
|
None))
|
2015-11-30 08:55:36 +00:00
|
|
|
|
2015-09-13 04:09:51 +00:00
|
|
|
if not players:
|
2015-11-30 08:55:36 +00:00
|
|
|
_LOGGER.warning('No Sonos speakers found.')
|
2015-09-13 04:09:51 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
add_devices(SonosDevice(hass, p) for p in players)
|
2015-09-13 04:39:13 +00:00
|
|
|
_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
|
|
|
|
|
|
|
|
2015-10-23 16:39:50 +00:00
|
|
|
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
2015-09-11 23:48:34 +00:00
|
|
|
# pylint: disable=abstract-method
|
2015-09-11 22:32:47 +00:00
|
|
|
class SonosDevice(MediaPlayerDevice):
|
2015-09-11 23:38:42 +00:00
|
|
|
""" Represents a Sonos device. """
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
# pylint: disable=too-many-arguments
|
2015-09-12 02:44:37 +00:00
|
|
|
def __init__(self, hass, player):
|
2015-09-13 20:53:05 +00:00
|
|
|
self.hass = hass
|
2015-09-11 22:32:47 +00:00
|
|
|
super(SonosDevice, self).__init__()
|
|
|
|
self._player = player
|
2015-09-11 23:38:42 +00:00
|
|
|
self.update()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self):
|
2015-09-14 00:49:09 +00:00
|
|
|
return True
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2015-09-12 03:57:34 +00:00
|
|
|
def update_sonos(self, now):
|
2015-10-23 16:39:50 +00:00
|
|
|
""" Updates state, called by track_utc_time_change. """
|
2015-09-12 03:57:34 +00:00
|
|
|
self.update_ha_state(True)
|
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
""" Returns the name of the device. """
|
|
|
|
return self._name
|
|
|
|
|
2015-09-13 20:52:15 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
""" Returns a unique id. """
|
|
|
|
return "{}.{}".format(self.__class__, self._player.uid)
|
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
@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
|
|
|
|
|
|
|
|
def update(self):
|
|
|
|
""" Retrieve latest state. """
|
2015-09-11 23:38:42 +00:00
|
|
|
self._name = self._player.get_speaker_info()['zone_name'].replace(
|
|
|
|
' (R)', '').replace(' (L)', '')
|
2015-09-11 22:55:23 +00:00
|
|
|
self._status = self._player.get_current_transport_info().get(
|
|
|
|
'current_transport_state')
|
2015-09-11 23:38:42 +00:00
|
|
|
self._trackinfo = self._player.get_current_track_info()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def volume_level(self):
|
|
|
|
""" Volume level of the media player (0..1). """
|
2015-09-13 01:42:36 +00:00
|
|
|
return self._player.volume / 100.0
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
@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')
|
2015-09-13 04:39:13 +00: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):
|
|
|
|
""" 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
|
|
|
|
|
|
|
|
def turn_off(self):
|
2015-10-23 16:39:50 +00:00
|
|
|
""" Turn off media player. """
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player.pause()
|
|
|
|
|
|
|
|
def volume_up(self):
|
2015-10-23 16:39:50 +00:00
|
|
|
""" Volume up media player. """
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player.volume += 1
|
|
|
|
|
|
|
|
def volume_down(self):
|
2015-10-23 16:39:50 +00:00
|
|
|
""" Volume down media player. """
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player.volume -= 1
|
|
|
|
|
|
|
|
def set_volume_level(self, volume):
|
2015-10-23 16:39:50 +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):
|
2015-10-23 16:39:50 +00:00
|
|
|
""" Mute (true) or unmute (false) media player. """
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player.mute = mute
|
|
|
|
|
|
|
|
def media_play(self):
|
2015-10-23 16:39:50 +00:00
|
|
|
""" Send paly command. """
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player.play()
|
|
|
|
|
|
|
|
def media_pause(self):
|
2015-10-23 16:39:50 +00:00
|
|
|
""" Send pause command. """
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player.pause()
|
|
|
|
|
|
|
|
def media_next_track(self):
|
|
|
|
""" Send next track command. """
|
|
|
|
self._player.next()
|
|
|
|
|
|
|
|
def media_previous_track(self):
|
|
|
|
""" Send next track command. """
|
|
|
|
self._player.previous()
|
|
|
|
|
|
|
|
def media_seek(self, position):
|
|
|
|
""" Send seek command. """
|
|
|
|
self._player.seek(str(datetime.timedelta(seconds=int(position))))
|
|
|
|
|
|
|
|
def turn_on(self):
|
2015-10-23 16:39:50 +00:00
|
|
|
""" Turn the media player on. """
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player.play()
|