diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index f391c1791aa..ade726da1fb 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -68,6 +68,9 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { return OTA_RESPONSE_ERROR_WRITING_FLASH; + } else if (err == ESP_ERR_OTA_PARTITION_CONFLICT) { + // This error appears with 1 factory and 1 ota partition + return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; } return OTA_RESPONSE_ERROR_UNKNOWN; } diff --git a/esphome/espota2.py b/esphome/espota2.py index c13c3ea207f..701a125bcdb 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -118,7 +118,8 @@ _ERROR_MESSAGES: dict[int, str] = { ), RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE: ( "The OTA partition on the ESP is too small. ESPHome needs to resize " - "this partition, please flash over USB." + "this partition. Please flash over USB or update the partition table " + "over the air." ), RESPONSE_ERROR_NO_UPDATE_PARTITION: ( "The OTA partition on the ESP couldn't be found. ESPHome needs to " @@ -202,19 +203,19 @@ def receive_exactly( try: data += recv_decode(sock, 1, decode=decode) # type: ignore[operator] except OSError as err: - raise OTAError(f"Error receiving acknowledge {msg}: {err}") from err + raise OTAError(f"receiving {msg} response: {err}") from err try: check_error(data, expect) except OTAError as err: sock.close() - raise OTAError(f"Error {msg}: {err}") from err + raise OTAError(f"receiving {msg}: {err}") from err while len(data) < amount: try: data += recv_decode(sock, amount - len(data), decode=decode) # type: ignore[operator] except OSError as err: - raise OTAError(f"Error receiving {msg}: {err}") from err + raise OTAError(f"receiving {msg}: {err}") from err return data @@ -231,14 +232,14 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None # silently passed through and surface later as cryptic decode/timeout failures. if not data: raise OTAError( - "Error: Device closed connection without responding. " + "Device closed connection without responding. " "This may indicate the device ran out of memory, " "a network issue, or the connection was interrupted." ) dat = data[0] error_msg = _ERROR_MESSAGES.get(dat) if error_msg is not None: - raise OTAError(f"Error: {error_msg}") + raise OTAError(error_msg) if expect is None: return if not isinstance(expect, (list, tuple)): @@ -267,7 +268,7 @@ def send_check( sock.sendall(data) except OSError as err: - raise OTAError(f"Error sending {msg}: {err}") from err + raise OTAError(f"sending {msg}: {err}") from err def perform_ota( @@ -376,7 +377,7 @@ def perform_ota( raise OTAError("ESP requests password, but no password given!") nonce_bytes = receive_exactly( - sock, nonce_size, f"{hash_name} authentication nonce", None, decode=False + sock, nonce_size, f"{hash_name} auth nonce", None, decode=False ) assert isinstance(nonce_bytes, bytes) nonce = nonce_bytes.decode() @@ -424,13 +425,13 @@ def perform_ota( (upload_size >> 0) & 0xFF, ] send_check(sock, upload_size_encoded, "binary size") - receive_exactly(sock, 1, "binary size", RESPONSE_UPDATE_PREPARE_OK) + receive_exactly(sock, 1, "update prepare result", RESPONSE_UPDATE_PREPARE_OK) upload_md5 = hashlib.md5(upload_contents).hexdigest() _LOGGER.debug("MD5 of upload is %s", upload_md5) send_check(sock, upload_md5, "file checksum") - receive_exactly(sock, 1, "file checksum", RESPONSE_BIN_MD5_OK) + receive_exactly(sock, 1, "file checksum result", RESPONSE_BIN_MD5_OK) # Disable nodelay for transfer sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0) @@ -451,10 +452,10 @@ def perform_ota( try: sock.sendall(chunk) if version >= OTA_VERSION_2_0: - receive_exactly(sock, 1, "chunk OK", RESPONSE_CHUNK_OK) + receive_exactly(sock, 1, "chunk result", RESPONSE_CHUNK_OK) except OSError as err: sys.stderr.write("\n") - raise OTAError(f"Error sending data: {err}") from err + raise OTAError(f"sending data: {err}") from err progress.update(offset / upload_size) progress.done() @@ -465,8 +466,8 @@ def perform_ota( _LOGGER.info("Upload took %.2f seconds, waiting for result...", duration) - receive_exactly(sock, 1, "receive OK", RESPONSE_RECEIVE_OK) - receive_exactly(sock, 1, "Update end", RESPONSE_UPDATE_END_OK) + receive_exactly(sock, 1, "update receive result", RESPONSE_RECEIVE_OK) + receive_exactly(sock, 1, "update end result", RESPONSE_UPDATE_END_OK) send_check(sock, RESPONSE_OK, "end acknowledgement") _LOGGER.info("OTA successful") diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index d7fcedfd66d..9413fbcf296 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -135,7 +135,9 @@ def test_receive_exactly_with_error_response(mock_socket: Mock) -> None: """Test receive_exactly raises OTAError on error response.""" mock_socket.recv.return_value = bytes([espota2.RESPONSE_ERROR_AUTH_INVALID]) - with pytest.raises(espota2.OTAError, match="Error auth:.*Authentication invalid"): + with pytest.raises( + espota2.OTAError, match="receiving auth:.*Authentication invalid" + ): espota2.receive_exactly(mock_socket, 1, "auth", [espota2.RESPONSE_OK]) mock_socket.close.assert_called_once() @@ -145,69 +147,69 @@ def test_receive_exactly_socket_error(mock_socket: Mock) -> None: """Test receive_exactly handles socket errors.""" mock_socket.recv.side_effect = OSError("Connection reset") - with pytest.raises(espota2.OTAError, match="Error receiving acknowledge test"): + with pytest.raises(espota2.OTAError, match="receiving test response"): espota2.receive_exactly(mock_socket, 1, "test", espota2.RESPONSE_OK) @pytest.mark.parametrize( ("error_code", "expected_msg"), [ - (espota2.RESPONSE_ERROR_MAGIC, "Error: Invalid magic byte"), - (espota2.RESPONSE_ERROR_UPDATE_PREPARE, "Error: Couldn't prepare flash memory"), - (espota2.RESPONSE_ERROR_AUTH_INVALID, "Error: Authentication invalid"), + (espota2.RESPONSE_ERROR_MAGIC, "Invalid magic byte"), + (espota2.RESPONSE_ERROR_UPDATE_PREPARE, "Couldn't prepare flash memory"), + (espota2.RESPONSE_ERROR_AUTH_INVALID, "Authentication invalid"), ( espota2.RESPONSE_ERROR_WRITING_FLASH, - "Error: Writing OTA data to flash memory failed", + "Writing OTA data to flash memory failed", ), - (espota2.RESPONSE_ERROR_UPDATE_END, "Error: Finishing update failed"), + (espota2.RESPONSE_ERROR_UPDATE_END, "Finishing update failed"), ( espota2.RESPONSE_ERROR_INVALID_BOOTSTRAPPING, - "Error: Please press the reset button", + "Please press the reset button", ), ( espota2.RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG, - "Error: ESP has been flashed with wrong flash size", + "ESP has been flashed with wrong flash size", ), ( espota2.RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG, - "Error: ESP does not have the requested flash size", + "ESP does not have the requested flash size", ), ( espota2.RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE, - "Error: ESP does not have enough space", + "ESP does not have enough space", ), ( espota2.RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE, - "Error: The OTA partition on the ESP is too small", + "The OTA partition on the ESP is too small", ), ( espota2.RESPONSE_ERROR_NO_UPDATE_PARTITION, - "Error: The OTA partition on the ESP couldn't be found", + "The OTA partition on the ESP couldn't be found", ), - (espota2.RESPONSE_ERROR_MD5_MISMATCH, "Error: Application MD5 code mismatch"), + (espota2.RESPONSE_ERROR_MD5_MISMATCH, "Application MD5 code mismatch"), ( espota2.RESPONSE_ERROR_SIGNATURE_INVALID, - "Error: Firmware signature verification failed", + "Firmware signature verification failed", ), ( espota2.RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE, - "Error: The requested OTA type is not supported by the device", + "The requested OTA type is not supported by the device", ), ( espota2.RESPONSE_ERROR_PARTITION_TABLE_VERIFY, - "Error: The partition table update could not be verified", + "The partition table update could not be verified", ), ( espota2.RESPONSE_ERROR_PARTITION_TABLE_UPDATE, - "Error: An error occurred while updating the partition table", + "An error occurred while updating the partition table", ), ( espota2.RESPONSE_ERROR_BOOTLOADER_VERIFY, - "Error: The bootloader update could not be verified", + "The bootloader update could not be verified", ), ( espota2.RESPONSE_ERROR_BOOTLOADER_UPDATE, - "Error: An error occurred while updating the bootloader", + "An error occurred while updating the bootloader", ), (espota2.RESPONSE_ERROR_UNKNOWN, "Unknown error from ESP"), ], @@ -262,7 +264,7 @@ def test_send_check_socket_error(mock_socket: Mock) -> None: """Test send_check handles socket errors.""" mock_socket.sendall.side_effect = OSError("Broken pipe") - with pytest.raises(espota2.OTAError, match="Error sending test"): + with pytest.raises(espota2.OTAError, match="sending test"): espota2.send_check(mock_socket, b"data", "test") @@ -417,7 +419,9 @@ def test_perform_ota_md5_auth_wrong_password( mock_socket.recv.side_effect = recv_responses - with pytest.raises(espota2.OTAError, match="Error auth.*Authentication invalid"): + with pytest.raises( + espota2.OTAError, match="receiving auth.*Authentication invalid" + ): espota2.perform_ota(mock_socket, "wrongpassword", mock_file, "test.bin") # Verify the socket was closed after auth failure @@ -441,7 +445,9 @@ def test_perform_ota_sha256_auth_wrong_password( mock_socket.recv.side_effect = recv_responses - with pytest.raises(espota2.OTAError, match="Error auth.*Authentication invalid"): + with pytest.raises( + espota2.OTAError, match="receiving auth.*Authentication invalid" + ): espota2.perform_ota(mock_socket, "wrongpassword", mock_file, "test.bin") # Verify the socket was closed after auth failure @@ -484,7 +490,7 @@ def test_perform_ota_unexpected_auth_response(mock_socket: Mock) -> None: # This will actually raise "Unexpected response from ESP" from check_error with pytest.raises( - espota2.OTAError, match=r"Error auth: Unexpected response from ESP: 0x03" + espota2.OTAError, match=r"receiving auth: Unexpected response from ESP: 0x03" ): espota2.perform_ota(mock_socket, "password", mock_file, "test.bin") @@ -520,7 +526,7 @@ def test_perform_ota_upload_error(mock_socket: Mock, mock_file: io.BytesIO) -> N mock_socket.recv.side_effect = recv_responses - with pytest.raises(espota2.OTAError, match="Error receiving acknowledge chunk OK"): + with pytest.raises(espota2.OTAError, match="receiving chunk result response"): espota2.perform_ota(mock_socket, None, mock_file, "test.bin") @@ -1109,7 +1115,7 @@ def test_check_error_detects_errors_when_expect_is_none() -> None: during feature negotiation and nonce reads) silently passed error bytes through, turning clean device errors into confusing later failures. """ - with pytest.raises(espota2.OTAError, match="Error: Authentication invalid"): + with pytest.raises(espota2.OTAError, match="Authentication invalid"): espota2.check_error([espota2.RESPONSE_ERROR_AUTH_INVALID], None)