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
|
|
|
"""
|
2017-04-06 06:24:30 +00:00
|
|
|
import asyncio
|
2015-09-11 22:32:47 +00:00
|
|
|
import datetime
|
2017-04-06 06:24:30 +00:00
|
|
|
import functools as ft
|
2016-02-19 05:27:50 +00:00
|
|
|
import logging
|
2016-05-20 16:54:15 +00:00
|
|
|
from os import path
|
2016-07-20 05:37:24 +00:00
|
|
|
import socket
|
2016-10-25 22:37:47 +00:00
|
|
|
import urllib
|
2017-04-06 06:24:30 +00:00
|
|
|
|
2016-07-20 05:37:24 +00:00
|
|
|
import voluptuous as vol
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
from homeassistant.components.media_player import (
|
2016-05-20 16:54:15 +00:00
|
|
|
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
|
|
|
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
2016-07-15 16:00:41 +00:00
|
|
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
2017-01-09 00:09:30 +00:00
|
|
|
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP,
|
|
|
|
SUPPORT_PLAY)
|
2015-09-11 22:32:47 +00:00
|
|
|
from homeassistant.const import (
|
2016-12-02 06:22:03 +00:00
|
|
|
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
|
|
|
|
CONF_HOSTS)
|
2016-05-20 16:54:15 +00:00
|
|
|
from homeassistant.config import load_yaml_config_file
|
2016-07-20 05:37:24 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-11-28 01:45:49 +00:00
|
|
|
from homeassistant.util.dt import utcnow
|
2015-09-13 04:09:51 +00:00
|
|
|
|
2016-11-11 05:01:42 +00:00
|
|
|
REQUIREMENTS = ['SoCo==0.12']
|
2016-11-01 17:42:38 +00:00
|
|
|
|
2015-09-13 04:09:51 +00:00
|
|
|
|
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)
|
|
|
|
|
2016-12-08 08:36:37 +00:00
|
|
|
SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\
|
|
|
|
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
|
|
|
|
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\
|
2017-01-09 00:09:30 +00:00
|
|
|
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
SERVICE_JOIN = 'sonos_join'
|
2016-06-30 21:21:57 +00:00
|
|
|
SERVICE_UNJOIN = 'sonos_unjoin'
|
2016-06-09 04:47:49 +00:00
|
|
|
SERVICE_SNAPSHOT = 'sonos_snapshot'
|
|
|
|
SERVICE_RESTORE = 'sonos_restore'
|
2016-10-26 06:22:17 +00:00
|
|
|
SERVICE_SET_TIMER = 'sonos_set_sleep_timer'
|
|
|
|
SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
|
2017-05-15 07:42:45 +00:00
|
|
|
SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
|
2016-05-20 16:54:15 +00:00
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
DATA_SONOS = 'sonos'
|
|
|
|
|
2016-07-15 16:00:41 +00:00
|
|
|
SUPPORT_SOURCE_LINEIN = 'Line-in'
|
|
|
|
SUPPORT_SOURCE_TV = 'TV'
|
|
|
|
|
2016-12-02 06:22:03 +00:00
|
|
|
CONF_ADVERTISE_ADDR = 'advertise_addr'
|
|
|
|
CONF_INTERFACE_ADDR = 'interface_addr'
|
|
|
|
|
2016-10-26 06:22:17 +00:00
|
|
|
# Service call validation schemas
|
|
|
|
ATTR_SLEEP_TIME = 'sleep_time'
|
2017-05-15 07:42:45 +00:00
|
|
|
ATTR_ALARM_ID = 'alarm_id'
|
|
|
|
ATTR_VOLUME = 'volume'
|
|
|
|
ATTR_ENABLED = 'enabled'
|
|
|
|
ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones'
|
|
|
|
ATTR_TIME = 'time'
|
2017-01-27 06:50:36 +00:00
|
|
|
ATTR_MASTER = 'master'
|
|
|
|
ATTR_WITH_GROUP = 'with_group'
|
2016-10-26 06:22:17 +00:00
|
|
|
|
2017-01-25 21:03:36 +00:00
|
|
|
ATTR_IS_COORDINATOR = 'is_coordinator'
|
|
|
|
|
2016-12-02 06:22:03 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
|
|
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
|
|
|
|
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
|
2016-12-03 23:31:27 +00:00
|
|
|
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]),
|
2016-12-02 06:22:03 +00:00
|
|
|
})
|
|
|
|
|
2016-07-20 05:37:24 +00:00
|
|
|
SONOS_SCHEMA = vol.Schema({
|
2017-01-27 14:37:16 +00:00
|
|
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
2016-07-20 05:37:24 +00:00
|
|
|
})
|
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
SONOS_JOIN_SCHEMA = SONOS_SCHEMA.extend({
|
|
|
|
vol.Required(ATTR_MASTER): cv.entity_id,
|
|
|
|
})
|
|
|
|
|
|
|
|
SONOS_STATES_SCHEMA = SONOS_SCHEMA.extend({
|
|
|
|
vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean,
|
2016-10-26 06:22:17 +00:00
|
|
|
})
|
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
SONOS_SET_TIMER_SCHEMA = SONOS_SCHEMA.extend({
|
|
|
|
vol.Required(ATTR_SLEEP_TIME):
|
|
|
|
vol.All(vol.Coerce(int), vol.Range(min=0, max=86399))
|
|
|
|
})
|
2016-07-20 05:37:24 +00:00
|
|
|
|
2017-05-15 07:42:45 +00:00
|
|
|
SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({
|
|
|
|
vol.Required(ATTR_ALARM_ID): cv.positive_int,
|
|
|
|
vol.Optional(ATTR_TIME): cv.time,
|
|
|
|
vol.Optional(ATTR_VOLUME): cv.small_float,
|
|
|
|
vol.Optional(ATTR_ENABLED): cv.boolean,
|
|
|
|
vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean,
|
|
|
|
})
|
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Set up the Sonos platform."""
|
2015-09-11 22:32:47 +00:00
|
|
|
import soco
|
2017-01-27 06:50:36 +00:00
|
|
|
|
|
|
|
if DATA_SONOS not in hass.data:
|
|
|
|
hass.data[DATA_SONOS] = []
|
2015-09-13 04:09:51 +00:00
|
|
|
|
2016-12-02 06:22:03 +00:00
|
|
|
advertise_addr = config.get(CONF_ADVERTISE_ADDR, None)
|
|
|
|
if advertise_addr:
|
|
|
|
soco.config.EVENT_ADVERTISE_IP = advertise_addr
|
|
|
|
|
2015-11-30 08:55:36 +00:00
|
|
|
if discovery_info:
|
2017-04-12 03:10:02 +00:00
|
|
|
player = soco.SoCo(discovery_info.get('host'))
|
2016-10-20 15:36:48 +00:00
|
|
|
|
2017-05-02 16:18:47 +00:00
|
|
|
# if device already exists by config
|
2017-01-27 06:50:36 +00:00
|
|
|
if player.uid in [x.unique_id for x in hass.data[DATA_SONOS]]:
|
|
|
|
return
|
2016-10-20 15:36:48 +00:00
|
|
|
|
2016-04-17 23:45:16 +00:00
|
|
|
if player.is_visible:
|
2017-04-06 06:24:30 +00:00
|
|
|
device = SonosDevice(player)
|
2016-11-05 23:58:29 +00:00
|
|
|
add_devices([device], True)
|
2017-01-27 06:50:36 +00:00
|
|
|
hass.data[DATA_SONOS].append(device)
|
|
|
|
if len(hass.data[DATA_SONOS]) > 1:
|
|
|
|
return
|
2016-07-20 05:37:24 +00:00
|
|
|
else:
|
2017-01-27 06:50:36 +00:00
|
|
|
players = None
|
|
|
|
hosts = config.get(CONF_HOSTS, None)
|
|
|
|
if hosts:
|
|
|
|
# Support retro compatibility with comma separated list of hosts
|
|
|
|
# from config
|
|
|
|
hosts = hosts[0] if len(hosts) == 1 else hosts
|
|
|
|
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(CONF_INTERFACE_ADDR))
|
|
|
|
|
|
|
|
if not players:
|
2017-05-02 16:18:47 +00:00
|
|
|
_LOGGER.warning("No Sonos speakers found")
|
2017-01-27 06:50:36 +00:00
|
|
|
return
|
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
hass.data[DATA_SONOS] = [SonosDevice(p) for p in players]
|
2017-01-27 06:50:36 +00:00
|
|
|
add_devices(hass.data[DATA_SONOS], True)
|
2017-05-02 16:18:47 +00:00
|
|
|
_LOGGER.info("Added %s Sonos speakers", len(players))
|
2016-10-26 06:22:17 +00:00
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
descriptions = load_yaml_config_file(
|
|
|
|
path.join(path.dirname(__file__), 'services.yaml'))
|
2016-05-17 05:58:57 +00:00
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
def service_handle(service):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle for services."""
|
2017-01-27 06:50:36 +00:00
|
|
|
entity_ids = service.data.get('entity_id')
|
2016-05-17 05:58:57 +00:00
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
if entity_ids:
|
|
|
|
devices = [device for device in hass.data[DATA_SONOS]
|
|
|
|
if device.entity_id in entity_ids]
|
2016-02-26 19:05:34 +00:00
|
|
|
else:
|
2017-01-27 06:50:36 +00:00
|
|
|
devices = hass.data[DATA_SONOS]
|
|
|
|
|
|
|
|
for device in devices:
|
|
|
|
if service.service == SERVICE_JOIN:
|
|
|
|
if device.entity_id != service.data[ATTR_MASTER]:
|
|
|
|
device.join(service.data[ATTR_MASTER])
|
|
|
|
elif service.service == SERVICE_UNJOIN:
|
|
|
|
device.unjoin()
|
|
|
|
elif service.service == SERVICE_SNAPSHOT:
|
|
|
|
device.snapshot(service.data[ATTR_WITH_GROUP])
|
|
|
|
elif service.service == SERVICE_RESTORE:
|
|
|
|
device.restore(service.data[ATTR_WITH_GROUP])
|
|
|
|
elif service.service == SERVICE_SET_TIMER:
|
2017-05-09 16:35:51 +00:00
|
|
|
device.set_sleep_timer(service.data[ATTR_SLEEP_TIME])
|
2017-01-27 06:50:36 +00:00
|
|
|
elif service.service == SERVICE_CLEAR_TIMER:
|
2017-05-09 16:35:51 +00:00
|
|
|
device.clear_sleep_timer()
|
2017-05-15 07:42:45 +00:00
|
|
|
elif service.service == SERVICE_UPDATE_ALARM:
|
|
|
|
device.update_alarm(**service.data)
|
2017-01-27 06:50:36 +00:00
|
|
|
|
|
|
|
device.schedule_update_ha_state(True)
|
|
|
|
|
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, SERVICE_JOIN, service_handle,
|
|
|
|
descriptions.get(SERVICE_JOIN), schema=SONOS_JOIN_SCHEMA)
|
|
|
|
|
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, SERVICE_UNJOIN, service_handle,
|
|
|
|
descriptions.get(SERVICE_UNJOIN), schema=SONOS_SCHEMA)
|
|
|
|
|
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, SERVICE_SNAPSHOT, service_handle,
|
|
|
|
descriptions.get(SERVICE_SNAPSHOT), schema=SONOS_STATES_SCHEMA)
|
|
|
|
|
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, SERVICE_RESTORE, service_handle,
|
|
|
|
descriptions.get(SERVICE_RESTORE), schema=SONOS_STATES_SCHEMA)
|
|
|
|
|
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, SERVICE_SET_TIMER, service_handle,
|
|
|
|
descriptions.get(SERVICE_SET_TIMER), schema=SONOS_SET_TIMER_SCHEMA)
|
|
|
|
|
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, SERVICE_CLEAR_TIMER, service_handle,
|
|
|
|
descriptions.get(SERVICE_CLEAR_TIMER), schema=SONOS_SCHEMA)
|
2016-02-26 19:05:34 +00:00
|
|
|
|
2017-05-15 07:42:45 +00:00
|
|
|
hass.services.register(
|
|
|
|
DOMAIN, SERVICE_UPDATE_ALARM, service_handle,
|
|
|
|
descriptions.get(SERVICE_UPDATE_ALARM),
|
|
|
|
schema=SONOS_UPDATE_ALARM_SCHEMA)
|
|
|
|
|
2016-02-26 19:05:34 +00:00
|
|
|
|
2016-11-01 17:42:38 +00:00
|
|
|
def _parse_timespan(timespan):
|
|
|
|
"""Parse a time-span into number of seconds."""
|
|
|
|
if timespan in ('', 'NOT_IMPLEMENTED', None):
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return sum(60 ** x[0] * int(x[1]) for x in enumerate(
|
|
|
|
reversed(timespan.split(':'))))
|
|
|
|
|
|
|
|
|
|
|
|
class _ProcessSonosEventQueue():
|
|
|
|
"""Queue like object for dispatching sonos events."""
|
|
|
|
|
|
|
|
def __init__(self, sonos_device):
|
|
|
|
self._sonos_device = sonos_device
|
|
|
|
|
|
|
|
def put(self, item, block=True, timeout=None):
|
|
|
|
"""Queue up event for processing."""
|
|
|
|
# Instead of putting events on a queue, dispatch them to the event
|
|
|
|
# processing method.
|
|
|
|
self._sonos_device.process_sonos_event(item)
|
|
|
|
|
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
def _get_entity_from_soco(hass, soco):
|
|
|
|
"""Return SonosDevice from SoCo."""
|
|
|
|
for device in hass.data[DATA_SONOS]:
|
2017-04-06 06:24:30 +00:00
|
|
|
if soco == device.soco:
|
2017-01-27 06:50:36 +00:00
|
|
|
return device
|
2017-05-02 16:18:47 +00:00
|
|
|
raise ValueError("No entity for SoCo device")
|
2017-01-27 06:50:36 +00:00
|
|
|
|
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
def soco_error(funct):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Catch soco exceptions."""
|
2017-04-06 06:24:30 +00:00
|
|
|
@ft.wraps(funct)
|
|
|
|
def wrapper(*args, **kwargs):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Wrap for all soco exception."""
|
2017-04-06 06:24:30 +00:00
|
|
|
from soco.exceptions import SoCoException
|
|
|
|
try:
|
|
|
|
return funct(*args, **kwargs)
|
|
|
|
except SoCoException as err:
|
2017-05-02 16:18:47 +00:00
|
|
|
_LOGGER.error("Error on %s with %s", funct.__name__, err)
|
2017-04-06 06:24:30 +00:00
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
|
|
def soco_coordinator(funct):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Call function on coordinator."""
|
2017-04-06 06:24:30 +00:00
|
|
|
@ft.wraps(funct)
|
|
|
|
def wrapper(device, *args, **kwargs):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Wrap for call to coordinator."""
|
2017-04-06 06:24:30 +00:00
|
|
|
if device.is_coordinator:
|
|
|
|
return funct(device, *args, **kwargs)
|
|
|
|
return funct(device.coordinator, *args, **kwargs)
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
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
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
def __init__(self, player):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Initialize the Sonos device."""
|
2016-04-06 18:32:27 +00:00
|
|
|
self.volume_increment = 5
|
2016-11-05 23:58:29 +00:00
|
|
|
self._unique_id = player.uid
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player = player
|
2016-11-01 17:42:38 +00:00
|
|
|
self._player_volume = None
|
|
|
|
self._player_volume_muted = None
|
2016-10-25 22:37:47 +00:00
|
|
|
self._speaker_info = None
|
2016-09-07 01:31:56 +00:00
|
|
|
self._name = None
|
2016-11-01 17:42:38 +00:00
|
|
|
self._status = None
|
2016-10-25 22:37:47 +00:00
|
|
|
self._coordinator = None
|
|
|
|
self._media_content_id = None
|
|
|
|
self._media_duration = None
|
2016-11-28 01:45:49 +00:00
|
|
|
self._media_position = None
|
|
|
|
self._media_position_updated_at = None
|
2016-10-25 22:37:47 +00:00
|
|
|
self._media_image_url = None
|
|
|
|
self._media_artist = None
|
|
|
|
self._media_album_name = None
|
|
|
|
self._media_title = None
|
2016-11-01 17:42:38 +00:00
|
|
|
self._media_radio_show = None
|
|
|
|
self._media_next_title = None
|
|
|
|
self._support_previous_track = False
|
|
|
|
self._support_next_track = False
|
2016-12-08 08:36:37 +00:00
|
|
|
self._support_stop = False
|
2016-11-01 17:42:38 +00:00
|
|
|
self._support_pause = False
|
|
|
|
self._current_track_uri = None
|
|
|
|
self._current_track_is_radio_stream = False
|
|
|
|
self._queue = None
|
|
|
|
self._last_avtransport_event = None
|
2016-11-05 23:58:29 +00:00
|
|
|
self._is_playing_line_in = None
|
|
|
|
self._is_playing_tv = None
|
2016-12-14 18:05:03 +00:00
|
|
|
self._favorite_sources = None
|
|
|
|
self._source_name = None
|
2017-04-06 06:24:30 +00:00
|
|
|
self._soco_snapshot = None
|
2017-01-27 06:50:36 +00:00
|
|
|
self._snapshot_group = None
|
2015-09-11 23:38:42 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_added_to_hass(self):
|
|
|
|
"""Subscribe sonos events."""
|
2017-04-12 07:51:19 +00:00
|
|
|
self.hass.async_add_job(self._subscribe_to_player_events)
|
2017-04-06 06:24:30 +00:00
|
|
|
|
2015-09-11 23:38:42 +00:00
|
|
|
@property
|
|
|
|
def should_poll(self):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Return the polling state."""
|
2015-09-14 00:49:09 +00:00
|
|
|
return True
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2016-10-20 15:36:48 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return an unique ID."""
|
2016-11-05 23:58:29 +00:00
|
|
|
return self._unique_id
|
2016-10-20 15:36:48 +00:00
|
|
|
|
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 state(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Return the state of the device."""
|
2016-11-01 17:42:38 +00:00
|
|
|
if self._coordinator:
|
|
|
|
return self._coordinator.state
|
|
|
|
if self._status in ('PAUSED_PLAYBACK', 'STOPPED'):
|
2015-09-11 22:32:47 +00:00
|
|
|
return STATE_PAUSED
|
2016-11-01 17:42:38 +00:00
|
|
|
if self._status in ('PLAYING', 'TRANSITIONING'):
|
2015-09-11 22:32:47 +00:00
|
|
|
return STATE_PLAYING
|
2016-10-25 22:37:47 +00:00
|
|
|
if self._status == 'OFF':
|
|
|
|
return STATE_OFF
|
2016-11-01 17:42:38 +00:00
|
|
|
return STATE_IDLE
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2016-02-26 19:05:34 +00:00
|
|
|
@property
|
|
|
|
def is_coordinator(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Return true if player is a coordinator."""
|
2016-11-01 17:42:38 +00:00
|
|
|
return self._coordinator is None
|
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
@property
|
2017-04-06 06:24:30 +00:00
|
|
|
def soco(self):
|
2017-01-27 06:50:36 +00:00
|
|
|
"""Return soco device."""
|
|
|
|
return self._player
|
|
|
|
|
|
|
|
@property
|
|
|
|
def coordinator(self):
|
|
|
|
"""Return coordinator of this player."""
|
|
|
|
return self._coordinator
|
|
|
|
|
2016-11-01 17:42:38 +00:00
|
|
|
def _is_available(self):
|
|
|
|
try:
|
|
|
|
sock = socket.create_connection(
|
2017-05-02 16:18:47 +00:00
|
|
|
address=(self._player.ip_address, 1443), timeout=3)
|
2016-11-01 17:42:38 +00:00
|
|
|
sock.close()
|
|
|
|
return True
|
|
|
|
except socket.error:
|
|
|
|
return False
|
2016-02-26 19:05:34 +00:00
|
|
|
|
2016-11-01 17:42:38 +00:00
|
|
|
# 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)
|
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
def update(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Retrieve latest state."""
|
2016-11-01 17:42:38 +00:00
|
|
|
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)', '')
|
2017-02-07 08:27:55 +00:00
|
|
|
self._favorite_sources = \
|
|
|
|
self._player.get_sonos_favorites()['favorites']
|
2016-11-01 17:42:38 +00:00
|
|
|
|
|
|
|
if self._last_avtransport_event:
|
|
|
|
is_available = True
|
|
|
|
else:
|
|
|
|
is_available = self._is_available()
|
|
|
|
|
2017-02-07 08:27:55 +00:00
|
|
|
if not is_available:
|
2016-11-01 17:42:38 +00:00
|
|
|
self._player_volume = None
|
|
|
|
self._player_volume_muted = None
|
2016-10-25 22:37:47 +00:00
|
|
|
self._status = 'OFF'
|
|
|
|
self._coordinator = None
|
|
|
|
self._media_content_id = None
|
|
|
|
self._media_duration = None
|
2016-11-28 01:45:49 +00:00
|
|
|
self._media_position = None
|
|
|
|
self._media_position_updated_at = None
|
2016-10-25 22:37:47 +00:00
|
|
|
self._media_image_url = None
|
|
|
|
self._media_artist = None
|
|
|
|
self._media_album_name = None
|
|
|
|
self._media_title = None
|
2016-11-01 17:42:38 +00:00
|
|
|
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
|
2016-12-08 08:36:37 +00:00
|
|
|
self._support_stop = False
|
2016-11-01 17:42:38 +00:00
|
|
|
self._support_pause = False
|
2016-11-05 23:58:29 +00:00
|
|
|
self._is_playing_tv = False
|
|
|
|
self._is_playing_line_in = False
|
2016-12-14 18:05:03 +00:00
|
|
|
self._source_name = None
|
2017-02-07 08:27:55 +00:00
|
|
|
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
|
2016-11-01 17:42:38 +00:00
|
|
|
self._last_avtransport_event = None
|
|
|
|
|
2016-12-18 20:58:59 +00:00
|
|
|
def _format_media_image_url(self, url, fallback_uri):
|
|
|
|
if url in ('', 'NOT_IMPLEMENTED', None):
|
2017-01-25 21:15:30 +00:00
|
|
|
if fallback_uri in ('', 'NOT_IMPLEMENTED', None):
|
|
|
|
return None
|
2016-12-18 20:58:59 +00:00
|
|
|
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
|
2016-11-01 17:42:38 +00:00
|
|
|
|
|
|
|
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(
|
2016-12-18 20:58:59 +00:00
|
|
|
None,
|
2016-11-01 17:42:38 +00:00
|
|
|
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'
|
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
self.schedule_update_ha_state(True)
|
2016-11-01 17:42:38 +00:00
|
|
|
|
|
|
|
if next_track_image_url:
|
|
|
|
self.preload_media_image_url(next_track_image_url)
|
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)."""
|
2016-11-01 17:42:38 +00:00
|
|
|
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."""
|
2016-11-01 17:42:38 +00:00
|
|
|
return self._player_volume_muted
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_content_id(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Content ID of current playing media."""
|
2016-10-25 22:37:47 +00:00
|
|
|
if self._coordinator:
|
|
|
|
return self._coordinator.media_content_id
|
|
|
|
else:
|
|
|
|
return self._media_content_id
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
@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."""
|
2016-10-25 22:37:47 +00:00
|
|
|
if self._coordinator:
|
|
|
|
return self._coordinator.media_duration
|
|
|
|
else:
|
|
|
|
return self._media_duration
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2016-11-28 01:45:49 +00:00
|
|
|
@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
|
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
@property
|
|
|
|
def media_image_url(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Image url of current playing media."""
|
2016-10-25 22:37:47 +00:00
|
|
|
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
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_title(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Title of current playing media."""
|
2016-10-25 22:37:47 +00:00
|
|
|
if self._coordinator:
|
|
|
|
return self._coordinator.media_title
|
|
|
|
else:
|
|
|
|
return self._media_title
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
@property
|
2017-02-08 04:42:45 +00:00
|
|
|
def supported_features(self):
|
|
|
|
"""Flag media player features that are supported."""
|
2016-11-01 17:42:38 +00:00
|
|
|
if self._coordinator:
|
2017-02-08 04:42:45 +00:00
|
|
|
return self._coordinator.supported_features
|
2016-11-01 17:42:38 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2016-12-08 08:36:37 +00:00
|
|
|
if not self._support_stop:
|
|
|
|
supported = supported ^ SUPPORT_STOP
|
|
|
|
|
2016-11-01 17:42:38 +00:00
|
|
|
if not self._support_pause:
|
|
|
|
supported = supported ^ SUPPORT_PAUSE
|
2016-10-25 22:37:47 +00:00
|
|
|
|
2016-11-01 17:42:38 +00:00
|
|
|
return supported
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
2015-09-11 22:32:47 +00:00
|
|
|
def volume_up(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Volume up media player."""
|
2016-04-06 18:32:27 +00:00
|
|
|
self._player.volume += self.volume_increment
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
2015-09-11 22:32:47 +00:00
|
|
|
def volume_down(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Volume down media player."""
|
2016-04-06 18:32:27 +00:00
|
|
|
self._player.volume -= self.volume_increment
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
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))
|
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
2015-09-11 22:32:47 +00:00
|
|
|
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
|
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_coordinator
|
2016-07-15 16:00:41 +00:00
|
|
|
def select_source(self, source):
|
|
|
|
"""Select input source."""
|
2017-04-06 06:24:30 +00:00
|
|
|
if source == SUPPORT_SOURCE_LINEIN:
|
2016-12-14 18:05:03 +00:00
|
|
|
self._source_name = SUPPORT_SOURCE_LINEIN
|
2016-07-15 16:00:41 +00:00
|
|
|
self._player.switch_to_line_in()
|
|
|
|
elif source == SUPPORT_SOURCE_TV:
|
2016-12-14 18:05:03 +00:00
|
|
|
self._source_name = SUPPORT_SOURCE_TV
|
2016-07-15 16:00:41 +00:00
|
|
|
self._player.switch_to_tv()
|
2016-12-14 18:05:03 +00:00
|
|
|
else:
|
2017-02-07 08:27:55 +00:00
|
|
|
fav = [fav for fav in self._favorite_sources
|
|
|
|
if fav['title'] == source]
|
2016-12-14 18:05:03 +00:00
|
|
|
if len(fav) == 1:
|
|
|
|
src = fav.pop()
|
|
|
|
self._source_name = src['title']
|
|
|
|
self._player.play_uri(src['uri'], src['meta'], src['title'])
|
2016-07-15 16:00:41 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def source_list(self):
|
|
|
|
"""List of available input sources."""
|
2017-02-07 08:27:55 +00:00
|
|
|
if self._coordinator:
|
|
|
|
return self._coordinator.source_list
|
|
|
|
|
2016-10-25 22:37:47 +00:00
|
|
|
model_name = self._speaker_info['model_name']
|
2017-02-07 08:27:55 +00:00
|
|
|
sources = []
|
2016-07-15 16:00:41 +00:00
|
|
|
|
2017-02-07 08:27:55 +00:00
|
|
|
if self._favorite_sources:
|
|
|
|
for fav in self._favorite_sources:
|
|
|
|
sources.append(fav['title'])
|
2016-12-14 18:05:03 +00:00
|
|
|
|
2016-10-25 22:37:47 +00:00
|
|
|
if 'PLAY:5' in model_name:
|
2016-12-14 18:05:03 +00:00
|
|
|
sources += [SUPPORT_SOURCE_LINEIN]
|
2016-10-25 22:37:47 +00:00
|
|
|
elif 'PLAYBAR' in model_name:
|
2016-12-14 18:05:03 +00:00
|
|
|
sources += [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV]
|
|
|
|
return sources
|
2016-07-15 16:00:41 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def source(self):
|
|
|
|
"""Name of the current input source."""
|
2016-11-19 15:29:00 +00:00
|
|
|
if self._coordinator:
|
|
|
|
return self._coordinator.source
|
|
|
|
else:
|
2016-12-14 18:05:03 +00:00
|
|
|
return self._source_name
|
2016-07-15 16:00:41 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
2016-03-31 19:36:58 +00:00
|
|
|
def turn_off(self):
|
|
|
|
"""Turn off media player."""
|
2016-11-01 17:42:38 +00:00
|
|
|
self.media_pause()
|
2016-03-31 19:36:58 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_coordinator
|
2015-09-11 22:32:47 +00:00
|
|
|
def media_play(self):
|
2016-03-26 05:57:28 +00:00
|
|
|
"""Send play command."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.play()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_coordinator
|
2016-12-08 08:36:37 +00:00
|
|
|
def media_stop(self):
|
|
|
|
"""Send stop command."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.stop()
|
2016-12-08 08:36:37 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_coordinator
|
2015-09-11 22:32:47 +00:00
|
|
|
def media_pause(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Send pause command."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.pause()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_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."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.next()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_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."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.previous()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_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."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.seek(str(datetime.timedelta(seconds=int(position))))
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_coordinator
|
2016-07-15 16:00:41 +00:00
|
|
|
def clear_playlist(self):
|
|
|
|
"""Clear players playlist."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.clear_queue()
|
2016-07-15 16:00:41 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
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."""
|
2016-11-01 17:42:38 +00:00
|
|
|
self.media_play()
|
2016-03-26 05:57:14 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_coordinator
|
2016-05-20 06:30:19 +00:00
|
|
|
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.
|
|
|
|
"""
|
2017-04-06 06:24:30 +00:00
|
|
|
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)
|
2016-05-20 06:30:19 +00:00
|
|
|
else:
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.play_uri(media_id)
|
2016-04-17 20:17:13 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
2017-01-27 06:50:36 +00:00
|
|
|
def join(self, master):
|
|
|
|
"""Join the player to a group."""
|
2017-01-27 14:37:16 +00:00
|
|
|
coord = [device for device in self.hass.data[DATA_SONOS]
|
2017-01-27 06:50:36 +00:00
|
|
|
if device.entity_id == master]
|
|
|
|
|
|
|
|
if coord and master != self.entity_id:
|
2017-01-27 14:37:16 +00:00
|
|
|
coord = coord[0]
|
2017-04-06 06:24:30 +00:00
|
|
|
if coord.soco.group.coordinator != coord.soco:
|
|
|
|
coord.soco.unjoin()
|
|
|
|
self._player.join(coord.soco)
|
2017-01-27 14:37:16 +00:00
|
|
|
self._coordinator = coord
|
2016-10-25 22:37:47 +00:00
|
|
|
else:
|
2017-01-27 06:50:36 +00:00
|
|
|
_LOGGER.error("Master not found %s", master)
|
2016-05-20 16:54:15 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
2016-06-30 21:21:57 +00:00
|
|
|
def unjoin(self):
|
|
|
|
"""Unjoin the player from a group."""
|
|
|
|
self._player.unjoin()
|
2017-01-27 14:37:16 +00:00
|
|
|
self._coordinator = None
|
2016-06-30 21:21:57 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
2017-01-27 06:50:36 +00:00
|
|
|
def snapshot(self, with_group=True):
|
2016-06-09 04:47:49 +00:00
|
|
|
"""Snapshot the player."""
|
2017-04-06 06:24:30 +00:00
|
|
|
from soco.snapshot import Snapshot
|
|
|
|
|
|
|
|
self._soco_snapshot = Snapshot(self._player)
|
|
|
|
self._soco_snapshot.snapshot()
|
2016-06-09 04:47:49 +00:00
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
if with_group:
|
|
|
|
self._snapshot_group = self._player.group
|
|
|
|
if self._coordinator:
|
|
|
|
self._coordinator.snapshot(False)
|
|
|
|
else:
|
|
|
|
self._snapshot_group = None
|
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
2017-01-27 06:50:36 +00:00
|
|
|
def restore(self, with_group=True):
|
2016-06-09 04:47:49 +00:00
|
|
|
"""Restore snapshot for the player."""
|
2017-01-27 06:50:36 +00:00
|
|
|
from soco.exceptions import SoCoException
|
|
|
|
try:
|
|
|
|
# need catch exception if a coordinator is going to slave.
|
|
|
|
# this state will recover with group part.
|
2017-04-06 06:24:30 +00:00
|
|
|
self._soco_snapshot.restore(False)
|
|
|
|
except (TypeError, AttributeError, SoCoException):
|
2017-01-27 06:50:36 +00:00
|
|
|
_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)
|
2016-06-09 04:47:49 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_coordinator
|
2016-10-26 06:22:17 +00:00
|
|
|
def set_sleep_timer(self, sleep_time):
|
|
|
|
"""Set the timer on the player."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.set_sleep_timer(sleep_time)
|
2016-10-26 06:22:17 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_error
|
|
|
|
@soco_coordinator
|
2016-10-26 06:22:17 +00:00
|
|
|
def clear_sleep_timer(self):
|
|
|
|
"""Clear the timer on the player."""
|
2017-04-06 06:24:30 +00:00
|
|
|
self._player.set_sleep_timer(None)
|
2017-01-25 21:03:36 +00:00
|
|
|
|
2017-05-15 07:42:45 +00:00
|
|
|
@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()
|
|
|
|
|
2017-01-25 21:03:36 +00:00
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return device specific state attributes."""
|
|
|
|
return {ATTR_IS_COORDINATOR: self.is_coordinator}
|