mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
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
@ -208,9 +208,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
elif service.service == SERVICE_CLEAR_TIMER:
|
elif service.service == SERVICE_CLEAR_TIMER:
|
||||||
device.clear_sleep_timer()
|
device.clear_sleep_timer()
|
||||||
elif service.service == SERVICE_UPDATE_ALARM:
|
elif service.service == SERVICE_UPDATE_ALARM:
|
||||||
device.update_alarm(**service.data)
|
device.set_alarm(**service.data)
|
||||||
elif service.service == SERVICE_SET_OPTION:
|
elif service.service == SERVICE_SET_OPTION:
|
||||||
device.update_option(**service.data)
|
device.set_option(**service.data)
|
||||||
|
|
||||||
device.schedule_update_ha_state(True)
|
device.schedule_update_ha_state(True)
|
||||||
|
|
||||||
@ -330,12 +330,13 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
def __init__(self, player):
|
def __init__(self, player):
|
||||||
"""Initialize the Sonos device."""
|
"""Initialize the Sonos device."""
|
||||||
|
self._receives_events = False
|
||||||
self._volume_increment = 5
|
self._volume_increment = 5
|
||||||
self._unique_id = player.uid
|
self._unique_id = player.uid
|
||||||
self._player = player
|
self._player = player
|
||||||
self._model = None
|
self._model = None
|
||||||
self._player_volume = None
|
self._player_volume = None
|
||||||
self._player_volume_muted = None
|
self._player_muted = None
|
||||||
self._play_mode = None
|
self._play_mode = None
|
||||||
self._name = None
|
self._name = None
|
||||||
self._coordinator = None
|
self._coordinator = None
|
||||||
@ -420,11 +421,9 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
speaker_info = self.soco.get_speaker_info(True)
|
speaker_info = self.soco.get_speaker_info(True)
|
||||||
self._name = speaker_info['zone_name']
|
self._name = speaker_info['zone_name']
|
||||||
self._model = speaker_info['model_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._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 = []
|
self._favorites = []
|
||||||
for fav in self.soco.music_library.get_sonos_favorites():
|
for fav in self.soco.music_library.get_sonos_favorites():
|
||||||
@ -437,124 +436,6 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.debug("Ignoring invalid favorite '%s'", fav.title)
|
_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):
|
def _radio_artwork(self, url):
|
||||||
"""Return the private URL with artwork for a radio stream."""
|
"""Return the private URL with artwork for a radio stream."""
|
||||||
if url not in ('', 'NOT_IMPLEMENTED', None):
|
if url not in ('', 'NOT_IMPLEMENTED', None):
|
||||||
@ -568,7 +449,88 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
)
|
)
|
||||||
return url
|
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."""
|
"""Update state when playing from line-in/tv."""
|
||||||
self._media_duration = None
|
self._media_duration = None
|
||||||
self._media_position = None
|
self._media_position = None
|
||||||
@ -582,7 +544,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
self._source_name = source
|
self._source_name = source
|
||||||
|
|
||||||
def _refresh_radio(self, variables, track_info):
|
def update_media_radio(self, variables, track_info):
|
||||||
"""Update state when streaming radio."""
|
"""Update state when streaming radio."""
|
||||||
self._media_duration = None
|
self._media_duration = None
|
||||||
self._media_position = None
|
self._media_position = None
|
||||||
@ -603,7 +565,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
artist=self._media_artist,
|
artist=self._media_artist,
|
||||||
title=self._media_title
|
title=self._media_title
|
||||||
)
|
)
|
||||||
else:
|
elif variables:
|
||||||
# "On Now" field in the sonos pc app
|
# "On Now" field in the sonos pc app
|
||||||
current_track_metadata = variables.get('current_track_meta_data')
|
current_track_metadata = variables.get('current_track_meta_data')
|
||||||
if current_track_metadata:
|
if current_track_metadata:
|
||||||
@ -643,7 +605,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
if fav.reference.get_uri() == media_info['CurrentURI']:
|
if fav.reference.get_uri() == media_info['CurrentURI']:
|
||||||
self._source_name = fav.title
|
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."""
|
"""Update state when playing music tracks."""
|
||||||
self._media_duration = _timespan_secs(track_info.get('duration'))
|
self._media_duration = _timespan_secs(track_info.get('duration'))
|
||||||
|
|
||||||
@ -682,6 +644,60 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
self._source_name = None
|
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
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
"""Volume level of the media player (0..1)."""
|
"""Volume level of the media player (0..1)."""
|
||||||
@ -690,7 +706,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def is_volume_muted(self):
|
def is_volume_muted(self):
|
||||||
"""Return true if volume is muted."""
|
"""Return true if volume is muted."""
|
||||||
return self._player_volume_muted
|
return self._player_muted
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@soco_coordinator
|
@soco_coordinator
|
||||||
@ -988,7 +1004,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
@soco_error()
|
@soco_error()
|
||||||
@soco_coordinator
|
@soco_coordinator
|
||||||
def update_alarm(self, **data):
|
def set_alarm(self, **data):
|
||||||
"""Set the alarm clock on the player."""
|
"""Set the alarm clock on the player."""
|
||||||
from soco import alarms
|
from soco import alarms
|
||||||
alarm = None
|
alarm = None
|
||||||
@ -1011,7 +1027,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
alarm.save()
|
alarm.save()
|
||||||
|
|
||||||
@soco_error()
|
@soco_error()
|
||||||
def update_option(self, **data):
|
def set_option(self, **data):
|
||||||
"""Modify playback options."""
|
"""Modify playback options."""
|
||||||
if ATTR_NIGHT_SOUND in data and self._night_sound is not None:
|
if ATTR_NIGHT_SOUND in data and self._night_sound is not None:
|
||||||
self.soco.night_mode = data[ATTR_NIGHT_SOUND]
|
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.SoCo', new=SoCoMock)
|
||||||
@mock.patch('soco.alarms.Alarm')
|
@mock.patch('soco.alarms.Alarm')
|
||||||
@mock.patch('socket.create_connection', side_effect=socket.error())
|
@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."""
|
"""Ensuring soco methods called for sonos_set_sleep_timer service."""
|
||||||
sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), {
|
sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), {
|
||||||
'host': '192.0.2.1'
|
'host': '192.0.2.1'
|
||||||
@ -293,9 +293,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
|
|||||||
'include_linked_zones': True,
|
'include_linked_zones': True,
|
||||||
'volume': 0.30,
|
'volume': 0.30,
|
||||||
}
|
}
|
||||||
device.update_alarm(alarm_id=2)
|
device.set_alarm(alarm_id=2)
|
||||||
alarm1.save.assert_not_called()
|
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.enabled, attrs['enabled'])
|
||||||
self.assertEqual(alarm1.start_time, attrs['time'])
|
self.assertEqual(alarm1.start_time, attrs['time'])
|
||||||
self.assertEqual(alarm1.include_linked_zones,
|
self.assertEqual(alarm1.include_linked_zones,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user