Add reauth support to Tailwind (#105959)
This commit is contained in:
parent
60fe6ff771
commit
3c73c0f17f
5 changed files with 187 additions and 5 deletions
|
@ -1,6 +1,7 @@
|
|||
"""Config flow to configure the Tailwind integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from gotailwind import (
|
||||
|
@ -14,7 +15,7 @@ from gotailwind import (
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
@ -38,6 +39,7 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
VERSION = 1
|
||||
|
||||
host: str
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
@ -140,6 +142,46 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, _: Mapping[str, Any]) -> FlowResult:
|
||||
"""Handle initiation of re-authentication with a Tailwind device."""
|
||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle re-authentication with a Tailwind device."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None and self.reauth_entry:
|
||||
try:
|
||||
return await self._async_step_create_entry(
|
||||
host=self.reauth_entry.data[CONF_HOST],
|
||||
token=user_input[CONF_TOKEN],
|
||||
)
|
||||
except TailwindAuthenticationError:
|
||||
errors[CONF_TOKEN] = "invalid_auth"
|
||||
except TailwindConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TOKEN): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
}
|
||||
),
|
||||
description_placeholders={"url": LOCAL_CONTROL_KEY_URL},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _async_step_create_entry(self, *, host: str, token: str) -> FlowResult:
|
||||
"""Create entry."""
|
||||
tailwind = Tailwind(
|
||||
|
@ -151,6 +193,16 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
except TailwindUnsupportedFirmwareVersionError:
|
||||
return self.async_abort(reason="unsupported_firmware")
|
||||
|
||||
if self.reauth_entry:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.reauth_entry,
|
||||
data={CONF_HOST: host, CONF_TOKEN: token},
|
||||
)
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
|
||||
)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
await self.async_set_unique_id(
|
||||
format_mac(status.mac_address), raise_on_progress=False
|
||||
)
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
"""Data update coordinator for Tailwind."""
|
||||
from datetime import timedelta
|
||||
|
||||
from gotailwind import Tailwind, TailwindDeviceStatus, TailwindError
|
||||
from gotailwind import (
|
||||
Tailwind,
|
||||
TailwindAuthenticationError,
|
||||
TailwindDeviceStatus,
|
||||
TailwindError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
|
@ -35,5 +41,7 @@ class TailwindDataUpdateCoordinator(DataUpdateCoordinator[TailwindDeviceStatus])
|
|||
"""Fetch data from the Tailwind device."""
|
||||
try:
|
||||
return await self.tailwind.status()
|
||||
except TailwindAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except TailwindError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "Reauthenticate with your Tailwind garage door opener.\n\nTo do so, you will need to get your new local control key of your Tailwind device. For more details, see the description below the field down below.",
|
||||
"data": {
|
||||
"token": "[%key:component::tailwind::config::step::user::data::token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"token": "[%key:component::tailwind::config::step::user::data_description::token%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"description": "Set up your Tailwind garage door opener to integrate with Home Assistant.\n\nTo do so, you will need to get the local control key and IP address of your Tailwind device. For more details, see the description below the fields down below.",
|
||||
"data": {
|
||||
|
@ -31,6 +40,7 @@
|
|||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"no_device_id": "The discovered Tailwind device did not provide a device ID.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unsupported_firmware": "The firmware of your Tailwind device is not supported. Please update your Tailwind device to the latest firmware version using the Tailwind app."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ from syrupy.assertion import SnapshotAssertion
|
|||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.tailwind.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_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
@ -293,3 +293,88 @@ async def test_zeroconf_flow_not_discovered_again(
|
|||
assert result.get("type") == FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.1"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_tailwind")
|
||||
async def test_reauth_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the reauthentication configuration flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert mock_config_entry.data[CONF_TOKEN] == "123456"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"unique_id": mock_config_entry.unique_id,
|
||||
"entry_id": mock_config_entry.entry_id,
|
||||
},
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
assert result.get("type") == FlowResultType.FORM
|
||||
assert result.get("step_id") == "reauth_confirm"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_TOKEN: "987654"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2.get("type") == FlowResultType.ABORT
|
||||
assert result2.get("reason") == "reauth_successful"
|
||||
|
||||
assert mock_config_entry.data[CONF_TOKEN] == "987654"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_error"),
|
||||
[
|
||||
(TailwindConnectionError, {"base": "cannot_connect"}),
|
||||
(TailwindAuthenticationError, {CONF_TOKEN: "invalid_auth"}),
|
||||
(Exception, {"base": "unknown"}),
|
||||
],
|
||||
)
|
||||
async def test_reauth_flow_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_tailwind: MagicMock,
|
||||
side_effect: Exception,
|
||||
expected_error: dict[str, str],
|
||||
) -> None:
|
||||
"""Test we show form on a error."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_tailwind.status.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"unique_id": mock_config_entry.unique_id,
|
||||
"entry_id": mock_config_entry.entry_id,
|
||||
},
|
||||
data=mock_config_entry.data,
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_TOKEN: "123456",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2.get("type") == FlowResultType.FORM
|
||||
assert result2.get("step_id") == "reauth_confirm"
|
||||
assert result2.get("errors") == expected_error
|
||||
|
||||
mock_tailwind.status.side_effect = None
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_TOKEN: "123456",
|
||||
},
|
||||
)
|
||||
|
||||
assert result3.get("type") == FlowResultType.ABORT
|
||||
assert result3.get("reason") == "reauth_successful"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
"""Integration tests for the Tailwind integration."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from gotailwind import TailwindConnectionError
|
||||
from gotailwind import TailwindAuthenticationError, TailwindConnectionError
|
||||
|
||||
from homeassistant.components.tailwind.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
@ -44,3 +44,30 @@ async def test_config_entry_not_ready(
|
|||
|
||||
assert len(mock_tailwind.status.mock_calls) == 1
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_authentication_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_tailwind: MagicMock,
|
||||
) -> None:
|
||||
"""Test trigger reauthentication flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
mock_tailwind.status.side_effect = TailwindAuthenticationError
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow.get("step_id") == "reauth_confirm"
|
||||
assert flow.get("handler") == DOMAIN
|
||||
|
||||
assert "context" in flow
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == mock_config_entry.entry_id
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue