From 27f3b538722f0d876f577abf640b9ed071e824f7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 14 Apr 2023 01:08:53 -0500 Subject: [PATCH] Support Sonos announcements using websockets (#91145) --- homeassistant/components/sonos/manifest.json | 2 +- .../components/sonos/media_player.py | 47 +++++++++++++------ homeassistant/components/sonos/speaker.py | 14 ++++-- requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index e1b3c6c1133..9c6f93fc2a4 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/sonos", "iot_class": "local_push", "loggers": ["soco"], - "requirements": ["soco==0.29.1"], + "requirements": ["soco==0.29.1", "sonos-websocket==0.0.5"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index cb18ec43887..7ef103e4d04 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1,8 +1,8 @@ """Support to interface with Sonos players.""" from __future__ import annotations -from asyncio import run_coroutine_threadsafe import datetime +from functools import partial import logging from typing import Any @@ -14,11 +14,13 @@ from soco.core import ( PLAY_MODES, ) from soco.data_structures import DidlFavorite +from sonos_websocket.exception import SonosWebsocketError import voluptuous as vol from homeassistant.components import media_source, spotify from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ENQUEUE, BrowseMedia, MediaPlayerDeviceClass, @@ -491,8 +493,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Clear players playlist.""" self.coordinator.soco.clear_queue() - @soco_error() - def play_media( # noqa: C901 + async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Send the play_media command to the media player. @@ -505,8 +506,21 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. """ - # Use 'replace' as the default enqueue option - enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) + if kwargs.get(ATTR_MEDIA_ANNOUNCE): + volume = kwargs.get("extra", {}).get("volume") + _LOGGER.debug("Playing %s using websocket audioclip", media_id) + try: + assert self.speaker.websocket + response, _ = await self.speaker.websocket.play_clip( + media_id, + volume=volume, + ) + except SonosWebsocketError as exc: + raise HomeAssistantError( + f"Error when calling Sonos websocket: {exc}" + ) from exc + if response["success"]: + return if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) @@ -517,16 +531,21 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if media_source.is_media_source_id(media_id): is_radio = media_id.startswith("media-source://radio_browser/") media_type = MediaType.MUSIC - media_id = ( - run_coroutine_threadsafe( - media_source.async_resolve_media( - self.hass, media_id, self.entity_id - ), - self.hass.loop, - ) - .result() - .url + media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id ) + media_id = media.url + + await self.hass.async_add_executor_job( + partial(self._play_media, media_type, media_id, is_radio, **kwargs) + ) + + @soco_error() + def _play_media( + self, media_type: MediaType | str, media_id: str, is_radio: bool, **kwargs: Any + ) -> None: + """Wrap sync calls to async_play_media.""" + enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) if media_type == "favorite_item_id": favorite = self.speaker.favorites.lookup_by_item_id(media_id) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 638ede722f5..25ba9c86d1f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -18,12 +18,14 @@ from soco.exceptions import SoCoException, SoCoUPnPException from soco.plugins.plex import PlexPlugin from soco.plugins.sharelink import ShareLinkPlugin from soco.snapshot import Snapshot +from sonos_websocket import SonosWebsocket from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -97,6 +99,7 @@ class SonosSpeaker: """Initialize a SonosSpeaker.""" self.hass = hass self.soco = soco + self.websocket: SonosWebsocket | None = None self.household_id: str = soco.household_id self.media = SonosMedia(hass, soco) self._plex_plugin: PlexPlugin | None = None @@ -170,8 +173,13 @@ class SonosSpeaker: self.snapshot_group: list[SonosSpeaker] = [] self._group_members_missing: set[str] = set() - async def async_setup_dispatchers(self, entry: ConfigEntry) -> None: - """Connect dispatchers in async context during setup.""" + async def async_setup(self, entry: ConfigEntry) -> None: + """Complete setup in async context.""" + self.websocket = SonosWebsocket( + self.soco.ip_address, + player_id=self.soco.uid, + session=async_get_clientsession(self.hass), + ) dispatch_pairs: tuple[tuple[str, Callable[..., Any]], ...] = ( (SONOS_CHECK_ACTIVITY, self.async_check_activity), (SONOS_SPEAKER_ADDED, self.update_group_for_uid), @@ -198,7 +206,7 @@ class SonosSpeaker: self.media.poll_media() future = asyncio.run_coroutine_threadsafe( - self.async_setup_dispatchers(entry), self.hass.loop + self.async_setup(entry), self.hass.loop ) future.result(timeout=10) diff --git a/requirements_all.txt b/requirements_all.txt index 7abb024fb4b..0449c309981 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,6 +2378,9 @@ solax==0.3.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 +# homeassistant.components.sonos +sonos-websocket==0.0.5 + # homeassistant.components.marytts speak2mary==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b649186650..65b65d317c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1702,6 +1702,9 @@ solax==0.3.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 +# homeassistant.components.sonos +sonos-websocket==0.0.5 + # homeassistant.components.marytts speak2mary==1.4.0