diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ecd49718559..9ed7ad7123d 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -55,6 +55,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._host: str | None = None self._port: int | None = None self._password: str | None = None + self._noise_required: bool | None = None self._noise_psk: str | None = None self._device_info: DeviceInfo | None = None self._reauth_entry: ConfigEntry | None = None @@ -151,33 +152,45 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"name": self._name} async def _async_try_fetch_device_info(self) -> FlowResult: - error = await self.fetch_device_info() + """Try to fetch device info and return any errors.""" + response: str | None + if self._noise_required: + # If we already know we need encryption, don't try to fetch device info + # without encryption. + response = ERROR_REQUIRES_ENCRYPTION_KEY + else: + # After 2024.08, stop trying to fetch device info without encryption + # so we can avoid probe requests to check for password. At this point + # most devices should announce encryption support and password is + # deprecated and can be discovered by trying to connect only after they + # interact with the flow since it is expected to be a rare case. + response = await self.fetch_device_info() - if error == ERROR_REQUIRES_ENCRYPTION_KEY: + if response == ERROR_REQUIRES_ENCRYPTION_KEY: if not self._device_name and not self._noise_psk: # If device name is not set we can send a zero noise psk # to get the device name which will allow us to populate # the device name and hopefully get the encryption key # from the dashboard. self._noise_psk = ZERO_NOISE_PSK - error = await self.fetch_device_info() + response = await self.fetch_device_info() self._noise_psk = None if ( self._device_name and await self._retrieve_encryption_key_from_dashboard() ): - error = await self.fetch_device_info() + response = await self.fetch_device_info() # If the fetched key is invalid, unset it again. - if error == ERROR_INVALID_ENCRYPTION_KEY: + if response == ERROR_INVALID_ENCRYPTION_KEY: self._noise_psk = None - error = ERROR_REQUIRES_ENCRYPTION_KEY + response = ERROR_REQUIRES_ENCRYPTION_KEY - if error == ERROR_REQUIRES_ENCRYPTION_KEY: + if response == ERROR_REQUIRES_ENCRYPTION_KEY: return await self.async_step_encryption_key() - if error is not None: - return await self._async_step_user_base(error=error) + if response is not None: + return await self._async_step_user_base(error=response) return await self._async_authenticate_or_add() async def _async_authenticate_or_add(self) -> FlowResult: @@ -220,6 +233,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._device_name = device_name self._host = discovery_info.host self._port = discovery_info.port + self._noise_required = bool(discovery_info.properties.get("api_encryption")) # Check if already configured await self.async_set_unique_id(mac_address) diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f5b7795a57b..28d411be939 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1233,6 +1233,72 @@ async def test_zeroconf_encryption_key_via_dashboard( assert mock_client.noise_psk == VALID_NOISE_PSK +async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( + hass: HomeAssistant, + mock_client, + mock_zeroconf: None, + mock_dashboard, + mock_setup_entry: None, +) -> None: + """Test encryption key retrieved from dashboard with api_encryption property set.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + "api_encryption": "any", + }, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert flow["type"] == FlowResultType.FORM + assert flow["step_id"] == "discovery_confirm" + + mock_dashboard["configured"].append( + { + "name": "test8266", + "configuration": "test8266.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + mock_client.device_info.side_effect = [ + DeviceInfo( + uses_password=False, + name="test8266", + mac_address="11:22:33:44:55:AA", + ), + ] + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + assert len(mock_get_encryption_key.mock_calls) == 1 + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test8266" + assert result["data"][CONF_HOST] == "192.168.43.183" + assert result["data"][CONF_PORT] == 6053 + assert result["data"][CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert result["result"] + assert result["result"].unique_id == "11:22:33:44:55:aa" + + assert mock_client.noise_psk == VALID_NOISE_PSK + + async def test_zeroconf_no_encryption_key_via_dashboard( hass: HomeAssistant, mock_client,