mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 17:57:55 +00:00
Add reauth flow to Meater (#69895)
This commit is contained in:
parent
d6617eba7c
commit
1e18307a66
@ -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"
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user