1
mirror of https://github.com/home-assistant/core synced 2024-07-09 04:58:30 +02:00

Add reauth flow to Meater (#69895)

This commit is contained in:
Erik Montnemery 2022-05-02 15:50:13 +02:00 committed by GitHub
parent d6617eba7c
commit 1e18307a66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 119 additions and 18 deletions

View File

@ -14,7 +14,7 @@ from meater.MeaterApi import MeaterProbe
from homeassistant.config_entries 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
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -40,8 +40,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except (ServiceUnavailableError, TooManyRequestsError) as err:
raise ConfigEntryNotReady from err
except AuthenticationError as err:
_LOGGER.error("Unable to authenticate with the Meater API: %s", err)
return False
raise ConfigEntryAuthFailed(
f"Unable to authenticate with the Meater API: {err}"
) from err
async def async_update_data() -> dict[str, MeaterProbe]:
"""Fetch data from API endpoint."""
@ -51,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async with async_timeout.timeout(10):
devices: list[MeaterProbe] = await meater_api.get_all_devices()
except AuthenticationError as err:
raise UpdateFailed("The API call wasn't authenticated") from err
raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err
except TooManyRequestsError as err:
raise UpdateFailed(
"Too many requests have been made to the API, rate limiting is in place"

View File

@ -1,14 +1,18 @@
"""Config flow for Meater."""
from __future__ import annotations
from meater import AuthenticationError, MeaterApi, ServiceUnavailableError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN
FLOW_SCHEMA = vol.Schema(
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
USER_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
@ -16,12 +20,17 @@ FLOW_SCHEMA = vol.Schema(
class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Meater Config Flow."""
async def async_step_user(self, user_input=None):
_data_schema = USER_SCHEMA
_username: str
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Define the login user step."""
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=FLOW_SCHEMA,
data_schema=self._data_schema,
)
username: str = user_input[CONF_USERNAME]
@ -31,13 +40,41 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
return await self._try_connect_meater("user", None, username, password)
async def async_step_reauth(self, data: dict[str, str]) -> FlowResult:
"""Handle configuration by re-auth."""
self._data_schema = REAUTH_SCHEMA
self._username = data[CONF_USERNAME]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle re-auth completion."""
placeholders = {"username": self._username}
if not user_input:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self._data_schema,
description_placeholders=placeholders,
)
password = user_input[CONF_PASSWORD]
return await self._try_connect_meater(
"reauth_confirm", placeholders, self._username, password
)
async def _try_connect_meater(
self, step_id, placeholders: dict[str, str] | None, username: str, password: str
) -> FlowResult:
session = aiohttp_client.async_get_clientsession(self.hass)
api = MeaterApi(session)
errors = {}
try:
await api.authenticate(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
await api.authenticate(username, password)
except AuthenticationError:
errors["base"] = "invalid_auth"
except ServiceUnavailableError:
@ -45,13 +82,20 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except Exception: # pylint: disable=broad-except
errors["base"] = "unknown_auth_error"
else:
data = {"username": username, "password": password}
existing_entry = await self.async_set_unique_id(username.lower())
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=data)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title="Meater",
data={"username": username, "password": password},
data=data,
)
return self.async_show_form(
step_id="user",
data_schema=FLOW_SCHEMA,
step_id=step_id,
data_schema=self._data_schema,
description_placeholders=placeholders,
errors=errors,
)

View File

@ -6,6 +6,15 @@
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"username": "Meater Cloud username, typically an email address."
}
},
"reauth_confirm": {
"description": "Confirm the password for Meater Cloud account {username}.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
},

View File

@ -6,11 +6,20 @@
"unknown_auth_error": "Unexpected error"
},
"step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"description": "Confirm the password for Meater Cloud account {username}."
},
"user": {
"data": {
"password": "Password",
"username": "Username"
},
"data_description": {
"username": "Meater Cloud username, typically an email address."
},
"description": "Set up your Meater Cloud account."
}
}

View File

@ -4,9 +4,8 @@ from unittest.mock import AsyncMock, patch
from meater import AuthenticationError, ServiceUnavailableError
import pytest
from homeassistant import data_entry_flow
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.meater import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry
@ -35,7 +34,7 @@ async def test_duplicate_error(hass):
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@ -48,7 +47,7 @@ async def test_unknown_auth_error(hass, mock_meater):
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
)
assert result["errors"] == {"base": "unknown_auth_error"}
@ -59,7 +58,7 @@ async def test_invalid_credentials(hass, mock_meater):
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
)
assert result["errors"] == {"base": "invalid_auth"}
@ -72,7 +71,7 @@ async def test_service_unavailable(hass, mock_meater):
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
)
assert result["errors"] == {"base": "service_unavailable_error"}
@ -82,7 +81,7 @@ async def test_user_flow(hass, mock_meater):
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=None
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
@ -106,3 +105,42 @@ async def test_user_flow(hass, mock_meater):
CONF_USERNAME: "user@host.com",
CONF_PASSWORD: "password123",
}
async def test_reauth_flow(hass, mock_meater):
"""Test that the reauth flow works."""
data = {
CONF_USERNAME: "user@host.com",
CONF_PASSWORD: "password123",
}
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id="user@host.com",
data=data,
)
mock_config.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_REAUTH},
data=data,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"password": "passwordabc"},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result2["reason"] == "reauth_successful"
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
assert config_entry.data == {
CONF_USERNAME: "user@host.com",
CONF_PASSWORD: "passwordabc",
}