Make sonos event asyncio (#48618)

This commit is contained in:
J. Nick Koston 2021-04-03 14:10:48 -10:00 committed by GitHub
parent d3b4a30e18
commit bc06100dd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 80 additions and 54 deletions

View file

@ -3,7 +3,7 @@
"name": "Sonos", "name": "Sonos",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos", "documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["pysonos==0.0.40"], "requirements": ["pysonos==0.0.41"],
"after_dependencies": ["plex"], "after_dependencies": ["plex"],
"ssdp": [ "ssdp": [
{ {

View file

@ -9,7 +9,7 @@ import urllib.parse
import async_timeout import async_timeout
import pysonos import pysonos
from pysonos import alarms from pysonos import alarms, events_asyncio
from pysonos.core import ( from pysonos.core import (
MUSIC_SRC_LINE_IN, MUSIC_SRC_LINE_IN,
MUSIC_SRC_RADIO, MUSIC_SRC_RADIO,
@ -162,6 +162,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
config = hass.data[SONOS_DOMAIN].get("media_player", {}) config = hass.data[SONOS_DOMAIN].get("media_player", {})
_LOGGER.debug("Reached async_setup_entry, config=%s", config) _LOGGER.debug("Reached async_setup_entry, config=%s", config)
pysonos.config.EVENTS_MODULE = events_asyncio
advertise_addr = config.get(CONF_ADVERTISE_ADDR) advertise_addr = config.get(CONF_ADVERTISE_ADDR)
if advertise_addr: if advertise_addr:
@ -224,6 +225,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
interval=DISCOVERY_INTERVAL, interval=DISCOVERY_INTERVAL,
interface_addr=config.get(CONF_INTERFACE_ADDR), interface_addr=config.get(CONF_INTERFACE_ADDR),
) )
hass.data[DATA_SONOS].discovery_thread.name = "Sonos-Discovery"
_LOGGER.debug("Adding discovery job") _LOGGER.debug("Adding discovery job")
hass.async_add_executor_job(_discovery) hass.async_add_executor_job(_discovery)
@ -446,12 +448,8 @@ class SonosEntity(MediaPlayerEntity):
self.hass.data[DATA_SONOS].entities.append(self) self.hass.data[DATA_SONOS].entities.append(self)
def _rebuild_groups(): for entity in self.hass.data[DATA_SONOS].entities:
"""Build the current group topology.""" await entity.async_update_groups_coro()
for entity in self.hass.data[DATA_SONOS].entities:
entity.update_groups()
self.hass.async_add_executor_job(_rebuild_groups)
@property @property
def unique_id(self): def unique_id(self):
@ -515,6 +513,7 @@ class SonosEntity(MediaPlayerEntity):
async def async_seen(self, player): async def async_seen(self, player):
"""Record that this player was seen right now.""" """Record that this player was seen right now."""
was_available = self.available was_available = self.available
_LOGGER.debug("Async seen: %s, was_available: %s", player, was_available)
self._player = player self._player = player
@ -532,15 +531,14 @@ class SonosEntity(MediaPlayerEntity):
self.update, datetime.timedelta(seconds=SCAN_INTERVAL) self.update, datetime.timedelta(seconds=SCAN_INTERVAL)
) )
done = await self.hass.async_add_executor_job(self._attach_player) done = await self._async_attach_player()
if not done: if not done:
self._seen_timer() self._seen_timer()
self.async_unseen() await self.async_unseen()
self.async_write_ha_state() self.async_write_ha_state()
@callback async def async_unseen(self, now=None):
def async_unseen(self, now=None):
"""Make this player unavailable when it was not seen recently.""" """Make this player unavailable when it was not seen recently."""
self._seen_timer = None self._seen_timer = None
@ -548,11 +546,8 @@ class SonosEntity(MediaPlayerEntity):
self._poll_timer() self._poll_timer()
self._poll_timer = None self._poll_timer = None
def _unsub(subscriptions): for subscription in self._subscriptions:
for subscription in subscriptions: await subscription.unsubscribe()
subscription.unsubscribe()
self.hass.async_add_executor_job(_unsub, self._subscriptions)
self._subscriptions = [] self._subscriptions = []
@ -581,29 +576,39 @@ class SonosEntity(MediaPlayerEntity):
_LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)
def _attach_player(self): def _attach_player(self):
"""Get basic information and add event subscriptions."""
self._play_mode = self.soco.play_mode
self.update_volume()
self._set_favorites()
async def _async_attach_player(self):
"""Get basic information and add event subscriptions.""" """Get basic information and add event subscriptions."""
try: try:
self._play_mode = self.soco.play_mode await self.hass.async_add_executor_job(self._attach_player)
self.update_volume()
self._set_favorites()
player = self.soco player = self.soco
def subscribe(sonos_service, action): if self._subscriptions:
"""Add a subscription to a pysonos service.""" raise RuntimeError(
queue = _ProcessSonosEventQueue(action) f"Attempted to attach subscriptions to player: {player} "
sub = sonos_service.subscribe(auto_renew=True, event_queue=queue) f"when existing subscriptions exist: {self._subscriptions}"
self._subscriptions.append(sub) )
subscribe(player.avTransport, self.update_media) await self._subscribe(player.avTransport, self.async_update_media)
subscribe(player.renderingControl, self.update_volume) await self._subscribe(player.renderingControl, self.async_update_volume)
subscribe(player.zoneGroupTopology, self.update_groups) await self._subscribe(player.zoneGroupTopology, self.async_update_groups)
subscribe(player.contentDirectory, self.update_content) await self._subscribe(player.contentDirectory, self.async_update_content)
return True return True
except SoCoException as ex: except SoCoException as ex:
_LOGGER.warning("Could not connect %s: %s", self.entity_id, ex) _LOGGER.warning("Could not connect %s: %s", self.entity_id, ex)
return False return False
async def _subscribe(self, target, sub_callback):
"""Create a sonos subscription."""
subscription = await target.subscribe(auto_renew=True)
subscription.callback = sub_callback
self._subscriptions.append(subscription)
@property @property
def should_poll(self): def should_poll(self):
"""Return that we should not be polled (we handle that internally).""" """Return that we should not be polled (we handle that internally)."""
@ -619,6 +624,11 @@ class SonosEntity(MediaPlayerEntity):
except SoCoException: except SoCoException:
pass pass
@callback
def async_update_media(self, event=None):
"""Update information about currently playing media."""
self.hass.async_add_job(self.update_media, event)
def update_media(self, event=None): def update_media(self, event=None):
"""Update information about currently playing media.""" """Update information about currently playing media."""
variables = event and event.variables variables = event and event.variables
@ -759,32 +769,47 @@ class SonosEntity(MediaPlayerEntity):
if playlist_position > 0: if playlist_position > 0:
self._queue_position = playlist_position - 1 self._queue_position = playlist_position - 1
def update_volume(self, event=None): @callback
def async_update_volume(self, event):
"""Update information about currently volume settings.""" """Update information about currently volume settings."""
if event: variables = event.variables
variables = event.variables
if "volume" in variables: if "volume" in variables:
self._player_volume = int(variables["volume"]["Master"]) self._player_volume = int(variables["volume"]["Master"])
if "mute" in variables: if "mute" in variables:
self._player_muted = variables["mute"]["Master"] == "1" self._player_muted = variables["mute"]["Master"] == "1"
if "night_mode" in variables: if "night_mode" in variables:
self._night_sound = variables["night_mode"] == "1" self._night_sound = variables["night_mode"] == "1"
if "dialog_level" in variables: if "dialog_level" in variables:
self._speech_enhance = variables["dialog_level"] == "1" self._speech_enhance = variables["dialog_level"] == "1"
self.schedule_update_ha_state() self.async_write_ha_state()
else:
self._player_volume = self.soco.volume def update_volume(self):
self._player_muted = self.soco.mute """Update information about currently volume settings."""
self._night_sound = self.soco.night_mode self._player_volume = self.soco.volume
self._speech_enhance = self.soco.dialog_mode 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): def update_groups(self, event=None):
"""Handle callback for topology change event.""" """Handle callback for topology change event."""
coro = self.async_update_groups_coro(event)
if coro:
self.hass.add_job(coro)
@callback
def async_update_groups(self, event=None):
"""Handle callback for topology change event."""
coro = self.async_update_groups_coro(event)
if coro:
self.hass.async_add_job(coro)
def async_update_groups_coro(self, event=None):
"""Handle callback for topology change event."""
def _get_soco_group(): def _get_soco_group():
"""Ask SoCo cache for existing topology.""" """Ask SoCo cache for existing topology."""
@ -849,13 +874,13 @@ class SonosEntity(MediaPlayerEntity):
if event and not hasattr(event, "zone_player_uui_ds_in_group"): if event and not hasattr(event, "zone_player_uui_ds_in_group"):
return return
self.hass.add_job(_async_handle_group_event(event)) return _async_handle_group_event(event)
def update_content(self, event=None): def async_update_content(self, event=None):
"""Update information about available content.""" """Update information about available content."""
if event and "favorites_update_id" in event.variables: if event and "favorites_update_id" in event.variables:
self._set_favorites() self.hass.async_add_job(self._set_favorites)
self.schedule_update_ha_state() self.async_write_ha_state()
@property @property
def volume_level(self): def volume_level(self):

View file

@ -1717,7 +1717,7 @@ pysnmp==4.4.12
pysoma==0.0.10 pysoma==0.0.10
# homeassistant.components.sonos # homeassistant.components.sonos
pysonos==0.0.40 pysonos==0.0.41
# homeassistant.components.spc # homeassistant.components.spc
pyspcwebgw==0.4.0 pyspcwebgw==0.4.0

View file

@ -926,7 +926,7 @@ pysmartthings==0.7.6
pysoma==0.0.10 pysoma==0.0.10
# homeassistant.components.sonos # homeassistant.components.sonos
pysonos==0.0.40 pysonos==0.0.41
# homeassistant.components.spc # homeassistant.components.spc
pyspcwebgw==0.4.0 pyspcwebgw==0.4.0

View file

@ -1,5 +1,5 @@
"""Configuration for Sonos tests.""" """Configuration for Sonos tests."""
from unittest.mock import Mock, patch as patch from unittest.mock import AsyncMock, MagicMock, Mock, patch as patch
import pytest import pytest
@ -41,6 +41,7 @@ def discover_fixture(soco):
def do_callback(callback, **kwargs): def do_callback(callback, **kwargs):
callback(soco) callback(soco)
return MagicMock()
with patch("pysonos.discover_thread", side_effect=do_callback) as mock: with patch("pysonos.discover_thread", side_effect=do_callback) as mock:
yield mock yield mock
@ -56,7 +57,7 @@ def config_fixture():
def dummy_soco_service_fixture(): def dummy_soco_service_fixture():
"""Create dummy_soco_service fixture.""" """Create dummy_soco_service fixture."""
service = Mock() service = Mock()
service.subscribe = Mock() service.subscribe = AsyncMock()
return service return service