Skip to content

Commit

Permalink
Add media_browser and media_source support (#22)
Browse files Browse the repository at this point in the history
* Add media_browser and media_source support

* Update README

* Bump minimum supported HA version

* Workaround: Enable 'play' support to draw Media Browser icon

* Use 'play' to replay the last clip

* Adjust playback debug message
  • Loading branch information
jjlawren authored Feb 24, 2022
1 parent c2b86b4 commit 3ea6c3d
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 12 deletions.
41 changes: 33 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,21 @@ 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.<platform>_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:
1. Use `data`->`extra`->`volume` if provided in the `media_player.play_media` call.
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)
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions custom_components/sonos_cloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"http"
],
"after_dependencies": [
"media_source",
"sonos"
],
"codeowners": [
Expand Down
87 changes: 84 additions & 3 deletions custom_components/sonos_cloud/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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"):
Expand All @@ -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,
)
2 changes: 1 addition & 1 deletion hacs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

0 comments on commit 3ea6c3d

Please sign in to comment.