Update integration with feedback from emontnemery

This commit is contained in:
mj23000
2023-12-01 21:00:50 +01:00
parent 6061171bb4
commit feb1e7301d
11 changed files with 77 additions and 131 deletions

View File

@@ -4,17 +4,15 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from mozart_api.exceptions import ServiceException from aiohttp.client_exceptions import ClientConnectorError
from mozart_api.mozart_client import MozartClient from mozart_api.mozart_client import MozartClient
from urllib3.exceptions import MaxRetryError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady 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 from .websocket import BangOlufsenWebsocket
PLATFORMS = [Platform.MEDIA_PLAYER] 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. # Check connection and try to initialize it.
try: async with client:
await client.get_battery_state(_request_timeout=3) try:
except (MaxRetryError, ServiceException, Exception) as error: await client.get_battery_state(_request_timeout=3)
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error except (TimeoutError, ClientConnectorError, Exception) as error:
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
websocket = BangOlufsenWebsocket(hass, entry) 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) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BangOlufsenData(websocket)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 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 return True

View File

@@ -5,9 +5,8 @@ import ipaddress
import logging import logging
from typing import Any, TypedDict 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 mozart_api.mozart_client import MozartClient
from urllib3.exceptions import MaxRetryError, NewConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo
@@ -62,7 +61,8 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
async def _compile_data(self) -> UserInput: async def _compile_data(self) -> UserInput:
"""Compile data for entry creation.""" """Compile data for entry creation."""
# Get current volume settings # 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 # Create a dict containing all necessary information for setup
data = UserInput() data = UserInput()
@@ -94,7 +94,6 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
# Check if the IP address is a valid address. # Check if the IP address is a valid address.
try: try:
ipaddress.ip_address(self._host) ipaddress.ip_address(self._host)
except ValueError: except ValueError:
return self.async_abort(reason="value_error") 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 to get information from Beolink self method.
try: 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 ( except (TimeoutError, ClientConnectorError) as error:
ApiException,
NewConnectionError,
MaxRetryError,
NotFoundException,
) as error:
return self.async_abort( return self.async_abort(
reason={ reason={
ApiException: "api_exception", TimeoutError: "timeout_error",
NewConnectionError: "new_connection_error", ClientConnectorError: "client_connector_error",
MaxRetryError: "max_retry_error",
NotFoundException: "not_found_exception",
}[type(error)] }[type(error)]
) )

View File

@@ -148,7 +148,6 @@ HIDDEN_SOURCE_IDS: Final[tuple] = (
"wpl", "wpl",
"pl", "pl",
"beolink", "beolink",
"classicsAdapter",
"usbIn", "usbIn",
) )
@@ -157,57 +156,57 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
items=[ items=[
Source( Source(
id="uriStreamer", id="uriStreamer",
isEnabled=True, is_enabled=True,
isPlayable=False, is_playable=False,
name="Audio Streamer", name="Audio Streamer",
type=SourceTypeEnum(value="uriStreamer"), type=SourceTypeEnum(value="uriStreamer"),
), ),
Source( Source(
id="bluetooth", id="bluetooth",
isEnabled=True, is_enabled=True,
isPlayable=False, is_playable=False,
name="Bluetooth", name="Bluetooth",
type=SourceTypeEnum(value="bluetooth"), type=SourceTypeEnum(value="bluetooth"),
), ),
Source( Source(
id="spotify", id="spotify",
isEnabled=True, is_enabled=True,
isPlayable=False, is_playable=False,
name="Spotify Connect", name="Spotify Connect",
type=SourceTypeEnum(value="spotify"), type=SourceTypeEnum(value="spotify"),
), ),
Source( Source(
id="lineIn", id="lineIn",
isEnabled=True, is_enabled=True,
isPlayable=True, is_playable=True,
name="Line-In", name="Line-In",
type=SourceTypeEnum(value="lineIn"), type=SourceTypeEnum(value="lineIn"),
), ),
Source( Source(
id="spdif", id="spdif",
isEnabled=True, is_enabled=True,
isPlayable=True, is_playable=True,
name="Optical", name="Optical",
type=SourceTypeEnum(value="spdif"), type=SourceTypeEnum(value="spdif"),
), ),
Source( Source(
id="netRadio", id="netRadio",
isEnabled=True, is_enabled=True,
isPlayable=True, is_playable=True,
name="B&O Radio", name="B&O Radio",
type=SourceTypeEnum(value="netRadio"), type=SourceTypeEnum(value="netRadio"),
), ),
Source( Source(
id="deezer", id="deezer",
isEnabled=True, is_enabled=True,
isPlayable=True, is_playable=True,
name="Deezer", name="Deezer",
type=SourceTypeEnum(value="deezer"), type=SourceTypeEnum(value="deezer"),
), ),
Source( Source(
id="tidalConnect", id="tidalConnect",
isEnabled=True, is_enabled=True,
isPlayable=True, is_playable=True,
name="Tidal Connect", name="Tidal Connect",
type=SourceTypeEnum(value="tidalConnect"), type=SourceTypeEnum(value="tidalConnect"),
), ),

View File

@@ -48,7 +48,7 @@ class BangOlufsenVariables:
# Objects that get directly updated by notifications. # Objects that get directly updated by notifications.
self._playback_metadata: PlaybackContentMetadata = PlaybackContentMetadata() 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_source: Source = Source()
self._playback_state: RenderingState = RenderingState() self._playback_state: RenderingState = RenderingState()
self._source_change: Source = Source() self._source_change: Source = Source()
@@ -77,3 +77,7 @@ class BangOlufsenEntity(Entity, BangOlufsenVariables):
self._attr_available = connection_state self._attr_available = connection_state
self.async_write_ha_state() self.async_write_ha_state()
async def async_will_remove_from_hass(self) -> None:
"""Close API client."""
await self._client.close()

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/bangolufsen", "documentation": "https://www.home-assistant.io/integrations/bangolufsen",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["mozart-api==3.2.1.150.4"], "requirements": ["mozart-api==3.2.1.150.5"],
"zeroconf": ["_bangolufsen._tcp.local."] "zeroconf": ["_bangolufsen._tcp.local."]
} }

