mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 13:57:10 +00:00
Add authentication support to MotionMount integration (#126487)
This commit is contained in:
parent
3f013ab620
commit
b16c3a55a5
@ -7,9 +7,9 @@ import socket
|
|||||||
import motionmount
|
import motionmount
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
|
||||||
from .const import DOMAIN, EMPTY_MAC
|
from .const import DOMAIN, EMPTY_MAC
|
||||||
@ -48,6 +48,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
f"Unexpected device found at {host}; expected {entry.unique_id}, found {found_mac}"
|
f"Unexpected device found at {host}; expected {entry.unique_id}, found {found_mac}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check we're properly authenticated or be able to become so
|
||||||
|
if not mm.is_authenticated:
|
||||||
|
if CONF_PIN not in entry.data:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="no_pin_provided",
|
||||||
|
)
|
||||||
|
|
||||||
|
pin = entry.data[CONF_PIN]
|
||||||
|
await mm.authenticate(pin)
|
||||||
|
if not mm.is_authenticated:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="incorrect_pin",
|
||||||
|
)
|
||||||
|
|
||||||
# Store an API object for your platforms to access
|
# Store an API object for your platforms to access
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = mm
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = mm
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Config flow for Vogel's MotionMount."""
|
"""Config flow for Vogel's MotionMount."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -9,10 +11,11 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
DEFAULT_DISCOVERY_UNIQUE_ID,
|
DEFAULT_DISCOVERY_UNIQUE_ID,
|
||||||
|
SOURCE_REAUTH,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_UUID
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT, CONF_UUID
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
@ -34,7 +37,9 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Set up the instance."""
|
"""Set up the instance."""
|
||||||
self.discovery_info: dict[str, Any] = {}
|
self.connection_data: dict[str, Any] = {}
|
||||||
|
self.backoff_task: asyncio.Task | None = None
|
||||||
|
self.backoff_time: int = 0
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@ -43,23 +48,16 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self._show_setup_form()
|
return self._show_setup_form()
|
||||||
|
|
||||||
|
self.connection_data.update(user_input)
|
||||||
info = {}
|
info = {}
|
||||||
try:
|
try:
|
||||||
info = await self._validate_input(user_input)
|
info = await self._validate_input_connect(self.connection_data)
|
||||||
except (ConnectionError, socket.gaierror):
|
except (ConnectionError, socket.gaierror):
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
return self.async_abort(reason="time_out")
|
return self.async_abort(reason="time_out")
|
||||||
except motionmount.NotConnectedError:
|
except motionmount.NotConnectedError:
|
||||||
return self.async_abort(reason="not_connected")
|
return self.async_abort(reason="not_connected")
|
||||||
except motionmount.MotionMountResponseError:
|
|
||||||
# This is most likely due to missing support for the mac address property
|
|
||||||
# Abort if the handler has config entries already
|
|
||||||
if self._async_current_entries():
|
|
||||||
return self.async_abort(reason="already_configured")
|
|
||||||
|
|
||||||
# Otherwise we try to continue with the generic uid
|
|
||||||
info[CONF_UUID] = DEFAULT_DISCOVERY_UNIQUE_ID
|
|
||||||
|
|
||||||
# If the device mac is valid we use it, otherwise we use the default id
|
# If the device mac is valid we use it, otherwise we use the default id
|
||||||
if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC:
|
if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC:
|
||||||
@ -67,17 +65,22 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
unique_id = DEFAULT_DISCOVERY_UNIQUE_ID
|
unique_id = DEFAULT_DISCOVERY_UNIQUE_ID
|
||||||
|
|
||||||
name = info.get(CONF_NAME, user_input[CONF_HOST])
|
name = info.get(CONF_NAME, self.connection_data[CONF_HOST])
|
||||||
|
self.connection_data[CONF_NAME] = name
|
||||||
|
|
||||||
await self.async_set_unique_id(unique_id)
|
await self.async_set_unique_id(unique_id)
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_if_unique_id_configured(
|
||||||
updates={
|
updates={
|
||||||
CONF_HOST: user_input[CONF_HOST],
|
CONF_HOST: self.connection_data[CONF_HOST],
|
||||||
CONF_PORT: user_input[CONF_PORT],
|
CONF_PORT: self.connection_data[CONF_PORT],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_create_entry(title=name, data=user_input)
|
if not info[CONF_PIN]:
|
||||||
|
# We need a pin to authenticate
|
||||||
|
return await self.async_step_auth()
|
||||||
|
# No pin is needed
|
||||||
|
return self._create_or_update_entry()
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
self, discovery_info: ZeroconfServiceInfo
|
self, discovery_info: ZeroconfServiceInfo
|
||||||
@ -91,7 +94,7 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
name = discovery_info.name.removesuffix(f".{zctype}")
|
name = discovery_info.name.removesuffix(f".{zctype}")
|
||||||
unique_id = discovery_info.properties.get("mac")
|
unique_id = discovery_info.properties.get("mac")
|
||||||
|
|
||||||
self.discovery_info.update(
|
self.connection_data.update(
|
||||||
{
|
{
|
||||||
CONF_HOST: host,
|
CONF_HOST: host,
|
||||||
CONF_PORT: port,
|
CONF_PORT: port,
|
||||||
@ -114,16 +117,13 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
self.context.update({"title_placeholders": {"name": name}})
|
self.context.update({"title_placeholders": {"name": name}})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info = await self._validate_input(self.discovery_info)
|
info = await self._validate_input_connect(self.connection_data)
|
||||||
except (ConnectionError, socket.gaierror):
|
except (ConnectionError, socket.gaierror):
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
return self.async_abort(reason="time_out")
|
return self.async_abort(reason="time_out")
|
||||||
except motionmount.NotConnectedError:
|
except motionmount.NotConnectedError:
|
||||||
return self.async_abort(reason="not_connected")
|
return self.async_abort(reason="not_connected")
|
||||||
except motionmount.MotionMountResponseError:
|
|
||||||
info = {}
|
|
||||||
# We continue as we want to be able to connect with older FW that does not support MAC address
|
|
||||||
|
|
||||||
# If the device supplied as with a valid MAC we use that
|
# If the device supplied as with a valid MAC we use that
|
||||||
if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC:
|
if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC:
|
||||||
@ -137,6 +137,10 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
await self._async_handle_discovery_without_unique_id()
|
await self._async_handle_discovery_without_unique_id()
|
||||||
|
|
||||||
|
if not info[CONF_PIN]:
|
||||||
|
# We need a pin to authenticate
|
||||||
|
return await self.async_step_auth()
|
||||||
|
# No pin is needed
|
||||||
return await self.async_step_zeroconf_confirm()
|
return await self.async_step_zeroconf_confirm()
|
||||||
|
|
||||||
async def async_step_zeroconf_confirm(
|
async def async_step_zeroconf_confirm(
|
||||||
@ -146,16 +150,82 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="zeroconf_confirm",
|
step_id="zeroconf_confirm",
|
||||||
description_placeholders={CONF_NAME: self.discovery_info[CONF_NAME]},
|
description_placeholders={CONF_NAME: self.connection_data[CONF_NAME]},
|
||||||
errors={},
|
errors={},
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self._create_or_update_entry()
|
||||||
title=self.discovery_info[CONF_NAME],
|
|
||||||
data=self.discovery_info,
|
async def async_step_reauth(
|
||||||
|
self, user_input: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle re-authentication."""
|
||||||
|
reauth_entry = self._get_reauth_entry()
|
||||||
|
self.connection_data.update(reauth_entry.data)
|
||||||
|
return await self.async_step_auth()
|
||||||
|
|
||||||
|
async def async_step_auth(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle authentication form."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
self.connection_data[CONF_PIN] = user_input[CONF_PIN]
|
||||||
|
|
||||||
|
# Validate pin code
|
||||||
|
valid_or_wait_time = await self._validate_input_pin(self.connection_data)
|
||||||
|
if valid_or_wait_time is True:
|
||||||
|
return self._create_or_update_entry()
|
||||||
|
|
||||||
|
if type(valid_or_wait_time) is int:
|
||||||
|
self.backoff_time = valid_or_wait_time
|
||||||
|
self.backoff_task = self.hass.async_create_task(
|
||||||
|
self._backoff(valid_or_wait_time)
|
||||||
|
)
|
||||||
|
return await self.async_step_backoff()
|
||||||
|
|
||||||
|
errors[CONF_PIN] = CONF_PIN
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="auth",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PIN): vol.All(int, vol.Range(min=1, max=9999)),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _validate_input(self, data: dict) -> dict[str, Any]:
|
async def async_step_backoff(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle backoff progress."""
|
||||||
|
if not self.backoff_task or self.backoff_task.done():
|
||||||
|
self.backoff_task = None
|
||||||
|
return self.async_show_progress_done(next_step_id="auth")
|
||||||
|
|
||||||
|
return self.async_show_progress(
|
||||||
|
step_id="backoff",
|
||||||
|
description_placeholders={
|
||||||
|
"timeout": str(self.backoff_time),
|
||||||
|
},
|
||||||
|
progress_action="progress_action",
|
||||||
|
progress_task=self.backoff_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_or_update_entry(self) -> ConfigFlowResult:
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
reauth_entry = self._get_reauth_entry()
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
reauth_entry, data_updates=self.connection_data
|
||||||
|
)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.connection_data[CONF_NAME],
|
||||||
|
data=self.connection_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _validate_input_connect(self, data: dict) -> dict[str, Any]:
|
||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
mm = motionmount.MotionMount(data[CONF_HOST], data[CONF_PORT])
|
mm = motionmount.MotionMount(data[CONF_HOST], data[CONF_PORT])
|
||||||
@ -164,7 +234,33 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
finally:
|
finally:
|
||||||
await mm.disconnect()
|
await mm.disconnect()
|
||||||
|
|
||||||
return {CONF_UUID: format_mac(mm.mac.hex()), CONF_NAME: mm.name}
|
return {
|
||||||
|
CONF_UUID: format_mac(mm.mac.hex()),
|
||||||
|
CONF_NAME: mm.name,
|
||||||
|
CONF_PIN: mm.is_authenticated,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _validate_input_pin(self, data: dict) -> bool | int:
|
||||||
|
"""Validate the user input allows us to authenticate."""
|
||||||
|
|
||||||
|
mm = motionmount.MotionMount(data[CONF_HOST], data[CONF_PORT])
|
||||||
|
try:
|
||||||
|
await mm.connect()
|
||||||
|
|
||||||
|
can_authenticate = mm.can_authenticate
|
||||||
|
if can_authenticate is True:
|
||||||
|
await mm.authenticate(data[CONF_PIN])
|
||||||
|
else:
|
||||||
|
# The backoff is running, return the remaining time
|
||||||
|
return can_authenticate
|
||||||
|
finally:
|
||||||
|
await mm.disconnect()
|
||||||
|
|
||||||
|
can_authenticate = mm.can_authenticate
|
||||||
|
if can_authenticate is True:
|
||||||
|
return mm.is_authenticated
|
||||||
|
|
||||||
|
return can_authenticate
|
||||||
|
|
||||||
def _show_setup_form(
|
def _show_setup_form(
|
||||||
self, errors: dict[str, str] | None = None
|
self, errors: dict[str, str] | None = None
|
||||||
@ -180,3 +276,9 @@ class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
),
|
),
|
||||||
errors=errors or {},
|
errors=errors or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _backoff(self, time: int) -> None:
|
||||||
|
while time > 0:
|
||||||
|
time -= 1
|
||||||
|
self.backoff_time = time
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
"""Support for MotionMount sensors."""
|
"""Support for MotionMount sensors."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import socket
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import motionmount
|
import motionmount
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS
|
from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS, CONF_PIN
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
@ -26,6 +25,11 @@ class MotionMountEntity(Entity):
|
|||||||
def __init__(self, mm: motionmount.MotionMount, config_entry: ConfigEntry) -> None:
|
def __init__(self, mm: motionmount.MotionMount, config_entry: ConfigEntry) -> None:
|
||||||
"""Initialize general MotionMount entity."""
|
"""Initialize general MotionMount entity."""
|
||||||
self.mm = mm
|
self.mm = mm
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
# We store the pin, as we might need it during reconnect
|
||||||
|
self.pin = config_entry.data[CONF_PIN]
|
||||||
|
|
||||||
mac = format_mac(mm.mac.hex())
|
mac = format_mac(mm.mac.hex())
|
||||||
|
|
||||||
# Create a base unique id
|
# Create a base unique id
|
||||||
@ -74,23 +78,3 @@ class MotionMountEntity(Entity):
|
|||||||
self.mm.remove_listener(self.async_write_ha_state)
|
self.mm.remove_listener(self.async_write_ha_state)
|
||||||
self.mm.remove_listener(self.update_name)
|
self.mm.remove_listener(self.update_name)
|
||||||
await super().async_will_remove_from_hass()
|
await super().async_will_remove_from_hass()
|
||||||
|
|
||||||
async def _ensure_connected(self) -> bool:
|
|
||||||
"""Make sure there is a connection with the MotionMount.
|
|
||||||
|
|
||||||
Returns false if the connection failed to be ensured.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.mm.is_connected:
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
await self.mm.connect()
|
|
||||||
except (ConnectionError, TimeoutError, socket.gaierror):
|
|
||||||
# We're not interested in exceptions here. In case of a failed connection
|
|
||||||
# the try/except from the caller will report it.
|
|
||||||
# The purpose of `_ensure_connected()` is only to make sure we try to
|
|
||||||
# reconnect, where failures should not be logged each time
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
_LOGGER.warning("Successfully reconnected to MotionMount")
|
|
||||||
return True
|
|
||||||
|
@ -51,6 +51,38 @@ class MotionMountPresets(MotionMountEntity, SelectEntity):
|
|||||||
|
|
||||||
self._attr_options = options
|
self._attr_options = options
|
||||||
|
|
||||||
|
async def _ensure_connected(self) -> bool:
|
||||||
|
"""Make sure there is a connection with the MotionMount.
|
||||||
|
|
||||||
|
Returns false if the connection failed to be ensured.
|
||||||
|
"""
|
||||||
|
if self.mm.is_connected:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
await self.mm.connect()
|
||||||
|
except (ConnectionError, TimeoutError, socket.gaierror):
|
||||||
|
# We're not interested in exceptions here. In case of a failed connection
|
||||||
|
# the try/except from the caller will report it.
|
||||||
|
# The purpose of `_ensure_connected()` is only to make sure we try to
|
||||||
|
# reconnect, where failures should not be logged each time
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check we're properly authenticated or be able to become so
|
||||||
|
if not self.mm.is_authenticated:
|
||||||
|
if self.pin is None:
|
||||||
|
await self.mm.disconnect()
|
||||||
|
self.config_entry.async_start_reauth(self.hass)
|
||||||
|
return False
|
||||||
|
await self.mm.authenticate(self.pin)
|
||||||
|
if not self.mm.is_authenticated:
|
||||||
|
self.pin = None
|
||||||
|
await self.mm.disconnect()
|
||||||
|
self.config_entry.async_start_reauth(self.hass)
|
||||||
|
return False
|
||||||
|
|
||||||
|
_LOGGER.debug("Successfully reconnected to MotionMount")
|
||||||
|
return True
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get latest state from MotionMount."""
|
"""Get latest state from MotionMount."""
|
||||||
if not await self._ensure_connected():
|
if not await self._ensure_connected():
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"common": {
|
||||||
|
"incorrect_pin": "Pin is not correct"
|
||||||
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"flow_title": "{name}",
|
"flow_title": "{name}",
|
||||||
"step": {
|
"step": {
|
||||||
@ -13,15 +16,33 @@
|
|||||||
"zeroconf_confirm": {
|
"zeroconf_confirm": {
|
||||||
"description": "Do you want to set up {name}?",
|
"description": "Do you want to set up {name}?",
|
||||||
"title": "Discovered MotionMount"
|
"title": "Discovered MotionMount"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"title": "Authenticate to your MotionMount",
|
||||||
|
"description": "Your MotionMount requires a pin to operate.",
|
||||||
|
"data": {
|
||||||
|
"pin": "[%key:common::config_flow::data::pin%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backoff": {
|
||||||
|
"title": "Authenticate to your MotionMount",
|
||||||
|
"description": "Too many incorrect pin attempts."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"pin": "[%key:component::motionmount::common::incorrect_pin%]"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"progress_action": "Too many incorrect pin attempts. Please wait {timeout} s..."
|
||||||
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"time_out": "Failed to connect due to a time out.",
|
"time_out": "[%key:common::config_flow::error::timeout_connect%]",
|
||||||
"not_connected": "Failed to connect.",
|
"not_connected": "Failed to connect.",
|
||||||
"invalid_response": "Failed to connect due to an invalid response from the MotionMount."
|
"invalid_response": "Failed to connect due to an invalid response from the MotionMount.",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@ -60,6 +81,12 @@
|
|||||||
"exceptions": {
|
"exceptions": {
|
||||||
"failed_communication": {
|
"failed_communication": {
|
||||||
"message": "Failed to communicate with MotionMount"
|
"message": "Failed to communicate with MotionMount"
|
||||||
|
},
|
||||||
|
"no_pin_provided": {
|
||||||
|
"message": "No pin provided"
|
||||||
|
},
|
||||||
|
"incorrect_pin": {
|
||||||
|
"message": "[%key:component::motionmount::common::incorrect_pin%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
HOST = "192.168.1.31"
|
HOST = "192.168.1.31"
|
||||||
@ -21,6 +21,8 @@ MOCK_USER_INPUT = {
|
|||||||
CONF_PORT: PORT,
|
CONF_PORT: PORT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MOCK_PIN_INPUT = {CONF_PIN: 1234}
|
||||||
|
|
||||||
MOCK_ZEROCONF_TVM_SERVICE_INFO_V1 = ZeroconfServiceInfo(
|
MOCK_ZEROCONF_TVM_SERVICE_INFO_V1 = ZeroconfServiceInfo(
|
||||||
type=TVM_ZEROCONF_SERVICE_TYPE,
|
type=TVM_ZEROCONF_SERVICE_TYPE,
|
||||||
name=f"{ZEROCONF_NAME}.{TVM_ZEROCONF_SERVICE_TYPE}",
|
name=f"{ZEROCONF_NAME}.{TVM_ZEROCONF_SERVICE_TYPE}",
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
"""Tests for the Vogel's MotionMount config flow."""
|
"""Tests for the Vogel's MotionMount config flow."""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
from datetime import timedelta
|
||||||
import socket
|
import socket
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import motionmount
|
import motionmount
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.motionmount.const import DOMAIN
|
from homeassistant.components.motionmount.const import DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
HOST,
|
HOST,
|
||||||
|
MOCK_PIN_INPUT,
|
||||||
MOCK_USER_INPUT,
|
MOCK_USER_INPUT,
|
||||||
MOCK_ZEROCONF_TVM_SERVICE_INFO_V1,
|
MOCK_ZEROCONF_TVM_SERVICE_INFO_V1,
|
||||||
MOCK_ZEROCONF_TVM_SERVICE_INFO_V2,
|
MOCK_ZEROCONF_TVM_SERVICE_INFO_V2,
|
||||||
@ -24,23 +27,12 @@ from . import (
|
|||||||
ZEROCONF_NAME,
|
ZEROCONF_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
MAC = bytes.fromhex("c4dd57f8a55f")
|
MAC = bytes.fromhex("c4dd57f8a55f")
|
||||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||||
|
|
||||||
|
|
||||||
async def test_show_user_form(hass: HomeAssistant) -> None:
|
|
||||||
"""Test that the user set up form is served."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_USER},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["step_id"] == "user"
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
|
||||||
|
|
||||||
|
|
||||||
async def test_user_connection_error(
|
async def test_user_connection_error(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_motionmount_config_flow: MagicMock,
|
mock_motionmount_config_flow: MagicMock,
|
||||||
@ -117,33 +109,6 @@ async def test_user_not_connected_error(
|
|||||||
assert result["reason"] == "not_connected"
|
assert result["reason"] == "not_connected"
|
||||||
|
|
||||||
|
|
||||||
async def test_user_response_error_single_device_old_ce_old_new_pro(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_motionmount_config_flow: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test that the flow creates an entry when there is a response error."""
|
|
||||||
mock_motionmount_config_flow.connect.side_effect = (
|
|
||||||
motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound)
|
|
||||||
)
|
|
||||||
|
|
||||||
user_input = MOCK_USER_INPUT.copy()
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_USER},
|
|
||||||
data=user_input,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
||||||
assert result["title"] == HOST
|
|
||||||
|
|
||||||
assert result["data"]
|
|
||||||
assert result["data"][CONF_HOST] == HOST
|
|
||||||
assert result["data"][CONF_PORT] == PORT
|
|
||||||
|
|
||||||
assert result["result"]
|
|
||||||
|
|
||||||
|
|
||||||
async def test_user_response_error_single_device_new_ce_old_pro(
|
async def test_user_response_error_single_device_new_ce_old_pro(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_motionmount_config_flow: MagicMock,
|
mock_motionmount_config_flow: MagicMock,
|
||||||
@ -199,30 +164,6 @@ async def test_user_response_error_single_device_new_ce_new_pro(
|
|||||||
assert result["result"].unique_id == ZEROCONF_MAC
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
async def test_user_response_error_multi_device_old_ce_old_new_pro(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_config_entry: MockConfigEntry,
|
|
||||||
mock_motionmount_config_flow: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test that the flow is aborted when there are multiple devices."""
|
|
||||||
mock_config_entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
mock_motionmount_config_flow.connect.side_effect = (
|
|
||||||
motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound)
|
|
||||||
)
|
|
||||||
|
|
||||||
user_input = MOCK_USER_INPUT.copy()
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_USER},
|
|
||||||
data=user_input,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.ABORT
|
|
||||||
assert result["reason"] == "already_configured"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_user_response_error_multi_device_new_ce_new_pro(
|
async def test_user_response_error_multi_device_new_ce_new_pro(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
@ -246,6 +187,53 @@ async def test_user_response_error_multi_device_new_ce_new_pro(
|
|||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_response_authentication_needed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_motionmount_config_flow: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that authentication is requested when needed."""
|
||||||
|
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||||
|
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=False
|
||||||
|
)
|
||||||
|
|
||||||
|
user_input = MOCK_USER_INPUT.copy()
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
# Now simulate the user entered the correct pin to finalize the test
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == HOST
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_connection_error(
|
async def test_zeroconf_connection_error(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_motionmount_config_flow: MagicMock,
|
mock_motionmount_config_flow: MagicMock,
|
||||||
@ -322,48 +310,6 @@ async def test_zeroconf_not_connected_error(
|
|||||||
assert result["reason"] == "not_connected"
|
assert result["reason"] == "not_connected"
|
||||||
|
|
||||||
|
|
||||||
async def test_show_zeroconf_form_old_ce_old_pro(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_motionmount_config_flow: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test that the zeroconf confirmation form is served."""
|
|
||||||
mock_motionmount_config_flow.connect.side_effect = (
|
|
||||||
motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound)
|
|
||||||
)
|
|
||||||
|
|
||||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V1)
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_ZEROCONF},
|
|
||||||
data=discovery_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["step_id"] == "zeroconf_confirm"
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
|
||||||
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_show_zeroconf_form_old_ce_new_pro(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_motionmount_config_flow: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test that the zeroconf confirmation form is served."""
|
|
||||||
mock_motionmount_config_flow.connect.side_effect = (
|
|
||||||
motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound)
|
|
||||||
)
|
|
||||||
|
|
||||||
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V2)
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": SOURCE_ZEROCONF},
|
|
||||||
data=discovery_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["step_id"] == "zeroconf_confirm"
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
|
||||||
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_show_zeroconf_form_new_ce_old_pro(
|
async def test_show_zeroconf_form_new_ce_old_pro(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_motionmount_config_flow: MagicMock,
|
mock_motionmount_config_flow: MagicMock,
|
||||||
@ -384,6 +330,21 @@ async def test_show_zeroconf_form_new_ce_old_pro(
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == ZEROCONF_HOSTNAME
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
assert result["data"][CONF_NAME] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id is None
|
||||||
|
|
||||||
|
|
||||||
async def test_show_zeroconf_form_new_ce_new_pro(
|
async def test_show_zeroconf_form_new_ce_new_pro(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -403,6 +364,21 @@ async def test_show_zeroconf_form_new_ce_new_pro(
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == ZEROCONF_HOSTNAME
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
assert result["data"][CONF_NAME] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_device_exists_abort(
|
async def test_zeroconf_device_exists_abort(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -423,6 +399,346 @@ async def test_zeroconf_device_exists_abort(
|
|||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_authentication_needed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_motionmount_config_flow: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that authentication is requested when needed."""
|
||||||
|
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=False
|
||||||
|
)
|
||||||
|
|
||||||
|
discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V2)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_ZEROCONF},
|
||||||
|
data=discovery_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
# Now simulate the user entered the correct pin to finalize the test
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == ZEROCONF_HOSTNAME
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
assert result["data"][CONF_NAME] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
|
async def test_authentication_incorrect_then_correct_pin(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_motionmount_config_flow: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that authentication is requested when needed."""
|
||||||
|
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||||
|
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=False
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
|
||||||
|
user_input = MOCK_USER_INPUT.copy()
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
assert result["errors"]
|
||||||
|
assert result["errors"][CONF_PIN] == CONF_PIN
|
||||||
|
|
||||||
|
# Now simulate the user entered the correct pin to finalize the test
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == HOST
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
|
async def test_authentication_first_incorrect_pin_to_backoff(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_motionmount_config_flow: MagicMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that authentication is requested when needed."""
|
||||||
|
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||||
|
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=False
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
side_effect=[True, 1]
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=MOCK_USER_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_motionmount_config_flow.authenticate.called
|
||||||
|
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||||
|
assert result["step_id"] == "backoff"
|
||||||
|
|
||||||
|
freezer.tick(timedelta(seconds=2))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Now simulate the user entered the correct pin to finalize the test
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == HOST
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
|
async def test_authentication_multiple_incorrect_pins(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_motionmount_config_flow: MagicMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that authentication is requested when needed."""
|
||||||
|
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||||
|
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=False
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(return_value=1)
|
||||||
|
|
||||||
|
user_input = MOCK_USER_INPUT.copy()
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||||
|
assert result["step_id"] == "backoff"
|
||||||
|
|
||||||
|
freezer.tick(timedelta(seconds=2))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Now simulate the user entered the correct pin to finalize the test
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == HOST
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
|
async def test_authentication_show_backoff_when_still_running(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_motionmount_config_flow: MagicMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that authentication is requested when needed."""
|
||||||
|
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||||
|
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=False
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(return_value=1)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=MOCK_USER_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||||
|
assert result["step_id"] == "backoff"
|
||||||
|
|
||||||
|
# This situation happens when the user cancels the progress dialog and tries to
|
||||||
|
# configure the MotionMount again
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||||
|
assert result["step_id"] == "backoff"
|
||||||
|
|
||||||
|
freezer.tick(timedelta(seconds=2))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Now simulate the user entered the correct pin to finalize the test
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == HOST
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
|
async def test_authentication_correct_pin(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_motionmount_config_flow: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that authentication is requested when needed."""
|
||||||
|
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||||
|
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=False
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
|
||||||
|
user_input = MOCK_USER_INPUT.copy()
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == ZEROCONF_NAME
|
||||||
|
|
||||||
|
assert result["data"]
|
||||||
|
assert result["data"][CONF_HOST] == HOST
|
||||||
|
assert result["data"][CONF_PORT] == PORT
|
||||||
|
|
||||||
|
assert result["result"]
|
||||||
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
async def test_full_user_flow_implementation(
|
async def test_full_user_flow_implementation(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_motionmount_config_flow: MagicMock,
|
mock_motionmount_config_flow: MagicMock,
|
||||||
@ -459,7 +775,7 @@ async def test_full_zeroconf_flow_implementation(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_motionmount_config_flow: MagicMock,
|
mock_motionmount_config_flow: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the full manual user flow from start to finish."""
|
"""Test the full zeroconf flow from start to finish."""
|
||||||
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME)
|
||||||
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC)
|
||||||
|
|
||||||
@ -487,3 +803,37 @@ async def test_full_zeroconf_flow_implementation(
|
|||||||
|
|
||||||
assert result["result"]
|
assert result["result"]
|
||||||
assert result["result"].unique_id == ZEROCONF_MAC
|
assert result["result"].unique_id == ZEROCONF_MAC
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_reauth_flow_implementation(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_motionmount_config_flow: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test reauthentication."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={
|
||||||
|
"source": SOURCE_REAUTH,
|
||||||
|
"entry_id": mock_config_entry.entry_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "auth"
|
||||||
|
|
||||||
|
type(mock_motionmount_config_flow).can_authenticate = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
type(mock_motionmount_config_flow).is_authenticated = PropertyMock(
|
||||||
|
return_value=True
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_PIN_INPUT.copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "reauth_successful"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user