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
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

View File

@@ -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)]
)

View File

@@ -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"),
),

View File

@@ -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()

View File

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

View File

@@ -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,
)

View File

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

View File

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

View File

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

View File

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

View File

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