From edb16a27d374c55c86e8679cae43a36afc343b6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Apr 2026 16:58:48 -1000 Subject: [PATCH 1/2] [esphome] Skip missing extra flash images in upload_using_esptool (#15723) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/__main__.py | 9 +++++++- tests/unit_tests/test_main.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 25b404ae45..7879cdad0c 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -750,8 +750,15 @@ def upload_using_esptool( platformio_api.FlashImage( path=idedata.firmware_bin_path, offset=firmware_offset ), - *idedata.extra_flash_images, ] + for image in idedata.extra_flash_images: + if not image.path.is_file(): + _LOGGER.warning( + "Skipping missing flash image declared by platform: %s", + image.path, + ) + continue + flash_images.append(image) mcu = "esp8266" if CORE.is_esp32: diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 85536d2f1c..e07b4accf2 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1231,6 +1231,48 @@ def test_upload_using_esptool_path_conversion( assert partitions_path.endswith("partitions.bin") +def test_upload_using_esptool_skips_missing_extra_flash_images( + tmp_path: Path, + mock_run_external_command_main: Mock, + mock_get_idedata: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """A non-existent path in extra_flash_images must be filtered out with a + warning, and must not appear in the esptool command line. Only the valid + images are flashed. Regression test for + https://github.com/esphome/esphome/issues/15634. + """ + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test") + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} + + missing_path = tmp_path / "variants" / "tasmota" / "tinyuf2.bin" + + mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" + mock_idedata.extra_flash_images = [ + platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), + platformio_api.FlashImage(path=missing_path, offset="0x2d0000"), + ] + mock_get_idedata.return_value = mock_idedata + + (tmp_path / "firmware.bin").touch() + (tmp_path / "bootloader.bin").touch() + # Intentionally do NOT create missing_path + + config = {CONF_ESPHOME: {"platformio_options": {}}} + + with caplog.at_level(logging.WARNING, logger="esphome.__main__"): + result = upload_using_esptool(config, "/dev/ttyUSB0", None, None) + + assert result == 0 + assert "Skipping missing flash image" in caplog.text + assert str(missing_path) in caplog.text + + cmd_list = list(mock_run_external_command_main.call_args[0][1:]) + assert str(missing_path) not in cmd_list + assert "0x2d0000" not in cmd_list + + def test_upload_using_esptool_with_file_path( tmp_path: Path, mock_run_external_command_main: Mock, From a0286616f71f0b654f25b443bcc0ae4ba0723bc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Apr 2026 17:16:49 -1000 Subject: [PATCH 2/2] [light] Force-inline LightCall::set_flag_/clear_flag_ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both are trivial bit operations on a uint16_t (one or/and on flags_), but without ESPHOME_ALWAYS_INLINE each call costs a call0/call8 plus the return/register-window rotation — more work than the body itself. validate_() invokes these ~8 times and transform_parameters_() adds more, so runtime benchmarks should reflect the elimination of the call overhead. Note: a previous attempt at this (#15402) was closed based on flash size deltas alone. This reopens the question with CodSpeed in-loop to measure actual runtime impact. --- esphome/components/light/light_call.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 88d29bd349..39953d0d20 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -222,7 +222,7 @@ class LightCall { inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } // Helper to set flag - defaults to true for common case - void set_flag_(FieldFlags flag, bool value = true) { + void set_flag_(FieldFlags flag, bool value = true) ESPHOME_ALWAYS_INLINE { if (value) { this->flags_ |= flag; } else { @@ -231,7 +231,7 @@ class LightCall { } // Helper to clear flag - reduces code size for common case - void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; } + void clear_flag_(FieldFlags flag) ESPHOME_ALWAYS_INLINE { this->flags_ &= ~flag; } // Helper to log unsupported feature and clear flag - reduces code duplication void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);