From 3734fa948f5248448d1c22eef20c8ebad3d2da09 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:12:42 +0200 Subject: [PATCH] LinkPlay multiroom support (#127862) --- homeassistant/components/linkplay/__init__.py | 17 ++++- homeassistant/components/linkplay/const.py | 5 ++ .../components/linkplay/media_player.py | 76 ++++++++++++++++++- .../components/linkplay/strings.json | 5 ++ 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/linkplay/__init__.py b/homeassistant/components/linkplay/__init__.py index 808f2f93ce2..918e52a755d 100644 --- a/homeassistant/components/linkplay/__init__.py +++ b/homeassistant/components/linkplay/__init__.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from aiohttp import ClientSession from linkplay.bridge import LinkPlayBridge +from linkplay.controller import LinkPlayController from linkplay.discovery import linkplay_factory_httpapi_bridge from linkplay.exceptions import LinkPlayRequestException @@ -12,7 +13,7 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import PLATFORMS +from .const import CONTROLLER, CONTROLLER_KEY, DOMAIN, PLATFORMS from .utils import async_get_client_session @@ -32,6 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> session: ClientSession = await async_get_client_session(hass) bridge: LinkPlayBridge | None = None + # try create a bridge try: bridge = await linkplay_factory_httpapi_bridge(entry.data[CONF_HOST], session) except LinkPlayRequestException as exception: @@ -39,6 +41,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> f"Failed to connect to LinkPlay device at {entry.data[CONF_HOST]}" ) from exception + # setup the controller and discover multirooms + controller: LinkPlayController | None = None + hass.data.setdefault(DOMAIN, {}) + if CONTROLLER not in hass.data[DOMAIN]: + controller = LinkPlayController(session) + hass.data[DOMAIN][CONTROLLER_KEY] = controller + else: + controller = hass.data[DOMAIN][CONTROLLER_KEY] + + await controller.add_bridge(bridge) + await controller.discover_multirooms() + + # forward to platforms entry.runtime_data = LinkPlayData(bridge=bridge) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/linkplay/const.py b/homeassistant/components/linkplay/const.py index f531e311f46..a776365e38f 100644 --- a/homeassistant/components/linkplay/const.py +++ b/homeassistant/components/linkplay/const.py @@ -1,7 +1,12 @@ """LinkPlay constants.""" +from linkplay.controller import LinkPlayController + from homeassistant.const import Platform +from homeassistant.util.hass_dict import HassKey DOMAIN = "linkplay" +CONTROLLER = "controller" +CONTROLLER_KEY: HassKey[LinkPlayController] = HassKey(CONTROLLER) PLATFORMS = [Platform.MEDIA_PLAYER] DATA_SESSION = "session" diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 8654600ac73..5e667af37ad 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -8,6 +8,7 @@ from typing import Any, Concatenate from linkplay.bridge import LinkPlayBridge from linkplay.consts import EqualizerMode, LoopMode, PlayingMode, PlayingStatus +from linkplay.controller import LinkPlayController, LinkPlayMultiroom from linkplay.exceptions import LinkPlayException, LinkPlayRequestException import voluptuous as vol @@ -22,18 +23,20 @@ from homeassistant.components.media_player import ( RepeatMode, async_process_play_media_url, ) +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_platform, + entity_registry as er, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from . import LinkPlayConfigEntry -from .const import DOMAIN +from . import LinkPlayConfigEntry, LinkPlayData +from .const import CONTROLLER_KEY, DOMAIN from .utils import MANUFACTURER_GENERIC, get_info_from_project _LOGGER = logging.getLogger(__name__) @@ -290,6 +293,73 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): """Play preset number.""" await self._bridge.player.play_preset(preset_number) + @exception_wrap + async def async_join_players(self, group_members: list[str]) -> None: + """Join `group_members` as a player group with the current player.""" + + controller: LinkPlayController = self.hass.data[DOMAIN][CONTROLLER_KEY] + multiroom = self._bridge.multiroom + if multiroom is None: + multiroom = LinkPlayMultiroom(self._bridge) + + for group_member in group_members: + bridge = self._get_linkplay_bridge(group_member) + if bridge: + await multiroom.add_follower(bridge) + + await controller.discover_multirooms() + + def _get_linkplay_bridge(self, entity_id: str) -> LinkPlayBridge: + """Get linkplay bridge from entity_id.""" + + entity_registry = er.async_get(self.hass) + + # Check for valid linkplay media_player entity + entity_entry = entity_registry.async_get(entity_id) + + if ( + entity_entry is None + or entity_entry.domain != Platform.MEDIA_PLAYER + or entity_entry.platform != DOMAIN + or entity_entry.config_entry_id is None + ): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_grouping_entity", + translation_placeholders={"entity_id": entity_id}, + ) + + config_entry = self.hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + assert config_entry + + # Return bridge + data: LinkPlayData = config_entry.runtime_data + return data.bridge + + @property + def group_members(self) -> list[str]: + """List of players which are grouped together.""" + multiroom = self._bridge.multiroom + if multiroom is not None: + return [multiroom.leader.device.uuid] + [ + follower.device.uuid for follower in multiroom.followers + ] + + return [] + + @exception_wrap + async def async_unjoin_player(self) -> None: + """Remove this player from any group.""" + controller: LinkPlayController = self.hass.data[DOMAIN][CONTROLLER_KEY] + + multiroom = self._bridge.multiroom + if multiroom is not None: + await multiroom.remove_follower(self._bridge) + + await controller.discover_multirooms() + def _update_properties(self) -> None: """Update the properties of the media player.""" self._attr_available = True diff --git a/homeassistant/components/linkplay/strings.json b/homeassistant/components/linkplay/strings.json index 12870816af7..f3495b293e0 100644 --- a/homeassistant/components/linkplay/strings.json +++ b/homeassistant/components/linkplay/strings.json @@ -34,5 +34,10 @@ } } } + }, + "exceptions": { + "invalid_grouping_entity": { + "message": "Entity with id {entity_id} can't be added to the LinkPlay multiroom. Is the entity a LinkPlay mediaplayer?" + } } }