From f8127a3902abaa6cb654ca7c28905adc81ee0623 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Mar 2018 03:27:07 +0100 Subject: [PATCH] Add a polling fallback for Sonos (#13310) * Prepare for poll * Add a polling fallback for Sonos --- .../components/media_player/sonos.py | 280 +++++++++--------- tests/components/media_player/test_sonos.py | 6 +- 2 files changed, 151 insertions(+), 135 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 091046a6e7a..34ef146fc05 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -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] diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index f741898d15e..7d0d675f66f 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -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,