diff --git a/README.md b/README.md index 6c1d851..c2b7dbb 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ On the Integrations page in Home Assistant, add a new "Sonos Cloud" integration. The integration will create new `media_player` entities for each Sonos device in your household. These are created in order to use the `tts._say` and `media_player.play_media` services to play the clips. +## Media Browser & Media Source + +Support for browsing and playing back local audio clips using the Media Browser is supported. [Media Source](https://www.home-assistant.io/integrations/media_source/) URLs for local media and TTS can also be provided to `media_player.play_media`. + ## Volume control The playback volume can be set per audio clip and will automatically revert to the previous level when the clip finishes playing. The volume used is chosen in the following order: @@ -37,17 +41,10 @@ The playback volume can be set per audio clip and will automatically revert to t 2. Use the volume on the `media_player` entity created by this integration. This default can be disabled by setting the volume slider back to 0. Note that this volume slider _only_ affects the default audio clip playback volume. 3. If neither of the above is provided, the current volume set on the speaker will be used. -**Note**: Volume adjustments only work with the `media_player.play_media` service call. +**Note**: Volume adjustments only work with the `media_player.play_media` service call. For TTS volume control, use `media_player.play_media` with a [Media Source](https://www.home-assistant.io/integrations/media_source/) TTS URL (see below). # Examples -```yaml -service: tts.cloud_say -data: - entity_id: media_player.front_room - message: "Hello there" -``` - Service calls to `media_player.play_media` can accept optional parameters under `data`->`extra`: * `volume` will play the clip at a different volume than the currently playing music * `play_on_bonded` will play on all _bonded_ speakers in a "room" (see [notes](#home-theater--stereo-pair-configurations) below) @@ -62,6 +59,34 @@ data: play_on_bonded: true ``` +[Media Source](https://www.home-assistant.io/integrations/media_source/) URLs are supported: +```yaml +service: media_player.play_media +data: + entity_id: media_player.kitchen + media_content_id: media-source://media_source/local/doorbell.mp3 + media_content_type: music +``` + +TTS volume controls can be used with a Media Source TTS URL: +```yaml +service: media_player.play_media +data: + entity_id: media_player.kitchen + media_content_id: media-source://tts/cloud?message=I am very loud + media_content_type: music + extra: + volume: 80 +``` + +"Standard" TTS service calls can also be used, but the extra parameters cannot be used: +```yaml +service: tts.cloud_say +data: + entity_id: media_player.front_room + message: "Hello there" +``` + A special `media_content_id` of "CHIME" can be used to test the integration using the built-in sound provided by Sonos. This can be useful for validation if your own URLs are not playing correctly: ```yaml service: media_player.play_media diff --git a/custom_components/sonos_cloud/manifest.json b/custom_components/sonos_cloud/manifest.json index df524d4..4bb21c8 100644 --- a/custom_components/sonos_cloud/manifest.json +++ b/custom_components/sonos_cloud/manifest.json @@ -12,6 +12,7 @@ "http" ], "after_dependencies": [ + "media_source", "sonos" ], "codeowners": [ diff --git a/custom_components/sonos_cloud/media_player.py b/custom_components/sonos_cloud/media_player.py index 13995d3..ce36b13 100644 --- a/custom_components/sonos_cloud/media_player.py +++ b/custom_components/sonos_cloud/media_player.py @@ -5,12 +5,21 @@ import logging from typing import Any -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components import media_source +from homeassistant.components.media_player import ( + BrowseMedia, + MediaPlayerEntity, + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( ATTR_MEDIA_EXTRA, + MEDIA_CLASS_DIRECTORY, + SUPPORT_BROWSE_MEDIA, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_SET, ) +from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.sonos.const import DOMAIN as SONOS_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE @@ -49,6 +58,7 @@ def __init__(self, player: dict[str, Any]): self._attr_unique_id = player["id"] self._attr_volume_level = 0 self.zone_devices = player["deviceIds"] + self.last_call = None async def async_added_to_hass(self): """Complete entity setup.""" @@ -71,7 +81,12 @@ def state(self) -> str: @property def supported_features(self) -> int: """Flag media player features that are supported.""" - return SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET + return ( + SUPPORT_BROWSE_MEDIA + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_VOLUME_SET + ) @property def device_info(self) -> DeviceInfo: @@ -86,6 +101,16 @@ async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" self._attr_volume_level = volume + async def async_media_play(self) -> None: + """Replay last clip.""" + if not self.last_call: + _LOGGER.debug("No previous clip found for %s", self.name) + return + + media_id, kwargs = self.last_call + _LOGGER.debug("Replaying last clip on %s: %s / %s", self.name, media_id, kwargs) + await self.async_play_media(None, media_id, **kwargs) + async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any ) -> None: @@ -94,12 +119,17 @@ async def async_play_media( Used to play audio clips over the currently playing music. """ + if media_source.is_media_source_id(media_id): + media_source_item = await media_source.async_resolve_media( + self.hass, media_id + ) + media_id = async_process_play_media_url(self.hass, media_source_item.url) + data = { "name": "HA Audio Clip", "appId": "jjlawren.home-assistant.sonos_cloud", } devices = [self.unique_id] - _LOGGER.debug("Playing %s on %s", media_id, self.name) if extra := kwargs.get(ATTR_MEDIA_EXTRA): if extra.get("play_on_bonded"): @@ -121,13 +151,64 @@ async def async_play_media( if media_id != "CHIME": data["streamUrl"] = media_id + self.last_call = (media_id, kwargs) + session = self.hass.data[DOMAIN][SESSION] requests = [] for device in devices: url = AUDIO_CLIP_URI.format(device=device) + _LOGGER.debug("Playing on %s (%s): %s", self.name, device, data) requests.append(session.async_request("post", url, json=data)) results = await asyncio.gather(*requests, return_exceptions=True) for result in results: json = await result.json() _LOGGER.debug("Response for %s: %s", result.url, json) + + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> Any: + """Implement the websocket media browsing helper.""" + if media_content_id is None: + return await root_payload(self.hass) + + if media_source.is_media_source_id(media_content_id): + return await media_source.async_browse_media( + self.hass, media_content_id, content_filter=media_source_filter + ) + + raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}") + + +def media_source_filter(item: BrowseMedia): + """Filter media sources.""" + return item.media_content_type.startswith("audio/") + + +async def root_payload( + hass: HomeAssistant, +): + """Return root payload for Sonos Cloud.""" + children = [] + + try: + item = await media_source.async_browse_media( + hass, None, content_filter=media_source_filter + ) + # If domain is None, it's overview of available sources + if item.domain is None: + children.extend(item.children) + else: + children.append(item) + except media_source.BrowseError: + pass + + return BrowseMedia( + title="Sonos Cloud", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="", + media_content_type="root", + can_play=False, + can_expand=True, + children=children, + ) diff --git a/hacs.json b/hacs.json index 7ff01bb..1528b01 100644 --- a/hacs.json +++ b/hacs.json @@ -2,7 +2,7 @@ "name": "Sonos Cloud", "country": "US", "domains": ["media_player"], - "homeassistant": "2021.7.0", + "homeassistant": "2022.3.0b0", "render_readme": true, "iot_class": "Cloud Polling" }