From 2f0695e4089cf2c451fa746c89bf53e33231c69f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:53:53 -1000 Subject: [PATCH] Fix missing unique id in enphase_envoy (#61083) --- .../components/enphase_envoy/__init__.py | 8 ++ .../components/enphase_envoy/config_flow.py | 53 ++++++++--- .../enphase_envoy/test_config_flow.py | 88 +++++++++++++++++++ 3 files changed, 136 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 69c488169a6..7b3765bd25c 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -75,6 +75,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: envoy_reader.get_inverters = False await coordinator.async_config_entry_first_refresh() + if not entry.unique_id: + try: + serial = await envoy_reader.get_full_serial_number() + except httpx.HTTPError: + pass + else: + hass.config_entries.async_update_entry(entry, unique_id=serial) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, NAME: name, diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 0b163e331d6..d1e0febe2e6 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Enphase Envoy integration.""" from __future__ import annotations +import contextlib import logging from typing import Any @@ -31,7 +32,7 @@ ENVOY = "Envoy" CONF_SERIAL = "serial" -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> EnvoyReader: """Validate the user input allows us to connect.""" envoy_reader = EnvoyReader( data[CONF_HOST], @@ -48,6 +49,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, except (RuntimeError, httpx.HTTPError) as err: raise CannotConnect from err + return envoy_reader + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Enphase Envoy.""" @@ -59,7 +62,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ip_address = None self.name = None self.username = None - self.serial = None self._reauth_entry = None @callback @@ -104,8 +106,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - self.serial = discovery_info.properties["serialnum"] - await self.async_set_unique_id(self.serial) + serial = discovery_info.properties["serialnum"] + await self.async_set_unique_id(serial) self.ip_address = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) for entry in self._async_current_entries(include_ignore=False): @@ -114,9 +116,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): and CONF_HOST in entry.data and entry.data[CONF_HOST] == self.ip_address ): - title = f"{ENVOY} {self.serial}" if entry.title == ENVOY else ENVOY + title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY self.hass.config_entries.async_update_entry( - entry, title=title, unique_id=self.serial + entry, title=title, unique_id=serial ) self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) @@ -132,6 +134,24 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_user() + def _async_envoy_name(self) -> str: + """Return the name of the envoy.""" + if self.name: + return self.name + if self.unique_id: + return f"{ENVOY} {self.unique_id}" + return ENVOY + + async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyReader) -> bool: + """Set the unique id by fetching it from the envoy.""" + serial = None + with contextlib.suppress(httpx.HTTPError): + serial = await envoy_reader.get_full_serial_number() + if serial: + await self.async_set_unique_id(serial) + return True + return False + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -145,7 +165,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="already_configured") try: - await validate_input(self.hass, user_input) + envoy_reader = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -155,21 +175,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: data = user_input.copy() - if self.serial: - data[CONF_NAME] = f"{ENVOY} {self.serial}" - else: - data[CONF_NAME] = self.name or ENVOY + data[CONF_NAME] = self._async_envoy_name() + if self._reauth_entry: self.hass.config_entries.async_update_entry( self._reauth_entry, data=data, ) return self.async_abort(reason="reauth_successful") + + if not self.unique_id and await self._async_set_unique_id_from_envoy( + envoy_reader + ): + data[CONF_NAME] = self._async_envoy_name() + + if self.unique_id: + self._abort_if_unique_id_configured({CONF_HOST: data[CONF_HOST]}) + return self.async_create_entry(title=data[CONF_NAME], data=data) - if self.serial: + if self.unique_id: self.context["title_placeholders"] = { - CONF_SERIAL: self.serial, + CONF_SERIAL: self.unique_id, CONF_HOST: self.ip_address, } return self.async_show_form( diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index fc9a7de188e..41a49a7b245 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -23,6 +23,91 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value="1234", + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + 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"] == "create_entry" + assert result2["title"] == "Envoy 1234" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy 1234", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_no_serial_number(hass: HomeAssistant) -> None: + """Test user setup without a serial number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value=None, + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + 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"] == "create_entry" + assert result2["title"] == "Envoy" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_fetching_serial_fails(hass: HomeAssistant) -> None: + """Test user setup without a serial number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + side_effect=httpx.HTTPStatusError( + "any", request=MagicMock(), response=MagicMock() + ), ), patch( "homeassistant.components.enphase_envoy.async_setup_entry", return_value=True, @@ -125,6 +210,9 @@ async def test_import(hass: HomeAssistant) -> None: with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value="1234", ), patch( "homeassistant.components.enphase_envoy.async_setup_entry", return_value=True,