Support Sonos announcements using websockets (#91145)

This commit is contained in:
jjlawren 2023-04-14 01:08:53 -05:00 committed by GitHub
parent a061f56833
commit 27f3b53872
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 51 additions and 18 deletions

View file

@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/sonos", "documentation": "https://www.home-assistant.io/integrations/sonos",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["soco"], "loggers": ["soco"],
"requirements": ["soco==0.29.1"], "requirements": ["soco==0.29.1", "sonos-websocket==0.0.5"],
"ssdp": [ "ssdp": [
{ {
"st": "urn:schemas-upnp-org:device:ZonePlayer:1" "st": "urn:schemas-upnp-org:device:ZonePlayer:1"

View file

@ -1,8 +1,8 @@
"""Support to interface with Sonos players.""" """Support to interface with Sonos players."""
from __future__ import annotations from __future__ import annotations
from asyncio import run_coroutine_threadsafe
import datetime import datetime
from functools import partial
import logging import logging
from typing import Any from typing import Any
@ -14,11 +14,13 @@ from soco.core import (
PLAY_MODES, PLAY_MODES,
) )
from soco.data_structures import DidlFavorite from soco.data_structures import DidlFavorite
from sonos_websocket.exception import SonosWebsocketError
import voluptuous as vol import voluptuous as vol
from homeassistant.components import media_source, spotify from homeassistant.components import media_source, spotify
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE,
ATTR_MEDIA_ANNOUNCE,
ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_ENQUEUE,
BrowseMedia, BrowseMedia,
MediaPlayerDeviceClass, MediaPlayerDeviceClass,
@ -491,8 +493,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
"""Clear players playlist.""" """Clear players playlist."""
self.coordinator.soco.clear_queue() self.coordinator.soco.clear_queue()
@soco_error() async def async_play_media(
def play_media( # noqa: C901
self, media_type: MediaType | str, media_id: str, **kwargs: Any self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None: ) -> None:
"""Send the play_media command to the media player. """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 If media_type is "playlist", media_id should be a Sonos
Playlist name. Otherwise, media_id should be a URI. Playlist name. Otherwise, media_id should be a URI.
""" """
# Use 'replace' as the default enqueue option if kwargs.get(ATTR_MEDIA_ANNOUNCE):
enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) 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): if spotify.is_spotify_media_type(media_type):
media_type = spotify.resolve_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): if media_source.is_media_source_id(media_id):
is_radio = media_id.startswith("media-source://radio_browser/") is_radio = media_id.startswith("media-source://radio_browser/")
media_type = MediaType.MUSIC media_type = MediaType.MUSIC
media_id = ( media = await media_source.async_resolve_media(
run_coroutine_threadsafe( self.hass, media_id, self.entity_id
media_source.async_resolve_media(
self.hass, media_id, self.entity_id
),
self.hass.loop,
)
.result()
.url
) )
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": if media_type == "favorite_item_id":
favorite = self.speaker.favorites.lookup_by_item_id(media_id) favorite = self.speaker.favorites.lookup_by_item_id(media_id)

View file

@ -18,12 +18,14 @@ from soco.exceptions import SoCoException, SoCoUPnPException
from soco.plugins.plex import PlexPlugin from soco.plugins.plex import PlexPlugin
from soco.plugins.sharelink import ShareLinkPlugin from soco.plugins.sharelink import ShareLinkPlugin
from soco.snapshot import Snapshot from soco.snapshot import Snapshot
from sonos_websocket import SonosWebsocket
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
@ -97,6 +99,7 @@ class SonosSpeaker:
"""Initialize a SonosSpeaker.""" """Initialize a SonosSpeaker."""
self.hass = hass self.hass = hass
self.soco = soco self.soco = soco
self.websocket: SonosWebsocket | None = None
self.household_id: str = soco.household_id self.household_id: str = soco.household_id
self.media = SonosMedia(hass, soco) self.media = SonosMedia(hass, soco)
self._plex_plugin: PlexPlugin | None = None self._plex_plugin: PlexPlugin | None = None
@ -170,8 +173,13 @@ class SonosSpeaker:
self.snapshot_group: list[SonosSpeaker] = [] self.snapshot_group: list[SonosSpeaker] = []
self._group_members_missing: set[str] = set() self._group_members_missing: set[str] = set()
async def async_setup_dispatchers(self, entry: ConfigEntry) -> None: async def async_setup(self, entry: ConfigEntry) -> None:
"""Connect dispatchers in async context during setup.""" """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]], ...] = ( dispatch_pairs: tuple[tuple[str, Callable[..., Any]], ...] = (
(SONOS_CHECK_ACTIVITY, self.async_check_activity), (SONOS_CHECK_ACTIVITY, self.async_check_activity),
(SONOS_SPEAKER_ADDED, self.update_group_for_uid), (SONOS_SPEAKER_ADDED, self.update_group_for_uid),
@ -198,7 +206,7 @@ class SonosSpeaker:
self.media.poll_media() self.media.poll_media()
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
self.async_setup_dispatchers(entry), self.hass.loop self.async_setup(entry), self.hass.loop
) )
future.result(timeout=10) future.result(timeout=10)

View file

@ -2378,6 +2378,9 @@ solax==0.3.0
# homeassistant.components.somfy_mylink # homeassistant.components.somfy_mylink
somfy-mylink-synergy==1.0.6 somfy-mylink-synergy==1.0.6
# homeassistant.components.sonos
sonos-websocket==0.0.5
# homeassistant.components.marytts # homeassistant.components.marytts
speak2mary==1.4.0 speak2mary==1.4.0

View file

@ -1702,6 +1702,9 @@ solax==0.3.0
# homeassistant.components.somfy_mylink # homeassistant.components.somfy_mylink
somfy-mylink-synergy==1.0.6 somfy-mylink-synergy==1.0.6
# homeassistant.components.sonos
sonos-websocket==0.0.5
# homeassistant.components.marytts # homeassistant.components.marytts
speak2mary==1.4.0 speak2mary==1.4.0