From 2e1732fadfe44f96ca48c8ac5b9fae20b669e3bb Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Fri, 27 Sep 2024 21:04:01 +0200 Subject: [PATCH] Add proper exception handling to lamarzocco (#125913) --- homeassistant/components/lamarzocco/button.py | 12 +++- homeassistant/components/lamarzocco/number.py | 31 +++++++++-- homeassistant/components/lamarzocco/select.py | 17 +++++- .../components/lamarzocco/strings.json | 26 +++++++++ homeassistant/components/lamarzocco/switch.py | 26 ++++++++- homeassistant/components/lamarzocco/update.py | 22 ++++++-- tests/components/lamarzocco/test_button.py | 25 +++++++++ tests/components/lamarzocco/test_number.py | 45 +++++++++++++++ tests/components/lamarzocco/test_select.py | 28 ++++++++++ tests/components/lamarzocco/test_switch.py | 55 +++++++++++++++++++ tests/components/lamarzocco/test_update.py | 15 ++++- 11 files changed, 285 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/lamarzocco/button.py b/homeassistant/components/lamarzocco/button.py index 7b38c9fbf72..4e3052c0c0f 100644 --- a/homeassistant/components/lamarzocco/button.py +++ b/homeassistant/components/lamarzocco/button.py @@ -4,10 +4,12 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any +from lmcloud.exceptions import RequestNotSuccessful from lmcloud.lm_machine import LaMarzoccoMachine from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LaMarzoccoConfigEntry @@ -55,5 +57,13 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity): async def async_press(self) -> None: """Press button.""" - await self.entity_description.press_fn(self.coordinator.device) + try: + await self.entity_description.press_fn(self.coordinator.device) + except RequestNotSuccessful as exc: + raise HomeAssistantError( + translation_key="button_error", + translation_placeholders={ + "key": self.entity_description.key, + }, + ) from exc await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 69e5b42c116..879535688d5 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -11,6 +11,7 @@ from lmcloud.const import ( PhysicalKey, PrebrewMode, ) +from lmcloud.exceptions import RequestNotSuccessful from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig @@ -27,6 +28,7 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LaMarzoccoConfigEntry @@ -220,7 +222,18 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set the value.""" if value != self.native_value: - await self.entity_description.set_value_fn(self.coordinator.device, value) + try: + await self.entity_description.set_value_fn( + self.coordinator.device, value + ) + except RequestNotSuccessful as exc: + raise HomeAssistantError( + translation_key="number_exception", + translation_placeholders={ + "key": self.entity_description.key, + "value": str(value), + }, + ) from exc self.async_write_ha_state() @@ -258,7 +271,17 @@ class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set the value.""" if value != self.native_value: - await self.entity_description.set_value_fn( - self.coordinator.device, value, PhysicalKey(self.pyhsical_key) - ) + try: + await self.entity_description.set_value_fn( + self.coordinator.device, value, PhysicalKey(self.pyhsical_key) + ) + except RequestNotSuccessful as exc: + raise HomeAssistantError( + translation_key="number_exception_key", + translation_placeholders={ + "key": self.entity_description.key, + "value": str(value), + "physical_key": str(self.pyhsical_key), + }, + ) from exc self.async_write_ha_state() diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py index 5bff815fb95..59dac69b35a 100644 --- a/homeassistant/components/lamarzocco/select.py +++ b/homeassistant/components/lamarzocco/select.py @@ -5,12 +5,14 @@ from dataclasses import dataclass from typing import Any from lmcloud.const import MachineModel, PrebrewMode, SteamLevel +from lmcloud.exceptions import RequestNotSuccessful from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LaMarzoccoConfigEntry @@ -113,7 +115,16 @@ class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" if option != self.current_option: - await self.entity_description.select_option_fn( - self.coordinator.device, option - ) + try: + await self.entity_description.select_option_fn( + self.coordinator.device, option + ) + except RequestNotSuccessful as exc: + raise HomeAssistantError( + translation_key="select_option_error", + translation_placeholders={ + "key": self.entity_description.key, + "option": option, + }, + ) from exc self.async_write_ha_state() diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 39cc24388ab..71b13e2b789 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -180,5 +180,31 @@ "title": "Unsupported gateway firmware", "description": "Gateway firmware {gateway_version} is no longer supported by this integration, please update." } + }, + "exceptions": { + "auto_on_off_error": { + "message": "Error while setting auto on/off to {state} for {id}" + }, + "button_error": { + "message": "Error while executing button {key}" + }, + "number_exception": { + "message": "Error while setting value {value} for number {key}" + }, + "number_exception_key": { + "message": "Error while setting value {value} for number {key}, key {physical_key}" + }, + "select_option_error": { + "message": "Error while setting select option {option} for {key}" + }, + "switch_on_error": { + "message": "Error while turning on switch {key}" + }, + "switch_off_error": { + "message": "Error while turning off switch {key}" + }, + "update_failed": { + "message": "Error while updating {key}" + } } } diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index c57e0662ab2..5498b07401e 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -5,12 +5,14 @@ from dataclasses import dataclass from typing import Any from lmcloud.const import BoilerType +from lmcloud.exceptions import RequestNotSuccessful from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.models import LaMarzoccoMachineConfig from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LaMarzoccoConfigEntry @@ -77,12 +79,24 @@ class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" - await self.entity_description.control_fn(self.coordinator.device, True) + try: + await self.entity_description.control_fn(self.coordinator.device, True) + except RequestNotSuccessful as exc: + raise HomeAssistantError( + translation_key="switch_on_error", + translation_placeholders={"key": self.entity_description.key}, + ) from exc self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" - await self.entity_description.control_fn(self.coordinator.device, False) + try: + await self.entity_description.control_fn(self.coordinator.device, False) + except RequestNotSuccessful as exc: + raise HomeAssistantError( + translation_key="switch_off_error", + translation_placeholders={"name": self.entity_description.key}, + ) from exc self.async_write_ha_state() @property @@ -114,7 +128,13 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity): self._identifier ] wake_up_sleep_entry.enabled = state - await self.coordinator.device.set_wake_up_sleep(wake_up_sleep_entry) + try: + await self.coordinator.device.set_wake_up_sleep(wake_up_sleep_entry) + except RequestNotSuccessful as exc: + raise HomeAssistantError( + translation_key="auto_on_off_error", + translation_placeholders={"id": self._identifier, "state": str(state)}, + ) from exc self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py index 2769016e43b..cbbdee68c40 100644 --- a/homeassistant/components/lamarzocco/update.py +++ b/homeassistant/components/lamarzocco/update.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import Any from lmcloud.const import FirmwareType +from lmcloud.exceptions import RequestNotSuccessful from homeassistant.components.update import ( UpdateDeviceClass, @@ -94,10 +95,23 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): """Install an update.""" self._attr_in_progress = True self.async_write_ha_state() - success = await self.coordinator.device.update_firmware( - self.entity_description.component - ) + try: + success = await self.coordinator.device.update_firmware( + self.entity_description.component + ) + except RequestNotSuccessful as exc: + raise HomeAssistantError( + translation_key="update_failed", + translation_placeholders={ + "key": self.entity_description.key, + }, + ) from exc if not success: - raise HomeAssistantError("Update failed") + raise HomeAssistantError( + translation_key="update_failed", + translation_placeholders={ + "key": self.entity_description.key, + }, + ) self._attr_in_progress = False await self.coordinator.async_request_refresh() diff --git a/tests/components/lamarzocco/test_button.py b/tests/components/lamarzocco/test_button.py index e1a036df17a..b754688f369 100644 --- a/tests/components/lamarzocco/test_button.py +++ b/tests/components/lamarzocco/test_button.py @@ -2,12 +2,14 @@ from unittest.mock import MagicMock +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er pytestmark = pytest.mark.usefixtures("init_integration") @@ -42,3 +44,26 @@ async def test_start_backflush( assert len(mock_lamarzocco.start_backflush.mock_calls) == 1 mock_lamarzocco.start_backflush.assert_called_once() + + +async def test_button_error( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, +) -> None: + """Test the La Marzocco button error.""" + serial_number = mock_lamarzocco.serial_number + + state = hass.states.get(f"button.{serial_number}_start_backflush") + assert state + + mock_lamarzocco.start_backflush.side_effect = RequestNotSuccessful("Boom.") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: f"button.{serial_number}_start_backflush", + }, + blocking=True, + ) + assert exc_info.value.translation_key == "button_error" diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index 288c78c26dd..70d8efa5de7 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -9,6 +9,7 @@ from lmcloud.const import ( PhysicalKey, PrebrewMode, ) +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -19,6 +20,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from . import async_init_integration @@ -379,3 +381,46 @@ async def test_not_existing_key_entities( for key in range(1, KEYS_PER_MODEL[MachineModel.GS3_AV] + 1): state = hass.states.get(f"number.{serial_number}_{entity}_key_{key}") assert state is None + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_number_error( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test number entities raise error on service call.""" + await async_init_integration(hass, mock_config_entry) + serial_number = mock_lamarzocco.serial_number + + state = hass.states.get(f"number.{serial_number}_coffee_target_temperature") + assert state + + mock_lamarzocco.set_temp.side_effect = RequestNotSuccessful("Boom") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: f"number.{serial_number}_coffee_target_temperature", + ATTR_VALUE: 94, + }, + blocking=True, + ) + assert exc_info.value.translation_key == "number_exception" + + state = hass.states.get(f"number.{serial_number}_dose_key_1") + assert state + + mock_lamarzocco.set_dose.side_effect = RequestNotSuccessful("Boom") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: f"number.{serial_number}_dose_key_1", + ATTR_VALUE: 99, + }, + blocking=True, + ) + assert exc_info.value.translation_key == "number_exception_key" diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index e3521b473bd..862898428f5 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from lmcloud.const import MachineModel, PrebrewMode, SteamLevel +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -13,6 +14,7 @@ from homeassistant.components.select import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er pytestmark = pytest.mark.usefixtures("init_integration") @@ -117,3 +119,29 @@ async def test_pre_brew_infusion_select_none( state = hass.states.get(f"select.{serial_number}_prebrew_infusion_mode") assert state is None + + +async def test_select_errors( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, +) -> None: + """Test select errors.""" + serial_number = mock_lamarzocco.serial_number + + state = hass.states.get(f"select.{serial_number}_prebrew_infusion_mode") + assert state + + mock_lamarzocco.set_prebrew_mode.side_effect = RequestNotSuccessful("Boom") + + # Test setting invalid option + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: f"select.{serial_number}_prebrew_infusion_mode", + ATTR_OPTION: "prebrew", + }, + blocking=True, + ) + assert exc_info.value.translation_key == "select_option_error" diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index 4f60b264a1d..a09d254ffe9 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -12,6 +13,7 @@ from homeassistant.components.switch import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from . import WAKE_UP_SLEEP_ENTRY_IDS, async_init_integration @@ -158,3 +160,56 @@ async def test_auto_on_off_switches( ) wake_up_sleep_entry.enabled = True mock_lamarzocco.set_wake_up_sleep.assert_called_with(wake_up_sleep_entry) + + +async def test_switch_exceptions( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the La Marzocco switches.""" + await async_init_integration(hass, mock_config_entry) + + serial_number = mock_lamarzocco.serial_number + + state = hass.states.get(f"switch.{serial_number}") + assert state + + mock_lamarzocco.set_power.side_effect = RequestNotSuccessful("Boom") + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: f"switch.{serial_number}", + }, + blocking=True, + ) + assert exc_info.value.translation_key == "switch_off_error" + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: f"switch.{serial_number}", + }, + blocking=True, + ) + assert exc_info.value.translation_key == "switch_on_error" + + state = hass.states.get(f"switch.{serial_number}_auto_on_off_os2oswx") + assert state + + mock_lamarzocco.set_wake_up_sleep.side_effect = RequestNotSuccessful("Boom") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: f"switch.{serial_number}_auto_on_off_os2oswx", + }, + blocking=True, + ) + assert exc_info.value.translation_key == "auto_on_off_error" diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index 02330daf794..3dc2a86b574 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from lmcloud.const import FirmwareType +from lmcloud.exceptions import RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -54,17 +55,26 @@ async def test_update_entites( mock_lamarzocco.update_firmware.assert_called_once_with(component) +@pytest.mark.parametrize( + ("attr", "value"), + [ + ("side_effect", RequestNotSuccessful("Boom")), + ("return_value", False), + ], +) async def test_update_error( hass: HomeAssistant, mock_lamarzocco: MagicMock, + attr: str, + value: bool | Exception, ) -> None: """Test error during update.""" state = hass.states.get(f"update.{mock_lamarzocco.serial_number}_machine_firmware") assert state - mock_lamarzocco.update_firmware.return_value = False + setattr(mock_lamarzocco.update_firmware, attr, value) - with pytest.raises(HomeAssistantError, match="Update failed"): + with pytest.raises(HomeAssistantError) as exc_info: await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, @@ -73,3 +83,4 @@ async def test_update_error( }, blocking=True, ) + assert exc_info.value.translation_key == "update_failed"