Add a polling fallback for Sonos (#13310)

* Prepare for poll

* Add a polling fallback for Sonos
This commit is contained in:
Anders Melchiorsen 2018-03-21 03:27:07 +01:00 committed by Paulus Schoutsen
parent 3426487277
commit f8127a3902
2 changed files with 151 additions and 135 deletions

View file

@ -208,9 +208,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
elif service.service == SERVICE_CLEAR_TIMER:
device.clear_sleep_timer()
elif service.service == SERVICE_UPDATE_ALARM:
device.update_alarm(**service.data)
device.set_alarm(**service.data)
elif service.service == SERVICE_SET_OPTION:
device.update_option(**service.data)
device.set_option(**service.data)
device.schedule_update_ha_state(True)
@ -330,12 +330,13 @@ class SonosDevice(MediaPlayerDevice):
def __init__(self, player):
"""Initialize the Sonos device."""
self._receives_events = False
self._volume_increment = 5
self._unique_id = player.uid
self._player = player
self._model = None
self._player_volume = None
self._player_volume_muted = None
self._player_muted = None
self._play_mode = None
self._name = None
self._coordinator = None
@ -420,11 +421,9 @@ class SonosDevice(MediaPlayerDevice):
speaker_info = self.soco.get_speaker_info(True)
self._name = speaker_info['zone_name']
self._model = speaker_info['model_name']
self._player_volume = self.soco.volume
self._player_volume_muted = self.soco.mute
self._play_mode = self.soco.play_mode
self._night_sound = self.soco.night_mode
self._speech_enhance = self.soco.dialog_mode
self.update_volume()
self._favorites = []
for fav in self.soco.music_library.get_sonos_favorites():
@ -437,124 +436,6 @@ class SonosDevice(MediaPlayerDevice):
except Exception:
_LOGGER.debug("Ignoring invalid favorite '%s'", fav.title)
def _subscribe_to_player_events(self):
"""Add event subscriptions."""
player = self.soco
# New player available, build the current group topology
for device in self.hass.data[DATA_SONOS].devices:
device.process_zonegrouptopology_event(None)
queue = _ProcessSonosEventQueue(self.process_avtransport_event)
player.avTransport.subscribe(auto_renew=True, event_queue=queue)
queue = _ProcessSonosEventQueue(self.process_rendering_event)
player.renderingControl.subscribe(auto_renew=True, event_queue=queue)
queue = _ProcessSonosEventQueue(self.process_zonegrouptopology_event)
player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue)
def update(self):
"""Retrieve latest state."""
available = self._check_available()
if self._available != available:
self._available = available
if available:
self._set_basic_information()
self._subscribe_to_player_events()
else:
self._player_volume = None
self._player_volume_muted = None
self._status = 'OFF'
self._coordinator = None
self._media_duration = None
self._media_position = None
self._media_position_updated_at = None
self._media_image_url = None
self._media_artist = None
self._media_album_name = None
self._media_title = None
self._source_name = None
def process_avtransport_event(self, event):
"""Process a track change event coming from a coordinator."""
transport_info = self.soco.get_current_transport_info()
new_status = transport_info.get('current_transport_state')
# Ignore transitions, we should get the target state soon
if new_status == 'TRANSITIONING':
return
self._play_mode = self.soco.play_mode
if self.soco.is_playing_tv:
self._refresh_linein(SOURCE_TV)
elif self.soco.is_playing_line_in:
self._refresh_linein(SOURCE_LINEIN)
else:
track_info = self.soco.get_current_track_info()
if _is_radio_uri(track_info['uri']):
self._refresh_radio(event.variables, track_info)
else:
update_position = (new_status != self._status)
self._refresh_music(update_position, track_info)
self._status = new_status
self.schedule_update_ha_state()
# Also update slaves
for entity in self.hass.data[DATA_SONOS].devices:
coordinator = entity.coordinator
if coordinator and coordinator.unique_id == self.unique_id:
entity.schedule_update_ha_state()
def process_rendering_event(self, event):
"""Process a volume change event coming from a player."""
variables = event.variables
if 'volume' in variables:
self._player_volume = int(variables['volume']['Master'])
if 'mute' in variables:
self._player_volume_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()
def process_zonegrouptopology_event(self, event):
"""Process a zone group topology event coming from a player."""
if event and not hasattr(event, 'zone_player_uui_ds_in_group'):
return
with self.hass.data[DATA_SONOS].topology_lock:
group = event and event.zone_player_uui_ds_in_group
if group:
# New group information is pushed
coordinator_uid, *slave_uids = group.split(',')
else:
# Use SoCo cache for existing topology
coordinator_uid = self.soco.group.coordinator.uid
slave_uids = [p.uid for p in self.soco.group.members
if p.uid != coordinator_uid]
if self.unique_id == coordinator_uid:
self._coordinator = None
self.schedule_update_ha_state()
for slave_uid in slave_uids:
slave = _get_entity_from_soco_uid(self.hass, slave_uid)
if slave:
# pylint: disable=protected-access
slave._coordinator = self
slave.schedule_update_ha_state()
def _radio_artwork(self, url):
"""Return the private URL with artwork for a radio stream."""
if url not in ('', 'NOT_IMPLEMENTED', None):
@ -568,7 +449,88 @@ class SonosDevice(MediaPlayerDevice):
)
return url
def _refresh_linein(self, source):
def _subscribe_to_player_events(self):
"""Add event subscriptions."""
self._receives_events = False
# New player available, build the current group topology
for device in self.hass.data[DATA_SONOS].devices:
device.update_groups()
player = self.soco
queue = _ProcessSonosEventQueue(self.update_media)
player.avTransport.subscribe(auto_renew=True, event_queue=queue)
queue = _ProcessSonosEventQueue(self.update_volume)
player.renderingControl.subscribe(auto_renew=True, event_queue=queue)
queue = _ProcessSonosEventQueue(self.update_groups)
player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue)
def update(self):
"""Retrieve latest state."""
available = self._check_available()
if self._available != available:
self._available = available
if available:
self._set_basic_information()
self._subscribe_to_player_events()
else:
self._player_volume = None
self._player_muted = None
self._status = 'OFF'
self._coordinator = None
self._media_duration = None
self._media_position = None
self._media_position_updated_at = None
self._media_image_url = None
self._media_artist = None
self._media_album_name = None
self._media_title = None
self._source_name = None
elif available and not self._receives_events:
self.update_groups()
self.update_volume()
if self.is_coordinator:
self.update_media()
def update_media(self, event=None):
"""Update information about currently playing media."""
transport_info = self.soco.get_current_transport_info()
new_status = transport_info.get('current_transport_state')
# Ignore transitions, we should get the target state soon
if new_status == 'TRANSITIONING':
return
self._play_mode = self.soco.play_mode
if self.soco.is_playing_tv:
self.update_media_linein(SOURCE_TV)
elif self.soco.is_playing_line_in:
self.update_media_linein(SOURCE_LINEIN)
else:
track_info = self.soco.get_current_track_info()
if _is_radio_uri(track_info['uri']):
variables = event and event.variables
self.update_media_radio(variables, track_info)
else:
update_position = (new_status != self._status)
self.update_media_music(update_position, track_info)
self._status = new_status
self.schedule_update_ha_state()
# Also update slaves
for entity in self.hass.data[DATA_SONOS].devices:
coordinator = entity.coordinator
if coordinator and coordinator.unique_id == self.unique_id:
entity.schedule_update_ha_state()
def update_media_linein(self, source):
"""Update state when playing from line-in/tv."""
self._media_duration = None
self._media_position = None
@ -582,7 +544,7 @@ class SonosDevice(MediaPlayerDevice):
self._source_name = source
def _refresh_radio(self, variables, track_info):
def update_media_radio(self, variables, track_info):
"""Update state when streaming radio."""
self._media_duration = None
self._media_position = None
@ -603,7 +565,7 @@ class SonosDevice(MediaPlayerDevice):
artist=self._media_artist,
title=self._media_title
)
else:
elif variables:
# "On Now" field in the sonos pc app
current_track_metadata = variables.get('current_track_meta_data')
if current_track_metadata:
@ -643,7 +605,7 @@ class SonosDevice(MediaPlayerDevice):
if fav.reference.get_uri() == media_info['CurrentURI']:
self._source_name = fav.title
def _refresh_music(self, update_media_position, track_info):
def update_media_music(self, update_media_position, track_info):
"""Update state when playing music tracks."""
self._media_duration = _timespan_secs(track_info.get('duration'))
@ -682,6 +644,60 @@ class SonosDevice(MediaPlayerDevice):
self._source_name = None
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):
"""Process a zone group topology event coming from a player."""
if event:
self._receives_events = True
if not hasattr(event, 'zone_player_uui_ds_in_group'):
return
with self.hass.data[DATA_SONOS].topology_lock:
group = event and event.zone_player_uui_ds_in_group
if group:
# New group information is pushed
coordinator_uid, *slave_uids = group.split(',')
else:
# Use SoCo cache for existing topology
coordinator_uid = self.soco.group.coordinator.uid
slave_uids = [p.uid for p in self.soco.group.members
if p.uid != coordinator_uid]
if self.unique_id == coordinator_uid:
self._coordinator = None
self.schedule_update_ha_state()
for slave_uid in slave_uids:
slave = _get_entity_from_soco_uid(self.hass, slave_uid)
if slave:
# pylint: disable=protected-access
slave._coordinator = self
slave.schedule_update_ha_state()
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
@ -690,7 +706,7 @@ class SonosDevice(MediaPlayerDevice):
@property
def is_volume_muted(self):
"""Return true if volume is muted."""
return self._player_volume_muted
return self._player_muted
@property
@soco_coordinator
@ -988,7 +1004,7 @@ class SonosDevice(MediaPlayerDevice):
@soco_error()
@soco_coordinator
def update_alarm(self, **data):
def set_alarm(self, **data):
"""Set the alarm clock on the player."""
from soco import alarms
alarm = None
@ -1011,7 +1027,7 @@ class SonosDevice(MediaPlayerDevice):
alarm.save()
@soco_error()
def update_option(self, **data):
def set_option(self, **data):
"""Modify playback options."""
if ATTR_NIGHT_SOUND in data and self._night_sound is not None:
self.soco.night_mode = data[ATTR_NIGHT_SOUND]

View file

@ -276,7 +276,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
@mock.patch('soco.SoCo', new=SoCoMock)
@mock.patch('soco.alarms.Alarm')
@mock.patch('socket.create_connection', side_effect=socket.error())
def test_update_alarm(self, soco_mock, alarm_mock, *args):
def test_set_alarm(self, soco_mock, alarm_mock, *args):
"""Ensuring soco methods called for sonos_set_sleep_timer service."""
sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), {
'host': '192.0.2.1'
@ -293,9 +293,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
'include_linked_zones': True,
'volume': 0.30,
}
device.update_alarm(alarm_id=2)
device.set_alarm(alarm_id=2)
alarm1.save.assert_not_called()
device.update_alarm(alarm_id=1, **attrs)
device.set_alarm(alarm_id=1, **attrs)
self.assertEqual(alarm1.enabled, attrs['enabled'])
self.assertEqual(alarm1.start_time, attrs['time'])
self.assertEqual(alarm1.include_linked_zones,