2019-02-13 20:21:14 +00:00
|
|
|
"""Support to interface with Sonos players."""
|
2019-03-21 05:56:46 +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-07-20 05:37:24 +00:00
|
|
|
import socket
|
2019-04-29 08:20:09 +00:00
|
|
|
import time
|
2018-09-09 12:26:06 +00:00
|
|
|
import urllib
|
2017-04-06 06:24:30 +00:00
|
|
|
|
2019-03-13 19:51:41 +00:00
|
|
|
import async_timeout
|
2019-04-27 17:05:50 +00:00
|
|
|
import pysonos
|
|
|
|
import pysonos.snapshot
|
|
|
|
from pysonos.exceptions import SoCoUPnPException, SoCoException
|
|
|
|
|
2019-04-26 06:56:43 +00:00
|
|
|
from homeassistant.components.media_player import MediaPlayerDevice
|
2019-02-08 22:18:18 +00:00
|
|
|
from homeassistant.components.media_player.const import (
|
2019-05-05 19:25:57 +00:00
|
|
|
ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST,
|
2019-03-21 05:56:46 +00:00
|
|
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
|
|
|
|
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE,
|
|
|
|
SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
|
2015-09-11 22:32:47 +00:00
|
|
|
from homeassistant.const import (
|
2019-05-29 18:12:44 +00:00
|
|
|
ENTITY_MATCH_ALL, STATE_IDLE, STATE_PAUSED, STATE_PLAYING)
|
2019-05-05 19:25:57 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
2016-11-28 01:45:49 +00:00
|
|
|
from homeassistant.util.dt import utcnow
|
2015-09-13 04:09:51 +00:00
|
|
|
|
2019-04-26 06:56:43 +00:00
|
|
|
from . import (
|
|
|
|
CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR,
|
2019-05-10 20:37:03 +00:00
|
|
|
DATA_SERVICE_EVENT, DOMAIN as SONOS_DOMAIN,
|
2019-05-05 19:25:57 +00:00
|
|
|
ATTR_ALARM_ID, ATTR_ENABLED, ATTR_INCLUDE_LINKED_ZONES, ATTR_MASTER,
|
|
|
|
ATTR_NIGHT_SOUND, ATTR_SLEEP_TIME, ATTR_SPEECH_ENHANCE, ATTR_TIME,
|
|
|
|
ATTR_VOLUME, ATTR_WITH_GROUP,
|
|
|
|
SERVICE_CLEAR_TIMER, SERVICE_JOIN, SERVICE_RESTORE, SERVICE_SET_OPTION,
|
|
|
|
SERVICE_SET_TIMER, SERVICE_SNAPSHOT, SERVICE_UNJOIN, SERVICE_UPDATE_ALARM)
|
2016-11-01 17:42:38 +00:00
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-11-17 12:18:51 +00:00
|
|
|
PARALLEL_UPDATES = 0
|
|
|
|
|
2019-04-29 08:20:09 +00:00
|
|
|
DISCOVERY_INTERVAL = 60
|
|
|
|
|
2018-09-20 21:50:11 +00:00
|
|
|
# Quiet down pysonos logging to just actual problems.
|
|
|
|
logging.getLogger('pysonos').setLevel(logging.WARNING)
|
|
|
|
logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR)
|
2015-09-17 03:54:43 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
SUPPORT_SONOS = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
2018-03-05 02:30:15 +00:00
|
|
|
SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_SELECT_SOURCE |\
|
|
|
|
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\
|
|
|
|
SUPPORT_PLAY_MEDIA | SUPPORT_SHUFFLE_SET | SUPPORT_CLEAR_PLAYLIST
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2019-02-24 17:45:08 +00:00
|
|
|
DATA_SONOS = 'sonos_media_player'
|
2017-01-27 06:50:36 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
SOURCE_LINEIN = 'Line-in'
|
|
|
|
SOURCE_TV = 'TV'
|
2016-07-15 16:00:41 +00:00
|
|
|
|
2018-04-25 18:31:42 +00:00
|
|
|
ATTR_SONOS_GROUP = 'sonos_group'
|
2017-01-25 21:03:36 +00:00
|
|
|
|
2018-10-01 15:58:04 +00:00
|
|
|
UPNP_ERRORS_TO_IGNORE = ['701', '711', '712']
|
2017-06-02 07:03:10 +00:00
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
class SonosData:
|
|
|
|
"""Storage class for platform global data."""
|
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
def __init__(self, hass):
|
2018-02-18 19:05:20 +00:00
|
|
|
"""Initialize the data."""
|
2019-02-24 17:45:08 +00:00
|
|
|
self.entities = []
|
2019-05-23 04:09:59 +00:00
|
|
|
self.topology_condition = asyncio.Condition()
|
2018-02-18 19:05:20 +00:00
|
|
|
|
|
|
|
|
2019-04-26 06:56:43 +00:00
|
|
|
async def async_setup_platform(hass,
|
|
|
|
config,
|
|
|
|
async_add_entities,
|
|
|
|
discovery_info=None):
|
|
|
|
"""Set up the Sonos platform. Obsolete."""
|
|
|
|
_LOGGER.error(
|
|
|
|
'Loading Sonos by media_player platform config is no longer supported')
|
2018-06-14 19:17:54 +00:00
|
|
|
|
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
2018-06-14 19:17:54 +00:00
|
|
|
"""Set up Sonos from a config entry."""
|
2017-01-27 06:50:36 +00:00
|
|
|
if DATA_SONOS not in hass.data:
|
2019-03-13 09:17:09 +00:00
|
|
|
hass.data[DATA_SONOS] = SonosData(hass)
|
2015-09-13 04:09:51 +00:00
|
|
|
|
2019-04-26 06:56:43 +00:00
|
|
|
config = hass.data[SONOS_DOMAIN].get('media_player', {})
|
|
|
|
|
2018-08-17 05:41:56 +00:00
|
|
|
advertise_addr = config.get(CONF_ADVERTISE_ADDR)
|
|
|
|
if advertise_addr:
|
2018-09-20 21:50:11 +00:00
|
|
|
pysonos.config.EVENT_ADVERTISE_IP = advertise_addr
|
2018-08-17 05:41:56 +00:00
|
|
|
|
2019-04-29 08:20:09 +00:00
|
|
|
def _discovery(now=None):
|
|
|
|
"""Discover players from network or configuration."""
|
2018-03-15 19:43:28 +00:00
|
|
|
hosts = config.get(CONF_HOSTS)
|
2019-04-26 06:56:43 +00:00
|
|
|
|
2019-04-29 08:20:09 +00:00
|
|
|
def _discovered_player(soco):
|
|
|
|
"""Handle a (re)discovered player."""
|
|
|
|
try:
|
|
|
|
# Make sure that the player is available
|
|
|
|
_ = soco.volume
|
|
|
|
|
|
|
|
entity = _get_entity_from_soco_uid(hass, soco.uid)
|
|
|
|
if not entity:
|
|
|
|
hass.add_job(async_add_entities, [SonosEntity(soco)])
|
|
|
|
else:
|
|
|
|
entity.seen()
|
|
|
|
except SoCoException:
|
|
|
|
pass
|
|
|
|
|
2017-01-27 06:50:36 +00:00
|
|
|
if hosts:
|
|
|
|
for host in hosts:
|
2017-12-08 11:01:10 +00:00
|
|
|
try:
|
2019-04-29 08:20:09 +00:00
|
|
|
player = pysonos.SoCo(socket.gethostbyname(host))
|
|
|
|
if player.is_visible:
|
|
|
|
_discovered_player(player)
|
2019-04-27 17:05:50 +00:00
|
|
|
except (OSError, SoCoException):
|
2019-04-29 08:20:09 +00:00
|
|
|
if now is None:
|
|
|
|
_LOGGER.warning("Failed to initialize '%s'", host)
|
2018-03-15 19:43:28 +00:00
|
|
|
else:
|
2019-04-29 08:20:09 +00:00
|
|
|
pysonos.discover_thread(
|
|
|
|
_discovered_player,
|
|
|
|
interface_addr=config.get(CONF_INTERFACE_ADDR))
|
2017-01-27 06:50:36 +00:00
|
|
|
|
2019-05-29 22:05:12 +00:00
|
|
|
for entity in hass.data[DATA_SONOS].entities:
|
|
|
|
entity.check_unseen()
|
|
|
|
|
2019-04-29 08:20:09 +00:00
|
|
|
hass.helpers.event.call_later(DISCOVERY_INTERVAL, _discovery)
|
2019-04-26 06:56:43 +00:00
|
|
|
|
2019-04-29 08:20:09 +00:00
|
|
|
hass.async_add_executor_job(_discovery)
|
2016-10-26 06:22:17 +00:00
|
|
|
|
2019-05-05 19:25:57 +00:00
|
|
|
async def async_service_handle(service, data):
|
|
|
|
"""Handle dispatched services."""
|
|
|
|
entity_ids = data.get('entity_id')
|
2019-02-24 17:45:08 +00:00
|
|
|
entities = hass.data[DATA_SONOS].entities
|
2019-05-05 19:25:57 +00:00
|
|
|
if entity_ids and entity_ids != ENTITY_MATCH_ALL:
|
2019-02-24 17:45:08 +00:00
|
|
|
entities = [e for e in entities if e.entity_id in entity_ids]
|
2018-02-18 19:05:20 +00:00
|
|
|
|
2019-05-05 19:25:57 +00:00
|
|
|
if service == SERVICE_JOIN:
|
2019-03-13 09:17:09 +00:00
|
|
|
master = [e for e in hass.data[DATA_SONOS].entities
|
2019-05-05 19:25:57 +00:00
|
|
|
if e.entity_id == data[ATTR_MASTER]]
|
2019-03-13 09:17:09 +00:00
|
|
|
if master:
|
|
|
|
await SonosEntity.join_multi(hass, master[0], entities)
|
2019-05-05 19:25:57 +00:00
|
|
|
elif service == SERVICE_UNJOIN:
|
2019-03-13 09:17:09 +00:00
|
|
|
await SonosEntity.unjoin_multi(hass, entities)
|
2019-05-05 19:25:57 +00:00
|
|
|
elif service == SERVICE_SNAPSHOT:
|
2019-03-13 09:17:09 +00:00
|
|
|
await SonosEntity.snapshot_multi(
|
2019-05-05 19:25:57 +00:00
|
|
|
hass, entities, data[ATTR_WITH_GROUP])
|
|
|
|
elif service == SERVICE_RESTORE:
|
2019-03-13 09:17:09 +00:00
|
|
|
await SonosEntity.restore_multi(
|
2019-05-05 19:25:57 +00:00
|
|
|
hass, entities, data[ATTR_WITH_GROUP])
|
|
|
|
else:
|
|
|
|
for entity in entities:
|
|
|
|
if service == SERVICE_SET_TIMER:
|
|
|
|
call = entity.set_sleep_timer
|
|
|
|
elif service == SERVICE_CLEAR_TIMER:
|
|
|
|
call = entity.clear_sleep_timer
|
|
|
|
elif service == SERVICE_UPDATE_ALARM:
|
|
|
|
call = entity.set_alarm
|
|
|
|
elif service == SERVICE_SET_OPTION:
|
|
|
|
call = entity.set_option
|
2016-02-26 19:05:34 +00:00
|
|
|
|
2019-05-05 19:25:57 +00:00
|
|
|
hass.async_add_executor_job(call, data)
|
2017-05-15 07:42:45 +00:00
|
|
|
|
2019-05-10 20:37:03 +00:00
|
|
|
# We are ready for the next service call
|
|
|
|
hass.data[DATA_SERVICE_EVENT].set()
|
|
|
|
|
2019-05-05 19:25:57 +00:00
|
|
|
async_dispatcher_connect(hass, SONOS_DOMAIN, async_service_handle)
|
2017-12-17 12:08:35 +00:00
|
|
|
|
2016-02-26 19:05:34 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
class _ProcessSonosEventQueue:
|
2016-11-01 17:42:38 +00:00
|
|
|
"""Queue like object for dispatching sonos events."""
|
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
def __init__(self, handler):
|
2018-01-21 06:35:38 +00:00
|
|
|
"""Initialize Sonos event queue."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self._handler = handler
|
2016-11-01 17:42:38 +00:00
|
|
|
|
|
|
|
def put(self, item, block=True, timeout=None):
|
2018-02-18 19:05:20 +00:00
|
|
|
"""Process event."""
|
|
|
|
self._handler(item)
|
2016-11-01 17:42:38 +00:00
|
|
|
|
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
def _get_entity_from_soco_uid(hass, uid):
|
2019-02-24 17:45:08 +00:00
|
|
|
"""Return SonosEntity from SoCo uid."""
|
|
|
|
for entity in hass.data[DATA_SONOS].entities:
|
|
|
|
if uid == entity.unique_id:
|
2018-02-18 19:05:20 +00:00
|
|
|
return entity
|
|
|
|
return None
|
2017-01-27 06:50:36 +00:00
|
|
|
|
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
def soco_error(errorcodes=None):
|
|
|
|
"""Filter out specified UPnP errors from logs and avoid exceptions."""
|
2017-06-02 07:03:10 +00:00
|
|
|
def decorator(funct):
|
2018-01-21 06:35:38 +00:00
|
|
|
"""Decorate functions."""
|
2017-06-02 07:03:10 +00:00
|
|
|
@ft.wraps(funct)
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
"""Wrap for all soco UPnP exception."""
|
|
|
|
try:
|
|
|
|
return funct(*args, **kwargs)
|
|
|
|
except SoCoUPnPException as err:
|
2018-02-21 20:11:14 +00:00
|
|
|
if errorcodes and err.error_code in errorcodes:
|
2017-06-02 07:03:10 +00:00
|
|
|
pass
|
|
|
|
else:
|
2018-01-13 08:59:50 +00:00
|
|
|
_LOGGER.error("Error on %s with %s", funct.__name__, err)
|
|
|
|
except SoCoException as err:
|
|
|
|
_LOGGER.error("Error on %s with %s", funct.__name__, err)
|
2017-06-02 07:03:10 +00:00
|
|
|
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
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)
|
2019-02-24 17:45:08 +00:00
|
|
|
def wrapper(entity, *args, **kwargs):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Wrap for call to coordinator."""
|
2019-02-24 17:45:08 +00:00
|
|
|
if entity.is_coordinator:
|
|
|
|
return funct(entity, *args, **kwargs)
|
|
|
|
return funct(entity.coordinator, *args, **kwargs)
|
2017-04-06 06:24:30 +00:00
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
def _timespan_secs(timespan):
|
|
|
|
"""Parse a time-span into number of seconds."""
|
|
|
|
if timespan in ('', 'NOT_IMPLEMENTED', None):
|
|
|
|
return None
|
|
|
|
|
|
|
|
return sum(60 ** x[0] * int(x[1]) for x in enumerate(
|
|
|
|
reversed(timespan.split(':'))))
|
|
|
|
|
|
|
|
|
|
|
|
def _is_radio_uri(uri):
|
|
|
|
"""Return whether the URI is a radio stream."""
|
2018-03-04 13:02:31 +00:00
|
|
|
radio_schemes = (
|
|
|
|
'x-rincon-mp3radio:', 'x-sonosapi-stream:', 'x-sonosapi-radio:',
|
2018-03-23 17:09:18 +00:00
|
|
|
'x-sonosapi-hls:', 'hls-radio:')
|
2018-03-04 13:02:31 +00:00
|
|
|
return uri.startswith(radio_schemes)
|
2018-02-18 19:05:20 +00:00
|
|
|
|
|
|
|
|
2019-02-24 17:45:08 +00:00
|
|
|
class SonosEntity(MediaPlayerDevice):
|
|
|
|
"""Representation of a Sonos entity."""
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-04-06 06:24:30 +00:00
|
|
|
def __init__(self, player):
|
2019-02-24 17:45:08 +00:00
|
|
|
"""Initialize the Sonos entity."""
|
2019-04-29 08:20:09 +00:00
|
|
|
self._seen = None
|
2018-11-17 12:18:51 +00:00
|
|
|
self._subscriptions = []
|
2018-03-21 02:27:07 +00:00
|
|
|
self._receives_events = False
|
2018-10-03 13:09:05 +00:00
|
|
|
self._volume_increment = 2
|
2016-11-05 23:58:29 +00:00
|
|
|
self._unique_id = player.uid
|
2015-09-11 22:32:47 +00:00
|
|
|
self._player = player
|
2018-02-18 19:05:20 +00:00
|
|
|
self._model = None
|
2016-11-01 17:42:38 +00:00
|
|
|
self._player_volume = None
|
2018-03-21 02:27:07 +00:00
|
|
|
self._player_muted = None
|
2018-10-03 12:50:13 +00:00
|
|
|
self._shuffle = None
|
2016-09-07 01:31:56 +00:00
|
|
|
self._name = None
|
2016-10-25 22:37:47 +00:00
|
|
|
self._coordinator = None
|
2019-02-25 21:03:15 +00:00
|
|
|
self._sonos_group = [self]
|
2018-02-18 19:05:20 +00:00
|
|
|
self._status = None
|
2016-10-25 22:37:47 +00:00
|
|
|
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
|
2017-12-17 12:08:35 +00:00
|
|
|
self._night_sound = None
|
|
|
|
self._speech_enhance = None
|
2016-12-14 18:05:03 +00:00
|
|
|
self._source_name = None
|
2018-02-18 19:05:20 +00:00
|
|
|
self._available = True
|
|
|
|
self._favorites = 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
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
self._set_basic_information()
|
2019-04-29 08:20:09 +00:00
|
|
|
self.seen()
|
2018-02-18 19:05:20 +00:00
|
|
|
|
2018-10-01 06:58:21 +00:00
|
|
|
async def async_added_to_hass(self):
|
2017-04-06 06:24:30 +00:00
|
|
|
"""Subscribe sonos events."""
|
2019-02-24 17:45:08 +00:00
|
|
|
self.hass.data[DATA_SONOS].entities.append(self)
|
2018-10-04 07:20:20 +00:00
|
|
|
self.hass.async_add_executor_job(self._subscribe_to_player_events)
|
2017-04-06 06:24:30 +00:00
|
|
|
|
2016-10-20 15:36:48 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
2018-03-03 18:23:55 +00:00
|
|
|
"""Return a unique ID."""
|
2016-11-05 23:58:29 +00:00
|
|
|
return self._unique_id
|
2016-10-20 15:36:48 +00:00
|
|
|
|
2019-02-25 21:03:15 +00:00
|
|
|
def __hash__(self):
|
|
|
|
"""Return a hash of self."""
|
|
|
|
return hash(self.unique_id)
|
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
2019-02-24 17:45:08 +00:00
|
|
|
"""Return the name of the entity."""
|
2015-09-11 22:32:47 +00:00
|
|
|
return self._name
|
|
|
|
|
2018-08-29 14:27:08 +00:00
|
|
|
@property
|
|
|
|
def device_info(self):
|
|
|
|
"""Return information about the device."""
|
|
|
|
return {
|
|
|
|
'identifiers': {
|
|
|
|
(SONOS_DOMAIN, self._unique_id)
|
|
|
|
},
|
|
|
|
'name': self._name,
|
|
|
|
'model': self._model.replace("Sonos ", ""),
|
|
|
|
'manufacturer': 'Sonos',
|
|
|
|
}
|
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2015-09-11 22:32:47 +00:00
|
|
|
def state(self):
|
2019-02-24 17:45:08 +00:00
|
|
|
"""Return the state of the entity."""
|
2016-11-01 17:42:38 +00:00
|
|
|
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-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):
|
2019-02-24 17:45:08 +00:00
|
|
|
"""Return soco object."""
|
2017-01-27 06:50:36 +00:00
|
|
|
return self._player
|
|
|
|
|
|
|
|
@property
|
|
|
|
def coordinator(self):
|
|
|
|
"""Return coordinator of this player."""
|
|
|
|
return self._coordinator
|
|
|
|
|
2019-04-29 08:20:09 +00:00
|
|
|
def seen(self):
|
|
|
|
"""Record that this player was seen right now."""
|
|
|
|
self._seen = time.monotonic()
|
|
|
|
|
2019-05-29 22:05:12 +00:00
|
|
|
if self._available:
|
|
|
|
return
|
|
|
|
|
|
|
|
self._available = True
|
|
|
|
self._set_basic_information()
|
|
|
|
self._subscribe_to_player_events()
|
|
|
|
self.schedule_update_ha_state()
|
|
|
|
|
|
|
|
def check_unseen(self):
|
|
|
|
"""Make this player unavailable if it was not seen recently."""
|
|
|
|
if not self._available:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._seen < time.monotonic() - 2*DISCOVERY_INTERVAL:
|
|
|
|
self._available = False
|
|
|
|
|
|
|
|
def _unsub(subscriptions):
|
|
|
|
for subscription in subscriptions:
|
|
|
|
subscription.unsubscribe()
|
|
|
|
self.hass.add_job(_unsub, self._subscriptions)
|
|
|
|
|
|
|
|
self._subscriptions = []
|
|
|
|
|
|
|
|
self.schedule_update_ha_state()
|
|
|
|
|
2017-08-31 22:23:11 +00:00
|
|
|
@property
|
|
|
|
def available(self) -> bool:
|
|
|
|
"""Return True if entity is available."""
|
|
|
|
return self._available
|
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
def _set_basic_information(self):
|
2019-02-24 17:45:08 +00:00
|
|
|
"""Set initial entity information."""
|
2018-02-18 19:05:20 +00:00
|
|
|
speaker_info = self.soco.get_speaker_info(True)
|
|
|
|
self._name = speaker_info['zone_name']
|
|
|
|
self._model = speaker_info['model_name']
|
2018-10-03 12:50:13 +00:00
|
|
|
self._shuffle = self.soco.shuffle
|
2018-03-21 02:27:07 +00:00
|
|
|
|
|
|
|
self.update_volume()
|
2018-03-14 19:08:41 +00:00
|
|
|
|
2018-07-23 10:31:03 +00:00
|
|
|
self._set_favorites()
|
|
|
|
|
|
|
|
def _set_favorites(self):
|
|
|
|
"""Set available favorites."""
|
2019-04-09 00:32:56 +00:00
|
|
|
self._favorites = self.soco.music_library.get_sonos_favorites()
|
2018-02-18 19:05:20 +00:00
|
|
|
|
2018-03-21 02:27:07 +00:00
|
|
|
def _radio_artwork(self, url):
|
|
|
|
"""Return the private URL with artwork for a radio stream."""
|
|
|
|
if url not in ('', 'NOT_IMPLEMENTED', None):
|
|
|
|
if url.find('tts_proxy') > 0:
|
|
|
|
# If the content is a tts don't try to fetch an image from it.
|
|
|
|
return None
|
|
|
|
url = 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
|
|
|
|
host=self.soco.ip_address,
|
|
|
|
port=1400,
|
|
|
|
uri=urllib.parse.quote(url, safe='')
|
|
|
|
)
|
|
|
|
return url
|
|
|
|
|
2016-11-01 17:42:38 +00:00
|
|
|
def _subscribe_to_player_events(self):
|
2018-02-18 19:05:20 +00:00
|
|
|
"""Add event subscriptions."""
|
2018-03-21 02:27:07 +00:00
|
|
|
self._receives_events = False
|
2018-02-18 19:05:20 +00:00
|
|
|
|
2018-03-08 23:39:31 +00:00
|
|
|
# New player available, build the current group topology
|
2019-02-24 17:45:08 +00:00
|
|
|
for entity in self.hass.data[DATA_SONOS].entities:
|
|
|
|
entity.update_groups()
|
2018-03-21 02:27:07 +00:00
|
|
|
|
|
|
|
player = self.soco
|
2018-03-08 23:39:31 +00:00
|
|
|
|
2018-11-17 12:18:51 +00:00
|
|
|
def subscribe(service, action):
|
|
|
|
"""Add a subscription to a pysonos service."""
|
|
|
|
queue = _ProcessSonosEventQueue(action)
|
|
|
|
sub = service.subscribe(auto_renew=True, event_queue=queue)
|
|
|
|
self._subscriptions.append(sub)
|
2018-02-18 19:05:20 +00:00
|
|
|
|
2018-11-17 12:18:51 +00:00
|
|
|
subscribe(player.avTransport, self.update_media)
|
|
|
|
subscribe(player.renderingControl, self.update_volume)
|
|
|
|
subscribe(player.zoneGroupTopology, self.update_groups)
|
|
|
|
subscribe(player.contentDirectory, self.update_content)
|
2018-07-23 10:31:03 +00:00
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
def update(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Retrieve latest state."""
|
2019-05-29 22:05:12 +00:00
|
|
|
if self._available and not self._receives_events:
|
2019-04-27 17:05:50 +00:00
|
|
|
try:
|
|
|
|
self.update_groups()
|
|
|
|
self.update_volume()
|
|
|
|
if self.is_coordinator:
|
|
|
|
self.update_media()
|
|
|
|
except SoCoException:
|
|
|
|
pass
|
2018-03-21 02:27:07 +00:00
|
|
|
|
|
|
|
def update_media(self, event=None):
|
|
|
|
"""Update information about currently playing media."""
|
2018-03-01 00:10:58 +00:00
|
|
|
transport_info = self.soco.get_current_transport_info()
|
|
|
|
new_status = transport_info.get('current_transport_state')
|
2018-02-18 19:05:20 +00:00
|
|
|
|
|
|
|
# Ignore transitions, we should get the target state soon
|
|
|
|
if new_status == 'TRANSITIONING':
|
2017-02-07 08:27:55 +00:00
|
|
|
return
|
|
|
|
|
2018-10-03 12:50:13 +00:00
|
|
|
self._shuffle = self.soco.shuffle
|
2018-02-18 19:05:20 +00:00
|
|
|
|
2019-05-26 19:49:26 +00:00
|
|
|
update_position = (new_status != self._status)
|
|
|
|
self._status = new_status
|
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
if self.soco.is_playing_tv:
|
2018-03-21 02:27:07 +00:00
|
|
|
self.update_media_linein(SOURCE_TV)
|
2018-02-18 19:05:20 +00:00
|
|
|
elif self.soco.is_playing_line_in:
|
2018-03-21 02:27:07 +00:00
|
|
|
self.update_media_linein(SOURCE_LINEIN)
|
2017-02-07 08:27:55 +00:00
|
|
|
else:
|
2018-02-18 19:05:20 +00:00
|
|
|
track_info = self.soco.get_current_track_info()
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
if _is_radio_uri(track_info['uri']):
|
2018-03-21 02:27:07 +00:00
|
|
|
variables = event and event.variables
|
|
|
|
self.update_media_radio(variables, track_info)
|
2018-02-18 19:05:20 +00:00
|
|
|
else:
|
2018-03-21 02:27:07 +00:00
|
|
|
self.update_media_music(update_position, track_info)
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
self.schedule_update_ha_state()
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
# Also update slaves
|
2019-02-24 17:45:08 +00:00
|
|
|
for entity in self.hass.data[DATA_SONOS].entities:
|
2018-02-18 19:05:20 +00:00
|
|
|
coordinator = entity.coordinator
|
|
|
|
if coordinator and coordinator.unique_id == self.unique_id:
|
|
|
|
entity.schedule_update_ha_state()
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-03-21 02:27:07 +00:00
|
|
|
def update_media_linein(self, source):
|
2018-02-18 19:05:20 +00:00
|
|
|
"""Update state when playing from line-in/tv."""
|
|
|
|
self._media_duration = None
|
|
|
|
self._media_position = None
|
|
|
|
self._media_position_updated_at = None
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
self._media_image_url = None
|
|
|
|
|
|
|
|
self._media_artist = source
|
|
|
|
self._media_album_name = None
|
|
|
|
self._media_title = None
|
|
|
|
|
|
|
|
self._source_name = source
|
|
|
|
|
2018-03-21 02:27:07 +00:00
|
|
|
def update_media_radio(self, variables, track_info):
|
2018-02-18 19:05:20 +00:00
|
|
|
"""Update state when streaming radio."""
|
|
|
|
self._media_duration = None
|
|
|
|
self._media_position = None
|
|
|
|
self._media_position_updated_at = None
|
|
|
|
|
2018-03-05 02:30:15 +00:00
|
|
|
media_info = self.soco.avTransport.GetMediaInfo([('InstanceID', 0)])
|
2018-02-18 19:05:20 +00:00
|
|
|
self._media_image_url = self._radio_artwork(media_info['CurrentURI'])
|
|
|
|
|
|
|
|
self._media_artist = track_info.get('artist')
|
|
|
|
self._media_album_name = None
|
|
|
|
self._media_title = track_info.get('title')
|
|
|
|
|
|
|
|
if self._media_artist and self._media_title:
|
|
|
|
# artist and album name are in the data, concatenate
|
|
|
|
# that do display as artist.
|
|
|
|
# "Information" field in the sonos pc app
|
|
|
|
self._media_artist = '{artist} - {title}'.format(
|
|
|
|
artist=self._media_artist,
|
|
|
|
title=self._media_title
|
2017-02-07 08:27:55 +00:00
|
|
|
)
|
2018-03-21 02:27:07 +00:00
|
|
|
elif variables:
|
2018-02-18 19:05:20 +00:00
|
|
|
# "On Now" field in the sonos pc app
|
2018-03-01 00:10:58 +00:00
|
|
|
current_track_metadata = variables.get('current_track_meta_data')
|
2018-02-18 19:05:20 +00:00
|
|
|
if current_track_metadata:
|
|
|
|
self._media_artist = \
|
|
|
|
current_track_metadata.radio_show.split(',')[0]
|
|
|
|
|
|
|
|
# For radio streams we set the radio station name as the title.
|
|
|
|
current_uri_metadata = media_info["CurrentURIMetaData"]
|
|
|
|
if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None):
|
|
|
|
# currently soco does not have an API for this
|
2018-09-20 21:50:11 +00:00
|
|
|
current_uri_metadata = pysonos.xml.XML.fromstring(
|
|
|
|
pysonos.utils.really_utf8(current_uri_metadata))
|
2018-02-18 19:05:20 +00:00
|
|
|
|
|
|
|
md_title = current_uri_metadata.findtext(
|
|
|
|
'.//{http://purl.org/dc/elements/1.1/}title')
|
|
|
|
|
|
|
|
if md_title not in ('', 'NOT_IMPLEMENTED', None):
|
|
|
|
self._media_title = md_title
|
|
|
|
|
|
|
|
if self._media_artist and self._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
|
|
|
|
trim = '{title} - '.format(title=self._media_title)
|
|
|
|
chars = min(len(self._media_artist), len(trim))
|
|
|
|
|
|
|
|
if self._media_artist[:chars].upper() == trim[:chars].upper():
|
|
|
|
self._media_artist = self._media_artist[chars:]
|
|
|
|
|
|
|
|
# Check if currently playing radio station is in favorites
|
|
|
|
self._source_name = None
|
|
|
|
for fav in self._favorites:
|
|
|
|
if fav.reference.get_uri() == media_info['CurrentURI']:
|
|
|
|
self._source_name = fav.title
|
|
|
|
|
2018-03-21 02:27:07 +00:00
|
|
|
def update_media_music(self, update_media_position, track_info):
|
2018-02-18 19:05:20 +00:00
|
|
|
"""Update state when playing music tracks."""
|
|
|
|
self._media_duration = _timespan_secs(track_info.get('duration'))
|
|
|
|
|
|
|
|
position_info = self.soco.avTransport.GetPositionInfo(
|
|
|
|
[('InstanceID', 0),
|
|
|
|
('Channel', 'Master')]
|
|
|
|
)
|
|
|
|
rel_time = _timespan_secs(position_info.get("RelTime"))
|
|
|
|
|
|
|
|
# player no longer reports position?
|
2018-03-01 00:10:58 +00:00
|
|
|
update_media_position |= rel_time is None and \
|
2018-02-18 19:05:20 +00:00
|
|
|
self._media_position is not None
|
|
|
|
|
|
|
|
# player started reporting position?
|
|
|
|
update_media_position |= rel_time is not None and \
|
|
|
|
self._media_position is None
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-03-01 00:10:58 +00:00
|
|
|
# position jumped?
|
2019-05-26 19:49:26 +00:00
|
|
|
if (self.state == STATE_PLAYING
|
|
|
|
and rel_time is not None
|
|
|
|
and self._media_position is not None):
|
2018-03-01 00:10:58 +00:00
|
|
|
time_diff = utcnow() - self._media_position_updated_at
|
|
|
|
time_diff = time_diff.total_seconds()
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-03-01 00:10:58 +00:00
|
|
|
calculated_position = self._media_position + time_diff
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-03-01 00:10:58 +00:00
|
|
|
update_media_position |= abs(calculated_position - rel_time) > 1.5
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
if update_media_position:
|
|
|
|
self._media_position = rel_time
|
|
|
|
self._media_position_updated_at = utcnow()
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
self._media_image_url = track_info.get('album_art')
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
self._media_artist = track_info.get('artist')
|
|
|
|
self._media_album_name = track_info.get('album')
|
|
|
|
self._media_title = track_info.get('title')
|
2016-11-01 17:42:38 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
self._source_name = None
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-03-21 02:27:07 +00:00
|
|
|
def update_volume(self, event=None):
|
|
|
|
"""Update information about currently volume settings."""
|
|
|
|
if event:
|
|
|
|
variables = event.variables
|
|
|
|
|
|
|
|
if 'volume' in variables:
|
|
|
|
self._player_volume = int(variables['volume']['Master'])
|
|
|
|
|
|
|
|
if 'mute' in variables:
|
|
|
|
self._player_muted = (variables['mute']['Master'] == '1')
|
|
|
|
|
|
|
|
if 'night_mode' in variables:
|
|
|
|
self._night_sound = (variables['night_mode'] == '1')
|
|
|
|
|
|
|
|
if 'dialog_level' in variables:
|
|
|
|
self._speech_enhance = (variables['dialog_level'] == '1')
|
|
|
|
|
|
|
|
self.schedule_update_ha_state()
|
|
|
|
else:
|
|
|
|
self._player_volume = self.soco.volume
|
|
|
|
self._player_muted = self.soco.mute
|
|
|
|
self._night_sound = self.soco.night_mode
|
|
|
|
self._speech_enhance = self.soco.dialog_mode
|
|
|
|
|
|
|
|
def update_groups(self, event=None):
|
2019-03-13 09:17:09 +00:00
|
|
|
"""Handle callback for topology change event."""
|
|
|
|
def _get_soco_group():
|
|
|
|
"""Ask SoCo cache for existing topology."""
|
|
|
|
coordinator_uid = self.unique_id
|
|
|
|
slave_uids = []
|
2018-03-21 02:27:07 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
try:
|
|
|
|
if self.soco.group and self.soco.group.coordinator:
|
|
|
|
coordinator_uid = self.soco.group.coordinator.uid
|
|
|
|
slave_uids = [p.uid for p in self.soco.group.members
|
|
|
|
if p.uid != coordinator_uid]
|
2019-04-27 17:05:50 +00:00
|
|
|
except SoCoException:
|
2019-03-13 09:17:09 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
return [coordinator_uid] + slave_uids
|
2018-03-21 02:27:07 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
async def _async_extract_group(event):
|
|
|
|
"""Extract group layout from a topology event."""
|
2018-03-21 02:27:07 +00:00
|
|
|
group = event and event.zone_player_uui_ds_in_group
|
|
|
|
if group:
|
2019-03-13 09:17:09 +00:00
|
|
|
return group.split(',')
|
2018-03-21 02:27:07 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
return await self.hass.async_add_executor_job(_get_soco_group)
|
2018-11-17 12:18:51 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
def _async_regroup(group):
|
|
|
|
"""Rebuild internal group layout."""
|
|
|
|
sonos_group = []
|
|
|
|
for uid in group:
|
|
|
|
entity = _get_entity_from_soco_uid(self.hass, uid)
|
|
|
|
if entity:
|
|
|
|
sonos_group.append(entity)
|
2019-02-28 18:25:31 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
self._coordinator = None
|
|
|
|
self._sonos_group = sonos_group
|
|
|
|
self.async_schedule_update_ha_state()
|
2018-04-25 18:31:42 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
for slave_uid in group[1:]:
|
|
|
|
slave = _get_entity_from_soco_uid(self.hass, slave_uid)
|
|
|
|
if slave:
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
slave._coordinator = self
|
|
|
|
slave._sonos_group = sonos_group
|
|
|
|
slave.async_schedule_update_ha_state()
|
2018-03-21 02:27:07 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
async def _async_handle_group_event(event):
|
|
|
|
"""Get async lock and handle event."""
|
2019-03-13 19:51:41 +00:00
|
|
|
async with self.hass.data[DATA_SONOS].topology_condition:
|
2019-03-13 09:17:09 +00:00
|
|
|
group = await _async_extract_group(event)
|
|
|
|
|
|
|
|
if self.unique_id == group[0]:
|
|
|
|
_async_regroup(group)
|
|
|
|
|
2019-03-13 19:51:41 +00:00
|
|
|
self.hass.data[DATA_SONOS].topology_condition.notify_all()
|
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
if event:
|
|
|
|
self._receives_events = True
|
|
|
|
|
|
|
|
if not hasattr(event, 'zone_player_uui_ds_in_group'):
|
|
|
|
return
|
|
|
|
|
|
|
|
self.hass.add_job(_async_handle_group_event(event))
|
2018-03-21 02:27:07 +00:00
|
|
|
|
2018-07-23 10:31:03 +00:00
|
|
|
def update_content(self, event=None):
|
|
|
|
"""Update information about available content."""
|
|
|
|
self._set_favorites()
|
|
|
|
self.schedule_update_ha_state()
|
|
|
|
|
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)."""
|
2018-02-18 19:05:20 +00:00
|
|
|
return self._player_volume / 100
|
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."""
|
2018-03-21 02:27:07 +00:00
|
|
|
return self._player_muted
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-12-07 19:44:06 +00:00
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2017-12-07 19:44:06 +00:00
|
|
|
def shuffle(self):
|
|
|
|
"""Shuffling state."""
|
2018-10-03 12:50:13 +00:00
|
|
|
return self._shuffle
|
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
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2015-09-11 22:32:47 +00:00
|
|
|
def media_duration(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Duration of current playing media in seconds."""
|
2017-07-06 06:30:01 +00:00
|
|
|
return self._media_duration
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2016-11-28 01:45:49 +00:00
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2016-11-28 01:45:49 +00:00
|
|
|
def media_position(self):
|
|
|
|
"""Position of current playing media in seconds."""
|
2017-07-06 06:30:01 +00:00
|
|
|
return self._media_position
|
2016-11-28 01:45:49 +00:00
|
|
|
|
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2016-11-28 01:45:49 +00:00
|
|
|
def media_position_updated_at(self):
|
2018-02-18 19:05:20 +00:00
|
|
|
"""When was the position of the current playing media valid."""
|
2017-07-06 06:30:01 +00:00
|
|
|
return self._media_position_updated_at
|
2016-11-28 01:45:49 +00:00
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2015-09-11 22:32:47 +00:00
|
|
|
def media_image_url(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Image url of current playing media."""
|
2018-02-18 19:05:20 +00:00
|
|
|
return self._media_image_url or None
|
2016-10-25 22:37:47 +00:00
|
|
|
|
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2016-10-25 22:37:47 +00:00
|
|
|
def media_artist(self):
|
|
|
|
"""Artist of current playing media, music track only."""
|
2017-07-06 06:30:01 +00:00
|
|
|
return self._media_artist
|
2016-10-25 22:37:47 +00:00
|
|
|
|
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2016-10-25 22:37:47 +00:00
|
|
|
def media_album_name(self):
|
|
|
|
"""Album name of current playing media, music track only."""
|
2017-07-06 06:30:01 +00:00
|
|
|
return self._media_album_name
|
2015-09-11 22:32:47 +00:00
|
|
|
|
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2015-09-11 22:32:47 +00:00
|
|
|
def media_title(self):
|
2016-03-08 09:34:33 +00:00
|
|
|
"""Title of current playing media."""
|
2017-07-06 06:30:01 +00:00
|
|
|
return self._media_title
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2017-12-17 12:08:35 +00:00
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
|
|
|
def source(self):
|
|
|
|
"""Name of the current input source."""
|
|
|
|
return self._source_name
|
2017-12-17 12:08:35 +00:00
|
|
|
|
2015-09-11 22:32:47 +00:00
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2017-02-08 04:42:45 +00:00
|
|
|
def supported_features(self):
|
|
|
|
"""Flag media player features that are supported."""
|
2018-03-05 02:30:15 +00:00
|
|
|
return SUPPORT_SONOS
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-01-13 08:59:50 +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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self._player.volume += self._volume_increment
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-01-13 08:59:50 +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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self._player.volume -= self._volume_increment
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-01-13 08:59:50 +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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.volume = str(int(volume * 100))
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-10-01 15:58:04 +00:00
|
|
|
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2017-12-07 19:44:06 +00:00
|
|
|
def set_shuffle(self, shuffle):
|
|
|
|
"""Enable/Disable shuffle mode."""
|
2018-10-03 12:50:13 +00:00
|
|
|
self.soco.shuffle = shuffle
|
2017-12-07 19:44:06 +00:00
|
|
|
|
2018-01-13 08:59:50 +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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.mute = mute
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_coordinator
|
2016-07-15 16:00:41 +00:00
|
|
|
def select_source(self, source):
|
|
|
|
"""Select input source."""
|
2018-02-18 19:05:20 +00:00
|
|
|
if source == SOURCE_LINEIN:
|
|
|
|
self.soco.switch_to_line_in()
|
|
|
|
elif source == SOURCE_TV:
|
|
|
|
self.soco.switch_to_tv()
|
2016-12-14 18:05:03 +00:00
|
|
|
else:
|
2018-02-18 19:05:20 +00:00
|
|
|
fav = [fav for fav in self._favorites
|
|
|
|
if fav.title == source]
|
2016-12-14 18:05:03 +00:00
|
|
|
if len(fav) == 1:
|
|
|
|
src = fav.pop()
|
2018-02-18 19:05:20 +00:00
|
|
|
uri = src.reference.get_uri()
|
|
|
|
if _is_radio_uri(uri):
|
2018-08-17 05:41:56 +00:00
|
|
|
self.soco.play_uri(uri, title=source)
|
2017-08-01 06:16:05 +00:00
|
|
|
else:
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.clear_queue()
|
|
|
|
self.soco.add_to_queue(src.reference)
|
|
|
|
self.soco.play_from_queue(0)
|
2016-07-15 16:00:41 +00:00
|
|
|
|
|
|
|
@property
|
2018-02-18 19:05:20 +00:00
|
|
|
@soco_coordinator
|
2016-07-15 16:00:41 +00:00
|
|
|
def source_list(self):
|
|
|
|
"""List of available input sources."""
|
2018-02-18 19:05:20 +00:00
|
|
|
sources = [fav.title for fav in self._favorites]
|
2017-02-07 08:27:55 +00:00
|
|
|
|
2018-07-23 10:29:37 +00:00
|
|
|
model = self._model.upper()
|
|
|
|
if 'PLAY:5' in model or 'CONNECT' in model:
|
2018-02-18 19:05:20 +00:00
|
|
|
sources += [SOURCE_LINEIN]
|
2018-07-23 10:29:37 +00:00
|
|
|
elif 'PLAYBAR' in model:
|
2018-02-18 19:05:20 +00:00
|
|
|
sources += [SOURCE_LINEIN, SOURCE_TV]
|
2018-09-01 14:02:38 +00:00
|
|
|
elif 'BEAM' in model:
|
|
|
|
sources += [SOURCE_TV]
|
2016-07-15 16:00:41 +00:00
|
|
|
|
2016-12-14 18:05:03 +00:00
|
|
|
return sources
|
2016-07-15 16:00:41 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
2017-04-06 06:24:30 +00:00
|
|
|
@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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.play()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_coordinator
|
2016-12-08 08:36:37 +00:00
|
|
|
def media_stop(self):
|
|
|
|
"""Send stop command."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.stop()
|
2016-12-08 08:36:37 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
2017-04-06 06:24:30 +00:00
|
|
|
@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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.pause()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-03-05 02:30:15 +00:00
|
|
|
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
2017-04-06 06:24:30 +00:00
|
|
|
@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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.next()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-03-05 02:30:15 +00:00
|
|
|
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
2017-04-06 06:24:30 +00:00
|
|
|
@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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.previous()
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-03-05 02:30:15 +00:00
|
|
|
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
2017-04-06 06:24:30 +00:00
|
|
|
@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."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.seek(str(datetime.timedelta(seconds=int(position))))
|
2015-09-11 22:32:47 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_coordinator
|
2016-07-15 16:00:41 +00:00
|
|
|
def clear_playlist(self):
|
|
|
|
"""Clear players playlist."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.clear_queue()
|
2016-03-26 05:57:14 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2017-04-06 06:24:30 +00:00
|
|
|
@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):
|
|
|
|
try:
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.add_uri_to_queue(media_id)
|
2017-04-06 06:24:30 +00:00
|
|
|
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:
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.play_uri(media_id)
|
2016-04-17 20:17:13 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2018-02-18 19:05:20 +00:00
|
|
|
def join(self, slaves):
|
|
|
|
"""Form a group with other players."""
|
|
|
|
if self._coordinator:
|
2018-03-15 23:12:43 +00:00
|
|
|
self.unjoin()
|
2019-03-13 19:51:41 +00:00
|
|
|
group = [self]
|
|
|
|
else:
|
|
|
|
group = self._sonos_group.copy()
|
2018-02-18 19:05:20 +00:00
|
|
|
|
|
|
|
for slave in slaves:
|
2018-03-14 19:07:50 +00:00
|
|
|
if slave.unique_id != self.unique_id:
|
|
|
|
slave.soco.join(self.soco)
|
2018-03-15 23:12:43 +00:00
|
|
|
# pylint: disable=protected-access
|
|
|
|
slave._coordinator = self
|
2019-03-13 19:51:41 +00:00
|
|
|
if slave not in group:
|
|
|
|
group.append(slave)
|
|
|
|
|
|
|
|
return group
|
2016-05-20 16:54:15 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
@staticmethod
|
|
|
|
async def join_multi(hass, master, entities):
|
|
|
|
"""Form a group with other players."""
|
2019-03-13 19:51:41 +00:00
|
|
|
async with hass.data[DATA_SONOS].topology_condition:
|
|
|
|
group = await hass.async_add_executor_job(master.join, entities)
|
|
|
|
await SonosEntity.wait_for_groups(hass, [group])
|
2019-03-13 09:17:09 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2016-06-30 21:21:57 +00:00
|
|
|
def unjoin(self):
|
|
|
|
"""Unjoin the player from a group."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.unjoin()
|
2018-03-15 23:12:43 +00:00
|
|
|
self._coordinator = None
|
2016-06-30 21:21:57 +00:00
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
@staticmethod
|
|
|
|
async def unjoin_multi(hass, entities):
|
|
|
|
"""Unjoin several players from their group."""
|
|
|
|
def _unjoin_all(entities):
|
|
|
|
"""Sync helper."""
|
2019-03-13 19:51:08 +00:00
|
|
|
# Unjoin slaves first to prevent inheritance of queues
|
|
|
|
coordinators = [e for e in entities if e.is_coordinator]
|
|
|
|
slaves = [e for e in entities if not e.is_coordinator]
|
|
|
|
|
|
|
|
for entity in slaves + coordinators:
|
2019-03-13 09:17:09 +00:00
|
|
|
entity.unjoin()
|
|
|
|
|
2019-03-13 19:51:41 +00:00
|
|
|
async with hass.data[DATA_SONOS].topology_condition:
|
2019-03-13 09:17:09 +00:00
|
|
|
await hass.async_add_executor_job(_unjoin_all, entities)
|
2019-03-13 19:51:41 +00:00
|
|
|
await SonosEntity.wait_for_groups(hass, [[e] for e in entities])
|
2019-03-13 09:17:09 +00:00
|
|
|
|
2019-02-28 18:25:31 +00:00
|
|
|
@soco_error()
|
|
|
|
def snapshot(self, with_group):
|
|
|
|
"""Snapshot the state of a player."""
|
2019-04-27 17:05:50 +00:00
|
|
|
self._soco_snapshot = pysonos.snapshot.Snapshot(self.soco)
|
2019-02-28 18:25:31 +00:00
|
|
|
self._soco_snapshot.snapshot()
|
|
|
|
if with_group:
|
|
|
|
self._snapshot_group = self._sonos_group.copy()
|
|
|
|
else:
|
|
|
|
self._snapshot_group = None
|
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
@staticmethod
|
|
|
|
async def snapshot_multi(hass, entities, with_group):
|
|
|
|
"""Snapshot all the entities and optionally their groups."""
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
|
|
|
def _snapshot_all(entities):
|
|
|
|
"""Sync helper."""
|
|
|
|
for entity in entities:
|
|
|
|
entity.snapshot(with_group)
|
|
|
|
|
|
|
|
# Find all affected players
|
|
|
|
entities = set(entities)
|
|
|
|
if with_group:
|
|
|
|
for entity in list(entities):
|
|
|
|
entities.update(entity._sonos_group)
|
|
|
|
|
2019-03-13 19:51:41 +00:00
|
|
|
async with hass.data[DATA_SONOS].topology_condition:
|
2019-03-13 09:17:09 +00:00
|
|
|
await hass.async_add_executor_job(_snapshot_all, entities)
|
|
|
|
|
2019-02-28 18:25:31 +00:00
|
|
|
@soco_error()
|
|
|
|
def restore(self):
|
|
|
|
"""Restore a snapshotted state to a player."""
|
|
|
|
try:
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
self._soco_snapshot.restore()
|
|
|
|
except (TypeError, AttributeError, SoCoException) as ex:
|
|
|
|
# Can happen if restoring a coordinator onto a current slave
|
|
|
|
_LOGGER.warning("Error on restore %s: %s", self.entity_id, ex)
|
|
|
|
|
|
|
|
self._soco_snapshot = None
|
|
|
|
self._snapshot_group = None
|
|
|
|
|
|
|
|
@staticmethod
|
2019-03-13 09:17:09 +00:00
|
|
|
async def restore_multi(hass, entities, with_group):
|
|
|
|
"""Restore snapshots for all the entities."""
|
2019-02-28 18:25:31 +00:00
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
2019-03-13 19:51:41 +00:00
|
|
|
def _restore_groups(entities, with_group):
|
|
|
|
"""Pause all current coordinators and restore groups."""
|
2019-03-13 09:17:09 +00:00
|
|
|
for entity in (e for e in entities if e.is_coordinator):
|
|
|
|
if entity.state == STATE_PLAYING:
|
|
|
|
entity.media_pause()
|
|
|
|
|
2019-03-13 19:51:41 +00:00
|
|
|
groups = []
|
|
|
|
|
2019-03-13 09:17:09 +00:00
|
|
|
if with_group:
|
2019-03-13 19:51:08 +00:00
|
|
|
# Unjoin slaves first to prevent inheritance of queues
|
2019-03-13 09:17:09 +00:00
|
|
|
for entity in [e for e in entities if not e.is_coordinator]:
|
|
|
|
if entity._snapshot_group != entity._sonos_group:
|
|
|
|
entity.unjoin()
|
|
|
|
|
|
|
|
# Bring back the original group topology
|
|
|
|
for entity in (e for e in entities if e._snapshot_group):
|
|
|
|
if entity._snapshot_group[0] == entity:
|
|
|
|
entity.join(entity._snapshot_group)
|
2019-03-13 19:51:41 +00:00
|
|
|
groups.append(entity._snapshot_group.copy())
|
2019-03-13 09:17:09 +00:00
|
|
|
|
2019-03-13 19:51:41 +00:00
|
|
|
return groups
|
|
|
|
|
|
|
|
def _restore_players(entities):
|
|
|
|
"""Restore state of all players."""
|
2019-03-13 09:17:09 +00:00
|
|
|
for entity in (e for e in entities if not e.is_coordinator):
|
|
|
|
entity.restore()
|
|
|
|
|
|
|
|
for entity in (e for e in entities if e.is_coordinator):
|
2019-03-13 19:51:41 +00:00
|
|
|
entity.restore()
|
2019-02-28 18:25:31 +00:00
|
|
|
|
|
|
|
# Find all affected players
|
|
|
|
entities = set(e for e in entities if e._soco_snapshot)
|
|
|
|
if with_group:
|
|
|
|
for entity in [e for e in entities if e._snapshot_group]:
|
|
|
|
entities.update(entity._snapshot_group)
|
|
|
|
|
2019-03-13 19:51:41 +00:00
|
|
|
async with hass.data[DATA_SONOS].topology_condition:
|
|
|
|
groups = await hass.async_add_executor_job(
|
|
|
|
_restore_groups, entities, with_group)
|
|
|
|
|
|
|
|
await SonosEntity.wait_for_groups(hass, groups)
|
|
|
|
|
|
|
|
await hass.async_add_executor_job(_restore_players, entities)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
async def wait_for_groups(hass, groups):
|
|
|
|
"""Wait until all groups are present, or timeout."""
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
|
|
|
def _test_groups(groups):
|
|
|
|
"""Return whether all groups exist now."""
|
|
|
|
for group in groups:
|
|
|
|
coordinator = group[0]
|
|
|
|
|
|
|
|
# Test that coordinator is coordinating
|
|
|
|
current_group = coordinator._sonos_group
|
|
|
|
if coordinator != current_group[0]:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# Test that slaves match
|
|
|
|
if set(group[1:]) != set(current_group[1:]):
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
try:
|
|
|
|
with async_timeout.timeout(5):
|
|
|
|
while not _test_groups(groups):
|
|
|
|
await hass.data[DATA_SONOS].topology_condition.wait()
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
_LOGGER.warning("Timeout waiting for target groups %s", groups)
|
|
|
|
|
|
|
|
for entity in hass.data[DATA_SONOS].entities:
|
|
|
|
entity.soco._zgs_cache.clear()
|
2019-02-28 18:25:31 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_coordinator
|
2019-05-05 19:25:57 +00:00
|
|
|
def set_sleep_timer(self, data):
|
2016-10-26 06:22:17 +00:00
|
|
|
"""Set the timer on the player."""
|
2019-05-05 19:25:57 +00:00
|
|
|
self.soco.set_sleep_timer(data[ATTR_SLEEP_TIME])
|
2016-10-26 06:22:17 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2017-04-06 06:24:30 +00:00
|
|
|
@soco_coordinator
|
2019-05-05 19:25:57 +00:00
|
|
|
def clear_sleep_timer(self, data):
|
2016-10-26 06:22:17 +00:00
|
|
|
"""Clear the timer on the player."""
|
2018-02-18 19:05:20 +00:00
|
|
|
self.soco.set_sleep_timer(None)
|
2017-01-25 21:03:36 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2017-05-15 07:42:45 +00:00
|
|
|
@soco_coordinator
|
2019-05-05 19:25:57 +00:00
|
|
|
def set_alarm(self, data):
|
2017-05-15 07:42:45 +00:00
|
|
|
"""Set the alarm clock on the player."""
|
2018-09-20 21:50:11 +00:00
|
|
|
from pysonos import alarms
|
2018-02-18 19:05:20 +00:00
|
|
|
alarm = None
|
|
|
|
for one_alarm in alarms.get_alarms(self.soco):
|
2017-05-15 07:42:45 +00:00
|
|
|
# pylint: disable=protected-access
|
2018-02-18 19:05:20 +00:00
|
|
|
if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]):
|
|
|
|
alarm = one_alarm
|
|
|
|
if alarm is None:
|
2017-05-15 07:42:45 +00:00
|
|
|
_LOGGER.warning("did not find alarm with id %s",
|
|
|
|
data[ATTR_ALARM_ID])
|
|
|
|
return
|
|
|
|
if ATTR_TIME in data:
|
2018-02-18 19:05:20 +00:00
|
|
|
alarm.start_time = data[ATTR_TIME]
|
2017-05-15 07:42:45 +00:00
|
|
|
if ATTR_VOLUME in data:
|
2018-02-18 19:05:20 +00:00
|
|
|
alarm.volume = int(data[ATTR_VOLUME] * 100)
|
2017-05-15 07:42:45 +00:00
|
|
|
if ATTR_ENABLED in data:
|
2018-02-18 19:05:20 +00:00
|
|
|
alarm.enabled = data[ATTR_ENABLED]
|
2017-05-15 07:42:45 +00:00
|
|
|
if ATTR_INCLUDE_LINKED_ZONES in data:
|
2018-02-18 19:05:20 +00:00
|
|
|
alarm.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
|
|
|
|
alarm.save()
|
2017-05-15 07:42:45 +00:00
|
|
|
|
2018-01-13 08:59:50 +00:00
|
|
|
@soco_error()
|
2019-05-05 19:25:57 +00:00
|
|
|
def set_option(self, data):
|
2017-12-17 12:08:35 +00:00
|
|
|
"""Modify playback options."""
|
2018-02-18 19:05:20 +00:00
|
|
|
if ATTR_NIGHT_SOUND in data and self._night_sound is not None:
|
2017-12-17 12:08:35 +00:00
|
|
|
self.soco.night_mode = data[ATTR_NIGHT_SOUND]
|
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
if ATTR_SPEECH_ENHANCE in data and self._speech_enhance is not None:
|
2017-12-17 12:08:35 +00:00
|
|
|
self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE]
|
|
|
|
|
2017-01-25 21:03:36 +00:00
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
2019-02-24 17:45:08 +00:00
|
|
|
"""Return entity specific state attributes."""
|
2019-02-25 21:03:15 +00:00
|
|
|
attributes = {
|
|
|
|
ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group],
|
|
|
|
}
|
2017-12-17 12:08:35 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
if self._night_sound is not None:
|
|
|
|
attributes[ATTR_NIGHT_SOUND] = self._night_sound
|
2017-12-17 12:08:35 +00:00
|
|
|
|
2018-02-18 19:05:20 +00:00
|
|
|
if self._speech_enhance is not None:
|
|
|
|
attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance
|
2017-12-17 12:08:35 +00:00
|
|
|
|
|
|
|
return attributes
|