Add error handling to enphase_envoy switch platform action (#136837)
* Add error handling to enphase_envoy switch platform action * Use decorators for exception handlingpull/132676/head^2
parent
a8175b785f
commit
97fcbed6e0
|
@ -2,13 +2,22 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from pyenphase import EnvoyData
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from httpx import HTTPError
|
||||
from pyenphase import EnvoyData
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EnphaseUpdateCoordinator
|
||||
|
||||
ACTIONERRORS = (EnvoyError, HTTPError)
|
||||
|
||||
|
||||
class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator]):
|
||||
"""Defines a base envoy entity."""
|
||||
|
@ -33,3 +42,29 @@ class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator]):
|
|||
data = self.coordinator.envoy.data
|
||||
assert data is not None
|
||||
return data
|
||||
|
||||
|
||||
def exception_handler[_EntityT: EnvoyBaseEntity, **_P](
|
||||
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
|
||||
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||
"""Decorate Enphase Envoy calls to handle exceptions.
|
||||
|
||||
A decorator that wraps the passed in function, catches enphase_envoy errors.
|
||||
"""
|
||||
|
||||
async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except ACTIONERRORS as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="action_error",
|
||||
translation_placeholders={
|
||||
"host": self.coordinator.envoy.host,
|
||||
"args": error.args[0],
|
||||
"action": func.__name__,
|
||||
"entity": self.entity_id,
|
||||
},
|
||||
) from error
|
||||
|
||||
return handler
|
||||
|
|
|
@ -400,6 +400,9 @@
|
|||
},
|
||||
"envoy_error": {
|
||||
"message": "Error communicating with Envoy API on {host}: {args}"
|
||||
},
|
||||
"action_error": {
|
||||
"message": "Failed to execute {action} for {entity}, host: {host}: {args}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
|
||||
from .entity import EnvoyBaseEntity
|
||||
from .entity import EnvoyBaseEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
@ -147,11 +147,13 @@ class EnvoyEnpowerSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
|||
assert enpower is not None
|
||||
return self.entity_description.value_fn(enpower)
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the Enpower switch."""
|
||||
await self.entity_description.turn_on_fn(self.envoy)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the Enpower switch."""
|
||||
await self.entity_description.turn_off_fn(self.envoy)
|
||||
|
@ -195,11 +197,13 @@ class EnvoyDryContactSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
|||
assert relay is not None
|
||||
return self.entity_description.value_fn(relay)
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on (close) the dry contact."""
|
||||
if await self.entity_description.turn_on_fn(self.envoy, self.relay_id):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off (open) the dry contact."""
|
||||
if await self.entity_description.turn_off_fn(self.envoy, self.relay_id):
|
||||
|
@ -252,11 +256,13 @@ class EnvoyStorageSettingsSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
|||
assert self.data.tariff.storage_settings is not None
|
||||
return self.entity_description.value_fn(self.data.tariff.storage_settings)
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the storage settings switch."""
|
||||
await self.entity_description.turn_on_fn(self.envoy)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the storage switch."""
|
||||
await self.entity_description.turn_off_fn(self.envoy)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
|
@ -16,6 +17,7 @@ from homeassistant.const import (
|
|||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
@ -112,6 +114,46 @@ async def test_switch_grid_operation(
|
|||
mock_envoy.go_off_grid.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_envoy", ["envoy_metered_batt_relay"], indirect=True)
|
||||
async def test_switch_grid_operation_with_error(
|
||||
hass: HomeAssistant,
|
||||
mock_envoy: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test switch platform operation for grid switches when error occurs."""
|
||||
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
sn = mock_envoy.data.enpower.serial_number
|
||||
test_entity = f"{Platform.SWITCH}.enpower_{sn}_grid_enabled"
|
||||
|
||||
mock_envoy.go_off_grid.side_effect = EnvoyError("Test")
|
||||
mock_envoy.go_on_grid.side_effect = EnvoyError("Test")
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"Failed to execute async_turn_off for {test_entity}, host",
|
||||
):
|
||||
# test grid status switch operation
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: test_entity},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"Failed to execute async_turn_on for {test_entity}, host",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: test_entity},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_envoy", "use_serial"),
|
||||
[
|
||||
|
@ -165,6 +207,53 @@ async def test_switch_charge_from_grid_operation(
|
|||
mock_envoy.disable_charge_from_grid.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_envoy", "use_serial"),
|
||||
[
|
||||
("envoy_metered_batt_relay", "enpower_654321"),
|
||||
("envoy_eu_batt", "envoy_1234"),
|
||||
],
|
||||
indirect=["mock_envoy"],
|
||||
)
|
||||
async def test_switch_charge_from_grid_operation_with_error(
|
||||
hass: HomeAssistant,
|
||||
mock_envoy: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
use_serial: str,
|
||||
) -> None:
|
||||
"""Test switch platform operation for charge from grid switches."""
|
||||
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
test_entity = f"{Platform.SWITCH}.{use_serial}_charge_from_grid"
|
||||
|
||||
mock_envoy.disable_charge_from_grid.side_effect = EnvoyError("Test")
|
||||
mock_envoy.enable_charge_from_grid.side_effect = EnvoyError("Test")
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"Failed to execute async_turn_off for {test_entity}, host",
|
||||
):
|
||||
# test grid status switch operation
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: test_entity},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"Failed to execute async_turn_on for {test_entity}, host",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: test_entity},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_envoy", "entity_states"),
|
||||
[
|
||||
|
@ -232,3 +321,51 @@ async def test_switch_relay_operation(
|
|||
assert mock_envoy.close_dry_contact.await_count == close_count
|
||||
mock_envoy.open_dry_contact.reset_mock()
|
||||
mock_envoy.close_dry_contact.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_envoy", "relay"),
|
||||
[("envoy_metered_batt_relay", "NC1")],
|
||||
indirect=["mock_envoy"],
|
||||
)
|
||||
async def test_switch_relay_operation_with_error(
|
||||
hass: HomeAssistant,
|
||||
mock_envoy: AsyncMock,
|
||||
config_entry: MockConfigEntry,
|
||||
relay: str,
|
||||
) -> None:
|
||||
"""Test enphase_envoy switch relay entities operation."""
|
||||
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
entity_base = f"{Platform.SWITCH}."
|
||||
|
||||
assert (dry_contact := mock_envoy.data.dry_contact_settings[relay])
|
||||
assert (name := dry_contact.load_name.lower().replace(" ", "_"))
|
||||
|
||||
test_entity = f"{entity_base}{name}"
|
||||
|
||||
mock_envoy.close_dry_contact.side_effect = EnvoyError("Test")
|
||||
mock_envoy.open_dry_contact.side_effect = EnvoyError("Test")
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"Failed to execute async_turn_off for {test_entity}, host",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: test_entity},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"Failed to execute async_turn_on for {test_entity}, host",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: test_entity},
|
||||
blocking=True,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue