Add exception handler and exception translations to eheimdigital (#145476)

* Add exception handler and exception translations to eheimdigital

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Sid 2025-05-26 15:05:11 +02:00 committed by GitHub
parent 5202bbb6af
commit 6ddc2193d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 65 additions and 43 deletions

View File

@ -4,7 +4,7 @@ from typing import Any
from eheimdigital.device import EheimDigitalDevice from eheimdigital.device import EheimDigitalDevice
from eheimdigital.heater import EheimDigitalHeater from eheimdigital.heater import EheimDigitalHeater
from eheimdigital.types import EheimDigitalClientError, HeaterMode, HeaterUnit from eheimdigital.types import HeaterMode, HeaterUnit
from homeassistant.components.climate import ( from homeassistant.components.climate import (
PRESET_NONE, PRESET_NONE,
@ -20,12 +20,11 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import HEATER_BIO_MODE, HEATER_PRESET_TO_HEATER_MODE, HEATER_SMART_MODE from .const import HEATER_BIO_MODE, HEATER_PRESET_TO_HEATER_MODE, HEATER_SMART_MODE
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity from .entity import EheimDigitalEntity, exception_handler
# Coordinator is used to centralize the data updates # Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -83,34 +82,28 @@ class EheimDigitalHeaterClimate(EheimDigitalEntity[EheimDigitalHeater], ClimateE
self._attr_unique_id = self._device_address self._attr_unique_id = self._device_address
self._async_update_attrs() self._async_update_attrs()
@exception_handler
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode.""" """Set the preset mode."""
try:
if preset_mode in HEATER_PRESET_TO_HEATER_MODE: if preset_mode in HEATER_PRESET_TO_HEATER_MODE:
await self._device.set_operation_mode( await self._device.set_operation_mode(
HEATER_PRESET_TO_HEATER_MODE[preset_mode] HEATER_PRESET_TO_HEATER_MODE[preset_mode]
) )
except EheimDigitalClientError as err:
raise HomeAssistantError from err
@exception_handler
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set a new temperature.""" """Set a new temperature."""
try:
if ATTR_TEMPERATURE in kwargs: if ATTR_TEMPERATURE in kwargs:
await self._device.set_target_temperature(kwargs[ATTR_TEMPERATURE]) await self._device.set_target_temperature(kwargs[ATTR_TEMPERATURE])
except EheimDigitalClientError as err:
raise HomeAssistantError from err
@exception_handler
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the heating mode.""" """Set the heating mode."""
try:
match hvac_mode: match hvac_mode:
case HVACMode.OFF: case HVACMode.OFF:
await self._device.set_active(active=False) await self._device.set_active(active=False)
case HVACMode.AUTO: case HVACMode.AUTO:
await self._device.set_active(active=True) await self._device.set_active(active=True)
except EheimDigitalClientError as err:
raise HomeAssistantError from err
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:
if self._device.temperature_unit == HeaterUnit.CELSIUS: if self._device.temperature_unit == HeaterUnit.CELSIUS:

View File

@ -1,12 +1,15 @@
"""Base entity for EHEIM Digital.""" """Base entity for EHEIM Digital."""
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING from collections.abc import Callable, Coroutine
from typing import TYPE_CHECKING, Any, Concatenate
from eheimdigital.device import EheimDigitalDevice from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import EheimDigitalClientError
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -51,3 +54,24 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
"""Update attributes when the coordinator updates.""" """Update attributes when the coordinator updates."""
self._async_update_attrs() self._async_update_attrs()
super()._handle_coordinator_update() super()._handle_coordinator_update()
def exception_handler[_EntityT: EheimDigitalEntity[EheimDigitalDevice], **_P](
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
"""Decorate AirGradient calls to handle exceptions.
A decorator that wraps the passed in function, catches AirGradient errors.
"""
async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except EheimDigitalClientError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
translation_placeholders={"error": str(error)},
) from error
return handler

View File

@ -4,7 +4,7 @@ from typing import Any
from eheimdigital.classic_led_ctrl import EheimDigitalClassicLEDControl from eheimdigital.classic_led_ctrl import EheimDigitalClassicLEDControl
from eheimdigital.device import EheimDigitalDevice from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import EheimDigitalClientError, LightMode from eheimdigital.types import LightMode
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -15,13 +15,12 @@ from homeassistant.components.light import (
LightEntityFeature, LightEntityFeature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.color import brightness_to_value, value_to_brightness from homeassistant.util.color import brightness_to_value, value_to_brightness
from .const import EFFECT_DAYCL_MODE, EFFECT_TO_LIGHT_MODE from .const import EFFECT_DAYCL_MODE, EFFECT_TO_LIGHT_MODE
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity from .entity import EheimDigitalEntity, exception_handler
BRIGHTNESS_SCALE = (1, 100) BRIGHTNESS_SCALE = (1, 100)
@ -88,6 +87,7 @@ class EheimDigitalClassicLEDControlLight(
"""Return whether the entity is available.""" """Return whether the entity is available."""
return super().available and self._device.light_level[self._channel] is not None return super().available and self._device.light_level[self._channel] is not None
@exception_handler
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light.""" """Turn on the light."""
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
@ -96,22 +96,17 @@ class EheimDigitalClassicLEDControlLight(
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
if self._device.light_mode == LightMode.DAYCL_MODE: if self._device.light_mode == LightMode.DAYCL_MODE:
await self._device.set_light_mode(LightMode.MAN_MODE) await self._device.set_light_mode(LightMode.MAN_MODE)
try:
await self._device.turn_on( await self._device.turn_on(
int(brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])), int(brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])),
self._channel, self._channel,
) )
except EheimDigitalClientError as err:
raise HomeAssistantError from err
@exception_handler
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light.""" """Turn off the light."""
if self._device.light_mode == LightMode.DAYCL_MODE: if self._device.light_mode == LightMode.DAYCL_MODE:
await self._device.set_light_mode(LightMode.MAN_MODE) await self._device.set_light_mode(LightMode.MAN_MODE)
try:
await self._device.turn_off(self._channel) await self._device.turn_off(self._channel)
except EheimDigitalClientError as err:
raise HomeAssistantError from err
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:
light_level = self._device.light_level[self._channel] light_level = self._device.light_level[self._channel]

View File

@ -26,7 +26,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity from .entity import EheimDigitalEntity, exception_handler
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -182,6 +182,7 @@ class EheimDigitalNumber(
self._attr_unique_id = f"{self._device_address}_{description.key}" self._attr_unique_id = f"{self._device_address}_{description.key}"
@override @override
@exception_handler
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
return await self.entity_description.set_value_fn(self._device, value) return await self.entity_description.set_value_fn(self._device, value)

View File

@ -58,7 +58,7 @@ rules:
entity-device-class: done entity-device-class: done
entity-disabled-by-default: done entity-disabled-by-default: done
entity-translations: done entity-translations: done
exception-translations: todo exception-translations: done
icon-translations: todo icon-translations: todo
reconfiguration-flow: todo reconfiguration-flow: todo
repair-issues: todo repair-issues: todo

View File

@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity from .entity import EheimDigitalEntity, exception_handler
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -94,6 +94,7 @@ class EheimDigitalSelect(
self._attr_unique_id = f"{self._device_address}_{description.key}" self._attr_unique_id = f"{self._device_address}_{description.key}"
@override @override
@exception_handler
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
return await self.entity_description.set_value_fn(self._device, option) return await self.entity_description.set_value_fn(self._device, option)

View File

@ -101,5 +101,10 @@
"name": "Night start time" "name": "Night start time"
} }
} }
},
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with the EHEIM Digital hub: {error}"
}
} }
} }

View File

@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity from .entity import EheimDigitalEntity, exception_handler
# Coordinator is used to centralize the data updates # Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -58,10 +58,12 @@ class EheimDigitalClassicVarioSwitch(
self._async_update_attrs() self._async_update_attrs()
@override @override
@exception_handler
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
await self._device.set_active(active=False) await self._device.set_active(active=False)
@override @override
@exception_handler
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
await self._device.set_active(active=True) await self._device.set_active(active=True)

View File

@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity from .entity import EheimDigitalEntity, exception_handler
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -122,6 +122,7 @@ class EheimDigitalTime(
self._attr_unique_id = f"{device.mac_address}_{description.key}" self._attr_unique_id = f"{device.mac_address}_{description.key}"
@override @override
@exception_handler
async def async_set_value(self, value: time) -> None: async def async_set_value(self, value: time) -> None:
"""Change the time.""" """Change the time."""
return await self.entity_description.set_value_fn(self._device, value) return await self.entity_description.set_value_fn(self._device, value)