Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bang & Olufsen add beolink grouping #113438

Merged
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
b19f5fb
Add Beolink custom services
mj23000 Feb 9, 2024
ff28056
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Feb 26, 2024
502136a
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Feb 26, 2024
bbc062e
Fix progress not being set to None as Beolink listener
mj23000 Feb 26, 2024
43b5dd1
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Mar 6, 2024
87cd8f5
Update API
mj23000 Mar 11, 2024
beba8f4
Improve beolink custom services
mj23000 Mar 13, 2024
640f698
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Mar 13, 2024
be0d05c
Fix Beolink expandable source check
mj23000 Mar 14, 2024
e6dbada
Handle entity naming as intended
mj23000 Mar 15, 2024
1c4413f
Fix "null" Beolink self friendly name
mj23000 Mar 16, 2024
a83bf58
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Mar 18, 2024
0b85264
Add regex service input validation
mj23000 Apr 4, 2024
bdea91b
Add service icons
mj23000 Apr 4, 2024
01ef098
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 May 16, 2024
bea3691
Fix merge
mj23000 May 16, 2024
95d29bf
Remove invalid typing
mj23000 May 28, 2024
4d85916
Revert to old typed response dict method
mj23000 May 28, 2024
deafd85
Re add debugging logging
mj23000 May 28, 2024
26047bd
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 May 29, 2024
08b94a7
Fix coroutine
mj23000 May 29, 2024
f940cd4
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 May 30, 2024
a97e978
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 May 31, 2024
c1a6c6d
Remove unnecessary update control
mj23000 May 31, 2024
919a747
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Jun 17, 2024
bdcac7a
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Jul 10, 2024
4569bea
Make tests pass
mj23000 Jul 10, 2024
6f45dfc
Fix naming and add callback decorators
mj23000 Jul 11, 2024
3a7ec93
Merge branch 'home-assistant:dev' into bang_olufsen_add_beolink_grouping
mj23000 Jul 11, 2024
a2ac751
Merge branch 'bang_olufsen_add_beolink_grouping' of https://github.co…
mj23000 Jul 11, 2024
4882f22
Move regex service check to variable
mj23000 Jul 11, 2024
3cfa05b
Merge branch 'home-assistant:dev' into bang_olufsen_add_beolink_grouping
mj23000 Jul 31, 2024
c2583a2
Re-add hass running check
mj23000 Jul 31, 2024
fe76b2c
Improve comments, naming and type hinting
mj23000 Jul 31, 2024
a82b0ca
Remove old temporary fix
mj23000 Jul 31, 2024
bf04790
Convert logged warning to raised exception for invalid media_player
mj23000 Jul 31, 2024
a8de449
Fix test for invalid media_player grouping
mj23000 Jul 31, 2024
92f4240
Improve method naming
mj23000 Jul 31, 2024
06e14a3
Improve _beolink_sources explanation
mj23000 Jul 31, 2024
99ccb3a
Improve _beolink_sources explanation
mj23000 Jul 31, 2024
27609c1
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Sep 17, 2024
ed8711a
Fix tests
mj23000 Sep 17, 2024
7e3e69a
Remove service responses
mj23000 Sep 17, 2024
70f16fc
Change service to action where applicable
mj23000 Sep 17, 2024
5ec7a6e
Show playback progress for listeners
mj23000 Sep 17, 2024
c4eaaec
Fix testing
mj23000 Sep 17, 2024
03cf63e
Remove useless initialization
mj23000 Sep 19, 2024
a64560a
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Sep 25, 2024
91ecb4a
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Sep 25, 2024
140273d
Fix allstandby name
mj23000 Sep 25, 2024
d517169
Fix various casts with assertions
mj23000 Sep 25, 2024
55d09c2
Add syrupy snapshots for Beolink tests, checking entity states
mj23000 Oct 11, 2024
dd05859
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Oct 11, 2024
e6a9be2
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Oct 16, 2024
e085f78
Add sections for fields using Beolink JIDs directly
mj23000 Oct 16, 2024
491cb5b
Fix typo
mj23000 Oct 16, 2024
1dd6cfb
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Oct 18, 2024
45a5567
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Nov 1, 2024
8ade5e1
Merge branch 'dev' into bang_olufsen_add_beolink_grouping
mj23000 Nov 6, 2024
199baeb
FIx rebase mistake
mj23000 Nov 8, 2024
bfb70f2
Sort actions alphabetically
mj23000 Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions homeassistant/components/bang_olufsen/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"services": {
"beolink_join": { "service": "mdi:location-enter" },
"beolink_expand": { "service": "mdi:location-enter" },
"beolink_unexpand": { "service": "mdi:location-exit" },
"beolink_leave": { "service": "mdi:close-circle-outline" },
"beolink_allstandby": { "service": "mdi:close-circle-multiple-outline" }
}
}
237 changes: 226 additions & 11 deletions homeassistant/components/bang_olufsen/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from aiohttp import ClientConnectorError
from mozart_api import __version__ as MOZART_API_VERSION
from mozart_api.exceptions import ApiException
from mozart_api.exceptions import ApiException, NotFoundException
from mozart_api.models import (
Action,
Art,
Expand All @@ -38,6 +38,7 @@
VolumeState,
)
from mozart_api.mozart_client import MozartClient, get_highest_resolution_artwork
import voluptuous as vol

