Bang & Olufsen add beolink grouping (#113438)

* Add Beolink custom services
Add support for media player grouping via beolink
Give media player entity name

* Fix progress not being set to None as Beolink listener
Revert naming changes

* Update API
simplify Beolink attributes

* Improve beolink custom services

* Fix Beolink expandable source check
Add unexpand return value
Set entity name on initialization

* Handle entity naming as intended

* Fix "null" Beolink self friendly name

* Add regex service input validation
Add all_discovered to beolink_expand service
Improve beolink_expand response

* Add service icons

* Fix merge
Remove unnecessary assignment

* Remove invalid typing
Update response typing for updated API

* Revert to old typed response dict method
Remove mypy ignore line
Fix jid possibly used before assignment

* Re add debugging logging

* Fix coroutine
Fix formatting

* Remove unnecessary update control

* Make tests pass
Fix remote leader media position bug
Improve remote leader BangOlufsenSource comparison

* Fix naming and add callback decorators

* Move regex service check to variable
Suppress KeyError
Update tests

* Re-add hass running check

* Improve comments, naming and type hinting

* Remove old temporary fix

* Convert logged warning to raised exception for invalid media_player
Simplify code using walrus operator

* Fix test for invalid media_player grouping

* Improve method naming

* Improve _beolink_sources explanation

* Improve _beolink_sources explanation

* Fix tests

* Remove service responses
Fix and add tests

* Change service to action where applicable

* Show playback progress for listeners

* Fix testing

* Remove useless initialization

* Fix allstandby name

* Fix various casts with assertions
Fix comment placement
Fix group leader group_members rebase error
Replace entity_id method call with attribute

* Add syrupy snapshots for Beolink tests, checking entity states
Use test JIDs 3 and 4 instead of 2 and 3 to avoid invalid attributes in testing

* Add sections for fields using Beolink JIDs directly

* Fix typo

* FIx rebase mistake

* Sort actions alphabetically
This commit is contained in:
Markus Jacobsen 2024-11-08 12:06:29 +01:00 committed by GitHub
parent ed1366f463
commit e3dfa84d65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1487 additions and 37 deletions

View File

@ -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" }
}
}

View File

@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, cast
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,
@ -38,6 +38,7 @@ from mozart_api.models import (
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 (
@ -55,10 +56,17 @@ from homeassistant.config_entries import ConfigEntry
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
@ -116,6 +124,58 @@ 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",
)
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Representation of a media player."""
@ -156,6 +216,8 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Beolink compatible sources
self._beolink_sources: dict[str, bool] = {}
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."""
@ -165,6 +227,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
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,
@ -230,6 +293,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
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.
@ -372,9 +438,44 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self.async_write_ha_state()
async def _async_update_name_and_beolink(self) -> None:
"""Update the device friendly name."""
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
@ -394,9 +495,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# 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:
@ -417,6 +523,18 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
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
@ -602,6 +720,17 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
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()
@ -873,23 +1002,30 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# 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:
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(
@ -901,10 +1037,37 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
},
)
# Try to expand to all defined devices
for beolink_jid in beolink_jids:
await self._client.post_beolink_expand(jid=beolink_jid)
# Expand to all discovered devices
if all_discovered:
peers = await self._client.get_beolink_peers()
async def _async_beolink_leave(self) -> None:
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_unexpand(jid=beolink_jid)
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()

View File

@ -0,0 +1,79 @@
beolink_allstandby:
target:
entity:
integration: bang_olufsen
domain: media_player
device:
integration: bang_olufsen
beolink_expand:
target:
entity:
integration: bang_olufsen
domain: media_player
device:
integration: bang_olufsen
fields:
all_discovered:
required: false
example: false
selector:
boolean:
jid_options:
collapsed: false
fields:
beolink_jids:
required: false
example: >-
[
1111.2222222.33333333@products.bang-olufsen.com,
4444.5555555.66666666@products.bang-olufsen.com
]
selector:
object:
beolink_join:
target:
entity:
integration: bang_olufsen
domain: media_player
device:
integration: bang_olufsen
fields:
jid_options:
collapsed: false
fields:
beolink_jid:
required: false
example: 1111.2222222.33333333@products.bang-olufsen.com
selector:
text:
beolink_leave:
target:
entity:
integration: bang_olufsen
domain: media_player
device:
integration: bang_olufsen
beolink_unexpand:
target:
entity:
integration: bang_olufsen
domain: media_player
device:
integration: bang_olufsen
fields:
jid_options:
collapsed: false
fields:
beolink_jids:
required: true
example: >-
[
1111.2222222.33333333@products.bang-olufsen.com,
4444.5555555.66666666@products.bang-olufsen.com
]
selector:
object:

