Support Sonos announcements using websockets (#91145)
This commit is contained in:
parent
a061f56833
commit
27f3b53872
5 changed files with 51 additions and 18 deletions
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue