From aeb519f13da92ec252778fe14c123067b6dfbbab Mon Sep 17 00:00:00 2001 From: mj23000 Date: Thu, 15 Dec 2022 17:08:48 +0100 Subject: [PATCH] Support media_player grouping with Beolink Add longpress device trigger to play/pause and bluetooth buttons Update friendly name in Beolink extra_state_attributes Tweaks --- custom_components/bangolufsen/const.py | 4 +- .../bangolufsen/device_trigger.py | 2 + custom_components/bangolufsen/manifest.json | 20 ++-- custom_components/bangolufsen/media_player.py | 97 ++++++++++++++++++- custom_components/bangolufsen/strings.json | 2 + .../bangolufsen/translations/da.json | 2 + .../bangolufsen/translations/en.json | 2 + 7 files changed, 115 insertions(+), 14 deletions(-) diff --git a/custom_components/bangolufsen/const.py b/custom_components/bangolufsen/const.py index a3fa9fe..8ec4cc9 100644 --- a/custom_components/bangolufsen/const.py +++ b/custom_components/bangolufsen/const.py @@ -2,7 +2,7 @@ from __future__ import annotations from enum import Enum -from typing import Final +from typing import Final, cast from mozart_api.models import ( AlarmTriggeredInfo, @@ -336,7 +336,7 @@ def get_device(hass: HomeAssistant | None, unique_id: str) -> DeviceEntry | None return None registry = device_registry.async_get(hass) - device = registry.async_get_device({(DOMAIN, unique_id)}) + device = cast(DeviceEntry, registry.async_get_device({(DOMAIN, unique_id)})) return device diff --git a/custom_components/bangolufsen/device_trigger.py b/custom_components/bangolufsen/device_trigger.py index c6ea592..f6e827a 100644 --- a/custom_components/bangolufsen/device_trigger.py +++ b/custom_components/bangolufsen/device_trigger.py @@ -21,10 +21,12 @@ "Preset3_shortPress", "Preset4_shortPress", "PlayPause_shortPress", + "PlayPause_longPress", "Next_shortPress", "Previous_shortPress", "Microphone_shortPress", "Bluetooth_shortPress", + "Bluetooth_longPress", ) ALL_TRIGGERS = ( diff --git a/custom_components/bangolufsen/manifest.json b/custom_components/bangolufsen/manifest.json index d84ef5e..e2efe14 100644 --- a/custom_components/bangolufsen/manifest.json +++ b/custom_components/bangolufsen/manifest.json @@ -1,12 +1,12 @@ { - "domain": "bangolufsen", - "name": "Bang & Olufsen", - "documentation": "https://github.com/bang-olufsen/bangolufsen-hacs", - "issue_tracker": "https://github.com/bang-olufsen/bangolufsen-hacs/issues", - "requirements": ["mozart-api==2.3.4.15123.5"], - "zeroconf": ["_bangolufsen._tcp.local."], - "version": "0.2.1", - "codeowners": ["@mj23000"], - "iot_class": "local_push", - "config_flow": true + "domain": "bangolufsen", + "name": "Bang & Olufsen", + "documentation": "https://github.com/bang-olufsen/bangolufsen-hacs", + "issue_tracker": "https://github.com/bang-olufsen/bangolufsen-hacs/issues", + "requirements": ["mozart-api==2.3.4.15123.5"], + "zeroconf": ["_bangolufsen._tcp.local."], + "version": "0.3.0", + "codeowners": ["@mj23000"], + "iot_class": "local_push", + "config_flow": true } diff --git a/custom_components/bangolufsen/media_player.py b/custom_components/bangolufsen/media_player.py index 2eaf1d3..21686df 100644 --- a/custom_components/bangolufsen/media_player.py +++ b/custom_components/bangolufsen/media_player.py @@ -48,7 +48,7 @@ async_process_play_media_url, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODEL +from homeassistant.const import CONF_MODEL, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, @@ -60,6 +60,8 @@ async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow @@ -112,6 +114,7 @@ | MediaPlayerEntityFeature.SHUFFLE_SET | MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.REPEAT_SET + | MediaPlayerEntityFeature.GROUPING ) @@ -237,6 +240,7 @@ def __init__(self, entry: ConfigEntry, coordinator: BangOlufsenCoordinator) -> N self._attr_icon = "mdi:speaker-wireless" self._attr_supported_features = BANGOLUFSEN_FEATURES self._attr_unique_id = self._unique_id + self._attr_group_members = [] self._beolink_jid: str = self.entry.data[CONF_BEOLINK_JID] self._max_volume: int = self.entry.data[CONF_MAX_VOLUME] @@ -245,6 +249,7 @@ def __init__(self, entry: ConfigEntry, coordinator: BangOlufsenCoordinator) -> N self._model: str = self.entry.data[CONF_MODEL] # Misc. variables. + self._friendly_name: str = "" self._state: MediaPlayerState | str = MediaPlayerState.IDLE self._media_image: Art = Art() self._last_update: datetime = datetime(1970, 1, 1, 0, 0, 0, 0) @@ -365,6 +370,10 @@ async def bangolufsen_init(self) -> bool: self._software_status.software_version, ) + # Get the device friendly name + beolink_self = self._client.get_beolink_self(async_req=True).get() + self._friendly_name = beolink_self.friendly_name + # If the device has been updated with new sources, then the API will fail here. try: # Get all available sources. @@ -436,6 +445,38 @@ async def bangolufsen_init(self) -> bool: return True + def _get_beolink_jid(self, entity_id: str) -> str | None: + """Get beolink JID from entity_id.""" + entity_registry = er.async_get(self.hass) + + # Make mypy happy + entity_entry = cast(RegistryEntry, entity_registry.async_get(entity_id)) + config_entry = cast( + ConfigEntry, + self.hass.config_entries.async_get_entry( + cast(str, entity_entry.config_entry_id) + ), + ) + + try: + jid = cast(str, config_entry.data[CONF_BEOLINK_JID]) + except KeyError: + jid = None + + return 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] + + entity_registry = er.async_get(self.hass) + entity_id = entity_registry.async_get_entity_id( + Platform.MEDIA_PLAYER, DOMAIN, unique_id + ) + + return entity_id + def _update_artwork(self) -> None: """Find the highest resolution image.""" # Ensure that the metadata doesn't change mid processing. @@ -470,7 +511,9 @@ async def _update_beolink(self) -> None: self._beolink_attribute = {} # Add Beolink JID - self._beolink_attribute = {"beolink": {"self": {self._name: self._beolink_jid}}} + self._beolink_attribute = { + "beolink": {"self": {self._friendly_name: self._beolink_jid}} + } peers = self._client.get_beolink_peers(async_req=True).get() @@ -490,8 +533,15 @@ async def _update_beolink(self) -> None: ): self._remote_leader = None + # Create group members list + group_members = [] + # If the device is a listener. if self._remote_leader is not None: + group_members.append( + cast(str, self._get_entity_id_from_jid(self._remote_leader.jid)) + ) + self._beolink_attribute["beolink"]["leader"] = { self._remote_leader.friendly_name: self._remote_leader.jid, } @@ -502,11 +552,18 @@ async def _update_beolink(self) -> None: async_req=True ).get() + group_members.append( + cast(str, self._get_entity_id_from_jid(self._beolink_jid)) + ) + # Check if the device is a leader. if len(self._beolink_listeners) > 0: # Get the friendly names from listeners from the peers beolink_listeners = {} for beolink_listener in self._beolink_listeners: + group_members.append( + cast(str, self._get_entity_id_from_jid(beolink_listener.jid)) + ) for peer in peers: if peer.jid == beolink_listener.jid: beolink_listeners[peer.friendly_name] = beolink_listener.jid @@ -514,6 +571,8 @@ async def _update_beolink(self) -> None: self._beolink_attribute["beolink"]["listeners"] = beolink_listeners + self._attr_group_members = group_members + @callback def _update_coordinator_data(self) -> None: """Update data from coordinator.""" @@ -605,7 +664,13 @@ async def _update_notification(self, data: WebsocketNotificationTag) -> None: if self._notification.value in ( "beolinkAvailableListeners", "beolinkListeners", + "configuration", ): + # Update the device friendly name + if self._notification.value == "configuration": + beolink_self = self._client.get_beolink_self(async_req=True).get() + self._friendly_name = beolink_self.friendly_name + await self._update_beolink() # Update bluetooth devices @@ -904,6 +969,33 @@ async def async_select_source(self, source: str) -> None: self._source_list_friendly, ) + 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 + 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(jids) + + async def async_unjoin_player(self) -> None: + """Unexpand device from Beolink session.""" + await self.async_beolink_leave() + async def async_play_media( self, media_type: MediaType | str, @@ -1015,6 +1107,7 @@ async def async_beolink_expand(self, beolink_jids: list[str]) -> None: # Check if the Beolink JIDs are valid. for beolink_jid in beolink_jids: if not check_valid_jid(beolink_jid): + _LOGGER.error("Invalid Beolink JID: %s", beolink_jid) return self.hass.async_create_task(self._beolink_expand(beolink_jids)) diff --git a/custom_components/bangolufsen/strings.json b/custom_components/bangolufsen/strings.json index 8577226..d63b584 100644 --- a/custom_components/bangolufsen/strings.json +++ b/custom_components/bangolufsen/strings.json @@ -31,6 +31,7 @@ "device_automation": { "trigger_type": { "Bluetooth_shortPress": "Short press of Bluetooth", + "Bluetooth_longPress": "Long press of Bluetooth", "Control/Blue_KeyPress": "Press of Beoremote One Control: Blue", "Control/Blue_KeyRelease": "Release of Beoremote One Control: Blue", "Control/Digit0_KeyPress": "Press of Beoremote One Control: 0", @@ -126,6 +127,7 @@ "Microphone_shortPress": "Short press of Microphone", "Next_shortPress": "Short press of Next", "PlayPause_shortPress": "Short press of Play/Pause", + "PlayPause_longPress": "Long press of Play/Pause", "Preset1_shortPress": "Short press of Favourite 1", "Preset2_shortPress": "Short press of Favourite 2", "Preset3_shortPress": "Short press of Favourite 3", diff --git a/custom_components/bangolufsen/translations/da.json b/custom_components/bangolufsen/translations/da.json index 1592f5a..bd89ba9 100644 --- a/custom_components/bangolufsen/translations/da.json +++ b/custom_components/bangolufsen/translations/da.json @@ -36,10 +36,12 @@ "Preset3_shortPress":"Kort tryk af Favourite 3", "Preset4_shortPress":"Kort tryk af Favourite 4", "PlayPause_shortPress":"Kort tryk af Play/Pause", + "PlayPause_longPress":"Langt tryk af Play/Pause", "Next_shortPress":"Kort tryk af Next", "Previous_shortPress":"Kort tryk af Previous", "Microphone_shortPress":"Kort tryk af Microphone", "Bluetooth_shortPress":"Kort tryk af Bluetooth", + "Bluetooth_longPress":"Langt tryk af Bluetooth", "Control/Wind_KeyPress": "Tryk af Beoremote One Control Fremad", "Control/Rewind_KeyPress":"Tryk af Beoremote One Control Tilbage", "Control/Play_KeyPress": "Tryk af Beoremote One Control: Play", diff --git a/custom_components/bangolufsen/translations/en.json b/custom_components/bangolufsen/translations/en.json index 3873b28..0eabe6d 100644 --- a/custom_components/bangolufsen/translations/en.json +++ b/custom_components/bangolufsen/translations/en.json @@ -30,6 +30,7 @@ }, "device_automation": { "trigger_type": { + "Bluetooth_longPress": "Long press of Bluetooth", "Bluetooth_shortPress": "Short press of Bluetooth", "Control/Blue_KeyPress": "Press of Beoremote One Control: Blue", "Control/Blue_KeyRelease": "Release of Beoremote One Control: Blue", @@ -125,6 +126,7 @@ "Light/Yellow_KeyRelease": "Release of Beoremote One Light: Yellow", "Microphone_shortPress": "Short press of Microphone", "Next_shortPress": "Short press of Next", + "PlayPause_longPress": "Long press of Play/Pause", "PlayPause_shortPress": "Short press of Play/Pause", "Preset1_shortPress": "Short press of Favourite 1", "Preset2_shortPress": "Short press of Favourite 2",