View File

@ -1,4 +1,8 @@
{
"common": {
"jid_options_name": "JID options",
"jid_options_description": "Advanced grouping options, where devices' unique Beolink IDs (Called JIDs) are used directly. JIDs can be found in the state attributes of the media player entity."
},
"config": {
"error": {
"api_exception": "[%key:common::config_flow::error::cannot_connect%]",
@ -25,6 +29,68 @@
}
}
},
"services": {
"beolink_allstandby": {
"name": "Beolink all standby",
"description": "Set all Connected Beolink devices to standby."
},
"beolink_expand": {
"name": "Beolink expand",
"description": "Expand current Beolink experience.",
"fields": {
"all_discovered": {
"name": "All discovered",
"description": "Expand Beolink experience to all discovered devices."
},
"beolink_jids": {
"name": "Beolink JIDs",
"description": "Specify which Beolink JIDs will join current Beolink experience."
}
},
"sections": {
"jid_options": {
"name": "[%key:component::bang_olufsen::common::jid_options_name%]",
"description": "[%key:component::bang_olufsen::common::jid_options_description%]"
}
}
},
"beolink_join": {
"name": "Beolink join",
"description": "Join a Beolink experience.",
"fields": {
"beolink_jid": {
"name": "Beolink JID",
"description": "Manually specify Beolink JID to join."
}
},
"sections": {
"jid_options": {
"name": "[%key:component::bang_olufsen::common::jid_options_name%]",
"description": "[%key:component::bang_olufsen::common::jid_options_description%]"
}
}
},
"beolink_leave": {
"name": "Beolink leave",
"description": "Leave a Beolink experience."
},
"beolink_unexpand": {
"name": "Beolink unexpand",
"description": "Unexpand from current Beolink experience.",
"fields": {
"beolink_jids": {
"name": "Beolink JIDs",
"description": "Specify which Beolink JIDs will leave from current Beolink experience."
}
},
"sections": {
"jid_options": {
"name": "[%key:component::bang_olufsen::common::jid_options_name%]",
"description": "[%key:component::bang_olufsen::common::jid_options_description%]"
}
}
}
},
"exceptions": {
"m3u_invalid_format": {
"message": "Media sources with the .m3u extension are not supported."

View File

@ -120,6 +120,11 @@ class BangOlufsenWebsocket(BangOlufsenBase):
self.hass,
f"{self._unique_id}_{WebsocketNotification.BEOLINK}",
)
elif notification_type is WebsocketNotification.CONFIGURATION:
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.CONFIGURATION}",
)
elif notification_type is WebsocketNotification.REMOTE_MENU_CHANGED:
async_dispatcher_send(
self.hass,

View File

@ -35,13 +35,13 @@ from .const import (
TEST_DATA_CREATE_ENTRY,
TEST_DATA_CREATE_ENTRY_2,
TEST_FRIENDLY_NAME,
TEST_FRIENDLY_NAME_2,
TEST_FRIENDLY_NAME_3,
TEST_HOST_2,
TEST_FRIENDLY_NAME_4,
TEST_HOST_3,
TEST_HOST_4,
TEST_JID_1,
TEST_JID_2,
TEST_JID_3,
TEST_JID_4,
TEST_NAME,
TEST_NAME_2,
TEST_SERIAL_NUMBER,
@ -267,29 +267,29 @@ def mock_mozart_client() -> Generator[AsyncMock]:
}
client.get_beolink_peers = AsyncMock()
client.get_beolink_peers.return_value = [
BeolinkPeer(
friendly_name=TEST_FRIENDLY_NAME_2,
jid=TEST_JID_2,
ip_address=TEST_HOST_2,
),
BeolinkPeer(
friendly_name=TEST_FRIENDLY_NAME_3,
jid=TEST_JID_3,
ip_address=TEST_HOST_3,
),
BeolinkPeer(
friendly_name=TEST_FRIENDLY_NAME_4,
jid=TEST_JID_4,
ip_address=TEST_HOST_4,
),
]
client.get_beolink_listeners = AsyncMock()
client.get_beolink_listeners.return_value = [
BeolinkPeer(
friendly_name=TEST_FRIENDLY_NAME_2,
jid=TEST_JID_2,
ip_address=TEST_HOST_2,
),
BeolinkPeer(
friendly_name=TEST_FRIENDLY_NAME_3,
jid=TEST_JID_3,
ip_address=TEST_HOST_3,
),
BeolinkPeer(
friendly_name=TEST_FRIENDLY_NAME_4,
jid=TEST_JID_4,
ip_address=TEST_HOST_4,
),
]
client.get_listening_mode_set = AsyncMock()

View File

@ -0,0 +1,874 @@
# serializer version: 1
# name: test_async_beolink_allstandby
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_beolink_expand[all_discovered-True-None-log_messages0-2]
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source': 'Tidal',
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095935>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_beolink_expand[all_discovered-True-expand_side_effect1-log_messages1-2]
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source': 'Tidal',
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095935>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_beolink_expand[beolink_jids-parameter_value2-None-log_messages2-1]
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source': 'Tidal',
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095935>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_beolink_expand[beolink_jids-parameter_value3-expand_side_effect3-log_messages3-1]
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source': 'Tidal',
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095935>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_beolink_join
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_beolink_unexpand
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_join_players[group_members0-1-0]
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source': 'Tidal',
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095935>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_join_players[group_members0-1-0].1
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_22222222',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_22222222',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_join_players[group_members1-0-1]
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source': 'Tidal',
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095935>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_join_players[group_members1-0-1].1
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_22222222',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_22222222',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_join_players_invalid[source0-group_members0-expected_result0-invalid_source]
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'media_position': 0,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source': 'Chromecast built-in',
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_join_players_invalid[source0-group_members0-expected_result0-invalid_source].1
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_22222222',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_22222222',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_join_players_invalid[source1-group_members1-expected_result1-invalid_grouping_entity]
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source': 'Tidal',
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095935>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_join_players_invalid[source1-group_members1-expected_result1-invalid_grouping_entity].1
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_22222222',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_22222222',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_unjoin_player
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_11111111',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_update_beolink_listener
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'leader': dict({
'Laundry room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_22222222',
'media_player.beosound_balance_11111111',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_11111111',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_async_update_beolink_listener.1
StateSnapshot({
'attributes': ReadOnlyDict({
'beolink': dict({
'listeners': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
}),
}),
'device_class': 'speaker',
'entity_picture_local': None,
'friendly_name': 'Living room Balance',
'group_members': list([
'media_player.beosound_balance_22222222',
'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com',
'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com',
]),
'icon': 'mdi:speaker-wireless',
'media_content_type': <MediaType.MUSIC: 'music'>,
'sound_mode': 'Test Listening Mode (123)',
'sound_mode_list': list([
'Test Listening Mode (123)',
'Test Listening Mode (234)',
'Test Listening Mode 2 (345)',
]),
'source_list': list([
'Tidal',
'Line-In',
'HDMI A',
]),
'supported_features': <MediaPlayerEntityFeature: 2095933>,
}),
'context': <ANY>,
'entity_id': 'media_player.beosound_balance_22222222',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---

View File

@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry
from .const import TEST_MODEL_BALANCE, TEST_NAME, TEST_SERIAL_NUMBER
from .const import TEST_FRIENDLY_NAME, TEST_MODEL_BALANCE, TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry
@ -35,7 +35,8 @@ async def test_setup_entry(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
assert device is not None
assert device.name == TEST_NAME
# Is usually TEST_NAME, but is updated to the device's friendly name by _update_name_and_beolink
assert device.name == TEST_FRIENDLY_NAME
assert device.model == TEST_MODEL_BALANCE
# Ensure that the connection has been checked WebSocket connection has been initialized

View File

@ -4,8 +4,10 @@ from contextlib import AbstractContextManager, nullcontext as does_not_raise
import logging
from unittest.mock import AsyncMock, patch
from mozart_api.exceptions import NotFoundException
from mozart_api.models import (
BeolinkLeader,
BeolinkSelf,
PlaybackContentMetadata,
PlayQueueSettings,
RenderingState,
@ -14,6 +16,8 @@ from mozart_api.models import (
WebsocketNotificationTag,
)
import pytest
from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props
from homeassistant.components.bang_olufsen.const import (
BANG_OLUFSEN_REPEAT_FROM_HA,
@ -46,24 +50,29 @@ from homeassistant.components.media_player import (
ATTR_SOUND_MODE_LIST,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_CLEAR_PLAYLIST,
SERVICE_JOIN,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PLAY_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_SEEK,
SERVICE_MEDIA_STOP,
SERVICE_PLAY_MEDIA,
SERVICE_REPEAT_SET,
SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE,
SERVICE_SHUFFLE_SET,
SERVICE_TURN_OFF,
SERVICE_UNJOIN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
MediaPlayerState,
MediaType,
RepeatMode,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_REPEAT_SET, SERVICE_SHUFFLE_SET
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.setup import async_setup_component
from .const import (
@ -76,7 +85,10 @@ from .const import (
TEST_DEEZER_TRACK,
TEST_FALLBACK_SOURCES,
TEST_FRIENDLY_NAME_2,
TEST_JID_1,
TEST_JID_2,
TEST_JID_3,
TEST_JID_4,
TEST_LISTENING_MODE_REF,
TEST_MEDIA_PLAYER_ENTITY_ID,
TEST_MEDIA_PLAYER_ENTITY_ID_2,
@ -136,6 +148,9 @@ async def test_initialization(
mock_mozart_client.get_remote_menu.assert_called_once()
mock_mozart_client.get_listening_mode_set.assert_called_once()
mock_mozart_client.get_active_listening_mode.assert_called_once()
mock_mozart_client.get_beolink_self.assert_called_once()
mock_mozart_client.get_beolink_peers.assert_called_once()
mock_mozart_client.get_beolink_listeners.assert_called_once()
async def test_async_update_sources_audio_only(
@ -530,11 +545,14 @@ async def test_async_update_beolink_line_in(
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states.attributes["group_members"] == []
assert mock_mozart_client.get_beolink_listeners.call_count == 1
# Called once during _initialize and once during _async_update_beolink
assert mock_mozart_client.get_beolink_listeners.call_count == 2
assert mock_mozart_client.get_beolink_peers.call_count == 2
async def test_async_update_beolink_listener(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_config_entry_2: MockConfigEntry,
@ -567,7 +585,56 @@ async def test_async_update_beolink_listener(
TEST_MEDIA_PLAYER_ENTITY_ID,
]
assert mock_mozart_client.get_beolink_listeners.call_count == 0
# Called once for each entity during _initialize
assert mock_mozart_client.get_beolink_listeners.call_count == 2
# Called once for each entity during _initialize and
# once more during _async_update_beolink for the entity that has the callback associated with it.
assert mock_mozart_client.get_beolink_peers.call_count == 3
# Main entity
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states == snapshot(exclude=props("media_position_updated_at"))
# Secondary entity
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID_2))
assert states == snapshot(exclude=props("media_position_updated_at"))
async def test_async_update_name_and_beolink(
hass: HomeAssistant,
device_registry: DeviceRegistry,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test _async_update_name_and_beolink."""
# Change response to ensure device name is changed
mock_mozart_client.get_beolink_self.return_value = BeolinkSelf(
friendly_name=TEST_FRIENDLY_NAME_2, jid=TEST_JID_1
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
configuration_callback = (
mock_mozart_client.get_notification_notifications.call_args[0][0]
)
# Trigger callback
configuration_callback(WebsocketNotificationTag(value="configuration"))
await hass.async_block_till_done()
assert mock_mozart_client.get_beolink_self.call_count == 2
assert mock_mozart_client.get_beolink_peers.call_count == 2
assert mock_mozart_client.get_beolink_listeners.call_count == 2
# Check that device name has been changed
assert mock_config_entry.unique_id
assert (
device := device_registry.async_get_device(
identifiers={(DOMAIN, mock_config_entry.unique_id)}
)
)
assert device.name == TEST_FRIENDLY_NAME_2
async def test_async_mute_volume(
@ -1343,6 +1410,7 @@ async def test_async_browse_media(
)
async def test_async_join_players(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_config_entry_2: MockConfigEntry,
@ -1367,8 +1435,8 @@ async def test_async_join_players(
source_change_callback(BangOlufsenSource.TIDAL)
await hass.services.async_call(
"media_player",
"join",
MEDIA_PLAYER_DOMAIN,
SERVICE_JOIN,
{
ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
ATTR_GROUP_MEMBERS: group_members,
@ -1379,6 +1447,14 @@ async def test_async_join_players(
assert mock_mozart_client.post_beolink_expand.call_count == expand_count
assert mock_mozart_client.join_latest_beolink_experience.call_count == join_count
# Main entity
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states == snapshot(exclude=props("media_position_updated_at"))
# Secondary entity
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID_2))
assert states == snapshot(exclude=props("media_position_updated_at"))
@pytest.mark.parametrize(
("source", "group_members", "expected_result", "error_type"),
@ -1401,6 +1477,7 @@ async def test_async_join_players(
)
async def test_async_join_players_invalid(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_config_entry_2: MockConfigEntry,
@ -1425,8 +1502,8 @@ async def test_async_join_players_invalid(
with expected_result as exc_info:
await hass.services.async_call(
"media_player",
"join",
MEDIA_PLAYER_DOMAIN,
SERVICE_JOIN,
{
ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
ATTR_GROUP_MEMBERS: group_members,
@ -1441,9 +1518,18 @@ async def test_async_join_players_invalid(
assert mock_mozart_client.post_beolink_expand.call_count == 0
assert mock_mozart_client.join_latest_beolink_experience.call_count == 0
# Main entity
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states == snapshot(exclude=props("media_position_updated_at"))
# Secondary entity
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID_2))
assert states == snapshot(exclude=props("media_position_updated_at"))
async def test_async_unjoin_player(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
@ -1453,14 +1539,181 @@ async def test_async_unjoin_player(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.services.async_call(
"media_player",
"unjoin",
MEDIA_PLAYER_DOMAIN,
SERVICE_UNJOIN,
{ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID},
blocking=True,
)
mock_mozart_client.post_beolink_leave.assert_called_once()
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states == snapshot(exclude=props("media_position_updated_at"))
async def test_async_beolink_join(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test async_beolink_join with defined JID."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.services.async_call(
DOMAIN,
"beolink_join",
{
ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
"beolink_jid": TEST_JID_2,
},
blocking=True,
)
mock_mozart_client.join_beolink_peer.assert_called_once_with(jid=TEST_JID_2)
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states == snapshot(exclude=props("media_position_updated_at"))
@pytest.mark.parametrize(
(
"parameter",
"parameter_value",
"expand_side_effect",
"log_messages",
"peers_call_count",
),
[
# All discovered
# Valid peers
("all_discovered", True, None, [], 2),
# Invalid peers
(
"all_discovered",
True,
NotFoundException(),
[f"Unable to expand to {TEST_JID_3}", f"Unable to expand to {TEST_JID_4}"],
2,
),
# Beolink JIDs
# Valid peer
("beolink_jids", [TEST_JID_3, TEST_JID_4], None, [], 1),
# Invalid peer
(
"beolink_jids",
[TEST_JID_3, TEST_JID_4],
NotFoundException(),
[
f"Unable to expand to {TEST_JID_3}. Is the device available on the network?",
f"Unable to expand to {TEST_JID_4}. Is the device available on the network?",
],
1,
),
],
)
async def test_async_beolink_expand(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
parameter: str,
parameter_value: bool | list[str],
expand_side_effect: NotFoundException | None,
log_messages: list[str],
peers_call_count: int,
) -> None:
"""Test async_beolink_expand."""
mock_mozart_client.post_beolink_expand.side_effect = expand_side_effect
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
source_change_callback = (
mock_mozart_client.get_source_change_notifications.call_args[0][0]
)
# Set the source to a beolink expandable source
source_change_callback(BangOlufsenSource.TIDAL)
await hass.services.async_call(
DOMAIN,
"beolink_expand",
{
ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
parameter: parameter_value,
},
blocking=True,
)
# Check log messages
for log_message in log_messages:
assert log_message in caplog.text
# Called once during _initialize and once during async_beolink_expand for all_discovered
assert mock_mozart_client.get_beolink_peers.call_count == peers_call_count
assert mock_mozart_client.post_beolink_expand.call_count == len(
await mock_mozart_client.get_beolink_peers()
)
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states == snapshot(exclude=props("media_position_updated_at"))
async def test_async_beolink_unexpand(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test test_async_beolink_unexpand."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.services.async_call(
DOMAIN,
"beolink_unexpand",
{
ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID,
"beolink_jids": [TEST_JID_3, TEST_JID_4],
},
blocking=True,
)
assert mock_mozart_client.post_beolink_unexpand.call_count == 2
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states == snapshot(exclude=props("media_position_updated_at"))
async def test_async_beolink_allstandby(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_mozart_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test async_beolink_allstandby."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.services.async_call(
DOMAIN,
"beolink_allstandby",
{ATTR_ENTITY_ID: TEST_MEDIA_PLAYER_ENTITY_ID},
blocking=True,
)
mock_mozart_client.post_beolink_allstandby.assert_called_once()
assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID))
assert states == snapshot(exclude=props("media_position_updated_at"))
@pytest.mark.parametrize(
("repeat"),