From 1d5be91f10c3f2064d6cb82209331037b861ec00 Mon Sep 17 00:00:00 2001 From: mj23000 Date: Thu, 19 Dec 2024 17:19:18 +0100 Subject: [PATCH] Improve Beoremote One paired / unpaired checks for config entry reloading Restructure code to resemble core version of integration closer Cleanup Small improvements --- .../bang_olufsen/binary_sensor.py | 2 +- custom_components/bang_olufsen/const.py | 62 ++-- custom_components/bang_olufsen/entity.py | 7 +- custom_components/bang_olufsen/event.py | 2 +- custom_components/bang_olufsen/manifest.json | 2 +- .../bang_olufsen/media_player.py | 336 +++++++++--------- custom_components/bang_olufsen/select.py | 2 +- custom_components/bang_olufsen/sensor.py | 4 +- custom_components/bang_olufsen/text.py | 2 +- custom_components/bang_olufsen/util.py | 15 + custom_components/bang_olufsen/websocket.py | 92 +++-- 11 files changed, 270 insertions(+), 256 deletions(-) diff --git a/custom_components/bang_olufsen/binary_sensor.py b/custom_components/bang_olufsen/binary_sensor.py index 6f19250..793ea4f 100644 --- a/custom_components/bang_olufsen/binary_sensor.py +++ b/custom_components/bang_olufsen/binary_sensor.py @@ -23,7 +23,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Binary Sensor entities from config entry.""" - entities: list[BangOlufsenEntity] = [] + entities: list[BangOlufsenBinarySensor] = [] # Check if device has a battery battery_state = await config_entry.runtime_data.client.get_battery_state() diff --git a/custom_components/bang_olufsen/const.py b/custom_components/bang_olufsen/const.py index 1ea968a..a7a7e32 100644 --- a/custom_components/bang_olufsen/const.py +++ b/custom_components/bang_olufsen/const.py @@ -119,16 +119,11 @@ class WebsocketNotification(StrEnum): ALL = "all" -# Range for bass and treble entities -BASS_TREBLE_RANGE = range(-6, 6, 1) - DOMAIN: Final[str] = "bang_olufsen" # Default values for configuration. DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE -MANUFACTURER: Final[str] = "Bang & Olufsen" - # Configuration. CONF_BEOLINK_JID: Final = "jid" CONF_SERIAL_NUMBER: Final = "serial_number" @@ -138,6 +133,8 @@ class WebsocketNotification(StrEnum): model for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE ] +MANUFACTURER: Final[str] = "Bang & Olufsen" + # Attribute names for zeroconf discovery. ATTR_TYPE_NUMBER: Final[str] = "tn" ATTR_SERIAL_NUMBER: Final[str] = "sn" @@ -159,17 +156,6 @@ class WebsocketNotification(StrEnum): MediaType.CHANNEL, ) -# Playback states for playing and not playing -PLAYING: Final[tuple[str, ...]] = ("started", "buffering", BANG_OLUFSEN_ON) -NOT_PLAYING: Final[tuple[str, ...]] = ( - "idle", - "paused", - "stopped", - "ended", - "unknown", - "error", -) - # Fallback sources to use in case of API failure. FALLBACK_SOURCES: Final[SourceArray] = SourceArray( items=[ @@ -274,22 +260,24 @@ class WebsocketNotification(StrEnum): # Device events BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event" - -CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS" - # Dict used to translate native Bang & Olufsen event names to string.json compatible ones EVENT_TRANSLATION_MAP: dict[str, str] = { + # Beoremote One "KeyPress": "key_press", "KeyRelease": "key_release", + # Physical "buttons" "shortPress (Release)": "short_press_release", "longPress (Timeout)": "long_press_timeout", "longPress (Release)": "long_press_release", "veryLongPress (Timeout)": "very_long_press_timeout", "veryLongPress (Release)": "very_long_press_release", + # Proximity sensor "proximityPresenceDetected": "proximity_presence_detected", "proximityPresenceNotDetected": "proximity_presence_not_detected", } +CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS" + DEVICE_BUTTONS: Final[list[str]] = [ "Bluetooth", "Microphone", @@ -360,7 +348,7 @@ class WebsocketNotification(StrEnum): "Func17", ) -# "keys" that are unique to the Control and Light submenus +# "keys" that are unique to the Control submenu BEO_REMOTE_CONTROL_KEYS: Final[tuple[str, ...]] = ( "Func18", "Func19", @@ -382,6 +370,23 @@ class WebsocketNotification(StrEnum): "proximity_presence_not_detected", ] +# Beolink Converter NL/ML sources need to be transformed to upper case +BEOLINK_JOIN_SOURCES_TO_UPPER = ( + "aux_a", + "cd", + "ph", + "radio", + "tp1", + "tp2", +) +BEOLINK_JOIN_SOURCES = ( + *BEOLINK_JOIN_SOURCES_TO_UPPER, + "beoradio", + "deezer", + "spotify", + "tidal", +) + BEOLINK_LEADER_COMMAND: Final[str] = "BEOLINK_LEADER_COMMAND" BEOLINK_LISTENER_COMMAND: Final[str] = "BEOLINK_LISTENER_COMMAND" @@ -430,20 +435,3 @@ class WebsocketNotification(StrEnum): STR_PARAMETERS, NONE_PARAMETERS, ) - -# Beolink Converter NL/ML sources need to be transformed to upper case -BEOLINK_JOIN_SOURCES_TO_UPPER = ( - "aux_a", - "cd", - "ph", - "radio", - "tp1", - "tp2", -) -BEOLINK_JOIN_SOURCES = ( - *BEOLINK_JOIN_SOURCES_TO_UPPER, - "beoradio", - "deezer", - "spotify", - "tidal", -) diff --git a/custom_components/bang_olufsen/entity.py b/custom_components/bang_olufsen/entity.py index d3e54ec..d7fd20c 100644 --- a/custom_components/bang_olufsen/entity.py +++ b/custom_components/bang_olufsen/entity.py @@ -50,12 +50,11 @@ def __init__( self._client = config_entry.runtime_data.client # Get the input from the config entry. - # Use _entry instead of config_entry to avoid conflicts with Home Assistant classes such as DataUpdateCoordinator. - self._entry = config_entry + self.entry = config_entry # Set the configuration variables. - self._host: str = self._entry.data[CONF_HOST] - self._unique_id: str = cast(str, self._entry.unique_id) + self._host: str = self.entry.data[CONF_HOST] + self._unique_id: str = cast(str, self.entry.unique_id) # Objects that get directly updated by notifications. self._active_listening_mode = ListeningModeProps() diff --git a/custom_components/bang_olufsen/event.py b/custom_components/bang_olufsen/event.py index 2009615..d2d4ac1 100644 --- a/custom_components/bang_olufsen/event.py +++ b/custom_components/bang_olufsen/event.py @@ -39,7 +39,7 @@ async def async_setup_entry( ) -> None: """Set up Sensor entities from config entry.""" - entities: list[EventEntity] = [] + entities: list[BangOlufsenEvent] = [] # Add physical "buttons" if config_entry.data[CONF_MODEL] in MODEL_SUPPORT_MAP[MODEL_SUPPORT_DEVICE_BUTTONS]: diff --git a/custom_components/bang_olufsen/manifest.json b/custom_components/bang_olufsen/manifest.json index c794b56..46cb999 100644 --- a/custom_components/bang_olufsen/manifest.json +++ b/custom_components/bang_olufsen/manifest.json @@ -9,6 +9,6 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/bang-olufsen/bang_olufsen-hacs/issues", "requirements": ["mozart-api==4.1.1.116.4"], - "version": "3.2.0", + "version": "3.2.1", "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/custom_components/bang_olufsen/media_player.py b/custom_components/bang_olufsen/media_player.py index a852083..9fa00b0 100644 --- a/custom_components/bang_olufsen/media_player.py +++ b/custom_components/bang_olufsen/media_player.py @@ -137,7 +137,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Media Player entity from config entry.""" - entities: list[BangOlufsenEntity] = [] + entities: list[BangOlufsenMediaPlayer] = [] entities.append(BangOlufsenMediaPlayer(config_entry)) @@ -239,8 +239,8 @@ def __init__(self, config_entry: BangOlufsenConfigEntry) -> None: """Initialize the media player.""" super().__init__(config_entry) - self._beolink_jid: str = self._entry.data[CONF_BEOLINK_JID] - self._model: str = self._entry.data[CONF_MODEL] + self._beolink_jid: str = self.entry.data[CONF_BEOLINK_JID] + self._model: str = self.entry.data[CONF_MODEL] self._attr_device_info = DeviceInfo( configuration_url=f"http://{self._host}/#/", @@ -268,7 +268,7 @@ def __init__(self, config_entry: BangOlufsenConfigEntry) -> None: # Beolink self._beolink_sources: dict[str, bool] = {} self._remote_leader: BeolinkLeader | None = None - self._beolink_attribute: dict[str, dict[str, Any]] = {} + self._beolink_attributes: dict[str, dict[str, Any]] = {} self._beolink_listeners: list[BeolinkListener] = [] self._favourite_attribute: dict[str, dict[str, Any]] = {} @@ -345,8 +345,8 @@ async def _initialize(self) -> None: if product_state.playback.state: self._playback_state = product_state.playback.state # Set initial state - if product_state.playback.state.value: - self._state = product_state.playback.state.value + if self._playback_state.value: + self._state = self._playback_state.value self._attr_media_position_updated_at = utcnow() @@ -358,6 +358,7 @@ 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: @@ -440,21 +441,6 @@ async def _generate_favourite_attributes( # Add current favourite to attribute self._favourite_attribute["favourites"][favourite_id] = favourite_attribute - 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) - device_registry.async_update_device( - device_id=cast(DeviceEntry, self.device_entry).id, - name=beolink_self.friendly_name, - ) - - await self._async_update_beolink(should_update=False) - - self.async_write_ha_state() - async def _async_update_sources(self, _: Source | None = None) -> None: """Get sources for the specific product.""" @@ -525,130 +511,6 @@ async def _async_update_sources(self, _: Source | None = None) -> None: self.async_write_ha_state() - def _get_beolink_jid(self, entity_id: str) -> str: - """Get beolink JID from entity_id.""" - - entity_registry = er.async_get(self.hass) - - # Check for valid bang_olufsen media_player entity - entity_entry = entity_registry.async_get(entity_id) - - if ( - entity_entry is None - or entity_entry.domain != Platform.MEDIA_PLAYER - or entity_entry.platform != DOMAIN - or entity_entry.config_entry_id is None - ): - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="invalid_grouping_entity", - translation_placeholders={"entity_id": entity_id}, - ) - - config_entry = self.hass.config_entries.async_get_entry( - entity_entry.config_entry_id - ) - if TYPE_CHECKING: - assert config_entry - - # Return JID - return cast(str, config_entry.data[CONF_BEOLINK_JID]) - - def _get_entity_id_from_jid(self, jid: str) -> str | None: - """Get entity_id from Beolink JID (if available).""" - - unique_id = get_serial_number_from_jid(jid) - - entity_registry = er.async_get(self.hass) - return entity_registry.async_get_entity_id( - Platform.MEDIA_PLAYER, DOMAIN, unique_id - ) - - async def _async_update_beolink(self, should_update: bool = True) -> None: - """Update the current Beolink leader, listeners, peers and self.""" - - self._beolink_attribute = {} - - # Add Beolink self - assert self.device_entry - - self._beolink_attribute = { - "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_attribute["beolink"]["peers"] = {} - for peer in peers: - self._beolink_attribute["beolink"]["peers"][peer.friendly_name] = ( - peer.jid - ) - - self._remote_leader = self._playback_metadata.remote_leader - - # Temp fix for mismatch in WebSocket metadata and "real" REST endpoint where the remote leader is not deleted. - if self._source_change.id in ( - BangOlufsenSource.LINE_IN.id, - BangOlufsenSource.SPDIF.id, - ): - self._remote_leader = None - - # Add Beolink listeners / leader - - # Create group members list - group_members = [] - - # If the device is a listener. - if self._remote_leader is not None: - # Add leader - group_members.append( - cast(str, self._get_entity_id_from_jid(self._remote_leader.jid)) - ) - - # Add self - group_members.append( - cast(str, self._get_entity_id_from_jid(self._beolink_jid)) - ) - - self._beolink_attribute["beolink"]["leader"] = { - self._remote_leader.friendly_name: self._remote_leader.jid, - } - - # If not listener, check if leader. - else: - self._beolink_listeners = await self._client.get_beolink_listeners() - - # Check if the device is a leader. - if len(self._beolink_listeners) > 0: - # Add self - group_members.append( - cast(str, self._get_entity_id_from_jid(self._beolink_jid)) - ) - - # Get the friendly names for the listeners from the peers - beolink_listeners_attribute = {} - for beolink_listener in self._beolink_listeners: - group_members.append( - cast(str, self._get_entity_id_from_jid(beolink_listener.jid)) - ) - for peer in peers: - if peer.jid == beolink_listener.jid: - beolink_listeners_attribute[peer.friendly_name] = ( - beolink_listener.jid - ) - break - - self._beolink_attribute["beolink"]["listeners"] = ( - beolink_listeners_attribute - ) - - self._attr_group_members = group_members - - if should_update: - self.async_write_ha_state() - async def _async_update_playback_metadata_and_beolink( self, data: PlaybackContentMetadata ) -> None: @@ -657,9 +519,7 @@ async def _async_update_playback_metadata_and_beolink( # Update current artwork and remote_leader. self._media_image = get_highest_resolution_artwork(self._playback_metadata) - await self._async_update_beolink(should_update=False) - - self.async_write_ha_state() + await self._async_update_beolink() @callback def _async_update_playback_error(self, data: PlaybackError) -> None: @@ -729,6 +589,144 @@ 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.""" + beolink_self = await self._client.get_beolink_self() + + # Update device name + device_registry = dr.async_get(self.hass) + device_registry.async_update_device( + device_id=cast(DeviceEntry, 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 + + # 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 + + # Create group members list + group_members = [] + + # If the device is a listener. + if self._remote_leader is not None: + # Add leader if available in Home Assistant + leader = self._get_entity_id_from_jid(self._remote_leader.jid) + group_members.append( + leader + if leader is not None + else f"leader_not_in_hass-{self._remote_leader.friendly_name}" + ) + + # 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: + self._beolink_listeners = await self._client.get_beolink_listeners() + beolink_listeners_attribute = {} + + # Check if the device is a leader. + if len(self._beolink_listeners) > 0: + # Add self + group_members.append(self.entity_id) + + # Get the entity_ids of the listeners if available in Home Assistant + group_members.extend( + [ + listener + if ( + listener := self._get_entity_id_from_jid( + beolink_listener.jid + ) + ) + is not None + else f"listener_not_in_hass-{beolink_listener.jid}" + for beolink_listener in self._beolink_listeners + ] + ) + # Update Beolink attributes + for beolink_listener in self._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 + + self.async_write_ha_state() + + def _get_entity_id_from_jid(self, jid: str) -> str | None: + """Get entity_id from Beolink JID (if available).""" + + unique_id = get_serial_number_from_jid(jid) + + entity_registry = er.async_get(self.hass) + return entity_registry.async_get_entity_id( + Platform.MEDIA_PLAYER, DOMAIN, unique_id + ) + + def _get_beolink_jid(self, entity_id: str) -> str: + """Get beolink JID from entity_id.""" + + entity_registry = er.async_get(self.hass) + + # Check for valid bang_olufsen media_player entity + entity_entry = entity_registry.async_get(entity_id) + + if ( + entity_entry is None + or entity_entry.domain != Platform.MEDIA_PLAYER + or entity_entry.platform != DOMAIN + or entity_entry.config_entry_id is None + ): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_grouping_entity", + translation_placeholders={"entity_id": entity_id}, + ) + + config_entry = self.hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + if TYPE_CHECKING: + assert config_entry + + # Return JID + return cast(str, config_entry.data[CONF_BEOLINK_JID]) + async def _async_update_sound_modes( self, active_sound_mode: ListeningModeProps | ListeningModeRef | None = None ) -> None: @@ -846,8 +844,8 @@ def extra_state_attributes(self) -> dict[str, Any] | None: attributes: dict[str, Any] = {} # Add Beolink attributes - if self._beolink_attribute: - attributes.update(self._beolink_attribute) + if self._beolink_attributes: + attributes.update(self._beolink_attributes) # Add favourite attributes if self._favourite_attribute: @@ -976,25 +974,6 @@ async def async_select_sound_mode(self, sound_mode: str) -> None: await self._client.activate_listening_mode(id=self._sound_modes[sound_mode]) - async def async_join_players(self, group_members: list[str]) -> None: - """Create a Beolink session with defined group members.""" - - # Use the touch to join if no entities have been defined - # Touch to join will make the device connect to any other currently-playing - # Beolink compatible B&O device. - # Repeated presses / calls will cycle between compatible playing devices. - if len(group_members) == 0: - 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) - - async def async_unjoin_player(self) -> None: - """Unjoin Beolink session. End session if leader.""" - await self._client.post_beolink_leave() - async def async_play_media( self, media_type: MediaType | str, @@ -1153,6 +1132,25 @@ async def async_browse_media( content_filter=lambda item: item.media_content_type.startswith("audio/"), ) + async def async_join_players(self, group_members: list[str]) -> None: + """Create a Beolink session with defined group members.""" + + # Use the touch to join if no entities have been defined + # Touch to join will make the device connect to any other currently-playing + # Beolink compatible B&O device. + # Repeated presses / calls will cycle between compatible playing devices. + if len(group_members) == 0: + 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) + + async def async_unjoin_player(self) -> None: + """Unjoin Beolink session. End session if leader.""" + await self._client.post_beolink_leave() + # Custom services: async def async_beolink_join( self, beolink_jid: str | None = None, source_id: str | None = None diff --git a/custom_components/bang_olufsen/select.py b/custom_components/bang_olufsen/select.py index 15f1425..ffd0631 100644 --- a/custom_components/bang_olufsen/select.py +++ b/custom_components/bang_olufsen/select.py @@ -25,7 +25,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Select entities from config entry.""" - entities: list[BangOlufsenEntity] = [] + entities: list[BangOlufsenSelect] = [] # Create the listening position entity if supported scenes = await config_entry.runtime_data.client.get_all_scenes() diff --git a/custom_components/bang_olufsen/sensor.py b/custom_components/bang_olufsen/sensor.py index 1f56297..3ea3f69 100644 --- a/custom_components/bang_olufsen/sensor.py +++ b/custom_components/bang_olufsen/sensor.py @@ -37,7 +37,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Sensor entities from config entry.""" - entities: list[BangOlufsenEntity] = [ + entities: list[BangOlufsenSensor] = [ BangOlufsenSensorInputSignal(config_entry), BangOlufsenSensorMediaId(config_entry), ] @@ -280,7 +280,7 @@ async def async_added_to_hass(self) -> None: self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._entry.unique_id}_{WebsocketNotification.PLAYBACK_METADATA}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_METADATA}", self._update_playback_metadata, ) ) diff --git a/custom_components/bang_olufsen/text.py b/custom_components/bang_olufsen/text.py index 010fae5..12b9142 100644 --- a/custom_components/bang_olufsen/text.py +++ b/custom_components/bang_olufsen/text.py @@ -21,7 +21,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Text entities from config entry.""" - entities: list[BangOlufsenEntity] = [] + entities: list[BangOlufsenText] = [] # Add the Home Control URI entity if the device supports it if config_entry.data[CONF_MODEL] in MODEL_SUPPORT_MAP[MODEL_SUPPORT_HOME_CONTROL]: diff --git a/custom_components/bang_olufsen/util.py b/custom_components/bang_olufsen/util.py index b2db1d9..2489a36 100644 --- a/custom_components/bang_olufsen/util.py +++ b/custom_components/bang_olufsen/util.py @@ -7,6 +7,21 @@ from mozart_api.models import PairedRemote from mozart_api.mozart_client import MozartClient +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import DOMAIN + + +def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry: + """Get the device.""" + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, unique_id)}) + assert device + + return device + def get_serial_number_from_jid(jid: str) -> str: """Get serial number from Beolink JID.""" diff --git a/custom_components/bang_olufsen/websocket.py b/custom_components/bang_olufsen/websocket.py index 99ded7b..e48eecf 100644 --- a/custom_components/bang_olufsen/websocket.py +++ b/custom_components/bang_olufsen/websocket.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING from mozart_api.models import ( BatteryState, @@ -30,11 +31,12 @@ from .const import ( BANG_OLUFSEN_WEBSOCKET_EVENT, CONNECTION_STATUS, - DOMAIN, EVENT_TRANSLATION_MAP, + BangOlufsenModel, WebsocketNotification, ) from .entity import BangOlufsenBase +from .util import get_device, get_remotes _LOGGER = logging.getLogger(__name__) @@ -52,7 +54,7 @@ def __init__( super().__init__(config_entry, client) self.hass = hass - self._device = self.get_device() + self._device = get_device(hass, self._unique_id) # WebSocket callbacks self._client.get_active_listening_mode_notifications( @@ -93,14 +95,6 @@ def __init__( # Used for firing events and debugging self._client.get_all_notifications_raw(self.on_all_notifications_raw) - def get_device(self) -> dr.DeviceEntry: - """Get the device.""" - device_registry = dr.async_get(self.hass) - device = device_registry.async_get_device({(DOMAIN, self._unique_id)}) - assert device - - return device - def _update_connection_status(self) -> None: """Update all entities of the connection status.""" async_dispatcher_send( @@ -111,12 +105,12 @@ def _update_connection_status(self) -> None: def on_connection(self) -> None: """Handle WebSocket connection made.""" - _LOGGER.debug("Connected to the %s notification channel", self._entry.title) + _LOGGER.debug("Connected to the %s notification channel", self.entry.title) self._update_connection_status() def on_connection_lost(self) -> None: """Handle WebSocket connection lost.""" - _LOGGER.error("Lost connection to the %s", self._entry.title) + _LOGGER.error("Lost connection to the %s", self.entry.title) self._update_connection_status() def on_active_listening_mode(self, notification: ListeningModeProps) -> None: @@ -145,7 +139,9 @@ def on_battery_notification(self, notification: BatteryState) -> None: def on_beo_remote_button_notification(self, notification: BeoRemoteButton) -> None: """Send beo_remote_button dispatch.""" - assert notification.type + if TYPE_CHECKING: + assert notification.type + # Send to event entity async_dispatcher_send( self.hass, @@ -163,7 +159,7 @@ def on_button_notification(self, notification: ButtonEvent) -> None: EVENT_TRANSLATION_MAP[notification.state], ) - def on_notification_notification( + async def on_notification_notification( self, notification: WebsocketNotificationTag ) -> None: """Send notification dispatch.""" @@ -173,45 +169,63 @@ def on_notification_notification( notification_type = try_parse_enum(WebsocketNotification, notification.value) if notification_type in ( - WebsocketNotification.PROXIMITY_PRESENCE_DETECTED, - WebsocketNotification.PROXIMITY_PRESENCE_NOT_DETECTED, + WebsocketNotification.BEOLINK_PEERS, + WebsocketNotification.BEOLINK_LISTENERS, + WebsocketNotification.BEOLINK_AVAILABLE_LISTENERS, ): async_dispatcher_send( self.hass, - f"{self._unique_id}_{WebsocketNotification.PROXIMITY}", - EVENT_TRANSLATION_MAP[notification.value], + f"{self._unique_id}_{WebsocketNotification.BEOLINK}", ) - - elif notification_type is WebsocketNotification.REMOTE_MENU_CHANGED: + elif notification_type is WebsocketNotification.CONFIGURATION: async_dispatcher_send( self.hass, - f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}", + f"{self._unique_id}_{WebsocketNotification.CONFIGURATION}", ) - - elif notification_type is WebsocketNotification.CONFIGURATION: + elif notification_type in ( + WebsocketNotification.PROXIMITY_PRESENCE_DETECTED, + WebsocketNotification.PROXIMITY_PRESENCE_NOT_DETECTED, + ): async_dispatcher_send( self.hass, - f"{self._unique_id}_{WebsocketNotification.CONFIGURATION}", + f"{self._unique_id}_{WebsocketNotification.PROXIMITY}", + EVENT_TRANSLATION_MAP[notification.value], ) - + # This notification is triggered by a remote pairing, unpairing and connecting to a device + # So the current remote devices have to be compared to available remotes to determine action elif notification_type is WebsocketNotification.REMOTE_CONTROL_DEVICES: - # Reinitialize the config entry to update Beoremote One entities and device - # Wait 5 seconds for the remote to be properly available to the device - _LOGGER.warning("Remote control has been modified. Reloading integration") - self.hass.loop.call_later( - 5, - self.hass.config_entries.async_schedule_reload, - self._entry.entry_id, - ) + device_registry = dr.async_get(self.hass) + device_serial_numbers = [ + device.serial_number + for device in device_registry.devices.get_devices_for_config_entry_id( + self.entry.entry_id + ) + if device.serial_number is not None + and device.model == BangOlufsenModel.BEOREMOTE_ONE + ] + remote_serial_numbers = [ + remote.serial_number + for remote in await get_remotes(self._client) + if remote.serial_number is not None + ] + # Check if number of remote devices correspond to number of paired remotes + if len(remote_serial_numbers) != len(device_serial_numbers): + # Reinitialize the config entry to update Beoremote One entities and device + # Wait 5 seconds for the remote to be properly available to the device + _LOGGER.info( + "A Beoremote One has been paired or unpaired to %s. Reloading config entry to add device", + self._device.name, + ) + self.hass.loop.call_later( + 5, + self.hass.config_entries.async_schedule_reload, + self.entry.entry_id, + ) - elif notification_type in ( - WebsocketNotification.BEOLINK_PEERS, - WebsocketNotification.BEOLINK_LISTENERS, - WebsocketNotification.BEOLINK_AVAILABLE_LISTENERS, - ): + elif notification_type is WebsocketNotification.REMOTE_MENU_CHANGED: async_dispatcher_send( self.hass, - f"{self._unique_id}_{WebsocketNotification.BEOLINK}", + f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}", ) def on_playback_error_notification(self, notification: PlaybackError) -> None: