From a6cd2a9f4d55535920ced66aade860c95eb1f75f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 May 2026 09:08:07 -0500 Subject: [PATCH] improve error reporting in case we mess it up later --- esphome/espota2.py | 14 ++++++++++- tests/unit_tests/test_espota2.py | 40 ++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/esphome/espota2.py b/esphome/espota2.py index e1cc52e7ce..ff94417f00 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -285,7 +285,19 @@ def perform_ota( features = 0 if ota_type != OTA_TYPE_UPDATE_APP: - raise OTAError(f"Unsupported OTA type: 0x{ota_type:02X}") + # Any non-app OTA type requires the extended protocol and the + # partition-access server feature. Reject up front so the user gets + # a clear capability error instead of a post-auth 0x8E from the device. + if not extended_proto: + raise OTAError( + f"Device does not support extended OTA protocol; " + f"OTA type 0x{ota_type:02X} requires it" + ) + if not (features & SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS): + raise OTAError( + f"Device does not support partition access; " + f"OTA type 0x{ota_type:02X} cannot be used" + ) if features & SERVER_FEATURE_SUPPORTS_COMPRESSION: upload_contents = gzip.compress(file_contents, compresslevel=9) diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index 1baf3f5db4..2b2a8f90f2 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -832,20 +832,50 @@ def test_perform_ota_extended_protocol_app( @pytest.mark.usefixtures("mock_time") -def test_perform_ota_extended_protocol_unsupported_type( +def test_perform_ota_non_app_type_requires_extended_protocol( mock_socket: Mock, mock_file: io.BytesIO ) -> None: - """Test OTA fails when OTA type is unsupported by the client.""" - # Setup socket responses for recv calls + """Non-app OTA type must fail when device only supports the legacy protocol.""" recv_responses = [ bytes([espota2.RESPONSE_OK]), # First byte of version response bytes([espota2.OTA_VERSION_2_0]), # Version number - bytes([espota2.RESPONSE_HEADER_OK]), # Features response + bytes([espota2.RESPONSE_HEADER_OK]), # Legacy single-byte feature ack ] mock_socket.recv.side_effect = recv_responses - with pytest.raises(espota2.OTAError, match="Unsupported OTA type"): + with pytest.raises( + espota2.OTAError, match="Device does not support extended OTA protocol" + ): + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + 255, + ) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_non_app_type_requires_partition_access( + mock_socket: Mock, mock_file: io.BytesIO +) -> None: + """Non-app OTA type must fail when device advertises extended protocol but + not the partition-access feature.""" + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_FEATURE_FLAGS]), # Extended protocol marker + bytes( + [espota2.SERVER_FEATURE_SUPPORTS_COMPRESSION] + ), # Compression only, no partition access + ] + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises( + espota2.OTAError, match="Device does not support partition access" + ): espota2.perform_ota( mock_socket, "testpass",