diff --git a/homeassistant/components/bangolufsen/__init__.py b/homeassistant/components/bangolufsen/__init__.py index c9778eb86dd..e6cc6c94fc6 100644 --- a/homeassistant/components/bangolufsen/__init__.py +++ b/homeassistant/components/bangolufsen/__init__.py @@ -4,17 +4,15 @@ from __future__ import annotations from dataclasses import dataclass import logging -from mozart_api.exceptions import ServiceException +from aiohttp.client_exceptions import ClientConnectorError from mozart_api.mozart_client import MozartClient -from urllib3.exceptions import MaxRetryError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.event import async_call_later -from .const import DOMAIN, WEBSOCKET_CONNECTION_DELAY +from .const import DOMAIN from .websocket import BangOlufsenWebsocket PLATFORMS = [Platform.MEDIA_PLAYER] @@ -36,10 +34,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Check connection and try to initialize it. - try: - await client.get_battery_state(_request_timeout=3) - except (MaxRetryError, ServiceException, Exception) as error: - raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error + async with client: + try: + await client.get_battery_state(_request_timeout=3) + except (TimeoutError, ClientConnectorError, Exception) as error: + raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error websocket = BangOlufsenWebsocket(hass, entry) @@ -47,7 +46,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BangOlufsenData(websocket) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async_call_later(hass, WEBSOCKET_CONNECTION_DELAY, websocket.connect_websocket) + websocket.connect_websocket() + # async_call_later(hass, WEBSOCKET_CONNECTION_DELAY, websocket.connect_websocket) return True diff --git a/homeassistant/components/bangolufsen/config_flow.py b/homeassistant/components/bangolufsen/config_flow.py index 0b6984c5dc8..0afb160ea26 100644 --- a/homeassistant/components/bangolufsen/config_flow.py +++ b/homeassistant/components/bangolufsen/config_flow.py @@ -5,9 +5,8 @@ import ipaddress import logging from typing import Any, TypedDict -from mozart_api.exceptions import ApiException, NotFoundException +from aiohttp.client_exceptions import ClientConnectorError from mozart_api.mozart_client import MozartClient -from urllib3.exceptions import MaxRetryError, NewConnectionError import voluptuous as vol from homeassistant.components.zeroconf import ZeroconfServiceInfo @@ -62,7 +61,8 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def _compile_data(self) -> UserInput: """Compile data for entry creation.""" # Get current volume settings - volume_settings = await self._client.get_volume_settings() + async with self._client: + volume_settings = await self._client.get_volume_settings() # Create a dict containing all necessary information for setup data = UserInput() @@ -94,7 +94,6 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): # Check if the IP address is a valid address. try: ipaddress.ip_address(self._host) - except ValueError: return self.async_abort(reason="value_error") @@ -102,20 +101,16 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): # Try to get information from Beolink self method. try: - beolink_self = await self._client.get_beolink_self(_request_timeout=3) + async with self._client: + beolink_self = await self._client.get_beolink_self( + _request_timeout=3 + ) - except ( - ApiException, - NewConnectionError, - MaxRetryError, - NotFoundException, - ) as error: + except (TimeoutError, ClientConnectorError) as error: return self.async_abort( reason={ - ApiException: "api_exception", - NewConnectionError: "new_connection_error", - MaxRetryError: "max_retry_error", - NotFoundException: "not_found_exception", + TimeoutError: "timeout_error", + ClientConnectorError: "client_connector_error", }[type(error)] ) diff --git a/homeassistant/components/bangolufsen/const.py b/homeassistant/components/bangolufsen/const.py index a4fdf552a2c..534cb2471fa 100644 --- a/homeassistant/components/bangolufsen/const.py +++ b/homeassistant/components/bangolufsen/const.py @@ -148,7 +148,6 @@ HIDDEN_SOURCE_IDS: Final[tuple] = ( "wpl", "pl", "beolink", - "classicsAdapter", "usbIn", ) @@ -157,57 +156,57 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray( items=[ Source( id="uriStreamer", - isEnabled=True, - isPlayable=False, + is_enabled=True, + is_playable=False, name="Audio Streamer", type=SourceTypeEnum(value="uriStreamer"), ), Source( id="bluetooth", - isEnabled=True, - isPlayable=False, + is_enabled=True, + is_playable=False, name="Bluetooth", type=SourceTypeEnum(value="bluetooth"), ), Source( id="spotify", - isEnabled=True, - isPlayable=False, + is_enabled=True, + is_playable=False, name="Spotify Connect", type=SourceTypeEnum(value="spotify"), ), Source( id="lineIn", - isEnabled=True, - isPlayable=True, + is_enabled=True, + is_playable=True, name="Line-In", type=SourceTypeEnum(value="lineIn"), ), Source( id="spdif", - isEnabled=True, - isPlayable=True, + is_enabled=True, + is_playable=True, name="Optical", type=SourceTypeEnum(value="spdif"), ), Source( id="netRadio", - isEnabled=True, - isPlayable=True, + is_enabled=True, + is_playable=True, name="B&O Radio", type=SourceTypeEnum(value="netRadio"), ), Source( id="deezer", - isEnabled=True, - isPlayable=True, + is_enabled=True, + is_playable=True, name="Deezer", type=SourceTypeEnum(value="deezer"), ), Source( id="tidalConnect", - isEnabled=True, - isPlayable=True, + is_enabled=True, + is_playable=True, name="Tidal Connect", type=SourceTypeEnum(value="tidalConnect"), ), diff --git a/homeassistant/components/bangolufsen/entity.py b/homeassistant/components/bangolufsen/entity.py index 374c152f91f..525439bd4f2 100644 --- a/homeassistant/components/bangolufsen/entity.py +++ b/homeassistant/components/bangolufsen/entity.py @@ -48,7 +48,7 @@ class BangOlufsenVariables: # Objects that get directly updated by notifications. self._playback_metadata: PlaybackContentMetadata = PlaybackContentMetadata() - self._playback_progress: PlaybackProgress = PlaybackProgress(totalDuration=0) + self._playback_progress: PlaybackProgress = PlaybackProgress(total_duration=0) self._playback_source: Source = Source() self._playback_state: RenderingState = RenderingState() self._source_change: Source = Source() @@ -77,3 +77,7 @@ class BangOlufsenEntity(Entity, BangOlufsenVariables): self._attr_available = connection_state self.async_write_ha_state() + + async def async_will_remove_from_hass(self) -> None: + """Close API client.""" + await self._client.close() diff --git a/homeassistant/components/bangolufsen/manifest.json b/homeassistant/components/bangolufsen/manifest.json index 763d317c1c6..cb18c6adcee 100644 --- a/homeassistant/components/bangolufsen/manifest.json +++ b/homeassistant/components/bangolufsen/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/bangolufsen", "integration_type": "device", "iot_class": "local_push", - "requirements": ["mozart-api==3.2.1.150.4"], + "requirements": ["mozart-api==3.2.1.150.5"], "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/homeassistant/components/bangolufsen/media_player.py b/homeassistant/components/bangolufsen/media_player.py index 89f10d0c71f..598e86253a4 100644 --- a/homeassistant/components/bangolufsen/media_player.py +++ b/homeassistant/components/bangolufsen/media_player.py @@ -57,6 +57,7 @@ from .const import ( CONF_BEOLINK_JID, CONF_DEFAULT_VOLUME, CONF_MAX_VOLUME, + CONNECTION_STATUS, DOMAIN, FALLBACK_SOURCES, HIDDEN_SOURCE_IDS, @@ -131,10 +132,9 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): # Misc. variables. self._audio_sources: dict[str, str] = {} self._media_image: Art = Art() - # self._queue_settings: PlayQueueSettings = PlayQueueSettings() self._software_status: SoftwareUpdateStatus = SoftwareUpdateStatus( - softwareVersion="", - state=SoftwareUpdateState(secondsRemaining=0, value="idle"), + software_version="", + state=SoftwareUpdateState(seconds_remaining=0, value="idle"), ) self._sources: dict[str, str] = {} self._state: str = MediaPlayerState.IDLE @@ -144,6 +144,14 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): """Turn on the dispatchers.""" await self._initialize() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self._unique_id}_{CONNECTION_STATUS}", + self._update_connection_state, + ) + ) + self.async_on_remove( async_dispatcher_connect( self.hass, @@ -615,10 +623,10 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): elif media_type == BANGOLUFSEN_MEDIA_TYPE.RADIO: await self._client.run_provided_scene( scene_properties=SceneProperties( - actionList=[ + action_list=[ Action( type="radio", - radioStationId=media_id, + radio_station_id=media_id, ) ] ) @@ -637,7 +645,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): # Play Deezer flow. await self._client.start_deezer_flow( - user_flow=UserFlow(userId=deezer_id) + user_flow=UserFlow(user_id=deezer_id) ) # Play a Deezer playlist or album. @@ -649,7 +657,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): await self._client.add_to_queue( play_queue_item=PlayQueueItem( provider=PlayQueueItemType(value="deezer"), - startNowFromPosition=start_from, + start_now_from_position=start_from, type="playlist", uri=media_id, ) @@ -660,7 +668,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): await self._client.add_to_queue( play_queue_item=PlayQueueItem( provider=PlayQueueItemType(value="deezer"), - startNowFromPosition=0, + start_now_from_position=0, type="track", uri=media_id, ) diff --git a/homeassistant/components/bangolufsen/strings.json b/homeassistant/components/bangolufsen/strings.json index 0b002506330..65848cf9e9d 100644 --- a/homeassistant/components/bangolufsen/strings.json +++ b/homeassistant/components/bangolufsen/strings.json @@ -3,11 +3,8 @@ "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]", - "api_exception": "An error occurred while initializing the device. Try waiting or restarting the device.", - "max_retry_error": "[%key:common::config_flow::error::timeout_connect%]", - "new_connection_error": "[%key:common::config_flow::error::cannot_connect%]", - "value_error": "[%key:common::config_flow::error::invalid_host%]", - "not_found_exception": "[%key:common::config_flow::error::cannot_connect%]" + "timeout_error": "[%key:common::config_flow::error::timeout_connect%]", + "client_connector_error": "[%key:common::config_flow::error::invalid_host%]" }, "flow_title": "{name}", "step": { @@ -18,7 +15,7 @@ "user": { "data": { "host": "[%key:common::config_flow::data::ip%]", - "model": "Device model" + "model": "[%key:common::generic::model%]" }, "description": "Manually configure your Bang & Olufsen device." } diff --git a/requirements_all.txt b/requirements_all.txt index 4480ed7a83f..ef409176c61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1270,7 +1270,7 @@ motionblinds==0.6.18 motioneye-client==0.3.14 # homeassistant.components.bangolufsen -mozart-api==3.2.1.150.4 +mozart-api==3.2.1.150.5 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2abc08bf40..f4b115b2ab8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ motionblinds==0.6.18 motioneye-client==0.3.14 # homeassistant.components.bangolufsen -mozart-api==3.2.1.150.4 +mozart-api==3.2.1.150.5 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/tests/components/bangolufsen/conftest.py b/tests/components/bangolufsen/conftest.py index 630e1c7acfb..b0b855ee249 100644 --- a/tests/components/bangolufsen/conftest.py +++ b/tests/components/bangolufsen/conftest.py @@ -23,6 +23,12 @@ from tests.common import MockConfigEntry class MockMozartClient: """Class for mocking MozartClient objects and methods.""" + async def __aenter__(self): + """Mock async context entry.""" + + async def __aexit__(self, exc_type, exc, tb): + """Mock async context exit.""" + # API call results get_beolink_self_result = BeolinkPeer( friendly_name=TEST_FRIENDLY_NAME, jid=TEST_JID_1 diff --git a/tests/components/bangolufsen/test_config_flow.py b/tests/components/bangolufsen/test_config_flow.py index d70574372a8..4b3d05ebba9 100644 --- a/tests/components/bangolufsen/test_config_flow.py +++ b/tests/components/bangolufsen/test_config_flow.py @@ -3,9 +3,8 @@ from unittest.mock import Mock -from mozart_api.exceptions import ApiException, NotFoundException +from aiohttp.client_exceptions import ClientConnectorError import pytest -from urllib3.exceptions import MaxRetryError, NewConnectionError from homeassistant.components.bangolufsen.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF @@ -25,11 +24,11 @@ from .const import ( pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_config_flow_max_retry_error( +async def test_config_flow_timeout_error( hass: HomeAssistant, mock_client: MockMozartClient ) -> None: - """Test we handle not_mozart_device.""" - mock_client.get_beolink_self.side_effect = MaxRetryError(pool=Mock(), url="") + """Test we handle timeout_error.""" + mock_client.get_beolink_self.side_effect = TimeoutError() result_user = await hass.config_entries.flow.async_init( handler=DOMAIN, @@ -37,17 +36,17 @@ async def test_config_flow_max_retry_error( data=TEST_DATA_USER, ) assert result_user["type"] == FlowResultType.ABORT - assert result_user["reason"] == "max_retry_error" + assert result_user["reason"] == "timeout_error" assert mock_client.get_beolink_self.call_count == 1 assert mock_client.get_volume_settings.call_count == 0 -async def test_config_flow_api_exception( +async def test_config_flow_client_connector_error( hass: HomeAssistant, mock_client: MockMozartClient ) -> None: - """Test we handle api_exception.""" - mock_client.get_beolink_self.side_effect = ApiException() + """Test we handle client_connector_error.""" + mock_client.get_beolink_self.side_effect = ClientConnectorError(Mock(), Mock()) result_user = await hass.config_entries.flow.async_init( handler=DOMAIN, @@ -55,44 +54,7 @@ async def test_config_flow_api_exception( data=TEST_DATA_USER, ) assert result_user["type"] == FlowResultType.ABORT - assert result_user["reason"] == "api_exception" - - assert mock_client.get_beolink_self.call_count == 1 - assert mock_client.get_volume_settings.call_count == 0 - - -async def test_config_flow_new_connection_error( - hass: HomeAssistant, mock_client: MockMozartClient -) -> None: - """Test we handle new_connection_error.""" - mock_client.get_beolink_self.side_effect = NewConnectionError(Mock(), "") - - result_user = await hass.config_entries.flow.async_init( - handler=DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - data=TEST_DATA_USER, - ) - assert result_user["type"] == FlowResultType.ABORT - assert result_user["reason"] == "new_connection_error" - - assert mock_client.get_beolink_self.call_count == 1 - assert mock_client.get_volume_settings.call_count == 0 - - -async def test_config_flow_not_found_exception( - hass: HomeAssistant, - mock_client: MockMozartClient, -) -> None: - """Test we handle not_found_exception.""" - mock_client.get_beolink_self.side_effect = NotFoundException() - - result_user = await hass.config_entries.flow.async_init( - handler=DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - data=TEST_DATA_USER, - ) - assert result_user["type"] == FlowResultType.ABORT - assert result_user["reason"] == "not_found_exception" + assert result_user["reason"] == "client_connector_error" assert mock_client.get_beolink_self.call_count == 1 assert mock_client.get_volume_settings.call_count == 0 @@ -181,28 +143,3 @@ async def test_config_flow_zeroconf_not_mozart_device(hass: HomeAssistant) -> No assert result_user["type"] == FlowResultType.ABORT assert result_user["reason"] == "not_mozart_device" - - -# async def test_config_flow_options(hass: HomeAssistant, mock_config_entry) -> None: -# """Test config flow options.""" - -# mock_config_entry.add_to_hass(hass) - -# assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - -# result_user = await hass.config_entries.options.async_init( -# mock_config_entry.entry_id -# ) - -# assert result_user["type"] == FlowResultType.FORM -# assert result_user["step_id"] == "init" - -# result_confirm = await hass.config_entries.options.async_configure( -# flow_id=result_user["flow_id"], -# user_input=TEST_DATA_OPTIONS, -# ) - -# assert result_confirm["type"] == FlowResultType.CREATE_ENTRY -# new_data = TEST_DATA_CONFIRM -# new_data.update(TEST_DATA_OPTIONS) -# assert result_confirm["data"] == new_data