mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Handle API rate limit error on Home Connect entities fetch (#139384)
* Handle API rate limit error on entities fetch * Apply suggestions Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Add decorator (does not work) * Fix decorator * Apply suggestions Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Add test --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
65aef40a3f
commit
43e24cf833
@ -10,6 +10,7 @@ from .utils import bsh_key_to_translation_key
|
||||
|
||||
DOMAIN = "home_connect"
|
||||
|
||||
API_DEFAULT_RETRY_AFTER = 60
|
||||
|
||||
APPLIANCES_WITH_PROGRAMS = (
|
||||
"CleaningRobot",
|
||||
|
@ -1,21 +1,28 @@
|
||||
"""Home Connect entity base class."""
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Coroutine
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import cast
|
||||
from typing import Any, Concatenate, cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, OptionKey
|
||||
from aiohomeconnect.model.error import ActiveProgramNotSetError, HomeConnectError
|
||||
from aiohomeconnect.model.error import (
|
||||
ActiveProgramNotSetError,
|
||||
HomeConnectError,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import API_DEFAULT_RETRY_AFTER, DOMAIN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
@ -127,3 +134,34 @@ class HomeConnectOptionEntity(HomeConnectEntity):
|
||||
def bsh_key(self) -> OptionKey:
|
||||
"""Return the BSH key."""
|
||||
return cast(OptionKey, self.entity_description.key)
|
||||
|
||||
|
||||
def constraint_fetcher[_EntityT: HomeConnectEntity, **_P](
|
||||
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
|
||||
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||
"""Decorate the function to catch Home Connect too many requests error and retry later.
|
||||
|
||||
If it needs to be called later, it will call async_write_ha_state function
|
||||
"""
|
||||
|
||||
async def handler_to_return(
|
||||
self: _EntityT, *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> None:
|
||||
async def handler(_datetime: datetime | None = None) -> None:
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except TooManyRequestsError as err:
|
||||
if (retry_after := err.retry_after) is None:
|
||||
retry_after = API_DEFAULT_RETRY_AFTER
|
||||
async_call_later(self.hass, retry_after, handler)
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error(
|
||||
"Error fetching constraints for %s: %s", self.entity_id, err
|
||||
)
|
||||
else:
|
||||
if _datetime is not None:
|
||||
self.async_write_ha_state()
|
||||
|
||||
await handler()
|
||||
|
||||
return handler_to_return
|
||||
|
@ -25,7 +25,7 @@ from .const import (
|
||||
UNIT_MAP,
|
||||
)
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -189,19 +189,25 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
||||
},
|
||||
) from err
|
||||
|
||||
@constraint_fetcher
|
||||
async def async_fetch_constraints(self) -> None:
|
||||
"""Fetch the max and min values and step for the number entity."""
|
||||
try:
|
||||
setting_key = cast(SettingKey, self.bsh_key)
|
||||
data = self.appliance.settings.get(setting_key)
|
||||
if not data or not data.unit or not data.constraints:
|
||||
data = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id, setting_key=SettingKey(self.bsh_key)
|
||||
self.appliance.info.ha_id, setting_key=setting_key
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error("An error occurred: %s", err)
|
||||
else:
|
||||
if data.unit:
|
||||
self._attr_native_unit_of_measurement = data.unit
|
||||
self.set_constraints(data)
|
||||
|
||||
def set_constraints(self, setting: GetSetting) -> None:
|
||||
"""Set constraints for the number entity."""
|
||||
if setting.unit:
|
||||
self._attr_native_unit_of_measurement = UNIT_MAP.get(
|
||||
setting.unit, setting.unit
|
||||
)
|
||||
if not (constraints := setting.constraints):
|
||||
return
|
||||
if constraints.max:
|
||||
@ -222,10 +228,10 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
|
||||
self._attr_native_unit_of_measurement = data.unit
|
||||
self.set_constraints(data)
|
||||
if (
|
||||
not hasattr(self, "_attr_native_min_value")
|
||||
not hasattr(self, "_attr_native_unit_of_measurement")
|
||||
or not hasattr(self, "_attr_native_min_value")
|
||||
or not hasattr(self, "_attr_native_max_value")
|
||||
or not hasattr(self, "_attr_native_step")
|
||||
):
|
||||
@ -253,7 +259,6 @@ class HomeConnectOptionNumberEntity(HomeConnectOptionEntity, NumberEntity):
|
||||
or candidate_unit != self._attr_native_unit_of_measurement
|
||||
):
|
||||
self._attr_native_unit_of_measurement = candidate_unit
|
||||
self.__dict__.pop("unit_of_measurement", None)
|
||||
option_constraints = option_definition.constraints
|
||||
if option_constraints:
|
||||
if (
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""Provides a select platform for Home Connect."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
@ -47,9 +47,11 @@ from .coordinator import (
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
|
||||
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = {
|
||||
@ -458,17 +460,21 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self.async_fetch_options()
|
||||
|
||||
@constraint_fetcher
|
||||
async def async_fetch_options(self) -> None:
|
||||
"""Fetch options from the API."""
|
||||
setting = self.appliance.settings.get(cast(SettingKey, self.bsh_key))
|
||||
if (
|
||||
not setting
|
||||
or not setting.constraints
|
||||
or not setting.constraints.allowed_values
|
||||
):
|
||||
with contextlib.suppress(HomeConnectError):
|
||||
setting = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=cast(SettingKey, self.bsh_key),
|
||||
)
|
||||
setting = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=cast(SettingKey, self.bsh_key),
|
||||
)
|
||||
|
||||
if setting and setting.constraints and setting.constraints.allowed_values:
|
||||
self._attr_options = [
|
||||
|
@ -1,12 +1,11 @@
|
||||
"""Provides a sensor for Home Connect."""
|
||||
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, StatusKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@ -28,7 +27,9 @@ from .const import (
|
||||
UNIT_MAP,
|
||||
)
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity
|
||||
from .entity import HomeConnectEntity, constraint_fetcher
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@ -335,16 +336,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
|
||||
else:
|
||||
await self.fetch_unit()
|
||||
|
||||
@constraint_fetcher
|
||||
async def fetch_unit(self) -> None:
|
||||
"""Fetch the unit of measurement."""
|
||||
with contextlib.suppress(HomeConnectError):
|
||||
data = await self.coordinator.client.get_status_value(
|
||||
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
|
||||
)
|
||||
if data.unit:
|
||||
self._attr_native_unit_of_measurement = UNIT_MAP.get(
|
||||
data.unit, data.unit
|
||||
)
|
||||
data = await self.coordinator.client.get_status_value(
|
||||
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
|
||||
)
|
||||
if data.unit:
|
||||
self._attr_native_unit_of_measurement = UNIT_MAP.get(data.unit, data.unit)
|
||||
|
||||
|
||||
class HomeConnectProgramSensor(HomeConnectSensor):
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
import random
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfEvents,
|
||||
@ -22,6 +22,7 @@ from aiohomeconnect.model.error import (
|
||||
HomeConnectApiError,
|
||||
HomeConnectError,
|
||||
SelectedProgramNotSetError,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
from aiohomeconnect.model.program import (
|
||||
ProgramDefinitionConstraints,
|
||||
@ -47,7 +48,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -340,6 +341,98 @@ async def test_number_entity_functionality(
|
||||
assert hass.states.is_state(entity_id, str(float(value)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance_ha_id", ["FridgeFreezer"], indirect=True)
|
||||
@pytest.mark.parametrize("retry_after", [0, None])
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"setting_key",
|
||||
"type",
|
||||
"min_value",
|
||||
"max_value",
|
||||
"step_size",
|
||||
"unit_of_measurement",
|
||||
),
|
||||
[
|
||||
(
|
||||
f"{NUMBER_DOMAIN.lower()}.fridgefreezer_refrigerator_temperature",
|
||||
SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
|
||||
"Double",
|
||||
7,
|
||||
15,
|
||||
5,
|
||||
"°C",
|
||||
),
|
||||
],
|
||||
)
|
||||
@patch("homeassistant.components.home_connect.entity.API_DEFAULT_RETRY_AFTER", new=0)
|
||||
async def test_fetch_constraints_after_rate_limit_error(
|
||||
retry_after: int | None,
|
||||
appliance_ha_id: str,
|
||||
entity_id: str,
|
||||
setting_key: SettingKey,
|
||||
type: str,
|
||||
min_value: int,
|
||||
max_value: int,
|
||||
step_size: int,
|
||||
unit_of_measurement: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that, if a API rate limit error is raised, the constraints are fetched later."""
|
||||
|
||||
def get_settings_side_effect(ha_id: str):
|
||||
if ha_id != appliance_ha_id:
|
||||
return ArrayOfSettings([])
|
||||
return ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value=random.randint(min_value, max_value),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
|
||||
client.get_setting = AsyncMock(
|
||||
side_effect=[
|
||||
TooManyRequestsError("error.key", retry_after=retry_after),
|
||||
GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value=random.randint(min_value, max_value),
|
||||
unit=unit_of_measurement,
|
||||
type=type,
|
||||
constraints=SettingConstraints(
|
||||
min=min_value,
|
||||
max=max_value,
|
||||
step_size=step_size,
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert client.get_setting.call_count == 2
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
attributes = entity_state.attributes
|
||||
assert attributes["min"] == min_value
|
||||
assert attributes["max"] == max_value
|
||||
assert attributes["step"] == step_size
|
||||
assert attributes["unit_of_measurement"] == unit_of_measurement
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "setting_key", "mock_attr"),
|
||||
[
|
||||
|
@ -21,6 +21,7 @@ from aiohomeconnect.model.error import (
|
||||
ActiveProgramNotSetError,
|
||||
HomeConnectError,
|
||||
SelectedProgramNotSetError,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
from aiohomeconnect.model.program import (
|
||||
EnumerateProgram,
|
||||
@ -50,7 +51,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -566,6 +567,139 @@ async def test_fetch_allowed_values(
|
||||
assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance_ha_id", ["Hood"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"setting_key",
|
||||
"allowed_values",
|
||||
"expected_options",
|
||||
),
|
||||
[
|
||||
(
|
||||
"select.hood_ambient_light_color",
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR,
|
||||
[f"BSH.Common.EnumType.AmbientLightColor.Color{i}" for i in range(50)],
|
||||
{str(i) for i in range(1, 50)},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_fetch_allowed_values_after_rate_limit_error(
|
||||
appliance_ha_id: str,
|
||||
entity_id: str,
|
||||
setting_key: SettingKey,
|
||||
allowed_values: list[str | None],
|
||||
expected_options: set[str],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test fetch allowed values."""
|
||||
|
||||
def get_settings_side_effect(ha_id: str):
|
||||
if ha_id != appliance_ha_id:
|
||||
return ArrayOfSettings([])
|
||||
return ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value="", # Not important
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
|
||||
client.get_setting = AsyncMock(
|
||||
side_effect=[
|
||||
TooManyRequestsError("error.key", retry_after=0),
|
||||
GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value="", # Not important
|
||||
constraints=SettingConstraints(
|
||||
allowed_values=allowed_values,
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert client.get_setting.call_count == 2
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance_ha_id", ["Hood"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"setting_key",
|
||||
"exception",
|
||||
"expected_options",
|
||||
),
|
||||
[
|
||||
(
|
||||
"select.hood_ambient_light_color",
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR,
|
||||
HomeConnectError(),
|
||||
{
|
||||
"b_s_h_common_enum_type_ambient_light_color_custom_color",
|
||||
*{str(i) for i in range(1, 100)},
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_default_values_after_fetch_allowed_values_error(
|
||||
appliance_ha_id: str,
|
||||
entity_id: str,
|
||||
setting_key: SettingKey,
|
||||
exception: Exception,
|
||||
expected_options: set[str],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test fetch allowed values."""
|
||||
|
||||
def get_settings_side_effect(ha_id: str):
|
||||
if ha_id != appliance_ha_id:
|
||||
return ArrayOfSettings([])
|
||||
return ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value="", # Not important
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
|
||||
client.get_setting = AsyncMock(side_effect=exception)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert client.get_setting.call_count == 1
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "setting_key", "allowed_value", "value_to_set", "mock_attr"),
|
||||
[
|
||||
|
@ -13,7 +13,7 @@ from aiohomeconnect.model import (
|
||||
Status,
|
||||
StatusKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import HomeConnectApiError
|
||||
from aiohomeconnect.model.error import HomeConnectApiError, TooManyRequestsError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
@ -26,12 +26,13 @@ from homeassistant.components.home_connect.const import (
|
||||
BSH_EVENT_PRESENT_STATE_PRESENT,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.home_connect.coordinator import HomeConnectError
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
TEST_HC_APP = "Dishwasher"
|
||||
|
||||
@ -724,3 +725,122 @@ async def test_sensor_unit_fetching(
|
||||
)
|
||||
|
||||
assert client.get_status_value.call_count == get_status_value_call_count
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"appliance_ha_id",
|
||||
"entity_id",
|
||||
"status_key",
|
||||
),
|
||||
[
|
||||
(
|
||||
"Oven",
|
||||
"sensor.oven_current_oven_cavity_temperature",
|
||||
StatusKey.COOKING_OVEN_CURRENT_CAVITY_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_sensor_unit_fetching_error(
|
||||
appliance_ha_id: str,
|
||||
entity_id: str,
|
||||
status_key: StatusKey,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the sensor entities are capable of fetching units."""
|
||||
|
||||
async def get_status_mock(ha_id: str) -> ArrayOfStatus:
|
||||
if ha_id != appliance_ha_id:
|
||||
return ArrayOfStatus([])
|
||||
return ArrayOfStatus(
|
||||
[
|
||||
Status(
|
||||
key=status_key,
|
||||
raw_key=status_key.value,
|
||||
value=0,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
client.get_status = AsyncMock(side_effect=get_status_mock)
|
||||
client.get_status_value = AsyncMock(side_effect=HomeConnectError())
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert hass.states.get(entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"appliance_ha_id",
|
||||
"entity_id",
|
||||
"status_key",
|
||||
"unit",
|
||||
),
|
||||
[
|
||||
(
|
||||
"Oven",
|
||||
"sensor.oven_current_oven_cavity_temperature",
|
||||
StatusKey.COOKING_OVEN_CURRENT_CAVITY_TEMPERATURE,
|
||||
"°C",
|
||||
),
|
||||
],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_sensor_unit_fetching_after_rate_limit_error(
|
||||
appliance_ha_id: str,
|
||||
entity_id: str,
|
||||
status_key: StatusKey,
|
||||
unit: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the sensor entities are capable of fetching units."""
|
||||
|
||||
async def get_status_mock(ha_id: str) -> ArrayOfStatus:
|
||||
if ha_id != appliance_ha_id:
|
||||
return ArrayOfStatus([])
|
||||
return ArrayOfStatus(
|
||||
[
|
||||
Status(
|
||||
key=status_key,
|
||||
raw_key=status_key.value,
|
||||
value=0,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
client.get_status = AsyncMock(side_effect=get_status_mock)
|
||||
client.get_status_value = AsyncMock(
|
||||
side_effect=[
|
||||
TooManyRequestsError("error.key", retry_after=0),
|
||||
Status(
|
||||
key=status_key,
|
||||
raw_key=status_key.value,
|
||||
value=0,
|
||||
unit=unit,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert client.get_status_value.call_count == 2
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.attributes["unit_of_measurement"] == unit
|
||||
|
Loading…
x
Reference in New Issue
Block a user