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.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, 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.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 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: except (ServiceUnavailableError, TooManyRequestsError) as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
except AuthenticationError as err: except AuthenticationError as err:
_LOGGER.error("Unable to authenticate with the Meater API: %s", err) raise ConfigEntryAuthFailed(
return False f"Unable to authenticate with the Meater API: {err}"
) from err
async def async_update_data() -> dict[str, MeaterProbe]: async def async_update_data() -> dict[str, MeaterProbe]:
"""Fetch data from API endpoint.""" """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): async with async_timeout.timeout(10):
devices: list[MeaterProbe] = await meater_api.get_all_devices() devices: list[MeaterProbe] = await meater_api.get_all_devices()
except AuthenticationError as err: 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: except TooManyRequestsError as err:
raise UpdateFailed( raise UpdateFailed(
"Too many requests have been made to the API, rate limiting is in place" "Too many requests have been made to the API, rate limiting is in place"

View File

@ -1,14 +1,18 @@
"""Config flow for Meater.""" """Config flow for Meater."""
from __future__ import annotations
from meater import AuthenticationError, MeaterApi, ServiceUnavailableError from meater import AuthenticationError, MeaterApi, ServiceUnavailableError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import DOMAIN 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} {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): class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Meater Config Flow.""" """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.""" """Define the login user step."""
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=FLOW_SCHEMA, data_schema=self._data_schema,
) )
username: str = user_input[CONF_USERNAME] username: str = user_input[CONF_USERNAME]
@ -31,13 +40,41 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
username = user_input[CONF_USERNAME] username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD] 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) session = aiohttp_client.async_get_clientsession(self.hass)
api = MeaterApi(session) api = MeaterApi(session)
errors = {} errors = {}
try: try:
await api.authenticate(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) await api.authenticate(username, password)
except AuthenticationError: except AuthenticationError:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except ServiceUnavailableError: except ServiceUnavailableError:
@ -45,13 +82,20 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
errors["base"] = "unknown_auth_error" errors["base"] = "unknown_auth_error"
else: 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( return self.async_create_entry(
title="Meater", title="Meater",
data={"username": username, "password": password}, data=data,
) )
return self.async_show_form( return self.async_show_form(
step_id="user", step_id=step_id,
data_schema=FLOW_SCHEMA, data_schema=self._data_schema,
description_placeholders=placeholders,
errors=errors, errors=errors,
) )

View File

@ -6,6 +6,15 @@
"data": { "data": {
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]" "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" "unknown_auth_error": "Unexpected error"
}, },
"step": { "step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"description": "Confirm the password for Meater Cloud account {username}."
},
"user": { "user": {
"data": { "data": {
"password": "Password", "password": "Password",
"username": "Username" "username": "Username"
}, },
"data_description": {
"username": "Meater Cloud username, typically an email address."
},
"description": "Set up your Meater Cloud account." "description": "Set up your Meater Cloud account."
} }
} }

View File

@ -4,9 +4,8 @@ from unittest.mock import AsyncMock, patch
from meater import AuthenticationError, ServiceUnavailableError from meater import AuthenticationError, ServiceUnavailableError
import pytest import pytest
from homeassistant import data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components.meater import DOMAIN from homeassistant.components.meater import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -35,7 +34,7 @@ async def test_duplicate_error(hass):
) )
result = await hass.config_entries.flow.async_init( 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 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"} conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
result = await hass.config_entries.flow.async_init( 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"} 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"} conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
result = await hass.config_entries.flow.async_init( 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"} 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"} conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
result = await hass.config_entries.flow.async_init( 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"} 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"} conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
result = await hass.config_entries.flow.async_init( 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["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
@ -106,3 +105,42 @@ async def test_user_flow(hass, mock_meater):
CONF_USERNAME: "user@host.com", CONF_USERNAME: "user@host.com",
CONF_PASSWORD: "password123", 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",
}