diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index 3c30c49fd9..d010871a04 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -1333,7 +1333,14 @@ def download_from_mirrors( sweep + 1, _MIRROR_SWEEP_ATTEMPTS, ) - _cancellable_sleep(delay, progress, 0) + # Tick with the bytes already on disk so a combined bar holds + # steady during the backoff instead of rewinding to zero + if f is not None: + done = f.tell() + else: + part = path_target.with_name(path_target.name + ".part") + done = part.stat().st_size if part.is_file() else 0 + _cancellable_sleep(delay, progress, done) # 4. Report every attempted URL if all mirrors failed. failures spans # all sweeps (deduplicated by URL and reason), so neither an early diff --git a/tests/unit_tests/test_framework_helpers.py b/tests/unit_tests/test_framework_helpers.py index e8f3669332..bfb9df91bb 100644 --- a/tests/unit_tests/test_framework_helpers.py +++ b/tests/unit_tests/test_framework_helpers.py @@ -1703,6 +1703,27 @@ class TestDownloadFromMirrors: assert mock_get.call_count == 2 mock_sleep.assert_called_once_with(2) + def test_backoff_tick_reports_partial_bytes(self, tmp_path: Path) -> None: + """The backoff tick carries the bytes already in the part file, so a + combined bar holds steady instead of rewinding to zero.""" + dest = tmp_path / "out.bin" + (tmp_path / "out.bin.part").write_bytes(b"12345") + ticks: list[int] = [] + with ( + patch( + "requests.get", + side_effect=[ + req.ConnectionError("down"), + _mock_response(b"data"), + ], + ), + patch("esphome.framework_helpers._cancellable_sleep") as mock_sleep, + ): + download_from_mirrors( + ["https://mirror1.com/f"], {}, dest, progress=ticks.append + ) + assert mock_sleep.call_args == call(2, ticks.append, 5) + def test_permanent_failure_does_not_retry_sweep(self, tmp_path: Path) -> None: """An HTTP 404 will not heal on its own; fail after a single pass.""" with (