Make sonos event asyncio (#48618)
This commit is contained in:
parent
d3b4a30e18
commit
bc06100dd8
5 changed files with 80 additions and 54 deletions
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue