diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index fccd8979631..8d91b984d46 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -1,4 +1,5 @@ """The Tesla Powerwall integration.""" +import contextlib from datetime import timedelta import logging @@ -8,6 +9,7 @@ from tesla_powerwall import ( APIError, MissingAttributeError, Powerwall, + PowerwallError, PowerwallUnreachableError, ) @@ -19,12 +21,14 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.network import is_ip_address from .const import ( DOMAIN, POWERWALL_API_CHANGED, POWERWALL_API_CHARGE, POWERWALL_API_DEVICE_TYPE, + POWERWALL_API_GATEWAY_DIN, POWERWALL_API_GRID_SERVICES_ACTIVE, POWERWALL_API_GRID_STATUS, POWERWALL_API_METERS, @@ -117,6 +121,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed from err await _migrate_old_unique_ids(hass, entry_id, powerwall_data) + + gateway_din = powerwall_data[POWERWALL_API_GATEWAY_DIN] + if gateway_din and entry.unique_id is not None and is_ip_address(entry.unique_id): + hass.config_entries.async_update_entry(entry, unique_id=gateway_din) + login_failed_count = 0 runtime_data = hass.data[DOMAIN][entry.entry_id] = { @@ -224,14 +233,16 @@ def _login_and_fetch_base_info(power_wall: Powerwall, password: str): def call_base_info(power_wall): """Wrap powerwall properties to be a callable.""" - serial_numbers = power_wall.get_serial_numbers() # Make sure the serial numbers always have the same order - serial_numbers.sort() + gateway_din = None + with contextlib.suppress((AssertionError, PowerwallError)): + gateway_din = power_wall.get_gateway_din().upper() return { POWERWALL_API_SITE_INFO: power_wall.get_site_info(), POWERWALL_API_STATUS: power_wall.get_status(), POWERWALL_API_DEVICE_TYPE: power_wall.get_device_type(), - POWERWALL_API_SERIAL_NUMBERS: serial_numbers, + POWERWALL_API_SERIAL_NUMBERS: sorted(power_wall.get_serial_numbers()), + POWERWALL_API_GATEWAY_DIN: gateway_din, } diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index cb9929a890e..9814a52d8a0 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -1,5 +1,8 @@ """Config flow for Tesla Powerwall integration.""" +from __future__ import annotations + import logging +from typing import Any from tesla_powerwall import ( AccessDeniedError, @@ -13,6 +16,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.components import dhcp from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_ip_address from .const import DOMAIN @@ -24,10 +28,12 @@ def _login_and_fetch_site_info(power_wall: Powerwall, password: str): if password is not None: power_wall.login(password) power_wall.detect_and_pin_version() - return power_wall.get_site_info() + return power_wall.get_site_info(), power_wall.get_gateway_din() -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, str] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from schema with values provided by the user. @@ -37,7 +43,7 @@ async def validate_input(hass: core.HomeAssistant, data): password = data[CONF_PASSWORD] try: - site_info = await hass.async_add_executor_job( + site_info, gateway_din = await hass.async_add_executor_job( _login_and_fetch_site_info, power_wall, password ) except MissingAttributeError as err: @@ -46,7 +52,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise WrongVersion from err # Return info that you want to store in the config entry. - return {"title": site_info.site_name} + return {"title": site_info.site_name, "unique_id": gateway_din.upper()} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -56,41 +62,107 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the powerwall flow.""" - self.ip_address = None + self.ip_address: str | None = None + self.title: str | None = None + self.reauth_entry: config_entries.ConfigEntry | None = None async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info.ip - self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address}) - self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} - return await self.async_step_user() + gateway_din = discovery_info.hostname.upper() + # The hostname is the gateway_din (unique_id) + await self.async_set_unique_id(gateway_din) + self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: self.ip_address}) + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_IP_ADDRESS] == discovery_info.ip: + if entry.unique_id is not None and is_ip_address(entry.unique_id): + if self.hass.config_entries.async_update_entry( + entry, unique_id=gateway_din + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + self.context["title_placeholders"] = { + "name": gateway_din, + "ip_address": self.ip_address, + } + errors, info = await self._async_try_connect( + {CONF_IP_ADDRESS: self.ip_address, CONF_PASSWORD: gateway_din[-5:]} + ) + if errors: + if CONF_PASSWORD in errors: + # The default password is the gateway din last 5 + # if it does not work, we have to ask + return await self.async_step_user() + return self.async_abort(reason="cannot_connect") + assert info is not None + self.title = info["title"] + return await self.async_step_confirm_discovery() + + async def _async_try_connect( + self, user_input + ) -> tuple[dict[str, Any] | None, dict[str, str] | None]: + """Try to connect to the powerwall.""" + info = None + errors: dict[str, str] = {} + try: + info = await validate_input(self.hass, user_input) + except PowerwallUnreachableError: + errors[CONF_IP_ADDRESS] = "cannot_connect" + except WrongVersion: + errors["base"] = "wrong_version" + except AccessDeniedError: + errors[CONF_PASSWORD] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return errors, info + + async def async_step_confirm_discovery(self, user_input=None) -> FlowResult: + """Confirm a discovered powerwall.""" + assert self.ip_address is not None + assert self.unique_id is not None + if user_input is not None: + assert self.title is not None + return self.async_create_entry( + title=self.title, + data={ + CONF_IP_ADDRESS: self.ip_address, + CONF_PASSWORD: self.unique_id[-5:], + }, + ) + + self._set_confirm_only() + self.context["title_placeholders"] = { + "name": self.title, + "ip_address": self.ip_address, + } + return self.async_show_form( + step_id="confirm_discovery", + data_schema=vol.Schema({}), + description_placeholders={ + "name": self.title, + "ip_address": self.ip_address, + }, + ) async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: - try: - info = await validate_input(self.hass, user_input) - except PowerwallUnreachableError: - errors[CONF_IP_ADDRESS] = "cannot_connect" - except WrongVersion: - errors["base"] = "wrong_version" - except AccessDeniedError: - errors[CONF_PASSWORD] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - + errors, info = await self._async_try_connect(user_input) if not errors: - existing_entry = await self.async_set_unique_id( - user_input[CONF_IP_ADDRESS] - ) - if existing_entry: - self.hass.config_entries.async_update_entry( - existing_entry, data=user_input + assert info is not None + if info["unique_id"]: + await self.async_set_unique_id( + info["unique_id"], raise_on_progress=False ) - await self.hass.config_entries.async_reload(existing_entry.entry_id) - return self.async_abort(reason="reauth_successful") + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} + ) + self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address}) return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( @@ -104,10 +176,33 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth_confirm(self, user_input=None): + """Handle reauth confirmation.""" + errors = {} + if user_input is not None: + entry_data = self.reauth_entry.data + errors, _ = await self._async_try_connect( + {CONF_IP_ADDRESS: entry_data[CONF_IP_ADDRESS], **user_input} + ) + if not errors: + self.hass.config_entries.async_update_entry( + self.reauth_entry, data={**entry_data, **user_input} + ) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}), + errors=errors, + ) + async def async_step_reauth(self, data): """Handle configuration by re-auth.""" - self.ip_address = data[CONF_IP_ADDRESS] - return await self.async_step_user() + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() class WrongVersion(exceptions.HomeAssistantError): diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 8cc0cbc27cd..b2f0e3afe80 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -26,6 +26,7 @@ POWERWALL_API_STATUS = "status" POWERWALL_API_DEVICE_TYPE = "device_type" POWERWALL_API_SITE_INFO = "site_info" POWERWALL_API_SERIAL_NUMBERS = "serial_numbers" +POWERWALL_API_GATEWAY_DIN = "gateway_din" POWERWALL_HTTP_SESSION = "http_session" diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index fd17557abe1..55c7ab41e64 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,16 +3,11 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.3.12"], + "requirements": ["tesla-powerwall==0.3.15"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ { - "hostname": "1118431-*", - "macaddress": "88DA1A*" - }, - { - "hostname": "1118431-*", - "macaddress": "000145*" + "hostname": "1118431-*" } ], "iot_class": "local_polling", diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index e1b2f2dbd3b..8995a0f956e 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { "user": { "title": "Connect to the powerwall", @@ -9,6 +9,17 @@ "ip_address": "[%key:common::config_flow::data::ip%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confim": { + "title": "Reauthenticate the powerwall", + "description": "[%key:component::powerwall::config::step::user::description%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, + "confirm_discovery": { + "title": "[%key:component::powerwall::config::step::user::title%]", + "description": "Do you want to setup {name} ({ip_address})?" } }, "error": { @@ -18,6 +29,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index 3be711d94c5..04279759888 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", "reauth_successful": "Re-authentication was successful" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Unexpected error", "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Do you want to setup {name} ({ip_address})?", + "title": "Connect to the powerwall" + }, + "reauth_confim": { + "data": { + "password": "Password" + }, + "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Tesla app or the last 5 characters of the password found inside the door for Backup Gateway 2.", + "title": "Reauthenticate the powerwall" + }, "user": { "data": { "ip_address": "IP Address", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index f05a7f73e50..701117cb562 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -218,13 +218,7 @@ DHCP = [ }, { "domain": "powerwall", - "hostname": "1118431-*", - "macaddress": "88DA1A*" - }, - { - "domain": "powerwall", - "hostname": "1118431-*", - "macaddress": "000145*" + "hostname": "1118431-*" }, { "domain": "rachio", diff --git a/requirements_all.txt b/requirements_all.txt index 79407a8df6d..c858f443769 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2341,7 +2341,7 @@ temperusb==1.5.3 # tensorflow==2.5.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.12 +tesla-powerwall==0.3.15 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d71ccbfadc5..9f5e8578696 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1429,7 +1429,7 @@ tailscale==0.2.0 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.3.12 +tesla-powerwall==0.3.15 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 9d253c3a74b..1eac0319819 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -16,6 +16,8 @@ from tesla_powerwall import ( from tests.common import load_fixture +MOCK_GATEWAY_DIN = "111-0----2-000000000FFA" + async def _mock_powerwall_with_fixtures(hass): """Mock data used to build powerwall state.""" @@ -70,6 +72,7 @@ async def _mock_powerwall_site_name(hass, site_name): # Sets site_info_resp.site_name to return site_name site_info_resp.response["site_name"] = site_name powerwall_mock.get_site_info = Mock(return_value=site_info_resp) + powerwall_mock.get_gateway_din = Mock(return_value=MOCK_GATEWAY_DIN) return powerwall_mock diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 29a04a70085..3ef9e9c0fd1 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -12,8 +12,13 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT -from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name +from .mocks import ( + MOCK_GATEWAY_DIN, + _mock_powerwall_side_effect, + _mock_powerwall_site_name, +) from tests.common import MockConfigEntry @@ -162,34 +167,63 @@ async def test_already_configured_with_ignored(hass): ) config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip="1.1.1.1", - macaddress="AA:BB:CC:DD:EE:FF", - hostname="any", - ), - ) + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="00GGX", + ), + ) assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Some site" + assert result2["data"] == {"ip_address": "1.1.1.1", "password": "00GGX"} + assert len(mock_setup_entry.mock_calls) == 1 -async def test_dhcp_discovery(hass): - """Test we can process the discovery from dhcp.""" +async def test_dhcp_discovery_manual_configure(hass): + """Test we can process the discovery from dhcp and manually configure.""" + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip="1.1.1.1", - macaddress="AA:BB:CC:DD:EE:FF", - hostname="any", - ), - ) + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall.login", + side_effect=AccessDeniedError("xyz"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="any", + ), + ) assert result["type"] == "form" assert result["errors"] == {} - mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") with patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, @@ -209,18 +243,80 @@ async def test_dhcp_discovery(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_dhcp_discovery_auto_configure(hass): + """Test we can process the discovery from dhcp and auto configure.""" + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="00GGX", + ), + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Some site" + assert result2["data"] == {"ip_address": "1.1.1.1", "password": "00GGX"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_cannot_connect(hass): + """Test we can process the discovery from dhcp and we cannot connect.""" + mock_powerwall = _mock_powerwall_side_effect(site_info=PowerwallUnreachableError) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="00GGX", + ), + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + async def test_form_reauth(hass): """Test reauthenticate.""" entry = MockConfigEntry( domain=DOMAIN, data=VALID_CONFIG, - unique_id="1.2.3.4", + unique_id=MOCK_GATEWAY_DIN, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, ) assert result["type"] == "form" assert result["errors"] == {} @@ -237,7 +333,6 @@ async def test_form_reauth(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "new-test-password", }, ) @@ -246,3 +341,68 @@ async def test_form_reauth(hass): assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_update_ip_address(hass): + """Test we can update the ip address from dhcp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=MOCK_GATEWAY_DIN, + ) + entry.add_to_hass(hass) + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" + + +async def test_dhcp_discovery_updates_unique_id(hass): + """Test we can update the unique id from dhcp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id="1.2.3.4", + ) + entry.add_to_hass(hass) + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" + assert entry.unique_id == MOCK_GATEWAY_DIN