mirror of
https://github.com/esphome/esphome.git
synced 2026-05-23 11:16:52 +08:00
[ota] Improve OTA error messages (#16327)
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Check import esphome.__main__ time (push) Has been cancelled
CI / Test downstream esphome/device-builder (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.14) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.14) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (${{ matrix.bucket.name }}) (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run CodSpeed benchmarks (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / Test components with native ESP-IDF (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04-arm) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04-arm) (push) Has been cancelled
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Check import esphome.__main__ time (push) Has been cancelled
CI / Test downstream esphome/device-builder (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.14) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.14) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (${{ matrix.bucket.name }}) (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run CodSpeed benchmarks (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / Test components with native ESP-IDF (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04-arm) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04-arm) (push) Has been cancelled
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+15
-14
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user