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 from collections.abc import Mapping
import logging import logging
from types import MappingProxyType
from typing import Any from typing import Any
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
@ -213,3 +214,71 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders=description_placeholders, description_placeholders=description_placeholders,
errors=errors, 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": { "data_description": {
"host": "The hostname or IP address of your Enphase Envoy gateway." "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": { "error": {
"cannot_connect": "Cannot connect: {reason}", "cannot_connect": "Cannot connect: {reason}",
"invalid_auth": "Invalid authentication: {reason}", "invalid_auth": "Invalid authentication: {reason}",
"unexpected_envoy": "Unexpected Envoy: {reason}",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {

View File

@ -11,6 +11,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.components.enphase_envoy.const import DOMAIN, PLATFORMS 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.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType 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" 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: async def test_platforms(snapshot: SnapshotAssertion) -> None:
"""Test if platform list changed and requires more tests.""" """Test if platform list changed and requires more tests."""
assert snapshot == PLATFORMS assert snapshot == PLATFORMS