Skip to content

Commit

Permalink
Support media_player grouping with Beolink
Browse files Browse the repository at this point in the history
Add longpress device trigger to play/pause and bluetooth buttons
Update friendly name in Beolink extra_state_attributes
Tweaks
  • Loading branch information
mj23000 committed Dec 15, 2022
1 parent 89421be commit aeb519f
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 14 deletions.
4 changes: 2 additions & 2 deletions custom_components/bangolufsen/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand Down
2 changes: 2 additions & 0 deletions custom_components/bangolufsen/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
"Preset3_shortPress",
"Preset4_shortPress",
"PlayPause_shortPress",
"PlayPause_longPress",
"Next_shortPress",
"Previous_shortPress",
"Microphone_shortPress",
"Bluetooth_shortPress",
"Bluetooth_longPress",
)

ALL_TRIGGERS = (
Expand Down
20 changes: 10 additions & 10 deletions custom_components/bangolufsen/manifest.json
Original file line number Diff line number Diff line change
@@ -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
}
97 changes: 95 additions & 2 deletions custom_components/bangolufsen/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -112,6 +114,7 @@
| MediaPlayerEntityFeature.SHUFFLE_SET
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.REPEAT_SET
| MediaPlayerEntityFeature.GROUPING
)


Expand Down Expand Up @@ -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]
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand All @@ -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,
}
Expand All @@ -502,18 +552,27 @@ 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
break

self._beolink_attribute["beolink"]["listeners"] = beolink_listeners

self._attr_group_members = group_members

@callback
def _update_coordinator_data(self) -> None:
"""Update data from coordinator."""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions custom_components/bangolufsen/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions custom_components/bangolufsen/translations/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions custom_components/bangolufsen/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit aeb519f

Please sign in to comment.