Add translated action exceptions to Airgradient (#136322)

* Add translated action exceptions to Airgradient

* Add translated action exceptions to Airgradient
This commit is contained in:
Joost Lekkerkerker 2025-01-23 13:48:46 +01:00 committed by GitHub
parent d6f6961674
commit 40ed0562bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 208 additions and 12 deletions

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry
from .const import DOMAIN
from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity
from .entity import AirGradientEntity, exception_handler
PARALLEL_UPDATES = 1
@ -102,6 +102,7 @@ class AirGradientButton(AirGradientEntity, ButtonEntity):
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
@exception_handler
async def async_press(self) -> None:
"""Press the button."""
await self.entity_description.press_fn(self.coordinator.client)

View File

@ -55,7 +55,11 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
measures = await self.client.get_current_measures()
config = await self.client.get_config()
except AirGradientError as error:
raise UpdateFailed(error) from error
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": str(error)},
) from error
if measures.firmware_version != self._current_version:
device_registry = dr.async_get(self.hass)
device_entry = device_registry.async_get_device(

View File

@ -1,7 +1,11 @@
"""Base class for AirGradient entities."""
from airgradient import get_model_name
from collections.abc import Callable, Coroutine
from typing import Any, Concatenate
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -26,3 +30,31 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
serial_number=coordinator.serial_number,
sw_version=measures.firmware_version,
)
def exception_handler[_EntityT: AirGradientEntity, **_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 AirGradientConnectionError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
translation_placeholders={"error": str(error)},
) from error
except AirGradientError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unknown_error",
translation_placeholders={"error": str(error)},
) from error
return handler

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry
from .const import DOMAIN
from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity
from .entity import AirGradientEntity, exception_handler
PARALLEL_UPDATES = 1
@ -123,6 +123,7 @@ class AirGradientNumber(AirGradientEntity, NumberEntity):
"""Return the state of the number."""
return self.entity_description.value_fn(self.coordinator.data.config)
@exception_handler
async def async_set_native_value(self, value: float) -> None:
"""Set the selected value."""
await self.entity_description.set_value_fn(self.coordinator.client, int(value))

View File

@ -29,7 +29,7 @@ rules:
unique-config-entry: done
# Silver
action-exceptions: todo
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
@ -68,7 +68,7 @@ rules:
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues:

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry
from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE
from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity
from .entity import AirGradientEntity, exception_handler
PARALLEL_UPDATES = 1
@ -218,6 +218,7 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
"""Return the state of the select."""
return self.entity_description.value_fn(self.coordinator.data.config)
@exception_handler
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.entity_description.set_value_fn(self.coordinator.client, option)

View File

@ -165,5 +165,16 @@
"name": "Post data to Airgradient"
}
}
},
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with the Airgradient device: {error}"
},
"unknown_error": {
"message": "An unknown error occurred while communicating with the Airgradient device: {error}"
},
"update_error": {
"message": "An error occurred while communicating with the Airgradient device: {error}"
}
}
}

View File

@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry
from .const import DOMAIN
from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity
from .entity import AirGradientEntity, exception_handler
PARALLEL_UPDATES = 1
@ -101,11 +101,13 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
"""Return the state of the switch."""
return self.entity_description.value_fn(self.coordinator.data.config)
@exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.entity_description.set_value_fn(self.coordinator.client, True)
await self.coordinator.async_request_refresh()
@exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.entity_description.set_value_fn(self.coordinator.client, False)

View File

