1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

openevse: Add reathentication flow (#169632)

This commit is contained in:
Colin
2026-05-04 00:38:31 -06:00
committed by GitHub
parent 77bd6a720d
commit 9bea2d149a
6 changed files with 180 additions and 2 deletions
@@ -1,10 +1,11 @@
"""The OpenEVSE integration."""
from openevsehttp.__main__ import OpenEVSE
from openevsehttp.exceptions import AuthenticationError
from homeassistant.const import CONF_HOST, 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 .coordinator import OpenEVSEConfigEntry, OpenEVSEDataUpdateCoordinator
@@ -25,6 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenEVSEConfigEntry) ->
await charger.test_and_get()
except TimeoutError as ex:
raise ConfigEntryNotReady("Unable to connect to charger") from ex
except AuthenticationError as ex:
raise ConfigEntryAuthFailed("Invalid credentials for charger") from ex
coordinator = OpenEVSEDataUpdateCoordinator(hass, entry, charger)
await coordinator.async_config_entry_first_refresh()
@@ -1,5 +1,6 @@
"""Config flow for OpenEVSE integration."""
from collections.abc import Mapping
from typing import Any
from openevsehttp.__main__ import OpenEVSE
@@ -170,3 +171,38 @@ class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=self.add_suggested_values_to_schema(AUTH_SCHEMA, user_input),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication on an authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
errors, _ = await self.check_status(
reauth_entry.data[CONF_HOST],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
if not errors:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(AUTH_SCHEMA, user_input),
description_placeholders={CONF_HOST: reauth_entry.data[CONF_HOST]},
errors=errors,
)
@@ -4,9 +4,11 @@ from datetime import timedelta
import logging
from openevsehttp.__main__ import OpenEVSE
from openevsehttp.exceptions import AuthenticationError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -63,3 +65,5 @@ class OpenEVSEDataUpdateCoordinator(DataUpdateCoordinator[None]):
raise UpdateFailed(
f"Timeout communicating with charger: {error}"
) from error
except AuthenticationError as error:
raise ConfigEntryAuthFailed("Invalid credentials for charger") from error
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "This charger is already configured",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unavailable_host": "Unable to connect to host"
},
"error": {
@@ -19,6 +20,17 @@
"username": "The username to access your OpenEVSE charger"
}
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "[%key:component::openevse::config::step::auth::data_description::password%]",
"username": "[%key:component::openevse::config::step::auth::data_description::username%]"
},
"description": "The credentials for your OpenEVSE charger at {host} are no longer valid. Please enter your current username and password."
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
@@ -4,6 +4,7 @@ from ipaddress import ip_address
from unittest.mock import AsyncMock, MagicMock
from openevsehttp.exceptions import AuthenticationError, MissingSerial
import pytest
from homeassistant.components.openevse.const import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF
@@ -486,3 +487,80 @@ async def test_zeroconf_already_configured_host(
# Should abort because the host matches an existing entry
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_reauth_flow(
hass: HomeAssistant,
mock_charger: MagicMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test reauthentication flow."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["description_placeholders"][CONF_HOST] == "192.168.1.100"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "newuser", CONF_PASSWORD: "newpassword"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data == {
CONF_HOST: "192.168.1.100",
CONF_USERNAME: "newuser",
CONF_PASSWORD: "newpassword",
}
@pytest.mark.parametrize(
("exception", "error_base"),
[
(AuthenticationError, "invalid_auth"),
(TimeoutError, "cannot_connect"),
],
)
async def test_reauth_flow_errors(
hass: HomeAssistant,
mock_charger: MagicMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
error_base: str,
) -> None:
"""Test reauthentication flow recovers from errors."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
mock_charger.test_and_get.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "newuser", CONF_PASSWORD: "wrongpassword"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {"base": error_base}
mock_charger.test_and_get.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "newuser", CONF_PASSWORD: "newpassword"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data == {
CONF_HOST: "192.168.1.100",
CONF_USERNAME: "newuser",
CONF_PASSWORD: "newpassword",
}
+46 -1
View File
@@ -2,7 +2,9 @@
from unittest.mock import MagicMock
from homeassistant.config_entries import ConfigEntryState
from openevsehttp.exceptions import AuthenticationError
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -23,6 +25,49 @@ async def test_setup_entry_timeout(
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_entry_auth_error_starts_reauth(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_charger: MagicMock,
) -> None:
"""Test setup entry triggers reauth on authentication error."""
mock_charger.test_and_get.side_effect = AuthenticationError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["context"]["source"] == SOURCE_REAUTH
assert flows[0]["context"]["entry_id"] == mock_config_entry.entry_id
async def test_coordinator_update_auth_error_starts_reauth(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_charger: MagicMock,
) -> None:
"""Test coordinator update triggers reauth on authentication error."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
coordinator = mock_config_entry.runtime_data
mock_charger.update.side_effect = AuthenticationError
await coordinator.async_refresh()
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["context"]["source"] == SOURCE_REAUTH
assert flows[0]["context"]["entry_id"] == mock_config_entry.entry_id
async def test_unload_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,