mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 03:37:07 +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"
|
DOMAIN = "home_connect"
|
||||||
|
|
||||||
|
API_DEFAULT_RETRY_AFTER = 60
|
||||||
|
|
||||||
APPLIANCES_WITH_PROGRAMS = (
|
APPLIANCES_WITH_PROGRAMS = (
|
||||||
"CleaningRobot",
|
"CleaningRobot",
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
"""Home Connect entity base class."""
|
"""Home Connect entity base class."""
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import cast
|
from typing import Any, Concatenate, cast
|
||||||
|
|
||||||
from aiohomeconnect.model import EventKey, OptionKey
|
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.const import STATE_UNAVAILABLE
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
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 .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
|
||||||
from .utils import get_dict_from_home_connect_error
|
from .utils import get_dict_from_home_connect_error
|
||||||
|
|
||||||
@ -127,3 +134,34 @@ class HomeConnectOptionEntity(HomeConnectEntity):
|
|||||||
def bsh_key(self) -> OptionKey:
|
def bsh_key(self) -> OptionKey:
|
||||||
"""Return the BSH key."""
|
"""Return the BSH key."""
|
||||||
return cast(OptionKey, self.entity_description.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,
|
UNIT_MAP,
|
||||||
)
|
)
|
||||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
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
|
from .utils import get_dict_from_home_connect_error
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -189,19 +189,25 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
|||||||
},
|
},
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
@constraint_fetcher
|
||||||
async def async_fetch_constraints(self) -> None:
|
async def async_fetch_constraints(self) -> None:
|
||||||
"""Fetch the max and min values and step for the number entity."""
|
"""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(
|
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:
|
if data.unit:
|
||||||
_LOGGER.error("An error occurred: %s", err)
|
self._attr_native_unit_of_measurement = data.unit
|
||||||
else:
|
|
||||||
self.set_constraints(data)
|
self.set_constraints(data)
|
||||||
|
|
||||||
def set_constraints(self, setting: GetSetting) -> None:
|
def set_constraints(self, setting: GetSetting) -> None:
|
||||||
"""Set constraints for the number entity."""
|
"""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):
|
if not (constraints := setting.constraints):
|
||||||
return
|
return
|
||||||
if constraints.max:
|
if constraints.max:
|
||||||
@ -222,10 +228,10 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
|||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
|
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
|
||||||
self._attr_native_unit_of_measurement = data.unit
|
|
||||||
self.set_constraints(data)
|
self.set_constraints(data)
|
||||||
if (
|
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_max_value")
|
||||||
or not hasattr(self, "_attr_native_step")
|
or not hasattr(self, "_attr_native_step")
|
||||||
):
|
):
|
||||||
@ -253,7 +259,6 @@ class HomeConnectOptionNumberEntity(HomeConnectOptionEntity, NumberEntity):
|
|||||||
or candidate_unit != self._attr_native_unit_of_measurement
|
or candidate_unit != self._attr_native_unit_of_measurement
|
||||||
):
|
):
|
||||||
self._attr_native_unit_of_measurement = candidate_unit
|
self._attr_native_unit_of_measurement = candidate_unit
|
||||||
self.__dict__.pop("unit_of_measurement", None)
|
|
||||||
option_constraints = option_definition.constraints
|
option_constraints = option_definition.constraints
|
||||||
if option_constraints:
|
if option_constraints:
|
||||||
if (
|
if (
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"""Provides a select platform for Home Connect."""
|
"""Provides a select platform for Home Connect."""
|
||||||
|
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
import contextlib
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from aiohomeconnect.client import Client as HomeConnectClient
|
from aiohomeconnect.client import Client as HomeConnectClient
|
||||||
@ -47,9 +47,11 @@ from .coordinator import (
|
|||||||
HomeConnectConfigEntry,
|
HomeConnectConfigEntry,
|
||||||
HomeConnectCoordinator,
|
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
|
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = {
|
FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = {
|
||||||
@ -458,17 +460,21 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
|
|||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
await super().async_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))
|
setting = self.appliance.settings.get(cast(SettingKey, self.bsh_key))
|
||||||
if (
|
if (
|
||||||
not setting
|
not setting
|
||||||
or not setting.constraints
|
or not setting.constraints
|
||||||
or not setting.constraints.allowed_values
|
or not setting.constraints.allowed_values
|
||||||
):
|
):
|
||||||
with contextlib.suppress(HomeConnectError):
|
setting = await self.coordinator.client.get_setting(
|
||||||
setting = await self.coordinator.client.get_setting(
|
self.appliance.info.ha_id,
|
||||||
self.appliance.info.ha_id,
|
setting_key=cast(SettingKey, self.bsh_key),
|
||||||
setting_key=cast(SettingKey, self.bsh_key),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if setting and setting.constraints and setting.constraints.allowed_values:
|
if setting and setting.constraints and setting.constraints.allowed_values:
|
||||||
self._attr_options = [
|
self._attr_options = [
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
"""Provides a sensor for Home Connect."""
|
"""Provides a sensor for Home Connect."""
|
||||||
|
|
||||||
import contextlib
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from aiohomeconnect.model import EventKey, StatusKey
|
from aiohomeconnect.model import EventKey, StatusKey
|
||||||
from aiohomeconnect.model.error import HomeConnectError
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -28,7 +27,9 @@ from .const import (
|
|||||||
UNIT_MAP,
|
UNIT_MAP,
|
||||||
)
|
)
|
||||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||||
from .entity import HomeConnectEntity
|
from .entity import HomeConnectEntity, constraint_fetcher
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@ -335,16 +336,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
|
|||||||
else:
|
else:
|
||||||
await self.fetch_unit()
|
await self.fetch_unit()
|
||||||
|
|
||||||
|
@constraint_fetcher
|
||||||
async def fetch_unit(self) -> None:
|
async def fetch_unit(self) -> None:
|
||||||
"""Fetch the unit of measurement."""
|
"""Fetch the unit of measurement."""
|
||||||
with contextlib.suppress(HomeConnectError):
|
data = await self.coordinator.client.get_status_value(
|
||||||
data = await self.coordinator.client.get_status_value(
|
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
|
||||||
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
|
)
|
||||||
)
|
if data.unit:
|
||||||
if data.unit:
|
self._attr_native_unit_of_measurement = UNIT_MAP.get(data.unit, data.unit)
|
||||||
self._attr_native_unit_of_measurement = UNIT_MAP.get(
|
|
||||||
data.unit, data.unit
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HomeConnectProgramSensor(HomeConnectSensor):
|
class HomeConnectProgramSensor(HomeConnectSensor):
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import random
|
import random
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from aiohomeconnect.model import (
|
from aiohomeconnect.model import (
|
||||||
ArrayOfEvents,
|
ArrayOfEvents,
|
||||||
@ -22,6 +22,7 @@ from aiohomeconnect.model.error import (
|
|||||||
HomeConnectApiError,
|
HomeConnectApiError,
|
||||||
HomeConnectError,
|
HomeConnectError,
|
||||||
SelectedProgramNotSetError,
|
SelectedProgramNotSetError,
|
||||||
|
TooManyRequestsError,
|
||||||
)
|
)
|
||||||
from aiohomeconnect.model.program import (
|
from aiohomeconnect.model.program import (
|
||||||
ProgramDefinitionConstraints,
|
ProgramDefinitionConstraints,
|
||||||
@ -47,7 +48,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
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
|
@pytest.fixture
|
||||||
@ -340,6 +341,98 @@ async def test_number_entity_functionality(
|
|||||||
assert hass.states.is_state(entity_id, str(float(value)))
|
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(
|
@pytest.mark.parametrize(
|
||||||
("entity_id", "setting_key", "mock_attr"),
|
("entity_id", "setting_key", "mock_attr"),
|
||||||
[
|
[
|
||||||
|
@ -21,6 +21,7 @@ from aiohomeconnect.model.error import (
|
|||||||
ActiveProgramNotSetError,
|
ActiveProgramNotSetError,
|
||||||
HomeConnectError,
|
HomeConnectError,
|
||||||
SelectedProgramNotSetError,
|
SelectedProgramNotSetError,
|
||||||
|
TooManyRequestsError,
|
||||||
)
|
)
|
||||||
from aiohomeconnect.model.program import (
|
from aiohomeconnect.model.program import (
|
||||||
EnumerateProgram,
|
EnumerateProgram,
|
||||||
@ -50,7 +51,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
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
|
@pytest.fixture
|
||||||
@ -566,6 +567,139 @@ async def test_fetch_allowed_values(
|
|||||||
assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options
|
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(
|
@pytest.mark.parametrize(
|
||||||
("entity_id", "setting_key", "allowed_value", "value_to_set", "mock_attr"),
|
("entity_id", "setting_key", "allowed_value", "value_to_set", "mock_attr"),
|
||||||
[
|
[
|
||||||
|
@ -13,7 +13,7 @@ from aiohomeconnect.model import (
|
|||||||
Status,
|
Status,
|
||||||
StatusKey,
|
StatusKey,
|
||||||
)
|
)
|
||||||
from aiohomeconnect.model.error import HomeConnectApiError
|
from aiohomeconnect.model.error import HomeConnectApiError, TooManyRequestsError
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -26,12 +26,13 @@ from homeassistant.components.home_connect.const import (
|
|||||||
BSH_EVENT_PRESENT_STATE_PRESENT,
|
BSH_EVENT_PRESENT_STATE_PRESENT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.home_connect.coordinator import HomeConnectError
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
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"
|
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
|
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