@ -3,14 +3,16 @@
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from airgradient import Config
from airgradient import AirGradientConnectionError, AirGradientError, Config
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.airgradient.const import DOMAIN
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
@ -97,3 +99,37 @@ async def test_cloud_creates_no_button(
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
@pytest.mark.parametrize(
("exception", "error_message"),
[
(
AirGradientConnectionError("Something happened"),
"An error occurred while communicating with the Airgradient device: Something happened",
),
(
AirGradientError("Something else happened"),
"An unknown error occurred while communicating with the Airgradient device: Something else happened",
),
],
)
async def test_exception_handling(
hass: HomeAssistant,
mock_airgradient_client: AsyncMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
error_message: str,
) -> None:
"""Test exception handling."""
await setup_integration(hass, mock_config_entry)
mock_airgradient_client.request_co2_calibration.side_effect = exception
with pytest.raises(HomeAssistantError, match=error_message):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{
ATTR_ENTITY_ID: "button.airgradient_calibrate_co2_sensor",
},
blocking=True,
)

View File

@ -3,8 +3,9 @@
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from airgradient import Config
from airgradient import AirGradientConnectionError, AirGradientError, Config
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.airgradient.const import DOMAIN
@ -15,6 +16,7 @@ from homeassistant.components.number import (
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
@ -99,3 +101,37 @@ async def test_cloud_creates_no_number(
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
@pytest.mark.parametrize(
("exception", "error_message"),
[
(
AirGradientConnectionError("Something happened"),
"An error occurred while communicating with the Airgradient device: Something happened",
),
(
AirGradientError("Something else happened"),
"An unknown error occurred while communicating with the Airgradient device: Something else happened",
),
],
)
async def test_exception_handling(
hass: HomeAssistant,
mock_airgradient_client: AsyncMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
error_message: str,
) -> None:
"""Test exception handling."""
await setup_integration(hass, mock_config_entry)
mock_airgradient_client.set_display_brightness.side_effect = exception
with pytest.raises(HomeAssistantError, match=error_message):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
service_data={ATTR_VALUE: 50},
target={ATTR_ENTITY_ID: "number.airgradient_display_brightness"},
blocking=True,
)

View File

@ -3,7 +3,7 @@
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from airgradient import Config
from airgradient import AirGradientConnectionError, AirGradientError, Config
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
@ -15,6 +15,7 @@ from homeassistant.components.select import (
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
@ -94,3 +95,39 @@ async def test_cloud_creates_no_number(
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
@pytest.mark.parametrize(
("exception", "error_message"),
[
(
AirGradientConnectionError("Something happened"),
"An error occurred while communicating with the Airgradient device: Something happened",
),
(
AirGradientError("Something else happened"),
"An unknown error occurred while communicating with the Airgradient device: Something else happened",
),
],
)
async def test_exception_handling(
hass: HomeAssistant,
mock_airgradient_client: AsyncMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
error_message: str,
) -> None:
"""Test exception handling."""
await setup_integration(hass, mock_config_entry)
mock_airgradient_client.set_configuration_control.side_effect = exception
with pytest.raises(HomeAssistantError, match=error_message):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.airgradient_configuration_source",
ATTR_OPTION: "local",
},
blocking=True,
)

View File

@ -3,8 +3,9 @@
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from airgradient import Config
from airgradient import AirGradientConnectionError, AirGradientError, Config
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.airgradient.const import DOMAIN
@ -16,6 +17,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
@ -99,3 +101,36 @@ async def test_cloud_creates_no_switch(
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
@pytest.mark.parametrize(
("exception", "error_message"),
[
(
AirGradientConnectionError("Something happened"),
"An error occurred while communicating with the Airgradient device: Something happened",
),
(
AirGradientError("Something else happened"),
"An unknown error occurred while communicating with the Airgradient device: Something else happened",
),
],
)
async def test_exception_handling(
hass: HomeAssistant,
mock_airgradient_client: AsyncMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
error_message: str,
) -> None:
"""Test exception handling."""
await setup_integration(hass, mock_config_entry)
mock_airgradient_client.enable_sharing_data.side_effect = exception
with pytest.raises(HomeAssistantError, match=error_message):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
target={ATTR_ENTITY_ID: "switch.airgradient_post_data_to_airgradient"},
blocking=True,
)