From b1b0638fabef80ac5dc2186e0be8f600018eafba Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 3 Aug 2025 18:35:52 +1000 Subject: [PATCH 1/3] [config] Fix reversion of excessive yaml output after error (#10043) Co-authored-by: J. Nick Koston --- esphome/log.py | 2 + tests/unit_tests/test_log.py | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/unit_tests/test_log.py diff --git a/esphome/log.py b/esphome/log.py index 8831b1b2b3..bfd1875b55 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -37,6 +37,8 @@ class AnsiStyle(Enum): def color(col: AnsiFore, msg: str, reset: bool = True) -> str: + if col == AnsiFore.KEEP: + return msg s = col.value + msg if reset and col: s += AnsiStyle.RESET_ALL.value diff --git a/tests/unit_tests/test_log.py b/tests/unit_tests/test_log.py new file mode 100644 index 0000000000..02798f1029 --- /dev/null +++ b/tests/unit_tests/test_log.py @@ -0,0 +1,80 @@ +import pytest + +from esphome.log import AnsiFore, AnsiStyle, color + + +def test_color_keep_returns_unchanged_message() -> None: + """Test that AnsiFore.KEEP returns the message unchanged.""" + msg = "test message" + result = color(AnsiFore.KEEP, msg) + assert result == msg + + +def test_color_keep_ignores_reset_parameter() -> None: + """Test that reset parameter is ignored when using AnsiFore.KEEP.""" + msg = "test message" + result_with_reset = color(AnsiFore.KEEP, msg, reset=True) + result_without_reset = color(AnsiFore.KEEP, msg, reset=False) + assert result_with_reset == msg + assert result_without_reset == msg + + +def test_color_applies_color_code() -> None: + """Test that color codes are properly applied to messages.""" + msg = "test message" + result = color(AnsiFore.RED, msg, reset=False) + assert result == f"{AnsiFore.RED.value}{msg}" + + +def test_color_applies_reset_when_requested() -> None: + """Test that RESET_ALL is added when reset=True.""" + msg = "test message" + result = color(AnsiFore.GREEN, msg, reset=True) + expected = f"{AnsiFore.GREEN.value}{msg}{AnsiStyle.RESET_ALL.value}" + assert result == expected + + +def test_color_no_reset_when_not_requested() -> None: + """Test that RESET_ALL is not added when reset=False.""" + msg = "test message" + result = color(AnsiFore.BLUE, msg, reset=False) + expected = f"{AnsiFore.BLUE.value}{msg}" + assert result == expected + + +def test_color_with_empty_message() -> None: + """Test color function with empty message.""" + result = color(AnsiFore.YELLOW, "", reset=True) + expected = f"{AnsiFore.YELLOW.value}{AnsiStyle.RESET_ALL.value}" + assert result == expected + + +@pytest.mark.parametrize( + "col", + [ + AnsiFore.BLACK, + AnsiFore.RED, + AnsiFore.GREEN, + AnsiFore.YELLOW, + AnsiFore.BLUE, + AnsiFore.MAGENTA, + AnsiFore.CYAN, + AnsiFore.WHITE, + AnsiFore.RESET, + ], +) +def test_all_ansi_colors(col: AnsiFore) -> None: + """Test that all AnsiFore colors work correctly.""" + msg = "test" + result = color(col, msg, reset=True) + expected = f"{col.value}{msg}{AnsiStyle.RESET_ALL.value}" + assert result == expected + + +def test_ansi_fore_keep_is_enum_member() -> None: + """Ensure AnsiFore.KEEP is an Enum member and evaluates to truthy.""" + assert isinstance(AnsiFore.KEEP, AnsiFore) + # Enum members are truthy, even with empty string values + assert bool(AnsiFore.KEEP) is True + # But the value itself is still an empty string + assert AnsiFore.KEEP.value == "" From be7b63898ff6878eb192cd762d0a1daf0cf8dbd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 13:43:15 -1000 Subject: [PATCH 2/3] [api] Fix OTA progress updates not being sent when main loop is blocked --- esphome/components/api/api_connection.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 21688e601c..d15ad58114 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -703,10 +703,13 @@ class APIConnection : public APIServerConnection { bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { // Try to send immediately if: - // 1. We should try to send immediately (should_try_send_immediately = true) - // 2. Batch delay is 0 (user has opted in to immediate sending) - // 3. Buffer has space available - if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 && + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. OR: We should try to send immediately (should_try_send_immediately = true) + // AND Batch delay is 0 (user has opted in to immediate sending) + // 3. AND: Buffer has space available + if ((message_type == UpdateStateResponse::MESSAGE_TYPE || + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && From 3e69f41b42a71f6ec81617744e3ee6ac879e6442 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 13:54:03 -1000 Subject: [PATCH 3/3] needs ifdef --- esphome/components/api/api_connection.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index d15ad58114..f0f308c248 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -708,8 +708,11 @@ class APIConnection : public APIServerConnection { // 2. OR: We should try to send immediately (should_try_send_immediately = true) // AND Batch delay is 0 (user has opted in to immediate sending) // 3. AND: Buffer has space available - if ((message_type == UpdateStateResponse::MESSAGE_TYPE || - (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && + if (( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&