diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index d2ed3b15e9c..2335e947fdc 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -436,9 +436,17 @@ def upload_program(config: ConfigType, args, host: str) -> bool: mcumgr_device = host if mcumgr_device: - firmware = Path( - CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin") - ).resolve() + # `esphome upload --prebuilt-dir ` ships the MCUboot-signed + # update image at the root of the prebuilt directory; prefer that so + # the dashboard's transparent BLE install on a Bluetooth proxy works + # without a local Zephyr build tree. + prebuilt = CORE.prebuilt_artifact_path("app_update.bin") + if prebuilt is not None: + firmware = prebuilt.resolve() + else: + firmware = Path( + CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin") + ).resolve() asyncio.run(smpmgr_upload(mcumgr_device, firmware)) return True # Handled: mcumgr OTA upload diff --git a/tests/unit_tests/components/test_nrf52.py b/tests/unit_tests/components/test_nrf52.py new file mode 100644 index 00000000000..5864b6e0e20 --- /dev/null +++ b/tests/unit_tests/components/test_nrf52.py @@ -0,0 +1,52 @@ +"""Tests for the nrf52 component's custom upload_program.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +from esphome.core import CORE + + +def test_nrf52_upload_program_prebuilt_dir_for_ble_ota(tmp_path: Path) -> None: + """`esphome upload --prebuilt-dir ` for an nRF52 Bluetooth proxy + ships the MCUboot-signed update at /app_update.bin so the + dashboard's transparent BLE install doesn't need a local Zephyr build + tree. Verify the nrf52 upload_program picks up that path instead of + `CORE.relative_pioenvs_path(..., "zephyr", "app_update.bin")`. + """ + prebuilt = tmp_path / "prebuilt" + prebuilt.mkdir() + expected_firmware = prebuilt / "app_update.bin" + expected_firmware.write_bytes(b"signed-mcuboot-image") + CORE.prebuilt_dir = prebuilt + CORE.name = "bt-proxy" + + captured_firmware: list[Path] = [] + + async def fake_upload(device: str, firmware: Path) -> None: + captured_firmware.append(firmware) + + # Patch the heavy nrf52 surface so we exercise only the firmware-path + # resolution: BLE scan returns a fake device, smpmgr_upload records the + # firmware path it would have flashed. + with ( + patch( + "esphome.components.nrf52.ota.smpmgr_scan", + new=AsyncMock(return_value="AA:BB:CC:DD:EE:FF"), + ), + patch( + "esphome.components.nrf52.ota.smpmgr_upload", + new=fake_upload, + ), + patch( + "esphome.components.nrf52.ble_logger.is_mac_address", + return_value=False, + ), + ): + from esphome.components.nrf52 import upload_program + + handled = upload_program({}, MagicMock(), "BLE") + + assert handled is True + assert captured_firmware == [expected_firmware.resolve()]