Add reauth to SleepIQ (#67321)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Keilin Bickar 2022-02-27 20:47:31 -05:00 committed by GitHub
parent 1cca317294
commit bafa99fe3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 110 additions and 16 deletions

View file

@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
@ -63,9 +63,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
await gateway.login(email, password)
except SleepIQLoginException:
except SleepIQLoginException as err:
_LOGGER.error("Could not authenticate with SleepIQ server")
return False
raise ConfigEntryAuthFailed(err) from err
except SleepIQTimeoutException as err:
raise ConfigEntryNotReady(
str(err) or "Timed out during authentication"

View file

@ -7,7 +7,7 @@ from typing import Any
from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
@ -18,11 +18,15 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a SleepIQ config flow."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._reauth_entry: ConfigEntry | None = None
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Import a SleepIQ account as a config entry.
@ -75,6 +79,42 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
last_step=True,
)
async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult:
"""Perform reauth upon an API authentication error."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm(user_input)
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm reauth."""
errors: dict[str, str] = {}
assert self._reauth_entry is not None
if user_input is not None:
data = {
CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
if not (error := await try_connection(self.hass, data)):
self.hass.config_entries.async_update_entry(
self._reauth_entry, data=data
)
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
errors["base"] = error
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
errors=errors,
description_placeholders={
CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME],
},
)
async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> str | None:
"""Test if the given credentials can successfully login to SleepIQ."""

View file

@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@ -13,6 +14,13 @@
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The SleepIQ integration needs to re-authenticate your account {username}.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
}
}

View file

@ -1,13 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured"
"already_configured": "Account is already configured",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
},
"step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"description": "The SleepIQ integration needs to re-authenticate your account {username}.",
"title": "Reauthenticate Integration"
},
"user": {
"data": {
"password": "Password",

View file

@ -1,4 +1,6 @@
"""Common methods for SleepIQ."""
from __future__ import annotations
from unittest.mock import create_autospec, patch
from asyncsleepiq import SleepIQBed, SleepIQSleeper
@ -20,6 +22,12 @@ SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_")
SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_")
SLEEPIQ_CONFIG = {
CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password",
}
@pytest.fixture
def mock_asyncsleepiq():
"""Mock an AsyncSleepIQ object."""
@ -49,14 +57,14 @@ def mock_asyncsleepiq():
yield client
async def setup_platform(hass: HomeAssistant, platform) -> MockConfigEntry:
async def setup_platform(
hass: HomeAssistant, platform: str | None = None
) -> MockConfigEntry:
"""Set up the SleepIQ platform."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password",
},
data=SLEEPIQ_CONFIG,
unique_id=SLEEPIQ_CONFIG[CONF_USERNAME].lower(),
)
mock_entry.add_to_hass(hass)

View file

@ -9,10 +9,7 @@ from homeassistant.components.sleepiq.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
SLEEPIQ_CONFIG = {
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
}
from tests.components.sleepiq.conftest import SLEEPIQ_CONFIG, setup_platform
async def test_import(hass: HomeAssistant) -> None:
@ -97,3 +94,36 @@ async def test_success(hass: HomeAssistant) -> None:
assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME]
assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD]
assert len(mock_setup_entry.mock_calls) == 1
async def test_reauth_password(hass):
"""Test reauth form."""
# set up initially
entry = await setup_platform(hass)
with patch(
"homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login",
side_effect=SleepIQLoginException,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
data=entry.data,
)
with patch(
"homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login",
return_value=True,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"password": "password"},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result2["reason"] == "reauth_successful"