from homeassistant.components import media_source
from homeassistant.components.media_player import (
Expand All @@ -55,10 +56,17 @@
from homeassistant.const import CONF_MODEL, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.util.dt import utcnow

from . import BangOlufsenConfigEntry
Expand Down Expand Up @@ -116,6 +124,110 @@ async def async_setup_entry(
]
)

# Register actions.
platform = async_get_current_platform()

jid_regex = vol.Match(
r"(^\d{4})[.](\d{7})[.](\d{8})(@products\.bang-olufsen\.com)$"
)

platform.async_register_entity_service(
name="beolink_join",
schema={vol.Optional("beolink_jid"): jid_regex},
func="async_beolink_join",
)

platform.async_register_entity_service(
name="beolink_expand",
schema={
vol.Exclusive("all_discovered", "devices", ""): cv.boolean,
vol.Exclusive(
"beolink_jids",
"devices",
"Define either specific Beolink JIDs or all discovered",
): vol.All(
cv.ensure_list,
[jid_regex],
),
},
func="async_beolink_expand",
)

platform.async_register_entity_service(
name="beolink_unexpand",
schema={
vol.Required("beolink_jids"): vol.All(
cv.ensure_list,
[jid_regex],
),
},
func="async_beolink_unexpand",
)

platform.async_register_entity_service(
name="beolink_leave",
schema=None,
func="async_beolink_leave",
)

platform.async_register_entity_service(
name="beolink_allstandby",
schema=None,
func="async_beolink_allstandby",
)

# Register actions.
platform = async_get_current_platform()

jid_regex = vol.Match(
r"(^\d{4})[.](\d{7})[.](\d{8})(@products\.bang-olufsen\.com)$"
)

platform.async_register_entity_service(
name="beolink_join",
schema={vol.Optional("beolink_jid"): jid_regex},
emontnemery marked this conversation as resolved.
Show resolved Hide resolved
mj23000 marked this conversation as resolved.
Show resolved Hide resolved
func="async_beolink_join",
)

platform.async_register_entity_service(
name="beolink_expand",
schema={
vol.Exclusive("all_discovered", "devices", ""): cv.boolean,
vol.Exclusive(
"beolink_jids",
emontnemery marked this conversation as resolved.
Show resolved Hide resolved
"devices",
"Define either specific Beolink JIDs or all discovered",
): vol.All(
cv.ensure_list,
[jid_regex],
),
},
func="async_beolink_expand",
)

platform.async_register_entity_service(
name="beolink_unexpand",
schema={
vol.Required("beolink_jids"): vol.All(
cv.ensure_list,
[jid_regex],
),
},
func="async_beolink_unexpand",
)

platform.async_register_entity_service(
name="beolink_leave",
schema=None,
func="async_beolink_leave",
)

platform.async_register_entity_service(
name="beolink_allstandby",
schema=None,
func="async_beolink_allstandby",
)


