Add platform services to Roon media player to allow grouping, ungrouping and transfer between players (#42133)

This commit is contained in:
Greg Dowling 2020-10-27 20:50:20 +00:00 committed by GitHub
parent e8ec9b5b40
commit 30a3fe69bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 174 additions and 0 deletions

View File

@ -1,6 +1,8 @@
"""MediaPlayer platform for Roon integration."""
import logging
import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
SUPPORT_BROWSE_MEDIA,
@ -26,6 +28,7 @@ from homeassistant.const import (
STATE_PLAYING,
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
@ -55,12 +58,38 @@ SUPPORT_ROON = (
_LOGGER = logging.getLogger(__name__)
SERVICE_JOIN = "join"
SERVICE_UNJOIN = "unjoin"
SERVICE_TRANSFER = "transfer"
ATTR_JOIN = "join_ids"
ATTR_UNJOIN = "unjoin_ids"
ATTR_TRANSFER = "transfer_id"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Roon MediaPlayer from Config Entry."""
roon_server = hass.data[DOMAIN][config_entry.entry_id]
media_players = set()
# Register entity services
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_JOIN,
{vol.Required(ATTR_JOIN): vol.All(cv.ensure_list, [cv.entity_id])},
"join",
)
platform.async_register_entity_service(
SERVICE_UNJOIN,
{vol.Optional(ATTR_UNJOIN): vol.All(cv.ensure_list, [cv.entity_id])},
"unjoin",
)
platform.async_register_entity_service(
SERVICE_TRANSFER,
{vol.Required(ATTR_TRANSFER): cv.entity_id},
"async_transfer",
)
@callback
def async_update_media_player(player_data):
"""Add or update Roon MediaPlayer."""
@ -118,6 +147,7 @@ class RoonDevice(MediaPlayerEntity):
self.async_update_callback,
)
)
self._server.add_player_id(self.entity_id, self.name)
@callback
def async_update_callback(self, player_data):
@ -478,6 +508,112 @@ class RoonDevice(MediaPlayerEntity):
media_id,
)
def join(self, join_ids):
"""Add another Roon player to this player's join group."""
zone_data = self._server.roonapi.zone_by_output_id(self._output_id)
if zone_data is None:
_LOGGER.error("No zone data for %s", self.name)
return
sync_available = {}
for zone in self._server.zones.values():
for output in zone["outputs"]:
if (
zone["display_name"] != self.name
and output["output_id"]
in self.player_data["can_group_with_output_ids"]
and zone["display_name"] not in sync_available.keys()
):
sync_available[zone["display_name"]] = output["output_id"]
names = []
for entity_id in join_ids:
name = self._server.roon_name(entity_id)
if name is None:
_LOGGER.error("No roon player found for %s", entity_id)
return
if name not in sync_available.keys():
_LOGGER.error(
"Can't join player %s with %s because it's not in the join available list %s",
name,
self.name,
list(sync_available.keys()),
)
return
names.append(name)
_LOGGER.info("Joining %s to %s", names, self.name)
self._server.roonapi.group_outputs(
[self._output_id] + [sync_available[name] for name in names]
)
def unjoin(self, unjoin_ids=None):
"""Remove a Roon player to this player's join group."""
zone_data = self._server.roonapi.zone_by_output_id(self._output_id)
if zone_data is None:
_LOGGER.error("No zone data for %s", self.name)
return
join_group = {
output["display_name"]: output["output_id"]
for output in zone_data["outputs"]
if output["display_name"] != self.name
}
if unjoin_ids is None:
# unjoin everything
names = list(join_group.keys())
else:
names = []
for entity_id in unjoin_ids:
name = self._server.roon_name(entity_id)
if name is None:
_LOGGER.error("No roon player found for %s", entity_id)
return
if name not in join_group.keys():
_LOGGER.error(
"Can't unjoin player %s from %s because it's not in the joined group %s",
name,
self.name,
list(join_group.keys()),
)
return
names.append(name)
_LOGGER.info("Unjoining %s from %s", names, self.name)
self._server.roonapi.ungroup_outputs([join_group[name] for name in names])
async def async_transfer(self, transfer_id):
"""Transfer playback from this roon player to another."""
name = self._server.roon_name(transfer_id)
if name is None:
_LOGGER.error("No roon player found for %s", transfer_id)
return
zone_ids = {
output["display_name"]: output["zone_id"]
for output in self._server.zones.values()
if output["display_name"] != self.name
}
transfer_id = zone_ids.get(name)
if transfer_id is None:
_LOGGER.error(
"Can't transfer from %s to %s because destination is not known %s",
self.name,
transfer_id,
list(zone_ids.keys()),
)
_LOGGER.info("Transferring from %s to %s", self.name, name)
await self.hass.async_add_executor_job(
self._server.roonapi.transfer_zone, self._zone_id, transfer_id
)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
return await self.hass.async_add_executor_job(

View File

@ -26,6 +26,7 @@ class RoonServer:
self.all_playlists = []
self.offline_devices = set()
self._exit = False
self._roon_name_by_id = {}
@property
def host(self):
@ -69,6 +70,14 @@ class RoonServer:
"""Return list of zones."""
return self.roonapi.zones
def add_player_id(self, entity_id, roon_name):
"""Register a roon player."""
self._roon_name_by_id[entity_id] = roon_name
def roon_name(self, entity_id):
"""Get the name of the roon player from entity_id."""
return self._roon_name_by_id[entity_id]
def stop_roon(self):
"""Stop background worker."""
self.roonapi.stop()

View File

@ -0,0 +1,29 @@
join:
description: Group players together.
fields:
entity_id:
description: id of the player that will be the master of the group.
example: "media_player.study"
join_ids:
description: id(s) of the players that will join the master.
example: "['media_player.bedroom', 'media_player.kitchen']"
unjoin:
description: Remove players from a group.
fields:
entity_id:
description: id of the player that is the master of the group..
example: "media_player.study"
unjoin_ids:
description: Optional id(s) of the players that will be unjoined from the group. If not specified, all players will be unjoined from the master.
example: "['media_player.bedroom', 'media_player.kitchen']"
transfer:
description: Transfer playback from one player to another.
fields:
entity_id:
description: id of the source player.
example: "media_player.bedroom"
transfer_id:
description: id of the destination player.
example: "media_player.study"