From d8239701dbe984b90921a18e2d932217649400b4 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Fri, 14 Nov 2025 11:59:10 +0100 Subject: [PATCH 1/3] Add media_id extra state attribute --- .../components/bang_olufsen/media_player.py | 17 +++++++++++++++-- tests/components/bang_olufsen/const.py | 1 + .../bang_olufsen/test_media_player.py | 3 +++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 583b419eadf08..c7bb6a47b5f58 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -223,8 +223,11 @@ def __init__(self, entry: ConfigEntry, client: MozartClient) -> None: # 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 + # Extra state attributes: + # Beolink: peer(s), listener(s), leader and self self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {} + # Media ID: Currently playing Deezer, Tidal and radio station IDs + self._media_id_attribute: str | None = None async def async_added_to_hass(self) -> None: """Turn on the dispatchers.""" @@ -370,6 +373,9 @@ async def _async_update_playback_metadata_and_beolink( """Update _playback_metadata and related.""" self._playback_metadata = data + # Update media id attribute + self._media_id_attribute = data.source_internal_id + # Update current artwork and remote_leader. self._media_image = get_highest_resolution_artwork(self._playback_metadata) await self._async_update_beolink() @@ -435,7 +441,10 @@ async def _async_update_name_and_beolink(self) -> None: await self._async_update_beolink() async def _async_update_beolink(self) -> None: - """Update the current Beolink leader, listeners, peers and self.""" + """Update the current Beolink leader, listeners, peers and self. + + Updates Home Assistant state. + """ self._beolink_attributes = {} @@ -676,6 +685,10 @@ def extra_state_attributes(self) -> dict[str, Any] | None: """Return information that is not returned anywhere else.""" attributes: dict[str, Any] = {} + # Add media id attribute + if self._media_id_attribute: + attributes.update({"media_id": self._media_id_attribute}) + # Add Beolink attributes if self._beolink_attributes: attributes.update(self._beolink_attributes) diff --git a/tests/components/bang_olufsen/const.py b/tests/components/bang_olufsen/const.py index c21afb4a130ee..05c8982c36fc5 100644 --- a/tests/components/bang_olufsen/const.py +++ b/tests/components/bang_olufsen/const.py @@ -153,6 +153,7 @@ title="Test title", total_duration_seconds=123, track=1, + source_internal_id="123", ) TEST_PLAYBACK_ERROR = PlaybackError(error="Test error") TEST_PLAYBACK_PROGRESS = PlaybackProgress(progress=123) diff --git a/tests/components/bang_olufsen/test_media_player.py b/tests/components/bang_olufsen/test_media_player.py index 9c2bf99f87a9d..b49898f803e0b 100644 --- a/tests/components/bang_olufsen/test_media_player.py +++ b/tests/components/bang_olufsen/test_media_player.py @@ -260,6 +260,7 @@ async def test_async_update_playback_metadata( assert ATTR_MEDIA_ALBUM_ARTIST not in states.attributes assert ATTR_MEDIA_TRACK not in states.attributes assert ATTR_MEDIA_CHANNEL not in states.attributes + assert "media_id" not in states.attributes # Send the WebSocket event dispatch playback_metadata_callback(TEST_PLAYBACK_METADATA) @@ -276,6 +277,8 @@ async def test_async_update_playback_metadata( ) assert states.attributes[ATTR_MEDIA_TRACK] == TEST_PLAYBACK_METADATA.track assert states.attributes[ATTR_MEDIA_CHANNEL] == TEST_PLAYBACK_METADATA.organization + assert states.attributes[ATTR_MEDIA_CHANNEL] == TEST_PLAYBACK_METADATA.organization + assert states.attributes["media_id"] == TEST_PLAYBACK_METADATA.source_internal_id async def test_async_update_playback_error( From 0dd394af08066aa99b5644c25ee928c3afed8c36 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Fri, 14 Nov 2025 14:25:58 +0100 Subject: [PATCH 2/3] Add and use attribute enum helper class --- .../components/bang_olufsen/const.py | 11 ++ .../components/bang_olufsen/media_player.py | 29 ++- .../snapshots/test_media_player.ambr | 184 +++++++++--------- .../bang_olufsen/test_media_player.py | 8 +- 4 files changed, 128 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py index c5ee5d1a26e34..e4f7de76c2515 100644 --- a/homeassistant/components/bang_olufsen/const.py +++ b/homeassistant/components/bang_olufsen/const.py @@ -74,6 +74,17 @@ class BangOlufsenModel(StrEnum): BEOSOUND_THEATRE = "Beosound Theatre" +class BangOlufsenAttribute(StrEnum): + """Enum for extra_state_attribute keys.""" + + BEOLINK = "beolink" + BEOLINK_PEERS = "peers" + BEOLINK_SELF = "self" + BEOLINK_LEADER = "leader" + BEOLINK_LISTENERS = "listeners" + MEDIA_ID = "media_id" + + # Dispatcher events class WebsocketNotification(StrEnum): """Enum for WebSocket notification types.""" diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index c7bb6a47b5f58..600388ba9ff70 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -81,6 +81,7 @@ DOMAIN, FALLBACK_SOURCES, VALID_MEDIA_TYPES, + BangOlufsenAttribute, BangOlufsenMediaType, BangOlufsenSource, WebsocketNotification, @@ -453,18 +454,24 @@ async def _async_update_beolink(self) -> None: # Add Beolink self self._beolink_attributes = { - "beolink": {"self": {self.device_entry.name: self._beolink_jid}} + BangOlufsenAttribute.BEOLINK: { + BangOlufsenAttribute.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"] = {} + self._beolink_attributes[BangOlufsenAttribute.BEOLINK][ + BangOlufsenAttribute.BEOLINK_PEERS + ] = {} for peer in peers: - self._beolink_attributes["beolink"]["peers"][peer.friendly_name] = ( - peer.jid - ) + self._beolink_attributes[BangOlufsenAttribute.BEOLINK][ + BangOlufsenAttribute.BEOLINK_PEERS + ][peer.friendly_name] = peer.jid # Add Beolink listeners / leader self._remote_leader = self._playback_metadata.remote_leader @@ -485,7 +492,9 @@ async def _async_update_beolink(self) -> None: # Add self group_members.append(self.entity_id) - self._beolink_attributes["beolink"]["leader"] = { + self._beolink_attributes[BangOlufsenAttribute.BEOLINK][ + BangOlufsenAttribute.BEOLINK_LEADER + ] = { self._remote_leader.friendly_name: self._remote_leader.jid, } @@ -522,9 +531,9 @@ async def _async_update_beolink(self) -> None: beolink_listener.jid ) break - self._beolink_attributes["beolink"]["listeners"] = ( - beolink_listeners_attribute - ) + self._beolink_attributes[BangOlufsenAttribute.BEOLINK][ + BangOlufsenAttribute.BEOLINK_LISTENERS + ] = beolink_listeners_attribute self._attr_group_members = group_members @@ -687,7 +696,7 @@ def extra_state_attributes(self) -> dict[str, Any] | None: # Add media id attribute if self._media_id_attribute: - attributes.update({"media_id": self._media_id_attribute}) + attributes.update({BangOlufsenAttribute.MEDIA_ID: self._media_id_attribute}) # Add Beolink attributes if self._beolink_attributes: diff --git a/tests/components/bang_olufsen/snapshots/test_media_player.ambr b/tests/components/bang_olufsen/snapshots/test_media_player.ambr index 38b2d9b4156e7..5c76c5942f356 100644 --- a/tests/components/bang_olufsen/snapshots/test_media_player.ambr +++ b/tests/components/bang_olufsen/snapshots/test_media_player.ambr @@ -2,16 +2,16 @@ # name: test_async_beolink_allstandby StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -50,16 +50,16 @@ # name: test_async_beolink_expand[all_discovered-True-None-log_messages0-3] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -99,16 +99,16 @@ # name: test_async_beolink_expand[all_discovered-True-expand_side_effect1-log_messages1-3] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -148,16 +148,16 @@ # name: test_async_beolink_expand[beolink_jids-parameter_value2-None-log_messages2-2] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -197,16 +197,16 @@ # name: test_async_beolink_expand[beolink_jids-parameter_value3-expand_side_effect3-log_messages3-2] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -246,16 +246,16 @@ # name: test_async_beolink_join[service_parameters0-method_parameters0] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -294,16 +294,16 @@ # name: test_async_beolink_join[service_parameters1-method_parameters1] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -342,16 +342,16 @@ # name: test_async_beolink_join[service_parameters2-method_parameters2] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -390,16 +390,16 @@ # name: test_async_beolink_join_invalid[service_parameters0-expected_result0] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -438,16 +438,16 @@ # name: test_async_beolink_join_invalid[service_parameters1-expected_result1] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -486,16 +486,16 @@ # name: test_async_beolink_join_invalid[service_parameters2-expected_result2] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -534,16 +534,16 @@ # name: test_async_beolink_unexpand StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -582,16 +582,16 @@ # name: test_async_join_players[group_members0-1-0] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -631,16 +631,16 @@ # name: test_async_join_players[group_members0-1-0].1 StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com', }), }), @@ -680,16 +680,16 @@ # name: test_async_join_players[group_members1-0-1] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -729,16 +729,16 @@ # name: test_async_join_players[group_members1-0-1].1 StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com', }), }), @@ -778,16 +778,16 @@ # name: test_async_join_players_invalid[source0-group_members0-expected_result0-invalid_source] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -828,16 +828,16 @@ # name: test_async_join_players_invalid[source0-group_members0-expected_result0-invalid_source].1 StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com', }), }), @@ -877,16 +877,16 @@ # name: test_async_join_players_invalid[source1-group_members1-expected_result1-invalid_grouping_entity] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -926,16 +926,16 @@ # name: test_async_join_players_invalid[source1-group_members1-expected_result1-invalid_grouping_entity].1 StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com', }), }), @@ -975,16 +975,16 @@ # name: test_async_unjoin_player StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -1023,15 +1023,15 @@ # name: test_async_update_beolink_listener StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'leader': dict({ + : dict({ + : dict({ 'Laundry room Core': '1111.1111111.22222222@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com', }), }), @@ -1069,16 +1069,16 @@ # name: test_async_update_beolink_listener.1 StateSnapshot({ 'attributes': ReadOnlyDict({ - 'beolink': dict({ - 'listeners': dict({ + : dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'peers': dict({ + : dict({ 'Lego room Balance': '1111.1111111.33333333@products.bang-olufsen.com', 'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com', }), - 'self': dict({ + : dict({ 'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com', }), }), diff --git a/tests/components/bang_olufsen/test_media_player.py b/tests/components/bang_olufsen/test_media_player.py index b49898f803e0b..d4fe3d6638bce 100644 --- a/tests/components/bang_olufsen/test_media_player.py +++ b/tests/components/bang_olufsen/test_media_player.py @@ -24,6 +24,7 @@ BANG_OLUFSEN_REPEAT_FROM_HA, BANG_OLUFSEN_STATES, DOMAIN, + BangOlufsenAttribute, BangOlufsenSource, ) from homeassistant.components.media_player import ( @@ -260,7 +261,7 @@ async def test_async_update_playback_metadata( assert ATTR_MEDIA_ALBUM_ARTIST not in states.attributes assert ATTR_MEDIA_TRACK not in states.attributes assert ATTR_MEDIA_CHANNEL not in states.attributes - assert "media_id" not in states.attributes + assert BangOlufsenAttribute.MEDIA_ID not in states.attributes # Send the WebSocket event dispatch playback_metadata_callback(TEST_PLAYBACK_METADATA) @@ -278,7 +279,10 @@ async def test_async_update_playback_metadata( assert states.attributes[ATTR_MEDIA_TRACK] == TEST_PLAYBACK_METADATA.track assert states.attributes[ATTR_MEDIA_CHANNEL] == TEST_PLAYBACK_METADATA.organization assert states.attributes[ATTR_MEDIA_CHANNEL] == TEST_PLAYBACK_METADATA.organization - assert states.attributes["media_id"] == TEST_PLAYBACK_METADATA.source_internal_id + assert ( + states.attributes[BangOlufsenAttribute.MEDIA_ID] + == TEST_PLAYBACK_METADATA.source_internal_id + ) async def test_async_update_playback_error( From a8184e1b77806feab98ce98a6ad764524e3cd30f Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Tue, 18 Nov 2025 11:06:58 +0100 Subject: [PATCH 3/3] Use media ID attribute and update content type --- .../components/bang_olufsen/const.py | 5 +- .../components/bang_olufsen/media_player.py | 29 ++++++------ .../snapshots/test_media_player.ambr | 14 +++--- .../bang_olufsen/test_media_player.py | 46 ++++++++++++++----- 4 files changed, 61 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py index e4f7de76c2515..a04e4a108e747 100644 --- a/homeassistant/components/bang_olufsen/const.py +++ b/homeassistant/components/bang_olufsen/const.py @@ -17,8 +17,12 @@ class BangOlufsenSource: """Class used for associating device source ids with friendly names. May not include all sources.""" + DEEZER: Final[Source] = Source(name="Deezer", id="deezer") LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn") + NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio") SPDIF: Final[Source] = Source(name="Optical", id="spdif") + TIDAL: Final[Source] = Source(name="Tidal", id="tidal") + UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown") URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer") @@ -82,7 +86,6 @@ class BangOlufsenAttribute(StrEnum): BEOLINK_SELF = "self" BEOLINK_LEADER = "leader" BEOLINK_LISTENERS = "listeners" - MEDIA_ID = "media_id" # Dispatcher events diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 600388ba9ff70..72a107163234e 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -227,8 +227,6 @@ def __init__(self, entry: ConfigEntry, client: MozartClient) -> None: # Extra state attributes: # Beolink: peer(s), listener(s), leader and self self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {} - # Media ID: Currently playing Deezer, Tidal and radio station IDs - self._media_id_attribute: str | None = None async def async_added_to_hass(self) -> None: """Turn on the dispatchers.""" @@ -374,9 +372,6 @@ async def _async_update_playback_metadata_and_beolink( """Update _playback_metadata and related.""" self._playback_metadata = data - # Update media id attribute - self._media_id_attribute = data.source_internal_id - # Update current artwork and remote_leader. self._media_image = get_highest_resolution_artwork(self._playback_metadata) await self._async_update_beolink() @@ -632,11 +627,18 @@ def is_volume_muted(self) -> bool | None: return None @property - def media_content_type(self) -> str: + def media_content_type(self) -> MediaType | str | None: """Return the current media type.""" - # Hard to determine content type - if self._source_change.id == BangOlufsenSource.URI_STREAMER.id: - return MediaType.URL + content_type = { + BangOlufsenSource.URI_STREAMER.id: MediaType.URL, + BangOlufsenSource.DEEZER.id: BangOlufsenMediaType.DEEZER, + BangOlufsenSource.TIDAL.id: BangOlufsenMediaType.TIDAL, + BangOlufsenSource.NET_RADIO.id: BangOlufsenMediaType.RADIO, + } + # Hard to determine content type. + if self._source_change.id in content_type: + return content_type[self._source_change.id] + return MediaType.MUSIC @property @@ -649,6 +651,11 @@ def media_position(self) -> int | None: """Return the current playback progress.""" return self._playback_progress.progress + @property + def media_content_id(self) -> str | None: + """Return internal ID of Deezer, Tidal and radio stations.""" + return self._playback_metadata.source_internal_id + @property def media_image_url(self) -> str | None: """Return URL of the currently playing music.""" @@ -694,10 +701,6 @@ def extra_state_attributes(self) -> dict[str, Any] | None: """Return information that is not returned anywhere else.""" attributes: dict[str, Any] = {} - # Add media id attribute - if self._media_id_attribute: - attributes.update({BangOlufsenAttribute.MEDIA_ID: self._media_id_attribute}) - # Add Beolink attributes if self._beolink_attributes: attributes.update(self._beolink_attributes) diff --git a/tests/components/bang_olufsen/snapshots/test_media_player.ambr b/tests/components/bang_olufsen/snapshots/test_media_player.ambr index 5c76c5942f356..01e6c13fca508 100644 --- a/tests/components/bang_olufsen/snapshots/test_media_player.ambr +++ b/tests/components/bang_olufsen/snapshots/test_media_player.ambr @@ -71,7 +71,7 @@ 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com', 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com', ]), - 'media_content_type': , + 'media_content_type': , 'repeat': , 'shuffle': False, 'sound_mode': 'Test Listening Mode (123)', @@ -120,7 +120,7 @@ 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com', 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com', ]), - 'media_content_type': , + 'media_content_type': , 'repeat': , 'shuffle': False, 'sound_mode': 'Test Listening Mode (123)', @@ -169,7 +169,7 @@ 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com', 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com', ]), - 'media_content_type': , + 'media_content_type': , 'repeat': , 'shuffle': False, 'sound_mode': 'Test Listening Mode (123)', @@ -218,7 +218,7 @@ 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com', 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com', ]), - 'media_content_type': , + 'media_content_type': , 'repeat': , 'shuffle': False, 'sound_mode': 'Test Listening Mode (123)', @@ -603,7 +603,7 @@ 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com', 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com', ]), - 'media_content_type': , + 'media_content_type': , 'repeat': , 'shuffle': False, 'sound_mode': 'Test Listening Mode (123)', @@ -701,7 +701,7 @@ 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com', 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com', ]), - 'media_content_type': , + 'media_content_type': , 'repeat': , 'shuffle': False, 'sound_mode': 'Test Listening Mode (123)', @@ -898,7 +898,7 @@ 'listener_not_in_hass-1111.1111111.33333333@products.bang-olufsen.com', 'listener_not_in_hass-1111.1111111.44444444@products.bang-olufsen.com', ]), - 'media_content_type': , + 'media_content_type': , 'repeat': , 'shuffle': False, 'sound_mode': 'Test Listening Mode (123)', diff --git a/tests/components/bang_olufsen/test_media_player.py b/tests/components/bang_olufsen/test_media_player.py index d4fe3d6638bce..e89a48866625b 100644 --- a/tests/components/bang_olufsen/test_media_player.py +++ b/tests/components/bang_olufsen/test_media_player.py @@ -24,7 +24,7 @@ BANG_OLUFSEN_REPEAT_FROM_HA, BANG_OLUFSEN_STATES, DOMAIN, - BangOlufsenAttribute, + BangOlufsenMediaType, BangOlufsenSource, ) from homeassistant.components.media_player import ( @@ -261,7 +261,7 @@ async def test_async_update_playback_metadata( assert ATTR_MEDIA_ALBUM_ARTIST not in states.attributes assert ATTR_MEDIA_TRACK not in states.attributes assert ATTR_MEDIA_CHANNEL not in states.attributes - assert BangOlufsenAttribute.MEDIA_ID not in states.attributes + assert ATTR_MEDIA_CONTENT_ID not in states.attributes # Send the WebSocket event dispatch playback_metadata_callback(TEST_PLAYBACK_METADATA) @@ -280,9 +280,10 @@ async def test_async_update_playback_metadata( assert states.attributes[ATTR_MEDIA_CHANNEL] == TEST_PLAYBACK_METADATA.organization assert states.attributes[ATTR_MEDIA_CHANNEL] == TEST_PLAYBACK_METADATA.organization assert ( - states.attributes[BangOlufsenAttribute.MEDIA_ID] + states.attributes[ATTR_MEDIA_CONTENT_ID] == TEST_PLAYBACK_METADATA.source_internal_id ) + assert states.attributes[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC async def test_async_update_playback_error( @@ -349,28 +350,47 @@ async def test_async_update_playback_state( @pytest.mark.parametrize( - ("source", "content_type", "progress", "metadata"), + ("source", "content_type", "progress", "metadata", "content_id_available"), [ - # Normal source, music mediatype expected - ( - TEST_SOURCE, - MediaType.MUSIC, - TEST_PLAYBACK_PROGRESS.progress, - PlaybackContentMetadata(), - ), # URI source, url media type expected ( BangOlufsenSource.URI_STREAMER, MediaType.URL, TEST_PLAYBACK_PROGRESS.progress, PlaybackContentMetadata(), + False, ), - # Line-In source,media type expected, progress 0 expected + # Line-In source, music media type expected, progress 0 expected ( BangOlufsenSource.LINE_IN, MediaType.MUSIC, 0, PlaybackContentMetadata(), + False, + ), + # Tidal source, tidal media type expected, media content id expected + ( + BangOlufsenSource.TIDAL, + BangOlufsenMediaType.TIDAL, + TEST_PLAYBACK_PROGRESS.progress, + PlaybackContentMetadata(source_internal_id="123"), + True, + ), + # Deezer source, deezer media type expected, media content id expected + ( + BangOlufsenSource.DEEZER, + BangOlufsenMediaType.DEEZER, + TEST_PLAYBACK_PROGRESS.progress, + PlaybackContentMetadata(source_internal_id="123"), + True, + ), + # Radio source, radio media type expected, media content id expected + ( + BangOlufsenSource.NET_RADIO, + BangOlufsenMediaType.RADIO, + TEST_PLAYBACK_PROGRESS.progress, + PlaybackContentMetadata(source_internal_id="123"), + True, ), ], ) @@ -382,6 +402,7 @@ async def test_async_update_source_change( content_type: MediaType, progress: int, metadata: PlaybackContentMetadata, + content_id_available: bool, ) -> None: """Test _async_update_source_change.""" playback_progress_callback = ( @@ -409,6 +430,7 @@ async def test_async_update_source_change( assert states.attributes[ATTR_INPUT_SOURCE] == source.name assert states.attributes[ATTR_MEDIA_CONTENT_TYPE] == content_type assert states.attributes[ATTR_MEDIA_POSITION] == progress + assert (ATTR_MEDIA_CONTENT_ID in states.attributes) == content_id_available async def test_async_turn_off(