Add reauthentication flow for Autarco integration (#131816)

This commit is contained in:
Klaas Schoute 2024-12-01 22:02:50 +01:00 committed by GitHub
parent bd8cd87fae
commit 78ced997e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 154 additions and 6 deletions

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any from typing import Any
from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError
@ -20,6 +21,12 @@ DATA_SCHEMA = vol.Schema(
} }
) )
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN): class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Autarco.""" """Handle a config flow for Autarco."""
@ -55,3 +62,40 @@ class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
data_schema=DATA_SCHEMA, data_schema=DATA_SCHEMA,
) )
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication request from Autarco."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle re-authentication confirmation."""
errors = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
client = Autarco(
email=reauth_entry.data[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
session=async_get_clientsession(self.hass),
)
try:
await client.get_account()
except AutarcoAuthenticationError:
errors["base"] = "invalid_auth"
except AutarcoConnectionError:
errors["base"] = "cannot_connect"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={"email": reauth_entry.data[CONF_EMAIL]},
data_schema=STEP_REAUTH_SCHEMA,
errors=errors,
)

View File

@ -7,6 +7,7 @@ from typing import NamedTuple
from autarco import ( from autarco import (
AccountSite, AccountSite,
Autarco, Autarco,
AutarcoAuthenticationError,
AutarcoConnectionError, AutarcoConnectionError,
Battery, Battery,
Inverter, Inverter,
@ -16,6 +17,7 @@ from autarco import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@ -60,8 +62,10 @@ class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
inverters = await self.client.get_inverters(self.account_site.public_key) inverters = await self.client.get_inverters(self.account_site.public_key)
if site.has_battery: if site.has_battery:
battery = await self.client.get_battery(self.account_site.public_key) battery = await self.client.get_battery(self.account_site.public_key)
except AutarcoConnectionError as error: except AutarcoAuthenticationError as err:
raise UpdateFailed(error) from error raise ConfigEntryAuthFailed(err) from err
except AutarcoConnectionError as err:
raise UpdateFailed(err) from err
return AutarcoData( return AutarcoData(
solar=solar, solar=solar,
inverters=inverters, inverters=inverters,

View File

@ -51,7 +51,7 @@ rules:
This integration only polls data using a coordinator. This integration only polls data using a coordinator.
Since the integration is read-only and poll-only (only provide sensor Since the integration is read-only and poll-only (only provide sensor
data), there is no need to implement parallel updates. data), there is no need to implement parallel updates.
reauthentication-flow: todo reauthentication-flow: done
test-coverage: done test-coverage: done
# Gold # Gold

View File

@ -2,7 +2,7 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"description": "Connect to your Autarco account to get information about your solar panels.", "description": "Connect to your Autarco account, to get information about your sites.",
"data": { "data": {
"email": "[%key:common::config_flow::data::email%]", "email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
@ -11,6 +11,16 @@
"email": "The email address of your Autarco account.", "email": "The email address of your Autarco account.",
"password": "The password of your Autarco account." "password": "The password of your Autarco account."
} }
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The password for {email} is no longer valid.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::autarco::config::step::user::data_description::password%]"
}
} }
}, },
"error": { "error": {
@ -18,7 +28,8 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
}, },
"entity": { "entity": {

View File

@ -1,6 +1,6 @@
"""Test the Autarco config flow.""" """Test the Autarco config flow."""
from unittest.mock import AsyncMock from unittest.mock import AsyncMock, patch
from autarco import AutarcoAuthenticationError, AutarcoConnectionError from autarco import AutarcoAuthenticationError, AutarcoConnectionError
import pytest import pytest
@ -92,6 +92,7 @@ async def test_exceptions(
assert result.get("type") is FlowResultType.FORM assert result.get("type") is FlowResultType.FORM
assert result.get("errors") == {"base": error} assert result.get("errors") == {"base": error}
# Recover from error
mock_autarco_client.get_account.side_effect = None mock_autarco_client.get_account.side_effect = None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@ -99,3 +100,72 @@ async def test_exceptions(
user_input={CONF_EMAIL: "test@autarco.com", CONF_PASSWORD: "test-password"}, user_input={CONF_EMAIL: "test@autarco.com", CONF_PASSWORD: "test-password"},
) )
assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("type") is FlowResultType.CREATE_ENTRY
async def test_step_reauth(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test reauth flow."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reauth_flow(hass)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "reauth_confirm"
with patch("homeassistant.components.autarco.config_flow.Autarco", autospec=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: "new-password"},
)
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "reauth_successful"
assert len(hass.config_entries.async_entries()) == 1
assert mock_config_entry.data[CONF_PASSWORD] == "new-password"
@pytest.mark.parametrize(
("exception", "error"),
[
(AutarcoConnectionError, "cannot_connect"),
(AutarcoAuthenticationError, "invalid_auth"),
],
)
async def test_step_reauth_exceptions(
hass: HomeAssistant,
mock_autarco_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
exception: Exception,
error: str,
) -> None:
"""Test exceptions in reauth flow."""
mock_autarco_client.get_account.side_effect = exception
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reauth_flow(hass)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: "new-password"},
)
assert result.get("type") is FlowResultType.FORM
assert result.get("errors") == {"base": error}
# Recover from error
mock_autarco_client.get_account.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: "new-password"},
)
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "reauth_successful"
assert len(hass.config_entries.async_entries()) == 1
assert mock_config_entry.data[CONF_PASSWORD] == "new-password"

View File

@ -4,6 +4,8 @@ from __future__ import annotations
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from autarco import AutarcoAuthenticationError
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -26,3 +28,20 @@ async def test_load_unload_entry(
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_setup_entry_exception(
hass: HomeAssistant,
mock_autarco_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test ConfigEntryNotReady when API raises an exception during entry setup."""
mock_config_entry.add_to_hass(hass)
mock_autarco_client.get_site.side_effect = AutarcoAuthenticationError
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"