class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Representation of a media player."""
Expand Down Expand Up @@ -156,6 +268,8 @@ def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
# Beolink compatible sources
self._beolink_sources: dict[str, bool] = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment explaining what the string and bool means would be nice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is explained where the variable is set, is that not enough?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That comment says:

        # Some sources are not Beolink expandable. _source_change, which is used throughout the entity does not have this information.
        # Save expandable sources for Beolink services

It's still not clear to me what the key and values represent after reading that.
Maybe "Beolink expandable" has some clear meaning to you, it doesn't for me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've improved the explanation now.

self._remote_leader: BeolinkLeader | None = None
# Extra state attributes for showing Beolink: peer(s), listener(s), leader and self
self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {}

async def async_added_to_hass(self) -> None:
"""Turn on the dispatchers."""
Expand All @@ -165,6 +279,7 @@ async def async_added_to_hass(self) -> None:
CONNECTION_STATUS: self._async_update_connection_state,
WebsocketNotification.ACTIVE_LISTENING_MODE: self._async_update_sound_modes,
WebsocketNotification.BEOLINK: self._async_update_beolink,
WebsocketNotification.CONFIGURATION: self._async_update_name_and_beolink,
WebsocketNotification.PLAYBACK_ERROR: self._async_update_playback_error,
WebsocketNotification.PLAYBACK_METADATA: self._async_update_playback_metadata_and_beolink,
WebsocketNotification.PLAYBACK_PROGRESS: self._async_update_playback_progress,
Expand Down Expand Up @@ -230,6 +345,9 @@ async def _initialize(self) -> None:

await self._async_update_sound_modes()

# Update beolink attributes and device name.
await self._async_update_name_and_beolink()

async def async_update(self) -> None:
"""Update queue settings."""
# The WebSocket event listener is the main handler for connection state.
Expand Down Expand Up @@ -372,9 +490,44 @@ def _async_update_volume(self, data: VolumeState) -> None:

self.async_write_ha_state()

async def _async_update_name_and_beolink(self) -> None:
"""Update the device friendly name."""
emontnemery marked this conversation as resolved.
Show resolved Hide resolved
beolink_self = await self._client.get_beolink_self()

# Update device name
device_registry = dr.async_get(self.hass)
assert self.device_entry is not None

device_registry.async_update_device(
device_id=self.device_entry.id,
name=beolink_self.friendly_name,
)

await self._async_update_beolink()

async def _async_update_beolink(self) -> None:
"""Update the current Beolink leader, listeners, peers and self."""

self._beolink_attributes = {}

assert self.device_entry is not None
assert self.device_entry.name is not None

# Add Beolink self
self._beolink_attributes = {
"beolink": {"self": {self.device_entry.name: self._beolink_jid}}
}

# Add Beolink peers
peers = await self._client.get_beolink_peers()

if len(peers) > 0:
self._beolink_attributes["beolink"]["peers"] = {}
for peer in peers:
self._beolink_attributes["beolink"]["peers"][peer.friendly_name] = (
peer.jid
)

# Add Beolink listeners / leader
self._remote_leader = self._playback_metadata.remote_leader

Expand All @@ -394,9 +547,14 @@ async def _async_update_beolink(self) -> None:
# Add self
group_members.append(self.entity_id)

self._beolink_attributes["beolink"]["leader"] = {
self._remote_leader.friendly_name: self._remote_leader.jid,
}

# If not listener, check if leader.
else:
beolink_listeners = await self._client.get_beolink_listeners()
beolink_listeners_attribute = {}

# Check if the device is a leader.
if len(beolink_listeners) > 0:
Expand All @@ -417,6 +575,18 @@ async def _async_update_beolink(self) -> None:
for beolink_listener in beolink_listeners
]
)
# Update Beolink attributes
for beolink_listener in beolink_listeners:
for peer in peers:
if peer.jid == beolink_listener.jid:
# Get the friendly names for the listeners from the peers
beolink_listeners_attribute[peer.friendly_name] = (
beolink_listener.jid
)
break
self._beolink_attributes["beolink"]["listeners"] = (
beolink_listeners_attribute
)

self._attr_group_members = group_members

Expand Down Expand Up @@ -602,6 +772,17 @@ def source(self) -> str | None:

return self._source_change.name

@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return information that is not returned anywhere else."""
attributes: dict[str, Any] = {}

# Add Beolink attributes
if self._beolink_attributes:
attributes.update(self._beolink_attributes)

return attributes

async def async_turn_off(self) -> None:
"""Set the device to "networkStandby"."""
await self._client.post_standby()
Expand Down Expand Up @@ -873,23 +1054,30 @@ async def async_join_players(self, group_members: list[str]) -> None:
# Beolink compatible B&O device.
# Repeated presses / calls will cycle between compatible playing devices.
if len(group_members) == 0:
await self._async_beolink_join()
await self.async_beolink_join()
return

# Get JID for each group member
jids = [self._get_beolink_jid(group_member) for group_member in group_members]
await self._async_beolink_expand(jids)
await self.async_beolink_expand(jids)

async def async_unjoin_player(self) -> None:
"""Unjoin Beolink session. End session if leader."""
await self._async_beolink_leave()
await self.async_beolink_leave()

async def _async_beolink_join(self) -> None:
# Custom actions:
emontnemery marked this conversation as resolved.
Show resolved Hide resolved
async def async_beolink_join(self, beolink_jid: str | None = None) -> None:
"""Join a Beolink multi-room experience."""
await self._client.join_latest_beolink_experience()
if beolink_jid is None:
await self._client.join_latest_beolink_experience()
else:
await self._client.join_beolink_peer(jid=beolink_jid)

async def _async_beolink_expand(self, beolink_jids: list[str]) -> None:
async def async_beolink_expand(
self, beolink_jids: list[str] | None = None, all_discovered: bool = False
) -> None:
"""Expand a Beolink multi-room experience with a device or devices."""

# Ensure that the current source is expandable
if not self._beolink_sources[cast(str, self._source_change.id)]:
raise ServiceValidationError(
Expand All @@ -901,10 +1089,37 @@ async def _async_beolink_expand(self, beolink_jids: list[str]) -> None:
},
)

# Expand to all discovered devices
if all_discovered:
peers = await self._client.get_beolink_peers()

for peer in peers:
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:
for beolink_jid in beolink_jids:
try:
await self._client.post_beolink_expand(jid=beolink_jid)
except NotFoundException:
_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."""
# Unexpand all defined devices
for beolink_jid in beolink_jids:
await self._client.post_beolink_expand(jid=beolink_jid)
await self._client.post_beolink_unexpand(jid=beolink_jid)

async def _async_beolink_leave(self) -> None:
async def async_beolink_leave(self) -> None:
"""Leave the current Beolink experience."""
await self._client.post_beolink_leave()

async def async_beolink_allstandby(self) -> None:
"""Set all connected Beolink devices to standby."""
await self._client.post_beolink_allstandby()
Loading