mirror of
https://github.com/home-assistant/core.git
synced 2026-09-27 09:58:07 -04:00
Start Imou reauthentication when an entity action rejects the credentials (#181005)
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
from typing import override
|
||||
|
||||
from pyimouapi.const import PARAM_RESTART_DEVICE
|
||||
from pyimouapi.exceptions import ImouException
|
||||
from pyimouapi.ha_device import ImouHaDevice
|
||||
|
||||
from homeassistant.components.button import (
|
||||
@@ -12,12 +11,12 @@ from homeassistant.components.button import (
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PTZ_MOVE_DURATION_MS, imou_device_identifier
|
||||
from .const import PTZ_MOVE_DURATION_MS, imou_device_identifier
|
||||
from .coordinator import ImouConfigEntry, ImouDataUpdateCoordinator
|
||||
from .entity import ImouEntity
|
||||
from .helpers import async_wrap_imou_command
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
# Button types not yet exported by pyimouapi (keep module-local).
|
||||
@@ -100,18 +99,12 @@ class ImouButton(ImouEntity, ButtonEntity):
|
||||
entity_description: ButtonEntityDescription
|
||||
|
||||
@override
|
||||
@async_wrap_imou_command("press_button_failed")
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press."""
|
||||
duration = PTZ_MOVE_DURATION_MS if self._entity_type in PTZ_BUTTON_TYPES else 0
|
||||
try:
|
||||
await self.coordinator.device_manager.async_press_button(
|
||||
self.device,
|
||||
self._entity_type,
|
||||
duration,
|
||||
)
|
||||
except ImouException as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="press_button_failed",
|
||||
translation_placeholders={"error": e.message},
|
||||
) from e
|
||||
await self.coordinator.device_manager.async_press_button(
|
||||
self.device,
|
||||
self._entity_type,
|
||||
duration,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from pyimouapi.const import PARAM_HD, PARAM_MOTION_DETECT, PARAM_STATE
|
||||
from pyimouapi.exceptions import ImouException
|
||||
from pyimouapi.ha_device import ImouHaDevice
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
@@ -13,12 +12,12 @@ from homeassistant.components.camera import (
|
||||
CameraEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PARAM_HEADER_DETECT, imou_device_identifier
|
||||
from .const import PARAM_HEADER_DETECT, imou_device_identifier
|
||||
from .coordinator import ImouConfigEntry, ImouDataUpdateCoordinator
|
||||
from .entity import ImouEntity
|
||||
from .helpers import async_wrap_imou_command
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -89,37 +88,25 @@ class ImouCamera(ImouEntity, Camera):
|
||||
super().__init__(coordinator, description, device)
|
||||
|
||||
@override
|
||||
@async_wrap_imou_command("get_stream_failed")
|
||||
async def stream_source(self) -> str | None:
|
||||
"""Return the live stream URL from the Imou cloud."""
|
||||
try:
|
||||
return await self.coordinator.device_manager.async_get_device_stream(
|
||||
self.device,
|
||||
self.entity_description.resolution,
|
||||
PYIMOUAPI_LIVE_PROTOCOL,
|
||||
)
|
||||
except ImouException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="get_stream_failed",
|
||||
translation_placeholders={"error": err.message},
|
||||
) from err
|
||||
return await self.coordinator.device_manager.async_get_device_stream(
|
||||
self.device,
|
||||
self.entity_description.resolution,
|
||||
PYIMOUAPI_LIVE_PROTOCOL,
|
||||
)
|
||||
|
||||
@override
|
||||
@async_wrap_imou_command("get_image_failed")
|
||||
async def async_camera_image(
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return bytes of camera image."""
|
||||
try:
|
||||
return await self.coordinator.device_manager.async_get_device_image(
|
||||
self.device,
|
||||
PYIMOUAPI_SNAPSHOT_WAIT_SECONDS,
|
||||
)
|
||||
except ImouException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="get_image_failed",
|
||||
translation_placeholders={"error": err.message},
|
||||
) from err
|
||||
return await self.coordinator.device_manager.async_get_device_image(
|
||||
self.device,
|
||||
PYIMOUAPI_SNAPSHOT_WAIT_SECONDS,
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Helpers for Imou."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from pyimouapi.exceptions import ImouException, InvalidAppIdOrSecretException
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import ImouEntity
|
||||
|
||||
|
||||
def async_wrap_imou_command[_T: ImouEntity, **_P, _R](
|
||||
error_key: str,
|
||||
) -> Callable[
|
||||
[Callable[Concatenate[_T, _P], Awaitable[_R]]],
|
||||
Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]],
|
||||
]:
|
||||
"""Wrap an Imou command and start reauthentication when credentials are rejected."""
|
||||
|
||||
def decorator(
|
||||
func: Callable[Concatenate[_T, _P], Awaitable[_R]],
|
||||
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]]:
|
||||
@wraps(func)
|
||||
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except InvalidAppIdOrSecretException as err:
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
) from err
|
||||
except ImouException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=error_key,
|
||||
) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@@ -8,18 +8,17 @@ from pyimouapi.const import (
|
||||
PARAM_NIGHT_VISION_MODE,
|
||||
PARAM_OPTIONS,
|
||||
)
|
||||
from pyimouapi.exceptions import ImouException
|
||||
from pyimouapi.ha_device import ImouHaDevice
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, imou_device_identifier
|
||||
from .const import imou_device_identifier
|
||||
from .coordinator import ImouConfigEntry, ImouDataUpdateCoordinator
|
||||
from .entity import ImouEntity
|
||||
from .helpers import async_wrap_imou_command
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -87,18 +86,12 @@ class ImouSelect(ImouEntity, SelectEntity):
|
||||
return self.device.selects[self._entity_type][PARAM_CURRENT_OPTION]
|
||||
|
||||
@override
|
||||
@async_wrap_imou_command("select_option_failed")
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
try:
|
||||
await self.coordinator.device_manager.async_select_option(
|
||||
self.device,
|
||||
self._entity_type,
|
||||
option,
|
||||
)
|
||||
except ImouException as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="select_option_failed",
|
||||
translation_placeholders={"error": e.message},
|
||||
) from e
|
||||
await self.coordinator.device_manager.async_select_option(
|
||||
self.device,
|
||||
self._entity_type,
|
||||
option,
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -138,22 +138,22 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"get_image_failed": {
|
||||
"message": "Could not get a snapshot from Imou: {error}"
|
||||
"message": "Could not get a snapshot from Imou"
|
||||
},
|
||||
"get_stream_failed": {
|
||||
"message": "Could not get the live stream URL from Imou: {error}"
|
||||
"message": "Could not get the live stream URL from Imou"
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "Imou rejected the App ID and App secret"
|
||||
},
|
||||
"press_button_failed": {
|
||||
"message": "Imou rejected the button press: {error}"
|
||||
"message": "Imou rejected the button press"
|
||||
},
|
||||
"select_option_failed": {
|
||||
"message": "Imou rejected the new option: {error}"
|
||||
"message": "Imou rejected the new option"
|
||||
},
|
||||
"switch_operation_failed": {
|
||||
"message": "Imou rejected the switch change: {error}"
|
||||
"message": "Imou rejected the switch change"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from typing import Any, override
|
||||
|
||||
from pyimouapi.const import PARAM_MOTION_DETECT, PARAM_STATE
|
||||
from pyimouapi.exceptions import ImouException
|
||||
from pyimouapi.ha_device import ImouHaDevice
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
@@ -12,11 +11,9 @@ from homeassistant.components.switch import (
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PARAM_AB_ALARM_SOUND,
|
||||
PARAM_AUDIO_ENCODE_CONTROL,
|
||||
PARAM_CLOSE_CAMERA,
|
||||
@@ -28,6 +25,7 @@ from .const import (
|
||||
)
|
||||
from .coordinator import ImouConfigEntry, ImouDataUpdateCoordinator
|
||||
from .entity import ImouEntity
|
||||
from .helpers import async_wrap_imou_command
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -122,18 +120,12 @@ class ImouSwitch(ImouEntity, SwitchEntity):
|
||||
"""Turn the switch off."""
|
||||
await self._async_switch_operation(False)
|
||||
|
||||
@async_wrap_imou_command("switch_operation_failed")
|
||||
async def _async_switch_operation(self, enable: bool) -> None:
|
||||
"""Call the vendor library to change switch state."""
|
||||
try:
|
||||
await self.coordinator.device_manager.async_switch_operation(
|
||||
self.device,
|
||||
self._entity_type,
|
||||
enable,
|
||||
)
|
||||
except ImouException as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_operation_failed",
|
||||
translation_placeholders={"error": e.message},
|
||||
) from e
|
||||
await self.coordinator.device_manager.async_switch_operation(
|
||||
self.device,
|
||||
self._entity_type,
|
||||
enable,
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyimouapi.const import PARAM_STATE, PARAM_STATUS
|
||||
from pyimouapi.exceptions import ImouException
|
||||
from pyimouapi.exceptions import ImouException, InvalidAppIdOrSecretException
|
||||
from pyimouapi.ha_device import DeviceStatus, ImouHaDevice
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRE
|
||||
from homeassistant.components.imou.button import PARAM_MUTE, PARAM_PTZ_UP
|
||||
from homeassistant.components.imou.const import PTZ_MOVE_DURATION_MS
|
||||
from homeassistant.components.imou.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -130,8 +131,30 @@ async def test_press_button_service_propagates_api_error(
|
||||
|
||||
entity_id = hass.states.async_all("button")[0].entity_id
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="Imou rejected the button press"):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_press_button_invalid_auth_starts_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_imou_ha_device_manager: MagicMock,
|
||||
) -> None:
|
||||
"""Rejected credentials while pressing a button start reauthentication."""
|
||||
mock_imou_ha_device_manager.async_press_button.side_effect = (
|
||||
InvalidAppIdOrSecretException("fail")
|
||||
)
|
||||
|
||||
entity_id = hass.states.async_all("button")[0].entity_id
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match="Imou rejected the button press: cloud failure"
|
||||
HomeAssistantError, match="Imou rejected the App ID and App secret"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
@@ -139,6 +162,10 @@ async def test_press_button_service_propagates_api_error(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyimouapi.const import PARAM_HD, PARAM_MOTION_DETECT, PARAM_STATE
|
||||
from pyimouapi.exceptions import ImouException
|
||||
from pyimouapi.exceptions import ImouException, InvalidAppIdOrSecretException
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.imou.camera import (
|
||||
)
|
||||
from homeassistant.components.imou.const import PARAM_HEADER_DETECT
|
||||
from homeassistant.components.imou.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -299,11 +300,48 @@ async def test_camera_stream_source_propagates_api_error(
|
||||
entity_id = _camera_entity_id(entity_registry, mock_config_entry)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Could not get the live stream URL from Imou: stream failure",
|
||||
match="Could not get the live stream URL from Imou",
|
||||
):
|
||||
await async_get_stream_source(hass, entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"imou_mock_devices",
|
||||
[
|
||||
[
|
||||
create_online_device(
|
||||
"d1",
|
||||
"Device 1",
|
||||
channel_id="1",
|
||||
button_keys=(),
|
||||
)
|
||||
]
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_camera_stream_source_invalid_auth_starts_reauth(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_imou_ha_device_manager: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Rejected credentials while fetching a stream start reauthentication."""
|
||||
mock_imou_ha_device_manager.async_get_device_stream.side_effect = (
|
||||
InvalidAppIdOrSecretException("fail")
|
||||
)
|
||||
|
||||
entity_id = _camera_entity_id(entity_registry, mock_config_entry)
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match="Imou rejected the App ID and App secret"
|
||||
):
|
||||
await async_get_stream_source(hass, entity_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"imou_mock_devices",
|
||||
[
|
||||
@@ -333,11 +371,48 @@ async def test_camera_image_propagates_api_error(
|
||||
entity_id = _camera_entity_id(entity_registry, mock_config_entry)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Could not get a snapshot from Imou: image failure",
|
||||
match="Could not get a snapshot from Imou",
|
||||
):
|
||||
await async_get_image(hass, entity_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"imou_mock_devices",
|
||||
[
|
||||
[
|
||||
create_online_device(
|
||||
"d1",
|
||||
"Device 1",
|
||||
channel_id="1",
|
||||
button_keys=(),
|
||||
)
|
||||
]
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_camera_image_invalid_auth_starts_reauth(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_imou_ha_device_manager: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Rejected credentials while fetching a snapshot start reauthentication."""
|
||||
mock_imou_ha_device_manager.async_get_device_image.side_effect = (
|
||||
InvalidAppIdOrSecretException("fail")
|
||||
)
|
||||
|
||||
entity_id = _camera_entity_id(entity_registry, mock_config_entry)
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match="Imou rejected the App ID and App secret"
|
||||
):
|
||||
await async_get_image(hass, entity_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"imou_mock_devices",
|
||||
[
|
||||
|
||||
@@ -11,13 +11,14 @@ from pyimouapi.const import (
|
||||
PARAM_STATE,
|
||||
PARAM_STATUS,
|
||||
)
|
||||
from pyimouapi.exceptions import ImouException
|
||||
from pyimouapi.exceptions import ImouException, InvalidAppIdOrSecretException
|
||||
from pyimouapi.ha_device import DeviceStatus, ImouHaDevice
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.imou.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_OPTION,
|
||||
@@ -145,8 +146,39 @@ async def test_select_option_propagates_api_error(
|
||||
if entry.unique_id == "d1$device_volume"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="Imou rejected the new option"):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: volume_entry.entity_id, ATTR_OPTION: "high"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platforms", [[Platform.SELECT]], indirect=True)
|
||||
@pytest.mark.parametrize("imou_mock_devices", [select_mock_devices], indirect=True)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_select_option_invalid_auth_starts_reauth(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_imou_ha_device_manager: MagicMock,
|
||||
) -> None:
|
||||
"""Rejected credentials while changing a select start reauthentication."""
|
||||
mock_imou_ha_device_manager.async_select_option.side_effect = (
|
||||
InvalidAppIdOrSecretException("fail")
|
||||
)
|
||||
|
||||
volume_entry = next(
|
||||
entry
|
||||
for entry in er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
if entry.unique_id == "d1$device_volume"
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match="Imou rejected the new option: cloud failure"
|
||||
HomeAssistantError, match="Imou rejected the App ID and App secret"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
@@ -154,6 +186,10 @@ async def test_select_option_propagates_api_error(
|
||||
{ATTR_ENTITY_ID: volume_entry.entity_id, ATTR_OPTION: "high"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platforms", [[Platform.SELECT]], indirect=True)
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyimouapi.const import PARAM_MOTION_DETECT, PARAM_STATE, PARAM_STATUS
|
||||
from pyimouapi.exceptions import ImouException
|
||||
from pyimouapi.exceptions import ImouException, InvalidAppIdOrSecretException
|
||||
from pyimouapi.ha_device import DeviceStatus, ImouHaDevice
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -12,6 +12,7 @@ from syrupy.assertion import SnapshotAssertion
|
||||
from homeassistant.components.imou.const import PARAM_HEADER_DETECT
|
||||
from homeassistant.components.imou.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
@@ -173,8 +174,31 @@ async def test_turn_on_service_propagates_api_error(
|
||||
|
||||
entity_id = hass.states.async_all("switch")[0].entity_id
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="Imou rejected the switch change"):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("imou_mock_devices", [SWITCH_MOCK_DEVICES], indirect=True)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_turn_on_invalid_auth_starts_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_imou_ha_device_manager: MagicMock,
|
||||
) -> None:
|
||||
"""Rejected credentials while toggling a switch start reauthentication."""
|
||||
mock_imou_ha_device_manager.async_switch_operation.side_effect = (
|
||||
InvalidAppIdOrSecretException("fail")
|
||||
)
|
||||
|
||||
entity_id = hass.states.async_all("switch")[0].entity_id
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match="Imou rejected the switch change: cloud failure"
|
||||
HomeAssistantError, match="Imou rejected the App ID and App secret"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
@@ -182,6 +206,10 @@ async def test_turn_on_service_propagates_api_error(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user