Implement reconfigure step for enphase_envoy (#115781)

This commit is contained in:
Arie Catsman 2024-05-27 11:06:55 +02:00 committed by GitHub
parent 10291b1ce8
commit 83e4c2927c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 380 additions and 0 deletions

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping
import logging
from types import MappingProxyType
from typing import Any
from awesomeversion import AwesomeVersion
@ -213,3 +214,71 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders=description_placeholders,
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Add reconfigure step to allow to manually reconfigure a config entry."""
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert entry
suggested_values: dict[str, Any] | MappingProxyType[str, Any] = (
user_input or entry.data
)
host: Any = suggested_values.get(CONF_HOST)
username: Any = suggested_values.get(CONF_USERNAME)
password: Any = suggested_values.get(CONF_PASSWORD)
if user_input is not None:
try:
envoy = await validate_input(
self.hass,
host,
username,
password,
)
except INVALID_AUTH_ERRORS as e:
errors["base"] = "invalid_auth"
description_placeholders = {"reason": str(e)}
except EnvoyError as e:
errors["base"] = "cannot_connect"
description_placeholders = {"reason": str(e)}
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if self.unique_id != envoy.serial_number:
errors["base"] = "unexpected_envoy"
description_placeholders = {
"reason": f"target: {self.unique_id}, actual: {envoy.serial_number}"
}
else:
# If envoy exists in configuration update fields and exit
self._abort_if_unique_id_configured(
{
CONF_HOST: host,
CONF_USERNAME: username,
CONF_PASSWORD: password,
},
error="reconfigure_successful",
)
if not self.unique_id:
await self.async_set_unique_id(entry.unique_id)
self.context["title_placeholders"] = {
CONF_SERIAL: self.unique_id,
CONF_HOST: host,
}
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
self._async_generate_schema(), suggested_values
),
description_placeholders=description_placeholders,
errors=errors,
)

View File

@ -12,11 +12,23 @@
"data_description": {
"host": "The hostname or IP address of your Enphase Envoy gateway."
}
},
"reconfigure": {
"description": "[%key:component::enphase_envoy::config::step::user::description%]",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "[%key:component::enphase_envoy::config::step::user::data_description::host%]"
}
}
},
"error": {
"cannot_connect": "Cannot connect: {reason}",
"invalid_auth": "Invalid authentication: {reason}",
"unexpected_envoy": "Unexpected Envoy: {reason}",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {

View File

@ -11,6 +11,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.enphase_envoy.const import DOMAIN, PLATFORMS
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@ -656,6 +657,304 @@ async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) ->
assert result2["reason"] == "reauth_successful"
async def test_reconfigure(
hass: HomeAssistant, config_entry, setup_enphase_envoy
) -> None:
"""Test we can reconfiger the entry."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_RECONFIGURE,
"entry_id": config_entry.entry_id,
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
assert result["errors"] == {}
# original entry
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.2",
"username": "test-username2",
"password": "test-password2",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "reconfigure_successful"
# changed entry
assert config_entry.data["host"] == "1.1.1.2"
assert config_entry.data["username"] == "test-username2"
assert config_entry.data["password"] == "test-password2"
async def test_reconfigure_nochange(
hass: HomeAssistant, config_entry, setup_enphase_envoy
) -> None:
"""Test we get the reconfigure form and apply nochange."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_RECONFIGURE,
"entry_id": config_entry.entry_id,
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
assert result["errors"] == {}
# original entry
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
"username": "test-username",
"password": "test-password",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "reconfigure_successful"
# unchanged original entry
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
async def test_reconfigure_otherenvoy(
hass: HomeAssistant, config_entry, setup_enphase_envoy, mock_envoy
) -> None:
"""Test entering ip of other envoy and prevent changing it based on serial."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_RECONFIGURE,
"entry_id": config_entry.entry_id,
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
assert result["errors"] == {}
# let mock return different serial from first time, sim it's other one on changed ip
mock_envoy.serial_number = "45678"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.2",
"username": "test-username",
"password": "new-password",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "unexpected_envoy"}
# entry should still be original entry
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
# set serial back to original to finsich flow
mock_envoy.serial_number = "1234"
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
"host": "1.1.1.1",
"username": "test-username",
"password": "new-password",
},
)
assert result3["type"] is FlowResultType.ABORT
assert result3["reason"] == "reconfigure_successful"
# updated original entry
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "new-password"
@pytest.mark.parametrize(
"mock_authenticate",
[
AsyncMock(
side_effect=[
None,
EnvoyAuthenticationError("fail authentication"),
EnvoyError("cannot_connect"),
Exception("Unexpected exception"),
None,
]
),
],
)
async def test_reconfigure_auth_failure(
hass: HomeAssistant, config_entry, setup_enphase_envoy
) -> None:
"""Test changing credentials for existing host with auth failure."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_RECONFIGURE,
"entry_id": config_entry.entry_id,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
# existing config
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
# mock failing authentication on first try
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.2",
"username": "test-username",
"password": "wrong-password",
},
)
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_auth"}
# still original config after failure
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
# mock failing authentication on first try
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.2",
"username": "new-username",
"password": "wrong-password",
},
)
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"}
# still original config after failure
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
# mock failing authentication on first try
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.2",
"username": "other-username",
"password": "test-password",
},
)
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "unknown"}
# still original config after failure
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
# mock successful authentication and update of credentials
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.2",
"username": "test-username",
"password": "changed-password",
},
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.ABORT
assert result3["reason"] == "reconfigure_successful"
# updated config with new ip and changed pw
assert config_entry.data["host"] == "1.1.1.2"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "changed-password"
async def test_reconfigure_change_ip_to_existing(
hass: HomeAssistant, config_entry, setup_enphase_envoy
) -> None:
"""Test reconfiguration to existing entry with same ip does not harm existing one."""
other_entry = MockConfigEntry(
domain=DOMAIN,
entry_id="65432155aaddb2007c5f6602e0c38e72",
title="Envoy 654321",
unique_id="654321",
data={
CONF_HOST: "1.1.1.2",
CONF_NAME: "Envoy 654321",
CONF_USERNAME: "other-username",
CONF_PASSWORD: "other-password",
},
)
other_entry.add_to_hass(hass)
# original other entry
assert other_entry.data["host"] == "1.1.1.2"
assert other_entry.data["username"] == "other-username"
assert other_entry.data["password"] == "other-password"
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_RECONFIGURE,
"entry_id": config_entry.entry_id,
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
assert result["errors"] == {}
# original entry
assert config_entry.data["host"] == "1.1.1.1"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.2",
"username": "test-username",
"password": "test-password2",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "reconfigure_successful"
# updated entry
assert config_entry.data["host"] == "1.1.1.2"
assert config_entry.data["username"] == "test-username"
assert config_entry.data["password"] == "test-password2"
# unchanged other entry
assert other_entry.data["host"] == "1.1.1.2"
assert other_entry.data["username"] == "other-username"
assert other_entry.data["password"] == "other-password"
async def test_platforms(snapshot: SnapshotAssertion) -> None:
"""Test if platform list changed and requires more tests."""
assert snapshot == PLATFORMS