View File

@@ -57,6 +57,7 @@ from .const import (
CONF_BEOLINK_JID, CONF_BEOLINK_JID,
CONF_DEFAULT_VOLUME, CONF_DEFAULT_VOLUME,
CONF_MAX_VOLUME, CONF_MAX_VOLUME,
CONNECTION_STATUS,
DOMAIN, DOMAIN,
FALLBACK_SOURCES, FALLBACK_SOURCES,
HIDDEN_SOURCE_IDS, HIDDEN_SOURCE_IDS,
@@ -131,10 +132,9 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
# Misc. variables. # Misc. variables.
self._audio_sources: dict[str, str] = {} self._audio_sources: dict[str, str] = {}
self._media_image: Art = Art() self._media_image: Art = Art()
# self._queue_settings: PlayQueueSettings = PlayQueueSettings()
self._software_status: SoftwareUpdateStatus = SoftwareUpdateStatus( self._software_status: SoftwareUpdateStatus = SoftwareUpdateStatus(
softwareVersion="", software_version="",
state=SoftwareUpdateState(secondsRemaining=0, value="idle"), state=SoftwareUpdateState(seconds_remaining=0, value="idle"),
) )
self._sources: dict[str, str] = {} self._sources: dict[str, str] = {}
self._state: str = MediaPlayerState.IDLE self._state: str = MediaPlayerState.IDLE
@@ -144,6 +144,14 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
"""Turn on the dispatchers.""" """Turn on the dispatchers."""
await self._initialize() 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( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,
@@ -615,10 +623,10 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
elif media_type == BANGOLUFSEN_MEDIA_TYPE.RADIO: elif media_type == BANGOLUFSEN_MEDIA_TYPE.RADIO:
await self._client.run_provided_scene( await self._client.run_provided_scene(
scene_properties=SceneProperties( scene_properties=SceneProperties(
actionList=[ action_list=[
Action( Action(
type="radio", type="radio",
radioStationId=media_id, radio_station_id=media_id,
) )
] ]
) )
@@ -637,7 +645,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
# Play Deezer flow. # Play Deezer flow.
await self._client.start_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. # Play a Deezer playlist or album.
@@ -649,7 +657,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
await self._client.add_to_queue( await self._client.add_to_queue(
play_queue_item=PlayQueueItem( play_queue_item=PlayQueueItem(
provider=PlayQueueItemType(value="deezer"), provider=PlayQueueItemType(value="deezer"),
startNowFromPosition=start_from, start_now_from_position=start_from,
type="playlist", type="playlist",
uri=media_id, uri=media_id,
) )
@@ -660,7 +668,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity):
await self._client.add_to_queue( await self._client.add_to_queue(
play_queue_item=PlayQueueItem( play_queue_item=PlayQueueItem(
provider=PlayQueueItemType(value="deezer"), provider=PlayQueueItemType(value="deezer"),
startNowFromPosition=0, start_now_from_position=0,
type="track", type="track",
uri=media_id, uri=media_id,
) )

View File

@@ -3,11 +3,8 @@
"abort": { "abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]", "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.", "timeout_error": "[%key:common::config_flow::error::timeout_connect%]",
"max_retry_error": "[%key:common::config_flow::error::timeout_connect%]", "client_connector_error": "[%key:common::config_flow::error::invalid_host%]"
"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%]"
}, },
"flow_title": "{name}", "flow_title": "{name}",
"step": { "step": {
@@ -18,7 +15,7 @@
"user": { "user": {
"data": { "data": {
"host": "[%key:common::config_flow::data::ip%]", "host": "[%key:common::config_flow::data::ip%]",
"model": "Device model" "model": "[%key:common::generic::model%]"
}, },
"description": "Manually configure your Bang & Olufsen device." "description": "Manually configure your Bang & Olufsen device."
} }

View File

@@ -1270,7 +1270,7 @@ motionblinds==0.6.18
motioneye-client==0.3.14 motioneye-client==0.3.14
# homeassistant.components.bangolufsen # homeassistant.components.bangolufsen
mozart-api==3.2.1.150.4 mozart-api==3.2.1.150.5
# homeassistant.components.mullvad # homeassistant.components.mullvad
mullvad-api==1.0.0 mullvad-api==1.0.0

View File

@@ -994,7 +994,7 @@ motionblinds==0.6.18
motioneye-client==0.3.14 motioneye-client==0.3.14
# homeassistant.components.bangolufsen # homeassistant.components.bangolufsen
mozart-api==3.2.1.150.4 mozart-api==3.2.1.150.5
# homeassistant.components.mullvad # homeassistant.components.mullvad
mullvad-api==1.0.0 mullvad-api==1.0.0

View File

@@ -23,6 +23,12 @@ from tests.common import MockConfigEntry
class MockMozartClient: class MockMozartClient:
"""Class for mocking MozartClient objects and methods.""" """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 # API call results
get_beolink_self_result = BeolinkPeer( get_beolink_self_result = BeolinkPeer(
friendly_name=TEST_FRIENDLY_NAME, jid=TEST_JID_1 friendly_name=TEST_FRIENDLY_NAME, jid=TEST_JID_1

View File

@@ -3,9 +3,8 @@
from unittest.mock import Mock from unittest.mock import Mock
from mozart_api.exceptions import ApiException, NotFoundException from aiohttp.client_exceptions import ClientConnectorError
import pytest import pytest
from urllib3.exceptions import MaxRetryError, NewConnectionError
from homeassistant.components.bangolufsen.const import DOMAIN from homeassistant.components.bangolufsen.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
@@ -25,11 +24,11 @@ from .const import (
pytestmark = pytest.mark.usefixtures("mock_setup_entry") 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 hass: HomeAssistant, mock_client: MockMozartClient
) -> None: ) -> None:
"""Test we handle not_mozart_device.""" """Test we handle timeout_error."""
mock_client.get_beolink_self.side_effect = MaxRetryError(pool=Mock(), url="") mock_client.get_beolink_self.side_effect = TimeoutError()
result_user = await hass.config_entries.flow.async_init( result_user = await hass.config_entries.flow.async_init(
handler=DOMAIN, handler=DOMAIN,
@@ -37,17 +36,17 @@ async def test_config_flow_max_retry_error(
data=TEST_DATA_USER, data=TEST_DATA_USER,
) )
assert result_user["type"] == FlowResultType.ABORT 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_beolink_self.call_count == 1
assert mock_client.get_volume_settings.call_count == 0 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 hass: HomeAssistant, mock_client: MockMozartClient
) -> None: ) -> None:
"""Test we handle api_exception.""" """Test we handle client_connector_error."""
mock_client.get_beolink_self.side_effect = ApiException() mock_client.get_beolink_self.side_effect = ClientConnectorError(Mock(), Mock())
result_user = await hass.config_entries.flow.async_init( result_user = await hass.config_entries.flow.async_init(
handler=DOMAIN, handler=DOMAIN,
@@ -55,44 +54,7 @@ async def test_config_flow_api_exception(
data=TEST_DATA_USER, data=TEST_DATA_USER,
) )
assert result_user["type"] == FlowResultType.ABORT assert result_user["type"] == FlowResultType.ABORT
assert result_user["reason"] == "api_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
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 mock_client.get_beolink_self.call_count == 1 assert mock_client.get_beolink_self.call_count == 1
assert mock_client.get_volume_settings.call_count == 0 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["type"] == FlowResultType.ABORT
assert result_user["reason"] == "not_mozart_device" 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