Add reauthentication flow for Powerfox Local integration (#163966)

This commit is contained in:
Klaas Schoute
2026-02-24 23:29:19 +01:00
committed by GitHub
parent bc324a1a6e
commit 697441969b
6 changed files with 161 additions and 5 deletions
@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from powerfox import PowerfoxAuthenticationError, PowerfoxConnectionError, PowerfoxLocal
@@ -21,6 +22,12 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
}
)
class PowerfoxLocalConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Powerfox Local."""
@@ -33,7 +40,7 @@ class PowerfoxLocalConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step."""
errors: dict[str, str] = {}
errors = {}
if user_input is not None:
self._host = user_input[CONF_HOST]
@@ -84,6 +91,45 @@ class PowerfoxLocalConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a confirmation flow for zeroconf discovery."""
return self._async_create_entry()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication flow."""
self._host = entry_data[CONF_HOST]
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 = {}
if user_input is not None:
self._api_key = user_input[CONF_API_KEY]
reauth_entry = self._get_reauth_entry()
client = PowerfoxLocal(
host=reauth_entry.data[CONF_HOST],
api_key=user_input[CONF_API_KEY],
session=async_get_clientsession(self.hass),
)
try:
await client.value()
except PowerfoxAuthenticationError:
errors["base"] = "invalid_auth"
except PowerfoxConnectionError:
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",
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
)
def _async_create_entry(self) -> ConfigFlowResult:
"""Create a config entry."""
return self.async_create_entry(
@@ -2,11 +2,17 @@
from __future__ import annotations
from powerfox import LocalResponse, PowerfoxConnectionError, PowerfoxLocal
from powerfox import (
LocalResponse,
PowerfoxAuthenticationError,
PowerfoxConnectionError,
PowerfoxLocal,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -40,6 +46,12 @@ class PowerfoxLocalDataUpdateCoordinator(DataUpdateCoordinator[LocalResponse]):
"""Fetch data from the local poweropti."""
try:
return await self.client.value()
except PowerfoxAuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": str(err)},
) from err
except PowerfoxConnectionError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
@@ -43,7 +43,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: done
# Gold
@@ -2,13 +2,24 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::powerfox_local::config::step::user::data_description::api_key%]"
},
"description": "The API key for your Poweropti device is no longer valid.",
"title": "[%key:common::config_flow::title::reauth%]"
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
@@ -43,6 +54,9 @@
}
},
"exceptions": {
"invalid_auth": {
"message": "Error while authenticating with the device: {error}"
},
"update_failed": {
"message": "Error while updating the device: {error}"
}
@@ -184,3 +184,70 @@ async def test_user_flow_exceptions(
user_input={CONF_HOST: MOCK_HOST, CONF_API_KEY: MOCK_API_KEY},
)
assert result.get("type") is FlowResultType.CREATE_ENTRY
async def test_step_reauth(
hass: HomeAssistant,
mock_powerfox_local_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test re-authentication 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"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new-api-key"},
)
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "reauth_successful"
assert mock_config_entry.data[CONF_API_KEY] == "new-api-key"
@pytest.mark.parametrize(
("exception", "error"),
[
(PowerfoxConnectionError, "cannot_connect"),
(PowerfoxAuthenticationError, "invalid_auth"),
],
)
async def test_step_reauth_exceptions(
hass: HomeAssistant,
mock_powerfox_local_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
exception: Exception,
error: str,
) -> None:
"""Test exceptions during re-authentication flow."""
mock_powerfox_local_client.value.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_API_KEY: "new-api-key"},
)
assert result.get("type") is FlowResultType.FORM
assert result.get("errors") == {"base": error}
# Recover from error
mock_powerfox_local_client.value.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new-api-key"},
)
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "reauth_successful"
assert mock_config_entry.data[CONF_API_KEY] == "new-api-key"
+18 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import AsyncMock
from powerfox import PowerfoxConnectionError
from powerfox import PowerfoxAuthenticationError, PowerfoxConnectionError
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
@@ -43,3 +43,20 @@ async def test_config_entry_not_ready(
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_entry_exception(
hass: HomeAssistant,
mock_powerfox_local_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_powerfox_local_client.value.side_effect = PowerfoxAuthenticationError
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"