Add a polling fallback for Sonos (#13310)
* Prepare for poll * Add a polling fallback for Sonos
This commit is contained in:
parent
3426487277
commit
f8127a3902
2 changed files with 151 additions and 135 deletions
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue