diff --git a/custom_components/bang_olufsen/config_flow.py b/custom_components/bang_olufsen/config_flow.py index 76e4656..85b7a22 100644 --- a/custom_components/bang_olufsen/config_flow.py +++ b/custom_components/bang_olufsen/config_flow.py @@ -25,6 +25,7 @@ DEFAULT_MODEL, DOMAIN, ) +from .util import get_serial_number_from_jid class EntryData(TypedDict, total=False): @@ -107,7 +108,7 @@ async def async_step_user( ) self._beolink_jid = beolink_self.jid - self._serial_number = beolink_self.jid.split(".")[2].split("@")[0] + self._serial_number = get_serial_number_from_jid(beolink_self.jid) await self.async_set_unique_id(self._serial_number) self._abort_if_unique_id_configured() diff --git a/custom_components/bang_olufsen/icons.json b/custom_components/bang_olufsen/icons.json index 2fbde89..162b261 100644 --- a/custom_components/bang_olufsen/icons.json +++ b/custom_components/bang_olufsen/icons.json @@ -1,13 +1,13 @@ { "services": { - "beolink_join": "mdi:location-enter", - "beolink_expand": "mdi:location-enter", - "beolink_unexpand": "mdi:location-exit", - "beolink_leave": "mdi:close-circle-outline", - "beolink_allstandby": "mdi:close-circle-multiple-outline", - "beolink_set_volume": "mdi:volume-equal", - "beolink_set_relative_volume": "mdi:volume-plus", - "beolink_leader_command": "mdi:location-enter", - "reboot": "mdi:restart" + "beolink_allstandby": { "service": "mdi:close-circle-multiple-outline" }, + "beolink_expand": { "service": "mdi:location-enter" }, + "beolink_join": { "service": "mdi:location-enter" }, + "beolink_leader_command": { "service": "mdi:location-enter" }, + "beolink_leave": { "service": "mdi:close-circle-outline" }, + "beolink_set_relative_volume": { "service": "mdi:volume-plus" }, + "beolink_set_volume": { "service": "mdi:volume-equal" }, + "beolink_unexpand": { "service": "mdi:location-exit" }, + "reboot": { "service": "mdi:restart" } } } diff --git a/custom_components/bang_olufsen/manifest.json b/custom_components/bang_olufsen/manifest.json index d3a7100..80b30be 100644 --- a/custom_components/bang_olufsen/manifest.json +++ b/custom_components/bang_olufsen/manifest.json @@ -9,6 +9,6 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/bang-olufsen/bang_olufsen-hacs/issues", "requirements": ["mozart-api==3.4.1.8.7"], - "version": "2.4.0", + "version": "2.4.1", "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/custom_components/bang_olufsen/media_player.py b/custom_components/bang_olufsen/media_player.py index e0e7e0e..b06cb21 100644 --- a/custom_components/bang_olufsen/media_player.py +++ b/custom_components/bang_olufsen/media_player.py @@ -3,10 +3,9 @@ from __future__ import annotations from collections.abc import Callable -import contextlib import json import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from mozart_api import __version__ as MOZART_API_VERSION from mozart_api.exceptions import ApiException, NotFoundException @@ -99,7 +98,7 @@ WebsocketNotification, ) from .entity import BangOlufsenEntity -from .util import set_platform_initialized +from .util import get_serial_number_from_jid, set_platform_initialized _LOGGER = logging.getLogger(__name__) @@ -453,30 +452,39 @@ async def _async_update_sources(self) -> None: self.async_write_ha_state() - def _get_beolink_jid(self, entity_id: str) -> str | None: + def _get_beolink_jid(self, entity_id: str) -> str: """Get beolink JID from entity_id.""" - jid = None entity_registry = er.async_get(self.hass) + # Check for valid bang_olufsen media_player entity entity_entry = entity_registry.async_get(entity_id) - if entity_entry: - config_entry = cast( - ConfigEntry, - self.hass.config_entries.async_get_entry( - cast(str, entity_entry.config_entry_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}, ) - with contextlib.suppress(KeyError): - jid = cast(str, config_entry.data[CONF_BEOLINK_JID]) + config_entry = self.hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + if TYPE_CHECKING: + assert config_entry - return jid + # Return JID + return cast(str, config_entry.data[CONF_BEOLINK_JID]) def _get_entity_id_from_jid(self, jid: str) -> str | None: """Get entity_id from Beolink JID (if available).""" - unique_id = jid.split(".")[2].split("@")[0] + unique_id = get_serial_number_from_jid(jid) entity_registry = er.async_get(self.hass) return entity_registry.async_get_entity_id( @@ -968,23 +976,16 @@ async def async_join_players(self, group_members: list[str]) -> None: """Create a Beolink session with defined group members.""" # Use the touch to join if no entities have been defined + # Touch to join will make the device connect to any other currently-playing + # Beolink compatible B&O device. + # Repeated presses / calls will cycle between compatible playing devices. if len(group_members) == 0: await self.async_beolink_join() return - jids = [] # Get JID for each group member - for group_member in group_members: - jid = self._get_beolink_jid(group_member) - - # Invalid entity - if jid is None: - _LOGGER.warning("Error adding %s to group", group_member) - continue - - jids.append(jid) - - await self.async_beolink_expand(beolink_jids=jids) + jids = [self._get_beolink_jid(group_member) for group_member in group_members] + await self.async_beolink_expand(jids) async def async_unjoin_player(self) -> None: """Unjoin Beolink session. End session if leader.""" @@ -1162,21 +1163,29 @@ async def async_beolink_join( async def async_beolink_expand( self, beolink_jids: list[str] | None = None, all_discovered: bool = False - ) -> ServiceResponse: + ) -> None: """Expand a Beolink multi-room experience with a device or devices.""" - response: dict[str, Any] = {"not_on_network": []} # Ensure that the current source is expandable - with contextlib.suppress(KeyError): - if not self._beolink_sources[cast(str, self._source_change.id)]: - return {"invalid_source": self.source} + if not self._beolink_sources[cast(str, self._source_change.id)]: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_source", + translation_placeholders={ + "invalid_source": cast(str, self._source_change.id), + "valid_sources": ", ".join(list(self._beolink_sources.keys())), + }, + ) # Expand to all discovered devices if all_discovered: peers = await self._client.get_beolink_peers() for peer in peers: - await self._client.post_beolink_expand(jid=peer.jid) + try: + await self._client.post_beolink_expand(jid=peer.jid) + except NotFoundException: + _LOGGER.warning("Unable to expand to %s", peer.jid) # Try to expand to all defined devices elif beolink_jids: @@ -1184,12 +1193,10 @@ async def async_beolink_expand( try: await self._client.post_beolink_expand(jid=beolink_jid) except NotFoundException: - response["not_on_network"].append(beolink_jid) - - if len(response["not_on_network"]) > 0: - return response - - return None + _LOGGER.warning( + "Unable to expand to %s. Is the device available on the network?", + beolink_jid, + ) async def async_beolink_unexpand(self, beolink_jids: list[str]) -> None: """Unexpand a Beolink multi-room experience with a device or devices.""" @@ -1222,6 +1229,8 @@ async def async_beolink_listener_command( elif parameter_type is None: await getattr(self, f"async_{command}")() + return + @callback async def async_beolink_leader_command( self, command: str, parameter: float | bool | str | None = None @@ -1275,6 +1284,8 @@ async def async_beolink_leader_command( elif parameter_type is None: await getattr(self, f"async_{command}")() + return + @callback async def async_beolink_set_volume(self, volume_level: str) -> None: """Set volume level for all connected Beolink devices.""" diff --git a/custom_components/bang_olufsen/strings.json b/custom_components/bang_olufsen/strings.json index b5faa37..c131103 100644 --- a/custom_components/bang_olufsen/strings.json +++ b/custom_components/bang_olufsen/strings.json @@ -270,6 +270,9 @@ }, "invalid_beolink_parameter": { "message": "Invalid parameter: {parameter} for {command} which expects a parameter of type: {parameter_type}." + }, + "invalid_grouping_entity": { + "message": "Entity with id: {entity_id} can't be added to the Beolink session. Is the entity a Bang & Olufsen media_player?" } } } diff --git a/custom_components/bang_olufsen/translations/en.json b/custom_components/bang_olufsen/translations/en.json index ffb2285..8cc5374 100644 --- a/custom_components/bang_olufsen/translations/en.json +++ b/custom_components/bang_olufsen/translations/en.json @@ -194,6 +194,9 @@ "invalid_beolink_parameter": { "message": "Invalid parameter: {parameter} for {command} which expects a parameter of type: {parameter_type}." }, + "invalid_grouping_entity": { + "message": "Entity with id: {entity_id} can't be added to the Beolink session. Is the entity a Bang & Olufsen media_player?" + }, "invalid_media_type": { "message": "{invalid_media_type} is an invalid type. Valid values are: {valid_media_types}." }, diff --git a/custom_components/bang_olufsen/util.py b/custom_components/bang_olufsen/util.py index 36bc719..ba93ead 100644 --- a/custom_components/bang_olufsen/util.py +++ b/custom_components/bang_olufsen/util.py @@ -8,3 +8,8 @@ def set_platform_initialized(data: BangOlufsenData) -> None: """Increment platforms_initialized to indicate that a platform has been initialized.""" data.platforms_initialized += 1 + + +def get_serial_number_from_jid(jid: str) -> str: + """Get serial number from Beolink JID.""" + return jid.split(".")[2].split("@")[0] diff --git a/hacs.json b/hacs.json index 1a7a3a9..68db82c 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Bang & Olufsen", - "homeassistant": "2024.4.0", + "homeassistant": "2024.9.0", "render_readme": true }