Wrap tplink exceptions caused by user actions inside HomeAssistantError (#114919)
This commit is contained in:
parent
0a57641f3f
commit
bf3eb463ae
5 changed files with 184 additions and 4 deletions
|
@ -7,6 +7,7 @@ import logging
|
||||||
|
|
||||||
from kasa import AuthenticationException, SmartDevice, SmartDeviceException
|
from kasa import AuthenticationException, SmartDevice, SmartDeviceException
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
|
@ -20,6 +21,8 @@ REQUEST_REFRESH_DELAY = 0.35
|
||||||
class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
"""DataUpdateCoordinator to gather data for a specific TPLink device."""
|
"""DataUpdateCoordinator to gather data for a specific TPLink device."""
|
||||||
|
|
||||||
|
config_entry: config_entries.ConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
|
@ -5,8 +5,14 @@ from __future__ import annotations
|
||||||
from collections.abc import Awaitable, Callable, Coroutine
|
from collections.abc import Awaitable, Callable, Coroutine
|
||||||
from typing import Any, Concatenate, ParamSpec, TypeVar
|
from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||||
|
|
||||||
from kasa import SmartDevice
|
from kasa import (
|
||||||
|
AuthenticationException,
|
||||||
|
SmartDevice,
|
||||||
|
SmartDeviceException,
|
||||||
|
TimeoutException,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
@ -21,10 +27,39 @@ _P = ParamSpec("_P")
|
||||||
def async_refresh_after(
|
def async_refresh_after(
|
||||||
func: Callable[Concatenate[_T, _P], Awaitable[None]],
|
func: Callable[Concatenate[_T, _P], Awaitable[None]],
|
||||||
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
|
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
|
||||||
"""Define a wrapper to refresh after."""
|
"""Define a wrapper to raise HA errors and refresh after."""
|
||||||
|
|
||||||
async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||||
await func(self, *args, **kwargs)
|
try:
|
||||||
|
await func(self, *args, **kwargs)
|
||||||
|
except AuthenticationException as ex:
|
||||||
|
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="device_authentication",
|
||||||
|
translation_placeholders={
|
||||||
|
"func": func.__name__,
|
||||||
|
"exc": str(ex),
|
||||||
|
},
|
||||||
|
) from ex
|
||||||
|
except TimeoutException as ex:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="device_timeout",
|
||||||
|
translation_placeholders={
|
||||||
|
"func": func.__name__,
|
||||||
|
"exc": str(ex),
|
||||||
|
},
|
||||||
|
) from ex
|
||||||
|
except SmartDeviceException as ex:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="device_error",
|
||||||
|
translation_placeholders={
|
||||||
|
"func": func.__name__,
|
||||||
|
"exc": str(ex),
|
||||||
|
},
|
||||||
|
) from ex
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
return _async_wrap
|
return _async_wrap
|
||||||
|
|
|
@ -184,5 +184,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"device_timeout": {
|
||||||
|
"message": "Timeout communicating with the device {func}: {exc}"
|
||||||
|
},
|
||||||
|
"device_error": {
|
||||||
|
"message": "Unable to communicate with the device {func}: {exc}"
|
||||||
|
},
|
||||||
|
"device_authentication": {
|
||||||
|
"message": "Device authentication error {func}: {exc}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
|
from kasa import AuthenticationException, SmartDeviceException, TimeoutException
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import tplink
|
from homeassistant.components import tplink
|
||||||
|
@ -24,8 +25,10 @@ from homeassistant.components.light import (
|
||||||
DOMAIN as LIGHT_DOMAIN,
|
DOMAIN as LIGHT_DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.components.tplink.const import DOMAIN
|
from homeassistant.components.tplink.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_ON
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_ON
|
||||||
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
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
@ -730,3 +733,66 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
strip.set_custom_effect.reset_mock()
|
strip.set_custom_effect.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception_type", "msg", "reauth_expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
AuthenticationException,
|
||||||
|
"Device authentication error async_turn_on: test error",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeoutException,
|
||||||
|
"Timeout communicating with the device async_turn_on: test error",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SmartDeviceException,
|
||||||
|
"Unable to communicate with the device async_turn_on: test error",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=["Authentication", "Timeout", "Other"],
|
||||||
|
)
|
||||||
|
async def test_light_errors_when_turned_on(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
exception_type,
|
||||||
|
msg,
|
||||||
|
reauth_expected,
|
||||||
|
) -> None:
|
||||||
|
"""Tests the light wraps errors correctly."""
|
||||||
|
already_migrated_config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||||
|
)
|
||||||
|
already_migrated_config_entry.add_to_hass(hass)
|
||||||
|
bulb = _mocked_bulb()
|
||||||
|
bulb.turn_on.side_effect = exception_type(msg)
|
||||||
|
|
||||||
|
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||||
|
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_id = "light.my_bulb"
|
||||||
|
|
||||||
|
assert not any(
|
||||||
|
already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match=msg):
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert bulb.turn_on.call_count == 1
|
||||||
|
assert (
|
||||||
|
any(
|
||||||
|
flow
|
||||||
|
for flow in already_migrated_config_entry.async_get_active_flows(
|
||||||
|
hass, {SOURCE_REAUTH}
|
||||||
|
)
|
||||||
|
if flow["handler"] == tplink.DOMAIN
|
||||||
|
)
|
||||||
|
== reauth_expected
|
||||||
|
)
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from kasa import SmartDeviceException
|
from kasa import AuthenticationException, SmartDeviceException, TimeoutException
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import tplink
|
from homeassistant.components import tplink
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
from homeassistant.components.tplink.const import DOMAIN
|
from homeassistant.components.tplink.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
@ -17,6 +18,7 @@ from homeassistant.const import (
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
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
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util, slugify
|
from homeassistant.util import dt as dt_util, slugify
|
||||||
|
@ -202,3 +204,66 @@ async def test_strip_unique_ids(hass: HomeAssistant) -> None:
|
||||||
assert (
|
assert (
|
||||||
entity_registry.async_get(entity_id).unique_id == f"PLUG{plug_id}DEVICEID"
|
entity_registry.async_get(entity_id).unique_id == f"PLUG{plug_id}DEVICEID"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception_type", "msg", "reauth_expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
AuthenticationException,
|
||||||
|
"Device authentication error async_turn_on: test error",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TimeoutException,
|
||||||
|
"Timeout communicating with the device async_turn_on: test error",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SmartDeviceException,
|
||||||
|
"Unable to communicate with the device async_turn_on: test error",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=["Authentication", "Timeout", "Other"],
|
||||||
|
)
|
||||||
|
async def test_plug_errors_when_turned_on(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
exception_type,
|
||||||
|
msg,
|
||||||
|
reauth_expected,
|
||||||
|
) -> None:
|
||||||
|
"""Tests the plug wraps errors correctly."""
|
||||||
|
already_migrated_config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||||
|
)
|
||||||
|
already_migrated_config_entry.add_to_hass(hass)
|
||||||
|
plug = _mocked_plug()
|
||||||
|
plug.turn_on.side_effect = exception_type("test error")
|
||||||
|
|
||||||
|
with _patch_discovery(device=plug), _patch_connect(device=plug):
|
||||||
|
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_id = "switch.my_plug"
|
||||||
|
|
||||||
|
assert not any(
|
||||||
|
already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match=msg):
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert plug.turn_on.call_count == 1
|
||||||
|
assert (
|
||||||
|
any(
|
||||||
|
flow
|
||||||
|
for flow in already_migrated_config_entry.async_get_active_flows(
|
||||||
|
hass, {SOURCE_REAUTH}
|
||||||
|
)
|
||||||
|
if flow["handler"] == tplink.DOMAIN
|
||||||
|
)
|
||||||
|
== reauth_expected
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue