Skip to content

Commit

Permalink
Add support for simultaneous playback on bonded speakers
Browse files Browse the repository at this point in the history
  • Loading branch information
jjlawren committed Jan 10, 2022
1 parent 9ab0844 commit 0e7cbbb
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 11 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ data:
message: "Hello there"
```

Service calls to `media_player.play_media` can accept an optional `volume` parameter to play the clip at a different volume than the currently playing music:
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)
```yaml
service: media_player.play_media
data:
Expand All @@ -48,6 +50,7 @@ data:
media_content_type: music
extra:
volume: 35 # Can be provided as 0-100 or 0.0-0.99
play_on_bonded: true
```

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:
Expand All @@ -65,7 +68,9 @@ If you encounter issues playing audio when using this integration, it may be rel

## Home theater & stereo pair configurations

This API targets a specific speaker to play the alert and does not play on all speakers in a "room". For example, a stereo pair will only play back audio on the left speaker and a home theater setup will play from the "primary" speaker. This appears to be a current limitation of the Sonos API.
A stereo pair will only play back audio on the left speaker and a home theater setup will play from the "primary" speaker. This is because of a limitation in the API which can only target a single speaker device at a time.

When using the `play_on_bonded` extra key, the integration will attempt to play the audio on all bonded speakers in a "room" by making multiple simultaneous calls. Since playback may not be perfectly synchronized with this method it is not enabled by default.

## Media URLs

Expand Down
33 changes: 24 additions & 9 deletions custom_components/sonos_cloud/media_player.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Support to interface with Sonos players."""
from __future__ import annotations

import asyncio
import logging
from typing import Any

Expand All @@ -23,24 +24,28 @@

ATTR_VOLUME = "volume"

AUDIO_CLIP_URI = "https://api.ws.sonos.com/control/api/v1/players/{device}/audioClip"


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Sonos cloud from a config entry."""
for player in hass.data[DOMAIN][PLAYERS]:
async_add_entities([SonosCloudMediaPlayerEntity(player["name"], player["id"])])
async_add_entities(
[SonosCloudMediaPlayerEntity(player) for player in hass.data[DOMAIN][PLAYERS]]
)


class SonosCloudMediaPlayerEntity(MediaPlayerEntity):
"""Representation of a Sonos Cloud entity."""

def __init__(self, zone_name, identifier):
def __init__(self, player: dict[str, Any]):
"""Initializle the entity."""
self._attr_name = zone_name
self._attr_unique_id = identifier
self._attr_name = player["name"]
self._attr_unique_id = player["id"]
self.zone_devices = player["deviceIds"]

@property
def state(self) -> str:
Expand Down Expand Up @@ -69,13 +74,16 @@ async def async_play_media(
Used to play audio clips over the currently playing music.
"""
url = f"https://api.ws.sonos.com/control/api/v1/players/{self.unique_id}/audioClip"
data = {
"name": "HA Audio Clip",
"appId": "jjlawren.home-assistant.sonos_cloud",
}
devices = [self.unique_id]
_LOGGER.info("Playing %s", media_id)

if extra := kwargs.get(ATTR_MEDIA_EXTRA):
if extra.get("play_on_bonded"):
devices = self.zone_devices
if volume := extra.get(ATTR_VOLUME):
_LOGGER.info("Type of %s: %s", volume, type(volume))
if type(volume) not in (int, float):
Expand All @@ -87,7 +95,14 @@ async def async_play_media(
data[ATTR_VOLUME] = int(volume)
if media_id != "CHIME":
data["streamUrl"] = media_id

session = self.hass.data[DOMAIN][SESSION]
result = await session.async_request("post", url, json=data)
json = await result.json()
_LOGGER.debug("Result: %s", json)
requests = []

for device in devices:
url = AUDIO_CLIP_URI.format(device=device)
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)

0 comments on commit 0e7cbbb

Please sign in to comment.