Use auth token in Ezviz (#54663)
* Initial commit * Revert "Initial commit" This reverts commit 452027f1a3c1be186cedd4115cea6928917c9467. * Change ezviz to token auth * Bump API version. * Add fix for token expired. Fix options update and unload. * Fix tests (PLATFORM to PLATFORM_BY_TYPE) * Uses and stores token only, added reauth step when token expires. * Add tests MFA code exceptions. * Fix tests. * Remove redundant try/except blocks. * Rebase fixes. * Fix errors in reauth config flow * Implement recommendations * Fix typing error in config_flow * Fix tests after rebase, readd camera check on init * Change to platform setup * Cleanup init. * Test for MFA required under user form * Remove useless if block. * Fix formating after rebase * Fix formating. * No longer stored in the repository --------- Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
4c21caa917
commit
93d1961aae
9 changed files with 535 additions and 217 deletions
|
@ -2,26 +2,26 @@
|
|||
import logging
|
||||
|
||||
from pyezviz.client import EzvizClient
|
||||
from pyezviz.exceptions import HTTPError, InvalidURL, PyEzvizError
|
||||
from pyezviz.exceptions import (
|
||||
EzvizAuthTokenExpired,
|
||||
EzvizAuthVerificationCode,
|
||||
HTTPError,
|
||||
InvalidURL,
|
||||
PyEzvizError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_TIMEOUT,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.const import CONF_TIMEOUT, CONF_TYPE, CONF_URL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import (
|
||||
ATTR_TYPE_CAMERA,
|
||||
ATTR_TYPE_CLOUD,
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
CONF_RFSESSION_ID,
|
||||
CONF_SESSION_ID,
|
||||
DATA_COORDINATOR,
|
||||
DATA_UNDO_UPDATE_LISTENER,
|
||||
DEFAULT_FFMPEG_ARGUMENTS,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
|
@ -30,17 +30,22 @@ from .coordinator import EzvizDataUpdateCoordinator
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CAMERA,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
PLATFORMS_BY_TYPE: dict[str, list] = {
|
||||
ATTR_TYPE_CAMERA: [],
|
||||
ATTR_TYPE_CLOUD: [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CAMERA,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up EZVIZ from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
sensor_type: str = entry.data[CONF_TYPE]
|
||||
ezviz_client = None
|
||||
|
||||
if not entry.options:
|
||||
options = {
|
||||
|
@ -50,69 +55,71 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
hass.config_entries.async_update_entry(entry, options=options)
|
||||
|
||||
if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA:
|
||||
if hass.data.get(DOMAIN):
|
||||
# Should only execute on addition of new camera entry.
|
||||
# Fetch Entry id of main account and reload it.
|
||||
for item in hass.config_entries.async_entries():
|
||||
if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD:
|
||||
_LOGGER.info("Reload EZVIZ integration with new camera rtsp entry")
|
||||
await hass.config_entries.async_reload(item.entry_id)
|
||||
# Initialize EZVIZ cloud entities
|
||||
if PLATFORMS_BY_TYPE[sensor_type]:
|
||||
# Initiate reauth config flow if account token if not present.
|
||||
if not entry.data.get(CONF_SESSION_ID):
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
return True
|
||||
|
||||
try:
|
||||
ezviz_client = await hass.async_add_executor_job(
|
||||
_get_ezviz_client_instance, entry
|
||||
ezviz_client = EzvizClient(
|
||||
token={
|
||||
CONF_SESSION_ID: entry.data.get(CONF_SESSION_ID),
|
||||
CONF_RFSESSION_ID: entry.data.get(CONF_RFSESSION_ID),
|
||||
"api_url": entry.data.get(CONF_URL),
|
||||
},
|
||||
timeout=entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
|
||||
)
|
||||
except (InvalidURL, HTTPError, PyEzvizError) as error:
|
||||
_LOGGER.error("Unable to connect to EZVIZ service: %s", str(error))
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
coordinator = EzvizDataUpdateCoordinator(
|
||||
hass, api=ezviz_client, api_timeout=entry.options[CONF_TIMEOUT]
|
||||
try:
|
||||
await hass.async_add_executor_job(ezviz_client.login)
|
||||
|
||||
except (EzvizAuthTokenExpired, EzvizAuthVerificationCode) as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
|
||||
except (InvalidURL, HTTPError, PyEzvizError) as error:
|
||||
_LOGGER.error("Unable to connect to Ezviz service: %s", str(error))
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
coordinator = EzvizDataUpdateCoordinator(
|
||||
hass, api=ezviz_client, api_timeout=entry.options[CONF_TIMEOUT]
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator}
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
|
||||
# Check EZVIZ cloud account entity is present, reload cloud account entities for camera entity change to take effect.
|
||||
# Cameras are accessed via local RTSP stream with unique credentials per camera.
|
||||
# Separate camera entities allow for credential changes per camera.
|
||||
if sensor_type == ATTR_TYPE_CAMERA and hass.data[DOMAIN]:
|
||||
for item in hass.config_entries.async_entries(domain=DOMAIN):
|
||||
if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD:
|
||||
_LOGGER.info("Reload Ezviz main account with camera entry")
|
||||
await hass.config_entries.async_reload(item.entry_id)
|
||||
return True
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, PLATFORMS_BY_TYPE[sensor_type]
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_COORDINATOR: coordinator,
|
||||
DATA_UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
sensor_type = entry.data[CONF_TYPE]
|
||||
|
||||
if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA:
|
||||
return True
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]()
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
entry, PLATFORMS_BY_TYPE[sensor_type]
|
||||
)
|
||||
if sensor_type == ATTR_TYPE_CLOUD and unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
def _get_ezviz_client_instance(entry: ConfigEntry) -> EzvizClient:
|
||||
"""Initialize a new instance of EzvizClientApi."""
|
||||
ezviz_client = EzvizClient(
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_URL],
|
||||
entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
|
||||
)
|
||||
ezviz_client.login()
|
||||
return ezviz_client
|
||||
|
|
|
@ -34,7 +34,6 @@ from .const import (
|
|||
DATA_COORDINATOR,
|
||||
DEFAULT_CAMERA_USERNAME,
|
||||
DEFAULT_FFMPEG_ARGUMENTS,
|
||||
DEFAULT_RTSP_PORT,
|
||||
DIR_DOWN,
|
||||
DIR_LEFT,
|
||||
DIR_RIGHT,
|
||||
|
@ -70,24 +69,17 @@ async def async_setup_entry(
|
|||
if item.unique_id == camera and item.source != SOURCE_IGNORE
|
||||
]
|
||||
|
||||
# There seem to be a bug related to localRtspPort in EZVIZ API.
|
||||
local_rtsp_port = (
|
||||
value["local_rtsp_port"]
|
||||
if value["local_rtsp_port"] != 0
|
||||
else DEFAULT_RTSP_PORT
|
||||
)
|
||||
|
||||
if camera_rtsp_entry:
|
||||
ffmpeg_arguments = camera_rtsp_entry[0].options[CONF_FFMPEG_ARGUMENTS]
|
||||
camera_username = camera_rtsp_entry[0].data[CONF_USERNAME]
|
||||
camera_password = camera_rtsp_entry[0].data[CONF_PASSWORD]
|
||||
|
||||
camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{value['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}"
|
||||
camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{value['local_ip']}:{value['local_rtsp_port']}{ffmpeg_arguments}"
|
||||
_LOGGER.debug(
|
||||
"Configuring Camera %s with ip: %s rtsp port: %s ffmpeg arguments: %s",
|
||||
camera,
|
||||
value["local_ip"],
|
||||
local_rtsp_port,
|
||||
value["local_rtsp_port"],
|
||||
ffmpeg_arguments,
|
||||
)
|
||||
|
||||
|
@ -123,7 +115,7 @@ async def async_setup_entry(
|
|||
camera_username,
|
||||
camera_password,
|
||||
camera_rtsp_stream,
|
||||
local_rtsp_port,
|
||||
value["local_rtsp_port"],
|
||||
ffmpeg_arguments,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
"""Config flow for ezviz."""
|
||||
"""Config flow for EZVIZ."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyezviz.client import EzvizClient
|
||||
from pyezviz.exceptions import (
|
||||
AuthTestResultFailed,
|
||||
HTTPError,
|
||||
EzvizAuthVerificationCode,
|
||||
InvalidHost,
|
||||
InvalidURL,
|
||||
PyEzvizError,
|
||||
|
@ -25,12 +27,15 @@ from homeassistant.const import (
|
|||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import (
|
||||
ATTR_SERIAL,
|
||||
ATTR_TYPE_CAMERA,
|
||||
ATTR_TYPE_CLOUD,
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
CONF_RFSESSION_ID,
|
||||
CONF_SESSION_ID,
|
||||
DEFAULT_CAMERA_USERNAME,
|
||||
DEFAULT_FFMPEG_ARGUMENTS,
|
||||
DEFAULT_TIMEOUT,
|
||||
|
@ -40,23 +45,37 @@ from .const import (
|
|||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_OPTIONS = {
|
||||
CONF_FFMPEG_ARGUMENTS: DEFAULT_FFMPEG_ARGUMENTS,
|
||||
CONF_TIMEOUT: DEFAULT_TIMEOUT,
|
||||
}
|
||||
|
||||
|
||||
def _get_ezviz_client_instance(data):
|
||||
"""Initialize a new instance of EzvizClientApi."""
|
||||
def _validate_and_create_auth(data: dict) -> dict[str, Any]:
|
||||
"""Try to login to EZVIZ cloud account and return token."""
|
||||
# Verify cloud credentials by attempting a login request with username and password.
|
||||
# Return login token.
|
||||
|
||||
ezviz_client = EzvizClient(
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
data.get(CONF_URL, EU_URL),
|
||||
data[CONF_URL],
|
||||
data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
|
||||
)
|
||||
|
||||
ezviz_client.login()
|
||||
return ezviz_client
|
||||
ezviz_token = ezviz_client.login()
|
||||
|
||||
auth_data = {
|
||||
CONF_SESSION_ID: ezviz_token[CONF_SESSION_ID],
|
||||
CONF_RFSESSION_ID: ezviz_token[CONF_RFSESSION_ID],
|
||||
CONF_URL: ezviz_token["api_url"],
|
||||
CONF_TYPE: ATTR_TYPE_CLOUD,
|
||||
}
|
||||
|
||||
return auth_data
|
||||
|
||||
|
||||
def _test_camera_rtsp_creds(data):
|
||||
def _test_camera_rtsp_creds(data: dict) -> None:
|
||||
"""Try DESCRIBE on RTSP camera with credentials."""
|
||||
|
||||
test_rtsp = TestRTSPAuth(
|
||||
|
@ -71,89 +90,43 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
VERSION = 1
|
||||
|
||||
async def _validate_and_create_auth(self, data):
|
||||
"""Try to login to ezviz cloud account and create entry if successful."""
|
||||
await self.async_set_unique_id(data[CONF_USERNAME])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Verify cloud credentials by attempting a login request.
|
||||
try:
|
||||
await self.hass.async_add_executor_job(_get_ezviz_client_instance, data)
|
||||
|
||||
except InvalidURL as err:
|
||||
raise InvalidURL from err
|
||||
|
||||
except HTTPError as err:
|
||||
raise InvalidHost from err
|
||||
|
||||
except PyEzvizError as err:
|
||||
raise PyEzvizError from err
|
||||
|
||||
auth_data = {
|
||||
CONF_USERNAME: data[CONF_USERNAME],
|
||||
CONF_PASSWORD: data[CONF_PASSWORD],
|
||||
CONF_URL: data.get(CONF_URL, EU_URL),
|
||||
CONF_TYPE: ATTR_TYPE_CLOUD,
|
||||
}
|
||||
|
||||
return self.async_create_entry(title=data[CONF_USERNAME], data=auth_data)
|
||||
|
||||
async def _validate_and_create_camera_rtsp(self, data):
|
||||
async def _validate_and_create_camera_rtsp(self, data: dict) -> FlowResult:
|
||||
"""Try DESCRIBE on RTSP camera with credentials."""
|
||||
|
||||
# Get EZVIZ cloud credentials from config entry
|
||||
ezviz_client_creds = {
|
||||
CONF_USERNAME: None,
|
||||
CONF_PASSWORD: None,
|
||||
CONF_URL: None,
|
||||
ezviz_token = {
|
||||
CONF_SESSION_ID: None,
|
||||
CONF_RFSESSION_ID: None,
|
||||
"api_url": None,
|
||||
}
|
||||
ezviz_timeout = DEFAULT_TIMEOUT
|
||||
|
||||
for item in self._async_current_entries():
|
||||
if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD:
|
||||
ezviz_client_creds = {
|
||||
CONF_USERNAME: item.data.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: item.data.get(CONF_PASSWORD),
|
||||
CONF_URL: item.data.get(CONF_URL),
|
||||
ezviz_token = {
|
||||
CONF_SESSION_ID: item.data.get(CONF_SESSION_ID),
|
||||
CONF_RFSESSION_ID: item.data.get(CONF_RFSESSION_ID),
|
||||
"api_url": item.data.get(CONF_URL),
|
||||
}
|
||||
ezviz_timeout = item.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)
|
||||
|
||||
# Abort flow if user removed cloud account before adding camera.
|
||||
if ezviz_client_creds[CONF_USERNAME] is None:
|
||||
if ezviz_token.get(CONF_SESSION_ID) is None:
|
||||
return self.async_abort(reason="ezviz_cloud_account_missing")
|
||||
|
||||
ezviz_client = EzvizClient(token=ezviz_token, timeout=ezviz_timeout)
|
||||
|
||||
# We need to wake hibernating cameras.
|
||||
# First create EZVIZ API instance.
|
||||
try:
|
||||
ezviz_client = await self.hass.async_add_executor_job(
|
||||
_get_ezviz_client_instance, ezviz_client_creds
|
||||
)
|
||||
await self.hass.async_add_executor_job(ezviz_client.login)
|
||||
|
||||
except InvalidURL as err:
|
||||
raise InvalidURL from err
|
||||
|
||||
except HTTPError as err:
|
||||
raise InvalidHost from err
|
||||
|
||||
except PyEzvizError as err:
|
||||
raise PyEzvizError from err
|
||||
|
||||
# Secondly try to wake hibernating camera.
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
ezviz_client.get_detection_sensibility, data[ATTR_SERIAL]
|
||||
)
|
||||
|
||||
except HTTPError as err:
|
||||
raise InvalidHost from err
|
||||
# Secondly try to wake hybernating camera.
|
||||
await self.hass.async_add_executor_job(
|
||||
ezviz_client.get_detection_sensibility, data[ATTR_SERIAL]
|
||||
)
|
||||
|
||||
# Thirdly attempts an authenticated RTSP DESCRIBE request.
|
||||
try:
|
||||
await self.hass.async_add_executor_job(_test_camera_rtsp_creds, data)
|
||||
|
||||
except InvalidHost as err:
|
||||
raise InvalidHost from err
|
||||
|
||||
except AuthTestResultFailed as err:
|
||||
raise AuthTestResultFailed from err
|
||||
await self.hass.async_add_executor_job(_test_camera_rtsp_creds, data)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=data[ATTR_SERIAL],
|
||||
|
@ -162,6 +135,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
CONF_PASSWORD: data[CONF_PASSWORD],
|
||||
CONF_TYPE: ATTR_TYPE_CAMERA,
|
||||
},
|
||||
options=DEFAULT_OPTIONS,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -170,18 +144,24 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
"""Get the options flow for this handler."""
|
||||
return EzvizOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
|
||||
# Check if ezviz cloud account is present in entry config,
|
||||
# Check if EZVIZ cloud account is present in entry config,
|
||||
# abort if already configured.
|
||||
for item in self._async_current_entries():
|
||||
if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD:
|
||||
return self.async_abort(reason="already_configured_account")
|
||||
|
||||
errors = {}
|
||||
auth_data = {}
|
||||
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[CONF_USERNAME])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
if user_input[CONF_URL] == CONF_CUSTOMIZE:
|
||||
self.context["data"] = {
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
|
@ -189,11 +169,10 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
}
|
||||
return await self.async_step_user_custom_url()
|
||||
|
||||
if CONF_TIMEOUT not in user_input:
|
||||
user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT
|
||||
|
||||
try:
|
||||
return await self._validate_and_create_auth(user_input)
|
||||
auth_data = await self.hass.async_add_executor_job(
|
||||
_validate_and_create_auth, user_input
|
||||
)
|
||||
|
||||
except InvalidURL:
|
||||
errors["base"] = "invalid_host"
|
||||
|
@ -201,6 +180,9 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
except InvalidHost:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
except EzvizAuthVerificationCode:
|
||||
errors["base"] = "mfa_required"
|
||||
|
||||
except PyEzvizError:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
|
@ -208,6 +190,13 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data=auth_data,
|
||||
options=DEFAULT_OPTIONS,
|
||||
)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
|
@ -222,20 +211,21 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
step_id="user", data_schema=data_schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_user_custom_url(self, user_input=None):
|
||||
async def async_step_user_custom_url(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initiated by the user for custom region url."""
|
||||
|
||||
errors = {}
|
||||
auth_data = {}
|
||||
|
||||
if user_input is not None:
|
||||
user_input[CONF_USERNAME] = self.context["data"][CONF_USERNAME]
|
||||
user_input[CONF_PASSWORD] = self.context["data"][CONF_PASSWORD]
|
||||
|
||||
if CONF_TIMEOUT not in user_input:
|
||||
user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT
|
||||
|
||||
try:
|
||||
return await self._validate_and_create_auth(user_input)
|
||||
auth_data = await self.hass.async_add_executor_job(
|
||||
_validate_and_create_auth, user_input
|
||||
)
|
||||
|
||||
except InvalidURL:
|
||||
errors["base"] = "invalid_host"
|
||||
|
@ -243,6 +233,9 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
except InvalidHost:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
except EzvizAuthVerificationCode:
|
||||
errors["base"] = "mfa_required"
|
||||
|
||||
except PyEzvizError:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
|
@ -250,6 +243,13 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data=auth_data,
|
||||
options=DEFAULT_OPTIONS,
|
||||
)
|
||||
|
||||
data_schema_custom_url = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL, default=EU_URL): str,
|
||||
|
@ -260,18 +260,22 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
step_id="user_custom_url", data_schema=data_schema_custom_url, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_integration_discovery(self, discovery_info):
|
||||
async def async_step_integration_discovery(
|
||||
self, discovery_info: dict[str, Any]
|
||||
) -> FlowResult:
|
||||
"""Handle a flow for discovered camera without rtsp config entry."""
|
||||
|
||||
await self.async_set_unique_id(discovery_info[ATTR_SERIAL])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self.context["title_placeholders"] = {"serial": self.unique_id}
|
||||
self.context["title_placeholders"] = {ATTR_SERIAL: self.unique_id}
|
||||
self.context["data"] = {CONF_IP_ADDRESS: discovery_info[CONF_IP_ADDRESS]}
|
||||
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(self, user_input=None):
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Confirm and create entry from discovery step."""
|
||||
errors = {}
|
||||
|
||||
|
@ -284,6 +288,9 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
except (InvalidHost, InvalidURL):
|
||||
errors["base"] = "invalid_host"
|
||||
|
||||
except EzvizAuthVerificationCode:
|
||||
errors["base"] = "mfa_required"
|
||||
|
||||
except (PyEzvizError, AuthTestResultFailed):
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
|
@ -303,11 +310,76 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
data_schema=discovered_camera_schema,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"serial": self.unique_id,
|
||||
ATTR_SERIAL: self.unique_id,
|
||||
CONF_IP_ADDRESS: self.context["data"][CONF_IP_ADDRESS],
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult:
|
||||
"""Handle a flow for reauthentication with password."""
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a Confirm flow for reauthentication with password."""
|
||||
auth_data = {}
|
||||
errors = {}
|
||||
entry = None
|
||||
|
||||
for item in self._async_current_entries():
|
||||
if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD:
|
||||
self.context["title_placeholders"] = {ATTR_SERIAL: item.title}
|
||||
entry = await self.async_set_unique_id(item.title)
|
||||
|
||||
if not entry:
|
||||
return self.async_abort(reason="ezviz_cloud_account_missing")
|
||||
|
||||
if user_input is not None:
|
||||
user_input[CONF_URL] = entry.data[CONF_URL]
|
||||
|
||||
try:
|
||||
auth_data = await self.hass.async_add_executor_job(
|
||||
_validate_and_create_auth, user_input
|
||||
)
|
||||
|
||||
except (InvalidHost, InvalidURL):
|
||||
errors["base"] = "invalid_host"
|
||||
|
||||
except EzvizAuthVerificationCode:
|
||||
errors["base"] = "mfa_required"
|
||||
|
||||
except (PyEzvizError, AuthTestResultFailed):
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
else:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data=auth_data,
|
||||
)
|
||||
|
||||
await self.hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME, default=entry.title): vol.In([entry.title]),
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class EzvizOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle EZVIZ client options."""
|
||||
|
@ -316,22 +388,28 @@ class EzvizOptionsFlowHandler(OptionsFlow):
|
|||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage EZVIZ options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
options = {
|
||||
vol.Optional(
|
||||
CONF_TIMEOUT,
|
||||
default=self.config_entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
|
||||
),
|
||||
): str,
|
||||
}
|
||||
options = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_TIMEOUT,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TIMEOUT, DEFAULT_TIMEOUT
|
||||
),
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
|
||||
),
|
||||
): str,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
|
||||
return self.async_show_form(step_id="init", data_schema=options)
|
||||
|
|
|
@ -10,6 +10,9 @@ ATTR_HOME = "HOME_MODE"
|
|||
ATTR_AWAY = "AWAY_MODE"
|
||||
ATTR_TYPE_CLOUD = "EZVIZ_CLOUD_ACCOUNT"
|
||||
ATTR_TYPE_CAMERA = "CAMERA_ACCOUNT"
|
||||
CONF_SESSION_ID = "session_id"
|
||||
CONF_RFSESSION_ID = "rf_session_id"
|
||||
CONF_EZVIZ_ACCOUNT = "ezviz_account"
|
||||
|
||||
# Services data
|
||||
DIR_UP = "up"
|
||||
|
@ -33,10 +36,8 @@ SERVICE_DETECTION_SENSITIVITY = "set_alarm_detection_sensibility"
|
|||
EU_URL = "apiieu.ezvizlife.com"
|
||||
RUSSIA_URL = "apirus.ezvizru.com"
|
||||
DEFAULT_CAMERA_USERNAME = "admin"
|
||||
DEFAULT_RTSP_PORT = 554
|
||||
DEFAULT_TIMEOUT = 25
|
||||
DEFAULT_FFMPEG_ARGUMENTS = ""
|
||||
|
||||
# Data
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DATA_UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
|
|
|
@ -4,9 +4,16 @@ import logging
|
|||
|
||||
from async_timeout import timeout
|
||||
from pyezviz.client import EzvizClient
|
||||
from pyezviz.exceptions import HTTPError, InvalidURL, PyEzvizError
|
||||
from pyezviz.exceptions import (
|
||||
EzvizAuthTokenExpired,
|
||||
EzvizAuthVerificationCode,
|
||||
HTTPError,
|
||||
InvalidURL,
|
||||
PyEzvizError,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
@ -27,15 +34,16 @@ class EzvizDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||
|
||||
def _update_data(self) -> dict:
|
||||
"""Fetch data from EZVIZ via camera load function."""
|
||||
return self.ezviz_client.load_cameras()
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
"""Fetch data from EZVIZ."""
|
||||
try:
|
||||
async with timeout(self._api_timeout):
|
||||
return await self.hass.async_add_executor_job(self._update_data)
|
||||
return await self.hass.async_add_executor_job(
|
||||
self.ezviz_client.load_cameras
|
||||
)
|
||||
|
||||
except (EzvizAuthTokenExpired, EzvizAuthVerificationCode) as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
|
||||
except (InvalidURL, HTTPError, PyEzvizError) as error:
|
||||
raise UpdateFailed(f"Invalid response from API: {error}") from error
|
||||
|
|
|
@ -26,17 +26,27 @@
|
|||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "Enter credentials to reauthenticate to ezviz cloud account",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_host": "[%key:common::config_flow::error::invalid_host%]"
|
||||
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
|
||||
"mfa_required": "2FA enabled on account, please disable and retry"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"ezviz_cloud_account_missing": "EZVIZ cloud account missing. Please reconfigure EZVIZ cloud account"
|
||||
"ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
|
|
@ -3,8 +3,11 @@ from unittest.mock import patch
|
|||
|
||||
from homeassistant.components.ezviz.const import (
|
||||
ATTR_SERIAL,
|
||||
ATTR_TYPE_CAMERA,
|
||||
ATTR_TYPE_CLOUD,
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
CONF_RFSESSION_ID,
|
||||
CONF_SESSION_ID,
|
||||
DEFAULT_FFMPEG_ARGUMENTS,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
|
@ -22,8 +25,8 @@ from homeassistant.core import HomeAssistant
|
|||
from tests.common import MockConfigEntry
|
||||
|
||||
ENTRY_CONFIG = {
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_SESSION_ID: "test-username",
|
||||
CONF_RFSESSION_ID: "test-password",
|
||||
CONF_URL: "apiieu.ezvizlife.com",
|
||||
CONF_TYPE: ATTR_TYPE_CLOUD,
|
||||
}
|
||||
|
@ -46,6 +49,18 @@ USER_INPUT = {
|
|||
CONF_TYPE: ATTR_TYPE_CLOUD,
|
||||
}
|
||||
|
||||
USER_INPUT_CAMERA_VALIDATE = {
|
||||
ATTR_SERIAL: "C666666",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_USERNAME: "test-username",
|
||||
}
|
||||
|
||||
USER_INPUT_CAMERA = {
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_TYPE: ATTR_TYPE_CAMERA,
|
||||
}
|
||||
|
||||
DISCOVERY_INFO = {
|
||||
ATTR_SERIAL: "C666666",
|
||||
CONF_USERNAME: None,
|
||||
|
@ -59,6 +74,13 @@ TEST = {
|
|||
CONF_IP_ADDRESS: "127.0.0.1",
|
||||
}
|
||||
|
||||
API_LOGIN_RETURN_VALIDATE = {
|
||||
CONF_SESSION_ID: "fake_token",
|
||||
CONF_RFSESSION_ID: "fake_rf_token",
|
||||
CONF_URL: "apiieu.ezvizlife.com",
|
||||
CONF_TYPE: ATTR_TYPE_CLOUD,
|
||||
}
|
||||
|
||||
|
||||
def _patch_async_setup_entry(return_value=True):
|
||||
return patch(
|
||||
|
|
|
@ -5,6 +5,12 @@ from pyezviz import EzvizClient
|
|||
from pyezviz.test_cam_rtsp import TestRTSPAuth
|
||||
import pytest
|
||||
|
||||
ezviz_login_token_return = {
|
||||
"session_id": "fake_token",
|
||||
"rf_session_id": "fake_rf_token",
|
||||
"api_url": "apiieu.ezvizlife.com",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_ffmpeg(hass):
|
||||
|
@ -42,7 +48,7 @@ def ezviz_config_flow(hass):
|
|||
"1",
|
||||
)
|
||||
|
||||
instance.login = MagicMock(return_value=True)
|
||||
instance.login = MagicMock(return_value=ezviz_login_token_return)
|
||||
instance.get_detection_sensibility = MagicMock(return_value=True)
|
||||
|
||||
yield mock_ezviz
|
||||
|
|
|
@ -3,6 +3,7 @@ from unittest.mock import patch
|
|||
|
||||
from pyezviz.exceptions import (
|
||||
AuthTestResultFailed,
|
||||
EzvizAuthVerificationCode,
|
||||
HTTPError,
|
||||
InvalidHost,
|
||||
InvalidURL,
|
||||
|
@ -12,13 +13,16 @@ from pyezviz.exceptions import (
|
|||
from homeassistant.components.ezviz.const import (
|
||||
ATTR_SERIAL,
|
||||
ATTR_TYPE_CAMERA,
|
||||
ATTR_TYPE_CLOUD,
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
DEFAULT_FFMPEG_ARGUMENTS,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, SOURCE_USER
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_INTEGRATION_DISCOVERY,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_USER,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_CUSTOMIZE,
|
||||
CONF_IP_ADDRESS,
|
||||
|
@ -32,8 +36,8 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import (
|
||||
API_LOGIN_RETURN_VALIDATE,
|
||||
DISCOVERY_INFO,
|
||||
USER_INPUT,
|
||||
USER_INPUT_VALIDATE,
|
||||
_patch_async_setup_entry,
|
||||
init_integration,
|
||||
|
@ -59,7 +63,7 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None:
|
|||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-username"
|
||||
assert result["data"] == {**USER_INPUT}
|
||||
assert result["data"] == {**API_LOGIN_RETURN_VALIDATE}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
@ -78,7 +82,11 @@ async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None:
|
|||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "test-user", CONF_PASSWORD: "test-pass", CONF_URL: "customize"},
|
||||
{
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_URL: CONF_CUSTOMIZE,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
|
@ -90,21 +98,58 @@ async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None:
|
|||
result["flow_id"],
|
||||
{CONF_URL: "test-user"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_PASSWORD: "test-pass",
|
||||
CONF_TYPE: ATTR_TYPE_CLOUD,
|
||||
CONF_URL: "test-user",
|
||||
CONF_USERNAME: "test-user",
|
||||
}
|
||||
assert result["data"] == API_LOGIN_RETURN_VALIDATE
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_step_discovery_abort_if_cloud_account_missing(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
async def test_async_step_reauth(hass, ezviz_config_flow):
|
||||
"""Test the reauth step."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with _patch_async_setup_entry() as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT_VALIDATE,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-username"
|
||||
assert result["data"] == {**API_LOGIN_RETURN_VALIDATE}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
async def test_step_discovery_abort_if_cloud_account_missing(hass):
|
||||
"""Test discovery and confirm step, abort if cloud account was removed."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -127,11 +172,21 @@ async def test_step_discovery_abort_if_cloud_account_missing(
|
|||
assert result["reason"] == "ezviz_cloud_account_missing"
|
||||
|
||||
|
||||
async def test_step_reauth_abort_if_cloud_account_missing(hass):
|
||||
"""Test reauth and confirm step, abort if cloud account was removed."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "ezviz_cloud_account_missing"
|
||||
|
||||
|
||||
async def test_async_step_integration_discovery(
|
||||
hass: HomeAssistant, ezviz_config_flow, ezviz_test_rtsp_config_flow
|
||||
) -> None:
|
||||
hass, ezviz_config_flow, ezviz_test_rtsp_config_flow
|
||||
):
|
||||
"""Test discovery and confirm step."""
|
||||
with patch("homeassistant.components.ezviz.PLATFORMS", []):
|
||||
with patch("homeassistant.components.ezviz.PLATFORMS_BY_TYPE", []):
|
||||
await init_integration(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -189,11 +244,14 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
|||
|
||||
async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> None:
|
||||
"""Test we handle exception on user form."""
|
||||
ezviz_config_flow.side_effect = PyEzvizError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
ezviz_config_flow.side_effect = PyEzvizError
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
|
@ -215,6 +273,17 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No
|
|||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "invalid_host"}
|
||||
|
||||
ezviz_config_flow.side_effect = EzvizAuthVerificationCode
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT_VALIDATE,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "mfa_required"}
|
||||
|
||||
ezviz_config_flow.side_effect = HTTPError
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
|
@ -224,7 +293,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No
|
|||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
ezviz_config_flow.side_effect = Exception
|
||||
|
||||
|
@ -242,7 +311,7 @@ async def test_discover_exception_step1(
|
|||
ezviz_config_flow,
|
||||
) -> None:
|
||||
"""Test we handle unexpected exception on discovery."""
|
||||
with patch("homeassistant.components.ezviz.PLATFORMS", []):
|
||||
with patch("homeassistant.components.ezviz.PLATFORMS_BY_TYPE", []):
|
||||
await init_integration(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -295,7 +364,21 @@ async def test_discover_exception_step1(
|
|||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
assert result["errors"] == {"base": "invalid_host"}
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
ezviz_config_flow.side_effect = EzvizAuthVerificationCode
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test-user",
|
||||
CONF_PASSWORD: "test-pass",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
assert result["errors"] == {"base": "mfa_required"}
|
||||
|
||||
ezviz_config_flow.side_effect = Exception
|
||||
|
||||
|
@ -317,7 +400,7 @@ async def test_discover_exception_step3(
|
|||
ezviz_test_rtsp_config_flow,
|
||||
) -> None:
|
||||
"""Test we handle unexpected exception on discovery."""
|
||||
with patch("homeassistant.components.ezviz.PLATFORMS", []):
|
||||
with patch("homeassistant.components.ezviz.PLATFORMS_BY_TYPE", []):
|
||||
await init_integration(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -423,7 +506,18 @@ async def test_user_custom_url_exception(
|
|||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user_custom_url"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
ezviz_config_flow.side_effect = EzvizAuthVerificationCode
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_URL: "test-user"},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user_custom_url"
|
||||
assert result["errors"] == {"base": "mfa_required"}
|
||||
|
||||
ezviz_config_flow.side_effect = Exception
|
||||
|
||||
|
@ -434,3 +528,103 @@ async def test_user_custom_url_exception(
|
|||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "unknown"
|
||||
|
||||
|
||||
async def test_async_step_reauth_exception(hass, ezviz_config_flow):
|
||||
"""Test the reauth step exceptions."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with _patch_async_setup_entry() as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT_VALIDATE,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-username"
|
||||
assert result["data"] == {**API_LOGIN_RETURN_VALIDATE}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {}
|
||||
|
||||
ezviz_config_flow.side_effect = InvalidURL()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {"base": "invalid_host"}
|
||||
|
||||
ezviz_config_flow.side_effect = InvalidHost()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {"base": "invalid_host"}
|
||||
|
||||
ezviz_config_flow.side_effect = EzvizAuthVerificationCode()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {"base": "mfa_required"}
|
||||
|
||||
ezviz_config_flow.side_effect = PyEzvizError()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
ezviz_config_flow.side_effect = Exception()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "unknown"
|
||||
|
|
Loading…
Add table
Reference in a new issue