mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 10:17:51 +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.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"
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user