Add proper exception handling to lamarzocco (#125913)

This commit is contained in:
Josef Zweck 2024-09-27 21:04:01 +02:00 committed by GitHub
parent 57e041171b
commit 2e1732fadf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 285 additions and 17 deletions

View File

@ -4,10 +4,12 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from lmcloud.exceptions import RequestNotSuccessful
from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.lm_machine import LaMarzoccoMachine
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LaMarzoccoConfigEntry from . import LaMarzoccoConfigEntry
@ -55,5 +57,13 @@ class LaMarzoccoButtonEntity(LaMarzoccoEntity, ButtonEntity):
async def async_press(self) -> None: async def async_press(self) -> None:
"""Press button.""" """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() await self.coordinator.async_request_refresh()

View File

@ -11,6 +11,7 @@ from lmcloud.const import (
PhysicalKey, PhysicalKey,
PrebrewMode, PrebrewMode,
) )
from lmcloud.exceptions import RequestNotSuccessful
from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.lm_machine import LaMarzoccoMachine
from lmcloud.models import LaMarzoccoMachineConfig from lmcloud.models import LaMarzoccoMachineConfig
@ -27,6 +28,7 @@ from homeassistant.const import (
UnitOfTime, UnitOfTime,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LaMarzoccoConfigEntry from . import LaMarzoccoConfigEntry
@ -220,7 +222,18 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity):
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
"""Set the value.""" """Set the value."""
if value != self.native_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() self.async_write_ha_state()
@ -258,7 +271,17 @@ class LaMarzoccoKeyNumberEntity(LaMarzoccoEntity, NumberEntity):
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
"""Set the value.""" """Set the value."""
if value != self.native_value: if value != self.native_value:
await self.entity_description.set_value_fn( try:
self.coordinator.device, value, PhysicalKey(self.pyhsical_key) 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() self.async_write_ha_state()

View File

@ -5,12 +5,14 @@ from dataclasses import dataclass
from typing import Any from typing import Any
from lmcloud.const import MachineModel, PrebrewMode, SteamLevel from lmcloud.const import MachineModel, PrebrewMode, SteamLevel
from lmcloud.exceptions import RequestNotSuccessful
from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.lm_machine import LaMarzoccoMachine
from lmcloud.models import LaMarzoccoMachineConfig from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LaMarzoccoConfigEntry from . import LaMarzoccoConfigEntry
@ -113,7 +115,16 @@ class LaMarzoccoSelectEntity(LaMarzoccoEntity, SelectEntity):
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""
if option != self.current_option: if option != self.current_option:
await self.entity_description.select_option_fn( try:
self.coordinator.device, option 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() self.async_write_ha_state()

View File

@ -180,5 +180,31 @@
"title": "Unsupported gateway firmware", "title": "Unsupported gateway firmware",
"description": "Gateway firmware {gateway_version} is no longer supported by this integration, please update." "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}"
}
} }
} }

View File

@ -5,12 +5,14 @@ from dataclasses import dataclass
from typing import Any from typing import Any
from lmcloud.const import BoilerType from lmcloud.const import BoilerType
from lmcloud.exceptions import RequestNotSuccessful
from lmcloud.lm_machine import LaMarzoccoMachine from lmcloud.lm_machine import LaMarzoccoMachine
from lmcloud.models import LaMarzoccoMachineConfig from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LaMarzoccoConfigEntry from . import LaMarzoccoConfigEntry
@ -77,12 +79,24 @@ class LaMarzoccoSwitchEntity(LaMarzoccoEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn device on.""" """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() self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn device off.""" """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() self.async_write_ha_state()
@property @property
@ -114,7 +128,13 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity):
self._identifier self._identifier
] ]
wake_up_sleep_entry.enabled = state 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() self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:

View File

@ -4,6 +4,7 @@ from dataclasses import dataclass
from typing import Any from typing import Any
from lmcloud.const import FirmwareType from lmcloud.const import FirmwareType
from lmcloud.exceptions import RequestNotSuccessful
from homeassistant.components.update import ( from homeassistant.components.update import (
UpdateDeviceClass, UpdateDeviceClass,
@ -94,10 +95,23 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
"""Install an update.""" """Install an update."""
self._attr_in_progress = True self._attr_in_progress = True
self.async_write_ha_state() self.async_write_ha_state()
success = await self.coordinator.device.update_firmware( try:
self.entity_description.component 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: 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 self._attr_in_progress = False
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()

View File

@ -2,12 +2,14 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from lmcloud.exceptions import RequestNotSuccessful
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
pytestmark = pytest.mark.usefixtures("init_integration") pytestmark = pytest.mark.usefixtures("init_integration")
@ -42,3 +44,26 @@ async def test_start_backflush(
assert len(mock_lamarzocco.start_backflush.mock_calls) == 1 assert len(mock_lamarzocco.start_backflush.mock_calls) == 1
mock_lamarzocco.start_backflush.assert_called_once() 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"

View File

@ -9,6 +9,7 @@ from lmcloud.const import (
PhysicalKey, PhysicalKey,
PrebrewMode, PrebrewMode,
) )
from lmcloud.exceptions import RequestNotSuccessful
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -19,6 +20,7 @@ from homeassistant.components.number import (
) )
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
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 . import async_init_integration 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): for key in range(1, KEYS_PER_MODEL[MachineModel.GS3_AV] + 1):
state = hass.states.get(f"number.{serial_number}_{entity}_key_{key}") state = hass.states.get(f"number.{serial_number}_{entity}_key_{key}")
assert state is None 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"

View File

@ -3,6 +3,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from lmcloud.const import MachineModel, PrebrewMode, SteamLevel from lmcloud.const import MachineModel, PrebrewMode, SteamLevel
from lmcloud.exceptions import RequestNotSuccessful
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -13,6 +14,7 @@ from homeassistant.components.select import (
) )
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
pytestmark = pytest.mark.usefixtures("init_integration") 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") state = hass.states.get(f"select.{serial_number}_prebrew_infusion_mode")
assert state is None 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"

View File

@ -2,6 +2,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from lmcloud.exceptions import RequestNotSuccessful
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -12,6 +13,7 @@ from homeassistant.components.switch import (
) )
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
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 . import WAKE_UP_SLEEP_ENTRY_IDS, async_init_integration 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 wake_up_sleep_entry.enabled = True
mock_lamarzocco.set_wake_up_sleep.assert_called_with(wake_up_sleep_entry) 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"

View File

@ -3,6 +3,7 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from lmcloud.const import FirmwareType from lmcloud.const import FirmwareType
from lmcloud.exceptions import RequestNotSuccessful
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
@ -54,17 +55,26 @@ async def test_update_entites(
mock_lamarzocco.update_firmware.assert_called_once_with(component) 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( async def test_update_error(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,
attr: str,
value: bool | Exception,
) -> None: ) -> None:
"""Test error during update.""" """Test error during update."""
state = hass.states.get(f"update.{mock_lamarzocco.serial_number}_machine_firmware") state = hass.states.get(f"update.{mock_lamarzocco.serial_number}_machine_firmware")
assert state 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( await hass.services.async_call(
UPDATE_DOMAIN, UPDATE_DOMAIN,
SERVICE_INSTALL, SERVICE_INSTALL,
@ -73,3 +83,4 @@ async def test_update_error(
}, },
blocking=True, blocking=True,
) )
assert exc_info.value.translation_key == "update_failed"