mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Add exception handlers to Home Connect action calls (#131895)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
parent
18e8b080e0
commit
0d0ef6bf03
@ -13,6 +13,7 @@ import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_DEVICE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
@ -39,6 +40,9 @@ from .const import (
|
||||
SERVICE_SELECT_PROGRAM,
|
||||
SERVICE_SETTING,
|
||||
SERVICE_START_PROGRAM,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
|
||||
type HomeConnectConfigEntry = ConfigEntry[api.ConfigEntryAuth]
|
||||
@ -139,6 +143,43 @@ def _get_appliance(
|
||||
raise ValueError(f"Appliance for device id {device_entry.id} not found")
|
||||
|
||||
|
||||
def _get_appliance_or_raise_service_validation_error(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> api.HomeConnectAppliance:
|
||||
"""Return a Home Connect appliance instance or raise a service validation error."""
|
||||
try:
|
||||
return _get_appliance(hass, device_id)
|
||||
except (ValueError, AssertionError) as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="appliance_not_found",
|
||||
translation_placeholders={
|
||||
"device_id": device_id,
|
||||
},
|
||||
) from err
|
||||
|
||||
|
||||
async def _run_appliance_service[*_Ts](
|
||||
hass: HomeAssistant,
|
||||
appliance: api.HomeConnectAppliance,
|
||||
method: str,
|
||||
*args: *_Ts,
|
||||
error_translation_key: str,
|
||||
error_translation_placeholders: dict[str, str],
|
||||
) -> None:
|
||||
try:
|
||||
await hass.async_add_executor_job(getattr(appliance, method), args)
|
||||
except api.HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=error_translation_key,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
**error_translation_placeholders,
|
||||
},
|
||||
) from err
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Home Connect component."""
|
||||
|
||||
@ -158,16 +199,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
option[ATTR_UNIT] = option_unit
|
||||
|
||||
options.append(option)
|
||||
|
||||
appliance = _get_appliance(hass, device_id)
|
||||
await hass.async_add_executor_job(getattr(appliance, method), program, options)
|
||||
await _run_appliance_service(
|
||||
hass,
|
||||
_get_appliance_or_raise_service_validation_error(hass, device_id),
|
||||
method,
|
||||
program,
|
||||
options,
|
||||
error_translation_key=method,
|
||||
error_translation_placeholders={
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM: program,
|
||||
},
|
||||
)
|
||||
|
||||
async def _async_service_command(call, command):
|
||||
"""Execute calls to services executing a command."""
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
|
||||
appliance = _get_appliance(hass, device_id)
|
||||
await hass.async_add_executor_job(appliance.execute_command, command)
|
||||
appliance = _get_appliance_or_raise_service_validation_error(hass, device_id)
|
||||
await _run_appliance_service(
|
||||
hass,
|
||||
appliance,
|
||||
"execute_command",
|
||||
command,
|
||||
error_translation_key="execute_command",
|
||||
error_translation_placeholders={"command": command},
|
||||
)
|
||||
|
||||
async def _async_service_key_value(call, method):
|
||||
"""Execute calls to services taking a key and value."""
|
||||
@ -176,20 +232,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
unit = call.data.get(ATTR_UNIT)
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
|
||||
appliance = _get_appliance(hass, device_id)
|
||||
if unit is not None:
|
||||
await hass.async_add_executor_job(
|
||||
getattr(appliance, method),
|
||||
key,
|
||||
value,
|
||||
unit,
|
||||
)
|
||||
else:
|
||||
await hass.async_add_executor_job(
|
||||
getattr(appliance, method),
|
||||
key,
|
||||
value,
|
||||
)
|
||||
await _run_appliance_service(
|
||||
hass,
|
||||
_get_appliance_or_raise_service_validation_error(hass, device_id),
|
||||
method,
|
||||
*((key, value) if unit is None else (key, value, unit)),
|
||||
error_translation_key=method,
|
||||
error_translation_placeholders={
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE: str(value),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_service_option_active(call):
|
||||
"""Service for setting an option for an active program."""
|
||||
|
@ -127,9 +127,12 @@ ATTR_STEPSIZE = "stepsize"
|
||||
ATTR_UNIT = "unit"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
SVE_TRANSLATION_KEY_SET_SETTING = "set_setting_entity"
|
||||
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME = "appliance_name"
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID = "entity_id"
|
||||
SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY = "setting_key"
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM = "program"
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY = "key"
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE = "value"
|
||||
|
||||
OLD_NEW_UNIQUE_ID_SUFFIX_MAP = {
|
||||
|
@ -22,8 +22,9 @@ from .const import (
|
||||
ATTR_UNIT,
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
SVE_TRANSLATION_KEY_SET_SETTING,
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
|
||||
SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
@ -119,11 +120,11 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_setting",
|
||||
translation_key=SVE_TRANSLATION_KEY_SET_SETTING,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY: self.bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: self.bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE: str(value),
|
||||
},
|
||||
) from err
|
||||
|
@ -22,6 +22,7 @@ from .const import (
|
||||
BSH_ACTIVE_PROGRAM,
|
||||
BSH_SELECTED_PROGRAM,
|
||||
DOMAIN,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
|
||||
@ -294,7 +295,7 @@ class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
"program": bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM: bsh_key,
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
|
@ -22,6 +22,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"appliance_not_found": {
|
||||
"message": "Appliance for device id {device_id} not found"
|
||||
},
|
||||
"turn_on_light": {
|
||||
"message": "Error turning on {entity_id}: {description}"
|
||||
},
|
||||
@ -37,14 +40,17 @@
|
||||
"set_light_color": {
|
||||
"message": "Error setting color of {entity_id}: {description}"
|
||||
},
|
||||
"set_setting_entity": {
|
||||
"message": "Error assigning the value \"{value}\" to the setting \"{key}\" for {entity_id}: {description}"
|
||||
},
|
||||
"set_setting": {
|
||||
"message": "Error assigning the value \"{value}\" to the setting \"{setting_key}\" for {entity_id}: {description}"
|
||||
"message": "Error assigning the value \"{value}\" to the setting \"{key}\": {description}"
|
||||
},
|
||||
"turn_on": {
|
||||
"message": "Error turning on {entity_id} ({setting_key}): {description}"
|
||||
"message": "Error turning on {entity_id} ({key}): {description}"
|
||||
},
|
||||
"turn_off": {
|
||||
"message": "Error turning off {entity_id} ({setting_key}): {description}"
|
||||
"message": "Error turning off {entity_id} ({key}): {description}"
|
||||
},
|
||||
"select_program": {
|
||||
"message": "Error selecting program {program}: {description}"
|
||||
@ -52,8 +58,20 @@
|
||||
"start_program": {
|
||||
"message": "Error starting program {program}: {description}"
|
||||
},
|
||||
"pause_program": {
|
||||
"message": "Error pausing program: {description}"
|
||||
},
|
||||
"stop_program": {
|
||||
"message": "Error stopping program {program}: {description}"
|
||||
"message": "Error stopping program: {description}"
|
||||
},
|
||||
"set_options_active_program": {
|
||||
"message": "Error setting options for the active program: {description}"
|
||||
},
|
||||
"set_options_selected_program": {
|
||||
"message": "Error setting options for the selected program: {description}"
|
||||
},
|
||||
"execute_command": {
|
||||
"message": "Error executing command {command}: {description}"
|
||||
},
|
||||
"power_on": {
|
||||
"message": "Error turning on {appliance_name}: {description}"
|
||||
|
@ -30,7 +30,7 @@ from .const import (
|
||||
REFRIGERATION_SUPERMODEREFRIGERATOR,
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME,
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
|
||||
SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
from .entity import HomeConnectDevice, HomeConnectEntity
|
||||
@ -140,7 +140,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY: self.bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: self.bsh_key,
|
||||
},
|
||||
) from err
|
||||
|
||||
@ -164,7 +164,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY: self.bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: self.bsh_key,
|
||||
},
|
||||
) from err
|
||||
|
||||
@ -230,7 +230,6 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
translation_key="stop_program",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
"program": self.program_name,
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
|
@ -14,8 +14,9 @@ from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
|
||||
from .const import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
SVE_TRANSLATION_KEY_SET_SETTING,
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
|
||||
SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
@ -82,11 +83,11 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_setting",
|
||||
translation_key=SVE_TRANSLATION_KEY_SET_SETTING,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
SVE_TRANSLATION_PLACEHOLDER_SETTING_KEY: self.bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: self.bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE: str(value),
|
||||
},
|
||||
) from err
|
||||
|
@ -183,10 +183,15 @@ def mock_problematic_appliance(request: pytest.FixtureRequest) -> Mock:
|
||||
mock.get_programs_available.side_effect = HomeConnectError
|
||||
mock.start_program.side_effect = HomeConnectError
|
||||
mock.select_program.side_effect = HomeConnectError
|
||||
mock.pause_program.side_effect = HomeConnectError
|
||||
mock.stop_program.side_effect = HomeConnectError
|
||||
mock.set_options_active_program.side_effect = HomeConnectError
|
||||
mock.set_options_selected_program.side_effect = HomeConnectError
|
||||
mock.get_status.side_effect = HomeConnectError
|
||||
mock.get_settings.side_effect = HomeConnectError
|
||||
mock.set_setting.side_effect = HomeConnectError
|
||||
mock.set_setting.side_effect = HomeConnectError
|
||||
mock.execute_command.side_effect = HomeConnectError
|
||||
|
||||
return mock
|
||||
|
||||
|
@ -29,6 +29,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from script.hassfest.translations import RE_TRANSLATION_KEY
|
||||
|
||||
@ -290,8 +291,40 @@ async def test_services(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_call",
|
||||
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_services_exception(
|
||||
service_call: list[dict[str, Any]],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
problematic_appliance: Mock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Raise a HomeAssistantError when there is an API error."""
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, problematic_appliance.haId)},
|
||||
)
|
||||
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_services_appliance_not_found(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
@ -299,7 +332,7 @@ async def test_services_exception(
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
) -> None:
|
||||
"""Raise a ValueError when device id does not match."""
|
||||
"""Raise a ServiceValidationError when device id does not match."""
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
@ -309,7 +342,7 @@ async def test_services_exception(
|
||||
|
||||
service_call["service_data"]["device_id"] = "DOES_NOT_EXISTS"
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
with pytest.raises(ServiceValidationError, match=r"Appliance.*not found"):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user