From cdeed7afa72dc62a974ddf102ff1b8b8bf5512d8 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 17 Jul 2025 23:45:07 +0200 Subject: [PATCH 001/125] Fix template event web_server crash (#9618) --- esphome/components/web_server/web_server.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index fe5c197329..bb2640b539 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1620,7 +1620,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } -static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; } +static std::string get_event_type(event::Event *event) { + return (event && event->last_event_type) ? *event->last_event_type : ""; +} std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); From 21e66b76e4fe514014fe7c7edfc8441d59fb5cb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Jul 2025 12:55:39 -1000 Subject: [PATCH 002/125] [api] Fix compilation error with char* lambdas in HomeAssistant services (#9638) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/api/homeassistant_service.h | 3 +++ .../fixtures/api_string_lambda.yaml | 23 +++++++++++++++++++ tests/integration/test_api_string_lambda.py | 21 ++++++++++++++--- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 223af132db..f765f1f806 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -16,6 +16,9 @@ template class TemplatableStringValue : public TemplatableValue static std::string value_to_string(T &&val) { return to_string(std::forward(val)); } // Overloads for string types - needed because std::to_string doesn't support them + static std::string value_to_string(char *val) { + return val ? std::string(val) : std::string(); + } // For lambdas returning char* (e.g., itoa) static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str() static std::string value_to_string(const std::string &val) { return val; } static std::string value_to_string(std::string &&val) { return std::move(val); } diff --git a/tests/integration/fixtures/api_string_lambda.yaml b/tests/integration/fixtures/api_string_lambda.yaml index 18440b9984..e2da4683c0 100644 --- a/tests/integration/fixtures/api_string_lambda.yaml +++ b/tests/integration/fixtures/api_string_lambda.yaml @@ -60,5 +60,28 @@ api: data: value: !lambda 'return input_float;' + # Service that tests char* lambda functionality (e.g., from itoa or sprintf) + - action: test_char_ptr_lambda + variables: + input_number: int + input_string: string + then: + # Log the input to verify service was called + - logger.log: + format: "Service called with number for char* test: %d" + args: [input_number] + + # Test that char* lambdas work correctly + # This would fail in issue #9628 with "invalid conversion from 'char*' to 'long long unsigned int'" + - homeassistant.event: + event: esphome.test_char_ptr_lambda + data: + # Test snprintf returning char* + decimal_value: !lambda 'static char buffer[20]; snprintf(buffer, sizeof(buffer), "%d", input_number); return buffer;' + # Test strdup returning char* (dynamically allocated) + string_copy: !lambda 'return strdup(input_string.c_str());' + # Test string literal (const char*) + literal: !lambda 'return "test literal";' + logger: level: DEBUG diff --git a/tests/integration/test_api_string_lambda.py b/tests/integration/test_api_string_lambda.py index 3bef2d86e2..f4ef77bad8 100644 --- a/tests/integration/test_api_string_lambda.py +++ b/tests/integration/test_api_string_lambda.py @@ -19,15 +19,17 @@ async def test_api_string_lambda( """Test TemplatableStringValue works with lambdas that return different types.""" loop = asyncio.get_running_loop() - # Track log messages for all three service calls + # Track log messages for all four service calls string_called_future = loop.create_future() int_called_future = loop.create_future() float_called_future = loop.create_future() + char_ptr_called_future = loop.create_future() # Patterns to match in logs - confirms the lambdas compiled and executed string_pattern = re.compile(r"Service called with string: STRING_FROM_LAMBDA") int_pattern = re.compile(r"Service called with int: 42") float_pattern = re.compile(r"Service called with float: 3\.14") + char_ptr_pattern = re.compile(r"Service called with number for char\* test: 123") def check_output(line: str) -> None: """Check log output for expected messages.""" @@ -37,6 +39,8 @@ async def test_api_string_lambda( int_called_future.set_result(True) if not float_called_future.done() and float_pattern.search(line): float_called_future.set_result(True) + if not char_ptr_called_future.done() and char_ptr_pattern.search(line): + char_ptr_called_future.set_result(True) # Run with log monitoring async with ( @@ -65,17 +69,28 @@ async def test_api_string_lambda( ) assert float_service is not None, "test_float_lambda service not found" - # Execute all three services to test different lambda return types + char_ptr_service = next( + (s for s in services if s.name == "test_char_ptr_lambda"), None + ) + assert char_ptr_service is not None, "test_char_ptr_lambda service not found" + + # Execute all four services to test different lambda return types client.execute_service(string_service, {"input_string": "STRING_FROM_LAMBDA"}) client.execute_service(int_service, {"input_number": 42}) client.execute_service(float_service, {"input_float": 3.14}) + client.execute_service( + char_ptr_service, {"input_number": 123, "input_string": "test_string"} + ) # Wait for all service log messages # This confirms the lambdas compiled successfully and executed try: await asyncio.wait_for( asyncio.gather( - string_called_future, int_called_future, float_called_future + string_called_future, + int_called_future, + float_called_future, + char_ptr_called_future, ), timeout=5.0, ) From 4a43f922c65bb6a7c03f85cdda3daf932450b70a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Jul 2025 13:00:56 -1000 Subject: [PATCH 003/125] [wireguard] Fix boot loop when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled (#9637) --- esphome/components/wireguard/wireguard.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 1f61e2dda3..4efcf13e08 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -8,6 +8,7 @@ #include "esphome/core/log.h" #include "esphome/core/time.h" #include "esphome/components/network/util.h" +#include "esphome/core/helpers.h" #include #include @@ -42,7 +43,10 @@ void Wireguard::setup() { this->publish_enabled_state(); - this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); + { + LwIPLock lock; + this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); + } if (this->wg_initialized_ == ESP_OK) { ESP_LOGI(TAG, "Initialized"); @@ -249,7 +253,10 @@ void Wireguard::start_connection_() { } ESP_LOGD(TAG, "Starting connection"); - this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_)); + { + LwIPLock lock; + this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_)); + } if (this->wg_connected_ == ESP_OK) { ESP_LOGI(TAG, "Connection started"); @@ -280,7 +287,10 @@ void Wireguard::start_connection_() { void Wireguard::stop_connection_() { if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) { ESP_LOGD(TAG, "Stopping connection"); - esp_wireguard_disconnect(&(this->wg_ctx_)); + { + LwIPLock lock; + esp_wireguard_disconnect(&(this->wg_ctx_)); + } this->wg_connected_ = ESP_FAIL; } } From c602f3082eb92baa419eb16716561184e2b7de3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Jul 2025 16:20:35 -1000 Subject: [PATCH 004/125] [scheduler] Fix cancellation of timers with empty string names (#9641) --- esphome/core/component.cpp | 4 +- esphome/core/scheduler.cpp | 2 +- esphome/core/scheduler.h | 7 +- .../fixtures/scheduler_string_test.yaml | 117 +++++++++++++++++- .../integration/test_scheduler_heap_stress.py | 5 +- 5 files changed, 123 insertions(+), 12 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index c47f16b5f7..800fbcaa28 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -252,10 +252,10 @@ void Component::defer(const char *name, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); } void Component::set_timeout(uint32_t timeout, std::function &&f) { // NOLINT - App.scheduler.set_timeout(this, "", timeout, std::move(f)); + App.scheduler.set_timeout(this, static_cast(nullptr), timeout, std::move(f)); } void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT - App.scheduler.set_interval(this, "", interval, std::move(f)); + App.scheduler.set_interval(this, static_cast(nullptr), interval, std::move(f)); } void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, float backoff_increase_factor) { // NOLINT diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index c6893b128f..8a31e4f42e 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -446,7 +446,7 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co // Helper to cancel items by name - must be called with lock held bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) { // Early return if name is invalid - no items to cancel - if (name_cstr == nullptr || name_cstr[0] == '\0') { + if (name_cstr == nullptr) { return false; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 39cee5a876..a3da2c20f6 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -114,16 +114,17 @@ class Scheduler { name_is_dynamic = false; } - if (!name || !name[0]) { + if (!name) { + // nullptr case - no name provided name_.static_name = nullptr; } else if (make_copy) { - // Make a copy for dynamic strings + // Make a copy for dynamic strings (including empty strings) size_t len = strlen(name); name_.dynamic_name = new char[len + 1]; memcpy(name_.dynamic_name, name, len + 1); name_is_dynamic = true; } else { - // Use static string directly + // Use static string directly (including empty strings) name_.static_name = name; } } diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml index 3dfe891370..c53ec392df 100644 --- a/tests/integration/fixtures/scheduler_string_test.yaml +++ b/tests/integration/fixtures/scheduler_string_test.yaml @@ -4,9 +4,7 @@ esphome: priority: -100 then: - logger.log: "Starting scheduler string tests" - platformio_options: - build_flags: - - "-DESPHOME_DEBUG_SCHEDULER" # Enable scheduler debug logging + debug_scheduler: true # Enable scheduler debug logging host: api: @@ -32,6 +30,12 @@ globals: - id: results_reported type: bool initial_value: 'false' + - id: edge_tests_done + type: bool + initial_value: 'false' + - id: empty_cancel_failed + type: bool + initial_value: 'false' script: - id: test_static_strings @@ -147,12 +151,106 @@ script: static TestDynamicDeferComponent test_dynamic_defer_component; test_dynamic_defer_component.test_dynamic_defer(); + - id: test_cancellation_edge_cases + then: + - logger.log: "Testing cancellation edge cases" + - lambda: |- + auto *component1 = id(test_sensor1); + // Use a different component for empty string tests to avoid interference + auto *component2 = id(test_sensor2); + + // Test 12: Cancel with empty string - regression test for issue #9599 + // First create a timeout with empty name on component2 to avoid interference + App.scheduler.set_timeout(component2, "", 500, []() { + ESP_LOGE("test", "ERROR: Empty name timeout fired - it should have been cancelled!"); + id(empty_cancel_failed) = true; + }); + + // Now cancel it - this should work after our fix + bool cancelled_empty = App.scheduler.cancel_timeout(component2, ""); + ESP_LOGI("test", "Cancel empty string result: %s (should be true)", cancelled_empty ? "true" : "false"); + if (!cancelled_empty) { + ESP_LOGE("test", "ERROR: Failed to cancel empty string timeout!"); + id(empty_cancel_failed) = true; + } + + // Test 13: Cancel non-existent timeout + bool cancelled_nonexistent = App.scheduler.cancel_timeout(component1, "does_not_exist"); + ESP_LOGI("test", "Cancel non-existent timeout result: %s", + cancelled_nonexistent ? "true (unexpected!)" : "false (expected)"); + + // Test 14: Multiple timeouts with same name - only last should execute + for (int i = 0; i < 5; i++) { + App.scheduler.set_timeout(component1, "duplicate_timeout", 200 + i*10, [i]() { + ESP_LOGI("test", "Duplicate timeout %d fired", i); + id(timeout_counter) += 1; + }); + } + ESP_LOGI("test", "Created 5 timeouts with same name 'duplicate_timeout'"); + + // Test 15: Multiple intervals with same name - only last should run + for (int i = 0; i < 3; i++) { + App.scheduler.set_interval(component1, "duplicate_interval", 300, [i]() { + ESP_LOGI("test", "Duplicate interval %d fired", i); + id(interval_counter) += 10; // Large increment to detect multiple + // Cancel after first execution + App.scheduler.cancel_interval(id(test_sensor1), "duplicate_interval"); + }); + } + ESP_LOGI("test", "Created 3 intervals with same name 'duplicate_interval'"); + + // Test 16: Cancel with nullptr protection (via empty const char*) + const char* null_name = ""; + App.scheduler.set_timeout(component2, null_name, 600, []() { + ESP_LOGE("test", "ERROR: Const char* empty timeout fired - should have been cancelled!"); + id(empty_cancel_failed) = true; + }); + bool cancelled_const_empty = App.scheduler.cancel_timeout(component2, null_name); + ESP_LOGI("test", "Cancel const char* empty result: %s (should be true)", + cancelled_const_empty ? "true" : "false"); + if (!cancelled_const_empty) { + ESP_LOGE("test", "ERROR: Failed to cancel const char* empty timeout!"); + id(empty_cancel_failed) = true; + } + + // Test 17: Rapid create/cancel/create with same name + App.scheduler.set_timeout(component1, "rapid_test", 5000, []() { + ESP_LOGI("test", "First rapid timeout - should not fire"); + id(timeout_counter) += 100; + }); + App.scheduler.cancel_timeout(component1, "rapid_test"); + App.scheduler.set_timeout(component1, "rapid_test", 250, []() { + ESP_LOGI("test", "Second rapid timeout - should fire"); + id(timeout_counter) += 1; + }); + + // Test 18: Cancel all with a specific name (multiple instances) + // Create multiple with same name + App.scheduler.set_timeout(component1, "multi_cancel", 300, []() { + ESP_LOGI("test", "Multi-cancel timeout 1"); + }); + App.scheduler.set_timeout(component1, "multi_cancel", 350, []() { + ESP_LOGI("test", "Multi-cancel timeout 2"); + }); + App.scheduler.set_timeout(component1, "multi_cancel", 400, []() { + ESP_LOGI("test", "Multi-cancel timeout 3 - only this should fire"); + id(timeout_counter) += 1; + }); + // Note: Each set_timeout with same name cancels the previous one automatically + - id: report_results then: - lambda: |- ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d", id(timeout_counter), id(interval_counter)); + // Check if empty string cancellation test passed + if (id(empty_cancel_failed)) { + ESP_LOGE("test", "ERROR: Empty string cancellation test FAILED!"); + } else { + ESP_LOGI("test", "Empty string cancellation test PASSED"); + } + sensor: - platform: template name: Test Sensor 1 @@ -189,12 +287,23 @@ interval: - delay: 0.2s - script.execute: test_dynamic_strings + # Run cancellation edge case tests after dynamic tests + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(dynamic_tests_done) && !id(edge_tests_done);' + then: + - lambda: 'id(edge_tests_done) = true;' + - delay: 0.5s + - script.execute: test_cancellation_edge_cases + # Report results after all tests - interval: 0.2s then: - if: condition: - lambda: 'return id(dynamic_tests_done) && !id(results_reported);' + lambda: 'return id(edge_tests_done) && !id(results_reported);' then: - lambda: 'id(results_reported) = true;' - delay: 1s diff --git a/tests/integration/test_scheduler_heap_stress.py b/tests/integration/test_scheduler_heap_stress.py index 3c757bfc9d..229b5b98df 100644 --- a/tests/integration/test_scheduler_heap_stress.py +++ b/tests/integration/test_scheduler_heap_stress.py @@ -103,13 +103,14 @@ async def test_scheduler_heap_stress( # Wait for all callbacks to execute (should be quick, but give more time for scheduling) try: - await asyncio.wait_for(test_complete_future, timeout=60.0) + await asyncio.wait_for(test_complete_future, timeout=10.0) except asyncio.TimeoutError: # Report how many we got + missing_ids = sorted(set(range(1000)) - executed_callbacks) pytest.fail( f"Stress test timed out. Only {len(executed_callbacks)} of " f"1000 callbacks executed. Missing IDs: " - f"{sorted(set(range(1000)) - executed_callbacks)[:10]}..." + f"{missing_ids[:20]}... (total missing: {len(missing_ids)})" ) # Verify all callbacks executed From 121ed687f383e30b2824b8a2157b8d1f758d43dc Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Thu, 17 Jul 2025 20:08:18 -0700 Subject: [PATCH 005/125] [logger] fix on_message (#9642) Co-authored-by: Samuel Sieb Co-authored-by: J. Nick Koston Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/logger/__init__.py | 4 ++-- .../logger/test-on_message.host.yaml | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 tests/components/logger/test-on_message.host.yaml diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 9ac2999696..cf2af17677 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -183,7 +183,7 @@ def validate_local_no_higher_than_global(value): Logger = logger_ns.class_("Logger", cg.Component) LoggerMessageTrigger = logger_ns.class_( "LoggerMessageTrigger", - automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), + automation.Trigger.template(cg.uint8, cg.const_char_ptr, cg.const_char_ptr), ) CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" @@ -368,7 +368,7 @@ async def to_code(config): await automation.build_automation( trigger, [ - (cg.int_, "level"), + (cg.uint8, "level"), (cg.const_char_ptr, "tag"), (cg.const_char_ptr, "message"), ], diff --git a/tests/components/logger/test-on_message.host.yaml b/tests/components/logger/test-on_message.host.yaml new file mode 100644 index 0000000000..12211a257b --- /dev/null +++ b/tests/components/logger/test-on_message.host.yaml @@ -0,0 +1,18 @@ +logger: + id: logger_id + level: DEBUG + on_message: + - level: DEBUG + then: + - lambda: |- + ESP_LOGD("test", "Got message level %d: %s - %s", level, tag, message); + - level: WARN + then: + - lambda: |- + ESP_LOGW("test", "Warning level %d from %s", level, tag); + - level: ERROR + then: + - lambda: |- + // Test that level is uint8_t by using it in calculations + uint8_t adjusted_level = level + 1; + ESP_LOGE("test", "Error with adjusted level %d", adjusted_level); From 11a4115e30ead221f7886e64b2c565a6b3fd644b Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Fri, 18 Jul 2025 05:09:24 +0200 Subject: [PATCH 006/125] esp32_camera: deprecate i2c_pins; throw error if combined with i2c: block (#9615) --- esphome/components/esp32_camera/__init__.py | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 6e36f7d5a7..a99ec34087 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation, pins import esphome.codegen as cg from esphome.components import i2c @@ -8,6 +10,7 @@ from esphome.const import ( CONF_CONTRAST, CONF_DATA_PINS, CONF_FREQUENCY, + CONF_I2C, CONF_I2C_ID, CONF_ID, CONF_PIN, @@ -20,6 +23,9 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.core.entity_helpers import setup_entity +import esphome.final_validate as fv + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["esp32"] @@ -250,6 +256,22 @@ CONFIG_SCHEMA = cv.All( cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), ) + +def _final_validate(config): + if CONF_I2C_PINS not in config: + return + fconf = fv.full_config.get() + if fconf.get(CONF_I2C): + raise cv.Invalid( + "The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead" + ) + _LOGGER.warning( + "The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead." + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + SETTERS = { # pin assignment CONF_DATA_PINS: "set_data_pins", From 84a77ee427b391bd7f498d0a82c5034c748c473c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Jul 2025 18:07:59 -1000 Subject: [PATCH 007/125] [scheduler] Fix DelayAction cancellation in restart mode scripts (#9646) --- esphome/core/base_automation.h | 4 +- .../fixtures/delay_action_cancellation.yaml | 24 +++++ tests/integration/test_automations.py | 91 +++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 tests/integration/fixtures/delay_action_cancellation.yaml create mode 100644 tests/integration/test_automations.py diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 13179b90bb..740e10700b 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -158,14 +158,14 @@ template class DelayAction : public Action, public Compon void play_complex(Ts... x) override { auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; - this->set_timeout(this->delay_.value(x...), f); + this->set_timeout("delay", this->delay_.value(x...), f); } float get_setup_priority() const override { return setup_priority::HARDWARE; } void play(Ts... x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout(""); } + void stop() override { this->cancel_timeout("delay"); } }; template class LambdaAction : public Action { diff --git a/tests/integration/fixtures/delay_action_cancellation.yaml b/tests/integration/fixtures/delay_action_cancellation.yaml new file mode 100644 index 0000000000..e0dd427c2d --- /dev/null +++ b/tests/integration/fixtures/delay_action_cancellation.yaml @@ -0,0 +1,24 @@ +esphome: + name: test-delay-action + +host: +api: + actions: + - action: start_delay_then_restart + then: + - logger.log: "Starting first script execution" + - script.execute: test_delay_script + - delay: 250ms # Give first script time to start delay + - logger.log: "Restarting script (should cancel first delay)" + - script.execute: test_delay_script + +logger: + level: DEBUG + +script: + - id: test_delay_script + mode: restart + then: + - logger.log: "Script started, beginning delay" + - delay: 500ms # Long enough that it won't complete before restart + - logger.log: "Delay completed successfully" diff --git a/tests/integration/test_automations.py b/tests/integration/test_automations.py new file mode 100644 index 0000000000..bd2082e86b --- /dev/null +++ b/tests/integration/test_automations.py @@ -0,0 +1,91 @@ +"""Test ESPHome automations functionality.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_delay_action_cancellation( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that delay actions can be properly cancelled when script restarts.""" + loop = asyncio.get_running_loop() + + # Track log messages with timestamps + log_entries: list[tuple[float, str]] = [] + script_starts: list[float] = [] + delay_completions: list[float] = [] + script_restart_logged = False + test_started_time = None + + # Patterns to match + test_start_pattern = re.compile(r"Starting first script execution") + script_start_pattern = re.compile(r"Script started, beginning delay") + restart_pattern = re.compile(r"Restarting script \(should cancel first delay\)") + delay_complete_pattern = re.compile(r"Delay completed successfully") + + # Future to track when we can check results + second_script_started = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + nonlocal script_restart_logged, test_started_time + + current_time = loop.time() + log_entries.append((current_time, line)) + + if test_start_pattern.search(line): + test_started_time = current_time + elif script_start_pattern.search(line) and test_started_time: + script_starts.append(current_time) + if len(script_starts) == 2 and not second_script_started.done(): + second_script_started.set_result(True) + elif restart_pattern.search(line): + script_restart_logged = True + elif delay_complete_pattern.search(line): + delay_completions.append(current_time) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + entities, services = await client.list_entities_services() + + # Find our test service + test_service = next( + (s for s in services if s.name == "start_delay_then_restart"), None + ) + assert test_service is not None, "start_delay_then_restart service not found" + + # Execute the test sequence + client.execute_service(test_service, {}) + + # Wait for the second script to start + await asyncio.wait_for(second_script_started, timeout=5.0) + + # Wait for potential delay completion + await asyncio.sleep(0.75) # Original delay was 500ms + + # Check results + assert len(script_starts) == 2, ( + f"Script should have started twice, but started {len(script_starts)} times" + ) + assert script_restart_logged, "Script restart was not logged" + + # Verify we got exactly one completion and it happened ~500ms after the second start + assert len(delay_completions) == 1, ( + f"Expected 1 delay completion, got {len(delay_completions)}" + ) + time_from_second_start = delay_completions[0] - script_starts[1] + assert 0.4 < time_from_second_start < 0.6, ( + f"Delay completed {time_from_second_start:.3f}s after second start, expected ~0.5s" + ) From 85495d38b736b8a4bdd094c8f9129bbf35cafaa9 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:14:21 +1000 Subject: [PATCH 008/125] [lvgl] Fix meter rotation (#9605) Co-authored-by: clydeps --- esphome/components/lvgl/widgets/meter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 04de195e3c..acec986f99 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_VALUE, CONF_WIDTH, ) +from esphome.cpp_generator import IntLiteral from ..automation import action_to_code from ..defines import ( @@ -188,6 +189,8 @@ class MeterType(WidgetType): rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 if CONF_ROTATION in scale_conf: rotation = await lv_angle.process(scale_conf[CONF_ROTATION]) + if isinstance(rotation, IntLiteral): + rotation = int(str(rotation)) // 10 with LocalVariable( "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) ) as meter_var: From cc2c1b1d89216254b872f820f0a7aa6361b43cd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 01:40:14 -1000 Subject: [PATCH 009/125] [libretiny] Remove unsupported lock-free queue and event pool implementations (#9653) --- esphome/core/event_pool.h | 4 ++-- esphome/core/lock_free_queue.h | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h index 69e03bafac..928a4e7dee 100644 --- a/esphome/core/event_pool.h +++ b/esphome/core/event_pool.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) #include #include @@ -78,4 +78,4 @@ template class EventPool { } // namespace esphome -#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) +#endif // defined(USE_ESP32) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index f35cfa5af9..de07b0ebba 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -1,17 +1,12 @@ #pragma once -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) #include #include -#if defined(USE_ESP32) #include #include -#elif defined(USE_LIBRETINY) -#include -#include -#endif /* * Lock-free queue for single-producer single-consumer scenarios. @@ -148,4 +143,4 @@ template class NotifyingLockFreeQueue : public LockFreeQu } // namespace esphome -#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) +#endif // defined(USE_ESP32) From 976a1e27b4f0e9618c46a77a9ea56d36931289b8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:49:34 +1200 Subject: [PATCH 010/125] [lvgl] Prevent keyerror on min/max value widgets with no default (#9660) --- esphome/components/lvgl/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 40e69119f0..10b6f63528 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -192,7 +192,7 @@ class WidgetType: class NumberType(WidgetType): def get_max(self, config: dict): - return int(config[CONF_MAX_VALUE] or 100) + return int(config.get(CONF_MAX_VALUE, 100)) def get_min(self, config: dict): - return int(config[CONF_MIN_VALUE] or 0) + return int(config.get(CONF_MIN_VALUE, 0)) From 32d8c60a0b4a6d45f5b031b83545ec503e0dd4a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 09:20:08 -1000 Subject: [PATCH 011/125] Fix AsyncTCP version mismatch between platformio.ini and async_tcp component (#9676) --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 8fcc578103..7fb301c08b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -138,7 +138,7 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - ESP32Async/AsyncTCP@3.4.4 ; async_tcp + ESP32Async/AsyncTCP@3.4.5 ; async_tcp NetworkClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) From 8664ec0a3b42366c1823261430c297910d59914a Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 18 Jul 2025 20:21:36 +0100 Subject: [PATCH 012/125] [speaker] Media player's pipeline properly returns playing state near end of file (#9668) --- .../speaker/media_player/audio_pipeline.cpp | 16 ++++++++++++++-- .../speaker/media_player/audio_pipeline.h | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 333a076bec..8811ea1644 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -200,7 +200,7 @@ AudioPipelineState AudioPipeline::process_state() { if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) { this->delete_tasks_(); if (this->hard_stop_) { - // Stop command was sent, so immediately end of the playback + // Stop command was sent, so immediately end the playback this->speaker_->stop(); this->hard_stop_ = false; } else { @@ -210,13 +210,25 @@ AudioPipelineState AudioPipeline::process_state() { } } this->is_playing_ = false; - return AudioPipelineState::STOPPED; + if (!this->speaker_->is_running()) { + return AudioPipelineState::STOPPED; + } else { + this->is_finishing_ = true; + } } if (this->pause_state_) { return AudioPipelineState::PAUSED; } + if (this->is_finishing_) { + if (!this->speaker_->is_running()) { + this->is_finishing_ = false; + } else { + return AudioPipelineState::PLAYING; + } + } + if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) { // No tasks are running, so the pipeline is stopped. xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index 722d9cbb2a..98f43fda6e 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -114,6 +114,7 @@ class AudioPipeline { bool hard_stop_{false}; bool is_playing_{false}; + bool is_finishing_{false}; bool pause_state_{false}; bool task_stack_in_psram_; From 84607c1255a40935a843db511a126c42a46e1644 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 18 Jul 2025 20:24:55 +0100 Subject: [PATCH 013/125] [voice_assistant] Use media player callbacks to track TTS response status (#9670) --- .../voice_assistant/voice_assistant.cpp | 72 +++++++++++++------ .../voice_assistant/voice_assistant.h | 13 +++- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 9cf7d10936..647bbc7653 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -35,6 +35,27 @@ void VoiceAssistant::setup() { temp_ring_buffer->write((void *) data.data(), data.size()); } }); + +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->media_player_->add_on_state_callback([this]() { + switch (this->media_player_->state) { + case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING: + if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) { + // State changed to announcing after receiving the url + this->media_player_response_state_ = MediaPlayerResponseState::PLAYING; + } + break; + default: + if (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING) { + // No longer announcing the TTS response + this->media_player_response_state_ = MediaPlayerResponseState::FINISHED; + } + break; + } + }); + } +#endif } float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } @@ -223,6 +244,13 @@ void VoiceAssistant::loop() { msg.wake_word_phrase = this->wake_word_; this->wake_word_ = ""; + // Reset media player state tracking +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->media_player_response_state_ = MediaPlayerResponseState::IDLE; + } +#endif + if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) { ESP_LOGW(TAG, "Could not request start"); this->error_trigger_->trigger("not-connected", "Could not request start"); @@ -314,17 +342,10 @@ void VoiceAssistant::loop() { #endif #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { - playing = (this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING); + playing = (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING); - if (playing && this->media_player_wait_for_announcement_start_) { - // Announcement has started playing, wait for it to finish - this->media_player_wait_for_announcement_start_ = false; - this->media_player_wait_for_announcement_end_ = true; - } - - if (!playing && this->media_player_wait_for_announcement_end_) { - // Announcement has finished playing - this->media_player_wait_for_announcement_end_ = false; + if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) { + this->media_player_response_state_ = MediaPlayerResponseState::IDLE; this->cancel_timeout("playing"); ESP_LOGD(TAG, "Announcement finished playing"); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); @@ -555,7 +576,7 @@ void VoiceAssistant::request_stop() { break; case State::AWAITING_RESPONSE: this->signal_stop_(); - // Fallthrough intended to stop a streaming TTS announcement that has potentially started + break; case State::STREAMING_RESPONSE: #ifdef USE_MEDIA_PLAYER // Stop any ongoing media player announcement @@ -565,6 +586,10 @@ void VoiceAssistant::request_stop() { .set_announcement(true) .perform(); } + if (this->started_streaming_tts_) { + // Haven't reached the TTS_END stage, so send the stop signal to HA. + this->signal_stop_(); + } #endif break; case State::RESPONSE_FINISHED: @@ -648,13 +673,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (this->media_player_ != nullptr) { for (const auto &arg : msg.data) { if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { + this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT; + this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform(); - this->media_player_wait_for_announcement_start_ = true; - this->media_player_wait_for_announcement_end_ = false; this->started_streaming_tts_ = true; + this->start_playback_timeout_(); + tts_url_for_trigger = this->tts_response_url_; this->tts_response_url_.clear(); // Reset streaming URL + this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE); } } } @@ -713,18 +741,22 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { + this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT; + this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); - this->media_player_wait_for_announcement_start_ = true; - this->media_player_wait_for_announcement_end_ = false; - // Start the playback timeout, as the media player state isn't immediately updated this->start_playback_timeout_(); } + this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage #endif this->tts_end_trigger_->trigger(url); }); State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; - this->set_state_(new_state, new_state); + if (new_state != this->state_) { + // Don't needlessly change the state. The intent progress stage may have already changed the state to streaming + // response. + this->set_state_(new_state, new_state); + } break; } case api::enums::VOICE_ASSISTANT_RUN_END: { @@ -875,6 +907,9 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { this->tts_start_trigger_->trigger(msg.text); + + this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT; + if (!msg.preannounce_media_id.empty()) { this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform(); } @@ -886,9 +921,6 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) .perform(); this->continue_conversation_ = msg.start_conversation; - this->media_player_wait_for_announcement_start_ = true; - this->media_player_wait_for_announcement_end_ = false; - // Start the playback timeout, as the media player state isn't immediately updated this->start_playback_timeout_(); if (this->continuous_) { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 2424ea6052..95f77dbf09 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -90,6 +90,15 @@ struct Configuration { uint32_t max_active_wake_words; }; +#ifdef USE_MEDIA_PLAYER +enum class MediaPlayerResponseState { + IDLE, + URL_SENT, + PLAYING, + FINISHED, +}; +#endif + class VoiceAssistant : public Component { public: VoiceAssistant(); @@ -272,8 +281,8 @@ class VoiceAssistant : public Component { media_player::MediaPlayer *media_player_{nullptr}; std::string tts_response_url_{""}; bool started_streaming_tts_{false}; - bool media_player_wait_for_announcement_start_{false}; - bool media_player_wait_for_announcement_end_{false}; + + MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE}; #endif bool local_output_{false}; From 8a45e877bbf4fbc62c669a3423ec371386d5afbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 11:32:20 -1000 Subject: [PATCH 014/125] [gpio] Disable interrupt mode by default for LibreTiny platforms (#9687) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/gpio/binary_sensor/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 867a8efe49..59f54520fa 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -29,7 +29,21 @@ CONFIG_SCHEMA = ( .extend( { cv.Required(CONF_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean, + # Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms + # due to hardware limitations or lack of reliable interrupt support. This ensures + # stable operation on these platforms. Future maintainers should verify platform + # capabilities before changing this default behavior. + cv.SplitDefault( + CONF_USE_INTERRUPT, + bk72xx=False, + esp32=True, + esp8266=True, + host=True, + ln882x=False, + nrf52=True, + rp2040=True, + rtl87xx=False, + ): cv.boolean, cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( INTERRUPT_TYPES, upper=True ), From 576ce7ee351c448e49ba290ed7a65225bcda8f60 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 19 Jul 2025 09:56:08 +1200 Subject: [PATCH 015/125] Bump version to 2025.7.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 9c14041478..a601fcc8eb 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.7.1 +PROJECT_NUMBER = 2025.7.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 1eb6db118a..93b509f72a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.7.1" +__version__ = "2025.7.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 19a68dc6507fe4df81fec221d63129eb69232d1e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:55:22 +1200 Subject: [PATCH 016/125] Add core team as codeowner of .github folder (#9663) --- CODEOWNERS | 1 + script/build_codeowners.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 257f927fd9..cd11c7796f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,6 +9,7 @@ pyproject.toml @esphome/core esphome/*.py @esphome/core esphome/core/* @esphome/core +.github/** @esphome/core # Integrations esphome/components/a01nyub/* @MrSuicideParrot diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 523fe8ac7f..4581620095 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -31,6 +31,7 @@ BASE = """ pyproject.toml @esphome/core esphome/*.py @esphome/core esphome/core/* @esphome/core +.github/** @esphome/core # Integrations """.strip() From 65cbb0d7413d0396ac98679404a8c1b86b561fd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 19:31:53 -1000 Subject: [PATCH 017/125] [gpio] Auto-disable interrupts for shared GPIO pins in binary sensors (#9701) --- .../components/gpio/binary_sensor/__init__.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 59f54520fa..8372bc7e08 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -4,7 +4,13 @@ from esphome import pins import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN +from esphome.const import ( + CONF_ALLOW_OTHER_USES, + CONF_ID, + CONF_NAME, + CONF_NUMBER, + CONF_PIN, +) from esphome.core import CORE from .. import gpio_ns @@ -76,6 +82,18 @@ async def to_code(config): ) use_interrupt = False + # Check if pin is shared with other components (allow_other_uses) + # When a pin is shared, interrupts can interfere with other components + # (e.g., duty_cycle sensor) that need to monitor the pin's state changes + if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False): + _LOGGER.info( + "GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. " + "The sensor will use polling mode for compatibility with other pin uses.", + config.get(CONF_NAME, config[CONF_ID]), + config[CONF_PIN][CONF_NUMBER], + ) + use_interrupt = False + cg.add(var.set_use_interrupt(use_interrupt)) if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) From 89b9bddf1b23888f46f713714d562c149744d1b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 22:55:21 -1000 Subject: [PATCH 018/125] [CI] Fix clang-tidy not running when platformio.ini changes (#9678) --- script/determine-jobs.py | 10 +++++++++ tests/script/test_determine_jobs.py | 33 +++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index fc5c397c65..e26bc29c2f 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -137,6 +137,10 @@ def should_run_clang_tidy(branch: str | None = None) -> bool: - This ensures all C++ code is checked, including tests, examples, etc. - Examples: esphome/core/component.cpp, tests/custom/my_component.h + 3. The .clang-tidy.hash file itself changed + - This indicates the configuration has been updated and clang-tidy should run + - Ensures that PRs updating the clang-tidy configuration are properly validated + If the hash check fails for any reason, clang-tidy runs as a safety measure to ensure code quality is maintained. @@ -160,6 +164,12 @@ def should_run_clang_tidy(branch: str | None = None) -> bool: # If hash check fails, run clang-tidy to be safe return True + # Check if .clang-tidy.hash file itself was changed + # This handles the case where the hash was properly updated in the PR + files = changed_files(branch) + if ".clang-tidy.hash" in files: + return True + return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 4aaaadd80a..84be7344c3 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -6,7 +6,7 @@ import json import os import subprocess import sys -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch import pytest @@ -262,6 +262,8 @@ def test_should_run_integration_tests_component_dependency() -> None: (0, [], True), # Hash changed - need full scan (1, ["esphome/core.cpp"], True), # C++ file changed (1, ["README.md"], False), # No C++ files changed + (1, [".clang-tidy.hash"], True), # Hash file itself changed + (1, ["platformio.ini", ".clang-tidy.hash"], True), # Config + hash changed ], ) def test_should_run_clang_tidy( @@ -277,11 +279,26 @@ def test_should_run_clang_tidy( result = determine_jobs.should_run_clang_tidy() assert result == expected_result - # Test with hash check failing (exception) - if check_returncode != 0: - with patch("subprocess.run", side_effect=Exception("Failed")): - result = determine_jobs.should_run_clang_tidy() - assert result is True # Fail safe - run clang-tidy + +def test_should_run_clang_tidy_hash_check_exception() -> None: + """Test should_run_clang_tidy when hash check fails with exception.""" + # When hash check fails, clang-tidy should run as a safety measure + with ( + patch.object(determine_jobs, "changed_files", return_value=["README.md"]), + patch("subprocess.run", side_effect=Exception("Hash check failed")), + ): + result = determine_jobs.should_run_clang_tidy() + assert result is True # Fail safe - run clang-tidy + + # Even with C++ files, exception should trigger clang-tidy + with ( + patch.object( + determine_jobs, "changed_files", return_value=["esphome/core.cpp"] + ), + patch("subprocess.run", side_effect=Exception("Hash check failed")), + ): + result = determine_jobs.should_run_clang_tidy() + assert result is True def test_should_run_clang_tidy_with_branch() -> None: @@ -291,7 +308,9 @@ def test_should_run_clang_tidy_with_branch() -> None: with patch("subprocess.run") as mock_run: mock_run.return_value = Mock(returncode=1) # Hash unchanged determine_jobs.should_run_clang_tidy("release") - mock_changed.assert_called_once_with("release") + # Changed files is called twice now - once for hash check, once for .clang-tidy.hash check + assert mock_changed.call_count == 2 + mock_changed.assert_has_calls([call("release"), call("release")]) @pytest.mark.parametrize( From 5ed77c10ae823a8fd3c6dabd70f8793cc0f52aa1 Mon Sep 17 00:00:00 2001 From: tmpeh <41875356+tmpeh@users.noreply.github.com> Date: Sat, 19 Jul 2025 23:24:26 +0200 Subject: [PATCH 019/125] Fix format string error in ota_web_server.cpp (#9711) --- esphome/components/web_server/ota/ota_web_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 966c1c1024..7211f707e9 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -76,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); } else { - ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); + ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_); } #ifdef USE_OTA_STATE_CALLBACK // Report progress - use call_deferred since we're in web server task @@ -171,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // Finalize if (final) { - ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len, + ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len, this->ota_read_length_, request->contentLength()); // For Arduino framework, the Update library tracks expected size from firmware header From 727e8ca37673559f484a9fb038999578ce39acad Mon Sep 17 00:00:00 2001 From: JonasB2497 <45214989+JonasB2497@users.noreply.github.com> Date: Sun, 20 Jul 2025 00:29:02 +0200 Subject: [PATCH 020/125] [sdl][mipi_spi] Respect clipping when drawing (#9722) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/mipi_spi/mipi_spi.h | 2 ++ esphome/components/sdl/sdl_esphome.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index cdba5a3235..00b861f71b 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -534,6 +534,8 @@ class MipiSpiBuffer : public MipiSpiget_clipping().inside(x, y)) + return; rotate_coordinates_(x, y); if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_) return; diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index e55bff58fe..5ad18f6311 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t * } void Sdl::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); From 5b3d61b4a6832a516955181c3da8a6a9158e805d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Jul 2025 17:41:00 -1000 Subject: [PATCH 021/125] [api] Fix missing ifdef guards for field_ifdef fields in protobuf base classes (#9693) --- esphome/components/api/api_connection.h | 4 +++- esphome/components/api/api_pb2.h | 8 +++++++ script/api_protobuf/api_protobuf.py | 31 +++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3873c7fcac..9ed18c24dc 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -301,8 +301,10 @@ class APIConnection : public APIServerConnection { if (entity->has_own_name()) msg.name = entity->get_name(); - // Set common EntityBase properties + // Set common EntityBase properties +#ifdef USE_ENTITY_ICON msg.icon = entity->get_icon(); +#endif msg.disabled_by_default = entity->is_disabled_by_default(); msg.entity_category = static_cast(entity->get_entity_category()); #ifdef USE_DEVICES diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 39f00b4adc..95db58aae9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -292,9 +292,13 @@ class InfoResponseProtoMessage : public ProtoMessage { uint32_t key{0}; std::string name{}; bool disabled_by_default{false}; +#ifdef USE_ENTITY_ICON std::string icon{}; +#endif enums::EntityCategory entity_category{}; +#ifdef USE_DEVICES uint32_t device_id{0}; +#endif protected: }; @@ -303,7 +307,9 @@ class StateResponseProtoMessage : public ProtoMessage { public: ~StateResponseProtoMessage() override = default; uint32_t key{0}; +#ifdef USE_DEVICES uint32_t device_id{0}; +#endif protected: }; @@ -312,7 +318,9 @@ class CommandProtoMessage : public ProtoDecodableMessage { public: ~CommandProtoMessage() override = default; uint32_t key{0}; +#ifdef USE_DEVICES uint32_t device_id{0}; +#endif protected: }; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4df7692167..bb0e01d171 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1491,6 +1491,28 @@ def find_common_fields( return common_fields +def get_common_field_ifdef( + field_name: str, messages: list[descriptor.DescriptorProto] +) -> str | None: + """Get the field_ifdef option if it's consistent across all messages. + + Args: + field_name: Name of the field to check + messages: List of messages that contain this field + + Returns: + The field_ifdef string if all messages have the same value, None otherwise + """ + field_ifdefs = { + get_field_opt(field, pb.field_ifdef) + for msg in messages + if (field := next((f for f in msg.field if f.name == field_name), None)) + } + + # Return the ifdef only if all messages agree on the same value + return field_ifdefs.pop() if len(field_ifdefs) == 1 else None + + def build_base_class( base_class_name: str, common_fields: list[descriptor.FieldDescriptorProto], @@ -1506,9 +1528,14 @@ def build_base_class( for field in common_fields: ti = create_field_type_info(field) + # Get field_ifdef if it's consistent across all messages + field_ifdef = get_common_field_ifdef(field.name, messages) + # Only add field declarations, not encode/decode logic - protected_content.extend(ti.protected_content) - public_content.extend(ti.public_content) + if ti.protected_content: + protected_content.extend(wrap_with_ifdef(ti.protected_content, field_ifdef)) + if ti.public_content: + public_content.extend(wrap_with_ifdef(ti.public_content, field_ifdef)) # Determine if any message using this base class needs decoding needs_decode = any( From 1e35c07327ca5cfc8e02f3de6444d599ac878677 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 07:37:11 -1000 Subject: [PATCH 022/125] Bump aioesphomeapi from 37.0.1 to 37.0.2 (#9738) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4d94ce5557..6cc821e74c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==37.0.1 +aioesphomeapi==37.0.2 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import From 7d30d1e987c600e25331d27323b4b83fdf09cbc2 Mon Sep 17 00:00:00 2001 From: DT-art1 <81360462+DT-art1@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:07:56 +0200 Subject: [PATCH 023/125] [const] Move CONF_FLIP_X and CONF_FLIP_Y to ``const.py`` (#9741) --- esphome/components/max7219digit/display.py | 2 +- esphome/components/ssd1306_base/__init__.py | 5 ++--- esphome/const.py | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index f195078c1a..fef121ff10 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import display, spi import esphome.config_validation as cv from esphome.const import ( + CONF_FLIP_X, CONF_ID, CONF_INTENSITY, CONF_LAMBDA, @@ -14,7 +15,6 @@ CODEOWNERS = ["@rspaargaren"] DEPENDENCIES = ["spi"] CONF_ROTATE_CHIP = "rotate_chip" -CONF_FLIP_X = "flip_x" CONF_SCROLL_SPEED = "scroll_speed" CONF_SCROLL_DWELL = "scroll_dwell" CONF_SCROLL_DELAY = "scroll_delay" diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index ab2c7a5496..6633b24607 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -6,6 +6,8 @@ from esphome.const import ( CONF_BRIGHTNESS, CONF_CONTRAST, CONF_EXTERNAL_VCC, + CONF_FLIP_X, + CONF_FLIP_Y, CONF_INVERT, CONF_LAMBDA, CONF_MODEL, @@ -18,9 +20,6 @@ ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base") SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer) SSD1306Model = ssd1306_base_ns.enum("SSD1306Model") -CONF_FLIP_X = "flip_x" -CONF_FLIP_Y = "flip_y" - MODELS = { "SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32, "SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64, diff --git a/esphome/const.py b/esphome/const.py index 39578a1fcf..7da19a8c1b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -375,6 +375,8 @@ CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" +CONF_FLIP_X = "flip_x" +CONF_FLIP_Y = "flip_y" CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FONT = "font" From 6e31fb181ee6bbe4c285332c1d31714547eb644c Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 20 Jul 2025 23:57:52 +0200 Subject: [PATCH 024/125] core/scheduler: Make millis_64_ rollover monotonic on SMP (#9716) Co-authored-by: J. Nick Koston --- esphome/components/esp32/__init__.py | 2 + esphome/components/esp8266/__init__.py | 2 + esphome/components/host/__init__.py | 2 + esphome/components/libretiny/__init__.py | 2 + esphome/components/rp2040/__init__.py | 2 + esphome/const.py | 8 + esphome/core/defines.h | 3 + esphome/core/scheduler.cpp | 197 ++++++++++++++--------- esphome/core/scheduler.h | 44 +++-- 9 files changed, 175 insertions(+), 87 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index c772a3438c..6ddb579733 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -31,6 +31,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP32, + CoreModel, __version__, ) from esphome.core import CORE, HexInt, TimePeriod @@ -713,6 +714,7 @@ async def to_code(config): cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) + cg.add_define(CoreModel.MULTI_ATOMICS) cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "strict") diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 01b20bdcb1..d08d7121b7 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP8266, + CoreModel, ) from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed @@ -187,6 +188,7 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") + cg.add_define(CoreModel.SINGLE) cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index a67d73fbb7..2d77f2f7ab 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_HOST, + CoreModel, ) from esphome.core import CORE @@ -43,6 +44,7 @@ async def to_code(config): cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") + cg.add_define(CoreModel.MULTI_ATOMICS) cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "strict") diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 17d5d46ffd..7f2a0bc0a5 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -20,6 +20,7 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + CoreModel, __version__, ) from esphome.core import CORE @@ -260,6 +261,7 @@ async def component_to_code(config): cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) + cg.add_define(CoreModel.MULTI_NO_ATOMICS) # force using arduino framework cg.add_platformio_option("framework", "arduino") diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 0fa299ce5c..28c3bbd70c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_RP2040, + CoreModel, ) from esphome.core import CORE, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file @@ -171,6 +172,7 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") + cg.add_define(CoreModel.SINGLE) cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) diff --git a/esphome/const.py b/esphome/const.py index 7da19a8c1b..627b6bac18 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -35,6 +35,14 @@ class Framework(StrEnum): ZEPHYR = "zephyr" +class CoreModel(StrEnum): + """Core model identifiers for ESPHome scheduler.""" + + SINGLE = "ESPHOME_CORES_SINGLE" + MULTI_NO_ATOMICS = "ESPHOME_CORES_MULTI_NO_ATOMICS" + MULTI_ATOMICS = "ESPHOME_CORES_MULTI_ATOMICS" + + class PlatformFramework(Enum): """Combined platform-framework identifiers with tuple values.""" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7ddb3436cd..19d380bd29 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -15,6 +15,9 @@ #define ESPHOME_VARIANT "ESP32" #define ESPHOME_DEBUG_SCHEDULER +// Default threading model for static analysis (ESP32 is multi-core with atomics) +#define ESPHOME_CORES_MULTI_ATOMICS + // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 7a0c08e1f0..d6d99f82c8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -54,7 +54,7 @@ static void validate_static_string(const char *name) { ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str); } } -#endif +#endif /* ESPHOME_DEBUG_SCHEDULER */ // A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to // them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task, @@ -82,9 +82,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->callback = std::move(func); item->remove = false; -#if !defined(USE_ESP8266) && !defined(USE_RP2040) +#ifndef ESPHOME_CORES_SINGLE // Special handling for defer() (delay = 0, type = TIMEOUT) - // ESP8266 and RP2040 are excluded because they don't need thread-safe defer handling + // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution LockGuard guard{this->lock_}; @@ -92,7 +92,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type this->defer_queue_.push_back(std::move(item)); return; } -#endif +#endif /* not ESPHOME_CORES_SINGLE */ // Get fresh timestamp for new timer/interval - ensures accurate scheduling const auto now = this->millis_64_(millis()); // Fresh millis() call @@ -123,7 +123,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); } -#endif +#endif /* ESPHOME_DEBUG_SCHEDULER */ LockGuard guard{this->lock_}; // If name is provided, do atomic cancel-and-add @@ -231,7 +231,7 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { return item->next_execution_ - now_64; } void HOT Scheduler::call(uint32_t now) { -#if !defined(USE_ESP8266) && !defined(USE_RP2040) +#ifndef ESPHOME_CORES_SINGLE // Process defer queue first to guarantee FIFO execution order for deferred items. // Previously, defer() used the heap which gave undefined order for equal timestamps, // causing race conditions on multi-core systems (ESP32, BK7200). @@ -239,8 +239,7 @@ void HOT Scheduler::call(uint32_t now) { // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ // - Items execute in exact order they were deferred (FIFO guarantee) // - No deferred items exist in to_add_, so processing order doesn't affect correctness - // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach - // (ESP8266: single-core, RP2040: empty mutex implementation). + // Single-core platforms don't use this queue and fall back to the heap-based approach. // // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still // processed here. They are removed from the queue normally via pop_front() but skipped @@ -262,7 +261,7 @@ void HOT Scheduler::call(uint32_t now) { this->execute_item_(item.get(), now); } } -#endif +#endif /* not ESPHOME_CORES_SINGLE */ // Convert the fresh timestamp from main loop to 64-bit for scheduler operations const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() @@ -274,13 +273,15 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector> old_items; -#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, - this->millis_major_, this->last_millis_.load(std::memory_order_relaxed)); -#else - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, +#ifdef ESPHOME_CORES_MULTI_ATOMICS + const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); + const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, + major_dbg, last_dbg); +#else /* not ESPHOME_CORES_MULTI_ATOMICS */ + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, this->millis_major_, this->last_millis_); -#endif +#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ while (!this->empty_()) { std::unique_ptr item; { @@ -305,7 +306,7 @@ void HOT Scheduler::call(uint32_t now) { std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } } -#endif // ESPHOME_DEBUG_SCHEDULER +#endif /* ESPHOME_DEBUG_SCHEDULER */ // If we have too many items to remove if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { @@ -352,7 +353,7 @@ void HOT Scheduler::call(uint32_t now) { ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, item->next_execution_, now_64); -#endif +#endif /* ESPHOME_DEBUG_SCHEDULER */ // Warning: During callback(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers @@ -460,7 +461,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c size_t total_cancelled = 0; // Check all containers for matching items -#if !defined(USE_ESP8266) && !defined(USE_RP2040) +#ifndef ESPHOME_CORES_SINGLE // Only check defer queue for timeouts (intervals never go there) if (type == SchedulerItem::TIMEOUT) { for (auto &item : this->defer_queue_) { @@ -470,7 +471,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c } } } -#endif +#endif /* not ESPHOME_CORES_SINGLE */ // Cancel items in the main heap for (auto &item : this->items_) { @@ -495,24 +496,53 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c uint64_t Scheduler::millis_64_(uint32_t now) { // THREAD SAFETY NOTE: - // This function can be called from multiple threads simultaneously on ESP32/LibreTiny. - // On single-threaded platforms (ESP8266, RP2040), atomics are not needed. + // This function has three implementations, based on the precompiler flags + // - ESPHOME_CORES_SINGLE - Runs on single-core platforms (ESP8266, RP2040, etc.) + // - ESPHOME_CORES_MULTI_NO_ATOMICS - Runs on multi-core platforms without atomics (LibreTiny) + // - ESPHOME_CORES_MULTI_ATOMICS - Runs on multi-core platforms with atomics (ESP32, HOST, etc.) + // + // Make sure all changes are synchronized if you edit this function. // // IMPORTANT: Always pass fresh millis() values to this function. The implementation // handles out-of-order timestamps between threads, but minimizing time differences // helps maintain accuracy. // - // The implementation handles the 32-bit rollover (every 49.7 days) by: - // 1. Using a lock when detecting rollover to ensure atomic update - // 2. Restricting normal updates to forward movement within the same epoch - // This prevents race conditions at the rollover boundary without requiring - // 64-bit atomics or locking on every call. -#ifdef USE_LIBRETINY - // LibreTiny: Multi-threaded but lacks atomic operation support - // TODO: If LibreTiny ever adds atomic support, remove this entire block and - // let it fall through to the atomic-based implementation below - // We need to use a lock when near the rollover boundary to prevent races +#ifdef ESPHOME_CORES_SINGLE + // This is the single core implementation. + // + // Single-core platforms have no concurrency, so this is a simple implementation + // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. + + uint16_t major = this->millis_major_; + uint32_t last = this->last_millis_; + + // Check for rollover + if (now < last && (last - now) > HALF_MAX_UINT32) { + this->millis_major_++; + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + + // Only update if time moved forward + if (now > last) { + this->last_millis_ = now; + } + + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast(major) << 32); + +#elif defined(ESPHOME_CORES_MULTI_NO_ATOMICS) + // This is the multi core no atomics implementation. + // + // Without atomics, this implementation uses locks more aggressively: + // 1. Always locks when near the rollover boundary (within 10 seconds) + // 2. Always locks when detecting a large backwards jump + // 3. Updates without lock in normal forward progression (accepting minor races) + // This is less efficient but necessary without atomic operations. + uint16_t major = this->millis_major_; uint32_t last = this->last_millis_; // Define a safe window around the rollover point (10 seconds) @@ -531,9 +561,10 @@ uint64_t Scheduler::millis_64_(uint32_t now) { if (now < last && (last - now) > HALF_MAX_UINT32) { // True rollover detected (happens every ~49.7 days) this->millis_major_++; + major++; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif +#endif /* ESPHOME_DEBUG_SCHEDULER */ } // Update last_millis_ while holding lock this->last_millis_ = now; @@ -549,58 +580,76 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // If now <= last and we're not near rollover, don't update // This minimizes backwards time movement -#elif !defined(USE_ESP8266) && !defined(USE_RP2040) - // Multi-threaded platforms with atomic support (ESP32) - uint32_t last = this->last_millis_.load(std::memory_order_relaxed); + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast(major) << 32); - // If we might be near a rollover (large backwards jump), take the lock for the entire operation - // This ensures rollover detection and last_millis_ update are atomic together - if (now < last && (last - now) > HALF_MAX_UINT32) { - // Potential rollover - need lock for atomic rollover detection + update - LockGuard guard{this->lock_}; - // Re-read with lock held - last = this->last_millis_.load(std::memory_order_relaxed); +#elif defined(ESPHOME_CORES_MULTI_ATOMICS) + // This is the multi core with atomics implementation. + // + // Uses atomic operations with acquire/release semantics to ensure coherent + // reads of millis_major_ and last_millis_ across cores. Features: + // 1. Epoch-coherency retry loop to handle concurrent updates + // 2. Lock only taken for actual rollover detection and update + // 3. Lock-free CAS updates for normal forward time progression + // 4. Memory ordering ensures cores see consistent time values + for (;;) { + uint16_t major = this->millis_major_.load(std::memory_order_acquire); + + /* + * Acquire so that if we later decide **not** to take the lock we still + * observe a `millis_major_` value coherent with the loaded `last_millis_`. + * The acquire load ensures any later read of `millis_major_` sees its + * corresponding increment. + */ + uint32_t last = this->last_millis_.load(std::memory_order_acquire); + + // If we might be near a rollover (large backwards jump), take the lock for the entire operation + // This ensures rollover detection and last_millis_ update are atomic together if (now < last && (last - now) > HALF_MAX_UINT32) { - // True rollover detected (happens every ~49.7 days) - this->millis_major_++; + // Potential rollover - need lock for atomic rollover detection + update + LockGuard guard{this->lock_}; + // Re-read with lock held; mutex already provides ordering + last = this->last_millis_.load(std::memory_order_relaxed); + + if (now < last && (last - now) > HALF_MAX_UINT32) { + // True rollover detected (happens every ~49.7 days) + this->millis_major_.fetch_add(1, std::memory_order_relaxed); + major++; #ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif - } - // Update last_millis_ while holding lock to prevent races - this->last_millis_.store(now, std::memory_order_relaxed); - } else { - // Normal case: Try lock-free update, but only allow forward movement within same epoch - // This prevents accidentally moving backwards across a rollover boundary - while (now > last && (now - last) < HALF_MAX_UINT32) { - if (this->last_millis_.compare_exchange_weak(last, now, std::memory_order_relaxed)) { - break; + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + /* + * Update last_millis_ while holding the lock to prevent races + * Publish the new low-word *after* bumping `millis_major_` (done above) + * so readers never see a mismatched pair. + */ + this->last_millis_.store(now, std::memory_order_release); + } else { + // Normal case: Try lock-free update, but only allow forward movement within same epoch + // This prevents accidentally moving backwards across a rollover boundary + while (now > last && (now - last) < HALF_MAX_UINT32) { + if (this->last_millis_.compare_exchange_weak(last, now, + std::memory_order_release, // success + std::memory_order_relaxed)) { // failure + break; + } + // CAS failure means no data was published; relaxed is fine + // last is automatically updated by compare_exchange_weak if it fails } - // last is automatically updated by compare_exchange_weak if it fails } + uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed); + if (major_end == major) + return now + (static_cast(major) << 32); } + // Unreachable - the loop always returns when major_end == major + __builtin_unreachable(); #else - // Single-threaded platforms (ESP8266, RP2040): No atomics needed - uint32_t last = this->last_millis_; - - // Check for rollover - if (now < last && (last - now) > HALF_MAX_UINT32) { - this->millis_major_++; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#error \ + "No platform threading model defined. One of ESPHOME_CORES_SINGLE, ESPHOME_CORES_MULTI_NO_ATOMICS, or ESPHOME_CORES_MULTI_ATOMICS must be defined." #endif - } - - // Only update if time moved forward - if (now > last) { - this->last_millis_ = now; - } -#endif - - // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time - return now + (static_cast(this->millis_major_) << 32); } bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 64df2f2bb0..b539b26949 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -1,10 +1,11 @@ #pragma once +#include "esphome/core/defines.h" #include #include #include #include -#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) +#ifdef ESPHOME_CORES_MULTI_ATOMICS #include #endif @@ -204,23 +205,40 @@ class Scheduler { Mutex lock_; std::vector> items_; std::vector> to_add_; -#if !defined(USE_ESP8266) && !defined(USE_RP2040) - // ESP8266 and RP2040 don't need the defer queue because: - // ESP8266: Single-core with no preemptive multitasking - // RP2040: Currently has empty mutex implementation in ESPHome - // Both platforms save 40 bytes of RAM by excluding this +#ifndef ESPHOME_CORES_SINGLE + // Single-core platforms don't need the defer queue and save 40 bytes of RAM std::deque> defer_queue_; // FIFO queue for defer() calls -#endif -#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) - // Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates +#endif /* ESPHOME_CORES_SINGLE */ + uint32_t to_remove_{0}; + +#ifdef ESPHOME_CORES_MULTI_ATOMICS + /* + * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates + * + * MEMORY-ORDERING NOTE + * -------------------- + * `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half. + * Writers publish `last_millis_` with memory_order_release and readers use + * memory_order_acquire. This ensures that once a reader sees the new low word, + * it also observes the corresponding increment of `millis_major_`. + */ std::atomic last_millis_{0}; -#else +#else /* not ESPHOME_CORES_MULTI_ATOMICS */ // Platforms without atomic support or single-threaded platforms uint32_t last_millis_{0}; -#endif - // millis_major_ is protected by lock when incrementing +#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ + + /* + * Upper 16 bits of the 64-bit millis counter. Incremented only while holding + * `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race. + * Ordering relative to `last_millis_` is provided by its release store and the + * corresponding acquire loads. + */ +#ifdef ESPHOME_CORES_MULTI_ATOMICS + std::atomic millis_major_{0}; +#else /* not ESPHOME_CORES_MULTI_ATOMICS */ uint16_t millis_major_{0}; - uint32_t to_remove_{0}; +#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ }; } // namespace esphome From 335110d71fce3cc588d2af37b0681e7e9838c51f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 12:15:34 -1000 Subject: [PATCH 025/125] [bluetooth_proxy] Fix service discovery on disconnect and refactor connection handling (#9697) --- .../bluetooth_proxy/bluetooth_connection.cpp | 181 +++++++++++++++++- .../bluetooth_proxy/bluetooth_connection.h | 4 + .../bluetooth_proxy/bluetooth_proxy.cpp | 135 +------------ .../bluetooth_proxy/bluetooth_proxy.h | 6 +- 4 files changed, 183 insertions(+), 143 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 2bfccdb438..dae6e521bb 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -13,11 +13,180 @@ namespace bluetooth_proxy { static const char *const TAG = "bluetooth_proxy.connection"; +static std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { + esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); + return std::vector{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | + ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | + ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | + ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), + ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | + ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | + ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | + ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; +} + void BluetoothConnection::dump_config() { ESP_LOGCONFIG(TAG, "BLE Connection:"); BLEClientBase::dump_config(); } +void BluetoothConnection::loop() { + BLEClientBase::loop(); + + // Early return if no active connection or not in service discovery phase + if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) { + return; + } + + // Handle service discovery + this->send_service_for_discovery_(); +} + +void BluetoothConnection::reset_connection_(esp_err_t reason) { + // Send disconnection notification + this->proxy_->send_device_connection(this->address_, false, 0, reason); + + // Important: If we were in the middle of sending services, we do NOT send + // send_gatt_services_done() here. This ensures the client knows that + // the service discovery was interrupted and can retry. The client + // (aioesphomeapi) implements a 30-second timeout (DEFAULT_BLE_TIMEOUT) + // to detect incomplete service discovery rather than relying on us to + // tell them about a partial list. + this->set_address(0); + this->send_service_ = DONE_SENDING_SERVICES; + this->proxy_->send_connections_free(); +} + +void BluetoothConnection::send_service_for_discovery_() { + if (this->send_service_ == this->service_count_) { + this->send_service_ = DONE_SENDING_SERVICES; + this->proxy_->send_gatt_services_done(this->address_); + if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || + this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + this->release_services(); + } + return; + } + + // Early return if no API connection + auto *api_conn = this->proxy_->get_api_connection(); + if (api_conn == nullptr) { + return; + } + + // Send next service + esp_gattc_service_elem_t service_result; + uint16_t service_count = 1; + esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, + &service_result, &service_count, this->send_service_); + this->send_service_++; + + if (service_status != ESP_GATT_OK) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", this->connection_index_, + this->address_str().c_str(), this->send_service_ - 1, service_status); + return; + } + + if (service_count == 0) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", this->connection_index_, + this->address_str().c_str(), service_count); + return; + } + + api::BluetoothGATTGetServicesResponse resp; + resp.address = this->address_; + resp.services.reserve(1); // Always one service per response in this implementation + api::BluetoothGATTService service_resp; + service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); + service_resp.handle = service_result.start_handle; + + // Get the number of characteristics directly with one call + uint16_t total_char_count = 0; + esp_gatt_status_t char_count_status = + esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC, + service_result.start_handle, service_result.end_handle, 0, &total_char_count); + + if (char_count_status == ESP_GATT_OK && total_char_count > 0) { + // Only reserve if we successfully got a count + service_resp.characteristics.reserve(total_char_count); + } else if (char_count_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_, + this->address_str().c_str(), char_count_status); + } + + // Now process characteristics + uint16_t char_offset = 0; + esp_gattc_char_elem_t char_result; + while (true) { // characteristics + uint16_t char_count = 1; + esp_gatt_status_t char_status = + esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle, + service_result.end_handle, &char_result, &char_count, char_offset); + if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) { + break; + } + if (char_status != ESP_GATT_OK) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, + this->address_str().c_str(), char_status); + break; + } + if (char_count == 0) { + break; + } + + api::BluetoothGATTCharacteristic characteristic_resp; + characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); + characteristic_resp.handle = char_result.char_handle; + characteristic_resp.properties = char_result.properties; + char_offset++; + + // Get the number of descriptors directly with one call + uint16_t total_desc_count = 0; + esp_gatt_status_t desc_count_status = + esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, char_result.char_handle, + service_result.end_handle, 0, &total_desc_count); + + if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) { + // Only reserve if we successfully got a count + characteristic_resp.descriptors.reserve(total_desc_count); + } else if (desc_count_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_, + this->address_str().c_str(), char_result.char_handle, desc_count_status); + } + + // Now process descriptors + uint16_t desc_offset = 0; + esp_gattc_descr_elem_t desc_result; + while (true) { // descriptors + uint16_t desc_count = 1; + esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr( + this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset); + if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) { + break; + } + if (desc_status != ESP_GATT_OK) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_, + this->address_str().c_str(), desc_status); + break; + } + if (desc_count == 0) { + break; + } + + api::BluetoothGATTDescriptor descriptor_resp; + descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); + descriptor_resp.handle = desc_result.handle; + characteristic_resp.descriptors.push_back(std::move(descriptor_resp)); + desc_offset++; + } + service_resp.characteristics.push_back(std::move(characteristic_resp)); + } + resp.services.push_back(std::move(service_resp)); + + // Send the message (we already checked api_conn is not null at the beginning) + api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); +} + bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) @@ -25,22 +194,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga switch (event) { case ESP_GATTC_DISCONNECT_EVT: { - this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); - this->set_address(0); - this->proxy_->send_connections_free(); + this->reset_connection_(param->disconnect.reason); break; } case ESP_GATTC_CLOSE_EVT: { - this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason); - this->set_address(0); - this->proxy_->send_connections_free(); + this->reset_connection_(param->close.reason); break; } case ESP_GATTC_OPEN_EVT: { if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { - this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); - this->set_address(0); - this->proxy_->send_connections_free(); + this->reset_connection_(param->open.status); } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { this->proxy_->send_device_connection(this->address_, true, this->mtu_); this->proxy_->send_connections_free(); diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 73c034d93b..2673238fba 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -12,6 +12,7 @@ class BluetoothProxy; class BluetoothConnection : public esp32_ble_client::BLEClientBase { public: void dump_config() override; + void loop() override; bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; @@ -27,6 +28,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { protected: friend class BluetoothProxy; + void send_service_for_discovery_(); + void reset_connection_(esp_err_t reason); + // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) BluetoothProxy *proxy_; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 7d12842a24..f4b63f3a5d 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -11,19 +11,6 @@ namespace esphome { namespace bluetooth_proxy { static const char *const TAG = "bluetooth_proxy"; -static const int DONE_SENDING_SERVICES = -2; - -std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { - esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); - return std::vector{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | - ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | - ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | - ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), - ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | - ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | - ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | - ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; -} // Batch size for BLE advertisements to maximize WiFi efficiency // Each advertisement is up to 80 bytes when packaged (including protocol overhead) @@ -213,130 +200,12 @@ void BluetoothProxy::loop() { } // Flush any pending BLE advertisements that have been accumulated but not yet sent - static uint32_t last_flush_time = 0; uint32_t now = App.get_loop_component_start_time(); // Flush accumulated advertisements every 100ms - if (now - last_flush_time >= 100) { + if (now - this->last_advertisement_flush_time_ >= 100) { this->flush_pending_advertisements(); - last_flush_time = now; - } - for (auto *connection : this->connections_) { - if (connection->send_service_ == connection->service_count_) { - connection->send_service_ = DONE_SENDING_SERVICES; - this->send_gatt_services_done(connection->get_address()); - if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || - connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { - connection->release_services(); - } - } else if (connection->send_service_ >= 0) { - esp_gattc_service_elem_t service_result; - uint16_t service_count = 1; - esp_gatt_status_t service_status = - esp_ble_gattc_get_service(connection->get_gattc_if(), connection->get_conn_id(), nullptr, &service_result, - &service_count, connection->send_service_); - connection->send_service_++; - if (service_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", - connection->get_connection_index(), connection->address_str().c_str(), connection->send_service_ - 1, - service_status); - continue; - } - if (service_count == 0) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", - connection->get_connection_index(), connection->address_str().c_str(), service_count); - continue; - } - api::BluetoothGATTGetServicesResponse resp; - resp.address = connection->get_address(); - resp.services.reserve(1); // Always one service per response in this implementation - api::BluetoothGATTService service_resp; - service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); - service_resp.handle = service_result.start_handle; - uint16_t char_offset = 0; - esp_gattc_char_elem_t char_result; - // Get the number of characteristics directly with one call - uint16_t total_char_count = 0; - esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count( - connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC, - service_result.start_handle, service_result.end_handle, 0, &total_char_count); - - if (char_count_status == ESP_GATT_OK && total_char_count > 0) { - // Only reserve if we successfully got a count - service_resp.characteristics.reserve(total_char_count); - } else if (char_count_status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(), - connection->address_str().c_str(), char_count_status); - } - - // Now process characteristics - while (true) { // characteristics - uint16_t char_count = 1; - esp_gatt_status_t char_status = esp_ble_gattc_get_all_char( - connection->get_gattc_if(), connection->get_conn_id(), service_result.start_handle, - service_result.end_handle, &char_result, &char_count, char_offset); - if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) { - break; - } - if (char_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", connection->get_connection_index(), - connection->address_str().c_str(), char_status); - break; - } - if (char_count == 0) { - break; - } - api::BluetoothGATTCharacteristic characteristic_resp; - characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); - characteristic_resp.handle = char_result.char_handle; - characteristic_resp.properties = char_result.properties; - char_offset++; - - // Get the number of descriptors directly with one call - uint16_t total_desc_count = 0; - esp_gatt_status_t desc_count_status = - esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR, - char_result.char_handle, service_result.end_handle, 0, &total_desc_count); - - if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) { - // Only reserve if we successfully got a count - characteristic_resp.descriptors.reserve(total_desc_count); - } else if (desc_count_status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", - connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle, - desc_count_status); - } - - // Now process descriptors - uint16_t desc_offset = 0; - esp_gattc_descr_elem_t desc_result; - while (true) { // descriptors - uint16_t desc_count = 1; - esp_gatt_status_t desc_status = - esp_ble_gattc_get_all_descr(connection->get_gattc_if(), connection->get_conn_id(), - char_result.char_handle, &desc_result, &desc_count, desc_offset); - if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) { - break; - } - if (desc_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", connection->get_connection_index(), - connection->address_str().c_str(), desc_status); - break; - } - if (desc_count == 0) { - break; - } - api::BluetoothGATTDescriptor descriptor_resp; - descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); - descriptor_resp.handle = desc_result.handle; - characteristic_resp.descriptors.push_back(std::move(descriptor_resp)); - desc_offset++; - } - service_resp.characteristics.push_back(std::move(characteristic_resp)); - } - resp.services.push_back(std::move(service_resp)); - this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); - } + this->last_advertisement_flush_time_ = now; } } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 52f1d0f88a..9d84a9dbf2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -22,6 +22,7 @@ namespace esphome { namespace bluetooth_proxy { static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static const int DONE_SENDING_SERVICES = -2; using namespace esp32_ble_client; @@ -149,7 +150,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com std::vector advertisement_pool_; std::unique_ptr response_; - // Group 3: 1-byte types grouped together + // Group 3: 4-byte types + uint32_t last_advertisement_flush_time_{0}; + + // Group 4: 1-byte types grouped together bool active_; uint8_t advertisement_count_{0}; // 2 bytes used, 2 bytes padding From 534a1cf2e72e0efc392f571477ff1c1817c6ea43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 12:17:38 -1000 Subject: [PATCH 026/125] [esp32_ble_tracker] Batch BLE advertisement processing to reduce overhead (#9699) --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 44577afbbd..96003073d7 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -128,46 +128,53 @@ void ESP32BLETracker::loop() { uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); while (read_idx != write_idx) { - // Process one result at a time directly from ring buffer - BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; + // Calculate how many contiguous results we can process in one batch + // If write > read: process all results from read to write + // If write <= read (wraparound): process from read to end of buffer first + size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx); + // Process the batch for raw advertisements if (this->raw_advertisements_) { for (auto *listener : this->listeners_) { - listener->parse_devices(&scan_result, 1); + listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); } for (auto *client : this->clients_) { - client->parse_devices(&scan_result, 1); + client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); } } + // Process individual results for parsed advertisements if (this->parse_advertisements_) { #ifdef USE_ESP32_BLE_DEVICE - ESPBTDevice device; - device.parse_scan_rst(scan_result); + for (size_t i = 0; i < batch_size; i++) { + BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i]; + ESPBTDevice device; + device.parse_scan_rst(scan_result); - bool found = false; - for (auto *listener : this->listeners_) { - if (listener->parse_device(device)) - found = true; - } + bool found = false; + for (auto *listener : this->listeners_) { + if (listener->parse_device(device)) + found = true; + } - for (auto *client : this->clients_) { - if (client->parse_device(device)) { - found = true; - if (!connecting && client->state() == ClientState::DISCOVERED) { - promote_to_connecting = true; + for (auto *client : this->clients_) { + if (client->parse_device(device)) { + found = true; + if (!connecting && client->state() == ClientState::DISCOVERED) { + promote_to_connecting = true; + } } } - } - if (!found && !this->scan_continuous_) { - this->print_bt_device_info(device); + if (!found && !this->scan_continuous_) { + this->print_bt_device_info(device); + } } #endif // USE_ESP32_BLE_DEVICE } - // Move to next entry in ring buffer - read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; + // Update read index for entire batch + read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE; // Store with release to ensure reads complete before index update this->ring_read_index_.store(read_idx, std::memory_order_release); From e474a33abd984872273f1fd2942a34f54a2389d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 12:20:35 -1000 Subject: [PATCH 027/125] [api] Memory optimizations for API frame helper buffering (#9724) --- esphome/components/api/api_frame_helper.cpp | 78 ++++++++++----------- esphome/components/api/api_frame_helper.h | 13 ++-- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index afd64e8981..2e7956cb74 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -77,32 +77,45 @@ APIError APIFrameHelper::loop() { } // Helper method to buffer data from IOVs -void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { +void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, + uint16_t offset) { SendBuffer buffer; - buffer.data.reserve(total_write_len); + buffer.size = total_write_len - offset; + buffer.data = std::make_unique(buffer.size); + + uint16_t to_skip = offset; + uint16_t write_pos = 0; + for (int i = 0; i < iovcnt; i++) { - const uint8_t *data = reinterpret_cast(iov[i].iov_base); - buffer.data.insert(buffer.data.end(), data, data + iov[i].iov_len); + if (to_skip >= iov[i].iov_len) { + // Skip this entire segment + to_skip -= static_cast(iov[i].iov_len); + } else { + // Include this segment (partially or fully) + const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; + uint16_t len = static_cast(iov[i].iov_len) - to_skip; + std::memcpy(buffer.data.get() + write_pos, src, len); + write_pos += len; + to_skip = 0; + } } this->tx_buf_.push_back(std::move(buffer)); } // This method writes data to socket or buffers it -APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { +APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { // Returns APIError::OK if successful (or would block, but data has been buffered) // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED if (iovcnt == 0) return APIError::OK; // Nothing to do, success - uint16_t total_write_len = 0; - for (int i = 0; i < iovcnt; i++) { #ifdef HELPER_LOG_PACKETS + for (int i = 0; i < iovcnt; i++) { ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); -#endif - total_write_len += static_cast(iov[i].iov_len); } +#endif // Try to send any existing buffered data first if there is any if (!this->tx_buf_.empty()) { @@ -115,7 +128,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { // If there is still data in the buffer, we can't send, buffer // the new data and return if (!this->tx_buf_.empty()) { - this->buffer_data_from_iov_(iov, iovcnt, total_write_len); + this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); return APIError::OK; // Success, data buffered } } @@ -126,7 +139,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { if (sent == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { // Socket would block, buffer the data - this->buffer_data_from_iov_(iov, iovcnt, total_write_len); + this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); return APIError::OK; // Success, data buffered } // Socket error @@ -135,26 +148,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { return APIError::SOCKET_WRITE_FAILED; // Socket write failed } else if (static_cast(sent) < total_write_len) { // Partially sent, buffer the remaining data - SendBuffer buffer; - uint16_t to_consume = static_cast(sent); - uint16_t remaining = total_write_len - static_cast(sent); - - buffer.data.reserve(remaining); - - for (int i = 0; i < iovcnt; i++) { - if (to_consume >= iov[i].iov_len) { - // This segment was fully sent - to_consume -= static_cast(iov[i].iov_len); - } else { - // This segment was partially sent or not sent at all - const uint8_t *data = reinterpret_cast(iov[i].iov_base) + to_consume; - uint16_t len = static_cast(iov[i].iov_len) - to_consume; - buffer.data.insert(buffer.data.end(), data, data + len); - to_consume = 0; - } - } - - this->tx_buf_.push_back(std::move(buffer)); + this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast(sent)); } return APIError::OK; // Success, all data sent or buffered @@ -639,6 +633,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); + uint16_t total_write_len = 0; // We need to encrypt each packet in place for (const auto &packet : packets) { @@ -678,12 +673,13 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st buf_start[2] = static_cast(mbuf.size); // Add iovec for this encrypted packet - this->reusable_iovs_.push_back( - {buf_start, static_cast(3 + mbuf.size)}); // indicator + size + encrypted data + size_t packet_len = static_cast(3 + mbuf.size); // indicator + size + encrypted data + this->reusable_iovs_.push_back({buf_start, packet_len}); + total_write_len += packet_len; } // Send all encrypted packets in one writev call - return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size()); + return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); } APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { @@ -696,12 +692,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { iov[0].iov_base = header; iov[0].iov_len = 3; if (len == 0) { - return this->write_raw_(iov, 1); + return this->write_raw_(iov, 1, 3); // Just header } iov[1].iov_base = const_cast(data); iov[1].iov_len = len; - return this->write_raw_(iov, 2); + return this->write_raw_(iov, 2, 3 + len); // Header + data } /** Initiate the data structures for the handshake. @@ -990,7 +986,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { "Bad indicator byte"; iov[0].iov_base = (void *) msg; iov[0].iov_len = 19; - this->write_raw_(iov, 1); + this->write_raw_(iov, 1, 19); } return aerr; } @@ -1020,6 +1016,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); + uint16_t total_write_len = 0; for (const auto &packet : packets) { // Calculate varint sizes for header layout @@ -1064,12 +1061,13 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); // Add iovec for this packet (header + payload) - this->reusable_iovs_.push_back( - {buf_start + header_offset, static_cast(total_header_len + packet.payload_size)}); + size_t packet_len = static_cast(total_header_len + packet.payload_size); + this->reusable_iovs_.push_back({buf_start + header_offset, packet_len}); + total_write_len += packet_len; } // Send all packets in one writev call - return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size()); + return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); } #endif // USE_API_PLAINTEXT diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 4bcc4acd61..b5b25700a8 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -116,22 +116,23 @@ class APIFrameHelper { // Buffer containing data to be sent struct SendBuffer { - std::vector data; - uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage) + std::unique_ptr data; + uint16_t size{0}; // Total size of the buffer + uint16_t offset{0}; // Current offset within the buffer // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes - uint16_t remaining() const { return static_cast(data.size()) - offset; } - const uint8_t *current_data() const { return data.data() + offset; } + uint16_t remaining() const { return size - offset; } + const uint8_t *current_data() const { return data.get() + offset; } }; // Common implementation for writing raw data to socket - APIError write_raw_(const struct iovec *iov, int iovcnt); + APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); // Try to send data from the tx buffer APIError try_send_tx_buf_(); // Helper method to buffer data from IOVs - void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); + void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset); template APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); From 5511d61dba7db8e87677b57afd0a60da1881f5cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 12:24:57 -1000 Subject: [PATCH 028/125] [api] Eliminate heap allocation in process_batch_ using stack-allocated PacketInfo array (#9703) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 27 ++++++++++++++++------- esphome/components/api/api_connection.h | 12 +++++++++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2ac3303691..3f5262a985 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1671,6 +1671,10 @@ ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { } void APIConnection::process_batch_() { + // Ensure PacketInfo remains trivially destructible for our placement new approach + static_assert(std::is_trivially_destructible::value, + "PacketInfo must remain trivially destructible with this placement-new approach"); + if (this->deferred_batch_.empty()) { this->flags_.batch_scheduled = false; return; @@ -1708,9 +1712,12 @@ void APIConnection::process_batch_() { return; } - // Pre-allocate storage for packet info - std::vector packet_info; - packet_info.reserve(num_items); + size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH); + + // Stack-allocated array for packet info + alignas(PacketInfo) char packet_info_storage[MAX_PACKETS_PER_BATCH * sizeof(PacketInfo)]; + PacketInfo *packet_info = reinterpret_cast(packet_info_storage); + size_t packet_count = 0; // Cache these values to avoid repeated virtual calls const uint8_t header_padding = this->helper_->frame_header_padding(); @@ -1742,8 +1749,8 @@ void APIConnection::process_batch_() { // The actual message data follows after the header padding uint32_t current_offset = 0; - // Process items and encode directly to buffer - for (size_t i = 0; i < this->deferred_batch_.size(); i++) { + // Process items and encode directly to buffer (up to our limit) + for (size_t i = 0; i < packets_to_process; i++) { const auto &item = this->deferred_batch_[i]; // Try to encode message // The creator will calculate overhead to determine if the message fits @@ -1757,7 +1764,11 @@ void APIConnection::process_batch_() { // Message was encoded successfully // payload_size is header_padding + actual payload size + footer_size uint16_t proto_payload_size = payload_size - header_padding - footer_size; - packet_info.emplace_back(item.message_type, current_offset, proto_payload_size); + // Use placement new to construct PacketInfo in pre-allocated stack array + // This avoids default-constructing all MAX_PACKETS_PER_BATCH elements + // Explicit destruction is not needed because PacketInfo is trivially destructible, + // as ensured by the static_assert in its definition. + new (&packet_info[packet_count++]) PacketInfo(item.message_type, current_offset, proto_payload_size); // Update tracking variables items_processed++; @@ -1783,8 +1794,8 @@ void APIConnection::process_batch_() { } // Send all collected packets - APIError err = - this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); + APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, + std::span(packet_info, packet_count)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { on_fatal_error(); ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 9ed18c24dc..cad9bac3d4 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -19,7 +19,17 @@ namespace api { // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending -static constexpr size_t MAX_INITIAL_PER_BATCH = 20; +// This was increased from 20 to 24 after removing the unique_id field from entity info messages, +// which reduced message sizes allowing more entities per batch without exceeding packet limits +static constexpr size_t MAX_INITIAL_PER_BATCH = 24; +// Maximum number of packets to process in a single batch (platform-dependent) +// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_ +// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes +#if defined(USE_ESP32) || defined(USE_HOST) +static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty +#else +static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks +#endif class APIConnection : public APIServerConnection { public: From 2540e7edb2c37b4c722634c9cfb8fe042a460593 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 12:35:53 -1000 Subject: [PATCH 029/125] [api] Remove deprecated protobuf fields to reduce flash usage (#9679) --- esphome/components/api/api.proto | 58 ++++-- esphome/components/api/api_connection.cpp | 43 +---- esphome/components/api/api_connection.h | 1 - esphome/components/api/api_pb2.cpp | 92 --------- esphome/components/api/api_pb2.h | 95 +-------- esphome/components/api/api_pb2_dump.cpp | 180 ------------------ .../bluetooth_proxy/bluetooth_proxy.cpp | 40 ---- .../bluetooth_proxy/bluetooth_proxy.h | 3 - script/api_protobuf/api_protobuf.py | 66 ++++++- 9 files changed, 115 insertions(+), 463 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b0ce21b1ce..546c498ff3 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -230,14 +230,16 @@ message DeviceInfoResponse { uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"]; - uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; + // Deprecated in API version 1.9 + uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"]; uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; string manufacturer = 12; string friendly_name = 13; - uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; + // Deprecated in API version 1.10 + uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"]; uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; string suggested_area = 16 [(field_ifdef) = "USE_AREAS"]; @@ -337,7 +339,9 @@ message ListEntitiesCoverResponse { uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } +// Deprecated in API version 1.1 enum LegacyCoverState { + option deprecated = true; LEGACY_COVER_STATE_OPEN = 0; LEGACY_COVER_STATE_CLOSED = 1; } @@ -356,7 +360,8 @@ message CoverStateResponse { fixed32 key = 1; // legacy: state has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change - LegacyCoverState legacy_state = 2; + // Deprecated in API version 1.1 + LegacyCoverState legacy_state = 2 [deprecated=true]; float position = 3; float tilt = 4; @@ -364,7 +369,9 @@ message CoverStateResponse { uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; } +// Deprecated in API version 1.1 enum LegacyCoverCommand { + option deprecated = true; LEGACY_COVER_COMMAND_OPEN = 0; LEGACY_COVER_COMMAND_CLOSE = 1; LEGACY_COVER_COMMAND_STOP = 2; @@ -380,8 +387,10 @@ message CoverCommandRequest { // legacy: command has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change - bool has_legacy_command = 2; - LegacyCoverCommand legacy_command = 3; + // Deprecated in API version 1.1 + bool has_legacy_command = 2 [deprecated=true]; + // Deprecated in API version 1.1 + LegacyCoverCommand legacy_command = 3 [deprecated=true]; bool has_position = 4; float position = 5; @@ -413,7 +422,9 @@ message ListEntitiesFanResponse { repeated string supported_preset_modes = 12; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } +// Deprecated in API version 1.6 - only used in deprecated fields enum FanSpeed { + option deprecated = true; FAN_SPEED_LOW = 0; FAN_SPEED_MEDIUM = 1; FAN_SPEED_HIGH = 2; @@ -432,7 +443,8 @@ message FanStateResponse { fixed32 key = 1; bool state = 2; bool oscillating = 3; - FanSpeed speed = 4 [deprecated = true]; + // Deprecated in API version 1.6 + FanSpeed speed = 4 [deprecated=true]; FanDirection direction = 5; int32 speed_level = 6; string preset_mode = 7; @@ -448,8 +460,10 @@ message FanCommandRequest { fixed32 key = 1; bool has_state = 2; bool state = 3; - bool has_speed = 4 [deprecated = true]; - FanSpeed speed = 5 [deprecated = true]; + // Deprecated in API version 1.6 + bool has_speed = 4 [deprecated=true]; + // Deprecated in API version 1.6 + FanSpeed speed = 5 [deprecated=true]; bool has_oscillating = 6; bool oscillating = 7; bool has_direction = 8; @@ -488,9 +502,13 @@ message ListEntitiesLightResponse { repeated ColorMode supported_color_modes = 12; // next four supports_* are for legacy clients, newer clients should use color modes + // Deprecated in API version 1.6 bool legacy_supports_brightness = 5 [deprecated=true]; + // Deprecated in API version 1.6 bool legacy_supports_rgb = 6 [deprecated=true]; + // Deprecated in API version 1.6 bool legacy_supports_white_value = 7 [deprecated=true]; + // Deprecated in API version 1.6 bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; @@ -567,7 +585,9 @@ enum SensorStateClass { STATE_CLASS_TOTAL = 3; } +// Deprecated in API version 1.5 enum SensorLastResetType { + option deprecated = true; LAST_RESET_NONE = 0; LAST_RESET_NEVER = 1; LAST_RESET_AUTO = 2; @@ -591,7 +611,8 @@ message ListEntitiesSensorResponse { string device_class = 9; SensorStateClass state_class = 10; // Last reset type removed in 2021.9.0 - SensorLastResetType legacy_last_reset_type = 11; + // Deprecated in API version 1.5 + SensorLastResetType legacy_last_reset_type = 11 [deprecated=true]; bool disabled_by_default = 12; EntityCategory entity_category = 13; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; @@ -947,7 +968,8 @@ message ListEntitiesClimateResponse { float visual_target_temperature_step = 10; // for older peer versions - in new system this // is if CLIMATE_PRESET_AWAY exists is supported_presets - bool legacy_supports_away = 11; + // Deprecated in API version 1.5 + bool legacy_supports_away = 11 [deprecated=true]; bool supports_action = 12; repeated ClimateFanMode supported_fan_modes = 13; repeated ClimateSwingMode supported_swing_modes = 14; @@ -978,7 +1000,8 @@ message ClimateStateResponse { float target_temperature_low = 5; float target_temperature_high = 6; // For older peers, equal to preset == CLIMATE_PRESET_AWAY - bool unused_legacy_away = 7; + // Deprecated in API version 1.5 + bool unused_legacy_away = 7 [deprecated=true]; ClimateAction action = 8; ClimateFanMode fan_mode = 9; ClimateSwingMode swing_mode = 10; @@ -1006,8 +1029,10 @@ message ClimateCommandRequest { bool has_target_temperature_high = 8; float target_temperature_high = 9; // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset - bool unused_has_legacy_away = 10; - bool unused_legacy_away = 11; + // Deprecated in API version 1.5 + bool unused_has_legacy_away = 10 [deprecated=true]; + // Deprecated in API version 1.5 + bool unused_legacy_away = 11 [deprecated=true]; bool has_fan_mode = 12; ClimateFanMode fan_mode = 13; bool has_swing_mode = 14; @@ -1354,12 +1379,17 @@ message SubscribeBluetoothLEAdvertisementsRequest { uint32 flags = 1; } +// Deprecated - only used by deprecated BluetoothLEAdvertisementResponse message BluetoothServiceData { + option deprecated = true; string uuid = 1; - repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7 + // Deprecated in API version 1.7 + repeated uint32 legacy_data = 2 [deprecated=true]; // Removed in api version 1.7 bytes data = 3; // Added in api version 1.7 } +// Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead message BluetoothLEAdvertisementResponse { + option deprecated = true; option (id) = 67; option (source) = SOURCE_SERVER; option (ifdef) = "USE_BLUETOOTH_PROXY"; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 3f5262a985..24a0c910b9 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -362,8 +362,6 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * auto *cover = static_cast(entity); CoverStateResponse msg; auto traits = cover->get_traits(); - msg.legacy_state = - (cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED; msg.position = cover->position; if (traits.get_supports_tilt()) msg.tilt = cover->tilt; @@ -385,19 +383,6 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c } void APIConnection::cover_command(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) - if (msg.has_legacy_command) { - switch (msg.legacy_command) { - case enums::LEGACY_COVER_COMMAND_OPEN: - call.set_command_open(); - break; - case enums::LEGACY_COVER_COMMAND_CLOSE: - call.set_command_close(); - break; - case enums::LEGACY_COVER_COMMAND_STOP: - call.set_command_stop(); - break; - } - } if (msg.has_position) call.set_position(msg.position); if (msg.has_tilt) @@ -495,14 +480,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto traits = light->get_traits(); for (auto mode : traits.get_supported_color_modes()) msg.supported_color_modes.push_back(static_cast(mode)); - msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); - msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); - msg.legacy_supports_white_value = - msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) || - traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); - msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || - traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); - if (msg.legacy_supports_color_temperature) { + if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } @@ -692,7 +671,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity(); - msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); for (auto fan_mode : traits.get_supported_fan_modes()) msg.supported_fan_modes.push_back(static_cast(fan_mode)); @@ -1113,21 +1091,6 @@ void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoo void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); } -bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { - if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { - BluetoothLEAdvertisementResponse resp = msg; - for (auto &service : resp.service_data) { - service.legacy_data.assign(service.data.begin(), service.data.end()); - service.data.clear(); - } - for (auto &manufacturer_data : resp.manufacturer_data) { - manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end()); - manufacturer_data.data.clear(); - } - return this->send_message(resp, BluetoothLEAdvertisementResponse::MESSAGE_TYPE); - } - return this->send_message(msg, BluetoothLEAdvertisementResponse::MESSAGE_TYPE); -} void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); } @@ -1499,12 +1462,10 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY - resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version(); resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); #endif #ifdef USE_VOICE_ASSISTANT - resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version(); resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index cad9bac3d4..b0fd0f59b6 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -126,7 +126,6 @@ class APIConnection : public APIServerConnection { #ifdef USE_BLUETOOTH_PROXY void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; - bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 437c9ece1d..c32a15760a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -94,17 +94,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_WEBSERVER buffer.encode_uint32(10, this->webserver_port); #endif -#ifdef USE_BLUETOOTH_PROXY - buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version); -#endif #ifdef USE_BLUETOOTH_PROXY buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); #endif buffer.encode_string(12, this->manufacturer); buffer.encode_string(13, this->friendly_name); -#ifdef USE_VOICE_ASSISTANT - buffer.encode_uint32(14, this->legacy_voice_assistant_version); -#endif #ifdef USE_VOICE_ASSISTANT buffer.encode_uint32(17, this->voice_assistant_feature_flags); #endif @@ -150,17 +144,11 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { #ifdef USE_WEBSERVER ProtoSize::add_uint32_field(total_size, 1, this->webserver_port); #endif -#ifdef USE_BLUETOOTH_PROXY - ProtoSize::add_uint32_field(total_size, 1, this->legacy_bluetooth_proxy_version); -#endif #ifdef USE_BLUETOOTH_PROXY ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags); #endif ProtoSize::add_string_field(total_size, 1, this->manufacturer); ProtoSize::add_string_field(total_size, 1, this->friendly_name); -#ifdef USE_VOICE_ASSISTANT - ProtoSize::add_uint32_field(total_size, 1, this->legacy_voice_assistant_version); -#endif #ifdef USE_VOICE_ASSISTANT ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags); #endif @@ -270,7 +258,6 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { } void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_uint32(2, static_cast(this->legacy_state)); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); buffer.encode_uint32(5, static_cast(this->current_operation)); @@ -280,7 +267,6 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { } void CoverStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_state)); ProtoSize::add_float_field(total_size, 1, this->position); ProtoSize::add_float_field(total_size, 1, this->tilt); ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation)); @@ -290,12 +276,6 @@ void CoverStateResponse::calculate_size(uint32_t &total_size) const { } bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: - this->has_legacy_command = value.as_bool(); - break; - case 3: - this->legacy_command = static_cast(value.as_uint32()); - break; case 4: this->has_position = value.as_bool(); break; @@ -379,7 +359,6 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); - buffer.encode_uint32(4, static_cast(this->speed)); buffer.encode_uint32(5, static_cast(this->direction)); buffer.encode_int32(6, this->speed_level); buffer.encode_string(7, this->preset_mode); @@ -391,7 +370,6 @@ void FanStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->oscillating); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->speed)); ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction)); ProtoSize::add_int32_field(total_size, 1, this->speed_level); ProtoSize::add_string_field(total_size, 1, this->preset_mode); @@ -407,12 +385,6 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { case 3: this->state = value.as_bool(); break; - case 4: - this->has_speed = value.as_bool(); - break; - case 5: - this->speed = static_cast(value.as_uint32()); - break; case 6: this->has_oscillating = value.as_bool(); break; @@ -473,10 +445,6 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } - buffer.encode_bool(5, this->legacy_supports_brightness); - buffer.encode_bool(6, this->legacy_supports_rgb); - buffer.encode_bool(7, this->legacy_supports_white_value); - buffer.encode_bool(8, this->legacy_supports_color_temperature); buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); for (auto &it : this->effects) { @@ -500,10 +468,6 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); } } - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_brightness); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_rgb); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_white_value); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_color_temperature); ProtoSize::add_float_field(total_size, 1, this->min_mireds); ProtoSize::add_float_field(total_size, 1, this->max_mireds); if (!this->effects.empty()) { @@ -677,7 +641,6 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->force_update); buffer.encode_string(9, this->device_class); buffer.encode_uint32(10, static_cast(this->state_class)); - buffer.encode_uint32(11, static_cast(this->legacy_last_reset_type)); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_uint32(13, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -696,7 +659,6 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->force_update); ProtoSize::add_string_field(total_size, 1, this->device_class); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state_class)); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_last_reset_type)); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -1105,7 +1067,6 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(8, this->visual_min_temperature); buffer.encode_float(9, this->visual_max_temperature); buffer.encode_float(10, this->visual_target_temperature_step); - buffer.encode_bool(11, this->legacy_supports_away); buffer.encode_bool(12, this->supports_action); for (auto &it : this->supported_fan_modes) { buffer.encode_uint32(13, static_cast(it), true); @@ -1150,7 +1111,6 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_float_field(total_size, 1, this->visual_min_temperature); ProtoSize::add_float_field(total_size, 1, this->visual_max_temperature); ProtoSize::add_float_field(total_size, 1, this->visual_target_temperature_step); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_away); ProtoSize::add_bool_field(total_size, 1, this->supports_action); if (!this->supported_fan_modes.empty()) { for (const auto &it : this->supported_fan_modes) { @@ -1198,7 +1158,6 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(4, this->target_temperature); buffer.encode_float(5, this->target_temperature_low); buffer.encode_float(6, this->target_temperature_high); - buffer.encode_bool(7, this->unused_legacy_away); buffer.encode_uint32(8, static_cast(this->action)); buffer.encode_uint32(9, static_cast(this->fan_mode)); buffer.encode_uint32(10, static_cast(this->swing_mode)); @@ -1218,7 +1177,6 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_float_field(total_size, 1, this->target_temperature); ProtoSize::add_float_field(total_size, 1, this->target_temperature_low); ProtoSize::add_float_field(total_size, 1, this->target_temperature_high); - ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away); ProtoSize::add_enum_field(total_size, 1, static_cast(this->action)); ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode)); ProtoSize::add_enum_field(total_size, 1, static_cast(this->swing_mode)); @@ -1248,12 +1206,6 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) case 8: this->has_target_temperature_high = value.as_bool(); break; - case 10: - this->unused_has_legacy_away = value.as_bool(); - break; - case 11: - this->unused_legacy_away = value.as_bool(); - break; case 12: this->has_fan_mode = value.as_bool(); break; @@ -1869,50 +1821,6 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->uuid); - for (auto &it : this->legacy_data) { - buffer.encode_uint32(2, it, true); - } - buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); -} -void BluetoothServiceData::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->uuid); - if (!this->legacy_data.empty()) { - for (const auto &it : this->legacy_data) { - ProtoSize::add_uint32_field_repeated(total_size, 1, it); - } - } - ProtoSize::add_string_field(total_size, 1, this->data); -} -void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bytes(2, reinterpret_cast(this->name.data()), this->name.size()); - buffer.encode_sint32(3, this->rssi); - for (auto &it : this->service_uuids) { - buffer.encode_string(4, it, true); - } - for (auto &it : this->service_data) { - buffer.encode_message(5, it, true); - } - for (auto &it : this->manufacturer_data) { - buffer.encode_message(6, it, true); - } - buffer.encode_uint32(7, this->address_type); -} -void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_sint32_field(total_size, 1, this->rssi); - if (!this->service_uuids.empty()) { - for (const auto &it : this->service_uuids) { - ProtoSize::add_string_field_repeated(total_size, 1, it); - } - } - ProtoSize::add_repeated_message(total_size, 1, this->service_data); - ProtoSize::add_repeated_message(total_size, 1, this->manufacturer_data); - ProtoSize::add_uint32_field(total_size, 1, this->address_type); -} void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_sint32(2, this->rssi); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 95db58aae9..bb31c51278 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -17,27 +17,13 @@ enum EntityCategory : uint32_t { ENTITY_CATEGORY_DIAGNOSTIC = 2, }; #ifdef USE_COVER -enum LegacyCoverState : uint32_t { - LEGACY_COVER_STATE_OPEN = 0, - LEGACY_COVER_STATE_CLOSED = 1, -}; enum CoverOperation : uint32_t { COVER_OPERATION_IDLE = 0, COVER_OPERATION_IS_OPENING = 1, COVER_OPERATION_IS_CLOSING = 2, }; -enum LegacyCoverCommand : uint32_t { - LEGACY_COVER_COMMAND_OPEN = 0, - LEGACY_COVER_COMMAND_CLOSE = 1, - LEGACY_COVER_COMMAND_STOP = 2, -}; #endif #ifdef USE_FAN -enum FanSpeed : uint32_t { - FAN_SPEED_LOW = 0, - FAN_SPEED_MEDIUM = 1, - FAN_SPEED_HIGH = 2, -}; enum FanDirection : uint32_t { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1, @@ -65,11 +51,6 @@ enum SensorStateClass : uint32_t { STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL = 3, }; -enum SensorLastResetType : uint32_t { - LAST_RESET_NONE = 0, - LAST_RESET_NEVER = 1, - LAST_RESET_AUTO = 2, -}; #endif enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, @@ -485,7 +466,7 @@ class DeviceInfo : public ProtoMessage { class DeviceInfoResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 219; + static constexpr uint8_t ESTIMATED_SIZE = 211; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif @@ -507,17 +488,11 @@ class DeviceInfoResponse : public ProtoMessage { #ifdef USE_WEBSERVER uint32_t webserver_port{0}; #endif -#ifdef USE_BLUETOOTH_PROXY - uint32_t legacy_bluetooth_proxy_version{0}; -#endif #ifdef USE_BLUETOOTH_PROXY uint32_t bluetooth_proxy_feature_flags{0}; #endif std::string manufacturer{}; std::string friendly_name{}; -#ifdef USE_VOICE_ASSISTANT - uint32_t legacy_voice_assistant_version{0}; -#endif #ifdef USE_VOICE_ASSISTANT uint32_t voice_assistant_feature_flags{0}; #endif @@ -646,11 +621,10 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { class CoverStateResponse : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 22; - static constexpr uint8_t ESTIMATED_SIZE = 23; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "cover_state_response"; } #endif - enums::LegacyCoverState legacy_state{}; float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; @@ -665,12 +639,10 @@ class CoverStateResponse : public StateResponseProtoMessage { class CoverCommandRequest : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 30; - static constexpr uint8_t ESTIMATED_SIZE = 29; + static constexpr uint8_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "cover_command_request"; } #endif - bool has_legacy_command{false}; - enums::LegacyCoverCommand legacy_command{}; bool has_position{false}; float position{0.0f}; bool has_tilt{false}; @@ -709,13 +681,12 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { class FanStateResponse : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 23; - static constexpr uint8_t ESTIMATED_SIZE = 30; + static constexpr uint8_t ESTIMATED_SIZE = 28; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "fan_state_response"; } #endif bool state{false}; bool oscillating{false}; - enums::FanSpeed speed{}; enums::FanDirection direction{}; int32_t speed_level{0}; std::string preset_mode{}; @@ -730,14 +701,12 @@ class FanStateResponse : public StateResponseProtoMessage { class FanCommandRequest : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 31; - static constexpr uint8_t ESTIMATED_SIZE = 42; + static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "fan_command_request"; } #endif bool has_state{false}; bool state{false}; - bool has_speed{false}; - enums::FanSpeed speed{}; bool has_oscillating{false}; bool oscillating{false}; bool has_direction{false}; @@ -760,15 +729,11 @@ class FanCommandRequest : public CommandProtoMessage { class ListEntitiesLightResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 15; - static constexpr uint8_t ESTIMATED_SIZE = 81; + static constexpr uint8_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_light_response"; } #endif std::vector supported_color_modes{}; - bool legacy_supports_brightness{false}; - bool legacy_supports_rgb{false}; - bool legacy_supports_white_value{false}; - bool legacy_supports_color_temperature{false}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; @@ -854,7 +819,7 @@ class LightCommandRequest : public CommandProtoMessage { class ListEntitiesSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 16; - static constexpr uint8_t ESTIMATED_SIZE = 68; + static constexpr uint8_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_sensor_response"; } #endif @@ -863,7 +828,6 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { bool force_update{false}; std::string device_class{}; enums::SensorStateClass state_class{}; - enums::SensorLastResetType legacy_last_reset_type{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1289,7 +1253,7 @@ class CameraImageRequest : public ProtoDecodableMessage { class ListEntitiesClimateResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 46; - static constexpr uint8_t ESTIMATED_SIZE = 147; + static constexpr uint8_t ESTIMATED_SIZE = 145; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_climate_response"; } #endif @@ -1299,7 +1263,6 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; float visual_target_temperature_step{0.0f}; - bool legacy_supports_away{false}; bool supports_action{false}; std::vector supported_fan_modes{}; std::vector supported_swing_modes{}; @@ -1322,7 +1285,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { class ClimateStateResponse : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 47; - static constexpr uint8_t ESTIMATED_SIZE = 70; + static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "climate_state_response"; } #endif @@ -1331,7 +1294,6 @@ class ClimateStateResponse : public StateResponseProtoMessage { float target_temperature{0.0f}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - bool unused_legacy_away{false}; enums::ClimateAction action{}; enums::ClimateFanMode fan_mode{}; enums::ClimateSwingMode swing_mode{}; @@ -1351,7 +1313,7 @@ class ClimateStateResponse : public StateResponseProtoMessage { class ClimateCommandRequest : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 48; - static constexpr uint8_t ESTIMATED_SIZE = 88; + static constexpr uint8_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "climate_command_request"; } #endif @@ -1363,8 +1325,6 @@ class ClimateCommandRequest : public CommandProtoMessage { float target_temperature_low{0.0f}; bool has_target_temperature_high{false}; float target_temperature_high{0.0f}; - bool unused_has_legacy_away{false}; - bool unused_legacy_away{false}; bool has_fan_mode{false}; enums::ClimateFanMode fan_mode{}; bool has_swing_mode{false}; @@ -1736,41 +1696,6 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothServiceData : public ProtoMessage { - public: - std::string uuid{}; - std::vector legacy_data{}; - std::string data{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; -#ifdef HAS_PROTO_MESSAGE_DUMP - void dump_to(std::string &out) const override; -#endif - - protected: -}; -class BluetoothLEAdvertisementResponse : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 67; - static constexpr uint8_t ESTIMATED_SIZE = 107; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_le_advertisement_response"; } -#endif - uint64_t address{0}; - std::string name{}; - int32_t rssi{0}; - std::vector service_uuids{}; - std::vector service_data{}; - std::vector manufacturer_data{}; - uint32_t address_type{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; -#ifdef HAS_PROTO_MESSAGE_DUMP - void dump_to(std::string &out) const override; -#endif - - protected: -}; class BluetoothLERawAdvertisement : public ProtoMessage { public: uint64_t address{0}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index ad5a5fdcaa..b4da15da0d 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -23,16 +23,6 @@ template<> const char *proto_enum_to_string(enums::Entity } } #ifdef USE_COVER -template<> const char *proto_enum_to_string(enums::LegacyCoverState value) { - switch (value) { - case enums::LEGACY_COVER_STATE_OPEN: - return "LEGACY_COVER_STATE_OPEN"; - case enums::LEGACY_COVER_STATE_CLOSED: - return "LEGACY_COVER_STATE_CLOSED"; - default: - return "UNKNOWN"; - } -} template<> const char *proto_enum_to_string(enums::CoverOperation value) { switch (value) { case enums::COVER_OPERATION_IDLE: @@ -45,32 +35,8 @@ template<> const char *proto_enum_to_string(enums::CoverO return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(enums::LegacyCoverCommand value) { - switch (value) { - case enums::LEGACY_COVER_COMMAND_OPEN: - return "LEGACY_COVER_COMMAND_OPEN"; - case enums::LEGACY_COVER_COMMAND_CLOSE: - return "LEGACY_COVER_COMMAND_CLOSE"; - case enums::LEGACY_COVER_COMMAND_STOP: - return "LEGACY_COVER_COMMAND_STOP"; - default: - return "UNKNOWN"; - } -} #endif #ifdef USE_FAN -template<> const char *proto_enum_to_string(enums::FanSpeed value) { - switch (value) { - case enums::FAN_SPEED_LOW: - return "FAN_SPEED_LOW"; - case enums::FAN_SPEED_MEDIUM: - return "FAN_SPEED_MEDIUM"; - case enums::FAN_SPEED_HIGH: - return "FAN_SPEED_HIGH"; - default: - return "UNKNOWN"; - } -} template<> const char *proto_enum_to_string(enums::FanDirection value) { switch (value) { case enums::FAN_DIRECTION_FORWARD: @@ -127,18 +93,6 @@ template<> const char *proto_enum_to_string(enums::Sens return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(enums::SensorLastResetType value) { - switch (value) { - case enums::LAST_RESET_NONE: - return "LAST_RESET_NONE"; - case enums::LAST_RESET_NEVER: - return "LAST_RESET_NEVER"; - case enums::LAST_RESET_AUTO: - return "LAST_RESET_AUTO"; - default: - return "UNKNOWN"; - } -} #endif template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { @@ -737,13 +691,6 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); -#endif -#ifdef USE_BLUETOOTH_PROXY - out.append(" legacy_bluetooth_proxy_version: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_bluetooth_proxy_version); - out.append(buffer); - out.append("\n"); - #endif #ifdef USE_BLUETOOTH_PROXY out.append(" bluetooth_proxy_feature_flags: "); @@ -760,13 +707,6 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("'").append(this->friendly_name).append("'"); out.append("\n"); -#ifdef USE_VOICE_ASSISTANT - out.append(" legacy_voice_assistant_version: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version); - out.append(buffer); - out.append("\n"); - -#endif #ifdef USE_VOICE_ASSISTANT out.append(" voice_assistant_feature_flags: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); @@ -961,10 +901,6 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" legacy_state: "); - out.append(proto_enum_to_string(this->legacy_state)); - out.append("\n"); - out.append(" position: "); snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); @@ -996,14 +932,6 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" has_legacy_command: "); - out.append(YESNO(this->has_legacy_command)); - out.append("\n"); - - out.append(" legacy_command: "); - out.append(proto_enum_to_string(this->legacy_command)); - out.append("\n"); - out.append(" has_position: "); out.append(YESNO(this->has_position)); out.append("\n"); @@ -1115,10 +1043,6 @@ void FanStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->oscillating)); out.append("\n"); - out.append(" speed: "); - out.append(proto_enum_to_string(this->speed)); - out.append("\n"); - out.append(" direction: "); out.append(proto_enum_to_string(this->direction)); out.append("\n"); @@ -1157,14 +1081,6 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->state)); out.append("\n"); - out.append(" has_speed: "); - out.append(YESNO(this->has_speed)); - out.append("\n"); - - out.append(" speed: "); - out.append(proto_enum_to_string(this->speed)); - out.append("\n"); - out.append(" has_oscillating: "); out.append(YESNO(this->has_oscillating)); out.append("\n"); @@ -1231,22 +1147,6 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); } - out.append(" legacy_supports_brightness: "); - out.append(YESNO(this->legacy_supports_brightness)); - out.append("\n"); - - out.append(" legacy_supports_rgb: "); - out.append(YESNO(this->legacy_supports_rgb)); - out.append("\n"); - - out.append(" legacy_supports_white_value: "); - out.append(YESNO(this->legacy_supports_white_value)); - out.append("\n"); - - out.append(" legacy_supports_color_temperature: "); - out.append(YESNO(this->legacy_supports_color_temperature)); - out.append("\n"); - out.append(" min_mireds: "); snprintf(buffer, sizeof(buffer), "%g", this->min_mireds); out.append(buffer); @@ -1537,10 +1437,6 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->state_class)); out.append("\n"); - out.append(" legacy_last_reset_type: "); - out.append(proto_enum_to_string(this->legacy_last_reset_type)); - out.append("\n"); - out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -2107,10 +2003,6 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" legacy_supports_away: "); - out.append(YESNO(this->legacy_supports_away)); - out.append("\n"); - out.append(" supports_action: "); out.append(YESNO(this->supports_action)); out.append("\n"); @@ -2223,10 +2115,6 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" unused_legacy_away: "); - out.append(YESNO(this->unused_legacy_away)); - out.append("\n"); - out.append(" action: "); out.append(proto_enum_to_string(this->action)); out.append("\n"); @@ -2313,14 +2201,6 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" unused_has_legacy_away: "); - out.append(YESNO(this->unused_has_legacy_away)); - out.append("\n"); - - out.append(" unused_legacy_away: "); - out.append(YESNO(this->unused_legacy_away)); - out.append("\n"); - out.append(" has_fan_mode: "); out.append(YESNO(this->has_fan_mode)); out.append("\n"); @@ -3053,66 +2933,6 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const out.append("\n"); out.append("}"); } -void BluetoothServiceData::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothServiceData {\n"); - out.append(" uuid: "); - out.append("'").append(this->uuid).append("'"); - out.append("\n"); - - for (const auto &it : this->legacy_data) { - out.append(" legacy_data: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, it); - out.append(buffer); - out.append("\n"); - } - - out.append(" data: "); - out.append(format_hex_pretty(this->data)); - out.append("\n"); - out.append("}"); -} -void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLEAdvertisementResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append(format_hex_pretty(this->name)); - out.append("\n"); - - out.append(" rssi: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->service_uuids) { - out.append(" service_uuids: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - for (const auto &it : this->service_data) { - out.append(" service_data: "); - it.dump_to(out); - out.append("\n"); - } - - for (const auto &it : this->manufacturer_data) { - out.append(" manufacturer_data: "); - it.dump_to(out); - out.append("\n"); - } - - out.append(" address_type: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - out.append("}"); -} void BluetoothLERawAdvertisement::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothLERawAdvertisement {\n"); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index f4b63f3a5d..8a1a2bff6a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -127,46 +127,6 @@ void BluetoothProxy::flush_pending_advertisements() { this->advertisement_count_ = 0; } -#ifdef USE_ESP32_BLE_DEVICE -void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { - api::BluetoothLEAdvertisementResponse resp; - resp.address = device.address_uint64(); - resp.address_type = device.get_address_type(); - if (!device.get_name().empty()) - resp.name = device.get_name(); - resp.rssi = device.get_rssi(); - - // Pre-allocate vectors based on known sizes - auto service_uuids = device.get_service_uuids(); - resp.service_uuids.reserve(service_uuids.size()); - for (auto &uuid : service_uuids) { - resp.service_uuids.emplace_back(uuid.to_string()); - } - - // Pre-allocate service data vector - auto service_datas = device.get_service_datas(); - resp.service_data.reserve(service_datas.size()); - for (auto &data : service_datas) { - resp.service_data.emplace_back(); - auto &service_data = resp.service_data.back(); - service_data.uuid = data.uuid.to_string(); - service_data.data.assign(data.data.begin(), data.data.end()); - } - - // Pre-allocate manufacturer data vector - auto manufacturer_datas = device.get_manufacturer_datas(); - resp.manufacturer_data.reserve(manufacturer_datas.size()); - for (auto &data : manufacturer_datas) { - resp.manufacturer_data.emplace_back(); - auto &manufacturer_data = resp.manufacturer_data.back(); - manufacturer_data.uuid = data.uuid.to_string(); - manufacturer_data.data.assign(data.data.begin(), data.data.end()); - } - - this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE); -} -#endif // USE_ESP32_BLE_DEVICE - void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 9d84a9dbf2..b3d9044a2c 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -132,9 +132,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com } protected: -#ifdef USE_ESP32_BLE_DEVICE - void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); -#endif void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); BluetoothConnection *get_connection_(uint64_t address, bool reserve); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index bb0e01d171..2612d59544 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -971,11 +971,11 @@ class RepeatedTypeInfo(TypeInfo): def build_type_usage_map( file_desc: descriptor.FileDescriptorProto, -) -> tuple[dict[str, str | None], dict[str, str | None], dict[str, int]]: +) -> tuple[dict[str, str | None], dict[str, str | None], dict[str, int], set[str]]: """Build mappings for both enums and messages to their ifdefs based on usage. Returns: - tuple: (enum_ifdef_map, message_ifdef_map, message_source_map) + tuple: (enum_ifdef_map, message_ifdef_map, message_source_map, used_messages) """ enum_ifdef_map: dict[str, str | None] = {} message_ifdef_map: dict[str, str | None] = {} @@ -988,6 +988,7 @@ def build_type_usage_map( message_usage: dict[ str, set[str] ] = {} # message_name -> set of message names that use it + used_messages: set[str] = set() # Track which messages are actually used # Build message name to ifdef mapping for quick lookup message_to_ifdef: dict[str, str | None] = { @@ -996,17 +997,26 @@ def build_type_usage_map( # Analyze field usage for message in file_desc.message_type: + # Skip deprecated messages entirely + if message.options.deprecated: + continue + for field in message.field: + # Skip deprecated fields when tracking enum usage + if field.options.deprecated: + continue + type_name = field.type_name.split(".")[-1] if field.type_name else None if not type_name: continue - # Track enum usage + # Track enum usage (only from non-deprecated fields) if field.type == 14: # TYPE_ENUM enum_usage.setdefault(type_name, set()).add(message.name) # Track message usage elif field.type == 11: # TYPE_MESSAGE message_usage.setdefault(type_name, set()).add(message.name) + used_messages.add(type_name) # Helper to get unique ifdef from a set of messages def get_unique_ifdef(message_names: set[str]) -> str | None: @@ -1069,12 +1079,18 @@ def build_type_usage_map( # Build message source map # First pass: Get explicit sources for messages with source option or id for msg in file_desc.message_type: + # Skip deprecated messages + if msg.options.deprecated: + continue + if msg.options.HasExtension(pb.source): # Explicit source option takes precedence message_source_map[msg.name] = get_opt(msg, pb.source, SOURCE_BOTH) elif msg.options.HasExtension(pb.id): # Service messages (with id) default to SOURCE_BOTH message_source_map[msg.name] = SOURCE_BOTH + # Service messages are always used + used_messages.add(msg.name) # Second pass: Determine sources for embedded messages based on their usage for msg in file_desc.message_type: @@ -1103,7 +1119,12 @@ def build_type_usage_map( # Not used by any message and no explicit source - default to encode-only message_source_map[msg.name] = SOURCE_SERVER - return enum_ifdef_map, message_ifdef_map, message_source_map + return ( + enum_ifdef_map, + message_ifdef_map, + message_source_map, + used_messages, + ) def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: @@ -1145,6 +1166,10 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: total_size = 0 for field in desc.field: + # Skip deprecated fields + if field.options.deprecated: + continue + ti = create_field_type_info(field) # Add estimated size for this field @@ -1213,6 +1238,10 @@ def build_message_type( public_content.append("#endif") for field in desc.field: + # Skip deprecated fields completely + if field.options.deprecated: + continue + ti = create_field_type_info(field) # Skip field declarations for fields that are in the base class @@ -1459,8 +1488,10 @@ def find_common_fields( if not messages: return [] - # Start with fields from the first message - first_msg_fields = {field.name: field for field in messages[0].field} + # Start with fields from the first message (excluding deprecated fields) + first_msg_fields = { + field.name: field for field in messages[0].field if not field.options.deprecated + } common_fields = [] # Check each field to see if it exists in all messages with same type @@ -1471,6 +1502,9 @@ def find_common_fields( for msg in messages[1:]: found = False for other_field in msg.field: + # Skip deprecated fields + if other_field.options.deprecated: + continue if ( other_field.name == field_name and other_field.type == field.type @@ -1599,6 +1633,10 @@ def build_service_message_type( message_source_map: dict[str, int], ) -> tuple[str, str] | None: """Builds the service message type.""" + # Skip deprecated messages + if mt.options.deprecated: + return None + snake = camel_to_snake(mt.name) id_: int | None = get_opt(mt, pb.id) if id_ is None: @@ -1700,12 +1738,18 @@ namespace api { content += "namespace enums {\n\n" # Build dynamic ifdef mappings for both enums and messages - enum_ifdef_map, message_ifdef_map, message_source_map = build_type_usage_map(file) + enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( + build_type_usage_map(file) + ) # Simple grouping of enums by ifdef current_ifdef = None for enum in file.enum_type: + # Skip deprecated enums + if enum.options.deprecated: + continue + s, c, dc = build_enum_type(enum, enum_ifdef_map) enum_ifdef = enum_ifdef_map.get(enum.name) @@ -1756,6 +1800,14 @@ namespace api { current_ifdef = None for m in mt: + # Skip deprecated messages + if m.options.deprecated: + continue + + # Skip messages that aren't used (unless they have an ID/service message) + if m.name not in used_messages and not m.options.HasExtension(pb.id): + continue + s, c, dc = build_message_type(m, base_class_fields, message_source_map) msg_ifdef = message_ifdef_map.get(m.name) From e5aed29231779abee3c5f866f7496074fe3fb5c7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 10:39:30 +1200 Subject: [PATCH 030/125] [CI] Only mention codeowners once (#9727) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../workflows/codeowner-review-request.yml | 95 +++++++++++++++---- .github/workflows/issue-codeowner-notify.yml | 45 ++++++++- 2 files changed, 117 insertions(+), 23 deletions(-) diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index ddf5698211..9a0b43a51d 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -178,6 +178,51 @@ jobs: reviewedUsers.add(review.user.login); }); + // Check for previous comments from this workflow to avoid duplicate pings + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pr_number + }); + + const previouslyPingedUsers = new Set(); + const previouslyPingedTeams = new Set(); + + // Look for comments from github-actions bot that contain codeowner pings + const workflowComments = comments.filter(comment => + comment.user.type === 'Bot' && + comment.user.login === 'github-actions[bot]' && + comment.body.includes("I've automatically requested reviews from codeowners") + ); + + // Extract previously mentioned users and teams from workflow comments + for (const comment of workflowComments) { + // Match @username patterns (not team mentions) + const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || []; + userMentions.forEach(mention => { + const username = mention.slice(1); // remove @ + previouslyPingedUsers.add(username); + }); + + // Match @org/team patterns + const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || []; + teamMentions.forEach(mention => { + const teamName = mention.split('/')[1]; + previouslyPingedTeams.add(teamName); + }); + } + + console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`); + + // Remove users who have already been pinged in previous workflow comments + previouslyPingedUsers.forEach(user => { + matchedOwners.delete(user); + }); + + previouslyPingedTeams.forEach(team => { + matchedTeams.delete(team); + }); + // Remove only users who have already submitted reviews (not just requested reviewers) reviewedUsers.forEach(reviewer => { matchedOwners.delete(reviewer); @@ -192,7 +237,7 @@ jobs: const teamsList = Array.from(matchedTeams); if (reviewersList.length === 0 && teamsList.length === 0) { - console.log('No eligible reviewers found (all may already be requested or reviewed)'); + console.log('No eligible reviewers found (all may already be requested, reviewed, or previously pinged)'); return; } @@ -227,31 +272,41 @@ jobs: console.log('All codeowners are already requested reviewers or have reviewed'); } - // Add a comment to the PR mentioning what happened (include all matched codeowners) - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); + // Only add a comment if there are new codeowners to mention (not previously pinged) + if (reviewersList.length > 0 || teamsList.length > 0) { + const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pr_number, - body: commentBody - }); + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body: commentBody + }); + console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`); + } else { + console.log('No new codeowners to mention in comment (all previously pinged)'); + } } catch (error) { if (error.status === 422) { console.log('Some reviewers may already be requested or unavailable:', error.message); - // Try to add a comment even if review request failed - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); + // Only try to add a comment if there are new codeowners to mention + if (reviewersList.length > 0 || teamsList.length > 0) { + const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); - try { - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pr_number, - body: commentBody - }); - } catch (commentError) { - console.log('Failed to add comment:', commentError.message); + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body: commentBody + }); + console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`); + } catch (commentError) { + console.log('Failed to add comment:', commentError.message); + } + } else { + console.log('No new codeowners to mention in fallback comment'); } } else { throw error; diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index 3ff9c58510..27976a7952 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -92,10 +92,49 @@ jobs: mention !== `@${issueAuthor}` ); - const allMentions = [...filteredUserOwners, ...teamOwners]; + // Check for previous comments from this workflow to avoid duplicate pings + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: issue_number + }); + + const previouslyPingedUsers = new Set(); + const previouslyPingedTeams = new Set(); + + // Look for comments from github-actions bot that contain codeowner pings for this component + const workflowComments = comments.filter(comment => + comment.user.type === 'Bot' && + comment.user.login === 'github-actions[bot]' && + comment.body.includes(`component: ${componentName}`) && + comment.body.includes("you've been identified as a codeowner") + ); + + // Extract previously mentioned users and teams from workflow comments + for (const comment of workflowComments) { + // Match @username patterns (not team mentions) + const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || []; + userMentions.forEach(mention => { + previouslyPingedUsers.add(mention); // Keep @ prefix for easy comparison + }); + + // Match @org/team patterns + const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+/g) || []; + teamMentions.forEach(mention => { + previouslyPingedTeams.add(mention); + }); + } + + console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams for component ${componentName}`); + + // Remove previously pinged users and teams + const newUserOwners = filteredUserOwners.filter(mention => !previouslyPingedUsers.has(mention)); + const newTeamOwners = teamOwners.filter(mention => !previouslyPingedTeams.has(mention)); + + const allMentions = [...newUserOwners, ...newTeamOwners]; if (allMentions.length === 0) { - console.log('No codeowners to notify (issue author is the only codeowner)'); + console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)'); return; } @@ -111,7 +150,7 @@ jobs: body: commentBody }); - console.log(`Successfully notified codeowners: ${mentionString}`); + console.log(`Successfully notified new codeowners: ${mentionString}`); } catch (error) { console.log('Failed to process codeowner notifications:', error.message); From 0aabdaa0c784fd49af537ccb281061e180c695f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 12:52:46 -1000 Subject: [PATCH 031/125] [api] Consolidate error handling and remove unused code (#9726) --- esphome/components/api/api_frame_helper.cpp | 191 +++++++++----------- esphome/components/api/api_frame_helper.h | 14 +- 2 files changed, 96 insertions(+), 109 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 2e7956cb74..602c7359a8 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -76,6 +76,16 @@ APIError APIFrameHelper::loop() { return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination } +// Common socket write error handling +APIError APIFrameHelper::handle_socket_write_error_() { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } + ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno); + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; +} + // Helper method to buffer data from IOVs void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset) { @@ -137,15 +147,13 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ ssize_t sent = this->socket_->writev(iov, iovcnt); if (sent == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { + APIError err = this->handle_socket_write_error_(); + if (err == APIError::WOULD_BLOCK) { // Socket would block, buffer the data this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); return APIError::OK; // Success, data buffered } - // Socket error - ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno); - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; // Socket write failed + return err; // Socket write failed } else if (static_cast(sent) < total_write_len) { // Partially sent, buffer the remaining data this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast(sent)); @@ -167,14 +175,7 @@ APIError APIFrameHelper::try_send_tx_buf_() { ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining()); if (sent == -1) { - if (errno != EWOULDBLOCK && errno != EAGAIN) { - // Real socket error (not just would block) - ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno); - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; // Socket write failed - } - // Socket would block, we'll try again later - return APIError::WOULD_BLOCK; + return this->handle_socket_write_error_(); } else if (sent == 0) { // Nothing sent but not an error return APIError::WOULD_BLOCK; @@ -292,6 +293,26 @@ APIError APINoiseFrameHelper::init() { state_ = State::CLIENT_HELLO; return APIError::OK; } +// Helper for handling handshake frame errors +APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { + if (aerr == APIError::BAD_INDICATOR) { + send_explicit_handshake_reject_("Bad indicator byte"); + } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { + send_explicit_handshake_reject_("Bad handshake packet len"); + } + return aerr; +} + +// Helper for handling noise library errors +APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); + return api_err; + } + return APIError::OK; +} + /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { // During handshake phase, process as many actions as possible until we can't progress @@ -299,12 +320,12 @@ APIError APINoiseFrameHelper::loop() { // WOULD_BLOCK when no more data is available to read while (state_ != State::DATA && this->socket_->ready()) { APIError err = state_action_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; - } if (err == APIError::WOULD_BLOCK) { break; } + if (err != APIError::OK) { + return err; + } } // Use base class implementation for buffer sending @@ -325,7 +346,7 @@ APIError APINoiseFrameHelper::loop() { * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. */ -APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { +APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { if (frame == nullptr) { HELPER_LOG("Bad argument for try_read_frame_"); return APIError::BAD_ARG; @@ -388,7 +409,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { #ifdef HELPER_LOG_PACKETS ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); #endif - frame->msg = std::move(rx_buf_); + *frame = std::move(rx_buf_); // consume msg rx_buf_ = {}; rx_buf_len_ = 0; @@ -414,24 +435,17 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::CLIENT_HELLO) { // waiting for client hello - ParsedFrame frame; + std::vector frame; aerr = try_read_frame_(&frame); - if (aerr == APIError::BAD_INDICATOR) { - send_explicit_handshake_reject_("Bad indicator byte"); - return aerr; + if (aerr != APIError::OK) { + return handle_handshake_frame_error_(aerr); } - if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { - send_explicit_handshake_reject_("Bad handshake packet len"); - return aerr; - } - if (aerr != APIError::OK) - return aerr; // ignore contents, may be used in future for flags // Reserve space for: existing prologue + 2 size bytes + frame data - prologue_.reserve(prologue_.size() + 2 + frame.msg.size()); - prologue_.push_back((uint8_t) (frame.msg.size() >> 8)); - prologue_.push_back((uint8_t) frame.msg.size()); - prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); + prologue_.reserve(prologue_.size() + 2 + frame.size()); + prologue_.push_back((uint8_t) (frame.size() >> 8)); + prologue_.push_back((uint8_t) frame.size()); + prologue_.insert(prologue_.end(), frame.begin(), frame.end()); state_ = State::SERVER_HELLO; } @@ -469,41 +483,29 @@ APIError APINoiseFrameHelper::state_action_() { int action = noise_handshakestate_get_action(handshake_); if (action == NOISE_ACTION_READ_MESSAGE) { // waiting for handshake msg - ParsedFrame frame; + std::vector frame; aerr = try_read_frame_(&frame); - if (aerr == APIError::BAD_INDICATOR) { - send_explicit_handshake_reject_("Bad indicator byte"); - return aerr; + if (aerr != APIError::OK) { + return handle_handshake_frame_error_(aerr); } - if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { - send_explicit_handshake_reject_("Bad handshake packet len"); - return aerr; - } - if (aerr != APIError::OK) - return aerr; - if (frame.msg.empty()) { + if (frame.empty()) { send_explicit_handshake_reject_("Empty handshake message"); return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } else if (frame.msg[0] != 0x00) { - HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); + } else if (frame[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", frame[0]); send_explicit_handshake_reject_("Bad handshake error byte"); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); + noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1); err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); - if (err == NOISE_ERROR_MAC_FAILURE) { - send_explicit_handshake_reject_("Handshake MAC failure"); - } else { - send_explicit_handshake_reject_("Handshake error"); - } - return APIError::HANDSHAKESTATE_READ_FAILED; + // Special handling for MAC failure + send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); + return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); } aerr = check_handshake_finished_(); @@ -516,11 +518,10 @@ APIError APINoiseFrameHelper::state_action_() { noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_WRITE_FAILED; - } + APIError aerr_write = + handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); + if (aerr_write != APIError::OK) + return aerr_write; buffer[0] = 0x00; // success aerr = write_frame_(buffer, mbuf.size + 1); @@ -569,23 +570,21 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::WOULD_BLOCK; } - ParsedFrame frame; + std::vector frame; aerr = try_read_frame_(&frame); if (aerr != APIError::OK) return aerr; NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size()); + noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str()); - return APIError::CIPHERSTATE_DECRYPT_FAILED; - } + APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); + if (decrypt_err != APIError::OK) + return decrypt_err; uint16_t msg_size = mbuf.size; - uint8_t *msg_data = frame.msg.data(); + uint8_t *msg_data = frame.data(); if (msg_size < 4) { state_ = State::FAILED; HELPER_LOG("Bad data packet: size %d too short", msg_size); @@ -600,7 +599,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_PACKET; } - buffer->container = std::move(frame.msg); + buffer->container = std::move(frame); buffer->data_offset = 4; buffer->data_len = data_len; buffer->type = type; @@ -662,11 +661,9 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st 4 + packet.payload_size + frame_footer_size_); int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str()); - return APIError::CIPHERSTATE_ENCRYPT_FAILED; - } + APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); + if (aerr != APIError::OK) + return aerr; // Fill in the encrypted size buf_start[1] = static_cast(mbuf.size >> 8); @@ -718,35 +715,27 @@ APIError APINoiseFrameHelper::init_handshake_() { nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SETUP_FAILED; - } + APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; const auto &psk = ctx_->get_psk(); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SETUP_FAILED; - } + aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SETUP_FAILED; - } + aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now prologue_ = {}; err = noise_handshakestate_start(handshake_); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SETUP_FAILED; - } + aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; return APIError::OK; } @@ -762,11 +751,9 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { return APIError::HANDSHAKESTATE_BAD_STATE; } int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SPLIT_FAILED; - } + APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); + if (aerr != APIError::OK) + return aerr; frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); @@ -833,7 +820,7 @@ APIError APIPlaintextFrameHelper::loop() { * * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. */ -APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { +APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { if (frame == nullptr) { HELPER_LOG("Bad argument for try_read_frame_"); return APIError::BAD_ARG; @@ -951,7 +938,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { #ifdef HELPER_LOG_PACKETS ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); #endif - frame->msg = std::move(rx_buf_); + *frame = std::move(rx_buf_); // consume msg rx_buf_ = {}; rx_buf_len_ = 0; @@ -966,7 +953,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::WOULD_BLOCK; } - ParsedFrame frame; + std::vector frame; aerr = try_read_frame_(&frame); if (aerr != APIError::OK) { if (aerr == APIError::BAD_INDICATOR) { @@ -991,7 +978,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return aerr; } - buffer->container = std::move(frame.msg); + buffer->container = std::move(frame); buffer->data_offset = 0; buffer->data_len = rx_header_parsed_len_; buffer->type = rx_header_parsed_type_; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index b5b25700a8..421518188c 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -109,11 +109,6 @@ class APIFrameHelper { bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } protected: - // Struct for holding parsed frame data - struct ParsedFrame { - std::vector msg; - }; - // Buffer containing data to be sent struct SendBuffer { std::unique_ptr data; @@ -133,6 +128,9 @@ class APIFrameHelper { // Helper method to buffer data from IOVs void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset); + + // Common socket write error handling + APIError handle_socket_write_error_(); template APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); @@ -205,11 +203,13 @@ class APINoiseFrameHelper : public APIFrameHelper { protected: APIError state_action_(); - APIError try_read_frame_(ParsedFrame *frame); + APIError try_read_frame_(std::vector *frame); APIError write_frame_(const uint8_t *data, uint16_t len); APIError init_handshake_(); APIError check_handshake_finished_(); void send_explicit_handshake_reject_(const std::string &reason); + APIError handle_handshake_frame_error_(APIError aerr); + APIError handle_noise_error_(int err, const char *func_name, APIError api_err); // Pointers first (4 bytes each) NoiseHandshakeState *handshake_{nullptr}; @@ -257,7 +257,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper { uint8_t frame_footer_size() override { return frame_footer_size_; } protected: - APIError try_read_frame_(ParsedFrame *frame); + APIError try_read_frame_(std::vector *frame); // Group 2-byte aligned types uint16_t rx_header_parsed_type_ = 0; From acca629c5cc151b2f60fb8dfe7f82004fb1890c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 13:05:53 -1000 Subject: [PATCH 032/125] [api] Fix missing ifdef guards for AreaInfo and DeviceInfo messages (#9730) --- esphome/components/api/api_pb2.cpp | 4 ++++ esphome/components/api/api_pb2.h | 4 ++++ esphome/components/api/api_pb2_dump.cpp | 4 ++++ script/api_protobuf/api_protobuf.py | 21 ++++++++++++++++++--- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c32a15760a..4cf4b63269 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -57,6 +57,7 @@ void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool void ConnectResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->invalid_password); } +#ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); @@ -65,6 +66,8 @@ void AreaInfo::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->area_id); ProtoSize::add_string_field(total_size, 1, this->name); } +#endif +#ifdef USE_DEVICES void DeviceInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->device_id); buffer.encode_string(2, this->name); @@ -75,6 +78,7 @@ void DeviceInfo::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_uint32_field(total_size, 1, this->area_id); } +#endif void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->uses_password); buffer.encode_string(2, this->name); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index bb31c51278..e241451ec8 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -438,6 +438,7 @@ class DeviceInfoRequest : public ProtoDecodableMessage { protected: }; +#ifdef USE_AREAS class AreaInfo : public ProtoMessage { public: uint32_t area_id{0}; @@ -450,6 +451,8 @@ class AreaInfo : public ProtoMessage { protected: }; +#endif +#ifdef USE_DEVICES class DeviceInfo : public ProtoMessage { public: uint32_t device_id{0}; @@ -463,6 +466,7 @@ class DeviceInfo : public ProtoMessage { protected: }; +#endif class DeviceInfoResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index b4da15da0d..bda5ec5764 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -609,6 +609,7 @@ void DisconnectResponse::dump_to(std::string &out) const { out.append("Disconnec void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } +#ifdef USE_AREAS void AreaInfo::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("AreaInfo {\n"); @@ -622,6 +623,8 @@ void AreaInfo::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif +#ifdef USE_DEVICES void DeviceInfo::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DeviceInfo {\n"); @@ -640,6 +643,7 @@ void DeviceInfo::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void DeviceInfoResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DeviceInfoResponse {\n"); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2612d59544..ad6c3c3ed2 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -996,6 +996,11 @@ def build_type_usage_map( } # Analyze field usage + # Also track field_ifdef for message types + message_field_ifdefs: dict[ + str, set[str | None] + ] = {} # message_name -> set of field_ifdefs that use it + for message in file_desc.message_type: # Skip deprecated messages entirely if message.options.deprecated: @@ -1016,6 +1021,9 @@ def build_type_usage_map( # Track message usage elif field.type == 11: # TYPE_MESSAGE message_usage.setdefault(type_name, set()).add(message.name) + # Also track the field_ifdef if present + field_ifdef = get_field_opt(field, pb.field_ifdef) + message_field_ifdefs.setdefault(type_name, set()).add(field_ifdef) used_messages.add(type_name) # Helper to get unique ifdef from a set of messages @@ -1042,9 +1050,16 @@ def build_type_usage_map( message_ifdef_map[message.name] = explicit_ifdef elif message.name in message_usage: # Inherit ifdef if all parent messages have the same one - message_ifdef_map[message.name] = get_unique_ifdef( - message_usage[message.name] - ) + if parent_ifdef := get_unique_ifdef(message_usage[message.name]): + message_ifdef_map[message.name] = parent_ifdef + elif message.name in message_field_ifdefs: + # If no parent message ifdef, check if all fields using this message have the same field_ifdef + field_ifdefs = message_field_ifdefs[message.name] - {None} + message_ifdef_map[message.name] = ( + field_ifdefs.pop() if len(field_ifdefs) == 1 else None + ) + else: + message_ifdef_map[message.name] = None else: message_ifdef_map[message.name] = None From ecd310dae14e87c41b37fbdb67c03c55eb3e61eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 13:11:30 -1000 Subject: [PATCH 033/125] [core] Refactor scheduler to eliminate hidden side effects in empty_ (#9743) --- esphome/core/scheduler.cpp | 24 ++++++++++++++++-------- esphome/core/scheduler.h | 19 +++++++------------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d6d99f82c8..9e66fd3432 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -219,10 +219,13 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) optional HOT Scheduler::next_schedule_in(uint32_t now) { // IMPORTANT: This method should only be called from the main thread (loop task). - // It calls empty_() and accesses items_[0] without holding a lock, which is only + // It performs cleanup and accesses items_[0] without holding a lock, which is only // safe when called from the main thread. Other threads must not call this method. - if (this->empty_()) + + // If no items, return empty optional + if (this->cleanup_() == 0) return {}; + auto &item = this->items_[0]; // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller @@ -282,7 +285,9 @@ void HOT Scheduler::call(uint32_t now) { ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, this->millis_major_, this->last_millis_); #endif /* else ESPHOME_CORES_MULTI_ATOMICS */ - while (!this->empty_()) { + // Cleanup before debug output + this->cleanup_(); + while (!this->items_.empty()) { std::unique_ptr item; { LockGuard guard{this->lock_}; @@ -333,7 +338,9 @@ void HOT Scheduler::call(uint32_t now) { this->to_remove_ = 0; } - while (!this->empty_()) { + // Cleanup removed items before processing + this->cleanup_(); + while (!this->items_.empty()) { // use scoping to indicate visibility of `item` variable { // Don't copy-by value yet @@ -399,8 +406,8 @@ void HOT Scheduler::process_to_add() { } this->to_add_.clear(); } -void HOT Scheduler::cleanup_() { - // Fast path: if nothing to remove, just return +size_t HOT Scheduler::cleanup_() { + // Fast path: if nothing to remove, just return the current size // Reading to_remove_ without lock is safe because: // 1. We only call this from the main thread during call() // 2. If it's 0, there's definitely nothing to cleanup @@ -408,7 +415,7 @@ void HOT Scheduler::cleanup_() { // 4. Not all platforms support atomics, so we accept this race in favor of performance // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless if (this->to_remove_ == 0) - return; + return this->items_.size(); // We must hold the lock for the entire cleanup operation because: // 1. We're modifying items_ (via pop_raw_) which requires exclusive access @@ -422,10 +429,11 @@ void HOT Scheduler::cleanup_() { while (!this->items_.empty()) { auto &item = this->items_[0]; if (!item->remove) - return; + break; this->to_remove_--; this->pop_raw_(); } + return this->items_.size(); } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index b539b26949..c14b7debe4 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -58,6 +58,9 @@ class Scheduler { // Calculate when the next scheduled item should run // @param now Fresh timestamp from millis() - must not be stale/cached + // Returns the time in milliseconds until the next scheduled item, or nullopt if no items + // This method performs cleanup of removed items before checking the schedule + // IMPORTANT: This method should only be called from the main thread (loop task). optional next_schedule_in(uint32_t now); // Execute all scheduled items that are ready @@ -147,7 +150,10 @@ class Scheduler { uint32_t delay, std::function func); uint64_t millis_64_(uint32_t now); - void cleanup_(); + // Cleanup logically deleted items from the scheduler + // Returns the number of items remaining after cleanup + // IMPORTANT: This method should only be called from the main thread (loop task). + size_t cleanup_(); void pop_raw_(); private: @@ -191,17 +197,6 @@ class Scheduler { return item->remove || (item->component != nullptr && item->component->is_failed()); } - // Check if the scheduler has no items. - // IMPORTANT: This method should only be called from the main thread (loop task). - // It performs cleanup of removed items and checks if the queue is empty. - // The items_.empty() check at the end is done without a lock for performance, - // which is safe because this is only called from the main thread while other - // threads only add items (never remove them). - bool empty_() { - this->cleanup_(); - return this->items_.empty(); - } - Mutex lock_; std::vector> items_; std::vector> to_add_; From 5b5982cfdde22f0925006ce50fd43f7a6af168da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 13:34:59 -1000 Subject: [PATCH 034/125] [api] Reduce memory usage by eliminating duplicate client info strings (#9740) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 26 ++++++++++---------- esphome/components/api/api_connection.h | 27 +++++++++++++-------- esphome/components/api/api_frame_helper.cpp | 13 +++++----- esphome/components/api/api_frame_helper.h | 21 ++++++++++------ esphome/components/api/api_server.cpp | 4 +-- 5 files changed, 53 insertions(+), 38 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 24a0c910b9..c95992e172 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -79,14 +79,16 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) auto noise_ctx = parent->get_noise_ctx(); if (noise_ctx->has_psk()) { - this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; + this->helper_ = + std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; } else { - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; + this->helper_ = std::unique_ptr{ + new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)}; #else #error "No frame helper defined" #endif @@ -109,9 +111,8 @@ void APIConnection::start() { errno); return; } - this->client_info_ = helper_->getpeername(); - this->client_peername_ = this->client_info_; - this->helper_->set_log_info(this->client_info_); + this->client_info_.peername = helper_->getpeername(); + this->client_info_.name = this->client_info_.peername; } APIConnection::~APIConnection() { @@ -1374,7 +1375,7 @@ void APIConnection::complete_authentication_() { this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); + this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); #endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1384,13 +1385,12 @@ void APIConnection::complete_authentication_() { } HelloResponse APIConnection::hello(const HelloRequest &msg) { - this->client_info_ = msg.client_info; - this->client_peername_ = this->helper_->getpeername(); - this->helper_->set_log_info(this->get_client_combined_info()); + this->client_info_.name = msg.client_info; + this->client_info_.peername = this->helper_->getpeername(); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), - this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_); + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(), + this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b0fd0f59b6..de7e91de01 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -16,6 +16,20 @@ namespace esphome { namespace api { +// Client information structure +struct ClientInfo { + std::string name; // Client name from Hello message + std::string peername; // IP:port from socket + + std::string get_combined_info() const { + if (name == peername) { + // Before Hello message, both are the same + return name; + } + return name + " (" + peername + ")"; + } +}; + // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending @@ -270,13 +284,7 @@ class APIConnection : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - std::string get_client_combined_info() const { - if (this->client_info_ == this->client_peername_) { - // Before Hello message, both are the same (just IP:port) - return this->client_info_; - } - return this->client_info_ + " (" + this->client_peername_ + ")"; - } + std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } // Buffer allocator methods for batch processing ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); @@ -482,9 +490,8 @@ class APIConnection : public APIServerConnection { std::unique_ptr image_reader_; #endif - // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) - std::string client_info_; - std::string client_peername_; + // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) + ClientInfo client_info_; // Group 4: 4-byte types uint32_t last_traffic_; diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 602c7359a8..39c01c028c 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,5 +1,6 @@ #include "api_frame_helper.h" #ifdef USE_API +#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -13,6 +14,8 @@ namespace api { static const char *const TAG = "api.socket"; +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) + const char *api_error_to_str(APIError err) { // not using switch to ensure compiler doesn't try to build a big table out of it if (err == APIError::OK) { @@ -81,7 +84,7 @@ APIError APIFrameHelper::handle_socket_write_error_() { if (errno == EWOULDBLOCK || errno == EAGAIN) { return APIError::WOULD_BLOCK; } - ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno); + HELPER_LOG("Socket write failed with errno %d", errno); this->state_ = State::FAILED; return APIError::SOCKET_WRITE_FAILED; } @@ -198,13 +201,13 @@ APIError APIFrameHelper::try_send_tx_buf_() { APIError APIFrameHelper::init_common_() { if (state_ != State::INITIALIZE || this->socket_ == nullptr) { - ESP_LOGVV(TAG, "%s: Bad state for init %d", this->info_.c_str(), (int) state_); + HELPER_LOG("Bad state for init %d", (int) state_); return APIError::BAD_STATE; } int err = this->socket_->setblocking(false); if (err != 0) { state_ = State::FAILED; - ESP_LOGVV(TAG, "%s: Setting nonblocking failed with errno %d", this->info_.c_str(), errno); + HELPER_LOG("Setting nonblocking failed with errno %d", errno); return APIError::TCP_NONBLOCKING_FAILED; } @@ -212,14 +215,12 @@ APIError APIFrameHelper::init_common_() { err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); if (err != 0) { state_ = State::FAILED; - ESP_LOGVV(TAG, "%s: Setting nodelay failed with errno %d", this->info_.c_str(), errno); + HELPER_LOG("Setting nodelay failed with errno %d", errno); return APIError::TCP_NODELAY_FAILED; } return APIError::OK; } -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__) - APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { if (received == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 421518188c..87a4b57c2f 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -19,6 +19,9 @@ namespace esphome { namespace api { +// Forward declaration +struct ClientInfo; + class ProtoWriteBuffer; struct ReadPacketBuffer { @@ -68,7 +71,8 @@ const char *api_error_to_str(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; - explicit APIFrameHelper(std::unique_ptr socket) : socket_owned_(std::move(socket)) { + explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) + : socket_owned_(std::move(socket)), client_info_(client_info) { socket_ = socket_owned_.get(); } virtual ~APIFrameHelper() = default; @@ -94,8 +98,6 @@ class APIFrameHelper { } return APIError::OK; } - // Give this helper a name for logging - void set_log_info(std::string info) { info_ = std::move(info); } virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; // Write multiple protobuf packets in a single operation // packets contains (message_type, offset, length) for each message in the buffer @@ -160,10 +162,13 @@ class APIFrameHelper { // Containers (size varies, but typically 12+ bytes on 32-bit) std::deque tx_buf_; - std::string info_; std::vector reusable_iovs_; std::vector rx_buf_; + // Pointer to client info (4 bytes on 32-bit) + // Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance. + const ClientInfo *client_info_{nullptr}; + // Group smaller types together uint16_t rx_buf_len_ = 0; State state_{State::INITIALIZE}; @@ -181,8 +186,9 @@ class APIFrameHelper { #ifdef USE_API_NOISE class APINoiseFrameHelper : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx) - : APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) { + APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, + const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) @@ -238,7 +244,8 @@ class APINoiseFrameHelper : public APIFrameHelper { #ifdef USE_API_PLAINTEXT class APIPlaintextFrameHelper : public APIFrameHelper { public: - APIPlaintextFrameHelper(std::unique_ptr socket) : APIFrameHelper(std::move(socket)) { + APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info) { // Plaintext header structure (worst case): // Pos 0: indicator (0x00) // Pos 1-3: payload size varint (up to 3 bytes) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 78c04f79c2..88966089cc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -184,9 +184,9 @@ void APIServer::loop() { // Rare case: handle disconnection #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); + this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername); #endif - ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); + ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str()); // Swap with the last element and pop (avoids expensive vector shifts) if (client_index < this->clients_.size() - 1) { From bb9011d65dcbfcd56fb5fb405509163a3c54f744 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:01:16 +1200 Subject: [PATCH 035/125] [CI] Label PR too-big if it has more than 1000 lines changed (#9744) --- .github/workflows/auto-label-pr.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index c3e1c641ce..f1321a86ee 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -56,6 +56,7 @@ env: valve SMALL_PR_THRESHOLD: 30 MAX_LABELS: 15 + TOO_BIG_THRESHOLD: 1000 jobs: label: @@ -147,6 +148,7 @@ jobs: const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); const maxLabels = parseInt('${{ env.MAX_LABELS }}'); + const tooBigThreshold = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); // Strategy: Merge to release or beta branch const baseRef = context.payload.pull_request.base.ref; @@ -402,18 +404,34 @@ jobs: console.log('Computed labels:', finalLabels.join(', ')); - // Don't set more than max labels - if (finalLabels.length > maxLabels) { + // Check if PR is allowed to be too big + const allowedTooBig = currentLabels.includes('mega-pr'); + + // Check if PR is too big (either too many labels or too many line changes) + const tooManyLabels = finalLabels.length > maxLabels; + const tooManyChanges = totalChanges > tooBigThreshold; + + if ((tooManyLabels || tooManyChanges) && !allowedTooBig) { const originalLength = finalLabels.length; - console.log(`Not setting ${originalLength} labels because out of range`); + console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges}`); finalLabels = ['too-big']; + // Create appropriate review message + let reviewBody; + if (tooManyLabels && tooManyChanges) { + reviewBody = `This PR is too large with ${totalChanges} line changes and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; + } else if (tooManyLabels) { + reviewBody = `This PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; + } else { + reviewBody = `This PR is too large with ${totalChanges} line changes. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; + } + // Request changes on the PR await github.rest.pulls.createReview({ owner, repo, pull_number: pr_number, - body: `This PR is too large and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`, + body: reviewBody, event: 'REQUEST_CHANGES' }); } From 06bd1472deab2c957e4f1d021b850d1481b2779a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:10:47 +1200 Subject: [PATCH 036/125] [CI] Keep original labels when PR has too many lines (#9745) --- .github/workflows/auto-label-pr.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index f1321a86ee..488a72ffb3 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -414,7 +414,14 @@ jobs: if ((tooManyLabels || tooManyChanges) && !allowedTooBig) { const originalLength = finalLabels.length; console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges}`); - finalLabels = ['too-big']; + + // If too big due to line changes only, keep original labels and add too-big + // If too big due to too many labels, replace with just too-big + if (tooManyChanges && !tooManyLabels) { + finalLabels.push('too-big'); + } else { + finalLabels = ['too-big']; + } // Create appropriate review message let reviewBody; From efd83dedda8ae7ddf5b228d016c157278eba9daf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:48:00 +1200 Subject: [PATCH 037/125] [CI] Fetch platform components and target platforms from hosted json file (#9747) --- .github/workflows/auto-label-pr.yml | 63 +++++++++-------------------- 1 file changed, 18 insertions(+), 45 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 488a72ffb3..a6687a8c86 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -11,49 +11,6 @@ permissions: contents: read env: - TARGET_PLATFORMS: | - esp32 - esp8266 - rp2040 - libretiny - bk72xx - rtl87xx - ln882x - nrf52 - host - PLATFORM_COMPONENTS: | - alarm_control_panel - audio_adc - audio_dac - binary_sensor - button - canbus - climate - cover - datetime - display - event - fan - light - lock - media_player - microphone - number - one_wire - ota - output - packet_transport - select - sensor - speaker - stepper - switch - text - text_sensor - time - touchscreen - update - valve SMALL_PR_THRESHOLD: 30 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 @@ -143,9 +100,25 @@ jobs: const labels = new Set(); + // Fetch TARGET_PLATFORMS and PLATFORM_COMPONENTS from API + let targetPlatforms = []; + let platformComponents = []; + + try { + const response = await fetch('https://data.esphome.io/components.json'); + const componentsData = await response.json(); + + // Extract target platforms and platform components directly from API + targetPlatforms = componentsData.target_platforms || []; + platformComponents = componentsData.platform_components || []; + + console.log('Target platforms from API:', targetPlatforms.length, targetPlatforms); + console.log('Platform components from API:', platformComponents.length, platformComponents); + } catch (error) { + console.log('Failed to fetch components data from API:', error.message); + } + // Get environment variables - const targetPlatforms = `${{ env.TARGET_PLATFORMS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); - const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); const maxLabels = parseInt('${{ env.MAX_LABELS }}'); const tooBigThreshold = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); From 46da0752265bac9b6b17379ab2b7acb7c3d6636f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:49:00 +1200 Subject: [PATCH 038/125] [CI] Add url and dismiss reviews once conditions are met (#9748) --- .github/workflows/auto-label-pr.yml | 57 ++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index a6687a8c86..4dfd315349 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -14,6 +14,7 @@ env: SMALL_PR_THRESHOLD: 30 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 + BOT_NAME: "esphome[bot]" jobs: label: @@ -122,6 +123,7 @@ jobs: const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); const maxLabels = parseInt('${{ env.MAX_LABELS }}'); const tooBigThreshold = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); + const botName = process.env.BOT_NAME; // Strategy: Merge to release or beta branch const baseRef = context.payload.pull_request.base.ref; @@ -377,14 +379,14 @@ jobs: console.log('Computed labels:', finalLabels.join(', ')); - // Check if PR is allowed to be too big - const allowedTooBig = currentLabels.includes('mega-pr'); + // Check if PR has mega-pr label + const isMegaPR = currentLabels.includes('mega-pr'); // Check if PR is too big (either too many labels or too many line changes) const tooManyLabels = finalLabels.length > maxLabels; const tooManyChanges = totalChanges > tooBigThreshold; - if ((tooManyLabels || tooManyChanges) && !allowedTooBig) { + if ((tooManyLabels || tooManyChanges) && !isMegaPR) { const originalLength = finalLabels.length; console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges}`); @@ -399,11 +401,11 @@ jobs: // Create appropriate review message let reviewBody; if (tooManyLabels && tooManyChanges) { - reviewBody = `This PR is too large with ${totalChanges} line changes and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; + reviewBody = `This PR is too large with ${totalChanges} line changes and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; } else if (tooManyLabels) { - reviewBody = `This PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; + reviewBody = `This PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; } else { - reviewBody = `This PR is too large with ${totalChanges} line changes. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`; + reviewBody = `This PR is too large with ${totalChanges} line changes. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; } // Request changes on the PR @@ -414,6 +416,49 @@ jobs: body: reviewBody, event: 'REQUEST_CHANGES' }); + } else { + // Check if PR was previously too big but is now acceptable + const wasPreviouslyTooBig = currentLabels.includes('too-big'); + + if (wasPreviouslyTooBig || isMegaPR) { + console.log('PR is no longer too big or has mega-pr label - dismissing bot reviews'); + + // Get all reviews on this PR + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + // Find bot reviews that requested changes + const botReviews = reviews.filter(review => + review.user.login === botName && + review.state === 'CHANGES_REQUESTED' && + review.body && ( + review.body.includes('This PR is too large') || + review.body.includes('This PR affects') || + review.body.includes('different components/areas') + ) + ); + + // Dismiss bot reviews + for (const review of botReviews) { + try { + await github.rest.pulls.dismissReview({ + owner, + repo, + pull_number: pr_number, + review_id: review.id, + message: isMegaPR ? + 'Review dismissed: mega-pr label was added' : + 'Review dismissed: PR size is now acceptable' + }); + console.log(`Dismissed review ${review.id}`); + } catch (error) { + console.log(`Failed to dismiss review ${review.id}:`, error.message); + } + } + } } // Add new labels From a45a45c6880681fceb5673e81ac48e4a4c8325c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 15:10:08 -1000 Subject: [PATCH 039/125] [api] Split frame helper implementation into protocol-specific files (#9746) --- esphome/components/api/__init__.py | 19 +- esphome/components/api/api_connection.cpp | 6 + esphome/components/api/api_frame_helper.cpp | 851 +----------------- esphome/components/api/api_frame_helper.h | 123 +-- .../components/api/api_frame_helper_noise.cpp | 577 ++++++++++++ .../components/api/api_frame_helper_noise.h | 70 ++ .../api/api_frame_helper_plaintext.cpp | 292 ++++++ .../api/api_frame_helper_plaintext.h | 55 ++ 8 files changed, 1046 insertions(+), 947 deletions(-) create mode 100644 esphome/components/api/api_frame_helper_noise.cpp create mode 100644 esphome/components/api/api_frame_helper_noise.h create mode 100644 esphome/components/api/api_frame_helper_plaintext.cpp create mode 100644 esphome/components/api/api_frame_helper_plaintext.h diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 5b302760b1..9cbab8164f 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -323,9 +323,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args): def FILTER_SOURCE_FILES() -> list[str]: - """Filter out api_pb2_dump.cpp when proto message dumping is not enabled - and user_services.cpp when no services are defined.""" - files_to_filter = [] + """Filter out api_pb2_dump.cpp when proto message dumping is not enabled, + user_services.cpp when no services are defined, and protocol-specific + implementations based on encryption configuration.""" + files_to_filter: list[str] = [] # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined # This is a particularly large file that still needs to be opened and read @@ -341,4 +342,16 @@ def FILTER_SOURCE_FILES() -> list[str]: if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]: files_to_filter.append("user_services.cpp") + # Filter protocol-specific implementations based on encryption configuration + encryption_config = config.get(CONF_ENCRYPTION) if config else None + + # If encryption is not configured at all, we only need plaintext + if encryption_config is None: + files_to_filter.append("api_frame_helper_noise.cpp") + # If encryption is configured with a key, we only need noise + elif encryption_config.get(CONF_KEY): + files_to_filter.append("api_frame_helper_plaintext.cpp") + # If encryption is configured but no key is provided, we need both + # (this allows a plaintext client to provide a noise key) + return files_to_filter diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c95992e172..602a0256cf 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1,5 +1,11 @@ #include "api_connection.h" #ifdef USE_API +#ifdef USE_API_NOISE +#include "api_frame_helper_noise.h" +#endif +#ifdef USE_API_PLAINTEXT +#include "api_frame_helper_plaintext.h" +#endif #include #include #include diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 39c01c028c..b1c9478e59 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -12,18 +12,24 @@ namespace esphome { namespace api { -static const char *const TAG = "api.socket"; +static const char *const TAG = "api.frame_helper"; #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) +#ifdef HELPER_LOG_PACKETS +#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) +#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#else +#define LOG_PACKET_RECEIVED(buffer) ((void) 0) +#define LOG_PACKET_SENDING(data, len) ((void) 0) +#endif + const char *api_error_to_str(APIError err) { // not using switch to ensure compiler doesn't try to build a big table out of it if (err == APIError::OK) { return "OK"; } else if (err == APIError::WOULD_BLOCK) { return "WOULD_BLOCK"; - } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { - return "BAD_HANDSHAKE_PACKET_LEN"; } else if (err == APIError::BAD_INDICATOR) { return "BAD_INDICATOR"; } else if (err == APIError::BAD_DATA_PACKET) { @@ -44,6 +50,14 @@ const char *api_error_to_str(APIError err) { return "SOCKET_READ_FAILED"; } else if (err == APIError::SOCKET_WRITE_FAILED) { return "SOCKET_WRITE_FAILED"; + } else if (err == APIError::OUT_OF_MEMORY) { + return "OUT_OF_MEMORY"; + } else if (err == APIError::CONNECTION_CLOSED) { + return "CONNECTION_CLOSED"; + } +#ifdef USE_API_NOISE + else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { + return "BAD_HANDSHAKE_PACKET_LEN"; } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { return "HANDSHAKESTATE_READ_FAILED"; } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { @@ -54,17 +68,14 @@ const char *api_error_to_str(APIError err) { return "CIPHERSTATE_DECRYPT_FAILED"; } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { return "CIPHERSTATE_ENCRYPT_FAILED"; - } else if (err == APIError::OUT_OF_MEMORY) { - return "OUT_OF_MEMORY"; } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { return "HANDSHAKESTATE_SETUP_FAILED"; } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { return "HANDSHAKESTATE_SPLIT_FAILED"; } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { return "BAD_HANDSHAKE_ERROR_BYTE"; - } else if (err == APIError::CONNECTION_CLOSED) { - return "CONNECTION_CLOSED"; } +#endif return "UNKNOWN"; } @@ -125,8 +136,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_ #ifdef HELPER_LOG_PACKETS for (int i = 0; i < iovcnt; i++) { - ESP_LOGVV(TAG, "Sending raw: %s", - format_hex_pretty(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); + LOG_PACKET_SENDING(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); } #endif @@ -236,829 +246,6 @@ APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { } return APIError::OK; } -// uncomment to log raw packets -//#define HELPER_LOG_PACKETS - -#ifdef USE_API_NOISE -static const char *const PROLOGUE_INIT = "NoiseAPIInit"; - -/// Convert a noise error code to a readable error -std::string noise_err_to_str(int err) { - if (err == NOISE_ERROR_NO_MEMORY) - return "NO_MEMORY"; - if (err == NOISE_ERROR_UNKNOWN_ID) - return "UNKNOWN_ID"; - if (err == NOISE_ERROR_UNKNOWN_NAME) - return "UNKNOWN_NAME"; - if (err == NOISE_ERROR_MAC_FAILURE) - return "MAC_FAILURE"; - if (err == NOISE_ERROR_NOT_APPLICABLE) - return "NOT_APPLICABLE"; - if (err == NOISE_ERROR_SYSTEM) - return "SYSTEM"; - if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) - return "REMOTE_KEY_REQUIRED"; - if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) - return "LOCAL_KEY_REQUIRED"; - if (err == NOISE_ERROR_PSK_REQUIRED) - return "PSK_REQUIRED"; - if (err == NOISE_ERROR_INVALID_LENGTH) - return "INVALID_LENGTH"; - if (err == NOISE_ERROR_INVALID_PARAM) - return "INVALID_PARAM"; - if (err == NOISE_ERROR_INVALID_STATE) - return "INVALID_STATE"; - if (err == NOISE_ERROR_INVALID_NONCE) - return "INVALID_NONCE"; - if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) - return "INVALID_PRIVATE_KEY"; - if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) - return "INVALID_PUBLIC_KEY"; - if (err == NOISE_ERROR_INVALID_FORMAT) - return "INVALID_FORMAT"; - if (err == NOISE_ERROR_INVALID_SIGNATURE) - return "INVALID_SIGNATURE"; - return to_string(err); -} - -/// Initialize the frame helper, returns OK if successful. -APIError APINoiseFrameHelper::init() { - APIError err = init_common_(); - if (err != APIError::OK) { - return err; - } - - // init prologue - prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); - - state_ = State::CLIENT_HELLO; - return APIError::OK; -} -// Helper for handling handshake frame errors -APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { - if (aerr == APIError::BAD_INDICATOR) { - send_explicit_handshake_reject_("Bad indicator byte"); - } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { - send_explicit_handshake_reject_("Bad handshake packet len"); - } - return aerr; -} - -// Helper for handling noise library errors -APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); - return api_err; - } - return APIError::OK; -} - -/// Run through handshake messages (if in that phase) -APIError APINoiseFrameHelper::loop() { - // During handshake phase, process as many actions as possible until we can't progress - // socket_->ready() stays true until next main loop, but state_action() will return - // WOULD_BLOCK when no more data is available to read - while (state_ != State::DATA && this->socket_->ready()) { - APIError err = state_action_(); - if (err == APIError::WOULD_BLOCK) { - break; - } - if (err != APIError::OK) { - return err; - } - } - - // Use base class implementation for buffer sending - return APIFrameHelper::loop(); -} - -/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter - * - * @param frame: The struct to hold the frame information in. - * msg_start: points to the start of the payload - this pointer is only valid until the next - * try_receive_raw_ call - * - * @return 0 if a full packet is in rx_buf_ - * @return -1 if error, check errno. - * - * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. - * errno ENOMEM: Not enough memory for reading packet. - * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. - * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. - */ -APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { - if (frame == nullptr) { - HELPER_LOG("Bad argument for try_read_frame_"); - return APIError::BAD_ARG; - } - - // read header - if (rx_header_buf_len_ < 3) { - // no header information yet - uint8_t to_read = 3 - rx_header_buf_len_; - ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); - APIError err = handle_socket_read_result_(received); - if (err != APIError::OK) { - return err; - } - rx_header_buf_len_ += static_cast(received); - if (static_cast(received) != to_read) { - // not a full read - return APIError::WOULD_BLOCK; - } - - if (rx_header_buf_[0] != 0x01) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); - return APIError::BAD_INDICATOR; - } - // header reading done - } - - // read body - uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; - - if (state_ != State::DATA && msg_size > 128) { - // for handshake message only permit up to 128 bytes - state_ = State::FAILED; - HELPER_LOG("Bad packet len for handshake: %d", msg_size); - return APIError::BAD_HANDSHAKE_PACKET_LEN; - } - - // reserve space for body - if (rx_buf_.size() != msg_size) { - rx_buf_.resize(msg_size); - } - - if (rx_buf_len_ < msg_size) { - // more data to read - uint16_t to_read = msg_size - rx_buf_len_; - ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - APIError err = handle_socket_read_result_(received); - if (err != APIError::OK) { - return err; - } - rx_buf_len_ += static_cast(received); - if (static_cast(received) != to_read) { - // not all read - return APIError::WOULD_BLOCK; - } - } - - // uncomment for even more debugging -#ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); -#endif - *frame = std::move(rx_buf_); - // consume msg - rx_buf_ = {}; - rx_buf_len_ = 0; - rx_header_buf_len_ = 0; - return APIError::OK; -} - -/** To be called from read/write methods. - * - * This method runs through the internal handshake methods, if in that state. - * - * If the handshake is still active when this method returns and a read/write can't take place at - * the moment, returns WOULD_BLOCK. - * If an error occurred, returns that error. Only returns OK if the transport is ready for data - * traffic. - */ -APIError APINoiseFrameHelper::state_action_() { - int err; - APIError aerr; - if (state_ == State::INITIALIZE) { - HELPER_LOG("Bad state for method: %d", (int) state_); - return APIError::BAD_STATE; - } - if (state_ == State::CLIENT_HELLO) { - // waiting for client hello - std::vector frame; - aerr = try_read_frame_(&frame); - if (aerr != APIError::OK) { - return handle_handshake_frame_error_(aerr); - } - // ignore contents, may be used in future for flags - // Reserve space for: existing prologue + 2 size bytes + frame data - prologue_.reserve(prologue_.size() + 2 + frame.size()); - prologue_.push_back((uint8_t) (frame.size() >> 8)); - prologue_.push_back((uint8_t) frame.size()); - prologue_.insert(prologue_.end(), frame.begin(), frame.end()); - - state_ = State::SERVER_HELLO; - } - if (state_ == State::SERVER_HELLO) { - // send server hello - const std::string &name = App.get_name(); - const std::string &mac = get_mac_address(); - - std::vector msg; - // Reserve space for: 1 byte proto + name + null + mac + null - msg.reserve(1 + name.size() + 1 + mac.size() + 1); - - // chosen proto - msg.push_back(0x01); - - // node name, terminated by null byte - const uint8_t *name_ptr = reinterpret_cast(name.c_str()); - msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); - // node mac, terminated by null byte - const uint8_t *mac_ptr = reinterpret_cast(mac.c_str()); - msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); - - aerr = write_frame_(msg.data(), msg.size()); - if (aerr != APIError::OK) - return aerr; - - // start handshake - aerr = init_handshake_(); - if (aerr != APIError::OK) - return aerr; - - state_ = State::HANDSHAKE; - } - if (state_ == State::HANDSHAKE) { - int action = noise_handshakestate_get_action(handshake_); - if (action == NOISE_ACTION_READ_MESSAGE) { - // waiting for handshake msg - std::vector frame; - aerr = try_read_frame_(&frame); - if (aerr != APIError::OK) { - return handle_handshake_frame_error_(aerr); - } - - if (frame.empty()) { - send_explicit_handshake_reject_("Empty handshake message"); - return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } else if (frame[0] != 0x00) { - HELPER_LOG("Bad handshake error byte: %u", frame[0]); - send_explicit_handshake_reject_("Bad handshake error byte"); - return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } - - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1); - err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); - if (err != 0) { - // Special handling for MAC failure - send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); - return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); - } - - aerr = check_handshake_finished_(); - if (aerr != APIError::OK) - return aerr; - } else if (action == NOISE_ACTION_WRITE_MESSAGE) { - uint8_t buffer[65]; - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); - - err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); - APIError aerr_write = - handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); - if (aerr_write != APIError::OK) - return aerr_write; - buffer[0] = 0x00; // success - - aerr = write_frame_(buffer, mbuf.size + 1); - if (aerr != APIError::OK) - return aerr; - aerr = check_handshake_finished_(); - if (aerr != APIError::OK) - return aerr; - } else { - // bad state for action - state_ = State::FAILED; - HELPER_LOG("Bad action for handshake: %d", action); - return APIError::HANDSHAKESTATE_BAD_STATE; - } - } - if (state_ == State::CLOSED || state_ == State::FAILED) { - return APIError::BAD_STATE; - } - return APIError::OK; -} -void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { - std::vector data; - data.resize(reason.length() + 1); - data[0] = 0x01; // failure - - // Copy error message in bulk - if (!reason.empty()) { - std::memcpy(data.data() + 1, reason.c_str(), reason.length()); - } - - // temporarily remove failed state - auto orig_state = state_; - state_ = State::EXPLICIT_REJECT; - write_frame_(data.data(), data.size()); - state_ = orig_state; -} -APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { - int err; - APIError aerr; - aerr = state_action_(); - if (aerr != APIError::OK) { - return aerr; - } - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } - - std::vector frame; - aerr = try_read_frame_(&frame); - if (aerr != APIError::OK) - return aerr; - - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); - err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); - APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); - if (decrypt_err != APIError::OK) - return decrypt_err; - - uint16_t msg_size = mbuf.size; - uint8_t *msg_data = frame.data(); - if (msg_size < 4) { - state_ = State::FAILED; - HELPER_LOG("Bad data packet: size %d too short", msg_size); - return APIError::BAD_DATA_PACKET; - } - - uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; - uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; - if (data_len > msg_size - 4) { - state_ = State::FAILED; - HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); - return APIError::BAD_DATA_PACKET; - } - - buffer->container = std::move(frame); - buffer->data_offset = 4; - buffer->data_len = data_len; - buffer->type = type; - return APIError::OK; -} -APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - // Resize to include MAC space (required for Noise encryption) - buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); - PacketInfo packet{type, 0, - static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; - return write_protobuf_packets(buffer, std::span(&packet, 1)); -} - -APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { - APIError aerr = state_action_(); - if (aerr != APIError::OK) { - return aerr; - } - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } - - if (packets.empty()) { - return APIError::OK; - } - - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer - - this->reusable_iovs_.clear(); - this->reusable_iovs_.reserve(packets.size()); - uint16_t total_write_len = 0; - - // We need to encrypt each packet in place - for (const auto &packet : packets) { - // The buffer already has padding at offset - uint8_t *buf_start = buffer_data + packet.offset; - - // Write noise header - buf_start[0] = 0x01; // indicator - // buf_start[1], buf_start[2] to be set after encryption - - // Write message header (to be encrypted) - const uint8_t msg_offset = 3; - buf_start[msg_offset] = static_cast(packet.message_type >> 8); // type high byte - buf_start[msg_offset + 1] = static_cast(packet.message_type); // type low byte - buf_start[msg_offset + 2] = static_cast(packet.payload_size >> 8); // data_len high byte - buf_start[msg_offset + 3] = static_cast(packet.payload_size); // data_len low byte - // payload data is already in the buffer starting at offset + 7 - - // Make sure we have space for MAC - // The buffer should already have been sized appropriately - - // Encrypt the message in place - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size, - 4 + packet.payload_size + frame_footer_size_); - - int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); - APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); - if (aerr != APIError::OK) - return aerr; - - // Fill in the encrypted size - buf_start[1] = static_cast(mbuf.size >> 8); - buf_start[2] = static_cast(mbuf.size); - - // Add iovec for this encrypted packet - size_t packet_len = static_cast(3 + mbuf.size); // indicator + size + encrypted data - this->reusable_iovs_.push_back({buf_start, packet_len}); - total_write_len += packet_len; - } - - // Send all encrypted packets in one writev call - return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); -} - -APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { - uint8_t header[3]; - header[0] = 0x01; // indicator - header[1] = (uint8_t) (len >> 8); - header[2] = (uint8_t) len; - - struct iovec iov[2]; - iov[0].iov_base = header; - iov[0].iov_len = 3; - if (len == 0) { - return this->write_raw_(iov, 1, 3); // Just header - } - iov[1].iov_base = const_cast(data); - iov[1].iov_len = len; - - return this->write_raw_(iov, 2, 3 + len); // Header + data -} - -/** Initiate the data structures for the handshake. - * - * @return 0 on success, -1 on error (check errno) - */ -APIError APINoiseFrameHelper::init_handshake_() { - int err; - memset(&nid_, 0, sizeof(nid_)); - // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; - // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); - nid_.pattern_id = NOISE_PATTERN_NN; - nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; - nid_.dh_id = NOISE_DH_CURVE25519; - nid_.prefix_id = NOISE_PREFIX_STANDARD; - nid_.hybrid_id = NOISE_DH_NONE; - nid_.hash_id = NOISE_HASH_SHA256; - nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; - - err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); - APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); - if (aerr != APIError::OK) - return aerr; - - const auto &psk = ctx_->get_psk(); - err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); - aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); - if (aerr != APIError::OK) - return aerr; - - err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); - aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); - if (aerr != APIError::OK) - return aerr; - // set_prologue copies it into handshakestate, so we can get rid of it now - prologue_ = {}; - - err = noise_handshakestate_start(handshake_); - aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); - if (aerr != APIError::OK) - return aerr; - return APIError::OK; -} - -APIError APINoiseFrameHelper::check_handshake_finished_() { - assert(state_ == State::HANDSHAKE); - - int action = noise_handshakestate_get_action(handshake_); - if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) - return APIError::OK; - if (action != NOISE_ACTION_SPLIT) { - state_ = State::FAILED; - HELPER_LOG("Bad action for handshake: %d", action); - return APIError::HANDSHAKESTATE_BAD_STATE; - } - int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); - APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); - if (aerr != APIError::OK) - return aerr; - - frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); - - HELPER_LOG("Handshake complete!"); - noise_handshakestate_free(handshake_); - handshake_ = nullptr; - state_ = State::DATA; - return APIError::OK; -} - -APINoiseFrameHelper::~APINoiseFrameHelper() { - if (handshake_ != nullptr) { - noise_handshakestate_free(handshake_); - handshake_ = nullptr; - } - if (send_cipher_ != nullptr) { - noise_cipherstate_free(send_cipher_); - send_cipher_ = nullptr; - } - if (recv_cipher_ != nullptr) { - noise_cipherstate_free(recv_cipher_); - recv_cipher_ = nullptr; - } -} - -extern "C" { -// declare how noise generates random bytes (here with a good HWRNG based on the RF system) -void noise_rand_bytes(void *output, size_t len) { - if (!esphome::random_bytes(reinterpret_cast(output), len)) { - ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting"); - arch_restart(); - } -} -} - -#endif // USE_API_NOISE - -#ifdef USE_API_PLAINTEXT - -/// Initialize the frame helper, returns OK if successful. -APIError APIPlaintextFrameHelper::init() { - APIError err = init_common_(); - if (err != APIError::OK) { - return err; - } - - state_ = State::DATA; - return APIError::OK; -} -APIError APIPlaintextFrameHelper::loop() { - if (state_ != State::DATA) { - return APIError::BAD_STATE; - } - // Use base class implementation for buffer sending - return APIFrameHelper::loop(); -} - -/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter - * - * @param frame: The struct to hold the frame information in. - * msg: store the parsed frame in that struct - * - * @return See APIError - * - * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. - */ -APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { - if (frame == nullptr) { - HELPER_LOG("Bad argument for try_read_frame_"); - return APIError::BAD_ARG; - } - - // read header - while (!rx_header_parsed_) { - // Now that we know when the socket is ready, we can read up to 3 bytes - // into the rx_header_buf_ before we have to switch back to reading - // one byte at a time to ensure we don't read past the message and - // into the next one. - - // Read directly into rx_header_buf_ at the current position - // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time - ssize_t received = - this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); - APIError err = handle_socket_read_result_(received); - if (err != APIError::OK) { - return err; - } - - // If this was the first read, validate the indicator byte - if (rx_header_buf_pos_ == 0 && received > 0) { - if (rx_header_buf_[0] != 0x00) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); - return APIError::BAD_INDICATOR; - } - } - - rx_header_buf_pos_ += received; - - // Check for buffer overflow - if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) { - state_ = State::FAILED; - HELPER_LOG("Header buffer overflow"); - return APIError::BAD_DATA_PACKET; - } - - // Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse - if (rx_header_buf_pos_ < 3) { - continue; - } - - // At this point, we have at least 3 bytes total: - // - Validated indicator byte (0x00) stored at position 0 - // - At least 2 bytes in the buffer for the varints - // Buffer layout: - // [0]: indicator byte (0x00) - // [1-3]: Message size varint (variable length) - // - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535) - // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise - // [2-5]: Message type varint (variable length) - // We now attempt to parse both varints. If either is incomplete, - // we'll continue reading more bytes. - - // Skip indicator byte at position 0 - uint8_t varint_pos = 1; - uint32_t consumed = 0; - - auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); - if (!msg_size_varint.has_value()) { - // not enough data there yet - continue; - } - - if (msg_size_varint->as_uint32() > std::numeric_limits::max()) { - state_ = State::FAILED; - HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), - std::numeric_limits::max()); - return APIError::BAD_DATA_PACKET; - } - rx_header_parsed_len_ = msg_size_varint->as_uint16(); - - // Move to next varint position - varint_pos += consumed; - - auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); - if (!msg_type_varint.has_value()) { - // not enough data there yet - continue; - } - if (msg_type_varint->as_uint32() > std::numeric_limits::max()) { - state_ = State::FAILED; - HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), - std::numeric_limits::max()); - return APIError::BAD_DATA_PACKET; - } - rx_header_parsed_type_ = msg_type_varint->as_uint16(); - rx_header_parsed_ = true; - } - // header reading done - - // reserve space for body - if (rx_buf_.size() != rx_header_parsed_len_) { - rx_buf_.resize(rx_header_parsed_len_); - } - - if (rx_buf_len_ < rx_header_parsed_len_) { - // more data to read - uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; - ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - APIError err = handle_socket_read_result_(received); - if (err != APIError::OK) { - return err; - } - rx_buf_len_ += static_cast(received); - if (static_cast(received) != to_read) { - // not all read - return APIError::WOULD_BLOCK; - } - } - - // uncomment for even more debugging -#ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); -#endif - *frame = std::move(rx_buf_); - // consume msg - rx_buf_ = {}; - rx_buf_len_ = 0; - rx_header_buf_pos_ = 0; - rx_header_parsed_ = false; - return APIError::OK; -} -APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - APIError aerr; - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } - - std::vector frame; - aerr = try_read_frame_(&frame); - if (aerr != APIError::OK) { - if (aerr == APIError::BAD_INDICATOR) { - // Make sure to tell the remote that we don't - // understand the indicator byte so it knows - // we do not support it. - struct iovec iov[1]; - // The \x00 first byte is the marker for plaintext. - // - // The remote will know how to handle the indicator byte, - // but it likely won't understand the rest of the message. - // - // We must send at least 3 bytes to be read, so we add - // a message after the indicator byte to ensures its long - // enough and can aid in debugging. - const char msg[] = "\x00" - "Bad indicator byte"; - iov[0].iov_base = (void *) msg; - iov[0].iov_len = 19; - this->write_raw_(iov, 1, 19); - } - return aerr; - } - - buffer->container = std::move(frame); - buffer->data_offset = 0; - buffer->data_len = rx_header_parsed_len_; - buffer->type = rx_header_parsed_type_; - return APIError::OK; -} -APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - PacketInfo packet{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; - return write_protobuf_packets(buffer, std::span(&packet, 1)); -} - -APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { - if (state_ != State::DATA) { - return APIError::BAD_STATE; - } - - if (packets.empty()) { - return APIError::OK; - } - - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer - - this->reusable_iovs_.clear(); - this->reusable_iovs_.reserve(packets.size()); - uint16_t total_write_len = 0; - - for (const auto &packet : packets) { - // Calculate varint sizes for header layout - uint8_t size_varint_len = api::ProtoSize::varint(static_cast(packet.payload_size)); - uint8_t type_varint_len = api::ProtoSize::varint(static_cast(packet.message_type)); - uint8_t total_header_len = 1 + size_varint_len + type_varint_len; - - // Calculate where to start writing the header - // The header starts at the latest possible position to minimize unused padding - // - // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 - // [0-2] - Unused padding - // [3] - 0x00 indicator byte - // [4] - Payload size varint (1 byte, for sizes 0-127) - // [5] - Message type varint (1 byte, for types 0-127) - // [6...] - Actual payload data - // - // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 - // [0-1] - Unused padding - // [2] - 0x00 indicator byte - // [3-4] - Payload size varint (2 bytes, for sizes 128-16383) - // [5] - Message type varint (1 byte, for types 0-127) - // [6...] - Actual payload data - // - // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 - // [0] - 0x00 indicator byte - // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) - // [4-5] - Message type varint (2 bytes, for types 128-32767) - // [6...] - Actual payload data - // - // The message starts at offset + frame_header_padding_ - // So we write the header starting at offset + frame_header_padding_ - total_header_len - uint8_t *buf_start = buffer_data + packet.offset; - uint32_t header_offset = frame_header_padding_ - total_header_len; - - // Write the plaintext header - buf_start[header_offset] = 0x00; // indicator - - // Encode varints directly into buffer - ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - ProtoVarInt(packet.message_type) - .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); - - // Add iovec for this packet (header + payload) - size_t packet_len = static_cast(total_header_len + packet.payload_size); - this->reusable_iovs_.push_back({buf_start + header_offset, packet_len}); - total_write_len += packet_len; - } - - // Send all packets in one writev call - return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); -} - -#endif // USE_API_PLAINTEXT } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 87a4b57c2f..231a3366ce 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -8,17 +8,16 @@ #include "esphome/core/defines.h" #ifdef USE_API -#ifdef USE_API_NOISE -#include "noise/protocol.h" -#endif - -#include "api_noise_context.h" #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" namespace esphome { namespace api { +// uncomment to log raw packets +//#define HELPER_LOG_PACKETS + // Forward declaration struct ClientInfo; @@ -43,7 +42,6 @@ struct PacketInfo { enum class APIError : uint16_t { OK = 0, WOULD_BLOCK = 1001, - BAD_HANDSHAKE_PACKET_LEN = 1002, BAD_INDICATOR = 1003, BAD_DATA_PACKET = 1004, TCP_NODELAY_FAILED = 1005, @@ -54,16 +52,19 @@ enum class APIError : uint16_t { BAD_ARG = 1010, SOCKET_READ_FAILED = 1011, SOCKET_WRITE_FAILED = 1012, + OUT_OF_MEMORY = 1018, + CONNECTION_CLOSED = 1022, +#ifdef USE_API_NOISE + BAD_HANDSHAKE_PACKET_LEN = 1002, HANDSHAKESTATE_READ_FAILED = 1013, HANDSHAKESTATE_WRITE_FAILED = 1014, HANDSHAKESTATE_BAD_STATE = 1015, CIPHERSTATE_DECRYPT_FAILED = 1016, CIPHERSTATE_ENCRYPT_FAILED = 1017, - OUT_OF_MEMORY = 1018, HANDSHAKESTATE_SETUP_FAILED = 1019, HANDSHAKESTATE_SPLIT_FAILED = 1020, BAD_HANDSHAKE_ERROR_BYTE = 1021, - CONNECTION_CLOSED = 1022, +#endif }; const char *api_error_to_str(APIError err); @@ -183,109 +184,7 @@ class APIFrameHelper { APIError handle_socket_read_result_(ssize_t received); }; -#ifdef USE_API_NOISE -class APINoiseFrameHelper : public APIFrameHelper { - public: - APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, - const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { - // Noise header structure: - // Pos 0: indicator (0x01) - // Pos 1-2: encrypted payload size (16-bit big-endian) - // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) - // Pos 7+: actual payload data - frame_header_padding_ = 7; - } - ~APINoiseFrameHelper() override; - APIError init() override; - APIError loop() override; - APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; - // Get the frame header padding required by this protocol - uint8_t frame_header_padding() override { return frame_header_padding_; } - // Get the frame footer size required by this protocol - uint8_t frame_footer_size() override { return frame_footer_size_; } - - protected: - APIError state_action_(); - APIError try_read_frame_(std::vector *frame); - APIError write_frame_(const uint8_t *data, uint16_t len); - APIError init_handshake_(); - APIError check_handshake_finished_(); - void send_explicit_handshake_reject_(const std::string &reason); - APIError handle_handshake_frame_error_(APIError aerr); - APIError handle_noise_error_(int err, const char *func_name, APIError api_err); - - // Pointers first (4 bytes each) - NoiseHandshakeState *handshake_{nullptr}; - NoiseCipherState *send_cipher_{nullptr}; - NoiseCipherState *recv_cipher_{nullptr}; - - // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) - std::shared_ptr ctx_; - - // Vector (12 bytes on 32-bit) - std::vector prologue_; - - // NoiseProtocolId (size depends on implementation) - NoiseProtocolId nid_; - - // Group small types together - // Fixed-size header buffer for noise protocol: - // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) - // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase - uint8_t rx_header_buf_[3]; - uint8_t rx_header_buf_len_ = 0; - // 4 bytes total, no padding -}; -#endif // USE_API_NOISE - -#ifdef USE_API_PLAINTEXT -class APIPlaintextFrameHelper : public APIFrameHelper { - public: - APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info) { - // Plaintext header structure (worst case): - // Pos 0: indicator (0x00) - // Pos 1-3: payload size varint (up to 3 bytes) - // Pos 4-5: message type varint (up to 2 bytes) - // Pos 6+: actual payload data - frame_header_padding_ = 6; - } - ~APIPlaintextFrameHelper() override = default; - APIError init() override; - APIError loop() override; - APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; - uint8_t frame_header_padding() override { return frame_header_padding_; } - // Get the frame footer size required by this protocol - uint8_t frame_footer_size() override { return frame_footer_size_; } - - protected: - APIError try_read_frame_(std::vector *frame); - - // Group 2-byte aligned types - uint16_t rx_header_parsed_type_ = 0; - uint16_t rx_header_parsed_len_ = 0; - - // Group 1-byte types together - // Fixed-size header buffer for plaintext protocol: - // We now store the indicator byte + the two varints. - // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: - // 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint - // - // While varints could theoretically be up to 10 bytes each for 64-bit values, - // attempting to process messages with headers that large would likely crash the - // ESP32 due to memory constraints. - uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) - uint8_t rx_header_buf_pos_ = 0; - bool rx_header_parsed_ = false; - // 8 bytes total, no padding needed -}; -#endif - } // namespace api } // namespace esphome -#endif + +#endif // USE_API diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp new file mode 100644 index 0000000000..3c2c9e059e --- /dev/null +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -0,0 +1,577 @@ +#include "api_frame_helper_noise.h" +#ifdef USE_API +#ifdef USE_API_NOISE +#include "api_connection.h" // For ClientInfo struct +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "proto.h" +#include +#include + +namespace esphome { +namespace api { + +static const char *const TAG = "api.noise"; +static const char *const PROLOGUE_INIT = "NoiseAPIInit"; + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) + +#ifdef HELPER_LOG_PACKETS +#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) +#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#else +#define LOG_PACKET_RECEIVED(buffer) ((void) 0) +#define LOG_PACKET_SENDING(data, len) ((void) 0) +#endif + +/// Convert a noise error code to a readable error +std::string noise_err_to_str(int err) { + if (err == NOISE_ERROR_NO_MEMORY) + return "NO_MEMORY"; + if (err == NOISE_ERROR_UNKNOWN_ID) + return "UNKNOWN_ID"; + if (err == NOISE_ERROR_UNKNOWN_NAME) + return "UNKNOWN_NAME"; + if (err == NOISE_ERROR_MAC_FAILURE) + return "MAC_FAILURE"; + if (err == NOISE_ERROR_NOT_APPLICABLE) + return "NOT_APPLICABLE"; + if (err == NOISE_ERROR_SYSTEM) + return "SYSTEM"; + if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) + return "REMOTE_KEY_REQUIRED"; + if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) + return "LOCAL_KEY_REQUIRED"; + if (err == NOISE_ERROR_PSK_REQUIRED) + return "PSK_REQUIRED"; + if (err == NOISE_ERROR_INVALID_LENGTH) + return "INVALID_LENGTH"; + if (err == NOISE_ERROR_INVALID_PARAM) + return "INVALID_PARAM"; + if (err == NOISE_ERROR_INVALID_STATE) + return "INVALID_STATE"; + if (err == NOISE_ERROR_INVALID_NONCE) + return "INVALID_NONCE"; + if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) + return "INVALID_PRIVATE_KEY"; + if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) + return "INVALID_PUBLIC_KEY"; + if (err == NOISE_ERROR_INVALID_FORMAT) + return "INVALID_FORMAT"; + if (err == NOISE_ERROR_INVALID_SIGNATURE) + return "INVALID_SIGNATURE"; + return to_string(err); +} + +/// Initialize the frame helper, returns OK if successful. +APIError APINoiseFrameHelper::init() { + APIError err = init_common_(); + if (err != APIError::OK) { + return err; + } + + // init prologue + prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); + + state_ = State::CLIENT_HELLO; + return APIError::OK; +} +// Helper for handling handshake frame errors +APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { + if (aerr == APIError::BAD_INDICATOR) { + send_explicit_handshake_reject_("Bad indicator byte"); + } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { + send_explicit_handshake_reject_("Bad handshake packet len"); + } + return aerr; +} + +// Helper for handling noise library errors +APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); + return api_err; + } + return APIError::OK; +} + +/// Run through handshake messages (if in that phase) +APIError APINoiseFrameHelper::loop() { + // During handshake phase, process as many actions as possible until we can't progress + // socket_->ready() stays true until next main loop, but state_action() will return + // WOULD_BLOCK when no more data is available to read + while (state_ != State::DATA && this->socket_->ready()) { + APIError err = state_action_(); + if (err == APIError::WOULD_BLOCK) { + break; + } + if (err != APIError::OK) { + return err; + } + } + + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg_start: points to the start of the payload - this pointer is only valid until the next + * try_receive_raw_ call + * + * @return 0 if a full packet is in rx_buf_ + * @return -1 if error, check errno. + * + * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. + * errno ENOMEM: Not enough memory for reading packet. + * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. + */ +APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + if (rx_header_buf_len_ < 3) { + // no header information yet + uint8_t to_read = 3 - rx_header_buf_len_; + ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; + } + rx_header_buf_len_ += static_cast(received); + if (static_cast(received) != to_read) { + // not a full read + return APIError::WOULD_BLOCK; + } + + if (rx_header_buf_[0] != 0x01) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } + // header reading done + } + + // read body + uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; + + if (state_ != State::DATA && msg_size > 128) { + // for handshake message only permit up to 128 bytes + state_ = State::FAILED; + HELPER_LOG("Bad packet len for handshake: %d", msg_size); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } + + // reserve space for body + if (rx_buf_.size() != msg_size) { + rx_buf_.resize(msg_size); + } + + if (rx_buf_len_ < msg_size) { + // more data to read + uint16_t to_read = msg_size - rx_buf_len_; + ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; + } + rx_buf_len_ += static_cast(received); + if (static_cast(received) != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + LOG_PACKET_RECEIVED(rx_buf_); + *frame = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_len_ = 0; + return APIError::OK; +} + +/** To be called from read/write methods. + * + * This method runs through the internal handshake methods, if in that state. + * + * If the handshake is still active when this method returns and a read/write can't take place at + * the moment, returns WOULD_BLOCK. + * If an error occurred, returns that error. Only returns OK if the transport is ready for data + * traffic. + */ +APIError APINoiseFrameHelper::state_action_() { + int err; + APIError aerr; + if (state_ == State::INITIALIZE) { + HELPER_LOG("Bad state for method: %d", (int) state_); + return APIError::BAD_STATE; + } + if (state_ == State::CLIENT_HELLO) { + // waiting for client hello + std::vector frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) { + return handle_handshake_frame_error_(aerr); + } + // ignore contents, may be used in future for flags + // Reserve space for: existing prologue + 2 size bytes + frame data + prologue_.reserve(prologue_.size() + 2 + frame.size()); + prologue_.push_back((uint8_t) (frame.size() >> 8)); + prologue_.push_back((uint8_t) frame.size()); + prologue_.insert(prologue_.end(), frame.begin(), frame.end()); + + state_ = State::SERVER_HELLO; + } + if (state_ == State::SERVER_HELLO) { + // send server hello + const std::string &name = App.get_name(); + const std::string &mac = get_mac_address(); + + std::vector msg; + // Reserve space for: 1 byte proto + name + null + mac + null + msg.reserve(1 + name.size() + 1 + mac.size() + 1); + + // chosen proto + msg.push_back(0x01); + + // node name, terminated by null byte + const uint8_t *name_ptr = reinterpret_cast(name.c_str()); + msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); + // node mac, terminated by null byte + const uint8_t *mac_ptr = reinterpret_cast(mac.c_str()); + msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); + + aerr = write_frame_(msg.data(), msg.size()); + if (aerr != APIError::OK) + return aerr; + + // start handshake + aerr = init_handshake_(); + if (aerr != APIError::OK) + return aerr; + + state_ = State::HANDSHAKE; + } + if (state_ == State::HANDSHAKE) { + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE) { + // waiting for handshake msg + std::vector frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) { + return handle_handshake_frame_error_(aerr); + } + + if (frame.empty()) { + send_explicit_handshake_reject_("Empty handshake message"); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } else if (frame[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", frame[0]); + send_explicit_handshake_reject_("Bad handshake error byte"); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1); + err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); + if (err != 0) { + // Special handling for MAC failure + send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); + return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); + } + + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else if (action == NOISE_ACTION_WRITE_MESSAGE) { + uint8_t buffer[65]; + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); + + err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); + APIError aerr_write = + handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); + if (aerr_write != APIError::OK) + return aerr_write; + buffer[0] = 0x00; // success + + aerr = write_frame_(buffer, mbuf.size + 1); + if (aerr != APIError::OK) + return aerr; + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else { + // bad state for action + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + } + if (state_ == State::CLOSED || state_ == State::FAILED) { + return APIError::BAD_STATE; + } + return APIError::OK; +} +void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { + std::vector data; + data.resize(reason.length() + 1); + data[0] = 0x01; // failure + + // Copy error message in bulk + if (!reason.empty()) { + std::memcpy(data.data() + 1, reason.c_str(), reason.length()); + } + + // temporarily remove failed state + auto orig_state = state_; + state_ = State::EXPLICIT_REJECT; + write_frame_(data.data(), data.size()); + state_ = orig_state; +} +APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { + int err; + APIError aerr; + aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + std::vector frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); + err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); + APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); + if (decrypt_err != APIError::OK) + return decrypt_err; + + uint16_t msg_size = mbuf.size; + uint8_t *msg_data = frame.data(); + if (msg_size < 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: size %d too short", msg_size); + return APIError::BAD_DATA_PACKET; + } + + uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; + uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; + if (data_len > msg_size - 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); + return APIError::BAD_DATA_PACKET; + } + + buffer->container = std::move(frame); + buffer->data_offset = 4; + buffer->data_len = data_len; + buffer->type = type; + return APIError::OK; +} +APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { + // Resize to include MAC space (required for Noise encryption) + buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); + PacketInfo packet{type, 0, + static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + return write_protobuf_packets(buffer, std::span(&packet, 1)); +} + +APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { + APIError aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + if (packets.empty()) { + return APIError::OK; + } + + std::vector *raw_buffer = buffer.get_buffer(); + uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + + this->reusable_iovs_.clear(); + this->reusable_iovs_.reserve(packets.size()); + uint16_t total_write_len = 0; + + // We need to encrypt each packet in place + for (const auto &packet : packets) { + // The buffer already has padding at offset + uint8_t *buf_start = buffer_data + packet.offset; + + // Write noise header + buf_start[0] = 0x01; // indicator + // buf_start[1], buf_start[2] to be set after encryption + + // Write message header (to be encrypted) + const uint8_t msg_offset = 3; + buf_start[msg_offset] = static_cast(packet.message_type >> 8); // type high byte + buf_start[msg_offset + 1] = static_cast(packet.message_type); // type low byte + buf_start[msg_offset + 2] = static_cast(packet.payload_size >> 8); // data_len high byte + buf_start[msg_offset + 3] = static_cast(packet.payload_size); // data_len low byte + // payload data is already in the buffer starting at offset + 7 + + // Make sure we have space for MAC + // The buffer should already have been sized appropriately + + // Encrypt the message in place + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size, + 4 + packet.payload_size + frame_footer_size_); + + int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); + APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); + if (aerr != APIError::OK) + return aerr; + + // Fill in the encrypted size + buf_start[1] = static_cast(mbuf.size >> 8); + buf_start[2] = static_cast(mbuf.size); + + // Add iovec for this encrypted packet + size_t packet_len = static_cast(3 + mbuf.size); // indicator + size + encrypted data + this->reusable_iovs_.push_back({buf_start, packet_len}); + total_write_len += packet_len; + } + + // Send all encrypted packets in one writev call + return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); +} + +APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { + uint8_t header[3]; + header[0] = 0x01; // indicator + header[1] = (uint8_t) (len >> 8); + header[2] = (uint8_t) len; + + struct iovec iov[2]; + iov[0].iov_base = header; + iov[0].iov_len = 3; + if (len == 0) { + return this->write_raw_(iov, 1, 3); // Just header + } + iov[1].iov_base = const_cast(data); + iov[1].iov_len = len; + + return this->write_raw_(iov, 2, 3 + len); // Header + data +} + +/** Initiate the data structures for the handshake. + * + * @return 0 on success, -1 on error (check errno) + */ +APIError APINoiseFrameHelper::init_handshake_() { + int err; + memset(&nid_, 0, sizeof(nid_)); + // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); + nid_.pattern_id = NOISE_PATTERN_NN; + nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; + nid_.dh_id = NOISE_DH_CURVE25519; + nid_.prefix_id = NOISE_PREFIX_STANDARD; + nid_.hybrid_id = NOISE_DH_NONE; + nid_.hash_id = NOISE_HASH_SHA256; + nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; + + err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); + APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; + + const auto &psk = ctx_->get_psk(); + err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); + aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; + + err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); + aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; + // set_prologue copies it into handshakestate, so we can get rid of it now + prologue_ = {}; + + err = noise_handshakestate_start(handshake_); + aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; + return APIError::OK; +} + +APIError APINoiseFrameHelper::check_handshake_finished_() { + assert(state_ == State::HANDSHAKE); + + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) + return APIError::OK; + if (action != NOISE_ACTION_SPLIT) { + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); + APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); + if (aerr != APIError::OK) + return aerr; + + frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); + + HELPER_LOG("Handshake complete!"); + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + state_ = State::DATA; + return APIError::OK; +} + +APINoiseFrameHelper::~APINoiseFrameHelper() { + if (handshake_ != nullptr) { + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + } + if (send_cipher_ != nullptr) { + noise_cipherstate_free(send_cipher_); + send_cipher_ = nullptr; + } + if (recv_cipher_ != nullptr) { + noise_cipherstate_free(recv_cipher_); + recv_cipher_ = nullptr; + } +} + +extern "C" { +// declare how noise generates random bytes (here with a good HWRNG based on the RF system) +void noise_rand_bytes(void *output, size_t len) { + if (!esphome::random_bytes(reinterpret_cast(output), len)) { + ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting"); + arch_restart(); + } +} +} + +} // namespace api +} // namespace esphome +#endif // USE_API_NOISE +#endif // USE_API diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h new file mode 100644 index 0000000000..ed5141d625 --- /dev/null +++ b/esphome/components/api/api_frame_helper_noise.h @@ -0,0 +1,70 @@ +#pragma once +#include "api_frame_helper.h" +#ifdef USE_API +#ifdef USE_API_NOISE +#include "noise/protocol.h" +#include "api_noise_context.h" + +namespace esphome { +namespace api { + +class APINoiseFrameHelper : public APIFrameHelper { + public: + APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, + const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { + // Noise header structure: + // Pos 0: indicator (0x01) + // Pos 1-2: encrypted payload size (16-bit big-endian) + // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) + // Pos 7+: actual payload data + frame_header_padding_ = 7; + } + ~APINoiseFrameHelper() override; + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; + APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; + // Get the frame header padding required by this protocol + uint8_t frame_header_padding() override { return frame_header_padding_; } + // Get the frame footer size required by this protocol + uint8_t frame_footer_size() override { return frame_footer_size_; } + + protected: + APIError state_action_(); + APIError try_read_frame_(std::vector *frame); + APIError write_frame_(const uint8_t *data, uint16_t len); + APIError init_handshake_(); + APIError check_handshake_finished_(); + void send_explicit_handshake_reject_(const std::string &reason); + APIError handle_handshake_frame_error_(APIError aerr); + APIError handle_noise_error_(int err, const char *func_name, APIError api_err); + + // Pointers first (4 bytes each) + NoiseHandshakeState *handshake_{nullptr}; + NoiseCipherState *send_cipher_{nullptr}; + NoiseCipherState *recv_cipher_{nullptr}; + + // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) + std::shared_ptr ctx_; + + // Vector (12 bytes on 32-bit) + std::vector prologue_; + + // NoiseProtocolId (size depends on implementation) + NoiseProtocolId nid_; + + // Group small types together + // Fixed-size header buffer for noise protocol: + // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) + // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase + uint8_t rx_header_buf_[3]; + uint8_t rx_header_buf_len_ = 0; + // 4 bytes total, no padding +}; + +} // namespace api +} // namespace esphome +#endif // USE_API_NOISE +#endif // USE_API diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp new file mode 100644 index 0000000000..d0bc631e1b --- /dev/null +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -0,0 +1,292 @@ +#include "api_frame_helper_plaintext.h" +#ifdef USE_API +#ifdef USE_API_PLAINTEXT +#include "api_connection.h" // For ClientInfo struct +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "proto.h" +#include +#include + +namespace esphome { +namespace api { + +static const char *const TAG = "api.plaintext"; + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) + +#ifdef HELPER_LOG_PACKETS +#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) +#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#else +#define LOG_PACKET_RECEIVED(buffer) ((void) 0) +#define LOG_PACKET_SENDING(data, len) ((void) 0) +#endif + +/// Initialize the frame helper, returns OK if successful. +APIError APIPlaintextFrameHelper::init() { + APIError err = init_common_(); + if (err != APIError::OK) { + return err; + } + + state_ = State::DATA; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::loop() { + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg: store the parsed frame in that struct + * + * @return See APIError + * + * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + */ +APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + while (!rx_header_parsed_) { + // Now that we know when the socket is ready, we can read up to 3 bytes + // into the rx_header_buf_ before we have to switch back to reading + // one byte at a time to ensure we don't read past the message and + // into the next one. + + // Read directly into rx_header_buf_ at the current position + // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time + ssize_t received = + this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; + } + + // If this was the first read, validate the indicator byte + if (rx_header_buf_pos_ == 0 && received > 0) { + if (rx_header_buf_[0] != 0x00) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } + } + + rx_header_buf_pos_ += received; + + // Check for buffer overflow + if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) { + state_ = State::FAILED; + HELPER_LOG("Header buffer overflow"); + return APIError::BAD_DATA_PACKET; + } + + // Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse + if (rx_header_buf_pos_ < 3) { + continue; + } + + // At this point, we have at least 3 bytes total: + // - Validated indicator byte (0x00) stored at position 0 + // - At least 2 bytes in the buffer for the varints + // Buffer layout: + // [0]: indicator byte (0x00) + // [1-3]: Message size varint (variable length) + // - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535) + // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise + // [2-5]: Message type varint (variable length) + // We now attempt to parse both varints. If either is incomplete, + // we'll continue reading more bytes. + + // Skip indicator byte at position 0 + uint8_t varint_pos = 1; + uint32_t consumed = 0; + + auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + if (!msg_size_varint.has_value()) { + // not enough data there yet + continue; + } + + if (msg_size_varint->as_uint32() > std::numeric_limits::max()) { + state_ = State::FAILED; + HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), + std::numeric_limits::max()); + return APIError::BAD_DATA_PACKET; + } + rx_header_parsed_len_ = msg_size_varint->as_uint16(); + + // Move to next varint position + varint_pos += consumed; + + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + if (!msg_type_varint.has_value()) { + // not enough data there yet + continue; + } + if (msg_type_varint->as_uint32() > std::numeric_limits::max()) { + state_ = State::FAILED; + HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), + std::numeric_limits::max()); + return APIError::BAD_DATA_PACKET; + } + rx_header_parsed_type_ = msg_type_varint->as_uint16(); + rx_header_parsed_ = true; + } + // header reading done + + // reserve space for body + if (rx_buf_.size() != rx_header_parsed_len_) { + rx_buf_.resize(rx_header_parsed_len_); + } + + if (rx_buf_len_ < rx_header_parsed_len_) { + // more data to read + uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; + ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; + } + rx_buf_len_ += static_cast(received); + if (static_cast(received) != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + LOG_PACKET_RECEIVED(rx_buf_); + *frame = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_pos_ = 0; + rx_header_parsed_ = false; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { + APIError aerr; + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + std::vector frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) { + if (aerr == APIError::BAD_INDICATOR) { + // Make sure to tell the remote that we don't + // understand the indicator byte so it knows + // we do not support it. + struct iovec iov[1]; + // The \x00 first byte is the marker for plaintext. + // + // The remote will know how to handle the indicator byte, + // but it likely won't understand the rest of the message. + // + // We must send at least 3 bytes to be read, so we add + // a message after the indicator byte to ensures its long + // enough and can aid in debugging. + const char msg[] = "\x00" + "Bad indicator byte"; + iov[0].iov_base = (void *) msg; + iov[0].iov_len = 19; + this->write_raw_(iov, 1, 19); + } + return aerr; + } + + buffer->container = std::move(frame); + buffer->data_offset = 0; + buffer->data_len = rx_header_parsed_len_; + buffer->type = rx_header_parsed_type_; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { + PacketInfo packet{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; + return write_protobuf_packets(buffer, std::span(&packet, 1)); +} + +APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + + if (packets.empty()) { + return APIError::OK; + } + + std::vector *raw_buffer = buffer.get_buffer(); + uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + + this->reusable_iovs_.clear(); + this->reusable_iovs_.reserve(packets.size()); + uint16_t total_write_len = 0; + + for (const auto &packet : packets) { + // Calculate varint sizes for header layout + uint8_t size_varint_len = api::ProtoSize::varint(static_cast(packet.payload_size)); + uint8_t type_varint_len = api::ProtoSize::varint(static_cast(packet.message_type)); + uint8_t total_header_len = 1 + size_varint_len + type_varint_len; + + // Calculate where to start writing the header + // The header starts at the latest possible position to minimize unused padding + // + // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 + // [0-2] - Unused padding + // [3] - 0x00 indicator byte + // [4] - Payload size varint (1 byte, for sizes 0-127) + // [5] - Message type varint (1 byte, for types 0-127) + // [6...] - Actual payload data + // + // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 + // [0-1] - Unused padding + // [2] - 0x00 indicator byte + // [3-4] - Payload size varint (2 bytes, for sizes 128-16383) + // [5] - Message type varint (1 byte, for types 0-127) + // [6...] - Actual payload data + // + // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 + // [0] - 0x00 indicator byte + // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) + // [4-5] - Message type varint (2 bytes, for types 128-32767) + // [6...] - Actual payload data + // + // The message starts at offset + frame_header_padding_ + // So we write the header starting at offset + frame_header_padding_ - total_header_len + uint8_t *buf_start = buffer_data + packet.offset; + uint32_t header_offset = frame_header_padding_ - total_header_len; + + // Write the plaintext header + buf_start[header_offset] = 0x00; // indicator + + // Encode varints directly into buffer + ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); + ProtoVarInt(packet.message_type) + .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + + // Add iovec for this packet (header + payload) + size_t packet_len = static_cast(total_header_len + packet.payload_size); + this->reusable_iovs_.push_back({buf_start + header_offset, packet_len}); + total_write_len += packet_len; + } + + // Send all packets in one writev call + return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); +} + +} // namespace api +} // namespace esphome +#endif // USE_API_PLAINTEXT +#endif // USE_API diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h new file mode 100644 index 0000000000..465ceae827 --- /dev/null +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -0,0 +1,55 @@ +#pragma once +#include "api_frame_helper.h" +#ifdef USE_API +#ifdef USE_API_PLAINTEXT + +namespace esphome { +namespace api { + +class APIPlaintextFrameHelper : public APIFrameHelper { + public: + APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info) { + // Plaintext header structure (worst case): + // Pos 0: indicator (0x00) + // Pos 1-3: payload size varint (up to 3 bytes) + // Pos 4-5: message type varint (up to 2 bytes) + // Pos 6+: actual payload data + frame_header_padding_ = 6; + } + ~APIPlaintextFrameHelper() override = default; + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; + APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; + uint8_t frame_header_padding() override { return frame_header_padding_; } + // Get the frame footer size required by this protocol + uint8_t frame_footer_size() override { return frame_footer_size_; } + + protected: + APIError try_read_frame_(std::vector *frame); + + // Group 2-byte aligned types + uint16_t rx_header_parsed_type_ = 0; + uint16_t rx_header_parsed_len_ = 0; + + // Group 1-byte types together + // Fixed-size header buffer for plaintext protocol: + // We now store the indicator byte + the two varints. + // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: + // 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint + // + // While varints could theoretically be up to 10 bytes each for 64-bit values, + // attempting to process messages with headers that large would likely crash the + // ESP32 due to memory constraints. + uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) + uint8_t rx_header_buf_pos_ = 0; + bool rx_header_parsed_ = false; + // 8 bytes total, no padding needed +}; + +} // namespace api +} // namespace esphome +#endif // USE_API_PLAINTEXT +#endif // USE_API From 9508871474c20eb9d03ee852b0af98ec623783e6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:20:02 +1200 Subject: [PATCH 040/125] [CI] Fix codeowner workflow requesting the same multiple times (#9750) --- .github/workflows/codeowner-review-request.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 9a0b43a51d..121619e049 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -34,6 +34,9 @@ jobs: console.log(`Processing PR #${pr_number} for codeowner review requests`); + // Hidden marker to identify bot comments from this workflow + const BOT_COMMENT_MARKER = ''; + try { // Get the list of changed files in this PR const { data: files } = await github.rest.pulls.listFiles({ @@ -84,9 +87,9 @@ jobs: const allMentions = [...reviewerMentions, ...teamMentions].join(', '); if (isSuccessful) { - return `👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`; + return `${BOT_COMMENT_MARKER}\n👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`; } else { - return `👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`; + return `${BOT_COMMENT_MARKER}\n👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`; } } @@ -188,11 +191,11 @@ jobs: const previouslyPingedUsers = new Set(); const previouslyPingedTeams = new Set(); - // Look for comments from github-actions bot that contain codeowner pings + // Look for comments from github-actions bot that contain our bot marker const workflowComments = comments.filter(comment => comment.user.type === 'Bot' && comment.user.login === 'github-actions[bot]' && - comment.body.includes("I've automatically requested reviews from codeowners") + comment.body.includes(BOT_COMMENT_MARKER) ); // Extract previously mentioned users and teams from workflow comments From a8d53b7c686f890b21d7bfe8256bbe4cb6a41a25 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:33:20 +1200 Subject: [PATCH 041/125] [CI] Use comment marker in too-big reviews (#9751) --- .github/workflows/auto-label-pr.yml | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 4dfd315349..83121ff50c 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -14,7 +14,6 @@ env: SMALL_PR_THRESHOLD: 30 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 - BOT_NAME: "esphome[bot]" jobs: label: @@ -59,6 +58,9 @@ jobs: const { owner, repo } = context.repo; const pr_number = context.issue.number; + // Hidden marker to identify bot comments from this workflow + const BOT_COMMENT_MARKER = ''; + // Get current labels const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({ owner, @@ -123,7 +125,6 @@ jobs: const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); const maxLabels = parseInt('${{ env.MAX_LABELS }}'); const tooBigThreshold = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); - const botName = process.env.BOT_NAME; // Strategy: Merge to release or beta branch const baseRef = context.payload.pull_request.base.ref; @@ -401,11 +402,11 @@ jobs: // Create appropriate review message let reviewBody; if (tooManyLabels && tooManyChanges) { - reviewBody = `This PR is too large with ${totalChanges} line changes and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${totalChanges} line changes and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; } else if (tooManyLabels) { - reviewBody = `This PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; } else { - reviewBody = `This PR is too large with ${totalChanges} line changes. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${totalChanges} line changes. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; } // Request changes on the PR @@ -432,13 +433,9 @@ jobs: // Find bot reviews that requested changes const botReviews = reviews.filter(review => - review.user.login === botName && + review.user.type === 'Bot' && review.state === 'CHANGES_REQUESTED' && - review.body && ( - review.body.includes('This PR is too large') || - review.body.includes('This PR affects') || - review.body.includes('different components/areas') - ) + review.body && review.body.includes(BOT_COMMENT_MARKER) ); // Dismiss bot reviews From c60fe4c372ce4d316078c74bc42952ed8f001b21 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:59:25 +1200 Subject: [PATCH 042/125] [CI] Dont create new review if existing and dont count tests (#9753) --- .github/workflows/auto-label-pr.yml | 68 ++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 83121ff50c..b30f6cf28a 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -201,6 +201,14 @@ jobs: const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); + // Calculate changes excluding root tests directory for too-big calculation + const testChanges = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + + const nonTestChanges = totalChanges - testChanges; + console.log(`Test changes: ${testChanges}, Non-test changes: ${nonTestChanges}`); + // Strategy: New Component detection for (const file of addedFiles) { // Check for new component files: esphome/components/{component}/__init__.py @@ -385,11 +393,25 @@ jobs: // Check if PR is too big (either too many labels or too many line changes) const tooManyLabels = finalLabels.length > maxLabels; - const tooManyChanges = totalChanges > tooBigThreshold; + const tooManyChanges = nonTestChanges > tooBigThreshold; if ((tooManyLabels || tooManyChanges) && !isMegaPR) { const originalLength = finalLabels.length; - console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges}`); + console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges} (non-test: ${nonTestChanges})`); + + // Get all reviews on this PR to check for existing bot reviews + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + // Check if there's already an active bot review requesting changes + const existingBotReview = reviews.find(review => + review.user.type === 'Bot' && + review.state === 'CHANGES_REQUESTED' && + review.body && review.body.includes(BOT_COMMENT_MARKER) + ); // If too big due to line changes only, keep original labels and add too-big // If too big due to too many labels, replace with just too-big @@ -399,24 +421,30 @@ jobs: finalLabels = ['too-big']; } - // Create appropriate review message - let reviewBody; - if (tooManyLabels && tooManyChanges) { - reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${totalChanges} line changes and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; - } else if (tooManyLabels) { - reviewBody = `${BOT_COMMENT_MARKER}\nThis PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; - } else { - reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${totalChanges} line changes. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; - } + // Only create a new review if there isn't already an active bot review + if (!existingBotReview) { + // Create appropriate review message + let reviewBody; + if (tooManyLabels && tooManyChanges) { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } else if (tooManyLabels) { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } else { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${nonTestChanges} line changes (excluding tests). Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } - // Request changes on the PR - await github.rest.pulls.createReview({ - owner, - repo, - pull_number: pr_number, - body: reviewBody, - event: 'REQUEST_CHANGES' - }); + // Request changes on the PR + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pr_number, + body: reviewBody, + event: 'REQUEST_CHANGES' + }); + console.log('Created new "too big" review requesting changes'); + } else { + console.log('Skipping review creation - existing bot review already requesting changes'); + } } else { // Check if PR was previously too big but is now acceptable const wasPreviouslyTooBig = currentLabels.includes('too-big'); @@ -424,7 +452,7 @@ jobs: if (wasPreviouslyTooBig || isMegaPR) { console.log('PR is no longer too big or has mega-pr label - dismissing bot reviews'); - // Get all reviews on this PR + // Get all reviews on this PR to find reviews to dismiss const { data: reviews } = await github.rest.pulls.listReviews({ owner, repo, From fc286c8bf4553df23f4912acecb3847a00730201 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 16:20:54 -1000 Subject: [PATCH 043/125] Bump aioesphomeapi from 37.0.2 to 37.0.3 (#9754) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6cc821e74c..69d40587ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==37.0.2 +aioesphomeapi==37.0.3 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import From 305667b06dcdc254fdfb93a6ef3354bc4cf34a5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 18:59:48 -1000 Subject: [PATCH 044/125] [api] Sync uses_password field_ifdef optimization from aioesphomeapi (#9756) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 2 -- esphome/components/api/api_pb2.cpp | 4 ++++ esphome/components/api/api_pb2.h | 2 ++ esphome/components/api/api_pb2_dump.cpp | 2 ++ 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 546c498ff3..fd08e87bbf 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -203,7 +203,7 @@ message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; - bool uses_password = 1; + bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"]; // The name of the node, given by "App.set_name()" string name = 2; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 602a0256cf..bc0afd49eb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1432,8 +1432,6 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; #ifdef USE_API_PASSWORD resp.uses_password = true; -#else - resp.uses_password = false; #endif resp.name = App.get_name(); resp.friendly_name = App.get_friendly_name(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4cf4b63269..528c581ad7 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -80,7 +80,9 @@ void DeviceInfo::calculate_size(uint32_t &total_size) const { } #endif void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +#ifdef USE_API_PASSWORD buffer.encode_bool(1, this->uses_password); +#endif buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); buffer.encode_string(4, this->esphome_version); @@ -130,7 +132,9 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #endif } void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { +#ifdef USE_API_PASSWORD ProtoSize::add_bool_field(total_size, 1, this->uses_password); +#endif ProtoSize::add_string_field(total_size, 1, this->name); ProtoSize::add_string_field(total_size, 1, this->mac_address); ProtoSize::add_string_field(total_size, 1, this->esphome_version); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e241451ec8..7b64bd889f 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -474,7 +474,9 @@ class DeviceInfoResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif +#ifdef USE_API_PASSWORD bool uses_password{false}; +#endif std::string name{}; std::string mac_address{}; std::string esphome_version{}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index bda5ec5764..4951c6cebf 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -647,10 +647,12 @@ void DeviceInfo::dump_to(std::string &out) const { void DeviceInfoResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DeviceInfoResponse {\n"); +#ifdef USE_API_PASSWORD out.append(" uses_password: "); out.append(YESNO(this->uses_password)); out.append("\n"); +#endif out.append(" name: "); out.append("'").append(this->name).append("'"); out.append("\n"); From fe1050a583f76f5928b14dfb350d734265acb6d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 19:21:51 -1000 Subject: [PATCH 045/125] [tests] Fix flaky scheduler retry test timing (#9760) --- tests/integration/fixtures/scheduler_retry_test.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml index bae50e9ed7..1b6ee8e5c2 100644 --- a/tests/integration/fixtures/scheduler_retry_test.yaml +++ b/tests/integration/fixtures/scheduler_retry_test.yaml @@ -130,15 +130,15 @@ script: - logger.log: "=== Test 5: Empty name retry ===" - lambda: |- auto *component = id(test_sensor); - App.scheduler.set_retry(component, "", 50, 5, + App.scheduler.set_retry(component, "", 100, 5, [](uint8_t retry_countdown) { id(empty_name_retry_counter)++; ESP_LOGI("test", "Empty name retry attempt %d", id(empty_name_retry_counter)); return RetryResult::RETRY; }); - // Try to cancel after 75ms - App.scheduler.set_timeout(component, "empty_cancel_timer", 75, []() { + // Try to cancel after 150ms + App.scheduler.set_timeout(component, "empty_cancel_timer", 150, []() { bool cancelled = App.scheduler.cancel_retry(id(test_sensor), ""); ESP_LOGI("test", "Empty name retry cancel result: %s", cancelled ? "true" : "false"); From 5fed7087614eebdeacd0a3dba35ef9b8265cba3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 21:41:18 -1000 Subject: [PATCH 046/125] Bump aioesphomeapi from 37.0.3 to 37.0.4 (#9764) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 69d40587ec..cc69186e49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==37.0.3 +aioesphomeapi==37.0.4 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import From e485895d97d8e43086157a9b880662414d9b3b33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Jul 2025 22:26:20 -1000 Subject: [PATCH 047/125] [bluetooth_proxy] Optimize service discovery with in-place construction (#9765) --- .../bluetooth_proxy/bluetooth_connection.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index dae6e521bb..85380fa486 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -95,8 +95,8 @@ void BluetoothConnection::send_service_for_discovery_() { api::BluetoothGATTGetServicesResponse resp; resp.address = this->address_; - resp.services.reserve(1); // Always one service per response in this implementation - api::BluetoothGATTService service_resp; + resp.services.emplace_back(); + auto &service_resp = resp.services.back(); service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); service_resp.handle = service_result.start_handle; @@ -134,7 +134,8 @@ void BluetoothConnection::send_service_for_discovery_() { break; } - api::BluetoothGATTCharacteristic characteristic_resp; + service_resp.characteristics.emplace_back(); + auto &characteristic_resp = service_resp.characteristics.back(); characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); characteristic_resp.handle = char_result.char_handle; characteristic_resp.properties = char_result.properties; @@ -173,15 +174,13 @@ void BluetoothConnection::send_service_for_discovery_() { break; } - api::BluetoothGATTDescriptor descriptor_resp; + characteristic_resp.descriptors.emplace_back(); + auto &descriptor_resp = characteristic_resp.descriptors.back(); descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); descriptor_resp.handle = desc_result.handle; - characteristic_resp.descriptors.push_back(std::move(descriptor_resp)); desc_offset++; } - service_resp.characteristics.push_back(std::move(characteristic_resp)); } - resp.services.push_back(std::move(service_resp)); // Send the message (we already checked api_conn is not null at the beginning) api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); From 16a426c182599f0e3166d7e7009cab7bc58ca263 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Mon, 21 Jul 2025 04:28:11 -0400 Subject: [PATCH 048/125] Factor PlatformIO buildgen out of writer.py (#9378) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/__main__.py | 11 +++- esphome/build_gen/__init__.py | 0 esphome/build_gen/platformio.py | 102 ++++++++++++++++++++++++++++++ esphome/core/__init__.py | 106 ++++++++++++++++++-------------- esphome/writer.py | 98 ----------------------------- tests/unit_tests/test_core.py | 55 +++++++++++++++++ 6 files changed, 226 insertions(+), 146 deletions(-) create mode 100644 esphome/build_gen/__init__.py create mode 100644 esphome/build_gen/platformio.py diff --git a/esphome/__main__.py b/esphome/__main__.py index d8a79c018a..658aef4722 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -34,6 +34,7 @@ from esphome.const import ( CONF_PORT, CONF_SUBSTITUTIONS, CONF_TOPIC, + ENV_NOGITIGNORE, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -209,6 +210,9 @@ def wrap_to_code(name, comp): def write_cpp(config): + if not get_bool_env(ENV_NOGITIGNORE): + writer.write_gitignore() + generate_cpp_contents(config) return write_cpp_file() @@ -225,10 +229,13 @@ def generate_cpp_contents(config): def write_cpp_file(): - writer.write_platformio_project() - code_s = indent(CORE.cpp_main_section) writer.write_cpp(code_s) + + from esphome.build_gen import platformio + + platformio.write_project() + return 0 diff --git a/esphome/build_gen/__init__.py b/esphome/build_gen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/build_gen/platformio.py b/esphome/build_gen/platformio.py new file mode 100644 index 0000000000..9bbe86694b --- /dev/null +++ b/esphome/build_gen/platformio.py @@ -0,0 +1,102 @@ +import os + +from esphome.const import __version__ +from esphome.core import CORE +from esphome.helpers import mkdir_p, read_file, write_file_if_changed +from esphome.writer import find_begin_end, update_storage_json + +INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ===========" +INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============" + +INI_BASE_FORMAT = ( + """; Auto generated code by esphome + +[common] +lib_deps = +build_flags = +upload_flags = + +""", + """ + +""", +) + + +def format_ini(data: dict[str, str | list[str]]) -> str: + content = "" + for key, value in sorted(data.items()): + if isinstance(value, list): + content += f"{key} =\n" + for x in value: + content += f" {x}\n" + else: + content += f"{key} = {value}\n" + return content + + +def get_ini_content(): + CORE.add_platformio_option( + "lib_deps", + [x.as_lib_dep for x in CORE.platformio_libraries.values()] + + ["${common.lib_deps}"], + ) + # Sort to avoid changing build flags order + CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) + + # Sort to avoid changing build unflags order + CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) + + # Add extra script for C++ flags + CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) + + content = "[platformio]\n" + content += f"description = ESPHome {__version__}\n" + + content += f"[env:{CORE.name}]\n" + content += format_ini(CORE.platformio_options) + + return content + + +def write_ini(content): + update_storage_json() + path = CORE.relative_build_path("platformio.ini") + + if os.path.isfile(path): + text = read_file(path) + content_format = find_begin_end( + text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END + ) + else: + content_format = INI_BASE_FORMAT + full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}" + full_file += INI_AUTO_GENERATE_END + content_format[1] + write_file_if_changed(path, full_file) + + +def write_project(): + mkdir_p(CORE.build_path) + + content = get_ini_content() + write_ini(content) + + # Write extra script for C++ specific flags + write_cxx_flags_script() + + +CXX_FLAGS_FILE_NAME = "cxx_flags.py" +CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags +Import("env") + +# Add C++ specific flags +""" + + +def write_cxx_flags_script() -> None: + path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) + contents = CXX_FLAGS_FILE_CONTENTS + if not CORE.is_host: + contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' + contents += "\n" + write_file_if_changed(path, contents) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 5ce2ed5caf..472067797e 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -470,6 +470,52 @@ class Library: return self.as_tuple == other.as_tuple return NotImplemented + def reconcile_with(self, other): + """Merge two libraries, reconciling any conflicts.""" + + if self.name != other.name: + # Different libraries, no reconciliation possible + raise ValueError( + f"Cannot reconcile libraries with different names: {self.name} and {other.name}" + ) + + # repository specificity takes precedence over version specificity + if self.repository is None and other.repository is None: + pass # No repositories, no conflict, continue on + + elif self.repository is None: + # incoming library has a repository, use it + self.repository = other.repository + self.version = other.version + return self + + elif other.repository is None: + return self # use the repository/version already present + + elif self.repository != other.repository: + raise ValueError( + f"Reconciliation failed! Libraries {self} and {other} requested with conflicting repositories!" + ) + + if self.version is None and other.version is None: + return self # Arduino library reconciled against another Arduino library, current is acceptable + + if self.version is None: + # incoming library has a version, use it + self.version = other.version + return self + + if other.version is None: + return self # incoming library has no version, current is acceptable + + # Same versions, current library is acceptable + if self.version != other.version: + raise ValueError( + f"Version pinning failed! Libraries {other} and {self} " + "requested with conflicting versions!" + ) + return self + # pylint: disable=too-many-public-methods class EsphomeCore: @@ -505,8 +551,8 @@ class EsphomeCore: self.main_statements: list[Statement] = [] # A list of statements to insert in the global block (includes and global variables) self.global_statements: list[Statement] = [] - # A set of platformio libraries to add to the project - self.libraries: list[Library] = [] + # A map of platformio libraries to add to the project (shortname: (name, version, repository)) + self.platformio_libraries: dict[str, Library] = {} # A set of build flags to set in the platformio project self.build_flags: set[str] = set() # A set of build unflags to set in the platformio project @@ -550,7 +596,7 @@ class EsphomeCore: self.variables = {} self.main_statements = [] self.global_statements = [] - self.libraries = [] + self.platformio_libraries = {} self.build_flags = set() self.build_unflags = set() self.defines = set() @@ -738,54 +784,22 @@ class EsphomeCore: _LOGGER.debug("Adding global: %s", expression) return expression - def add_library(self, library): + def add_library(self, library: Library): if not isinstance(library, Library): - raise ValueError( + raise TypeError( f"Library {library} must be instance of Library, not {type(library)}" ) - for other in self.libraries[:]: - if other.name is None or library.name is None: - continue - library_name = ( - library.name if "/" not in library.name else library.name.split("/")[1] - ) - other_name = ( - other.name if "/" not in other.name else other.name.split("/")[1] - ) - if other_name != library_name: - continue - if other.repository is not None: - if library.repository is None or other.repository == library.repository: - # Other is using a/the same repository, takes precedence - break - raise ValueError( - f"Adding named Library with repository failed! Libraries {library} and {other} " - "requested with conflicting repositories!" - ) + short_name = ( + library.name if "/" not in library.name else library.name.split("/")[-1] + ) - if library.repository is not None: - # This is more specific since its using a repository - self.libraries.remove(other) - continue - - if library.version is None: - # Other requirement is more specific - break - if other.version is None: - # Found more specific version requirement - self.libraries.remove(other) - continue - if other.version == library.version: - break - - raise ValueError( - f"Version pinning failed! Libraries {library} and {other} " - "requested with conflicting versions!" - ) - else: + if short_name not in self.platformio_libraries: _LOGGER.debug("Adding library: %s", library) - self.libraries.append(library) - return library + self.platformio_libraries[short_name] = library + return library + + self.platformio_libraries[short_name].reconcile_with(library) + return self.platformio_libraries[short_name] def add_build_flag(self, build_flag: str) -> str: self.build_flags.add(build_flag) diff --git a/esphome/writer.py b/esphome/writer.py index 5438e48570..d2ec112ec8 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -7,7 +7,6 @@ import re from esphome import loader from esphome.config import iter_component_configs, iter_components from esphome.const import ( - ENV_NOGITIGNORE, HEADER_FILE_EXTENSIONS, PLATFORM_ESP32, SOURCE_FILE_EXTENSIONS, @@ -16,8 +15,6 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, - get_bool_env, - mkdir_p, read_file, walk_files, write_file_if_changed, @@ -30,8 +27,6 @@ CPP_AUTO_GENERATE_BEGIN = "// ========== AUTO GENERATED CODE BEGIN ===========" CPP_AUTO_GENERATE_END = "// =========== AUTO GENERATED CODE END ============" CPP_INCLUDE_BEGIN = "// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========" CPP_INCLUDE_END = "// ========== AUTO GENERATED INCLUDE BLOCK END ===========" -INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ===========" -INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============" CPP_BASE_FORMAT = ( """// Auto generated code by esphome @@ -50,20 +45,6 @@ void loop() { """, ) -INI_BASE_FORMAT = ( - """; Auto generated code by esphome - -[common] -lib_deps = -build_flags = -upload_flags = - -""", - """ - -""", -) - UPLOAD_SPEED_OVERRIDE = { "esp210": 57600, } @@ -140,40 +121,6 @@ def update_storage_json(): new.save(path) -def format_ini(data: dict[str, str | list[str]]) -> str: - content = "" - for key, value in sorted(data.items()): - if isinstance(value, list): - content += f"{key} =\n" - for x in value: - content += f" {x}\n" - else: - content += f"{key} = {value}\n" - return content - - -def get_ini_content(): - CORE.add_platformio_option( - "lib_deps", [x.as_lib_dep for x in CORE.libraries] + ["${common.lib_deps}"] - ) - # Sort to avoid changing build flags order - CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) - - # Sort to avoid changing build unflags order - CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) - - # Add extra script for C++ flags - CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) - - content = "[platformio]\n" - content += f"description = ESPHome {__version__}\n" - - content += f"[env:{CORE.name}]\n" - content += format_ini(CORE.platformio_options) - - return content - - def find_begin_end(text, begin_s, end_s): begin_index = text.find(begin_s) if begin_index == -1: @@ -201,34 +148,6 @@ def find_begin_end(text, begin_s, end_s): return text[:begin_index], text[(end_index + len(end_s)) :] -def write_platformio_ini(content): - update_storage_json() - path = CORE.relative_build_path("platformio.ini") - - if os.path.isfile(path): - text = read_file(path) - content_format = find_begin_end( - text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END - ) - else: - content_format = INI_BASE_FORMAT - full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}" - full_file += INI_AUTO_GENERATE_END + content_format[1] - write_file_if_changed(path, full_file) - - -def write_platformio_project(): - mkdir_p(CORE.build_path) - - content = get_ini_content() - if not get_bool_env(ENV_NOGITIGNORE): - write_gitignore() - write_platformio_ini(content) - - # Write extra script for C++ specific flags - write_cxx_flags_script() - - DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ #pragma once #include "esphome/core/macros.h" @@ -400,20 +319,3 @@ def write_gitignore(): if not os.path.isfile(path): with open(file=path, mode="w", encoding="utf-8") as f: f.write(GITIGNORE_CONTENT) - - -CXX_FLAGS_FILE_NAME = "cxx_flags.py" -CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags -Import("env") - -# Add C++ specific flags -""" - - -def write_cxx_flags_script() -> None: - path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) - contents = CXX_FLAGS_FILE_CONTENTS - if not CORE.is_host: - contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' - contents += "\n" - write_file_if_changed(path, contents) diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 4f2a6453b4..f7dda9fb95 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -473,6 +473,61 @@ class TestLibrary: assert actual == expected + @pytest.mark.parametrize( + "target, other, result, exception", + ( + (core.Library("libfoo", None), core.Library("libfoo", None), True, None), + ( + core.Library("libfoo", "1.2.3"), + core.Library("libfoo", "1.2.3"), + True, # target is unchanged + None, + ), + ( + core.Library("libfoo", None), + core.Library("libfoo", "1.2.3"), + False, # Use version from other + None, + ), + ( + core.Library("libfoo", "1.2.3"), + core.Library("libfoo", "1.2.4"), + False, + ValueError, # Version mismatch + ), + ( + core.Library("libfoo", "1.2.3"), + core.Library("libbar", "1.2.3"), + False, + ValueError, # Name mismatch + ), + ( + core.Library( + "libfoo", "1.2.4", "https://github.com/esphome/ESPAsyncWebServer" + ), + core.Library("libfoo", "1.2.3"), + True, # target is unchanged due to having a repository + None, + ), + ( + core.Library("libfoo", "1.2.3"), + core.Library( + "libfoo", "1.2.4", "https://github.com/esphome/ESPAsyncWebServer" + ), + False, # use other due to having a repository + None, + ), + ), + ) + def test_reconcile(self, target, other, result, exception): + if exception is not None: + with pytest.raises(exception): + target.reconcile_with(other) + else: + expected = target if result else other + actual = target.reconcile_with(other) + assert actual == expected + class TestEsphomeCore: @pytest.fixture From a04c2c8471ab3aa9c8449bc6bbb0f0229adee512 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:25:08 -0400 Subject: [PATCH 049/125] [esp32_touch] Fix setup mode in v1 driver (#9725) --- .../components/esp32_touch/esp32_touch_v1.cpp | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp index c3d43c6bbf..629dc8e793 100644 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -16,6 +16,8 @@ namespace esp32_touch { static const char *const TAG = "esp32_touch"; +static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF; + void ESP32TouchComponent::setup() { // Create queue for touch events // Queue size calculation: children * 4 allows for burst scenarios where ISR @@ -44,7 +46,11 @@ void ESP32TouchComponent::setup() { // Configure each touch pad for (auto *child : this->children_) { - touch_pad_config(child->get_touch_pad(), child->get_threshold()); + if (this->setup_mode_) { + touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD); + } else { + touch_pad_config(child->get_touch_pad(), child->get_threshold()); + } } // Register ISR handler @@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() { child->publish_state(new_state); // Original ESP32: ISR only fires when touched, release is detected by timeout // Note: ESP32 v1 uses inverted logic - touched when value < threshold - ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")", - child->get_name().c_str(), event.value, child->get_threshold()); + ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")", + child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold()); } break; // Exit inner loop after processing matching pad } @@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { // as any pad remains touched. This allows us to detect both new touches and // continued touches, but releases must be detected by timeout in the main loop. - // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! - // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE - // Therefore: touched = (value < threshold) - // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) - // Process all configured pads to check their current state // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, // so we must scan all configured pads to find which ones were touched @@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { } // Skip pads that aren’t in the trigger mask - bool is_touched = (mask >> pad) & 1; - if (!is_touched) { + if (((mask >> pad) & 1) == 0) { continue; } + // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! + // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE + // Therefore: touched = (value < threshold) + // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) + bool is_touched = value < child->get_threshold(); + // Always send the current state - the main loop will filter for changes // We send both touched and untouched states because the ISR doesn't // track previous state (to keep ISR fast and simple) From 74ce3d2c0bb909938d62ee72fc63101066128f7c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:20:25 +1200 Subject: [PATCH 050/125] [tuya] Update use of fan_schema (#9762) --- esphome/components/tuya/fan/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index c732bdaf31..de95888b6b 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT +from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT from .. import CONF_TUYA_ID, Tuya, tuya_ns @@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint" TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan) CONFIG_SCHEMA = cv.All( - fan.FAN_SCHEMA.extend( + fan.fan_schema(TuyaFan) + .extend( { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, @@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT), ) @@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): parent = await cg.get_variable(config[CONF_TUYA_ID]) - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], parent, config[CONF_SPEED_COUNT]) + var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT]) await cg.register_component(var, config) await fan.register_fan(var, config) From db62a94712c3d8379571f1481c8e76eb836bb200 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 11:38:39 -1000 Subject: [PATCH 051/125] [api] Implement zero-copy for all protobuf bytes fields (#9761) --- esphome/components/api/api.proto | 1 - esphome/components/api/api_connection.cpp | 48 +++------- esphome/components/api/api_pb2.cpp | 22 +++-- esphome/components/api/api_pb2.h | 37 ++++++-- esphome/components/api/api_pb2_dump.cpp | 24 ++--- esphome/components/api/proto.h | 32 +++---- .../bluetooth_proxy/bluetooth_connection.cpp | 8 +- .../voice_assistant/voice_assistant.cpp | 2 +- script/api_protobuf/api_protobuf.py | 88 +++++++++++++++---- 9 files changed, 154 insertions(+), 108 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index fd08e87bbf..e7c2fcaf8a 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -732,7 +732,6 @@ message SubscribeLogsResponse { LogLevel level = 1; bytes message = 3; - bool send_failed = 4; } // ==================== NOISE ENCRYPTION ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bc0afd49eb..6fe6037f31 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -225,24 +225,16 @@ void APIConnection::loop() { if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) { uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); bool done = this->image_reader_->available() == to_send; - uint32_t msg_size = 0; - ProtoSize::add_fixed_field<4>(msg_size, 1, true); - // partial message size calculated manually since its a special case - // 1 for the data field, varint for the data size, and the data itself - msg_size += 1 + ProtoSize::varint(to_send) + to_send; - ProtoSize::add_bool_field(msg_size, 1, done); - auto buffer = this->create_buffer(msg_size); - // fixed32 key = 1; - buffer.encode_fixed32(1, camera::Camera::instance()->get_object_id_hash()); - // bytes data = 2; - buffer.encode_bytes(2, this->image_reader_->peek_data_buffer(), to_send); - // bool done = 3; - buffer.encode_bool(3, done); + CameraImageResponse msg; + msg.key = camera::Camera::instance()->get_object_id_hash(); + msg.set_data(this->image_reader_->peek_data_buffer(), to_send); + msg.done = done; +#ifdef USE_DEVICES + msg.device_id = camera::Camera::instance()->get_device_id(); +#endif - bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); - - if (success) { + if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { this->image_reader_->consume_data(to_send); if (done) { this->image_reader_->return_image(); @@ -1350,26 +1342,10 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { #endif bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) { - // Pre-calculate message size to avoid reallocations - uint32_t msg_size = 0; - - // Add size for level field (field ID 1, varint type) - // 1 byte for field tag + size of the level varint - msg_size += 1 + api::ProtoSize::varint(static_cast(level)); - - // Add size for string field (field ID 3, string type) - // 1 byte for field tag + size of length varint + string length - msg_size += 1 + api::ProtoSize::varint(static_cast(message_len)) + message_len; - - // Create a pre-sized buffer - auto buffer = this->create_buffer(msg_size); - - // Encode the message (SubscribeLogsResponse) - buffer.encode_uint32(1, static_cast(level)); // LogLevel level = 1 - buffer.encode_string(3, line, message_len); // string message = 3 - - // SubscribeLogsResponse - 29 - return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); + SubscribeLogsResponse msg; + msg.level = static_cast(level); + msg.set_message(reinterpret_cast(line), message_len); + return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE); } void APIConnection::complete_authentication_() { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 528c581ad7..28d135ed6d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -822,13 +822,11 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, static_cast(this->level)); - buffer.encode_bytes(3, reinterpret_cast(this->message.data()), this->message.size()); - buffer.encode_bool(4, this->send_failed); + buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->level)); - ProtoSize::add_string_field(total_size, 1, this->message); - ProtoSize::add_bool_field(total_size, 1, this->send_failed); + ProtoSize::add_bytes_field(total_size, 1, this->message_len_); } #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { @@ -1034,7 +1032,7 @@ void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { } void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_bytes(2, reinterpret_cast(this->data.data()), this->data.size()); + buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); @@ -1042,7 +1040,7 @@ void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { } void CameraImageResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->data); + ProtoSize::add_bytes_field(total_size, 1, this->data_len_); ProtoSize::add_bool_field(total_size, 1, this->done); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -1976,12 +1974,12 @@ bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt valu void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); - buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); + buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address); ProtoSize::add_uint32_field(total_size, 1, this->handle); - ProtoSize::add_string_field(total_size, 1, this->data); + ProtoSize::add_bytes_field(total_size, 1, this->data_len_); } bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2064,12 +2062,12 @@ bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt va void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); - buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); + buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } void BluetoothGATTNotifyDataResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address); ProtoSize::add_uint32_field(total_size, 1, this->handle); - ProtoSize::add_string_field(total_size, 1, this->data); + ProtoSize::add_bytes_field(total_size, 1, this->data_len_); } void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->free); @@ -2268,11 +2266,11 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited return true; } void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { - buffer.encode_bytes(1, reinterpret_cast(this->data.data()), this->data.size()); + buffer.encode_bytes(1, this->data_ptr_, this->data_len_); buffer.encode_bool(2, this->end); } void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->data); + ProtoSize::add_bytes_field(total_size, 1, this->data_len_); ProtoSize::add_bool_field(total_size, 1, this->end); } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 7b64bd889f..7255aa7903 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -965,13 +965,17 @@ class SubscribeLogsRequest : public ProtoDecodableMessage { class SubscribeLogsResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 29; - static constexpr uint8_t ESTIMATED_SIZE = 13; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_logs_response"; } #endif enums::LogLevel level{}; - std::string message{}; - bool send_failed{false}; + const uint8_t *message_ptr_{nullptr}; + size_t message_len_{0}; + void set_message(const uint8_t *data, size_t len) { + this->message_ptr_ = data; + this->message_len_ = len; + } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1228,7 +1232,12 @@ class CameraImageResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "camera_image_response"; } #endif - std::string data{}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } bool done{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1882,7 +1891,12 @@ class BluetoothGATTReadResponse : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - std::string data{}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1970,7 +1984,12 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - std::string data{}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2264,6 +2283,12 @@ class VoiceAssistantAudio : public ProtoDecodableMessage { const char *message_name() const override { return "voice_assistant_audio"; } #endif std::string data{}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } bool end{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 4951c6cebf..03852bd365 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1668,11 +1668,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" message: "); - out.append(format_hex_pretty(this->message)); - out.append("\n"); - - out.append(" send_failed: "); - out.append(YESNO(this->send_failed)); + out.append(format_hex_pretty(this->message_ptr_, this->message_len_)); out.append("\n"); out.append("}"); } @@ -1681,7 +1677,7 @@ void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("NoiseEncryptionSetKeyRequest {\n"); out.append(" key: "); - out.append(format_hex_pretty(this->key)); + out.append(format_hex_pretty(reinterpret_cast(this->key.data()), this->key.size())); out.append("\n"); out.append("}"); } @@ -1934,7 +1930,7 @@ void CameraImageResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); out.append(" done: "); @@ -3143,7 +3139,7 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); out.append("}"); } @@ -3165,7 +3161,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); out.append("\n"); out.append("}"); } @@ -3197,7 +3193,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); out.append("\n"); out.append("}"); } @@ -3233,7 +3229,7 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); out.append("}"); } @@ -3487,7 +3483,11 @@ void VoiceAssistantAudio::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("VoiceAssistantAudio {\n"); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + if (this->data_ptr_ != nullptr) { + out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); + } else { + out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); + } out.append("\n"); out.append(" end: "); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index a2c31100bf..0ba8df84da 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -527,25 +527,6 @@ class ProtoSize { total_size += field_id_size + 1; } - /** - * @brief Calculates and adds the size of a fixed field to the total message size - * - * Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double). - * - * @tparam NumBytes The number of bytes for this fixed field (4 or 8) - * @param is_nonzero Whether the value is non-zero - */ - template - static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero) { - // Skip calculation if value is zero - if (!is_nonzero) { - return; // No need to update total_size - } - - // Fixed fields always take exactly NumBytes - total_size += field_id_size + NumBytes; - } - /** * @brief Calculates and adds the size of a float field to the total message size */ @@ -704,6 +685,19 @@ class ProtoSize { total_size += field_id_size + varint(str_size) + str_size; } + /** + * @brief Calculates and adds the size of a bytes field to the total message size + */ + static inline void add_bytes_field(uint32_t &total_size, uint32_t field_id_size, size_t len) { + // Skip calculation if bytes is empty + if (len == 0) { + return; // No need to update total_size + } + + // Field ID + length varint + data bytes + total_size += field_id_size + varint(static_cast(len)) + static_cast(len); + } + /** * @brief Calculates and adds the size of a nested message field to the total message size * diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 85380fa486..7c883b74a2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -234,9 +234,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTReadResponse resp; resp.address = this->address_; resp.handle = param->read.handle; - resp.data.reserve(param->read.value_len); - // Use bulk insert instead of individual push_backs - resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len); + resp.set_data(param->read.value, param->read.value_len); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); break; } @@ -287,9 +285,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; resp.handle = param->notify.handle; - resp.data.reserve(param->notify.value_len); - // Use bulk insert instead of individual push_backs - resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len); + resp.set_data(param->notify.value, param->notify.value_len); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); break; } diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 3c69dafa43..481a4efa62 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -273,7 +273,7 @@ void VoiceAssistant::loop() { size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); if (this->audio_mode_ == AUDIO_MODE_API) { api::VoiceAssistantAudio msg; - msg.data.assign((char *) this->send_buffer_, read_bytes); + msg.set_data(this->send_buffer_, read_bytes); this->api_client_->send_message(msg, api::VoiceAssistantAudio::MESSAGE_TYPE); } else { if (!this->udp_socket_running_) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index ad6c3c3ed2..2678b7009a 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -113,8 +113,15 @@ def force_str(force: bool) -> str: class TypeInfo(ABC): """Base class for all type information.""" - def __init__(self, field: descriptor.FieldDescriptorProto) -> None: + def __init__( + self, + field: descriptor.FieldDescriptorProto, + needs_decode: bool = True, + needs_encode: bool = True, + ) -> None: self._field = field + self._needs_decode = needs_decode + self._needs_encode = needs_encode @property def default_value(self) -> str: @@ -313,7 +320,11 @@ def validate_field_type(field_type: int, field_name: str = "") -> None: ) -def create_field_type_info(field: descriptor.FieldDescriptorProto) -> TypeInfo: +def create_field_type_info( + field: descriptor.FieldDescriptorProto, + needs_decode: bool = True, + needs_encode: bool = True, +) -> TypeInfo: """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" if field.label == 3: # repeated return RepeatedTypeInfo(field) @@ -325,6 +336,10 @@ def create_field_type_info(field: descriptor.FieldDescriptorProto) -> TypeInfo: ): return FixedArrayBytesType(field, fixed_size) + # Special handling for bytes fields + if field.type == 12: + return BytesType(field, needs_decode, needs_encode) + validate_field_type(field.type, field.name) return TYPE_INFO[field.type](field) @@ -589,20 +604,59 @@ class BytesType(TypeInfo): default_value = "" reference_type = "std::string &" const_reference_type = "const std::string &" - decode_length = "value.as_string()" encode_func = "encode_bytes" + decode_length = "value.as_string()" wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 + @property + def public_content(self) -> list[str]: + content: list[str] = [] + # Add std::string storage if message needs decoding + if self._needs_decode: + content.append(f"std::string {self.field_name}{{}};") + + if self._needs_encode: + content.extend( + [ + # Add pointer/length fields if message needs encoding + f"const uint8_t* {self.field_name}_ptr_{{nullptr}};", + f"size_t {self.field_name}_len_{{0}};", + # Add setter method if message needs encoding + f"void set_{self.field_name}(const uint8_t* data, size_t len) {{", + f" this->{self.field_name}_ptr_ = data;", + f" this->{self.field_name}_len_ = len;", + "}", + ] + ) + return content + @property def encode_content(self) -> str: - return f"buffer.encode_bytes({self.number}, reinterpret_cast(this->{self.field_name}.data()), this->{self.field_name}.size());" + return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" def dump(self, name: str) -> str: - o = f"out.append(format_hex_pretty({name}));" - return o + ptr_dump = f"format_hex_pretty(this->{self.field_name}_ptr_, this->{self.field_name}_len_)" + str_dump = f"format_hex_pretty(reinterpret_cast(this->{self.field_name}.data()), this->{self.field_name}.size())" + + # For SOURCE_CLIENT only, always use std::string + if not self._needs_encode: + return f"out.append({str_dump});" + + # For SOURCE_SERVER, always use pointer/length + if not self._needs_decode: + return f"out.append({ptr_dump});" + + # For SOURCE_BOTH, check if pointer is set (sending) or use string (received) + return ( + f"if (this->{self.field_name}_ptr_ != nullptr) {{\n" + f" out.append({ptr_dump});\n" + f" }} else {{\n" + f" out.append({str_dump});\n" + f" }}" + ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_string_field") + return f"ProtoSize::add_bytes_field(total_size, {self.calculate_field_id_size()}, this->{self.field_name}_len_);" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes @@ -1257,7 +1311,7 @@ def build_message_type( if field.options.deprecated: continue - ti = create_field_type_info(field) + ti = create_field_type_info(field, needs_decode, needs_encode) # Skip field declarations for fields that are in the base class # but include their encode/decode logic @@ -1572,10 +1626,20 @@ def build_base_class( public_content = [] protected_content = [] + # Determine if any message using this base class needs decoding/encoding + needs_decode = any( + message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_CLIENT) + for msg in messages + ) + needs_encode = any( + message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_SERVER) + for msg in messages + ) + # For base classes, we only declare the fields but don't handle encode/decode # The derived classes will handle encoding/decoding with their specific field numbers for field in common_fields: - ti = create_field_type_info(field) + ti = create_field_type_info(field, needs_decode, needs_encode) # Get field_ifdef if it's consistent across all messages field_ifdef = get_common_field_ifdef(field.name, messages) @@ -1586,12 +1650,6 @@ def build_base_class( if ti.public_content: public_content.extend(wrap_with_ifdef(ti.public_content, field_ifdef)) - # Determine if any message using this base class needs decoding - needs_decode = any( - message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_CLIENT) - for msg in messages - ) - # Build header parent_class = "ProtoDecodableMessage" if needs_decode else "ProtoMessage" out = f"class {base_class_name} : public {parent_class} {{\n" From 5343a6d16a373b66d1674dce1c183d3d3e85cc6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 11:39:28 -1000 Subject: [PATCH 052/125] [api] Optimize string encoding with memcpy for 10x performance improvement (#9778) --- esphome/components/api/proto.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 0ba8df84da..6ae4556cc1 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -5,6 +5,7 @@ #include "esphome/core/log.h" #include +#include #include #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE @@ -206,8 +207,13 @@ class ProtoWriteBuffer { this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_varint_raw(len); - auto *data = reinterpret_cast(string); - this->buffer_->insert(this->buffer_->end(), data, data + len); + + // Using resize + memcpy instead of insert provides significant performance improvement: + // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings + // as it avoids iterator checks and potential element moves that insert performs + size_t old_size = this->buffer_->size(); + this->buffer_->resize(old_size + len); + std::memcpy(this->buffer_->data() + old_size, string, len); } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); From 118b74b7cd108b358b5c3306e450041777e2a5e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 12:56:32 -1000 Subject: [PATCH 053/125] [api] Optimize noise handshake with memcpy for faster connection setup (#9779) --- .../components/api/api_frame_helper_noise.cpp | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 3c2c9e059e..dcb9de9c93 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -15,6 +15,7 @@ namespace api { static const char *const TAG = "api.noise"; static const char *const PROLOGUE_INIT = "NoiseAPIInit"; +static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) @@ -73,7 +74,9 @@ APIError APINoiseFrameHelper::init() { } // init prologue - prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); + size_t old_size = prologue_.size(); + prologue_.resize(old_size + PROLOGUE_INIT_LEN); + std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); state_ = State::CLIENT_HELLO; return APIError::OK; @@ -223,11 +226,12 @@ APIError APINoiseFrameHelper::state_action_() { return handle_handshake_frame_error_(aerr); } // ignore contents, may be used in future for flags - // Reserve space for: existing prologue + 2 size bytes + frame data - prologue_.reserve(prologue_.size() + 2 + frame.size()); - prologue_.push_back((uint8_t) (frame.size() >> 8)); - prologue_.push_back((uint8_t) frame.size()); - prologue_.insert(prologue_.end(), frame.begin(), frame.end()); + // Resize for: existing prologue + 2 size bytes + frame data + size_t old_size = prologue_.size(); + prologue_.resize(old_size + 2 + frame.size()); + prologue_[old_size] = (uint8_t) (frame.size() >> 8); + prologue_[old_size + 1] = (uint8_t) frame.size(); + std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size()); state_ = State::SERVER_HELLO; } @@ -237,18 +241,22 @@ APIError APINoiseFrameHelper::state_action_() { const std::string &mac = get_mac_address(); std::vector msg; - // Reserve space for: 1 byte proto + name + null + mac + null - msg.reserve(1 + name.size() + 1 + mac.size() + 1); + // Calculate positions and sizes + size_t name_len = name.size() + 1; // including null terminator + size_t mac_len = mac.size() + 1; // including null terminator + size_t name_offset = 1; + size_t mac_offset = name_offset + name_len; + size_t total_size = 1 + name_len + mac_len; + + msg.resize(total_size); // chosen proto - msg.push_back(0x01); + msg[0] = 0x01; // node name, terminated by null byte - const uint8_t *name_ptr = reinterpret_cast(name.c_str()); - msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); + std::memcpy(msg.data() + name_offset, name.c_str(), name_len); // node mac, terminated by null byte - const uint8_t *mac_ptr = reinterpret_cast(mac.c_str()); - msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); + std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len); aerr = write_frame_(msg.data(), msg.size()); if (aerr != APIError::OK) From 238c72b66f621fcfaad801383750978403bef402 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 21 Jul 2025 18:29:05 -0500 Subject: [PATCH 054/125] [config_validation] Add support for suggesting alternate component/platform (#9757) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/config_validation.py | 84 ++++++++++++++------- tests/component_tests/mipi_spi/test_init.py | 2 +- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index b1691fa43e..756464b563 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -73,6 +73,7 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, VALID_SUBSTITUTIONS_CHARACTERS, + Framework, __version__ as ESPHOME_VERSION, ) from esphome.core import ( @@ -282,6 +283,38 @@ class FinalExternalInvalid(Invalid): """Represents an invalid value in the final validation phase where the path should not be prepended.""" +@dataclass(frozen=True, order=True) +class Version: + major: int + minor: int + patch: int + extra: str = "" + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" + + @classmethod + def parse(cls, value: str) -> Version: + match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value) + if match is None: + raise ValueError(f"Not a valid version number {value}") + major = int(match[1]) + minor = int(match[2]) + patch = int(match[3]) + extra = match[4] or "" + return Version(major=major, minor=minor, patch=patch, extra=extra) + + @property + def is_beta(self) -> bool: + """Check if this version is a beta version.""" + return self.extra.startswith("b") + + @property + def is_dev(self) -> bool: + """Check if this version is a development version.""" + return self.extra.startswith("dev") + + def check_not_templatable(value): if isinstance(value, Lambda): raise Invalid("This option is not templatable!") @@ -619,16 +652,35 @@ def only_on(platforms): return validator_ -def only_with_framework(frameworks): +def only_with_framework( + frameworks: Framework | str | list[Framework | str], suggestions=None +): """Validate that this option can only be specified on the given frameworks.""" if not isinstance(frameworks, list): frameworks = [frameworks] + frameworks = [Framework(framework) for framework in frameworks] + + if suggestions is None: + suggestions = {} + + version = Version.parse(ESPHOME_VERSION) + if version.is_beta: + docs_format = "https://beta.esphome.io/components/{path}" + elif version.is_dev: + docs_format = "https://next.esphome.io/components/{path}" + else: + docs_format = "https://esphome.io/components/{path}" + def validator_(obj): if CORE.target_framework not in frameworks: - raise Invalid( - f"This feature is only available with frameworks {frameworks}" - ) + err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}" + if suggestion := suggestions.get(CORE.target_framework, None): + (component, docs_path) = suggestion + err_str += f"\nPlease use '{component}'" + if docs_path: + err_str += f": {docs_format.format(path=docs_path)}" + raise Invalid(err_str) return obj return validator_ @@ -637,8 +689,8 @@ def only_with_framework(frameworks): only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) only_on_rp2040 = only_on(PLATFORM_RP2040) -only_with_arduino = only_with_framework("arduino") -only_with_esp_idf = only_with_framework("esp-idf") +only_with_arduino = only_with_framework(Framework.ARDUINO) +only_with_esp_idf = only_with_framework(Framework.ESP_IDF) # Adapted from: @@ -1966,26 +2018,6 @@ def source_refresh(value: str): return positive_time_period_seconds(value) -@dataclass(frozen=True, order=True) -class Version: - major: int - minor: int - patch: int - - def __str__(self): - return f"{self.major}.{self.minor}.{self.patch}" - - @classmethod - def parse(cls, value: str) -> Version: - match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value) - if match is None: - raise ValueError(f"Not a valid version number {value}") - major = int(match[1]) - minor = int(match[2]) - patch = int(match[3]) - return Version(major=major, minor=minor, patch=patch) - - def version_number(value): value = string_strict(value) try: diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index 96abad02ad..9824852653 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -266,7 +266,7 @@ def test_framework_specific_errors( with pytest.raises( cv.Invalid, - match=r"This feature is only available with frameworks \['esp-idf'\]", + match=r"This feature is only available with framework\(s\) esp-idf", ): run_schema_validation({"model": "wt32-sc01-plus"}) From e56b681506e05f68e2d79656a5f58a9629d7f23c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 14:32:50 -1000 Subject: [PATCH 055/125] [nrf52] Add missing CoreModel define for scheduler (#9777) --- esphome/components/nrf52/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index c23298e38f..870c51066c 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -23,6 +23,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_NRF52, + CoreModel, ) from esphome.core import CORE, EsphomeError, coroutine_with_priority from esphome.storage_json import StorageJSON @@ -108,6 +109,8 @@ async def to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_NRF52") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "NRF52") + # nRF52 processors are single-core + cg.add_define(CoreModel.SINGLE) cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) cg.add_platformio_option( "platform", From b17e2019c7a790d1a4535115a2692d6ac44129a7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:49:48 +1200 Subject: [PATCH 056/125] [esp32_ble_tracker] Write require feature defines after all clients are registered (#9780) --- esphome/components/esp32_ble_tracker/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 68f4657515..046f3f679f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -30,7 +30,7 @@ from esphome.const import ( CONF_SERVICE_UUID, CONF_TRIGGER_ID, ) -from esphome.core import CORE +from esphome.core import CORE, coroutine_with_priority from esphome.enum import StrEnum from esphome.types import ConfigType @@ -365,14 +365,22 @@ async def to_code(config): cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") - # Add feature-specific defines based on what's needed - if BLEFeatures.ESP_BT_DEVICE in _required_features: - cg.add_define("USE_ESP32_BLE_DEVICE") + CORE.add_job(_add_ble_features) if config.get(CONF_SOFTWARE_COEXISTENCE): cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") +# This needs to be run as a job with very low priority so that all components have +# chance to call register_ble_tracker and register_client before the list is checked +# and added to the global defines list. +@coroutine_with_priority(-1000) +async def _add_ble_features(): + # Add feature-specific defines based on what's needed + if BLEFeatures.ESP_BT_DEVICE in _required_features: + cg.add_define("USE_ESP32_BLE_DEVICE") + + ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(ESP32BLETracker), From 0f0038df24ea837000ffbe51aed01e2ff119c485 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 17:47:43 -1000 Subject: [PATCH 057/125] [core] Process pending loop enables during setup blocking phase (#9787) --- esphome/core/application.cpp | 63 ++++++++++++++++++++++-------------- esphome/core/application.h | 2 ++ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 748c8f2237..873f342277 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -71,8 +71,11 @@ void Application::setup() { do { uint8_t new_app_state = STATUS_LED_WARNING; - this->scheduler.call(millis()); - this->feed_wdt(); + uint32_t now = millis(); + + // Process pending loop enables to handle GPIO interrupts during setup + this->before_loop_tasks_(now); + for (uint32_t j = 0; j <= i; j++) { // Update loop_component_start_time_ right before calling each component this->loop_component_start_time_ = millis(); @@ -81,6 +84,8 @@ void Application::setup() { this->app_state_ |= new_app_state; this->feed_wdt(); } + + this->after_loop_tasks_(); this->app_state_ = new_app_state; yield(); } while (!component->can_proceed()); @@ -100,27 +105,7 @@ void Application::loop() { // Get the initial loop time at the start uint32_t last_op_end_time = millis(); - this->scheduler.call(last_op_end_time); - - // Feed WDT with time - this->feed_wdt(last_op_end_time); - - // Process any pending enable_loop requests from ISRs - // This must be done before marking in_loop_ = true to avoid race conditions - if (this->has_pending_enable_loop_requests_) { - // Clear flag BEFORE processing to avoid race condition - // If ISR sets it during processing, we'll catch it next loop iteration - // This is safe because: - // 1. Each component has its own pending_enable_loop_ flag that we check - // 2. If we can't process a component (wrong state), enable_pending_loops_() - // will set this flag back to true - // 3. Any new ISR requests during processing will set the flag again - this->has_pending_enable_loop_requests_ = false; - this->enable_pending_loops_(); - } - - // Mark that we're in the loop for safe reentrant modifications - this->in_loop_ = true; + this->before_loop_tasks_(last_op_end_time); for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; this->current_loop_index_++) { @@ -141,7 +126,7 @@ void Application::loop() { this->feed_wdt(last_op_end_time); } - this->in_loop_ = false; + this->after_loop_tasks_(); this->app_state_ = new_app_state; #ifdef USE_RUNTIME_STATS @@ -411,6 +396,36 @@ void Application::enable_pending_loops_() { } } +void Application::before_loop_tasks_(uint32_t loop_start_time) { + // Process scheduled tasks + this->scheduler.call(loop_start_time); + + // Feed the watchdog timer + this->feed_wdt(loop_start_time); + + // Process any pending enable_loop requests from ISRs + // This must be done before marking in_loop_ = true to avoid race conditions + if (this->has_pending_enable_loop_requests_) { + // Clear flag BEFORE processing to avoid race condition + // If ISR sets it during processing, we'll catch it next loop iteration + // This is safe because: + // 1. Each component has its own pending_enable_loop_ flag that we check + // 2. If we can't process a component (wrong state), enable_pending_loops_() + // will set this flag back to true + // 3. Any new ISR requests during processing will set the flag again + this->has_pending_enable_loop_requests_ = false; + this->enable_pending_loops_(); + } + + // Mark that we're in the loop for safe reentrant modifications + this->in_loop_ = true; +} + +void Application::after_loop_tasks_() { + // Clear the in_loop_ flag to indicate we're done processing components + this->in_loop_ = false; +} + #ifdef USE_SOCKET_SELECT_SUPPORT bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop diff --git a/esphome/core/application.h b/esphome/core/application.h index 75b9769ca3..a83789837f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -512,6 +512,8 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); + void before_loop_tasks_(uint32_t loop_start_time); + void after_loop_tasks_(); void feed_wdt_arch_(); From ac08fb314f21ad05f8e7498872b9e0df845de057 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 23:50:49 -1000 Subject: [PATCH 058/125] [api] Optimize protobuf memory usage with fixed-size arrays for Bluetooth UUIDs (#9782) --- esphome/components/api/api.proto | 8 +- esphome/components/api/api_pb2.cpp | 42 ++---- esphome/components/api/api_pb2.h | 10 +- .../bluetooth_proxy/bluetooth_connection.cpp | 27 ++-- script/api_protobuf/api_protobuf.py | 129 ++++++++++++++++++ 5 files changed, 165 insertions(+), 51 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e7c2fcaf8a..93e84702e2 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1463,19 +1463,19 @@ message BluetoothGATTGetServicesRequest { } message BluetoothGATTDescriptor { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2]; uint32 handle = 2; } message BluetoothGATTCharacteristic { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2]; uint32 handle = 2; uint32 properties = 3; repeated BluetoothGATTDescriptor descriptors = 4; } message BluetoothGATTService { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2]; uint32 handle = 2; repeated BluetoothGATTCharacteristic characteristics = 3; } @@ -1486,7 +1486,7 @@ message BluetoothGATTGetServicesResponse { option (ifdef) = "USE_BLUETOOTH_PROXY"; uint64 address = 1; - repeated BluetoothGATTService services = 2; + repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1]; } message BluetoothGATTGetServicesDoneResponse { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 28d135ed6d..09a1522fb4 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1891,23 +1891,18 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI return true; } void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->uuid) { - buffer.encode_uint64(1, it, true); - } + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); buffer.encode_uint32(2, this->handle); } void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } - } + ProtoSize::add_uint64_field_repeated(total_size, 1, this->uuid[0]); + ProtoSize::add_uint64_field_repeated(total_size, 1, this->uuid[1]); ProtoSize::add_uint32_field(total_size, 1, this->handle); } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->uuid) { - buffer.encode_uint64(1, it, true); - } + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { @@ -1915,42 +1910,33 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { } } void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } - } + ProtoSize::add_uint64_field_repeated(total_size, 1, this->uuid[0]); + ProtoSize::add_uint64_field_repeated(total_size, 1, this->uuid[1]); ProtoSize::add_uint32_field(total_size, 1, this->handle); ProtoSize::add_uint32_field(total_size, 1, this->properties); ProtoSize::add_repeated_message(total_size, 1, this->descriptors); } void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->uuid) { - buffer.encode_uint64(1, it, true); - } + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { buffer.encode_message(3, it, true); } } void BluetoothGATTService::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } - } + ProtoSize::add_uint64_field_repeated(total_size, 1, this->uuid[0]); + ProtoSize::add_uint64_field_repeated(total_size, 1, this->uuid[1]); ProtoSize::add_uint32_field(total_size, 1, this->handle); ProtoSize::add_repeated_message(total_size, 1, this->characteristics); } void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); - for (auto &it : this->services) { - buffer.encode_message(2, it, true); - } + buffer.encode_message(2, this->services[0], true); } void BluetoothGATTGetServicesResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address); - ProtoSize::add_repeated_message(total_size, 1, this->services); + ProtoSize::add_message_object_repeated(total_size, 1, this->services[0]); } void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 7255aa7903..1d052f6114 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1796,7 +1796,7 @@ class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { }; class BluetoothGATTDescriptor : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1808,7 +1808,7 @@ class BluetoothGATTDescriptor : public ProtoMessage { }; class BluetoothGATTCharacteristic : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; uint32_t handle{0}; uint32_t properties{0}; std::vector descriptors{}; @@ -1822,7 +1822,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage { }; class BluetoothGATTService : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; uint32_t handle{0}; std::vector characteristics{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1836,12 +1836,12 @@ class BluetoothGATTService : public ProtoMessage { class BluetoothGATTGetServicesResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 71; - static constexpr uint8_t ESTIMATED_SIZE = 38; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } #endif uint64_t address{0}; - std::vector services{}; + std::array services{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 7c883b74a2..616dba891a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -13,16 +13,16 @@ namespace bluetooth_proxy { static const char *const TAG = "bluetooth_proxy.connection"; -static std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { +static void fill_128bit_uuid_array(std::array &out, esp_bt_uuid_t uuid_source) { esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); - return std::vector{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | - ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | - ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | - ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), - ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | - ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | - ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | - ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; + out[0] = ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | + ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | + ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | + ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]); + out[1] = ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | + ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | + ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | + ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); } void BluetoothConnection::dump_config() { @@ -95,9 +95,8 @@ void BluetoothConnection::send_service_for_discovery_() { api::BluetoothGATTGetServicesResponse resp; resp.address = this->address_; - resp.services.emplace_back(); - auto &service_resp = resp.services.back(); - service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); + auto &service_resp = resp.services[0]; + fill_128bit_uuid_array(service_resp.uuid, service_result.uuid); service_resp.handle = service_result.start_handle; // Get the number of characteristics directly with one call @@ -136,7 +135,7 @@ void BluetoothConnection::send_service_for_discovery_() { service_resp.characteristics.emplace_back(); auto &characteristic_resp = service_resp.characteristics.back(); - characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); + fill_128bit_uuid_array(characteristic_resp.uuid, char_result.uuid); characteristic_resp.handle = char_result.char_handle; characteristic_resp.properties = char_result.properties; char_offset++; @@ -176,7 +175,7 @@ void BluetoothConnection::send_service_for_discovery_() { characteristic_resp.descriptors.emplace_back(); auto &descriptor_resp = characteristic_resp.descriptors.back(); - descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); + fill_128bit_uuid_array(descriptor_resp.uuid, desc_result.uuid); descriptor_resp.handle = desc_result.handle; desc_offset++; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2678b7009a..2d49ac5ece 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -327,6 +327,9 @@ def create_field_type_info( ) -> TypeInfo: """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" if field.label == 3: # repeated + # Check if this repeated field has fixed_array_size option + if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None: + return FixedArrayRepeatedType(field, fixed_size) return RepeatedTypeInfo(field) # Check for fixed_array_size option on bytes fields @@ -593,6 +596,8 @@ class MessageType(TypeInfo): return self._get_simple_size_calculation(name, force, "add_message_object") def get_estimated_size(self) -> int: + # For message types, we can't easily estimate the submessage size without + # access to the actual message definition. This is just a rough estimate. return ( self.calculate_field_id_size() + 16 ) # field ID + 16 bytes estimated submessage @@ -883,6 +888,111 @@ class SInt64Type(TypeInfo): return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint +class FixedArrayRepeatedType(TypeInfo): + """Special type for fixed-size repeated fields using std::array. + + Fixed arrays are only supported for encoding (SOURCE_SERVER) since we cannot + control how many items we receive when decoding. + """ + + def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None: + super().__init__(field) + self.array_size = size + # Create the element type info + validate_field_type(field.type, field.name) + self._ti: TypeInfo = TYPE_INFO[field.type](field) + + @property + def cpp_type(self) -> str: + return f"std::array<{self._ti.cpp_type}, {self.array_size}>" + + @property + def reference_type(self) -> str: + return f"{self.cpp_type} &" + + @property + def const_reference_type(self) -> str: + return f"const {self.cpp_type} &" + + @property + def wire_type(self) -> WireType: + """Get the wire type for this fixed array field.""" + return self._ti.wire_type + + @property + def public_content(self) -> list[str]: + # Just the array member, no index needed since we don't decode + return [f"{self.cpp_type} {self.field_name}{{}};"] + + # No decode methods needed - fixed arrays don't support decoding + # The base class TypeInfo already returns None for all decode properties + + @property + def encode_content(self) -> str: + # Helper to generate encode statement for a single element + def encode_element(element: str) -> str: + if isinstance(self._ti, EnumType): + return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + else: + return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + + # Unroll small arrays for efficiency + if self.array_size == 1: + return encode_element(f"this->{self.field_name}[0]") + elif self.array_size == 2: + return ( + encode_element(f"this->{self.field_name}[0]") + + "\n " + + encode_element(f"this->{self.field_name}[1]") + ) + + # Use loops for larger arrays + o = f"for (const auto &it : this->{self.field_name}) {{\n" + o += f" {encode_element('it')}\n" + o += "}" + return o + + @property + def dump_content(self) -> str: + o = f"for (const auto &it : this->{self.field_name}) {{\n" + o += f' out.append(" {self.name}: ");\n' + o += indent(self._ti.dump("it")) + "\n" + o += ' out.append("\\n");\n' + o += "}\n" + return o + + def dump(self, name: str) -> str: + # This is used when dumping the array itself (not its elements) + # Since dump_content handles the iteration, this is not used directly + return "" + + def get_size_calculation(self, name: str, force: bool = False) -> str: + # For fixed arrays, we always encode all elements + + # Special case for single-element arrays - no loop needed + if self.array_size == 1: + return self._ti.get_size_calculation(f"{name}[0]", True) + + # Special case for 2-element arrays - unroll the calculation + if self.array_size == 2: + return ( + self._ti.get_size_calculation(f"{name}[0]", True) + + "\n " + + self._ti.get_size_calculation(f"{name}[1]", True) + ) + + # Use loops for larger arrays + o = f"for (const auto &it : {name}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += "}" + return o + + def get_estimated_size(self) -> int: + # For fixed arrays, estimate underlying type size * array size + underlying_size = self._ti.get_estimated_size() + return underlying_size * self.array_size + + class RepeatedTypeInfo(TypeInfo): def __init__(self, field: descriptor.FieldDescriptorProto) -> None: super().__init__(field) @@ -1311,6 +1421,19 @@ def build_message_type( if field.options.deprecated: continue + # Validate that fixed_array_size is only used in encode-only messages + if ( + needs_decode + and field.label == 3 + and get_field_opt(field, pb.fixed_array_size) is not None + ): + raise ValueError( + f"Message '{desc.name}' uses fixed_array_size on field '{field.name}' " + f"but has source={SOURCE_NAMES[source]}. " + f"Fixed arrays are only supported for SOURCE_SERVER (encode-only) messages " + f"since we cannot trust or control the number of items received from clients." + ) + ti = create_field_type_info(field, needs_decode, needs_encode) # Skip field declarations for fields that are in the base class @@ -1500,6 +1623,12 @@ SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2 +SOURCE_NAMES = { + SOURCE_BOTH: "SOURCE_BOTH", + SOURCE_SERVER: "SOURCE_SERVER", + SOURCE_CLIENT: "SOURCE_CLIENT", +} + RECEIVE_CASES: dict[int, tuple[str, str | None]] = {} ifdefs: dict[str, str] = {} From 7efe1b8698bb14213fd4b550b63d78158cea1c54 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:53:33 -0500 Subject: [PATCH 059/125] [fastled_clockless, fastled_spi] Add suggested alternate when using IDF (#9784) --- esphome/components/fastled_clockless/light.py | 19 +++++++++++++++++-- esphome/components/fastled_spi/light.py | 10 ++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index 49a6d390be..aa2172bf88 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -2,7 +2,13 @@ from esphome import pins import esphome.codegen as cg from esphome.components import fastled_base import esphome.config_validation as cv -from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER +from esphome.const import ( + CONF_CHIPSET, + CONF_NUM_LEDS, + CONF_PIN, + CONF_RGB_ORDER, + Framework, +) AUTO_LOAD = ["fastled_base"] @@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, } ), - _validate, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "esp32_rmt_led_strip", + "light/esp32_rmt_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 7, 4), esp32_arduino=cv.Version(99, 0, 0), max_version=True, extra_message="Please see note on documentation for FastLED", ), + _validate, ) diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index ac30721eb4..81d8c3b32a 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA_RATE, CONF_NUM_LEDS, CONF_RGB_ORDER, + Framework, ) AUTO_LOAD = ["fastled_base"] @@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DATA_RATE): cv.frequency, } ), + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "spi_led_strip", + "light/spi_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 7, 4), esp32_arduino=cv.Version(99, 0, 0), From 89924ae468513f3adeb6ed3d4c7444f788cecaef Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:53:45 -0500 Subject: [PATCH 060/125] [neopixelbus] Add suggested alternate when using IDF (#9783) --- esphome/components/neopixelbus/light.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 3cd1bfd357..c63790e60b 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_PIN, CONF_TYPE, CONF_VARIANT, + Framework, ) from esphome.core import CORE @@ -162,7 +163,15 @@ def _validate_method(value): CONFIG_SCHEMA = cv.All( - cv.only_with_arduino, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "esp32_rmt_led_strip", + "light/esp32_rmt_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 4, 0), esp32_arduino=cv.Version(0, 0, 0), From 71cb429a860cecc76bae4efdf67ae841b35aacc5 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:54:09 -0500 Subject: [PATCH 061/125] [bme680_bsec] Add suggested alternate when using IDF (#9785) --- esphome/components/bme680_bsec/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 8ee463a59a..330dc4dd9c 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import esp32, i2c import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET +from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] @@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_minutes, } ).extend(i2c.i2c_device_schema(0x76)), - cv.only_with_arduino, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "bme68x_bsec2_i2c", + "sensor/bme68x_bsec2", + ) + }, + ), cv.Any( cv.only_on_esp8266, cv.All( From 7ac60c15dc76a3dcd3995620b56423886d4adaa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Jul 2025 19:31:53 -1000 Subject: [PATCH 062/125] [gpio] Auto-disable interrupts for shared GPIO pins in binary sensors (#9701) --- .../components/gpio/binary_sensor/__init__.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 59f54520fa..8372bc7e08 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -4,7 +4,13 @@ from esphome import pins import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN +from esphome.const import ( + CONF_ALLOW_OTHER_USES, + CONF_ID, + CONF_NAME, + CONF_NUMBER, + CONF_PIN, +) from esphome.core import CORE from .. import gpio_ns @@ -76,6 +82,18 @@ async def to_code(config): ) use_interrupt = False + # Check if pin is shared with other components (allow_other_uses) + # When a pin is shared, interrupts can interfere with other components + # (e.g., duty_cycle sensor) that need to monitor the pin's state changes + if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False): + _LOGGER.info( + "GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. " + "The sensor will use polling mode for compatibility with other pin uses.", + config.get(CONF_NAME, config[CONF_ID]), + config[CONF_PIN][CONF_NUMBER], + ) + use_interrupt = False + cg.add(var.set_use_interrupt(use_interrupt)) if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) From d6ff79082392f5067c2416230eb925b7e76fe767 Mon Sep 17 00:00:00 2001 From: tmpeh <41875356+tmpeh@users.noreply.github.com> Date: Sat, 19 Jul 2025 23:24:26 +0200 Subject: [PATCH 063/125] Fix format string error in ota_web_server.cpp (#9711) --- esphome/components/web_server/ota/ota_web_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 966c1c1024..7211f707e9 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -76,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); } else { - ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); + ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_); } #ifdef USE_OTA_STATE_CALLBACK // Report progress - use call_deferred since we're in web server task @@ -171,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // Finalize if (final) { - ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len, + ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len, this->ota_read_length_, request->contentLength()); // For Arduino framework, the Update library tracks expected size from firmware header From d92ee563f27d9ad160c3dc7a3afb1523640c133d Mon Sep 17 00:00:00 2001 From: JonasB2497 <45214989+JonasB2497@users.noreply.github.com> Date: Sun, 20 Jul 2025 00:29:02 +0200 Subject: [PATCH 064/125] [sdl][mipi_spi] Respect clipping when drawing (#9722) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/sdl/sdl_esphome.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index e55bff58fe..5ad18f6311 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t * } void Sdl::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); From 896d7f8f7616ff705d0bdbbcd080f6aba420be28 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:25:08 -0400 Subject: [PATCH 065/125] [esp32_touch] Fix setup mode in v1 driver (#9725) --- .../components/esp32_touch/esp32_touch_v1.cpp | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp index c3d43c6bbf..629dc8e793 100644 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -16,6 +16,8 @@ namespace esp32_touch { static const char *const TAG = "esp32_touch"; +static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF; + void ESP32TouchComponent::setup() { // Create queue for touch events // Queue size calculation: children * 4 allows for burst scenarios where ISR @@ -44,7 +46,11 @@ void ESP32TouchComponent::setup() { // Configure each touch pad for (auto *child : this->children_) { - touch_pad_config(child->get_touch_pad(), child->get_threshold()); + if (this->setup_mode_) { + touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD); + } else { + touch_pad_config(child->get_touch_pad(), child->get_threshold()); + } } // Register ISR handler @@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() { child->publish_state(new_state); // Original ESP32: ISR only fires when touched, release is detected by timeout // Note: ESP32 v1 uses inverted logic - touched when value < threshold - ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")", - child->get_name().c_str(), event.value, child->get_threshold()); + ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")", + child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold()); } break; // Exit inner loop after processing matching pad } @@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { // as any pad remains touched. This allows us to detect both new touches and // continued touches, but releases must be detected by timeout in the main loop. - // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! - // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE - // Therefore: touched = (value < threshold) - // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) - // Process all configured pads to check their current state // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, // so we must scan all configured pads to find which ones were touched @@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { } // Skip pads that aren’t in the trigger mask - bool is_touched = (mask >> pad) & 1; - if (!is_touched) { + if (((mask >> pad) & 1) == 0) { continue; } + // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! + // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE + // Therefore: touched = (value < threshold) + // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) + bool is_touched = value < child->get_threshold(); + // Always send the current state - the main loop will filter for changes // We send both touched and untouched states because the ISR doesn't // track previous state (to keep ISR fast and simple) From 76e75f4cdc2c2bcfca6cf38d43a97c3c80e55c19 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:20:25 +1200 Subject: [PATCH 066/125] [tuya] Update use of fan_schema (#9762) --- esphome/components/tuya/fan/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index c732bdaf31..de95888b6b 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT +from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT from .. import CONF_TUYA_ID, Tuya, tuya_ns @@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint" TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan) CONFIG_SCHEMA = cv.All( - fan.FAN_SCHEMA.extend( + fan.fan_schema(TuyaFan) + .extend( { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, @@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT), ) @@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): parent = await cg.get_variable(config[CONF_TUYA_ID]) - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], parent, config[CONF_SPEED_COUNT]) + var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT]) await cg.register_component(var, config) await fan.register_fan(var, config) From f8777d3b6604d2ff8c0451c3e1925bb17a983a30 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 21 Jul 2025 18:29:05 -0500 Subject: [PATCH 067/125] [config_validation] Add support for suggesting alternate component/platform (#9757) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/config_validation.py | 84 +++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 09b132a458..1c524cab6b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -73,6 +73,7 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, VALID_SUBSTITUTIONS_CHARACTERS, + Framework, __version__ as ESPHOME_VERSION, ) from esphome.core import ( @@ -282,6 +283,38 @@ class FinalExternalInvalid(Invalid): """Represents an invalid value in the final validation phase where the path should not be prepended.""" +@dataclass(frozen=True, order=True) +class Version: + major: int + minor: int + patch: int + extra: str = "" + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" + + @classmethod + def parse(cls, value: str) -> Version: + match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value) + if match is None: + raise ValueError(f"Not a valid version number {value}") + major = int(match[1]) + minor = int(match[2]) + patch = int(match[3]) + extra = match[4] or "" + return Version(major=major, minor=minor, patch=patch, extra=extra) + + @property + def is_beta(self) -> bool: + """Check if this version is a beta version.""" + return self.extra.startswith("b") + + @property + def is_dev(self) -> bool: + """Check if this version is a development version.""" + return self.extra.startswith("dev") + + def check_not_templatable(value): if isinstance(value, Lambda): raise Invalid("This option is not templatable!") @@ -619,16 +652,35 @@ def only_on(platforms): return validator_ -def only_with_framework(frameworks): +def only_with_framework( + frameworks: Framework | str | list[Framework | str], suggestions=None +): """Validate that this option can only be specified on the given frameworks.""" if not isinstance(frameworks, list): frameworks = [frameworks] + frameworks = [Framework(framework) for framework in frameworks] + + if suggestions is None: + suggestions = {} + + version = Version.parse(ESPHOME_VERSION) + if version.is_beta: + docs_format = "https://beta.esphome.io/components/{path}" + elif version.is_dev: + docs_format = "https://next.esphome.io/components/{path}" + else: + docs_format = "https://esphome.io/components/{path}" + def validator_(obj): if CORE.target_framework not in frameworks: - raise Invalid( - f"This feature is only available with frameworks {frameworks}" - ) + err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}" + if suggestion := suggestions.get(CORE.target_framework, None): + (component, docs_path) = suggestion + err_str += f"\nPlease use '{component}'" + if docs_path: + err_str += f": {docs_format.format(path=docs_path)}" + raise Invalid(err_str) return obj return validator_ @@ -637,8 +689,8 @@ def only_with_framework(frameworks): only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) only_on_rp2040 = only_on(PLATFORM_RP2040) -only_with_arduino = only_with_framework("arduino") -only_with_esp_idf = only_with_framework("esp-idf") +only_with_arduino = only_with_framework(Framework.ARDUINO) +only_with_esp_idf = only_with_framework(Framework.ESP_IDF) # Adapted from: @@ -1965,26 +2017,6 @@ def source_refresh(value: str): return positive_time_period_seconds(value) -@dataclass(frozen=True, order=True) -class Version: - major: int - minor: int - patch: int - - def __str__(self): - return f"{self.major}.{self.minor}.{self.patch}" - - @classmethod - def parse(cls, value: str) -> Version: - match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value) - if match is None: - raise ValueError(f"Not a valid version number {value}") - major = int(match[1]) - minor = int(match[2]) - patch = int(match[3]) - return Version(major=major, minor=minor, patch=patch) - - def version_number(value): value = string_strict(value) try: From fc8c5a7438d4260772014f4c19d9925f1fa10216 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 17:47:43 -1000 Subject: [PATCH 068/125] [core] Process pending loop enables during setup blocking phase (#9787) --- esphome/core/application.cpp | 63 ++++++++++++++++++++++-------------- esphome/core/application.h | 2 ++ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index e19acd3ba6..68bcc99d31 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -68,8 +68,11 @@ void Application::setup() { do { uint8_t new_app_state = STATUS_LED_WARNING; - this->scheduler.call(); - this->feed_wdt(); + uint32_t now = millis(); + + // Process pending loop enables to handle GPIO interrupts during setup + this->before_loop_tasks_(now); + for (uint32_t j = 0; j <= i; j++) { // Update loop_component_start_time_ right before calling each component this->loop_component_start_time_ = millis(); @@ -78,6 +81,8 @@ void Application::setup() { this->app_state_ |= new_app_state; this->feed_wdt(); } + + this->after_loop_tasks_(); this->app_state_ = new_app_state; yield(); } while (!component->can_proceed()); @@ -94,30 +99,10 @@ void Application::setup() { void Application::loop() { uint8_t new_app_state = 0; - this->scheduler.call(); - // Get the initial loop time at the start uint32_t last_op_end_time = millis(); - // Feed WDT with time - this->feed_wdt(last_op_end_time); - - // Process any pending enable_loop requests from ISRs - // This must be done before marking in_loop_ = true to avoid race conditions - if (this->has_pending_enable_loop_requests_) { - // Clear flag BEFORE processing to avoid race condition - // If ISR sets it during processing, we'll catch it next loop iteration - // This is safe because: - // 1. Each component has its own pending_enable_loop_ flag that we check - // 2. If we can't process a component (wrong state), enable_pending_loops_() - // will set this flag back to true - // 3. Any new ISR requests during processing will set the flag again - this->has_pending_enable_loop_requests_ = false; - this->enable_pending_loops_(); - } - - // Mark that we're in the loop for safe reentrant modifications - this->in_loop_ = true; + this->before_loop_tasks_(last_op_end_time); for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; this->current_loop_index_++) { @@ -138,7 +123,7 @@ void Application::loop() { this->feed_wdt(last_op_end_time); } - this->in_loop_ = false; + this->after_loop_tasks_(); this->app_state_ = new_app_state; // Use the last component's end time instead of calling millis() again @@ -400,6 +385,36 @@ void Application::enable_pending_loops_() { } } +void Application::before_loop_tasks_(uint32_t loop_start_time) { + // Process scheduled tasks + this->scheduler.call(); + + // Feed the watchdog timer + this->feed_wdt(loop_start_time); + + // Process any pending enable_loop requests from ISRs + // This must be done before marking in_loop_ = true to avoid race conditions + if (this->has_pending_enable_loop_requests_) { + // Clear flag BEFORE processing to avoid race condition + // If ISR sets it during processing, we'll catch it next loop iteration + // This is safe because: + // 1. Each component has its own pending_enable_loop_ flag that we check + // 2. If we can't process a component (wrong state), enable_pending_loops_() + // will set this flag back to true + // 3. Any new ISR requests during processing will set the flag again + this->has_pending_enable_loop_requests_ = false; + this->enable_pending_loops_(); + } + + // Mark that we're in the loop for safe reentrant modifications + this->in_loop_ = true; +} + +void Application::after_loop_tasks_() { + // Clear the in_loop_ flag to indicate we're done processing components + this->in_loop_ = false; +} + #ifdef USE_SOCKET_SELECT_SUPPORT bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop diff --git a/esphome/core/application.h b/esphome/core/application.h index f2b5cb5c89..f675881a27 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -504,6 +504,8 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); + void before_loop_tasks_(uint32_t loop_start_time); + void after_loop_tasks_(); void feed_wdt_arch_(); From cb6acfe24bf2e73fd615ea39f70d543620519bd7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:53:33 -0500 Subject: [PATCH 069/125] [fastled_clockless, fastled_spi] Add suggested alternate when using IDF (#9784) --- esphome/components/fastled_clockless/light.py | 19 +++++++++++++++++-- esphome/components/fastled_spi/light.py | 10 ++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index 49a6d390be..aa2172bf88 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -2,7 +2,13 @@ from esphome import pins import esphome.codegen as cg from esphome.components import fastled_base import esphome.config_validation as cv -from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER +from esphome.const import ( + CONF_CHIPSET, + CONF_NUM_LEDS, + CONF_PIN, + CONF_RGB_ORDER, + Framework, +) AUTO_LOAD = ["fastled_base"] @@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, } ), - _validate, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "esp32_rmt_led_strip", + "light/esp32_rmt_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 7, 4), esp32_arduino=cv.Version(99, 0, 0), max_version=True, extra_message="Please see note on documentation for FastLED", ), + _validate, ) diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index ac30721eb4..81d8c3b32a 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA_RATE, CONF_NUM_LEDS, CONF_RGB_ORDER, + Framework, ) AUTO_LOAD = ["fastled_base"] @@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DATA_RATE): cv.frequency, } ), + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "spi_led_strip", + "light/spi_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 7, 4), esp32_arduino=cv.Version(99, 0, 0), From ae12deff877754b3defe2fbf432646c3799938ad Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:53:45 -0500 Subject: [PATCH 070/125] [neopixelbus] Add suggested alternate when using IDF (#9783) --- esphome/components/neopixelbus/light.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 3cd1bfd357..c63790e60b 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_PIN, CONF_TYPE, CONF_VARIANT, + Framework, ) from esphome.core import CORE @@ -162,7 +163,15 @@ def _validate_method(value): CONFIG_SCHEMA = cv.All( - cv.only_with_arduino, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "esp32_rmt_led_strip", + "light/esp32_rmt_led_strip", + ) + }, + ), cv.require_framework_version( esp8266_arduino=cv.Version(2, 4, 0), esp32_arduino=cv.Version(0, 0, 0), From 86740124064ad809f2609b2e85e113a1367f911d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 22 Jul 2025 06:54:09 -0500 Subject: [PATCH 071/125] [bme680_bsec] Add suggested alternate when using IDF (#9785) --- esphome/components/bme680_bsec/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 8ee463a59a..330dc4dd9c 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import esp32, i2c import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET +from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] @@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_minutes, } ).extend(i2c.i2c_device_schema(0x76)), - cv.only_with_arduino, + cv.only_with_framework( + frameworks=Framework.ARDUINO, + suggestions={ + Framework.ESP_IDF: ( + "bme68x_bsec2_i2c", + "sensor/bme68x_bsec2", + ) + }, + ), cv.Any( cv.only_on_esp8266, cv.All( From dc26ed9c46f8c8b09bca39df3a97caac6af802d1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:34:13 +1200 Subject: [PATCH 072/125] Bump version to 2025.7.3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a601fcc8eb..ae1e519030 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.7.2 +PROJECT_NUMBER = 2025.7.3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 93b509f72a..476059af62 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.7.2" +__version__ = "2025.7.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From a614a68f1ad358e9c521ec9b71597539645a8833 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Jul 2025 07:33:03 -1000 Subject: [PATCH 073/125] [api] Implement zero-copy string optimization for outgoing protobuf messages (#9790) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 205 +++++--- esphome/components/api/api_connection.h | 36 +- esphome/components/api/api_pb2.cpp | 492 +++++++++--------- esphome/components/api/api_pb2.h | 172 ++++-- esphome/components/api/api_pb2_dump.cpp | 277 +++++----- esphome/components/api/api_pb2_service.cpp | 27 +- esphome/components/api/api_pb2_service.h | 19 +- esphome/components/api/custom_api_device.h | 24 +- .../components/api/homeassistant_service.h | 30 +- esphome/components/api/proto.h | 46 +- esphome/components/api/user_services.h | 8 +- .../number/homeassistant_number.cpp | 24 +- .../switch/homeassistant_switch.cpp | 16 +- esphome/components/text/text_traits.h | 2 + .../voice_assistant/voice_assistant.cpp | 4 +- esphome/core/entity_base.h | 18 + script/api_protobuf/api_protobuf.py | 91 +++- 17 files changed, 863 insertions(+), 628 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 6fe6037f31..60dc0a113d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -248,8 +248,10 @@ void APIConnection::loop() { if (state_subs_at_ < static_cast(subs.size())) { auto &it = subs[state_subs_at_]; SubscribeHomeAssistantStateResponse resp; - resp.entity_id = it.entity_id; - resp.attribute = it.attribute.value(); + resp.set_entity_id(StringRef(it.entity_id)); + // attribute.value() returns temporary - must store it + std::string attribute_value = it.attribute.value(); + resp.set_attribute(StringRef(attribute_value)); resp.once = it.once; if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { state_subs_at_++; @@ -260,14 +262,14 @@ void APIConnection::loop() { } } -DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { +bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); this->flags_.next_close = true; DisconnectResponse resp; - return resp; + return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); } void APIConnection::on_disconnect_response(const DisconnectResponse &value) { this->helper_->close(); @@ -344,7 +346,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool is_single) { auto *binary_sensor = static_cast(entity); ListEntitiesBinarySensorResponse msg; - msg.device_class = binary_sensor->get_device_class(); + msg.set_device_class(binary_sensor->get_device_class_ref()); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -376,7 +378,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - msg.device_class = cover->get_device_class(); + msg.set_device_class(cover->get_device_class_ref()); return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -411,7 +413,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co if (traits.supports_direction()) msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes()) - msg.preset_mode = fan->preset_mode; + msg.set_preset_mode(StringRef(fan->preset_mode)); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -468,8 +470,11 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * resp.color_temperature = values.get_color_temperature(); resp.cold_white = values.get_cold_white(); resp.warm_white = values.get_warm_white(); - if (light->supports_effects()) - resp.effect = light->get_effect_name(); + if (light->supports_effects()) { + // get_effect_name() returns temporary std::string - must store it + std::string effect_name = light->get_effect_name(); + resp.set_effect(StringRef(effect_name)); + } return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -545,10 +550,10 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * bool is_single) { auto *sensor = static_cast(entity); ListEntitiesSensorResponse msg; - msg.unit_of_measurement = sensor->get_unit_of_measurement(); + msg.set_unit_of_measurement(sensor->get_unit_of_measurement_ref()); msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); - msg.device_class = sensor->get_device_class(); + msg.set_device_class(sensor->get_device_class_ref()); msg.state_class = static_cast(sensor->get_state_class()); return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -575,7 +580,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * auto *a_switch = static_cast(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - msg.device_class = a_switch->get_device_class(); + msg.set_device_class(a_switch->get_device_class_ref()); return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -600,7 +605,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec bool is_single) { auto *text_sensor = static_cast(entity); TextSensorStateResponse resp; - resp.state = text_sensor->state; + resp.set_state(StringRef(text_sensor->state)); resp.missing_state = !text_sensor->has_state(); return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -609,7 +614,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect bool is_single) { auto *text_sensor = static_cast(entity); ListEntitiesTextSensorResponse msg; - msg.device_class = text_sensor->get_device_class(); + msg.set_device_class(text_sensor->get_device_class_ref()); return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -637,13 +642,19 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection } if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); - if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) - resp.custom_fan_mode = climate->custom_fan_mode.value(); + if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { + // custom_fan_mode.value() returns temporary - must store it + std::string custom_fan_mode = climate->custom_fan_mode.value(); + resp.set_custom_fan_mode(StringRef(custom_fan_mode)); + } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } - if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) - resp.custom_preset = climate->custom_preset.value(); + if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { + // custom_preset.value() returns temporary - must store it + std::string custom_preset = climate->custom_preset.value(); + resp.set_custom_preset(StringRef(custom_preset)); + } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); if (traits.get_supports_current_humidity()) @@ -729,9 +740,9 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * bool is_single) { auto *number = static_cast(entity); ListEntitiesNumberResponse msg; - msg.unit_of_measurement = number->traits.get_unit_of_measurement(); + msg.set_unit_of_measurement(number->traits.get_unit_of_measurement_ref()); msg.mode = static_cast(number->traits.get_mode()); - msg.device_class = number->traits.get_device_class(); + msg.set_device_class(number->traits.get_device_class_ref()); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); @@ -844,7 +855,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c bool is_single) { auto *text = static_cast(entity); TextStateResponse resp; - resp.state = text->state; + resp.set_state(StringRef(text->state)); resp.missing_state = !text->has_state(); return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -856,7 +867,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co msg.mode = static_cast(text->traits.get_mode()); msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); - msg.pattern = text->traits.get_pattern(); + msg.set_pattern(text->traits.get_pattern_ref()); return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -877,7 +888,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.state = select->state; + resp.set_state(StringRef(select->state)); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -903,7 +914,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection * bool is_single) { auto *button = static_cast(entity); ListEntitiesButtonResponse msg; - msg.device_class = button->get_device_class(); + msg.set_device_class(button->get_device_class_ref()); return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -972,7 +983,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c auto *valve = static_cast(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); - msg.device_class = valve->get_device_class(); + msg.set_device_class(valve->get_device_class_ref()); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); @@ -1014,13 +1025,13 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec auto traits = media_player->get_traits(); msg.supports_pause = traits.get_supports_pause(); for (auto &supported_format : traits.get_supported_formats()) { - MediaPlayerSupportedFormat media_format; - media_format.format = supported_format.format; + msg.supported_formats.emplace_back(); + auto &media_format = msg.supported_formats.back(); + media_format.set_format(StringRef(supported_format.format)); media_format.sample_rate = supported_format.sample_rate; media_format.num_channels = supported_format.num_channels; media_format.purpose = static_cast(supported_format.purpose); media_format.sample_bytes = supported_format.sample_bytes; - msg.supported_formats.push_back(media_format); } return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -1083,6 +1094,12 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { } #endif +bool APIConnection::send_get_time_response(const GetTimeRequest &msg) { + GetTimeResponse resp; + resp.epoch_seconds = ::time(nullptr); + return this->send_message(resp, GetTimeResponse::MESSAGE_TYPE); +} + #ifdef USE_BLUETOOTH_PROXY void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); @@ -1113,12 +1130,12 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg); } -BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_free( +bool APIConnection::send_subscribe_bluetooth_connections_free_response( const SubscribeBluetoothConnectionsFreeRequest &msg) { BluetoothConnectionsFreeResponse resp; resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free(); resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit(); - return resp; + return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE); } void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) { @@ -1179,28 +1196,27 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno } } -VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration( - const VoiceAssistantConfigurationRequest &msg) { +bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; if (!this->check_voice_assistant_api_connection_()) { - return resp; + return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); } auto &config = voice_assistant::global_voice_assistant->get_configuration(); for (auto &wake_word : config.available_wake_words) { - VoiceAssistantWakeWord resp_wake_word; - resp_wake_word.id = wake_word.id; - resp_wake_word.wake_word = wake_word.wake_word; + resp.available_wake_words.emplace_back(); + auto &resp_wake_word = resp.available_wake_words.back(); + resp_wake_word.set_id(StringRef(wake_word.id)); + resp_wake_word.set_wake_word(StringRef(wake_word.wake_word)); for (const auto &lang : wake_word.trained_languages) { resp_wake_word.trained_languages.push_back(lang); } - resp.available_wake_words.push_back(std::move(resp_wake_word)); } for (auto &wake_word_id : config.active_wake_words) { resp.active_wake_words.push_back(wake_word_id); } resp.max_active_wake_words = config.max_active_wake_words; - return resp; + return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); } void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { @@ -1273,7 +1289,7 @@ void APIConnection::send_event(event::Event *event, const std::string &event_typ uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; - resp.event_type = event_type; + resp.set_event_type(StringRef(event_type)); return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1281,7 +1297,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c bool is_single) { auto *event = static_cast(entity); ListEntitiesEventResponse msg; - msg.device_class = event->get_device_class(); + msg.set_device_class(event->get_device_class_ref()); for (const auto &event_type : event->get_event_types()) msg.event_types.push_back(event_type); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, @@ -1305,11 +1321,11 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.has_progress = true; resp.progress = update->update_info.progress; } - resp.current_version = update->update_info.current_version; - resp.latest_version = update->update_info.latest_version; - resp.title = update->update_info.title; - resp.release_summary = update->update_info.summary; - resp.release_url = update->update_info.release_url; + resp.set_current_version(StringRef(update->update_info.current_version)); + resp.set_latest_version(StringRef(update->update_info.latest_version)); + resp.set_title(StringRef(update->update_info.title)); + resp.set_release_summary(StringRef(update->update_info.summary)); + resp.set_release_url(StringRef(update->update_info.release_url)); } return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1317,7 +1333,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection * bool is_single) { auto *update = static_cast(entity); ListEntitiesUpdateResponse msg; - msg.device_class = update->get_device_class(); + msg.set_device_class(update->get_device_class_ref()); return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1366,7 +1382,7 @@ void APIConnection::complete_authentication_() { #endif } -HelloResponse APIConnection::hello(const HelloRequest &msg) { +bool APIConnection::send_hello_response(const HelloRequest &msg) { this->client_info_.name = msg.client_info; this->client_info_.peername = this->helper_->getpeername(); this->client_api_version_major_ = msg.api_version_major; @@ -1377,8 +1393,10 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; resp.api_version_minor = 10; - resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; - resp.name = App.get_name(); + // Temporary string for concatenation - will be valid during send_message call + std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; + resp.set_server_info(StringRef(server_info)); + resp.set_name(StringRef(App.get_name())); #ifdef USE_API_PASSWORD // Password required - wait for authentication @@ -1388,9 +1406,9 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { this->complete_authentication_(); #endif - return resp; + return this->send_message(resp, HelloResponse::MESSAGE_TYPE); } -ConnectResponse APIConnection::connect(const ConnectRequest &msg) { +bool APIConnection::send_connect_response(const ConnectRequest &msg) { bool correct = true; #ifdef USE_API_PASSWORD correct = this->parent_->check_password(msg.password); @@ -1402,48 +1420,71 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { if (correct) { this->complete_authentication_(); } - return resp; + return this->send_message(resp, ConnectResponse::MESSAGE_TYPE); } -DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { + +bool APIConnection::send_ping_response(const PingRequest &msg) { + PingResponse resp; + return this->send_message(resp, PingResponse::MESSAGE_TYPE); +} + +bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; #ifdef USE_API_PASSWORD resp.uses_password = true; #endif - resp.name = App.get_name(); - resp.friendly_name = App.get_friendly_name(); + resp.set_name(StringRef(App.get_name())); + resp.set_friendly_name(StringRef(App.get_friendly_name())); #ifdef USE_AREAS - resp.suggested_area = App.get_area(); + resp.set_suggested_area(StringRef(App.get_area())); #endif - resp.mac_address = get_mac_address_pretty(); - resp.esphome_version = ESPHOME_VERSION; - resp.compilation_time = App.get_compilation_time(); + // mac_address must store temporary string - will be valid during send_message call + std::string mac_address = get_mac_address_pretty(); + resp.set_mac_address(StringRef(mac_address)); + + // Compile-time StringRef constants + static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); + resp.set_esphome_version(ESPHOME_VERSION_REF); + + // get_compilation_time() returns temporary std::string - must store it + std::string compilation_time = App.get_compilation_time(); + resp.set_compilation_time(StringRef(compilation_time)); + + // Compile-time StringRef constants for manufacturers #if defined(USE_ESP8266) || defined(USE_ESP32) - resp.manufacturer = "Espressif"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif"); #elif defined(USE_RP2040) - resp.manufacturer = "Raspberry Pi"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi"); #elif defined(USE_BK72XX) - resp.manufacturer = "Beken"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); #elif defined(USE_LN882X) - resp.manufacturer = "Lightning"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); #elif defined(USE_RTL87XX) - resp.manufacturer = "Realtek"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); #elif defined(USE_HOST) - resp.manufacturer = "Host"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Host"); #endif - resp.model = ESPHOME_BOARD; + resp.set_manufacturer(MANUFACTURER); + + static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD); + resp.set_model(MODEL); #ifdef USE_DEEP_SLEEP resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; #endif #ifdef ESPHOME_PROJECT_NAME - resp.project_name = ESPHOME_PROJECT_NAME; - resp.project_version = ESPHOME_PROJECT_VERSION; + static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME); + static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION); + resp.set_project_name(PROJECT_NAME); + resp.set_project_version(PROJECT_VERSION); #endif #ifdef USE_WEBSERVER resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); - resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); + // bt_mac must store temporary string - will be valid during send_message call + std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); + resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); #endif #ifdef USE_VOICE_ASSISTANT resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); @@ -1453,23 +1494,25 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { #endif #ifdef USE_DEVICES for (auto const &device : App.get_devices()) { - DeviceInfo device_info; + resp.devices.emplace_back(); + auto &device_info = resp.devices.back(); device_info.device_id = device->get_device_id(); - device_info.name = device->get_name(); + device_info.set_name(StringRef(device->get_name())); device_info.area_id = device->get_area_id(); - resp.devices.push_back(device_info); } #endif #ifdef USE_AREAS for (auto const &area : App.get_areas()) { - AreaInfo area_info; + resp.areas.emplace_back(); + auto &area_info = resp.areas.back(); area_info.area_id = area->get_area_id(); - area_info.name = area->get_name(); - resp.areas.push_back(area_info); + area_info.set_name(StringRef(area->get_name())); } #endif - return resp; + + return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); } + void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { for (auto &it : this->parent_->get_state_subs()) { if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) { @@ -1491,23 +1534,21 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } #endif #ifdef USE_API_NOISE -NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { +bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { psk_t psk{}; NoiseEncryptionSetKeyResponse resp; if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { ESP_LOGW(TAG, "Invalid encryption key length"); resp.success = false; - return resp; + return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); } - if (!this->parent_->save_noise_psk(psk, true)) { ESP_LOGW(TAG, "Failed to save encryption key"); resp.success = false; - return resp; + return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); } - resp.success = true; - return resp; + return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); } #endif void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index de7e91de01..5365a48292 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -148,8 +148,7 @@ class APIConnection : public APIServerConnection { void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; - BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( - const SubscribeBluetoothConnectionsFreeRequest &msg) override; + bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override; void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; #endif @@ -167,8 +166,7 @@ class APIConnection : public APIServerConnection { void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; - VoiceAssistantConfigurationResponse voice_assistant_get_configuration( - const VoiceAssistantConfigurationRequest &msg) override; + bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override; void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; #endif @@ -195,11 +193,11 @@ class APIConnection : public APIServerConnection { #ifdef USE_HOMEASSISTANT_TIME void on_get_time_response(const GetTimeResponse &value) override; #endif - HelloResponse hello(const HelloRequest &msg) override; - ConnectResponse connect(const ConnectRequest &msg) override; - DisconnectResponse disconnect(const DisconnectRequest &msg) override; - PingResponse ping(const PingRequest &msg) override { return {}; } - DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; + bool send_hello_response(const HelloRequest &msg) override; + bool send_connect_response(const ConnectRequest &msg) override; + bool send_disconnect_response(const DisconnectRequest &msg) override; + bool send_ping_response(const PingRequest &msg) override; + bool send_device_info_response(const DeviceInfoRequest &msg) override; void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void subscribe_states(const SubscribeStatesRequest &msg) override { this->flags_.state_subscription = true; @@ -214,15 +212,12 @@ class APIConnection : public APIServerConnection { this->flags_.service_call_subscription = true; } void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; - GetTimeResponse get_time(const GetTimeRequest &msg) override { - // TODO - return {}; - } + bool send_get_time_response(const GetTimeRequest &msg) override; #ifdef USE_API_SERVICES void execute_service(const ExecuteServiceRequest &msg) override; #endif #ifdef USE_API_NOISE - NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; + bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override; #endif bool is_authenticated() override { @@ -313,14 +308,17 @@ class APIConnection : public APIServerConnection { APIConnection *conn, uint32_t remaining_size, bool is_single) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); - msg.object_id = entity->get_object_id(); + // IMPORTANT: get_object_id() may return a temporary std::string + std::string object_id = entity->get_object_id(); + msg.set_object_id(StringRef(object_id)); - if (entity->has_own_name()) - msg.name = entity->get_name(); + if (entity->has_own_name()) { + msg.set_name(entity->get_name()); + } - // Set common EntityBase properties + // Set common EntityBase properties #ifdef USE_ENTITY_ICON - msg.icon = entity->get_icon(); + msg.set_icon(entity->get_icon_ref()); #endif msg.disabled_by_default = entity->is_disabled_by_default(); msg.entity_category = static_cast(entity->get_entity_category()); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 09a1522fb4..d51f641746 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -34,14 +34,14 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->api_version_major); buffer.encode_uint32(2, this->api_version_minor); - buffer.encode_string(3, this->server_info); - buffer.encode_string(4, this->name); + buffer.encode_string(3, this->server_info_ref_); + buffer.encode_string(4, this->name_ref_); } void HelloResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->api_version_major); ProtoSize::add_uint32_field(total_size, 1, this->api_version_minor); - ProtoSize::add_string_field(total_size, 1, this->server_info); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->server_info_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); } bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -60,22 +60,22 @@ void ConnectResponse::calculate_size(uint32_t &total_size) const { #ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->area_id); - buffer.encode_string(2, this->name); + buffer.encode_string(2, this->name_ref_); } void AreaInfo::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->area_id); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); } #endif #ifdef USE_DEVICES void DeviceInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->device_id); - buffer.encode_string(2, this->name); + buffer.encode_string(2, this->name_ref_); buffer.encode_uint32(3, this->area_id); } void DeviceInfo::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->device_id); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); ProtoSize::add_uint32_field(total_size, 1, this->area_id); } #endif @@ -83,19 +83,19 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_API_PASSWORD buffer.encode_bool(1, this->uses_password); #endif - buffer.encode_string(2, this->name); - buffer.encode_string(3, this->mac_address); - buffer.encode_string(4, this->esphome_version); - buffer.encode_string(5, this->compilation_time); - buffer.encode_string(6, this->model); + buffer.encode_string(2, this->name_ref_); + buffer.encode_string(3, this->mac_address_ref_); + buffer.encode_string(4, this->esphome_version_ref_); + buffer.encode_string(5, this->compilation_time_ref_); + buffer.encode_string(6, this->model_ref_); #ifdef USE_DEEP_SLEEP buffer.encode_bool(7, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - buffer.encode_string(8, this->project_name); + buffer.encode_string(8, this->project_name_ref_); #endif #ifdef ESPHOME_PROJECT_NAME - buffer.encode_string(9, this->project_version); + buffer.encode_string(9, this->project_version_ref_); #endif #ifdef USE_WEBSERVER buffer.encode_uint32(10, this->webserver_port); @@ -103,16 +103,16 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_BLUETOOTH_PROXY buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); #endif - buffer.encode_string(12, this->manufacturer); - buffer.encode_string(13, this->friendly_name); + buffer.encode_string(12, this->manufacturer_ref_); + buffer.encode_string(13, this->friendly_name_ref_); #ifdef USE_VOICE_ASSISTANT buffer.encode_uint32(17, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - buffer.encode_string(16, this->suggested_area); + buffer.encode_string(16, this->suggested_area_ref_); #endif #ifdef USE_BLUETOOTH_PROXY - buffer.encode_string(18, this->bluetooth_mac_address); + buffer.encode_string(18, this->bluetooth_mac_address_ref_); #endif #ifdef USE_API_NOISE buffer.encode_bool(19, this->api_encryption_supported); @@ -135,19 +135,19 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { #ifdef USE_API_PASSWORD ProtoSize::add_bool_field(total_size, 1, this->uses_password); #endif - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->mac_address); - ProtoSize::add_string_field(total_size, 1, this->esphome_version); - ProtoSize::add_string_field(total_size, 1, this->compilation_time); - ProtoSize::add_string_field(total_size, 1, this->model); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->mac_address_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->esphome_version_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->compilation_time_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->model_ref_.size()); #ifdef USE_DEEP_SLEEP ProtoSize::add_bool_field(total_size, 1, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - ProtoSize::add_string_field(total_size, 1, this->project_name); + ProtoSize::add_string_field(total_size, 1, this->project_name_ref_.size()); #endif #ifdef ESPHOME_PROJECT_NAME - ProtoSize::add_string_field(total_size, 1, this->project_version); + ProtoSize::add_string_field(total_size, 1, this->project_version_ref_.size()); #endif #ifdef USE_WEBSERVER ProtoSize::add_uint32_field(total_size, 1, this->webserver_port); @@ -155,16 +155,16 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { #ifdef USE_BLUETOOTH_PROXY ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags); #endif - ProtoSize::add_string_field(total_size, 1, this->manufacturer); - ProtoSize::add_string_field(total_size, 1, this->friendly_name); + ProtoSize::add_string_field(total_size, 1, this->manufacturer_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->friendly_name_ref_.size()); #ifdef USE_VOICE_ASSISTANT ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - ProtoSize::add_string_field(total_size, 2, this->suggested_area); + ProtoSize::add_string_field(total_size, 2, this->suggested_area_ref_.size()); #endif #ifdef USE_BLUETOOTH_PROXY - ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address); + ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address_ref_.size()); #endif #ifdef USE_API_NOISE ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported); @@ -181,14 +181,14 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(5, this->device_class); + buffer.encode_string(3, this->name_ref_); + buffer.encode_string(5, this->device_class_ref_); buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(8, this->icon); + buffer.encode_string(8, this->icon_ref_); #endif buffer.encode_uint32(9, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -196,14 +196,14 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->is_status_binary_sensor); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -229,16 +229,16 @@ void BinarySensorStateResponse::calculate_size(uint32_t &total_size) const { #endif #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); buffer.encode_bool(9, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(10, this->icon); + buffer.encode_string(10, this->icon_ref_); #endif buffer.encode_uint32(11, static_cast(this->entity_category)); buffer.encode_bool(12, this->supports_stop); @@ -247,16 +247,16 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->assumed_state); ProtoSize::add_bool_field(total_size, 1, this->supports_position); ProtoSize::add_bool_field(total_size, 1, this->supports_tilt); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_bool_field(total_size, 1, this->supports_stop); @@ -322,16 +322,16 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); buffer.encode_int32(8, this->supported_speed_count); buffer.encode_bool(9, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(10, this->icon); + buffer.encode_string(10, this->icon_ref_); #endif buffer.encode_uint32(11, static_cast(this->entity_category)); for (auto &it : this->supported_preset_modes) { @@ -342,16 +342,16 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->supports_oscillation); ProtoSize::add_bool_field(total_size, 1, this->supports_speed); ProtoSize::add_bool_field(total_size, 1, this->supports_direction); ProtoSize::add_int32_field(total_size, 1, this->supported_speed_count); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); if (!this->supported_preset_modes.empty()) { @@ -369,7 +369,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->oscillating); buffer.encode_uint32(5, static_cast(this->direction)); buffer.encode_int32(6, this->speed_level); - buffer.encode_string(7, this->preset_mode); + buffer.encode_string(7, this->preset_mode_ref_); #ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); #endif @@ -380,7 +380,7 @@ void FanStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->oscillating); ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction)); ProtoSize::add_int32_field(total_size, 1, this->speed_level); - ProtoSize::add_string_field(total_size, 1, this->preset_mode); + ProtoSize::add_string_field(total_size, 1, this->preset_mode_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif @@ -447,9 +447,9 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); for (auto &it : this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } @@ -460,7 +460,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(13, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(14, this->icon); + buffer.encode_string(14, this->icon_ref_); #endif buffer.encode_uint32(15, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -468,9 +468,9 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); if (!this->supported_color_modes.empty()) { for (const auto &it : this->supported_color_modes) { ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); @@ -485,7 +485,7 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { } ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -505,7 +505,7 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(8, this->color_temperature); buffer.encode_float(12, this->cold_white); buffer.encode_float(13, this->warm_white); - buffer.encode_string(9, this->effect); + buffer.encode_string(9, this->effect_ref_); #ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); #endif @@ -523,7 +523,7 @@ void LightStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_float_field(total_size, 1, this->color_temperature); ProtoSize::add_float_field(total_size, 1, this->cold_white); ProtoSize::add_float_field(total_size, 1, this->warm_white); - ProtoSize::add_string_field(total_size, 1, this->effect); + ProtoSize::add_string_field(total_size, 1, this->effect_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif @@ -638,16 +638,16 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif - buffer.encode_string(6, this->unit_of_measurement); + buffer.encode_string(6, this->unit_of_measurement_ref_); buffer.encode_int32(7, this->accuracy_decimals); buffer.encode_bool(8, this->force_update); - buffer.encode_string(9, this->device_class); + buffer.encode_string(9, this->device_class_ref_); buffer.encode_uint32(10, static_cast(this->state_class)); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_uint32(13, static_cast(this->entity_category)); @@ -656,16 +656,16 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif - ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement); + ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement_ref_.size()); ProtoSize::add_int32_field(total_size, 1, this->accuracy_decimals); ProtoSize::add_bool_field(total_size, 1, this->force_update); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state_class)); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); @@ -692,31 +692,31 @@ void SensorStateResponse::calculate_size(uint32_t &total_size) const { #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->assumed_state); buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); - buffer.encode_string(9, this->device_class); + buffer.encode_string(9, this->device_class_ref_); #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); #endif } void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->assumed_state); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif @@ -763,36 +763,36 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); #ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); #endif } void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif } void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->state); + buffer.encode_string(2, this->state_ref_); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); @@ -800,7 +800,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { } void TextSensorStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->state); + ProtoSize::add_string_field(total_size, 1, this->state_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->missing_state); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -845,15 +845,15 @@ void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const { } #endif void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->key); - buffer.encode_string(2, this->value); + buffer.encode_string(1, this->key_ref_); + buffer.encode_string(2, this->value_ref_); } void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->value); + ProtoSize::add_string_field(total_size, 1, this->key_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->value_ref_.size()); } void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->service); + buffer.encode_string(1, this->service_ref_); for (auto &it : this->data) { buffer.encode_message(2, it, true); } @@ -866,20 +866,20 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(5, this->is_event); } void HomeassistantServiceResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->service); + ProtoSize::add_string_field(total_size, 1, this->service_ref_.size()); ProtoSize::add_repeated_message(total_size, 1, this->data); ProtoSize::add_repeated_message(total_size, 1, this->data_template); ProtoSize::add_repeated_message(total_size, 1, this->variables); ProtoSize::add_bool_field(total_size, 1, this->is_event); } void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->entity_id); - buffer.encode_string(2, this->attribute); + buffer.encode_string(1, this->entity_id_ref_); + buffer.encode_string(2, this->attribute_ref_); buffer.encode_bool(3, this->once); } void SubscribeHomeAssistantStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->entity_id); - ProtoSize::add_string_field(total_size, 1, this->attribute); + ProtoSize::add_string_field(total_size, 1, this->entity_id_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->attribute_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->once); } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { @@ -914,22 +914,22 @@ void GetTimeResponse::calculate_size(uint32_t &total_size) const { } #ifdef USE_API_SERVICES void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->name); + buffer.encode_string(1, this->name_ref_); buffer.encode_uint32(2, static_cast(this->type)); } void ListEntitiesServicesArgument::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); ProtoSize::add_enum_field(total_size, 1, static_cast(this->type)); } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->name); + buffer.encode_string(1, this->name_ref_); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { buffer.encode_message(3, it, true); } } void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_repeated_message(total_size, 1, this->args); } @@ -1005,12 +1005,12 @@ bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(6, this->icon); + buffer.encode_string(6, this->icon_ref_); #endif buffer.encode_uint32(7, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -1018,12 +1018,12 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -1062,9 +1062,9 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (auto &it : this->supported_modes) { @@ -1091,7 +1091,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(18, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(19, this->icon); + buffer.encode_string(19, this->icon_ref_); #endif buffer.encode_uint32(20, static_cast(this->entity_category)); buffer.encode_float(21, this->visual_current_temperature_step); @@ -1104,9 +1104,9 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->supports_current_temperature); ProtoSize::add_bool_field(total_size, 1, this->supports_two_point_target_temperature); if (!this->supported_modes.empty()) { @@ -1145,7 +1145,7 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { } ProtoSize::add_bool_field(total_size, 2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 2, this->icon); + ProtoSize::add_string_field(total_size, 2, this->icon_ref_.size()); #endif ProtoSize::add_enum_field(total_size, 2, static_cast(this->entity_category)); ProtoSize::add_float_field(total_size, 2, this->visual_current_temperature_step); @@ -1167,9 +1167,9 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(8, static_cast(this->action)); buffer.encode_uint32(9, static_cast(this->fan_mode)); buffer.encode_uint32(10, static_cast(this->swing_mode)); - buffer.encode_string(11, this->custom_fan_mode); + buffer.encode_string(11, this->custom_fan_mode_ref_); buffer.encode_uint32(12, static_cast(this->preset)); - buffer.encode_string(13, this->custom_preset); + buffer.encode_string(13, this->custom_preset_ref_); buffer.encode_float(14, this->current_humidity); buffer.encode_float(15, this->target_humidity); #ifdef USE_DEVICES @@ -1186,9 +1186,9 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->action)); ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode)); ProtoSize::add_enum_field(total_size, 1, static_cast(this->swing_mode)); - ProtoSize::add_string_field(total_size, 1, this->custom_fan_mode); + ProtoSize::add_string_field(total_size, 1, this->custom_fan_mode_ref_.size()); ProtoSize::add_enum_field(total_size, 1, static_cast(this->preset)); - ProtoSize::add_string_field(total_size, 1, this->custom_preset); + ProtoSize::add_string_field(total_size, 1, this->custom_preset_ref_.size()); ProtoSize::add_float_field(total_size, 1, this->current_humidity); ProtoSize::add_float_field(total_size, 1, this->target_humidity); #ifdef USE_DEVICES @@ -1287,39 +1287,39 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_float(6, this->min_value); buffer.encode_float(7, this->max_value); buffer.encode_float(8, this->step); buffer.encode_bool(9, this->disabled_by_default); buffer.encode_uint32(10, static_cast(this->entity_category)); - buffer.encode_string(11, this->unit_of_measurement); + buffer.encode_string(11, this->unit_of_measurement_ref_); buffer.encode_uint32(12, static_cast(this->mode)); - buffer.encode_string(13, this->device_class); + buffer.encode_string(13, this->device_class_ref_); #ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); #endif } void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_float_field(total_size, 1, this->min_value); ProtoSize::add_float_field(total_size, 1, this->max_value); ProtoSize::add_float_field(total_size, 1, this->step); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement); + ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement_ref_.size()); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif @@ -1368,11 +1368,11 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif for (auto &it : this->options) { buffer.encode_string(6, it, true); @@ -1384,11 +1384,11 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif if (!this->options.empty()) { for (const auto &it : this->options) { @@ -1403,7 +1403,7 @@ void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { } void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->state); + buffer.encode_string(2, this->state_ref_); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); @@ -1411,7 +1411,7 @@ void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { } void SelectStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->state); + ProtoSize::add_string_field(total_size, 1, this->state_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->missing_state); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -1452,11 +1452,11 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); for (auto &it : this->tones) { @@ -1470,11 +1470,11 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); if (!this->tones.empty()) { @@ -1559,35 +1559,35 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->assumed_state); buffer.encode_bool(9, this->supports_open); buffer.encode_bool(10, this->requires_code); - buffer.encode_string(11, this->code_format); + buffer.encode_string(11, this->code_format_ref_); #ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); #endif } void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_bool_field(total_size, 1, this->assumed_state); ProtoSize::add_bool_field(total_size, 1, this->supports_open); ProtoSize::add_bool_field(total_size, 1, this->requires_code); - ProtoSize::add_string_field(total_size, 1, this->code_format); + ProtoSize::add_string_field(total_size, 1, this->code_format_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif @@ -1647,29 +1647,29 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); #ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); #endif } void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif @@ -1699,25 +1699,25 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_MEDIA_PLAYER void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->format); + buffer.encode_string(1, this->format_ref_); buffer.encode_uint32(2, this->sample_rate); buffer.encode_uint32(3, this->num_channels); buffer.encode_uint32(4, static_cast(this->purpose)); buffer.encode_uint32(5, this->sample_bytes); } void MediaPlayerSupportedFormat::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->format); + ProtoSize::add_string_field(total_size, 1, this->format_ref_.size()); ProtoSize::add_uint32_field(total_size, 1, this->sample_rate); ProtoSize::add_uint32_field(total_size, 1, this->num_channels); ProtoSize::add_enum_field(total_size, 1, static_cast(this->purpose)); ProtoSize::add_uint32_field(total_size, 1, this->sample_bytes); } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -1730,11 +1730,11 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); @@ -2172,17 +2172,17 @@ void VoiceAssistantAudioSettings::calculate_size(uint32_t &total_size) const { } void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); - buffer.encode_string(2, this->conversation_id); + buffer.encode_string(2, this->conversation_id_ref_); buffer.encode_uint32(3, this->flags); buffer.encode_message(4, this->audio_settings); - buffer.encode_string(5, this->wake_word_phrase); + buffer.encode_string(5, this->wake_word_phrase_ref_); } void VoiceAssistantRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->start); - ProtoSize::add_string_field(total_size, 1, this->conversation_id); + ProtoSize::add_string_field(total_size, 1, this->conversation_id_ref_.size()); ProtoSize::add_uint32_field(total_size, 1, this->flags); ProtoSize::add_message_object(total_size, 1, this->audio_settings); - ProtoSize::add_string_field(total_size, 1, this->wake_word_phrase); + ProtoSize::add_string_field(total_size, 1, this->wake_word_phrase_ref_.size()); } bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2322,15 +2322,15 @@ void VoiceAssistantAnnounceFinished::calculate_size(uint32_t &total_size) const ProtoSize::add_bool_field(total_size, 1, this->success); } void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->id); - buffer.encode_string(2, this->wake_word); + buffer.encode_string(1, this->id_ref_); + buffer.encode_string(2, this->wake_word_ref_); for (auto &it : this->trained_languages) { buffer.encode_string(3, it, true); } } void VoiceAssistantWakeWord::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->id); - ProtoSize::add_string_field(total_size, 1, this->wake_word); + ProtoSize::add_string_field(total_size, 1, this->id_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->wake_word_ref_.size()); if (!this->trained_languages.empty()) { for (const auto &it : this->trained_languages) { ProtoSize::add_string_field_repeated(total_size, 1, it); @@ -2368,11 +2368,11 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #endif #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -2384,11 +2384,11 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons #endif } void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); @@ -2451,34 +2451,34 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #endif #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_uint32(8, this->min_length); buffer.encode_uint32(9, this->max_length); - buffer.encode_string(10, this->pattern); + buffer.encode_string(10, this->pattern_ref_); buffer.encode_uint32(11, static_cast(this->mode)); #ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); #endif } void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_uint32_field(total_size, 1, this->min_length); ProtoSize::add_uint32_field(total_size, 1, this->max_length); - ProtoSize::add_string_field(total_size, 1, this->pattern); + ProtoSize::add_string_field(total_size, 1, this->pattern_ref_.size()); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -2486,7 +2486,7 @@ void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { } void TextStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->state); + buffer.encode_string(2, this->state_ref_); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); @@ -2494,7 +2494,7 @@ void TextStateResponse::encode(ProtoWriteBuffer buffer) const { } void TextStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->state); + ProtoSize::add_string_field(total_size, 1, this->state_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->missing_state); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); @@ -2535,11 +2535,11 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -2548,11 +2548,11 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); @@ -2614,11 +2614,11 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -2627,11 +2627,11 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); @@ -2693,15 +2693,15 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); for (auto &it : this->event_types) { buffer.encode_string(9, it, true); } @@ -2710,15 +2710,15 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); if (!this->event_types.empty()) { for (const auto &it : this->event_types) { ProtoSize::add_string_field_repeated(total_size, 1, it); @@ -2730,14 +2730,14 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { } void EventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->event_type); + buffer.encode_string(2, this->event_type_ref_); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); #endif } void EventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->event_type); + ProtoSize::add_string_field(total_size, 1, this->event_type_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif @@ -2745,15 +2745,15 @@ void EventResponse::calculate_size(uint32_t &total_size) const { #endif #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); buffer.encode_bool(9, this->assumed_state); buffer.encode_bool(10, this->supports_position); buffer.encode_bool(11, this->supports_stop); @@ -2762,15 +2762,15 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); ProtoSize::add_bool_field(total_size, 1, this->assumed_state); ProtoSize::add_bool_field(total_size, 1, this->supports_position); ProtoSize::add_bool_field(total_size, 1, this->supports_stop); @@ -2828,11 +2828,11 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -2841,11 +2841,11 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); @@ -2897,29 +2897,29 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); + buffer.encode_string(3, this->name_ref_); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + buffer.encode_string(5, this->icon_ref_); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); #ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); #endif } void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_string_field(total_size, 1, this->object_id_ref_.size()); ProtoSize::add_fixed32_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->name_ref_.size()); #ifdef USE_ENTITY_ICON - ProtoSize::add_string_field(total_size, 1, this->icon); + ProtoSize::add_string_field(total_size, 1, this->icon_ref_.size()); #endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_string_field(total_size, 1, this->device_class_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif @@ -2930,11 +2930,11 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->in_progress); buffer.encode_bool(4, this->has_progress); buffer.encode_float(5, this->progress); - buffer.encode_string(6, this->current_version); - buffer.encode_string(7, this->latest_version); - buffer.encode_string(8, this->title); - buffer.encode_string(9, this->release_summary); - buffer.encode_string(10, this->release_url); + buffer.encode_string(6, this->current_version_ref_); + buffer.encode_string(7, this->latest_version_ref_); + buffer.encode_string(8, this->title_ref_); + buffer.encode_string(9, this->release_summary_ref_); + buffer.encode_string(10, this->release_url_ref_); #ifdef USE_DEVICES buffer.encode_uint32(11, this->device_id); #endif @@ -2945,11 +2945,11 @@ void UpdateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->in_progress); ProtoSize::add_bool_field(total_size, 1, this->has_progress); ProtoSize::add_float_field(total_size, 1, this->progress); - ProtoSize::add_string_field(total_size, 1, this->current_version); - ProtoSize::add_string_field(total_size, 1, this->latest_version); - ProtoSize::add_string_field(total_size, 1, this->title); - ProtoSize::add_string_field(total_size, 1, this->release_summary); - ProtoSize::add_string_field(total_size, 1, this->release_url); + ProtoSize::add_string_field(total_size, 1, this->current_version_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->latest_version_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->title_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->release_summary_ref_.size()); + ProtoSize::add_string_field(total_size, 1, this->release_url_ref_.size()); #ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 1d052f6114..9bf50fd18b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3,6 +3,7 @@ #pragma once #include "esphome/core/defines.h" +#include "esphome/core/string_ref.h" #include "proto.h" @@ -269,12 +270,15 @@ enum UpdateCommand : uint32_t { class InfoResponseProtoMessage : public ProtoMessage { public: ~InfoResponseProtoMessage() override = default; - std::string object_id{}; + StringRef object_id_ref_{}; + void set_object_id(const StringRef &ref) { this->object_id_ref_ = ref; } uint32_t key{0}; - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } bool disabled_by_default{false}; #ifdef USE_ENTITY_ICON - std::string icon{}; + StringRef icon_ref_{}; + void set_icon(const StringRef &ref) { this->icon_ref_ = ref; } #endif enums::EntityCategory entity_category{}; #ifdef USE_DEVICES @@ -332,8 +336,10 @@ class HelloResponse : public ProtoMessage { #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; - std::string server_info{}; - std::string name{}; + StringRef server_info_ref_{}; + void set_server_info(const StringRef &ref) { this->server_info_ref_ = ref; } + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -442,7 +448,8 @@ class DeviceInfoRequest : public ProtoDecodableMessage { class AreaInfo : public ProtoMessage { public: uint32_t area_id{0}; - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -456,7 +463,8 @@ class AreaInfo : public ProtoMessage { class DeviceInfo : public ProtoMessage { public: uint32_t device_id{0}; - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } uint32_t area_id{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -477,19 +485,26 @@ class DeviceInfoResponse : public ProtoMessage { #ifdef USE_API_PASSWORD bool uses_password{false}; #endif - std::string name{}; - std::string mac_address{}; - std::string esphome_version{}; - std::string compilation_time{}; - std::string model{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } + StringRef mac_address_ref_{}; + void set_mac_address(const StringRef &ref) { this->mac_address_ref_ = ref; } + StringRef esphome_version_ref_{}; + void set_esphome_version(const StringRef &ref) { this->esphome_version_ref_ = ref; } + StringRef compilation_time_ref_{}; + void set_compilation_time(const StringRef &ref) { this->compilation_time_ref_ = ref; } + StringRef model_ref_{}; + void set_model(const StringRef &ref) { this->model_ref_ = ref; } #ifdef USE_DEEP_SLEEP bool has_deep_sleep{false}; #endif #ifdef ESPHOME_PROJECT_NAME - std::string project_name{}; + StringRef project_name_ref_{}; + void set_project_name(const StringRef &ref) { this->project_name_ref_ = ref; } #endif #ifdef ESPHOME_PROJECT_NAME - std::string project_version{}; + StringRef project_version_ref_{}; + void set_project_version(const StringRef &ref) { this->project_version_ref_ = ref; } #endif #ifdef USE_WEBSERVER uint32_t webserver_port{0}; @@ -497,16 +512,20 @@ class DeviceInfoResponse : public ProtoMessage { #ifdef USE_BLUETOOTH_PROXY uint32_t bluetooth_proxy_feature_flags{0}; #endif - std::string manufacturer{}; - std::string friendly_name{}; + StringRef manufacturer_ref_{}; + void set_manufacturer(const StringRef &ref) { this->manufacturer_ref_ = ref; } + StringRef friendly_name_ref_{}; + void set_friendly_name(const StringRef &ref) { this->friendly_name_ref_ = ref; } #ifdef USE_VOICE_ASSISTANT uint32_t voice_assistant_feature_flags{0}; #endif #ifdef USE_AREAS - std::string suggested_area{}; + StringRef suggested_area_ref_{}; + void set_suggested_area(const StringRef &ref) { this->suggested_area_ref_ = ref; } #endif #ifdef USE_BLUETOOTH_PROXY - std::string bluetooth_mac_address{}; + StringRef bluetooth_mac_address_ref_{}; + void set_bluetooth_mac_address(const StringRef &ref) { this->bluetooth_mac_address_ref_ = ref; } #endif #ifdef USE_API_NOISE bool api_encryption_supported{false}; @@ -575,7 +594,8 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_binary_sensor_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } bool is_status_binary_sensor{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -614,7 +634,8 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_tilt{false}; - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } bool supports_stop{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -695,7 +716,8 @@ class FanStateResponse : public StateResponseProtoMessage { bool oscillating{false}; enums::FanDirection direction{}; int32_t speed_level{0}; - std::string preset_mode{}; + StringRef preset_mode_ref_{}; + void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -769,7 +791,8 @@ class LightStateResponse : public StateResponseProtoMessage { float color_temperature{0.0f}; float cold_white{0.0f}; float warm_white{0.0f}; - std::string effect{}; + StringRef effect_ref_{}; + void set_effect(const StringRef &ref) { this->effect_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -829,10 +852,12 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_sensor_response"; } #endif - std::string unit_of_measurement{}; + StringRef unit_of_measurement_ref_{}; + void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; } int32_t accuracy_decimals{0}; bool force_update{false}; - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } enums::SensorStateClass state_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -869,7 +894,8 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_switch_response"; } #endif bool assumed_state{false}; - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -919,7 +945,8 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -935,7 +962,8 @@ class TextSensorStateResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_sensor_state_response"; } #endif - std::string state{}; + StringRef state_ref_{}; + void set_state(const StringRef &ref) { this->state_ref_ = ref; } bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1032,8 +1060,10 @@ class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage { }; class HomeassistantServiceMap : public ProtoMessage { public: - std::string key{}; - std::string value{}; + StringRef key_ref_{}; + void set_key(const StringRef &ref) { this->key_ref_ = ref; } + StringRef value_ref_{}; + void set_value(const StringRef &ref) { this->value_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1049,7 +1079,8 @@ class HomeassistantServiceResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_service_response"; } #endif - std::string service{}; + StringRef service_ref_{}; + void set_service(const StringRef &ref) { this->service_ref_ = ref; } std::vector data{}; std::vector data_template{}; std::vector variables{}; @@ -1082,8 +1113,10 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_home_assistant_state_response"; } #endif - std::string entity_id{}; - std::string attribute{}; + StringRef entity_id_ref_{}; + void set_entity_id(const StringRef &ref) { this->entity_id_ref_ = ref; } + StringRef attribute_ref_{}; + void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; } bool once{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1143,7 +1176,8 @@ class GetTimeResponse : public ProtoDecodableMessage { #ifdef USE_API_SERVICES class ListEntitiesServicesArgument : public ProtoMessage { public: - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } enums::ServiceArgType type{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1160,7 +1194,8 @@ class ListEntitiesServicesResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_services_response"; } #endif - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } uint32_t key{0}; std::vector args{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1312,9 +1347,11 @@ class ClimateStateResponse : public StateResponseProtoMessage { enums::ClimateAction action{}; enums::ClimateFanMode fan_mode{}; enums::ClimateSwingMode swing_mode{}; - std::string custom_fan_mode{}; + StringRef custom_fan_mode_ref_{}; + void set_custom_fan_mode(const StringRef &ref) { this->custom_fan_mode_ref_ = ref; } enums::ClimatePreset preset{}; - std::string custom_preset{}; + StringRef custom_preset_ref_{}; + void set_custom_preset(const StringRef &ref) { this->custom_preset_ref_ = ref; } float current_humidity{0.0f}; float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; @@ -1373,9 +1410,11 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage { float min_value{0.0f}; float max_value{0.0f}; float step{0.0f}; - std::string unit_of_measurement{}; + StringRef unit_of_measurement_ref_{}; + void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; } enums::NumberMode mode{}; - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1442,7 +1481,8 @@ class SelectStateResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "select_state_response"; } #endif - std::string state{}; + StringRef state_ref_{}; + void set_state(const StringRef &ref) { this->state_ref_ = ref; } bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1541,7 +1581,8 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_open{false}; bool requires_code{false}; - std::string code_format{}; + StringRef code_format_ref_{}; + void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1594,7 +1635,8 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_button_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1622,7 +1664,8 @@ class ButtonCommandRequest : public CommandProtoMessage { #ifdef USE_MEDIA_PLAYER class MediaPlayerSupportedFormat : public ProtoMessage { public: - std::string format{}; + StringRef format_ref_{}; + void set_format(const StringRef &ref) { this->format_ref_ = ref; } uint32_t sample_rate{0}; uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; @@ -2219,10 +2262,12 @@ class VoiceAssistantRequest : public ProtoMessage { const char *message_name() const override { return "voice_assistant_request"; } #endif bool start{false}; - std::string conversation_id{}; + StringRef conversation_id_ref_{}; + void set_conversation_id(const StringRef &ref) { this->conversation_id_ref_ = ref; } uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; - std::string wake_word_phrase{}; + StringRef wake_word_phrase_ref_{}; + void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2358,8 +2403,10 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { }; class VoiceAssistantWakeWord : public ProtoMessage { public: - std::string id{}; - std::string wake_word{}; + StringRef id_ref_{}; + void set_id(const StringRef &ref) { this->id_ref_ = ref; } + StringRef wake_word_ref_{}; + void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; } std::vector trained_languages{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2480,7 +2527,8 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage { #endif uint32_t min_length{0}; uint32_t max_length{0}; - std::string pattern{}; + StringRef pattern_ref_{}; + void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; } enums::TextMode mode{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2497,7 +2545,8 @@ class TextStateResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_state_response"; } #endif - std::string state{}; + StringRef state_ref_{}; + void set_state(const StringRef &ref) { this->state_ref_ = ref; } bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2641,7 +2690,8 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_event_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } std::vector event_types{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2658,7 +2708,8 @@ class EventResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "event_response"; } #endif - std::string event_type{}; + StringRef event_type_ref_{}; + void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2676,7 +2727,8 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_valve_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; @@ -2782,7 +2834,8 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_update_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2802,11 +2855,16 @@ class UpdateStateResponse : public StateResponseProtoMessage { bool in_progress{false}; bool has_progress{false}; float progress{0.0f}; - std::string current_version{}; - std::string latest_version{}; - std::string title{}; - std::string release_summary{}; - std::string release_url{}; + StringRef current_version_ref_{}; + void set_current_version(const StringRef &ref) { this->current_version_ref_ = ref; } + StringRef latest_version_ref_{}; + void set_latest_version(const StringRef &ref) { this->latest_version_ref_ = ref; } + StringRef title_ref_{}; + void set_title(const StringRef &ref) { this->title_ref_ = ref; } + StringRef release_summary_ref_{}; + void set_release_summary(const StringRef &ref) { this->release_summary_ref_ = ref; } + StringRef release_url_ref_{}; + void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 03852bd365..4e44bff11e 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -10,6 +10,15 @@ namespace esphome { namespace api { +// Helper function to append a quoted string, handling empty StringRef +static inline void append_quoted_string(std::string &out, const StringRef &ref) { + out.append("'"); + if (!ref.empty()) { + out.append(ref.c_str()); + } + out.append("'"); +} + template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: @@ -580,11 +589,11 @@ void HelloResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" server_info: "); - out.append("'").append(this->server_info).append("'"); + append_quoted_string(out, this->server_info_ref_); out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append("}"); } @@ -619,7 +628,7 @@ void AreaInfo::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append("}"); } @@ -634,7 +643,7 @@ void DeviceInfo::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" area_id: "); @@ -654,23 +663,23 @@ void DeviceInfoResponse::dump_to(std::string &out) const { #endif out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" mac_address: "); - out.append("'").append(this->mac_address).append("'"); + append_quoted_string(out, this->mac_address_ref_); out.append("\n"); out.append(" esphome_version: "); - out.append("'").append(this->esphome_version).append("'"); + append_quoted_string(out, this->esphome_version_ref_); out.append("\n"); out.append(" compilation_time: "); - out.append("'").append(this->compilation_time).append("'"); + append_quoted_string(out, this->compilation_time_ref_); out.append("\n"); out.append(" model: "); - out.append("'").append(this->model).append("'"); + append_quoted_string(out, this->model_ref_); out.append("\n"); #ifdef USE_DEEP_SLEEP @@ -681,13 +690,13 @@ void DeviceInfoResponse::dump_to(std::string &out) const { #endif #ifdef ESPHOME_PROJECT_NAME out.append(" project_name: "); - out.append("'").append(this->project_name).append("'"); + append_quoted_string(out, this->project_name_ref_); out.append("\n"); #endif #ifdef ESPHOME_PROJECT_NAME out.append(" project_version: "); - out.append("'").append(this->project_version).append("'"); + append_quoted_string(out, this->project_version_ref_); out.append("\n"); #endif @@ -706,11 +715,11 @@ void DeviceInfoResponse::dump_to(std::string &out) const { #endif out.append(" manufacturer: "); - out.append("'").append(this->manufacturer).append("'"); + append_quoted_string(out, this->manufacturer_ref_); out.append("\n"); out.append(" friendly_name: "); - out.append("'").append(this->friendly_name).append("'"); + append_quoted_string(out, this->friendly_name_ref_); out.append("\n"); #ifdef USE_VOICE_ASSISTANT @@ -722,13 +731,13 @@ void DeviceInfoResponse::dump_to(std::string &out) const { #endif #ifdef USE_AREAS out.append(" suggested_area: "); - out.append("'").append(this->suggested_area).append("'"); + append_quoted_string(out, this->suggested_area_ref_); out.append("\n"); #endif #ifdef USE_BLUETOOTH_PROXY out.append(" bluetooth_mac_address: "); - out.append("'").append(this->bluetooth_mac_address).append("'"); + append_quoted_string(out, this->bluetooth_mac_address_ref_); out.append("\n"); #endif @@ -770,7 +779,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesBinarySensorResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -779,11 +788,11 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); out.append(" is_status_binary_sensor: "); @@ -796,7 +805,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -844,7 +853,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesCoverResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -853,7 +862,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" assumed_state: "); @@ -869,7 +878,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); out.append(" disabled_by_default: "); @@ -878,7 +887,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -975,7 +984,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesFanResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -984,7 +993,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" supports_oscillation: "); @@ -1010,7 +1019,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -1020,7 +1029,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { for (const auto &it : this->supported_preset_modes) { out.append(" supported_preset_modes: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } @@ -1059,7 +1068,7 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" preset_mode: "); - out.append("'").append(this->preset_mode).append("'"); + append_quoted_string(out, this->preset_mode_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -1135,7 +1144,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesLightResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -1144,7 +1153,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); for (const auto &it : this->supported_color_modes) { @@ -1165,7 +1174,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { for (const auto &it : this->effects) { out.append(" effects: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } @@ -1175,7 +1184,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -1254,7 +1263,7 @@ void LightStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" effect: "); - out.append("'").append(this->effect).append("'"); + append_quoted_string(out, this->effect_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -1404,7 +1413,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesSensorResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -1413,17 +1422,17 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif out.append(" unit_of_measurement: "); - out.append("'").append(this->unit_of_measurement).append("'"); + append_quoted_string(out, this->unit_of_measurement_ref_); out.append("\n"); out.append(" accuracy_decimals: "); @@ -1436,7 +1445,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); out.append(" state_class: "); @@ -1492,7 +1501,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesSwitchResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -1501,12 +1510,12 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -1523,7 +1532,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -1583,7 +1592,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesTextSensorResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -1592,12 +1601,12 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -1610,7 +1619,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -1631,7 +1640,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" state: "); - out.append("'").append(this->state).append("'"); + append_quoted_string(out, this->state_ref_); out.append("\n"); out.append(" missing_state: "); @@ -1697,11 +1706,11 @@ void HomeassistantServiceMap::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("HomeassistantServiceMap {\n"); out.append(" key: "); - out.append("'").append(this->key).append("'"); + append_quoted_string(out, this->key_ref_); out.append("\n"); out.append(" value: "); - out.append("'").append(this->value).append("'"); + append_quoted_string(out, this->value_ref_); out.append("\n"); out.append("}"); } @@ -1709,7 +1718,7 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("HomeassistantServiceResponse {\n"); out.append(" service: "); - out.append("'").append(this->service).append("'"); + append_quoted_string(out, this->service_ref_); out.append("\n"); for (const auto &it : this->data) { @@ -1742,11 +1751,11 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SubscribeHomeAssistantStateResponse {\n"); out.append(" entity_id: "); - out.append("'").append(this->entity_id).append("'"); + append_quoted_string(out, this->entity_id_ref_); out.append("\n"); out.append(" attribute: "); - out.append("'").append(this->attribute).append("'"); + append_quoted_string(out, this->attribute_ref_); out.append("\n"); out.append(" once: "); @@ -1785,7 +1794,7 @@ void ListEntitiesServicesArgument::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesServicesArgument {\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" type: "); @@ -1797,7 +1806,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesServicesResponse {\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" key: "); @@ -1860,7 +1869,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { for (const auto &it : this->string_array) { out.append(" string_array: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } out.append("}"); @@ -1886,7 +1895,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesCameraResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -1895,7 +1904,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" disabled_by_default: "); @@ -1904,7 +1913,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -1964,7 +1973,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesClimateResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -1973,7 +1982,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); out.append(" supports_current_temperature: "); @@ -2023,7 +2032,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { for (const auto &it : this->supported_custom_fan_modes) { out.append(" supported_custom_fan_modes: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } @@ -2035,7 +2044,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { for (const auto &it : this->supported_custom_presets) { out.append(" supported_custom_presets: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } @@ -2045,7 +2054,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -2130,7 +2139,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" custom_fan_mode: "); - out.append("'").append(this->custom_fan_mode).append("'"); + append_quoted_string(out, this->custom_fan_mode_ref_); out.append("\n"); out.append(" preset: "); @@ -2138,7 +2147,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" custom_preset: "); - out.append("'").append(this->custom_preset).append("'"); + append_quoted_string(out, this->custom_preset_ref_); out.append("\n"); out.append(" current_humidity: "); @@ -2267,7 +2276,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesNumberResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -2276,12 +2285,12 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -2309,7 +2318,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" unit_of_measurement: "); - out.append("'").append(this->unit_of_measurement).append("'"); + append_quoted_string(out, this->unit_of_measurement_ref_); out.append("\n"); out.append(" mode: "); @@ -2317,7 +2326,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -2383,7 +2392,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesSelectResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -2392,18 +2401,18 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif for (const auto &it : this->options) { out.append(" options: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } @@ -2433,7 +2442,7 @@ void SelectStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" state: "); - out.append("'").append(this->state).append("'"); + append_quoted_string(out, this->state_ref_); out.append("\n"); out.append(" missing_state: "); @@ -2476,7 +2485,7 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesSirenResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -2485,12 +2494,12 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -2500,7 +2509,7 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { for (const auto &it : this->tones) { out.append(" tones: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } @@ -2603,7 +2612,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesLockResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -2612,12 +2621,12 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -2642,7 +2651,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" code_format: "); - out.append("'").append(this->code_format).append("'"); + append_quoted_string(out, this->code_format_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -2710,7 +2719,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesButtonResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -2719,12 +2728,12 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -2737,7 +2746,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -2772,7 +2781,7 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("MediaPlayerSupportedFormat {\n"); out.append(" format: "); - out.append("'").append(this->format).append("'"); + append_quoted_string(out, this->format_ref_); out.append("\n"); out.append(" sample_rate: "); @@ -2799,7 +2808,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesMediaPlayerResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -2808,12 +2817,12 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -3423,7 +3432,7 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" conversation_id: "); - out.append("'").append(this->conversation_id).append("'"); + append_quoted_string(out, this->conversation_id_ref_); out.append("\n"); out.append(" flags: "); @@ -3436,7 +3445,7 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" wake_word_phrase: "); - out.append("'").append(this->wake_word_phrase).append("'"); + append_quoted_string(out, this->wake_word_phrase_ref_); out.append("\n"); out.append("}"); } @@ -3557,16 +3566,16 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("VoiceAssistantWakeWord {\n"); out.append(" id: "); - out.append("'").append(this->id).append("'"); + append_quoted_string(out, this->id_ref_); out.append("\n"); out.append(" wake_word: "); - out.append("'").append(this->wake_word).append("'"); + append_quoted_string(out, this->wake_word_ref_); out.append("\n"); for (const auto &it : this->trained_languages) { out.append(" trained_languages: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } out.append("}"); @@ -3585,7 +3594,7 @@ void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { for (const auto &it : this->active_wake_words) { out.append(" active_wake_words: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } @@ -3600,7 +3609,7 @@ void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { out.append("VoiceAssistantSetConfiguration {\n"); for (const auto &it : this->active_wake_words) { out.append(" active_wake_words: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } out.append("}"); @@ -3611,7 +3620,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesAlarmControlPanelResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -3620,12 +3629,12 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -3711,7 +3720,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesTextResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -3720,12 +3729,12 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -3748,7 +3757,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" pattern: "); - out.append("'").append(this->pattern).append("'"); + append_quoted_string(out, this->pattern_ref_); out.append("\n"); out.append(" mode: "); @@ -3773,7 +3782,7 @@ void TextStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" state: "); - out.append("'").append(this->state).append("'"); + append_quoted_string(out, this->state_ref_); out.append("\n"); out.append(" missing_state: "); @@ -3816,7 +3825,7 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesDateResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -3825,12 +3834,12 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -3925,7 +3934,7 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesTimeResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -3934,12 +3943,12 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -4034,7 +4043,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesEventResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -4043,12 +4052,12 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -4061,12 +4070,12 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); for (const auto &it : this->event_types) { out.append(" event_types: "); - out.append("'").append(it).append("'"); + append_quoted_string(out, StringRef(it)); out.append("\n"); } @@ -4088,7 +4097,7 @@ void EventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" event_type: "); - out.append("'").append(this->event_type).append("'"); + append_quoted_string(out, this->event_type_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -4106,7 +4115,7 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesValveResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -4115,12 +4124,12 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -4133,7 +4142,7 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); out.append(" assumed_state: "); @@ -4219,7 +4228,7 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesDateTimeResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -4228,12 +4237,12 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -4308,7 +4317,7 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesUpdateResponse {\n"); out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); + append_quoted_string(out, this->object_id_ref_); out.append("\n"); out.append(" key: "); @@ -4317,12 +4326,12 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" name: "); - out.append("'").append(this->name).append("'"); + append_quoted_string(out, this->name_ref_); out.append("\n"); #ifdef USE_ENTITY_ICON out.append(" icon: "); - out.append("'").append(this->icon).append("'"); + append_quoted_string(out, this->icon_ref_); out.append("\n"); #endif @@ -4335,7 +4344,7 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); + append_quoted_string(out, this->device_class_ref_); out.append("\n"); #ifdef USE_DEVICES @@ -4373,23 +4382,23 @@ void UpdateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" current_version: "); - out.append("'").append(this->current_version).append("'"); + append_quoted_string(out, this->current_version_ref_); out.append("\n"); out.append(" latest_version: "); - out.append("'").append(this->latest_version).append("'"); + append_quoted_string(out, this->latest_version_ref_); out.append("\n"); out.append(" title: "); - out.append("'").append(this->title).append("'"); + append_quoted_string(out, this->title_ref_); out.append("\n"); out.append(" release_summary: "); - out.append("'").append(this->release_summary).append("'"); + append_quoted_string(out, this->release_summary_ref_); out.append("\n"); out.append(" release_url: "); - out.append("'").append(this->release_url).append("'"); + append_quoted_string(out, this->release_url_ref_); out.append("\n"); #ifdef USE_DEVICES diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 888dc16836..498c396ae3 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -597,33 +597,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } void APIServerConnection::on_hello_request(const HelloRequest &msg) { - HelloResponse ret = this->hello(msg); - if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) { + if (!this->send_hello_response(msg)) { this->on_fatal_error(); } } void APIServerConnection::on_connect_request(const ConnectRequest &msg) { - ConnectResponse ret = this->connect(msg); - if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) { + if (!this->send_connect_response(msg)) { this->on_fatal_error(); } } void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { - DisconnectResponse ret = this->disconnect(msg); - if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) { + if (!this->send_disconnect_response(msg)) { this->on_fatal_error(); } } void APIServerConnection::on_ping_request(const PingRequest &msg) { - PingResponse ret = this->ping(msg); - if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) { + if (!this->send_ping_response(msg)) { this->on_fatal_error(); } } void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { if (this->check_connection_setup_()) { - DeviceInfoResponse ret = this->device_info(msg); - if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) { + if (!this->send_device_info_response(msg)) { this->on_fatal_error(); } } @@ -656,8 +651,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc } void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { if (this->check_connection_setup_()) { - GetTimeResponse ret = this->get_time(msg); - if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) { + if (!this->send_get_time_response(msg)) { this->on_fatal_error(); } } @@ -672,8 +666,7 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest #ifdef USE_API_NOISE void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { if (this->check_authenticated_()) { - NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); - if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) { + if (!this->send_noise_encryption_set_key_response(msg)) { this->on_fatal_error(); } } @@ -866,8 +859,7 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo void APIServerConnection::on_subscribe_bluetooth_connections_free_request( const SubscribeBluetoothConnectionsFreeRequest &msg) { if (this->check_authenticated_()) { - BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); - if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) { + if (!this->send_subscribe_bluetooth_connections_free_response(msg)) { this->on_fatal_error(); } } @@ -898,8 +890,7 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { if (this->check_authenticated_()) { - VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); - if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) { + if (!this->send_voice_assistant_get_configuration_response(msg)) { this->on_fatal_error(); } } diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index f7076a28ca..f06ebdf9d5 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -207,22 +207,22 @@ class APIServerConnectionBase : public ProtoService { class APIServerConnection : public APIServerConnectionBase { public: - virtual HelloResponse hello(const HelloRequest &msg) = 0; - virtual ConnectResponse connect(const ConnectRequest &msg) = 0; - virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0; - virtual PingResponse ping(const PingRequest &msg) = 0; - virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0; + virtual bool send_hello_response(const HelloRequest &msg) = 0; + virtual bool send_connect_response(const ConnectRequest &msg) = 0; + virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; + virtual bool send_ping_response(const PingRequest &msg) = 0; + virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; virtual void list_entities(const ListEntitiesRequest &msg) = 0; virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; - virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; + virtual bool send_get_time_response(const GetTimeRequest &msg) = 0; #ifdef USE_API_SERVICES virtual void execute_service(const ExecuteServiceRequest &msg) = 0; #endif #ifdef USE_API_NOISE - virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; + virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0; #endif #ifdef USE_BUTTON virtual void button_command(const ButtonCommandRequest &msg) = 0; @@ -303,7 +303,7 @@ class APIServerConnection : public APIServerConnectionBase { virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; #endif #ifdef USE_BLUETOOTH_PROXY - virtual BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( + virtual bool send_subscribe_bluetooth_connections_free_response( const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; #endif #ifdef USE_BLUETOOTH_PROXY @@ -316,8 +316,7 @@ class APIServerConnection : public APIServerConnectionBase { virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; #endif #ifdef USE_VOICE_ASSISTANT - virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration( - const VoiceAssistantConfigurationRequest &msg) = 0; + virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0; #endif #ifdef USE_VOICE_ASSISTANT virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 35329c4a5e..6375dbfb9f 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -148,7 +148,7 @@ class CustomAPIDevice { */ void call_homeassistant_service(const std::string &service_name) { HomeassistantServiceResponse resp; - resp.service = service_name; + resp.set_service(StringRef(service_name)); global_api_server->send_homeassistant_service_call(resp); } @@ -168,12 +168,12 @@ class CustomAPIDevice { */ void call_homeassistant_service(const std::string &service_name, const std::map &data) { HomeassistantServiceResponse resp; - resp.service = service_name; + resp.set_service(StringRef(service_name)); for (auto &it : data) { - HomeassistantServiceMap kv; - kv.key = it.first; - kv.value = it.second; - resp.data.push_back(kv); + resp.data.emplace_back(); + auto &kv = resp.data.back(); + kv.set_key(StringRef(it.first)); + kv.set_value(StringRef(it.second)); } global_api_server->send_homeassistant_service_call(resp); } @@ -190,7 +190,7 @@ class CustomAPIDevice { */ void fire_homeassistant_event(const std::string &event_name) { HomeassistantServiceResponse resp; - resp.service = event_name; + resp.set_service(StringRef(event_name)); resp.is_event = true; global_api_server->send_homeassistant_service_call(resp); } @@ -210,13 +210,13 @@ class CustomAPIDevice { */ void fire_homeassistant_event(const std::string &service_name, const std::map &data) { HomeassistantServiceResponse resp; - resp.service = service_name; + resp.set_service(StringRef(service_name)); resp.is_event = true; for (auto &it : data) { - HomeassistantServiceMap kv; - kv.key = it.first; - kv.value = it.second; - resp.data.push_back(kv); + resp.data.emplace_back(); + auto &kv = resp.data.back(); + kv.set_key(StringRef(it.first)); + kv.set_value(StringRef(it.second)); } global_api_server->send_homeassistant_service_call(resp); } diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index f765f1f806..4980c0224c 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -59,25 +59,29 @@ template class HomeAssistantServiceCallAction : public Actionservice_.value(x...); + std::string service_value = this->service_.value(x...); + resp.set_service(StringRef(service_value)); resp.is_event = this->is_event_; for (auto &it : this->data_) { - HomeassistantServiceMap kv; - kv.key = it.key; - kv.value = it.value.value(x...); - resp.data.push_back(kv); + resp.data.emplace_back(); + auto &kv = resp.data.back(); + kv.set_key(StringRef(it.key)); + std::string value = it.value.value(x...); + kv.set_value(StringRef(value)); } for (auto &it : this->data_template_) { - HomeassistantServiceMap kv; - kv.key = it.key; - kv.value = it.value.value(x...); - resp.data_template.push_back(kv); + resp.data_template.emplace_back(); + auto &kv = resp.data_template.back(); + kv.set_key(StringRef(it.key)); + std::string value = it.value.value(x...); + kv.set_value(StringRef(value)); } for (auto &it : this->variables_) { - HomeassistantServiceMap kv; - kv.key = it.key; - kv.value = it.value.value(x...); - resp.variables.push_back(kv); + resp.variables.emplace_back(); + auto &kv = resp.variables.back(); + kv.set_key(StringRef(it.key)); + std::string value = it.value.value(x...); + kv.set_value(StringRef(value)); } this->parent_->send_homeassistant_service_call(resp); } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 6ae4556cc1..3e59ee1541 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include #include @@ -15,6 +16,37 @@ namespace esphome { namespace api { +/* + * StringRef Ownership Model for API Protocol Messages + * =================================================== + * + * StringRef is used for zero-copy string handling in outgoing (SOURCE_SERVER) messages. + * It holds a pointer and length to existing string data without copying. + * + * CRITICAL: The referenced string data MUST remain valid until message encoding completes. + * + * Safe StringRef Patterns: + * 1. String literals: StringRef("literal") - Always safe (static storage duration) + * 2. Member variables: StringRef(this->member_string_) - Safe if object outlives encoding + * 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe + * 4. Local variables: Safe ONLY if encoding happens before function returns: + * std::string temp = compute_value(); + * msg.set_field(StringRef(temp)); + * return this->send_message(msg); // temp is valid during encoding + * + * Unsafe Patterns (WILL cause crashes/corruption): + * 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value + * 2. Optional values: msg.set_field(StringRef(optional.value())) // value() returns a copy + * 3. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary + * + * For unsafe patterns, store in a local variable first: + * std::string temp = optional.value(); // or get_string() or str1 + str2 + * msg.set_field(StringRef(temp)); + * + * The send_*_response pattern ensures proper lifetime management by encoding + * within the same function scope where temporaries are created. + */ + /// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit class ProtoVarInt { public: @@ -218,6 +250,9 @@ class ProtoWriteBuffer { void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); } + void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) { + this->encode_string(field_id, ref.c_str(), ref.size(), force); + } void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { this->encode_string(field_id, reinterpret_cast(data), len, force); } @@ -669,17 +704,16 @@ class ProtoSize { // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems /** - * @brief Calculates and adds the size of a string/bytes field to the total message size + * @brief Calculates and adds the size of a string field using length */ - static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str) { + static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, size_t len) { // Skip calculation if string is empty - if (str.empty()) { + if (len == 0) { return; // No need to update total_size } - // Calculate and directly add to total_size - const uint32_t str_size = static_cast(str.size()); - total_size += field_id_size + varint(str_size) + str_size; + // Field ID + length varint + string bytes + total_size += field_id_size + varint(static_cast(len)) + static_cast(len); } /** diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 1420a15ff9..deec636cae 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -33,14 +33,14 @@ template class UserServiceBase : public UserServiceDescriptor { ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse msg; - msg.name = this->name_; + msg.set_name(StringRef(this->name_)); msg.key = this->key_; std::array arg_types = {to_service_arg_type()...}; for (int i = 0; i < sizeof...(Ts); i++) { - ListEntitiesServicesArgument arg; + msg.args.emplace_back(); + auto &arg = msg.args.back(); arg.type = arg_types[i]; - arg.name = this->arg_names_[i]; - msg.args.push_back(arg); + arg.set_name(StringRef(this->arg_names_[i])); } return msg; } diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index a7f71c3244..ffb352c969 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -83,18 +83,24 @@ void HomeassistantNumber::control(float value) { this->publish_state(value); + static constexpr auto SERVICE_NAME = StringRef::from_lit("number.set_value"); + static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); + static constexpr auto VALUE_KEY = StringRef::from_lit("value"); + api::HomeassistantServiceResponse resp; - resp.service = "number.set_value"; + resp.set_service(SERVICE_NAME); - api::HomeassistantServiceMap entity_id; - entity_id.key = "entity_id"; - entity_id.value = this->entity_id_; - resp.data.push_back(entity_id); + resp.data.emplace_back(); + auto &entity_id = resp.data.back(); + entity_id.set_key(ENTITY_ID_KEY); + entity_id.set_value(StringRef(this->entity_id_)); - api::HomeassistantServiceMap entity_value; - entity_value.key = "value"; - entity_value.value = to_string(value); - resp.data.push_back(entity_value); + resp.data.emplace_back(); + auto &entity_value = resp.data.back(); + entity_value.set_key(VALUE_KEY); + // to_string() returns a temporary - must store it to avoid dangling reference + std::string value_str = to_string(value); + entity_value.set_value(StringRef(value_str)); api::global_api_server->send_homeassistant_service_call(resp); } diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp index 0451c95069..0fe609bf43 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.cpp +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -40,17 +40,21 @@ void HomeassistantSwitch::write_state(bool state) { return; } + static constexpr auto SERVICE_ON = StringRef::from_lit("homeassistant.turn_on"); + static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off"); + static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); + api::HomeassistantServiceResponse resp; if (state) { - resp.service = "homeassistant.turn_on"; + resp.set_service(SERVICE_ON); } else { - resp.service = "homeassistant.turn_off"; + resp.set_service(SERVICE_OFF); } - api::HomeassistantServiceMap entity_id_kv; - entity_id_kv.key = "entity_id"; - entity_id_kv.value = this->entity_id_; - resp.data.push_back(entity_id_kv); + resp.data.emplace_back(); + auto &entity_id_kv = resp.data.back(); + entity_id_kv.set_key(ENTITY_ID_KEY); + entity_id_kv.set_value(StringRef(this->entity_id_)); api::global_api_server->send_homeassistant_service_call(resp); } diff --git a/esphome/components/text/text_traits.h b/esphome/components/text/text_traits.h index 952afa70c7..ceaba2dead 100644 --- a/esphome/components/text/text_traits.h +++ b/esphome/components/text/text_traits.h @@ -3,6 +3,7 @@ #include #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" namespace esphome { namespace text { @@ -23,6 +24,7 @@ class TextTraits { // Set/get the pattern. void set_pattern(std::string pattern) { this->pattern_ = std::move(pattern); } std::string get_pattern() const { return this->pattern_; } + StringRef get_pattern_ref() const { return StringRef(this->pattern_); } // Set/get the frontend mode. void set_mode(TextMode mode) { this->mode_ = mode; } diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 481a4efa62..743c90e700 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -238,10 +238,10 @@ void VoiceAssistant::loop() { api::VoiceAssistantRequest msg; msg.start = true; - msg.conversation_id = this->conversation_id_; + msg.set_conversation_id(StringRef(this->conversation_id_)); msg.flags = flags; msg.audio_settings = audio_settings; - msg.wake_word_phrase = this->wake_word_; + msg.set_wake_word_phrase(StringRef(this->wake_word_)); this->wake_word_ = ""; // Reset media player state tracking diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 00b1264ed0..e60e0728bc 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -54,6 +54,14 @@ class EntityBase { // Get/set this entity's icon std::string get_icon() const; void set_icon(const char *icon); + StringRef get_icon_ref() const { + static constexpr auto EMPTY_STRING = StringRef::from_lit(""); +#ifdef USE_ENTITY_ICON + return this->icon_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->icon_c_str_); +#else + return EMPTY_STRING; +#endif + } #ifdef USE_DEVICES // Get/set this entity's device id @@ -105,6 +113,11 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) std::string get_device_class(); /// Manually set the device class. void set_device_class(const char *device_class); + /// Get the device class as StringRef + StringRef get_device_class_ref() const { + static constexpr auto EMPTY_STRING = StringRef::from_lit(""); + return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_); + } protected: const char *device_class_{nullptr}; ///< Device class override @@ -116,6 +129,11 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) std::string get_unit_of_measurement(); /// Manually set the unit of measurement. void set_unit_of_measurement(const char *unit_of_measurement); + /// Get the unit of measurement as StringRef + StringRef get_unit_of_measurement_ref() const { + static constexpr auto EMPTY_STRING = StringRef::from_lit(""); + return this->unit_of_measurement_ == nullptr ? EMPTY_STRING : StringRef(this->unit_of_measurement_); + } protected: const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2d49ac5ece..766a84c9fd 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -343,6 +343,10 @@ def create_field_type_info( if field.type == 12: return BytesType(field, needs_decode, needs_encode) + # Special handling for string fields + if field.type == 9: + return StringType(field, needs_decode, needs_encode) + validate_field_type(field.type, field.name) return TYPE_INFO[field.type](field) @@ -543,12 +547,67 @@ class StringType(TypeInfo): encode_func = "encode_string" wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 + @property + def public_content(self) -> list[str]: + content: list[str] = [] + # Add std::string storage if message needs decoding + if self._needs_decode: + content.append(f"std::string {self.field_name}{{}};") + + if self._needs_encode: + content.extend( + [ + # Add StringRef field if message needs encoding + f"StringRef {self.field_name}_ref_{{}};", + # Add setter method if message needs encoding + f"void set_{self.field_name}(const StringRef &ref) {{", + f" this->{self.field_name}_ref_ = ref;", + "}", + ] + ) + return content + + @property + def encode_content(self) -> str: + return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" + def dump(self, name): - o = f'out.append("\'").append({name}).append("\'");' - return o + # If name is 'it', this is a repeated field element - always use string + if name == "it": + return "append_quoted_string(out, StringRef(it));" + + # For SOURCE_CLIENT only, always use std::string + if not self._needs_encode: + return f'out.append("\'").append(this->{self.field_name}).append("\'");' + + # For SOURCE_SERVER, always use StringRef + if not self._needs_decode: + return f"append_quoted_string(out, this->{self.field_name}_ref_);" + + # For SOURCE_BOTH, check if StringRef is set (sending) or use string (received) + return ( + f"if (!this->{self.field_name}_ref_.empty()) {{" + f' out.append("\'").append(this->{self.field_name}_ref_.c_str()).append("\'");' + f"}} else {{" + f' out.append("\'").append(this->{self.field_name}).append("\'");' + f"}}" + ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_string_field") + # For SOURCE_CLIENT only messages, use the string field directly + if not self._needs_encode: + return self._get_simple_size_calculation(name, force, "add_string_field") + + # Check if this is being called from a repeated field context + # In that case, 'name' will be 'it' and we need to use the repeated version + if name == "it": + # For repeated fields, we need to use add_string_field_repeated which includes field ID + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_string_field_repeated(total_size, {field_id_size}, it);" + + # For messages that need encoding, use the StringRef size + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_string_field(total_size, {field_id_size}, this->{self.field_name}_ref_.size());" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -1902,6 +1961,7 @@ def main() -> None: #pragma once #include "esphome/core/defines.h" +#include "esphome/core/string_ref.h" #include "proto.h" @@ -1935,6 +1995,15 @@ namespace api { namespace esphome { namespace api { +// Helper function to append a quoted string, handling empty StringRef +static inline void append_quoted_string(std::string &out, const StringRef &ref) { + out.append("'"); + if (!ref.empty()) { + out.append(ref.c_str()); + } + out.append("'"); +} + """ content += "namespace enums {\n\n" @@ -2174,7 +2243,13 @@ static const char *const TAG = "api.service"; cpp += f"#ifdef {ifdef}\n" hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" - hpp += f" virtual {ret} {func}(const {inp} &msg) = 0;\n" + + # For non-void methods, generate a send_ method instead of return-by-value + if is_void: + hpp += f" virtual void {func}(const {inp} &msg) = 0;\n" + else: + hpp += f" virtual bool send_{func}_response(const {inp} &msg) = 0;\n" + cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" # Start with authentication/connection check if needed @@ -2192,10 +2267,7 @@ static const char *const TAG = "api.service"; if is_void: handler_body = f"this->{func}(msg);\n" else: - handler_body = f"{ret} ret = this->{func}(msg);\n" - handler_body += ( - f"if (!this->send_message(ret, {ret}::MESSAGE_TYPE)) {{\n" - ) + handler_body = f"if (!this->send_{func}_response(msg)) {{\n" handler_body += " this->on_fatal_error();\n" handler_body += "}\n" @@ -2207,8 +2279,7 @@ static const char *const TAG = "api.service"; if is_void: body += f"this->{func}(msg);\n" else: - body += f"{ret} ret = this->{func}(msg);\n" - body += f"if (!this->send_message(ret, {ret}::MESSAGE_TYPE)) {{\n" + body += f"if (!this->send_{func}_response(msg)) {{\n" body += " this->on_fatal_error();\n" body += "}\n" From 3bb5a9e2f740c1067e7514bb55cdabece757d735 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 22 Jul 2025 15:52:56 -0300 Subject: [PATCH 074/125] [schema-gen] fix referenced schemas when schema in component platform (#9755) --- script/build_language_schema.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 4473ec1b5a..960c35ca0f 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -411,11 +411,16 @@ def add_referenced_recursive(referenced_schemas, config_var, path, eat_schema=Fa s1 = get_str_path_schema(k) p = k.split(".") - if len(p) == 3 and path[0] == f"{p[0]}.{p[1]}": - # special case for schema inside platforms - add_referenced_recursive( - referenced_schemas, s1, [path[0], "schemas", p[2]] - ) + if len(p) == 3: + if path[0] == f"{p[0]}.{p[1]}": + # special case for schema inside platforms + add_referenced_recursive( + referenced_schemas, s1, [path[0], "schemas", p[2]] + ) + else: + add_referenced_recursive( + referenced_schemas, s1, [f"{p[0]}.{p[1]}", "schemas", p[2]] + ) else: add_referenced_recursive( referenced_schemas, s1, [p[0], "schemas", p[1]] From cf403062979626731fc34e426618c77680655c2b Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Tue, 22 Jul 2025 22:24:40 +0200 Subject: [PATCH 075/125] [audio] fix typo `gneneral` and `divison` (#9808) --- esphome/components/audio/audio.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index 95c31872e3..e01d7eb101 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -15,7 +15,7 @@ class AudioStreamInfo { * - An audio sample represents a unit of audio for one channel. * - A frame represents a unit of audio with a sample for every channel. * - * In gneneral, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames + * In general, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames * are used as the main unit when transferring audio data. Durations may result in rounding for certain sample rates; * e.g., 44.1 KHz. The ``frames_to_milliseconds_with_remainder`` function should be used for accuracy, as it takes * into account the remainder rather than just ignoring any rounding. @@ -76,7 +76,7 @@ class AudioStreamInfo { /// @brief Computes the duration, in microseconds, the given amount of frames represents. /// @param frames Number of audio frames - /// @return Duration in microseconds `frames` respresents. May be slightly inaccurate due to integer divison rounding + /// @return Duration in microseconds `frames` represents. May be slightly inaccurate due to integer division rounding /// for certain sample rates. uint32_t frames_to_microseconds(uint32_t frames) const; From e2976162b5f9f76cf2f305f156d4d7a46b4a17c9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:54:03 -0400 Subject: [PATCH 076/125] [sgp4x] Fix build (#9794) --- esphome/components/sgp4x/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 4f29248881..7c6fe580b2 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -139,7 +139,7 @@ async def to_code(config): ) ) cg.add_library( - None, + "Sensirion Gas Index Algorithm", None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1", ) From 1a7757e7ca5711c8c97aa0e9d225567f8ec9a57d Mon Sep 17 00:00:00 2001 From: Stas Date: Wed, 23 Jul 2025 00:39:03 +0300 Subject: [PATCH 077/125] [http_request] set correct duration_ms for failed requests (#9789) --- esphome/components/http_request/http_request_idf.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 68c06d28f2..89a0891b03 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -157,8 +157,8 @@ std::shared_ptr HttpRequestIDF::perform(std::string url, std::str container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); container->set_response_headers(user_data.response_headers); + container->duration_ms = millis() - start; if (is_success(container->status_code)) { - container->duration_ms = millis() - start; return container; } @@ -191,8 +191,8 @@ std::shared_ptr HttpRequestIDF::perform(std::string url, std::str container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); + container->duration_ms = millis() - start; if (is_success(container->status_code)) { - container->duration_ms = millis() - start; return container; } From 5a4e2a3eaf7c1b951e86a9ed62c3e8955682d8f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:56:00 +1200 Subject: [PATCH 078/125] [udp] Move ``on_receive`` to const (#9811) --- esphome/components/const/__init__.py | 1 + esphome/components/udp/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 5b40545d89..18ef4d48b6 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -5,5 +5,6 @@ CODEOWNERS = ["@esphome/core"] CONF_BYTE_ORDER = "byte_order" CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index ed405d7c22..6b1e4f8ed8 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -1,6 +1,7 @@ from esphome import automation from esphome.automation import Trigger import esphome.codegen as cg +from esphome.components.const import CONF_ON_RECEIVE from esphome.components.packet_transport import ( CONF_BINARY_SENSORS, CONF_ENCRYPTION, @@ -27,7 +28,6 @@ trigger_args = cg.std_vector.template(cg.uint8) CONF_ADDRESSES = "addresses" CONF_LISTEN_ADDRESS = "listen_address" CONF_UDP_ID = "udp_id" -CONF_ON_RECEIVE = "on_receive" CONF_LISTEN_PORT = "listen_port" CONF_BROADCAST_PORT = "broadcast_port" From 116c91e9c5fc6d0d32191bd4e6d6e406e2bff6bf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:15:31 -0400 Subject: [PATCH 079/125] Bump ESP32 IDF version to 5.4.2 and Arduino version to 3.2.1 (#9770) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 16 ++++++++-------- esphome/components/ethernet/esp_eth_phy_jl1101.c | 5 +++++ esphome/components/ethernet/ethernet_component.h | 4 ++++ esphome/core/defines.h | 3 +-- platformio.ini | 8 ++++---- script/clang-tidy | 2 +- 7 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 50a7fa9709..9efe5b2270 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931 +7920671c938a5ea6a11ac4594204b5ec8f38d579c962bf1f185e8d5e3ad879be diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6ddb579733..587d75b64b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -310,19 +310,19 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) # The platform-espressif32 version to use for arduino frameworks # - https://github.com/pioarduino/platform-espressif32/releases -ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13) +ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 3, 2) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(53, 3, 13) +ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21) # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ @@ -357,8 +357,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(3, 1, 3), None), + "dev": (cv.Version(3, 2, 1), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(3, 2, 1), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -396,8 +396,8 @@ def _arduino_check_versions(value): def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 3, 2), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 3, 2), None), + "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(5, 2, 2), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 4f31e0a9fd..5e73e99101 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -25,6 +25,9 @@ #include "driver/gpio.h" #include "esp_rom_gpio.h" #include "esp_rom_sys.h" +#include "esp_idf_version.h" + +#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) static const char *TAG = "jl1101"; #define PHY_CHECK(a, str, goto_tag, ...) \ @@ -336,4 +339,6 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { err: return NULL; } + +#endif /* USE_ARDUINO */ #endif /* USE_ESP32 */ diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 1b347946f5..bdcda6afb4 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -11,6 +11,7 @@ #include "esp_eth_mac.h" #include "esp_netif.h" #include "esp_mac.h" +#include "esp_idf_version.h" namespace esphome { namespace ethernet { @@ -153,7 +154,10 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; + +#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); +#endif } // namespace ethernet } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 19d380bd29..9355d56084 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -166,12 +166,11 @@ #define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 3) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1) #define USE_ETHERNET #endif #ifdef USE_ESP_IDF -#define USE_ESP_IDF_VERSION_CODE VERSION_CODE(5, 3, 2) #define USE_MICRO_WAKE_WORD #define USE_MICRO_WAKE_WORD_VAD #if defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) diff --git a/platformio.ini b/platformio.ini index 7fb301c08b..350f220853 100644 --- a/platformio.ini +++ b/platformio.ini @@ -125,9 +125,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.1.3/esp32-3.1.3.zip + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip framework = arduino lib_deps = @@ -161,9 +161,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.3.2/esp-idf-v5.3.2.zip + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip framework = espidf lib_deps = diff --git a/script/clang-tidy b/script/clang-tidy index b5905e0e4e..187acd02ad 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -105,7 +105,7 @@ def clang_options(idedata): # idedata contains include directories for all toolchains of this platform, only use those from the one in use toolchain_dir = os.path.normpath(f"{idedata['cxx_path']}/../../") for directory in idedata["includes"]["toolchain"]: - if directory.startswith(toolchain_dir): + if directory.startswith(toolchain_dir) and "picolibc" not in directory: cmd.extend(["-isystem", directory]) # add library include directories using -isystem to suppress their errors From a994ad364257a1d8c47978d0304ef500cc5d2c88 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:28:15 +1000 Subject: [PATCH 080/125] Workflow - check all comments to find previous bot comment (#9815) --- .github/workflows/external-component-bot.yml | 26 ++++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 5f5bc703ad..29103e8eee 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -61,7 +61,8 @@ jobs: } async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) { - const commentMarker = ""; + const commentMarker = ""; + const legacyCommentMarker = ""; let commentBody; if (esphomeChanges.length === 1) { commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo); @@ -71,14 +72,23 @@ jobs: commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`; // Check for existing bot comment - const comments = await github.rest.issues.listComments({ - owner: owner, - repo: repo, - issue_number: prNumber, - }); + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner: owner, + repo: repo, + issue_number: prNumber, + per_page: 100, + } + ); - const botComment = comments.data.find(comment => - comment.body.includes(commentMarker) + const sorted = comments.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + + const botComment = sorted.find(comment => + ( + comment.body.includes(commentMarker) || + comment.body.includes(legacyCommentMarker) + ) && comment.user.type === "Bot" ); if (botComment && botComment.body === commentBody) { From 7bfb08e60233e7ec6271400adcc05e3d4364d464 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:46:14 +1200 Subject: [PATCH 081/125] [core] Match LockFreeQueue initialization order (#9813) --- esphome/core/lock_free_queue.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index de07b0ebba..68e2825d09 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -29,7 +29,7 @@ namespace esphome { // Base lock-free queue without task notification template class LockFreeQueue { public: - LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} + LockFreeQueue() : dropped_count_(0), head_(0), tail_(0) {} bool push(T *element) { bool was_empty; From ac7f125eb50ee8ea38b9addb7806273fd8fc0d0a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:22:54 +1200 Subject: [PATCH 082/125] [CI] Paginate codeowner comments to make sure we find it (#9817) --- .github/workflows/codeowner-review-request.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 121619e049..ab3377365d 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -182,11 +182,14 @@ jobs: }); // Check for previous comments from this workflow to avoid duplicate pings - const { data: comments } = await github.rest.issues.listComments({ - owner, - repo, - issue_number: pr_number - }); + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner, + repo, + issue_number: pr_number + } + ); const previouslyPingedUsers = new Set(); const previouslyPingedTeams = new Set(); @@ -194,7 +197,6 @@ jobs: // Look for comments from github-actions bot that contain our bot marker const workflowComments = comments.filter(comment => comment.user.type === 'Bot' && - comment.user.login === 'github-actions[bot]' && comment.body.includes(BOT_COMMENT_MARKER) ); From d7a5db3dda6655d2e2795eca12af80012ffba2fc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:23:06 +1200 Subject: [PATCH 083/125] [CI] Paginate codeowner comments to make sure we find it (#9818) --- .github/workflows/issue-codeowner-notify.yml | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index 27976a7952..3639d346f5 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -29,6 +29,9 @@ jobs: console.log(`Processing issue #${issue_number} with label: ${labelName}`); + // Hidden marker to identify bot comments from this workflow + const BOT_COMMENT_MARKER = ''; + // Extract component name from label const componentName = labelName.replace('component: ', ''); console.log(`Component: ${componentName}`); @@ -93,11 +96,14 @@ jobs: ); // Check for previous comments from this workflow to avoid duplicate pings - const { data: comments } = await github.rest.issues.listComments({ - owner, - repo, - issue_number: issue_number - }); + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner, + repo, + issue_number: issue_number + } + ); const previouslyPingedUsers = new Set(); const previouslyPingedTeams = new Set(); @@ -105,9 +111,8 @@ jobs: // Look for comments from github-actions bot that contain codeowner pings for this component const workflowComments = comments.filter(comment => comment.user.type === 'Bot' && - comment.user.login === 'github-actions[bot]' && - comment.body.includes(`component: ${componentName}`) && - comment.body.includes("you've been identified as a codeowner") + comment.body.includes(BOT_COMMENT_MARKER) && + comment.body.includes(`component: ${componentName}`) ); // Extract previously mentioned users and teams from workflow comments @@ -140,7 +145,7 @@ jobs: // Create comment body const mentionString = allMentions.join(', '); - const commentBody = `👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`; + const commentBody = `${BOT_COMMENT_MARKER}\n👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`; // Post comment await github.rest.issues.createComment({ From b636b844fce714b0f927eacd268aca763c4d329b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Jul 2025 16:43:22 -1000 Subject: [PATCH 084/125] [core] Initialize looping_components_ before setup blocking phase (#9820) --- esphome/core/application.cpp | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 873f342277..4fedd36120 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -55,6 +55,9 @@ void Application::setup() { return a->get_actual_setup_priority() > b->get_actual_setup_priority(); }); + // Initialize looping_components_ early so enable_pending_loops_() works during setup + this->calculate_looping_components_(); + for (uint32_t i = 0; i < this->components_.size(); i++) { Component *component = this->components_[i]; @@ -97,7 +100,6 @@ void Application::setup() { clear_setup_priority_overrides(); this->schedule_dump_config(); - this->calculate_looping_components_(); } void Application::loop() { uint8_t new_app_state = 0; @@ -269,24 +271,16 @@ void Application::calculate_looping_components_() { // Pre-reserve vector to avoid reallocations this->looping_components_.reserve(total_looping); - // First add all active components + // Add all components with loop override + // When called at start of setup, all components are in CONSTRUCTION state + // so none will be LOOP_DONE yet - they'll all go in the active section for (auto *obj : this->components_) { - if (obj->has_overridden_loop() && - (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { + if (obj->has_overridden_loop()) { this->looping_components_.push_back(obj); } } this->looping_components_active_end_ = this->looping_components_.size(); - - // Then add all inactive (LOOP_DONE) components - // This handles components that called disable_loop() during setup, before this method runs - for (auto *obj : this->components_) { - if (obj->has_overridden_loop() && - (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { - this->looping_components_.push_back(obj); - } - } } void Application::disable_component_loop_(Component *component) { From bb6f8aeb94922fb04fd03499695b56667de2ed92 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 22 Jul 2025 22:57:42 -0400 Subject: [PATCH 085/125] [remote_receiver] Fix idle validation (#9819) --- esphome/components/remote_receiver/__init__.py | 18 +++++++++++++++++- .../remote_receiver/remote_receiver_esp32.cpp | 3 +-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index dffc088085..9095016b55 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -60,6 +60,20 @@ RemoteReceiverComponent = remote_receiver_ns.class_( ) +def validate_config(config): + if CORE.is_esp32: + variant = esp32.get_esp32_variant() + if variant in (esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S2): + max_idle = 65535 + else: + max_idle = 32767 + if CONF_CLOCK_RESOLUTION in config: + max_idle = int(max_idle * 1000000 / config[CONF_CLOCK_RESOLUTION]) + if config[CONF_IDLE].total_microseconds > max_idle: + raise cv.Invalid(f"config 'idle' exceeds the maximum value of {max_idle}us") + return config + + def validate_tolerance(value): if isinstance(value, dict): return TOLERANCE_SCHEMA(value) @@ -136,7 +150,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.boolean, ), } - ).extend(cv.COMPONENT_SCHEMA) + ) + .extend(cv.COMPONENT_SCHEMA) + .add_extra(validate_config) ) diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 3d6346baec..3e6172c6d6 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -86,10 +86,9 @@ void RemoteReceiverComponent::setup() { uint32_t event_size = sizeof(rmt_rx_done_event_data_t); uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000); - uint32_t max_idle_ns = 65535u * 1000; memset(&this->store_.config, 0, sizeof(this->store_.config)); this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns); - this->store_.config.signal_range_max_ns = std::min(this->idle_us_ * 1000, max_idle_ns); + this->store_.config.signal_range_max_ns = this->idle_us_ * 1000; this->store_.filter_symbols = this->filter_symbols_; this->store_.receive_size = this->receive_symbols_ * sizeof(rmt_symbol_word_t); this->store_.buffer_size = std::max((event_size + this->store_.receive_size) * 2, this->buffer_size_); From babaa1db3f7e6d09448d4745772493d7aab7169e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jul 2025 23:31:13 +1200 Subject: [PATCH 086/125] [i2c] Use ``i2c_master_probe`` to scan i2c bus (#9831) --- esphome/components/i2c/i2c_bus.h | 2 +- esphome/components/i2c/i2c_bus_arduino.cpp | 2 +- esphome/components/i2c/i2c_bus_esp_idf.cpp | 17 ++++++++++++++--- esphome/components/i2c/i2c_bus_esp_idf.h | 3 ++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 5c1e15d814..da94aa940d 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -94,7 +94,7 @@ class I2CBus { protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. - void i2c_scan_() { + virtual void i2c_scan() { for (uint8_t address = 8; address < 120; address++) { auto err = writev(address, nullptr, 0); if (err == ERROR_OK) { diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index a85df0a4cd..1e84f122de 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -42,7 +42,7 @@ void ArduinoI2CBus::setup() { this->initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan_(); + this->i2c_scan(); } } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index c57d537bdb..141e6a670d 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,13 +1,13 @@ #ifdef USE_ESP_IDF #include "i2c_bus_esp_idf.h" +#include #include #include #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) #define SOC_HP_I2C_NUM SOC_I2C_NUM @@ -78,7 +78,7 @@ void IDFI2CBus::setup() { if (this->scan_) { ESP_LOGV(TAG, "Scanning for devices"); - this->i2c_scan_(); + this->i2c_scan(); } #else #if SOC_HP_I2C_NUM > 1 @@ -125,7 +125,7 @@ void IDFI2CBus::setup() { initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan_(); + this->i2c_scan(); } #endif } @@ -167,6 +167,17 @@ void IDFI2CBus::dump_config() { } } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) +void IDFI2CBus::i2c_scan() { + for (uint8_t address = 8; address < 120; address++) { + auto err = i2c_master_probe(this->bus_, address, 20); + if (err == ESP_OK) { + this->scan_results_.emplace_back(address, true); + } + } +} +#endif + ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { // logging is only enabled with vv level, if warnings are shown the caller // should log them diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 8d325de6bc..4e8f86fd0c 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -2,9 +2,9 @@ #ifdef USE_ESP_IDF +#include "esp_idf_version.h" #include "esphome/core/component.h" #include "i2c_bus.h" -#include "esp_idf_version.h" #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) #include #else @@ -46,6 +46,7 @@ class IDFI2CBus : public InternalI2CBus, public Component { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) i2c_master_dev_handle_t dev_; i2c_master_bus_handle_t bus_; + void i2c_scan() override; #endif i2c_port_t port_; uint8_t sda_pin_; From 378b687a821e0af060bb7187c3e43f773496b55f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 01:31:30 -1000 Subject: [PATCH 087/125] [core] Restore COMPONENT_STATE_LOOP_DONE check in calculate_looping_components (#9832) --- esphome/core/application.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 4fedd36120..3ac17849dd 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -271,16 +271,26 @@ void Application::calculate_looping_components_() { // Pre-reserve vector to avoid reallocations this->looping_components_.reserve(total_looping); - // Add all components with loop override - // When called at start of setup, all components are in CONSTRUCTION state - // so none will be LOOP_DONE yet - they'll all go in the active section + // Add all components with loop override that aren't already LOOP_DONE + // Some components (like logger) may call disable_loop() during initialization + // before setup runs, so we need to respect their LOOP_DONE state for (auto *obj : this->components_) { - if (obj->has_overridden_loop()) { + if (obj->has_overridden_loop() && + (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { this->looping_components_.push_back(obj); } } this->looping_components_active_end_ = this->looping_components_.size(); + + // Then add any components that are already LOOP_DONE to the inactive section + // This handles components that called disable_loop() during initialization + for (auto *obj : this->components_) { + if (obj->has_overridden_loop() && + (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { + this->looping_components_.push_back(obj); + } + } } void Application::disable_component_loop_(Component *component) { From 6ac1073469d131807c31964a09a008a0d70a2c3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 01:32:35 -1000 Subject: [PATCH 088/125] [ci] Support C++17 nested namespace syntax in linter (#9826) --- script/ci-custom.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 1172c7152f..e726fcefc0 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -575,13 +575,15 @@ def lint_namespace(fname, content): expected_name = re.match( r"^esphome/components/([^/]+)/.*", fname.replace(os.path.sep, "/") ).group(1) - search = f"namespace {expected_name}" - if search in content: + # Check for both old style and C++17 nested namespace syntax + search_old = f"namespace {expected_name}" + search_new = f"namespace esphome::{expected_name}" + if search_old in content or search_new in content: return None return ( "Invalid namespace found in C++ file. All integration C++ files should put all " "functions in a separate namespace that matches the integration's name. " - f"Please make sure the file contains {highlight(search)}" + f"Please make sure the file contains {highlight(search_old)} or {highlight(search_new)}" ) From e94cb0327208749507de219dde96496f244ec781 Mon Sep 17 00:00:00 2001 From: Olivier ARCHER Date: Wed, 23 Jul 2025 23:36:20 +0200 Subject: [PATCH 089/125] [modem] network component change (#9801) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/network/util.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index a8e792a2d7..bf76aefc30 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -13,15 +13,27 @@ #include "esphome/components/openthread/openthread.h" #endif +#ifdef USE_MODEM +#include "esphome/components/modem/modem_component.h" +#endif + namespace esphome { namespace network { +// The order of the components is important: WiFi should come after any possible main interfaces (it may be used as +// an AP that use a previous interface for NAT). + bool is_connected() { #ifdef USE_ETHERNET if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) return true; #endif +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->is_connected(); +#endif + #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) return wifi::global_wifi_component->is_connected(); @@ -39,6 +51,11 @@ bool is_connected() { } bool is_disabled() { +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->is_disabled(); +#endif + #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) return wifi::global_wifi_component->is_disabled(); @@ -51,6 +68,12 @@ network::IPAddresses get_ip_addresses() { if (ethernet::global_eth_component != nullptr) return ethernet::global_eth_component->get_ip_addresses(); #endif + +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->get_ip_addresses(); +#endif + #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) return wifi::global_wifi_component->get_ip_addresses(); @@ -67,6 +90,12 @@ std::string get_use_address() { if (ethernet::global_eth_component != nullptr) return ethernet::global_eth_component->get_use_address(); #endif + +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->get_use_address(); +#endif + #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) return wifi::global_wifi_component->get_use_address(); From 49df68beb6755a603bceda1a37c546c4b510d9ec Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 24 Jul 2025 07:52:07 +1000 Subject: [PATCH 090/125] [gt911] i2c fixes (#9822) --- .../gt911/touchscreen/gt911_touchscreen.cpp | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 1cead70181..5c540effd0 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -8,6 +8,8 @@ namespace gt911 { static const char *const TAG = "gt911.touchscreen"; +static const uint8_t PRIMARY_ADDRESS = 0x5D; // default I2C address for GT911 +static const uint8_t SECONDARY_ADDRESS = 0x14; // secondary I2C address for GT911 static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; @@ -18,8 +20,7 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ - ESP_LOGE(TAG, "Failed to communicate!"); \ - this->status_set_warning(); \ + this->status_set_warning("Communication failure"); \ return; \ } @@ -30,31 +31,31 @@ void GT911Touchscreen::setup() { this->reset_pin_->setup(); this->reset_pin_->digital_write(false); if (this->interrupt_pin_ != nullptr) { - // The interrupt pin is used as an input during reset to select the I2C address. + // temporarily set the interrupt pin to output to control address selection this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT); - this->interrupt_pin_->setup(); this->interrupt_pin_->digital_write(false); } delay(2); this->reset_pin_->digital_write(true); delay(50); // NOLINT - if (this->interrupt_pin_ != nullptr) { - this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); - this->interrupt_pin_->setup(); - } + } + if (this->interrupt_pin_ != nullptr) { + // set pre-configured input mode + this->interrupt_pin_->setup(); } // check the configuration of the int line. uint8_t data[4]; - err = this->write(GET_SWITCHES, 2); + err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); + if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) { + this->address_ = SECONDARY_ADDRESS; + err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); + } if (err == i2c::ERROR_OK) { err = this->read(data, 1); if (err == i2c::ERROR_OK) { - ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]); + ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]); if (this->interrupt_pin_ != nullptr) { - // datasheet says NOT to use pullup/down on the int line. - this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); - this->interrupt_pin_->setup(); this->attach_interrupt_(this->interrupt_pin_, (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); } @@ -63,7 +64,7 @@ void GT911Touchscreen::setup() { if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { // no calibration? Attempt to read the max values from the touchscreen. if (err == i2c::ERROR_OK) { - err = this->write(GET_MAX_VALUES, 2); + err = this->write(GET_MAX_VALUES, sizeof(GET_MAX_VALUES)); if (err == i2c::ERROR_OK) { err = this->read(data, sizeof(data)); if (err == i2c::ERROR_OK) { @@ -75,15 +76,12 @@ void GT911Touchscreen::setup() { } } if (err != i2c::ERROR_OK) { - ESP_LOGE(TAG, "Failed to read calibration values from touchscreen!"); - this->mark_failed(); + this->mark_failed("Failed to read calibration"); return; } } if (err != i2c::ERROR_OK) { - ESP_LOGE(TAG, "Failed to communicate!"); - this->mark_failed(); - return; + this->mark_failed("Failed to communicate"); } ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); @@ -94,7 +92,7 @@ void GT911Touchscreen::update_touches() { uint8_t touch_state = 0; uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte - err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); + err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE)); ERROR_CHECK(err); err = this->read(&touch_state, 1); ERROR_CHECK(err); @@ -106,7 +104,7 @@ void GT911Touchscreen::update_touches() { return; } - err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); + err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES)); ERROR_CHECK(err); // num_of_touches is guaranteed to be 0..5. Also read the key data err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); @@ -132,6 +130,7 @@ void GT911Touchscreen::dump_config() { ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); LOG_I2C_DEVICE(this); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); } } // namespace gt911 From 0744abe0980dd9e2492e3d57a5ef4972815ed0fe Mon Sep 17 00:00:00 2001 From: Eric Hoffmann Date: Wed, 23 Jul 2025 23:55:31 +0200 Subject: [PATCH 091/125] fix: non-optional x/y target calculation for ld2450 (#9849) --- esphome/components/ld2450/ld2450.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 8f3b3a3f21..09761b2937 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -477,10 +477,11 @@ void LD2450Component::handle_periodic_data_() { // X start = TARGET_X + index * 8; is_moving = false; + // tx is used for further calculations, so always needs to be populated + val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); + tx = val; sensor::Sensor *sx = this->move_x_sensors_[index]; if (sx != nullptr) { - val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - tx = val; if (this->cached_target_data_[index].x != val) { sx->publish_state(val); this->cached_target_data_[index].x = val; @@ -488,10 +489,11 @@ void LD2450Component::handle_periodic_data_() { } // Y start = TARGET_Y + index * 8; + // ty is used for further calculations, so always needs to be populated + val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); + ty = val; sensor::Sensor *sy = this->move_y_sensors_[index]; if (sy != nullptr) { - val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - ty = val; if (this->cached_target_data_[index].y != val) { sy->publish_state(val); this->cached_target_data_[index].y = val; From f9534fbd5d5117c2cd0f962da2e462036ab8909e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 24 Jul 2025 08:03:36 +1000 Subject: [PATCH 092/125] [interval] Fix startup behaviour (#9793) --- esphome/components/interval/interval.h | 14 +++++--------- esphome/core/component.cpp | 5 ++--- esphome/core/scheduler.cpp | 8 ++++++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 8f904b104d..e419841e6c 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/log.h" #include "esphome/core/automation.h" namespace esphome { @@ -8,16 +9,12 @@ namespace interval { class IntervalTrigger : public Trigger<>, public PollingComponent { public: - void update() override { - if (this->started_) - this->trigger(); - } + void update() override { this->trigger(); } void setup() override { - if (this->startup_delay_ == 0) { - this->started_ = true; - } else { - this->set_timeout(this->startup_delay_, [this] { this->started_ = true; }); + if (this->startup_delay_ != 0) { + this->stop_poller(); + this->set_timeout(this->startup_delay_, [this] { this->start_poller(); }); } } @@ -25,7 +22,6 @@ class IntervalTrigger : public Trigger<>, public PollingComponent { protected: uint32_t startup_delay_{0}; - bool started_{false}; }; } // namespace interval diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index aec6c17786..a6a59d30d5 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -373,11 +373,10 @@ bool Component::has_overridden_loop() const { PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} void PollingComponent::call_setup() { + // init the poller before calling setup, allowing setup to cancel it if desired + this->start_poller(); // Let the polling component subclass setup their HW. this->setup(); - - // init the poller - this->start_poller(); } void PollingComponent::start_poller() { diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 9e66fd3432..fc5d43d262 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -17,6 +17,8 @@ static const char *const TAG = "scheduler"; static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; +// max delay to start an interval sequence +static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER @@ -100,9 +102,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Type-specific setup if (type == SchedulerItem::INTERVAL) { item->interval = delay; - // Calculate random offset (0 to interval/2) - uint32_t offset = (delay != 0) ? (random_uint32() % delay) / 2 : 0; + // first execution happens immediately after a random smallish offset + // Calculate random offset (0 to min(interval/2, 5s)) + uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); item->next_execution_ = now + offset; + ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr, delay, offset); } else { item->interval = 0; item->next_execution_ = now + delay; From 3960e2bae77507d1b89ca1cccbcf22f3d2c15648 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:27:05 +1000 Subject: [PATCH 093/125] [mipi] Refactor constants and functions (#9853) --- esphome/components/const/__init__.py | 4 + .../components/esp32_rmt_led_strip/light.py | 2 +- esphome/components/lvgl/__init__.py | 10 +- esphome/components/lvgl/defines.py | 1 - esphome/components/mipi/__init__.py | 403 ++++++++++++++++++ esphome/components/mipi_spi/__init__.py | 7 - esphome/components/mipi_spi/display.py | 251 ++--------- .../components/mipi_spi/models/__init__.py | 65 --- esphome/components/mipi_spi/models/amoled.py | 18 +- .../components/mipi_spi/models/commands.py | 82 ---- esphome/components/mipi_spi/models/ili.py | 10 +- esphome/components/mipi_spi/models/jc.py | 4 +- esphome/components/mipi_spi/models/lilygo.py | 2 +- .../components/mipi_spi/models/waveshare.py | 2 +- esphome/components/rpi_dpi_rgb/display.py | 24 +- .../components/rpi_dpi_rgb/rpi_dpi_rgb.cpp | 1 - esphome/components/st7701s/display.py | 20 +- esphome/components/st7701s/st7701s.cpp | 1 - tests/component_tests/mipi_spi/test_init.py | 2 +- 19 files changed, 488 insertions(+), 421 deletions(-) create mode 100644 esphome/components/mipi/__init__.py delete mode 100644 esphome/components/mipi_spi/models/commands.py diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 18ef4d48b6..19924f0da7 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,8 +3,12 @@ CODEOWNERS = ["@esphome/core"] CONF_BYTE_ORDER = "byte_order" +BYTE_ORDER_LITTLE = "little_endian" +BYTE_ORDER_BIG = "big_endian" + CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" +CONF_USE_PSRAM = "use_psram" diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 33ae44e435..ac4f0b2e92 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -4,6 +4,7 @@ import logging from esphome import pins import esphome.codegen as cg from esphome.components import esp32, light +from esphome.components.const import CONF_USE_PSRAM import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -57,7 +58,6 @@ CHIPSETS = { "SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0), } -CONF_USE_PSRAM = "use_psram" CONF_IS_WRGB = "is_wrgb" CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_LOW = "bit0_low" diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 4a450375c4..b1879e6314 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -2,7 +2,7 @@ import logging from esphome.automation import build_automation, register_action, validate_automation import esphome.codegen as cg -from esphome.components.const import CONF_DRAW_ROUNDING +from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING from esphome.components.display import Display import esphome.config_validation as cv from esphome.const import ( @@ -186,7 +186,7 @@ def multi_conf_validate(configs: list[dict]): for config in configs[1:]: for item in ( df.CONF_LOG_LEVEL, - df.CONF_COLOR_DEPTH, + CONF_COLOR_DEPTH, df.CONF_BYTE_ORDER, df.CONF_TRANSPARENCY_KEY, ): @@ -275,11 +275,11 @@ async def to_code(configs): "LVGL_LOG_LEVEL", cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"), ) - add_define("LV_COLOR_DEPTH", config_0[df.CONF_COLOR_DEPTH]) + add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) for font in helpers.lv_fonts_used: add_define(f"LV_FONT_{font.upper()}") - if config_0[df.CONF_COLOR_DEPTH] == 16: + if config_0[CONF_COLOR_DEPTH] == 16: add_define( "LV_COLOR_16_SWAP", "1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0", @@ -416,7 +416,7 @@ LVGL_SCHEMA = cv.All( { cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), cv.GenerateID(df.CONF_DISPLAYS): display_schema, - cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16), + cv.Optional(CONF_COLOR_DEPTH, default=16): cv.one_of(16), cv.Optional( df.CONF_DEFAULT_FONT, default="montserrat_14" ): lvalid.lv_font, diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index baa9a19c51..206a3d1622 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -418,7 +418,6 @@ CONF_BUTTONS = "buttons" CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" -CONF_COLOR_DEPTH = "color_depth" CONF_CONTROL = "control" CONF_DEFAULT_FONT = "default_font" CONF_DEFAULT_GROUP = "default_group" diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py new file mode 100644 index 0000000000..a6cb8f31eb --- /dev/null +++ b/esphome/components/mipi/__init__.py @@ -0,0 +1,403 @@ +# Various constants used in MIPI DBI communication +# Various configuration constants for MIPI displays +# Various utility functions for MIPI DBI configuration + +from typing import Any + +from esphome.components.const import CONF_COLOR_DEPTH +from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns +import esphome.config_validation as cv +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_ORDER, + CONF_DIMENSIONS, + CONF_HEIGHT, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_PAGES, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_WIDTH, +) +from esphome.core import TimePeriod + +LOGGER = cv.logging.getLogger(__name__) + +ColorOrder = display_ns.enum("ColorMode") + +NOP = 0x00 +SWRESET = 0x01 +RDDID = 0x04 +RDDST = 0x09 +RDMODE = 0x0A +RDMADCTL = 0x0B +RDPIXFMT = 0x0C +RDIMGFMT = 0x0D +RDSELFDIAG = 0x0F +SLEEP_IN = 0x10 +SLPIN = 0x10 +SLEEP_OUT = 0x11 +SLPOUT = 0x11 +PTLON = 0x12 +NORON = 0x13 +INVERT_OFF = 0x20 +INVOFF = 0x20 +INVERT_ON = 0x21 +INVON = 0x21 +ALL_ON = 0x23 +WRAM = 0x24 +GAMMASET = 0x26 +MIPI = 0x26 +DISPOFF = 0x28 +DISPON = 0x29 +CASET = 0x2A +PASET = 0x2B +RASET = 0x2B +RAMWR = 0x2C +WDATA = 0x2C +RAMRD = 0x2E +PTLAR = 0x30 +VSCRDEF = 0x33 +TEON = 0x35 +MADCTL = 0x36 +MADCTL_CMD = 0x36 +VSCRSADD = 0x37 +IDMOFF = 0x38 +IDMON = 0x39 +COLMOD = 0x3A +PIXFMT = 0x3A +GETSCANLINE = 0x45 +BRIGHTNESS = 0x51 +WRDISBV = 0x51 +RDDISBV = 0x52 +WRCTRLD = 0x53 +SWIRE1 = 0x5A +SWIRE2 = 0x5B +IFMODE = 0xB0 +FRMCTR1 = 0xB1 +FRMCTR2 = 0xB2 +FRMCTR3 = 0xB3 +INVCTR = 0xB4 +DFUNCTR = 0xB6 +ETMOD = 0xB7 +PWCTR1 = 0xC0 +PWCTR2 = 0xC1 +PWCTR3 = 0xC2 +PWCTR4 = 0xC3 +PWCTR5 = 0xC4 +VMCTR1 = 0xC5 +IFCTR = 0xC6 +VMCTR2 = 0xC7 +GMCTR = 0xC8 +SETEXTC = 0xC8 +PWSET = 0xD0 +VMCTR = 0xD1 +PWSETN = 0xD2 +RDID4 = 0xD3 +RDINDEX = 0xD9 +RDID1 = 0xDA +RDID2 = 0xDB +RDID3 = 0xDC +RDIDX = 0xDD +GMCTRP1 = 0xE0 +GMCTRN1 = 0xE1 +CSCON = 0xF0 +PWCTR6 = 0xF6 +ADJCTL3 = 0xF7 +PAGESEL = 0xFE + +MADCTL_MY = 0x80 # Bit 7 Bottom to top +MADCTL_MX = 0x40 # Bit 6 Right to left +MADCTL_MV = 0x20 # Bit 5 Reverse Mode +MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top +MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order +MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order +MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left + +# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect +# partial updates. +MADCTL_XFLIP = 0x02 # Mirror the display horizontally +MADCTL_YFLIP = 0x01 # Mirror the display vertically + +# Special constant for delays in command sequences +DELAY_FLAG = 0xFFF # Special flag to indicate a delay + +CONF_PIXEL_MODE = "pixel_mode" +CONF_USE_AXIS_FLIPS = "use_axis_flips" + +PIXEL_MODE_24BIT = "24bit" +PIXEL_MODE_18BIT = "18bit" +PIXEL_MODE_16BIT = "16bit" + +PIXEL_MODES = { + PIXEL_MODE_16BIT: 0x55, + PIXEL_MODE_18BIT: 0x66, + PIXEL_MODE_24BIT: 0x77, +} + +MODE_RGB = "RGB" +MODE_BGR = "BGR" +COLOR_ORDERS = { + MODE_RGB: ColorOrder.COLOR_ORDER_RGB, + MODE_BGR: ColorOrder.COLOR_ORDER_BGR, +} + +CONF_HSYNC_BACK_PORCH = "hsync_back_porch" +CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" +CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" +CONF_VSYNC_BACK_PORCH = "vsync_back_porch" +CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" +CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" +CONF_PCLK_FREQUENCY = "pclk_frequency" +CONF_PCLK_INVERTED = "pclk_inverted" +CONF_NATIVE_HEIGHT = "native_height" +CONF_NATIVE_WIDTH = "native_width" + +CONF_DE_PIN = "de_pin" +CONF_PCLK_PIN = "pclk_pin" + + +def power_of_two(value): + value = cv.int_range(1, 128)(value) + if value & (value - 1) != 0: + raise cv.Invalid("value must be a power of two") + return value + + +def validate_dimension(rounding): + def validator(value): + value = cv.positive_int(value) + if value % rounding != 0: + raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}") + return value + + return validator + + +def dimension_schema(rounding): + return cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): validate_dimension(rounding), + cv.Required(CONF_HEIGHT): validate_dimension(rounding), + cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension( + rounding + ), + cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding), + } + ), + ) + + +def map_sequence(value): + """ + Maps one entry in a sequence to a command and data bytes. + The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred + from the length of the sequence and should not be explicit. + A single integer can be provided where there are no data bytes, in which case it is treated as a command. + A delay can be inserted by specifying "- delay N" where N is in ms + """ + if isinstance(value, str) and value.lower().startswith("delay "): + value = value.lower()[6:] + delay_value = cv.All( + cv.positive_time_period_milliseconds, + cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), + )(value) + return DELAY_FLAG, delay_value.total_milliseconds + value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value) + return tuple(value) + + +def delay(ms): + return DELAY_FLAG, ms + + +class DriverChip: + models = {} + + def __init__( + self, + name: str, + initsequence=None, + **defaults, + ): + name = name.upper() + self.name = name + self.initsequence = initsequence + self.defaults = defaults + DriverChip.models[name] = self + + def extend(self, name, **kwargs) -> "DriverChip": + defaults = self.defaults.copy() + if ( + CONF_WIDTH in defaults + and CONF_OFFSET_WIDTH in kwargs + and CONF_NATIVE_WIDTH not in defaults + ): + defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH] + if ( + CONF_HEIGHT in defaults + and CONF_OFFSET_HEIGHT in kwargs + and CONF_NATIVE_HEIGHT not in defaults + ): + defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] + defaults.update(kwargs) + return DriverChip(name, initsequence=self.initsequence, **defaults) + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + def option(self, name, fallback=False) -> cv.Optional: + return cv.Optional(name, default=self.get_default(name, fallback)) + + def rotation_as_transform(self, config) -> bool: + """ + Check if a rotation can be implemented in hardware using the MADCTL register. + A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. + """ + rotation = config.get(CONF_ROTATION, 0) + return rotation and ( + self.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 + ) + + def get_dimensions(self, config) -> tuple[int, int, int, int]: + if CONF_DIMENSIONS in config: + # Explicit dimensions, just use as is + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width = dimensions[CONF_WIDTH] + height = dimensions[CONF_HEIGHT] + offset_width = dimensions[CONF_OFFSET_WIDTH] + offset_height = dimensions[CONF_OFFSET_HEIGHT] + return width, height, offset_width, offset_height + (width, height) = dimensions + return width, height, 0, 0 + + # Default dimensions, use model defaults + transform = self.get_transform(config) + + width = self.get_default(CONF_WIDTH) + height = self.get_default(CONF_HEIGHT) + offset_width = self.get_default(CONF_OFFSET_WIDTH, 0) + offset_height = self.get_default(CONF_OFFSET_HEIGHT, 0) + + # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where + # the offset is asymmetric + if transform[CONF_MIRROR_X]: + native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) + offset_width = native_width - width - offset_width + if transform[CONF_MIRROR_Y]: + native_height = self.get_default( + CONF_NATIVE_HEIGHT, height + offset_height * 2 + ) + offset_height = native_height - height - offset_height + # Swap default dimensions if swap_xy is set + if transform[CONF_SWAP_XY] is True: + width, height = height, width + offset_height, offset_width = offset_width, offset_height + return width, height, offset_width, offset_height + + def get_transform(self, config) -> dict[str, bool]: + can_transform = self.rotation_as_transform(config) + transform = config.get( + CONF_TRANSFORM, + { + CONF_MIRROR_X: self.get_default(CONF_MIRROR_X, False), + CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y, False), + CONF_SWAP_XY: self.get_default(CONF_SWAP_XY, False), + }, + ) + + # Can we use the MADCTL register to set the rotation? + if can_transform and CONF_TRANSFORM not in config: + rotation = config[CONF_ROTATION] + if rotation == 180: + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + elif rotation == 90: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + else: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_TRANSFORM] = True + return transform + + def get_sequence(self, config) -> tuple[tuple[int, ...], int]: + """ + Create the init sequence for the display. + Use the default sequence from the model, if any, and append any custom sequence provided in the config. + Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence + Pixel format, color order, and orientation will be set. + Returns a tuple of the init sequence and the computed MADCTL value. + """ + sequence = list(self.initsequence) + custom_sequence = config.get(CONF_INIT_SEQUENCE, []) + sequence.extend(custom_sequence) + # Ensure each command is a tuple + sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] + + # Set pixel format if not already in the custom sequence + pixel_mode = config[CONF_PIXEL_MODE] + if not isinstance(pixel_mode, int): + pixel_mode = PIXEL_MODES[pixel_mode] + sequence.append((PIXFMT, pixel_mode)) + + # Does the chip use the flipping bits for mirroring rather than the reverse order bits? + use_flip = config.get(CONF_USE_AXIS_FLIPS) + madctl = 0 + transform = self.get_transform(config) + if self.rotation_as_transform(config): + LOGGER.info("Using hardware transform to implement rotation") + if transform.get(CONF_MIRROR_X): + madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX + if transform.get(CONF_MIRROR_Y): + madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY + if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined + madctl |= MADCTL_MV + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= MADCTL_BGR + sequence.append((MADCTL, madctl)) + if config[CONF_INVERT_COLORS]: + sequence.append((INVON,)) + else: + sequence.append((INVOFF,)) + if brightness := config.get(CONF_BRIGHTNESS, self.get_default(CONF_BRIGHTNESS)): + sequence.append((BRIGHTNESS, brightness)) + sequence.append((SLPOUT,)) + sequence.append((DISPON,)) + + # Flatten the sequence into a list of bytes, with the length of each command + # or the delay flag inserted where needed + return sum( + tuple( + (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] + for x in sequence + ), + (), + ), madctl + + +def requires_buffer(config) -> bool: + """ + Check if the display configuration requires a buffer. It will do so if any drawing methods are configured. + :param config: + :return: True if a buffer is required, False otherwise + """ + return any( + config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD) + ) + + +def get_color_depth(config) -> int: + """ + Get the color depth in bits from the configuration. + """ + return int(config[CONF_COLOR_DEPTH].removesuffix("bit")) diff --git a/esphome/components/mipi_spi/__init__.py b/esphome/components/mipi_spi/__init__.py index 879efda619..f0f02aedd8 100644 --- a/esphome/components/mipi_spi/__init__.py +++ b/esphome/components/mipi_spi/__init__.py @@ -3,11 +3,4 @@ CODEOWNERS = ["@clydebarrow"] DOMAIN = "mipi_spi" CONF_SPI_16 = "spi_16" -CONF_PIXEL_MODE = "pixel_mode" CONF_BUS_MODE = "bus_mode" -CONF_USE_AXIS_FLIPS = "use_axis_flips" -CONF_NATIVE_WIDTH = "native_width" -CONF_NATIVE_HEIGHT = "native_height" - -MODE_RGB = "RGB" -MODE_BGR = "BGR" diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index d25dfd8539..d5c9d7aa0f 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -9,6 +9,20 @@ from esphome.components.const import ( CONF_DRAW_ROUNDING, ) from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS +from esphome.components.mipi import ( + CONF_PIXEL_MODE, + CONF_USE_AXIS_FLIPS, + MADCTL, + MODE_BGR, + MODE_RGB, + PIXFMT, + DriverChip, + dimension_schema, + get_color_depth, + map_sequence, + power_of_two, + requires_buffer, +) from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE import esphome.config_validation as cv from esphome.config_validation import ALLOW_EXTRA @@ -21,7 +35,6 @@ from esphome.const import ( CONF_DC_PIN, CONF_DIMENSIONS, CONF_ENABLE_PIN, - CONF_HEIGHT, CONF_ID, CONF_INIT_SEQUENCE, CONF_INVERT_COLORS, @@ -29,49 +42,18 @@ from esphome.const import ( CONF_MIRROR_X, CONF_MIRROR_Y, CONF_MODEL, - CONF_OFFSET_HEIGHT, - CONF_OFFSET_WIDTH, - CONF_PAGES, CONF_RESET_PIN, CONF_ROTATION, CONF_SWAP_XY, CONF_TRANSFORM, CONF_WIDTH, ) -from esphome.core import CORE, TimePeriod +from esphome.core import CORE from esphome.cpp_generator import TemplateArguments from esphome.final_validate import full_config -from . import ( - CONF_BUS_MODE, - CONF_NATIVE_HEIGHT, - CONF_NATIVE_WIDTH, - CONF_PIXEL_MODE, - CONF_SPI_16, - CONF_USE_AXIS_FLIPS, - DOMAIN, - MODE_BGR, - MODE_RGB, -) -from .models import ( - DELAY_FLAG, - MADCTL_BGR, - MADCTL_MV, - MADCTL_MX, - MADCTL_MY, - MADCTL_XFLIP, - MADCTL_YFLIP, - DriverChip, - adafruit, - amoled, - cyd, - ili, - jc, - lanbon, - lilygo, - waveshare, -) -from .models.commands import BRIGHTNESS, DISPON, INVOFF, INVON, MADCTL, PIXFMT, SLPOUT +from . import CONF_BUS_MODE, CONF_SPI_16, DOMAIN +from .models import adafruit, amoled, cyd, ili, jc, lanbon, lilygo, waveshare DEPENDENCIES = ["spi"] @@ -124,45 +106,6 @@ DISPLAY_PIXEL_MODES = { } -def get_dimensions(config): - if CONF_DIMENSIONS in config: - # Explicit dimensions, just use as is - dimensions = config[CONF_DIMENSIONS] - if isinstance(dimensions, dict): - width = dimensions[CONF_WIDTH] - height = dimensions[CONF_HEIGHT] - offset_width = dimensions[CONF_OFFSET_WIDTH] - offset_height = dimensions[CONF_OFFSET_HEIGHT] - return width, height, offset_width, offset_height - (width, height) = dimensions - return width, height, 0, 0 - - # Default dimensions, use model defaults - transform = get_transform(config) - - model = MODELS[config[CONF_MODEL]] - width = model.get_default(CONF_WIDTH) - height = model.get_default(CONF_HEIGHT) - offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) - offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) - - # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where - # the offset is asymmetric - if transform[CONF_MIRROR_X]: - native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) - offset_width = native_width - width - offset_width - if transform[CONF_MIRROR_Y]: - native_height = model.get_default( - CONF_NATIVE_HEIGHT, height + offset_height * 2 - ) - offset_height = native_height - height - offset_height - # Swap default dimensions if swap_xy is set - if transform[CONF_SWAP_XY] is True: - width, height = height, width - offset_height, offset_width = offset_width, offset_height - return width, height, offset_width, offset_height - - def denominator(config): """ Calculate the best denominator for a buffer size fraction. @@ -171,10 +114,11 @@ def denominator(config): :config: The configuration dictionary containing the buffer size fraction and display dimensions :return: The denominator to use for the buffer size fraction """ + model = MODELS[config[CONF_MODEL]] frac = config.get(CONF_BUFFER_SIZE) if frac is None or frac > 0.75: return 1 - height, _width, _offset_width, _offset_height = get_dimensions(config) + height, _width, _offset_width, _offset_height = model.get_dimensions(config) try: return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0) except StopIteration: @@ -183,58 +127,6 @@ def denominator(config): ) from StopIteration -def validate_dimension(rounding): - def validator(value): - value = cv.positive_int(value) - if value % rounding != 0: - raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}") - return value - - return validator - - -def map_sequence(value): - """ - The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred - from the length of the sequence and should not be explicit. - A delay can be inserted by specifying "- delay N" where N is in ms - """ - if isinstance(value, str) and value.lower().startswith("delay "): - value = value.lower()[6:] - delay = cv.All( - cv.positive_time_period_milliseconds, - cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), - )(value) - return DELAY_FLAG, delay.total_milliseconds - if isinstance(value, int): - return (value,) - value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value) - return tuple(value) - - -def power_of_two(value): - value = cv.int_range(1, 128)(value) - if value & (value - 1) != 0: - raise cv.Invalid("value must be a power of two") - return value - - -def dimension_schema(rounding): - return cv.Any( - cv.dimensions, - cv.Schema( - { - cv.Required(CONF_WIDTH): validate_dimension(rounding), - cv.Required(CONF_HEIGHT): validate_dimension(rounding), - cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension( - rounding - ), - cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding), - } - ), - ) - - def swap_xy_schema(model): uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED @@ -250,7 +142,7 @@ def swap_xy_schema(model): def model_schema(config): model = MODELS[config[CONF_MODEL]] - bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) + bus_mode = config[CONF_BUS_MODE] transform = cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, @@ -340,18 +232,6 @@ def model_schema(config): return schema -def is_rotation_transformable(config): - """ - Check if a rotation can be implemented in hardware using the MADCTL register. - A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. - """ - model = MODELS[config[CONF_MODEL]] - rotation = config.get(CONF_ROTATION, 0) - return rotation and ( - model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 - ) - - def customise_schema(config): """ Create a customised config schema for a specific model and validate the configuration. @@ -367,7 +247,7 @@ def customise_schema(config): extra=ALLOW_EXTRA, )(config) model = MODELS[config[CONF_MODEL]] - bus_modes = model.modes + bus_modes = (TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL) config = cv.Schema( { model.option(CONF_BUS_MODE, TYPE_SINGLE): cv.one_of(*bus_modes, lower=True), @@ -375,7 +255,7 @@ def customise_schema(config): }, extra=ALLOW_EXTRA, )(config) - bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) + bus_mode = config[CONF_BUS_MODE] config = model_schema(config)(config) # Check for invalid combinations of MADCTL config if init_sequence := config.get(CONF_INIT_SEQUENCE): @@ -400,23 +280,9 @@ def customise_schema(config): CONFIG_SCHEMA = customise_schema -def requires_buffer(config): - """ - Check if the display configuration requires a buffer. It will do so if any drawing methods are configured. - :param config: - :return: True if a buffer is required, False otherwise - """ - return any( - config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD) - ) - - -def get_color_depth(config): - return int(config[CONF_COLOR_DEPTH].removesuffix("bit")) - - def _final_validate(config): global_config = full_config.get() + model = MODELS[config[CONF_MODEL]] from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN @@ -433,7 +299,7 @@ def _final_validate(config): return config color_depth = get_color_depth(config) frac = denominator(config) - height, width, _offset_width, _offset_height = get_dimensions(config) + height, width, _offset_width, _offset_height = model.get_dimensions(config) buffer_size = color_depth // 8 * width * height // frac # Target a buffer size of 20kB @@ -463,7 +329,7 @@ def get_transform(config): :return: """ model = MODELS[config[CONF_MODEL]] - can_transform = is_rotation_transformable(config) + can_transform = model.rotation_as_transform(config) transform = config.get( CONF_TRANSFORM, { @@ -489,63 +355,6 @@ def get_transform(config): return transform -def get_sequence(model, config): - """ - Create the init sequence for the display. - Use the default sequence from the model, if any, and append any custom sequence provided in the config. - Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence - Pixel format, color order, and orientation will be set. - """ - sequence = list(model.initsequence) - custom_sequence = config.get(CONF_INIT_SEQUENCE, []) - sequence.extend(custom_sequence) - # Ensure each command is a tuple - sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] - commands = [x[0] for x in sequence] - # Set pixel format if not already in the custom sequence - pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]] - sequence.append((PIXFMT, pixel_mode[0])) - # Does the chip use the flipping bits for mirroring rather than the reverse order bits? - use_flip = config[CONF_USE_AXIS_FLIPS] - if MADCTL not in commands: - madctl = 0 - transform = get_transform(config) - if transform.get(CONF_TRANSFORM): - LOGGER.info("Using hardware transform to implement rotation") - if transform.get(CONF_MIRROR_X): - madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX - if transform.get(CONF_MIRROR_Y): - madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY - if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined - madctl |= MADCTL_MV - if config[CONF_COLOR_ORDER] == MODE_BGR: - madctl |= MADCTL_BGR - sequence.append((MADCTL, madctl)) - if INVON not in commands and INVOFF not in commands: - if config[CONF_INVERT_COLORS]: - sequence.append((INVON,)) - else: - sequence.append((INVOFF,)) - if BRIGHTNESS not in commands: - if brightness := config.get( - CONF_BRIGHTNESS, model.get_default(CONF_BRIGHTNESS) - ): - sequence.append((BRIGHTNESS, brightness)) - if SLPOUT not in commands: - sequence.append((SLPOUT,)) - sequence.append((DISPON,)) - - # Flatten the sequence into a list of bytes, with the length of each command - # or the delay flag inserted where needed - return sum( - tuple( - (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] - for x in sequence - ), - (), - ) - - def get_instance(config): """ Get the type of MipiSpi instance to create based on the configuration, @@ -553,7 +362,8 @@ def get_instance(config): :param config: :return: type, template arguments """ - width, height, offset_width, offset_height = get_dimensions(config) + model = MODELS[config[CONF_MODEL]] + width, height, offset_width, offset_height = model.get_dimensions(config) color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit")) bufferpixels = COLOR_DEPTHS[color_depth] @@ -568,7 +378,7 @@ def get_instance(config): buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 frac = denominator(config) rotation = DISPLAY_ROTATIONS[ - 0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0) + 0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0) ] templateargs = [ buffer_type, @@ -594,8 +404,9 @@ async def to_code(config): var_id = config[CONF_ID] var_id.type, templateargs = get_instance(config) var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) - cg.add(var.set_init_sequence(get_sequence(model, config))) - if is_rotation_transformable(config): + init_sequence, _madctl = model.get_sequence(config) + cg.add(var.set_init_sequence(init_sequence)) + if model.rotation_as_transform(config): if CONF_TRANSFORM in config: LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") else: diff --git a/esphome/components/mipi_spi/models/__init__.py b/esphome/components/mipi_spi/models/__init__.py index e9726032d4..e69de29bb2 100644 --- a/esphome/components/mipi_spi/models/__init__.py +++ b/esphome/components/mipi_spi/models/__init__.py @@ -1,65 +0,0 @@ -from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE -import esphome.config_validation as cv -from esphome.const import CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_WIDTH - -from .. import CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH - -MADCTL_MY = 0x80 # Bit 7 Bottom to top -MADCTL_MX = 0x40 # Bit 6 Right to left -MADCTL_MV = 0x20 # Bit 5 Reverse Mode -MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top -MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order -MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order -MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left - -# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect -# partial updates. -MADCTL_XFLIP = 0x02 # Mirror the display horizontally -MADCTL_YFLIP = 0x01 # Mirror the display vertically - -DELAY_FLAG = 0xFFF # Special flag to indicate a delay - - -def delay(ms): - return DELAY_FLAG, ms - - -class DriverChip: - models = {} - - def __init__( - self, - name: str, - modes=(TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL), - initsequence=None, - **defaults, - ): - name = name.upper() - self.name = name - self.modes = modes - self.initsequence = initsequence - self.defaults = defaults - DriverChip.models[name] = self - - def extend(self, name, **kwargs): - defaults = self.defaults.copy() - if ( - CONF_WIDTH in defaults - and CONF_OFFSET_WIDTH in kwargs - and CONF_NATIVE_WIDTH not in defaults - ): - defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH] - if ( - CONF_HEIGHT in defaults - and CONF_OFFSET_HEIGHT in kwargs - and CONF_NATIVE_HEIGHT not in defaults - ): - defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] - defaults.update(kwargs) - return DriverChip(name, self.modes, initsequence=self.initsequence, **defaults) - - def get_default(self, key, fallback=False): - return self.defaults.get(key, fallback) - - def option(self, name, fallback=False): - return cv.Optional(name, default=self.get_default(name, fallback)) diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py index 882d19db30..6fe882b584 100644 --- a/esphome/components/mipi_spi/models/amoled.py +++ b/esphome/components/mipi_spi/models/amoled.py @@ -1,9 +1,19 @@ +from esphome.components.mipi import ( + MIPI, + MODE_RGB, + NORON, + PAGESEL, + PIXFMT, + SLPOUT, + SWIRE1, + SWIRE2, + TEON, + WRAM, + DriverChip, + delay, +) from esphome.components.spi import TYPE_QUAD -from .. import MODE_RGB -from . import DriverChip, delay -from .commands import MIPI, NORON, PAGESEL, PIXFMT, SLPOUT, SWIRE1, SWIRE2, TEON, WRAM - DriverChip( "T-DISPLAY-S3-AMOLED", width=240, diff --git a/esphome/components/mipi_spi/models/commands.py b/esphome/components/mipi_spi/models/commands.py deleted file mode 100644 index 032a6e6b2b..0000000000 --- a/esphome/components/mipi_spi/models/commands.py +++ /dev/null @@ -1,82 +0,0 @@ -# MIPI DBI commands - -NOP = 0x00 -SWRESET = 0x01 -RDDID = 0x04 -RDDST = 0x09 -RDMODE = 0x0A -RDMADCTL = 0x0B -RDPIXFMT = 0x0C -RDIMGFMT = 0x0D -RDSELFDIAG = 0x0F -SLEEP_IN = 0x10 -SLPIN = 0x10 -SLEEP_OUT = 0x11 -SLPOUT = 0x11 -PTLON = 0x12 -NORON = 0x13 -INVERT_OFF = 0x20 -INVOFF = 0x20 -INVERT_ON = 0x21 -INVON = 0x21 -ALL_ON = 0x23 -WRAM = 0x24 -GAMMASET = 0x26 -MIPI = 0x26 -DISPOFF = 0x28 -DISPON = 0x29 -CASET = 0x2A -PASET = 0x2B -RASET = 0x2B -RAMWR = 0x2C -WDATA = 0x2C -RAMRD = 0x2E -PTLAR = 0x30 -VSCRDEF = 0x33 -TEON = 0x35 -MADCTL = 0x36 -MADCTL_CMD = 0x36 -VSCRSADD = 0x37 -IDMOFF = 0x38 -IDMON = 0x39 -COLMOD = 0x3A -PIXFMT = 0x3A -GETSCANLINE = 0x45 -BRIGHTNESS = 0x51 -WRDISBV = 0x51 -RDDISBV = 0x52 -WRCTRLD = 0x53 -SWIRE1 = 0x5A -SWIRE2 = 0x5B -IFMODE = 0xB0 -FRMCTR1 = 0xB1 -FRMCTR2 = 0xB2 -FRMCTR3 = 0xB3 -INVCTR = 0xB4 -DFUNCTR = 0xB6 -ETMOD = 0xB7 -PWCTR1 = 0xC0 -PWCTR2 = 0xC1 -PWCTR3 = 0xC2 -PWCTR4 = 0xC3 -PWCTR5 = 0xC4 -VMCTR1 = 0xC5 -IFCTR = 0xC6 -VMCTR2 = 0xC7 -GMCTR = 0xC8 -SETEXTC = 0xC8 -PWSET = 0xD0 -VMCTR = 0xD1 -PWSETN = 0xD2 -RDID4 = 0xD3 -RDINDEX = 0xD9 -RDID1 = 0xDA -RDID2 = 0xDB -RDID3 = 0xDC -RDIDX = 0xDD -GMCTRP1 = 0xE0 -GMCTRN1 = 0xE1 -CSCON = 0xF0 -PWCTR6 = 0xF6 -ADJCTL3 = 0xF7 -PAGESEL = 0xFE diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py index cc12b38f5d..0102c0f665 100644 --- a/esphome/components/mipi_spi/models/ili.py +++ b/esphome/components/mipi_spi/models/ili.py @@ -1,8 +1,4 @@ -from esphome.components.spi import TYPE_OCTAL - -from .. import MODE_RGB -from . import DriverChip, delay -from .commands import ( +from esphome.components.mipi import ( ADJCTL3, CSCON, DFUNCTR, @@ -18,6 +14,7 @@ from .commands import ( IFCTR, IFMODE, INVCTR, + MODE_RGB, NORON, PWCTR1, PWCTR2, @@ -32,7 +29,10 @@ from .commands import ( VMCTR1, VMCTR2, VSCRSADD, + DriverChip, + delay, ) +from esphome.components.spi import TYPE_OCTAL DriverChip( "M5CORE", diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index 449c5b87ae..f1f046a427 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -1,10 +1,8 @@ +from esphome.components.mipi import MODE_RGB, DriverChip from esphome.components.spi import TYPE_QUAD import esphome.config_validation as cv from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER -from .. import MODE_RGB -from . import DriverChip - AXS15231 = DriverChip( "AXS15231", draw_rounding=8, diff --git a/esphome/components/mipi_spi/models/lilygo.py b/esphome/components/mipi_spi/models/lilygo.py index dd6f9c02f7..13ddc67465 100644 --- a/esphome/components/mipi_spi/models/lilygo.py +++ b/esphome/components/mipi_spi/models/lilygo.py @@ -1,6 +1,6 @@ +from esphome.components.mipi import MODE_BGR from esphome.components.spi import TYPE_OCTAL -from .. import MODE_BGR from .ili import ST7789V, ST7796 ST7789V.extend( diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index 726718aaf6..002f81f3a6 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -1,6 +1,6 @@ +from esphome.components.mipi import DriverChip import esphome.config_validation as cv -from . import DriverChip from .ili import ILI9488_A DriverChip( diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index c26143d63e..556ee5eeb4 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -2,6 +2,18 @@ from esphome import pins import esphome.codegen as cg from esphome.components import display from esphome.components.esp32 import const, only_on_variant +from esphome.components.mipi import ( + CONF_DE_PIN, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_FREQUENCY, + CONF_PCLK_INVERTED, + CONF_PCLK_PIN, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, +) import esphome.config_validation as cv from esphome.const import ( CONF_BLUE, @@ -27,18 +39,6 @@ from esphome.const import ( DEPENDENCIES = ["esp32"] -CONF_DE_PIN = "de_pin" -CONF_PCLK_PIN = "pclk_pin" - -CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" -CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" -CONF_HSYNC_BACK_PORCH = "hsync_back_porch" -CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" -CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" -CONF_VSYNC_BACK_PORCH = "vsync_back_porch" -CONF_PCLK_FREQUENCY = "pclk_frequency" -CONF_PCLK_INVERTED = "pclk_inverted" - rpi_dpi_rgb_ns = cg.esphome_ns.namespace("rpi_dpi_rgb") RPI_DPI_RGB = rpi_dpi_rgb_ns.class_("RpiDpiRgb", display.Display, cg.Component) ColorOrder = display.display_ns.enum("ColorMode") diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index 91eb947a3e..1706a7e59d 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -23,7 +23,6 @@ void RpiDpiRgb::setup() { config.timings.flags.pclk_active_neg = this->pclk_inverted_; config.timings.pclk_hz = this->pclk_frequency_; config.clk_src = LCD_CLK_SRC_PLL160M; - config.psram_trans_align = 64; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index c6ad43c14c..e2452a4c55 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -2,9 +2,17 @@ from esphome import pins import esphome.codegen as cg from esphome.components import display, spi from esphome.components.esp32 import const, only_on_variant -from esphome.components.rpi_dpi_rgb.display import ( +from esphome.components.mipi import ( + CONF_DE_PIN, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, CONF_PCLK_FREQUENCY, CONF_PCLK_INVERTED, + CONF_PCLK_PIN, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, ) import esphome.config_validation as cv from esphome.const import ( @@ -36,16 +44,6 @@ from esphome.core import TimePeriod from .init_sequences import ST7701S_INITS, cmd -CONF_DE_PIN = "de_pin" -CONF_PCLK_PIN = "pclk_pin" - -CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" -CONF_HSYNC_BACK_PORCH = "hsync_back_porch" -CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" -CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" -CONF_VSYNC_BACK_PORCH = "vsync_back_porch" -CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" - DEPENDENCIES = ["spi", "esp32"] st7701s_ns = cg.esphome_ns.namespace("st7701s") diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 46509a7f9f..2af88515c7 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -25,7 +25,6 @@ void ST7701S::setup() { config.timings.flags.pclk_active_neg = this->pclk_inverted_; config.timings.pclk_hz = this->pclk_frequency_; config.clk_src = LCD_CLK_SRC_PLL160M; - config.psram_trans_align = 64; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index 9824852653..c4c93866ca 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -16,9 +16,9 @@ from esphome.components.esp32 import ( VARIANTS, ) from esphome.components.esp32.gpio import validate_gpio_pin +from esphome.components.mipi import CONF_NATIVE_HEIGHT from esphome.components.mipi_spi.display import ( CONF_BUS_MODE, - CONF_NATIVE_HEIGHT, CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA, MODELS, From 5cd7f156b93d941e0a475c3654ebf46412e3800d Mon Sep 17 00:00:00 2001 From: Mayur Panchal Date: Thu, 24 Jul 2025 11:34:39 +1000 Subject: [PATCH 094/125] Update post_build.py.script to Fix #7137 (#9578) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32/post_build.py.script | 147 +++++++++++------- 1 file changed, 91 insertions(+), 56 deletions(-) diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index c181cf30b1..6e0e439011 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,77 +1,112 @@ -# Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 - -# pylint: disable=E0602 -Import("env") # noqa +Import("env") import os +import json import shutil +import pathlib +import itertools -if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: - try: - import esptool - except ImportError: - env.Execute("$PYTHONEXE -m pip install esptool") -else: - import subprocess -from SCons.Script import ARGUMENTS +def merge_factory_bin(source, target, env): + """ + Merges all flash sections into a single .factory.bin using esptool. + Attempts multiple methods to detect image layout: flasher_args.json, FLASH_EXTRA_IMAGES, fallback guesses. + """ + firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin" + build_dir = pathlib.Path(env.subst("$BUILD_DIR")) + firmware_path = build_dir / firmware_name + flash_size = env.BoardConfig().get("upload.flash_size", "4MB") + chip = env.BoardConfig().get("build.mcu", "esp32") -# Copy over the default sdkconfig. -from os import path + sections = [] + flasher_args_path = build_dir / "flasher_args.json" -if path.exists("./sdkconfig.defaults"): - os.makedirs(".temp", exist_ok=True) - shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf") + # 1. Try flasher_args.json + if flasher_args_path.exists(): + try: + with flasher_args_path.open() as f: + flash_data = json.load(f) + for addr, fname in sorted(flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16)): + file_path = pathlib.Path(fname) + if file_path.exists(): + sections.append((addr, str(file_path))) + else: + print(f"Info: {file_path.name} not found - skipping") + except Exception as e: + print(f"Warning: Failed to parse flasher_args.json - {e}") + # 2. Try FLASH_EXTRA_IMAGES if flasher_args.json failed or was empty + if not sections: + flash_images = env.get("FLASH_EXTRA_IMAGES") + if flash_images: + print("Using FLASH_EXTRA_IMAGES from PlatformIO environment") + # flatten any nested lists + flat = list(itertools.chain.from_iterable( + x if isinstance(x, (list, tuple)) else [x] for x in flash_images + )) + entries = [env.subst(x) for x in flat] + for i in range(0, len(entries) - 1, 2): + addr, fname = entries[i], entries[i + 1] + if isinstance(fname, (list, tuple)): + print(f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}") + continue + file_path = pathlib.Path(str(fname)) + if file_path.exists(): + sections.append((addr, str(file_path))) + else: + print(f"Info: {file_path.name} not found — skipping") -def esp32_create_combined_bin(source, target, env): - verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) - if verbose: - print("Generating combined binary for serial flashing") - app_offset = 0x10000 + # 3. Final fallback: guess standard image locations + if not sections: + print("Fallback: guessing legacy image paths") + guesses = [ + ("0x0", build_dir / "bootloader" / "bootloader.bin"), + ("0x8000", build_dir / "partition_table" / "partition-table.bin"), + ("0xe000", build_dir / "ota_data_initial.bin"), + ("0x10000", firmware_path) + ] + for addr, file_path in guesses: + if file_path.exists(): + sections.append((addr, str(file_path))) + else: + print(f"Info: {file_path.name} not found — skipping") - new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") - sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) - firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") - chip = env.get("BOARD_MCU") - flash_size = env.BoardConfig().get("upload.flash_size") + # If no valid sections found, skip merge + if not sections: + print("No valid flash sections found — skipping .factory.bin creation.") + return + + output_path = firmware_path.with_suffix(".factory.bin") cmd = [ - "--chip", - chip, + "--chip", chip, "merge_bin", - "-o", - new_file_name, - "--flash_size", - flash_size, + "--flash_size", flash_size, + "--output", str(output_path) ] - if verbose: - print(" Offset | File") - for section in sections: - sect_adr, sect_file = section.split(" ", 1) - if verbose: - print(f" - {sect_adr} | {sect_file}") - cmd += [sect_adr, sect_file] + for addr, file_path in sections: + cmd += [addr, file_path] - cmd += [hex(app_offset), firmware_name] + print(f"Merging binaries into {output_path}") + result = env.Execute( + env.VerboseAction( + f"{env.subst('$PYTHONEXE')} -m esptool " + " ".join(cmd), + "Merging binaries with esptool" + ) + ) - if verbose: - print(f" - {hex(app_offset)} | {firmware_name}") - print() - print(f"Using esptool.py arguments: {' '.join(cmd)}") - print() - - if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: - esptool.main(cmd) + if result == 0: + print(f"Successfully created {output_path}") else: - subprocess.run(["esptool.py", *cmd]) - + print(f"Error: esptool merge_bin failed with code {result}") def esp32_copy_ota_bin(source, target, env): + """ + Copy the main firmware to a .ota.bin file for compatibility with ESPHome OTA tools. + """ firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin") - shutil.copyfile(firmware_name, new_file_name) + print(f"Copied firmware to {new_file_name}") - -# pylint: disable=E0602 -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa +# Run merge first, then ota copy second +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) From a3e626757eefcfa80716b972ed4fd0dcc4f467e8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 23 Jul 2025 21:22:54 -0500 Subject: [PATCH 095/125] [helpers] Add "unknown" value handling to ``Deduplicator`` (#9855) --- esphome/core/helpers.h | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 260479c9e1..f67f13b71f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -578,21 +578,28 @@ template class CallbackManager { /// Helper class to deduplicate items in a series of values. template class Deduplicator { public: - /// Feeds the next item in the series to the deduplicator and returns whether this is a duplicate. + /// Feeds the next item in the series to the deduplicator and returns false if this is a duplicate. bool next(T value) { - if (this->has_value_) { - if (this->last_value_ == value) - return false; + if (this->has_value_ && !this->value_unknown_ && this->last_value_ == value) { + return false; } this->has_value_ = true; + this->value_unknown_ = false; this->last_value_ = value; return true; } - /// Returns whether this deduplicator has processed any items so far. + /// Returns true if the deduplicator's value was previously known. + bool next_unknown() { + bool ret = !this->value_unknown_; + this->value_unknown_ = true; + return ret; + } + /// Returns true if this deduplicator has processed any items. bool has_value() const { return this->has_value_; } protected: bool has_value_{false}; + bool value_unknown_{false}; T last_value_{}; }; From cc187ef2765fb97e406c2e0bf2bfad89441b2854 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:29:39 -0400 Subject: [PATCH 096/125] [ld2450] Set ``accuracy_decimals=0`` as default for "target" entities (#9842) --- esphome/components/ld2450/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/ld2450/sensor.py b/esphome/components/ld2450/sensor.py index 071ce8aa32..d16d9c834d 100644 --- a/esphome/components/ld2450/sensor.py +++ b/esphome/components/ld2450/sensor.py @@ -43,12 +43,15 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( icon=ICON_ACCOUNT_GROUP, + accuracy_decimals=0, ), cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( icon=ICON_HUMAN_GREETING_PROXIMITY, + accuracy_decimals=0, ), cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( icon=ICON_ACCOUNT_SWITCH, + accuracy_decimals=0, ), } ) @@ -95,12 +98,15 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( icon=ICON_MAP_MARKER_ACCOUNT, + accuracy_decimals=0, ), cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( icon=ICON_MAP_MARKER_ACCOUNT, + accuracy_decimals=0, ), cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( icon=ICON_MAP_MARKER_ACCOUNT, + accuracy_decimals=0, ), } ) From 108e447072b9a8a13917838e0192ce35a3811bd2 Mon Sep 17 00:00:00 2001 From: TJ Horner Date: Wed, 23 Jul 2025 19:51:47 -0700 Subject: [PATCH 097/125] [logger] remove unnecessary call to setTxTimeoutMs (#9854) --- esphome/components/logger/logger_esp32.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 2fde0f7d49..2ba1efec50 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -119,9 +119,6 @@ void Logger::pre_setup() { #ifdef USE_LOGGER_USB_CDC case UART_SELECTION_USB_CDC: this->hw_serial_ = &Serial; -#if ARDUINO_USB_CDC_ON_BOOT - Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection -#endif Serial.begin(this->baud_rate_); break; #endif From 6398bb2fdff277241cb4b058a05b3afbb0bbc449 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 24 Jul 2025 04:14:00 +0100 Subject: [PATCH 098/125] [i2s_audio] Speaker improvements: CPU core agnostic and more accurate timestamps (#9800) Co-authored-by: NP v/d Spek --- esphome/components/i2s_audio/__init__.py | 6 +- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 550 +++++++++--------- .../i2s_audio/speaker/i2s_audio_speaker.h | 53 +- 3 files changed, 302 insertions(+), 307 deletions(-) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 9a2aa0362f..575b914605 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,6 +1,6 @@ from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, @@ -258,6 +258,10 @@ async def to_code(config): if use_legacy(): cg.add_define("USE_I2S_LEGACY") + # Helps avoid callbacks being skipped due to processor load + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) + cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_I2S_BCLK_PIN in config: cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 1042a7ebee..6f8c13fe74 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -9,6 +9,7 @@ #endif #include "esphome/components/audio/audio.h" +#include "esphome/components/audio/audio_transfer_buffer.h" #include "esphome/core/application.h" #include "esphome/core/hal.h" @@ -19,72 +20,33 @@ namespace esphome { namespace i2s_audio { -static const uint8_t DMA_BUFFER_DURATION_MS = 15; +static const uint32_t DMA_BUFFER_DURATION_MS = 15; static const size_t DMA_BUFFERS_COUNT = 4; -static const size_t TASK_DELAY_MS = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT / 2; - static const size_t TASK_STACK_SIZE = 4096; -static const ssize_t TASK_PRIORITY = 23; +static const ssize_t TASK_PRIORITY = 19; static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; static const char *const TAG = "i2s_audio.speaker"; enum SpeakerEventGroupBits : uint32_t { - COMMAND_START = (1 << 0), // starts the speaker task + COMMAND_START = (1 << 0), // indicates loop should start speaker task COMMAND_STOP = (1 << 1), // stops the speaker task COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written - STATE_STARTING = (1 << 10), - STATE_RUNNING = (1 << 11), - STATE_STOPPING = (1 << 12), - STATE_STOPPED = (1 << 13), - ERR_TASK_FAILED_TO_START = (1 << 14), - ERR_ESP_INVALID_STATE = (1 << 15), - ERR_ESP_NOT_SUPPORTED = (1 << 16), - ERR_ESP_INVALID_ARG = (1 << 17), - ERR_ESP_INVALID_SIZE = (1 << 18), + + TASK_STARTING = (1 << 10), + TASK_RUNNING = (1 << 11), + TASK_STOPPING = (1 << 12), + TASK_STOPPED = (1 << 13), + ERR_ESP_NO_MEM = (1 << 19), - ERR_ESP_FAIL = (1 << 20), - ALL_ERR_ESP_BITS = ERR_ESP_INVALID_STATE | ERR_ESP_NOT_SUPPORTED | ERR_ESP_INVALID_ARG | ERR_ESP_INVALID_SIZE | - ERR_ESP_NO_MEM | ERR_ESP_FAIL, + + WARN_DROPPED_EVENT = (1 << 20), + ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits }; -// Translates a SpeakerEventGroupBits ERR_ESP bit to the coressponding esp_err_t -static esp_err_t err_bit_to_esp_err(uint32_t bit) { - switch (bit) { - case SpeakerEventGroupBits::ERR_ESP_INVALID_STATE: - return ESP_ERR_INVALID_STATE; - case SpeakerEventGroupBits::ERR_ESP_INVALID_ARG: - return ESP_ERR_INVALID_ARG; - case SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE: - return ESP_ERR_INVALID_SIZE; - case SpeakerEventGroupBits::ERR_ESP_NO_MEM: - return ESP_ERR_NO_MEM; - case SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED: - return ESP_ERR_NOT_SUPPORTED; - default: - return ESP_FAIL; - } -} - -/// @brief Multiplies the input array of Q15 numbers by a Q15 constant factor -/// -/// Based on `dsps_mulc_s16_ansi` from the esp-dsp library: -/// https://github.com/espressif/esp-dsp/blob/master/modules/math/mulc/fixed/dsps_mulc_s16_ansi.c -/// (accessed on 2024-09-30). -/// @param input Array of Q15 numbers -/// @param output Array of Q15 numbers -/// @param len Length of array -/// @param c Q15 constant factor -static void q15_multiplication(const int16_t *input, int16_t *output, size_t len, int16_t c) { - for (int i = 0; i < len; i++) { - int32_t acc = (int32_t) input[i] * (int32_t) c; - output[i] = (int16_t) (acc >> 15); - } -} - // Lists the Q15 fixed point scaling factor for volume reduction. // Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB. // dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) @@ -132,51 +94,80 @@ void I2SAudioSpeaker::dump_config() { void I2SAudioSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { - ESP_LOGD(TAG, "Starting"); + if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) { this->state_ = speaker::STATE_STARTING; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); } - if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) { + + // Handle the task's state + if (event_group_bits & SpeakerEventGroupBits::TASK_STARTING) { + ESP_LOGD(TAG, "Starting"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); + } + if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) { ESP_LOGD(TAG, "Started"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); this->state_ = speaker::STATE_RUNNING; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING); - this->status_clear_warning(); - this->status_clear_error(); } - if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) { + if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) { ESP_LOGD(TAG, "Stopping"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); this->state_ = speaker::STATE_STOPPING; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING); } - if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) { - if (!this->task_created_) { - ESP_LOGD(TAG, "Stopped"); - this->state_ = speaker::STATE_STOPPED; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); - this->speaker_task_handle_ = nullptr; - } + if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPED) { + ESP_LOGD(TAG, "Stopped"); + + vTaskDelete(this->speaker_task_handle_); + this->speaker_task_handle_ = nullptr; + + this->stop_i2s_driver_(); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); + this->status_clear_error(); + + this->state_ = speaker::STATE_STOPPED; } - if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { - this->status_set_error("Failed to start task"); - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); + // Log any errors encounted by the task + if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) { + ESP_LOGE(TAG, "Not enough memory"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); } - if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { - uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; - ESP_LOGW(TAG, "Writing failed: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); - this->status_set_warning(); + // Warn if any playback timestamp events are dropped, which drastically reduces synced playback accuracy + if (event_group_bits & SpeakerEventGroupBits::WARN_DROPPED_EVENT) { + ESP_LOGW(TAG, "Event dropped, synchronized playback accuracy is reduced"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT); } - if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Failed to adjust bus to match incoming audio"); - ESP_LOGE(TAG, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %u, bits per sample = %u", - this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(), - this->audio_stream_info_.get_bits_per_sample()); - } + // Handle the speaker's state + switch (this->state_) { + case speaker::STATE_STARTING: + if (this->status_has_error()) { + break; + } - xEventGroupClearBits(this->event_group_, ALL_ERR_ESP_BITS); + if (this->start_i2s_driver_(this->audio_stream_info_) != ESP_OK) { + ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); + this->status_momentary_error("driver-faiure", 1000); + break; + } + + if (this->speaker_task_handle_ == nullptr) { + xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, + &this->speaker_task_handle_); + + if (this->speaker_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); + this->status_momentary_error("task-failure", 1000); + this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt + } + } + break; + case speaker::STATE_RUNNING: // Intentional fallthrough + case speaker::STATE_STOPPING: // Intentional fallthrough + case speaker::STATE_STOPPED: + break; + } } void I2SAudioSpeaker::set_volume(float volume) { @@ -227,83 +218,76 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick this->start(); } - if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() != 1)) { + if (this->state_ != speaker::STATE_RUNNING) { // Unable to write data to a running speaker, so delay the max amount of time so it can get ready vTaskDelay(ticks_to_wait); ticks_to_wait = 0; } size_t bytes_written = 0; - if ((this->state_ == speaker::STATE_RUNNING) && (this->audio_ring_buffer_.use_count() == 1)) { - // Only one owner of the ring buffer (the speaker task), so the ring buffer is allocated and no other components are - // attempting to write to it. - - // Temporarily share ownership of the ring buffer so it won't be deallocated while writing - std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_; - bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait); + if (this->state_ == speaker::STATE_RUNNING) { + std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); + if (temp_ring_buffer.use_count() == 2) { + // Only the speaker task and this temp_ring_buffer own the ring buffer, so its safe to write to + bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait); + } } return bytes_written; } bool I2SAudioSpeaker::has_buffered_data() const { - if (this->audio_ring_buffer_ != nullptr) { - return this->audio_ring_buffer_->available() > 0; + if (this->audio_ring_buffer_.use_count() > 0) { + std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); + return temp_ring_buffer->available() > 0; } return false; } void I2SAudioSpeaker::speaker_task(void *params) { I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; - this_speaker->task_created_ = true; - uint32_t event_group_bits = - xEventGroupWaitBits(this_speaker->event_group_, - SpeakerEventGroupBits::COMMAND_START | SpeakerEventGroupBits::COMMAND_STOP | - SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY, // Bit message to read - pdTRUE, // Clear the bits on exit - pdFALSE, // Don't wait for all the bits, - portMAX_DELAY); // Block indefinitely until a bit is set - - if (event_group_bits & (SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY)) { - // Received a stop signal before the task was requested to start - this_speaker->delete_task_(0); - } - - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STARTING); - - audio::AudioStreamInfo audio_stream_info = this_speaker->audio_stream_info_; + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STARTING); const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT; // Ensure ring buffer duration is at least the duration of all DMA buffers const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_); // The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info - const size_t data_buffer_size = audio_stream_info.ms_to_bytes(dma_buffers_duration_ms); - const size_t ring_buffer_size = audio_stream_info.ms_to_bytes(ring_buffer_duration); + const size_t ring_buffer_size = this_speaker->current_stream_info_.ms_to_bytes(ring_buffer_duration); - const size_t single_dma_buffer_input_size = data_buffer_size / DMA_BUFFERS_COUNT; + const uint32_t frames_to_fill_single_dma_buffer = + this_speaker->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); + const size_t bytes_to_fill_single_dma_buffer = + this_speaker->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); - if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(data_buffer_size, ring_buffer_size))) { - // Failed to allocate buffers - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - this_speaker->delete_task_(data_buffer_size); + bool successful_setup = false; + std::unique_ptr transfer_buffer = + audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer); + + if (transfer_buffer != nullptr) { + std::shared_ptr temp_ring_buffer = RingBuffer::create(ring_buffer_size); + if (temp_ring_buffer.use_count() == 1) { + transfer_buffer->set_source(temp_ring_buffer); + this_speaker->audio_ring_buffer_ = temp_ring_buffer; + successful_setup = true; + } } - if (!this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_(audio_stream_info))) { - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_RUNNING); - + if (!successful_setup) { + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); + } else { bool stop_gracefully = false; + bool tx_dma_underflow = true; + + uint32_t frames_written = 0; uint32_t last_data_received_time = millis(); - bool tx_dma_underflow = false; - this_speaker->accumulated_frames_written_ = 0; + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_RUNNING); - // Keep looping if paused, there is no timeout configured, or data was received more recently than the configured - // timeout while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() || (millis() - last_data_received_time) <= this_speaker->timeout_.value()) { - event_group_bits = xEventGroupGetBits(this_speaker->event_group_); + uint32_t event_group_bits = xEventGroupGetBits(this_speaker->event_group_); if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP); @@ -314,7 +298,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { stop_gracefully = true; } - if (this_speaker->audio_stream_info_ != audio_stream_info) { + if (this_speaker->audio_stream_info_ != this_speaker->current_stream_info_) { // Audio stream info changed, stop the speaker task so it will restart with the proper settings. break; } @@ -326,36 +310,75 @@ void I2SAudioSpeaker::speaker_task(void *params) { } } #else - bool overflow; - while (xQueueReceive(this_speaker->i2s_event_queue_, &overflow, 0)) { - if (overflow) { + int64_t write_timestamp; + while (xQueueReceive(this_speaker->i2s_event_queue_, &write_timestamp, 0)) { + // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes + // on the timing info via the audio_output_callback. + uint32_t frames_sent = frames_to_fill_single_dma_buffer; + if (frames_to_fill_single_dma_buffer > frames_written) { tx_dma_underflow = true; + frames_sent = frames_written; + const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written; + write_timestamp -= this_speaker->current_stream_info_.frames_to_microseconds(frames_zeroed); + } else { + tx_dma_underflow = false; + } + frames_written -= frames_sent; + if (frames_sent > 0) { + this_speaker->audio_output_callback_(frames_sent, write_timestamp); } } #endif if (this_speaker->pause_state_) { // Pause state is accessed atomically, so thread safe - // Delay so the task can yields, then skip transferring audio data - delay(TASK_DELAY_MS); + // Delay so the task yields, then skip transferring audio data + vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); continue; } - size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, data_buffer_size, - pdMS_TO_TICKS(TASK_DELAY_MS)); + // Wait half the duration of the data already written to the DMA buffers for new audio data + // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000 + const uint32_t read_delay = + (this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; + + uint8_t *new_data = transfer_buffer->get_buffer_end(); // track start of any newly copied bytes + size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay)); if (bytes_read > 0) { - if ((audio_stream_info.get_bits_per_sample() == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) { - // Scale samples by the volume factor in place - q15_multiplication((int16_t *) this_speaker->data_buffer_, (int16_t *) this_speaker->data_buffer_, - bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_); + if (this_speaker->q15_volume_factor_ < INT16_MAX) { + // Apply the software volume adjustment by unpacking the sample into a Q31 fixed-point number, shifting it, + // multiplying by the volume factor, and packing the sample back into the original bytes per sample. + + const size_t bytes_per_sample = this_speaker->current_stream_info_.samples_to_bytes(1); + const uint32_t len = bytes_read / bytes_per_sample; + + // Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31 + int32_t shift = 15; // Q31 -> Q16 + int32_t gain_factor = this_speaker->q15_volume_factor_; // Q15 + + if (bytes_per_sample >= 3) { + // Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31 + + shift = 8; // Q31 -> Q23 + gain_factor >>= 7; // Q15 -> Q8 + } + + for (uint32_t i = 0; i < len; ++i) { + int32_t sample = + audio::unpack_audio_sample_to_q31(&new_data[i * bytes_per_sample], bytes_per_sample); // Q31 + sample >>= shift; + sample *= gain_factor; // Q31 + audio::pack_q31_as_audio_sample(sample, &new_data[i * bytes_per_sample], bytes_per_sample); + } } #ifdef USE_ESP32_VARIANT_ESP32 // For ESP32 8/16 bit mono mode samples need to be switched. - if (audio_stream_info.get_channels() == 1 && audio_stream_info.get_bits_per_sample() <= 16) { + if (this_speaker->current_stream_info_.get_channels() == 1 && + this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { size_t len = bytes_read / sizeof(int16_t); - int16_t *tmp_buf = (int16_t *) this_speaker->data_buffer_; + int16_t *tmp_buf = (int16_t *) new_data; for (int i = 0; i < len; i += 2) { int16_t tmp = tmp_buf[i]; tmp_buf[i] = tmp_buf[i + 1]; @@ -363,62 +386,87 @@ void I2SAudioSpeaker::speaker_task(void *params) { } } #endif - // Write the audio data to a single DMA buffer at a time to reduce latency for the audio duration played - // callback. - const uint32_t batches = (bytes_read + single_dma_buffer_input_size - 1) / single_dma_buffer_input_size; + } - for (uint32_t i = 0; i < batches; ++i) { - size_t bytes_written = 0; - size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read); - -#ifdef USE_I2S_LEGACY - if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) { - i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size, - bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); - } else if (audio_stream_info.get_bits_per_sample() < (uint8_t) this_speaker->bits_per_sample_) { - i2s_write_expand(this_speaker->parent_->get_port(), - this_speaker->data_buffer_ + i * single_dma_buffer_input_size, bytes_to_write, - audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written, - pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); - } -#else - i2s_channel_write(this_speaker->tx_handle_, this_speaker->data_buffer_ + i * single_dma_buffer_input_size, - bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); -#endif - - int64_t now = esp_timer_get_time(); - - if (bytes_written != bytes_to_write) { - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); - } - bytes_read -= bytes_written; - - this_speaker->audio_output_callback_(audio_stream_info.bytes_to_frames(bytes_written), - now + dma_buffers_duration_ms * 1000); - - tx_dma_underflow = false; - last_data_received_time = millis(); - } - } else { - // No data received + if (transfer_buffer->available() == 0) { if (stop_gracefully && tx_dma_underflow) { break; } + vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2)); + } else { + size_t bytes_written = 0; +#ifdef USE_I2S_LEGACY + if (this_speaker->current_stream_info_.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) { + i2s_write(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(), + transfer_buffer->available(), &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); + } else if (this_speaker->current_stream_info_.get_bits_per_sample() < + (uint8_t) this_speaker->bits_per_sample_) { + i2s_write_expand(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(), + transfer_buffer->available(), this_speaker->current_stream_info_.get_bits_per_sample(), + this_speaker->bits_per_sample_, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); + } +#else + if (tx_dma_underflow) { + // Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue so timing + // callbacks are accurate. Preload the data. + i2s_channel_disable(this_speaker->tx_handle_); + const i2s_event_callbacks_t callbacks = { + .on_sent = nullptr, + }; + + i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker); + i2s_channel_preload_data(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), + transfer_buffer->available(), &bytes_written); + } else { + // Audio is already playing, use regular I2S write to add to the DMA buffers + i2s_channel_write(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(), + &bytes_written, DMA_BUFFER_DURATION_MS); + } +#endif + if (bytes_written > 0) { + last_data_received_time = millis(); + frames_written += this_speaker->current_stream_info_.bytes_to_frames(bytes_written); + transfer_buffer->decrease_buffer_length(bytes_written); + if (tx_dma_underflow) { + tx_dma_underflow = false; +#ifndef USE_I2S_LEGACY + // Reset the event queue timestamps + // Enable the on_sent callback to accurately track the timestamps of played audio + // Enable the I2S channel to start sending the preloaded audio + + xQueueReset(this_speaker->i2s_event_queue_); + + const i2s_event_callbacks_t callbacks = { + .on_sent = i2s_on_sent_cb, + }; + i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker); + + i2s_channel_enable(this_speaker->tx_handle_); +#endif + } +#ifdef USE_I2S_LEGACY + // The legacy driver doesn't easily support the callback approach for timestamps, so fall back to a direct but + // less accurate approach. + this_speaker->audio_output_callback_(this_speaker->current_stream_info_.bytes_to_frames(bytes_written), + esp_timer_get_time() + dma_buffers_duration_ms * 1000); +#endif + } } } - - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING); -#ifdef USE_I2S_LEGACY - i2s_driver_uninstall(this_speaker->parent_->get_port()); -#else - i2s_channel_disable(this_speaker->tx_handle_); - i2s_del_channel(this_speaker->tx_handle_); -#endif - - this_speaker->parent_->unlock(); } - this_speaker->delete_task_(data_buffer_size); + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPING); + + if (transfer_buffer != nullptr) { + transfer_buffer.reset(); + } + + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPED); + + while (true) { + // Continuously delay until the loop method deletes the task + vTaskDelay(pdMS_TO_TICKS(10)); + } } void I2SAudioSpeaker::start() { @@ -427,16 +475,7 @@ void I2SAudioSpeaker::start() { if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) return; - if (!this->task_created_ && (this->speaker_task_handle_ == nullptr)) { - xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, - &this->speaker_task_handle_); - - if (this->speaker_task_handle_ != nullptr) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); - } else { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); - } - } + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); } void I2SAudioSpeaker::stop() { this->stop_(false); } @@ -456,61 +495,16 @@ void I2SAudioSpeaker::stop_(bool wait_on_empty) { } } -bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) { - switch (err) { - case ESP_OK: - return false; - case ESP_ERR_INVALID_STATE: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_STATE); - return true; - case ESP_ERR_INVALID_ARG: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_ARG); - return true; - case ESP_ERR_INVALID_SIZE: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); - return true; - case ESP_ERR_NO_MEM: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - return true; - case ESP_ERR_NOT_SUPPORTED: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED); - return true; - default: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_FAIL); - return true; - } -} - -esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) { - if (this->data_buffer_ == nullptr) { - // Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus - RAMAllocator allocator; - this->data_buffer_ = allocator.allocate(data_buffer_size); - } - - if (this->data_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - if (this->audio_ring_buffer_.use_count() == 0) { - // Allocate ring buffer. Uses a shared_ptr to ensure it isn't improperly deallocated. - this->audio_ring_buffer_ = RingBuffer::create(ring_buffer_size); - } - - if (this->audio_ring_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - return ESP_OK; -} - esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { + this->current_stream_info_ = audio_stream_info; // store the stream info settings the driver will use + #ifdef USE_I2S_LEGACY if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT #else if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT #endif // Can't reconfigure I2S bus, so the sample rate must match the configured value + ESP_LOGE(TAG, "Audio stream settings are not compatible with this I2S configuration"); return ESP_ERR_NOT_SUPPORTED; } @@ -521,10 +515,12 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { #endif // Currently can't handle the case when the incoming audio has more bits per sample than the configured value + ESP_LOGE(TAG, "Audio streams with more bits per sample than the I2S speaker's configuration is not supported"); return ESP_ERR_NOT_SUPPORTED; } if (!this->parent_->try_lock()) { + ESP_LOGE(TAG, "Parent I2S bus not free"); return ESP_ERR_INVALID_STATE; } @@ -575,6 +571,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea esp_err_t err = i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to install I2S legacy driver"); // Failed to install the driver, so unlock the I2S port this->parent_->unlock(); return err; @@ -595,6 +592,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea if (err != ESP_OK) { // Failed to set the data out pin, so uninstall the driver and unlock the I2S port + ESP_LOGE(TAG, "Failed to set the data out pin"); i2s_driver_uninstall(this->parent_->get_port()); this->parent_->unlock(); } @@ -605,10 +603,12 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea .dma_desc_num = DMA_BUFFERS_COUNT, .dma_frame_num = dma_buffer_length, .auto_clear = true, + .intr_priority = 3, }; /* Allocate a new TX channel and get the handle of this channel */ esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to allocate new I2S channel"); this->parent_->unlock(); return err; } @@ -652,7 +652,11 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea // per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to // make it play at the correct speed while sending more bits per slot. if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { - std_slot_cfg.ws_width = static_cast(this->slot_bit_width_); + uint32_t configured_bit_width = static_cast(this->slot_bit_width_); + std_slot_cfg.ws_width = configured_bit_width; + if (configured_bit_width > 16) { + std_slot_cfg.msb_right = false; + } } #else std_slot_cfg.slot_bit_width = this->slot_bit_width_; @@ -670,54 +674,56 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize channel"); i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; this->parent_->unlock(); return err; } if (this->i2s_event_queue_ == nullptr) { - this->i2s_event_queue_ = xQueueCreate(1, sizeof(bool)); + this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(int64_t)); } - const i2s_event_callbacks_t callbacks = { - .on_send_q_ovf = i2s_overflow_cb, - }; - i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); - - /* Before reading data, start the TX channel first */ i2s_channel_enable(this->tx_handle_); - if (err != ESP_OK) { - i2s_del_channel(this->tx_handle_); - this->parent_->unlock(); - } #endif return err; } -void I2SAudioSpeaker::delete_task_(size_t buffer_size) { - this->audio_ring_buffer_.reset(); // Releases ownership of the shared_ptr +#ifndef USE_I2S_LEGACY +bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { + int64_t now = esp_timer_get_time(); - if (this->data_buffer_ != nullptr) { - RAMAllocator allocator; - allocator.deallocate(this->data_buffer_, buffer_size); - this->data_buffer_ = nullptr; + BaseType_t need_yield1 = pdFALSE; + BaseType_t need_yield2 = pdFALSE; + BaseType_t need_yield3 = pdFALSE; + + I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx; + + if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) { + // Queue is full, so discard the oldest event and set the warning flag to inform the user + int64_t dummy; + xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1); + xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT, &need_yield2); } - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED); + xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3); - this->task_created_ = false; - vTaskDelete(nullptr); -} - -#ifndef USE_I2S_LEGACY -bool IRAM_ATTR I2SAudioSpeaker::i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { - I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx; - bool overflow = true; - xQueueOverwrite(this_speaker->i2s_event_queue_, &overflow); - return false; + return need_yield1 | need_yield2 | need_yield3; } #endif +void I2SAudioSpeaker::stop_i2s_driver_() { +#ifdef USE_I2S_LEGACY + i2s_driver_uninstall(this->parent_->get_port()); +#else + i2s_channel_disable(this->tx_handle_); + i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; +#endif + this->parent_->unlock(); +} + } // namespace i2s_audio } // namespace esphome diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index eb2a0ae756..1d03a4c495 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -72,70 +72,57 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp protected: /// @brief Function for the FreeRTOS task handling audio output. - /// After receiving the COMMAND_START signal, allocates space for the buffers, starts the I2S driver, and reads - /// audio from the ring buffer and writes audio to the I2S port. Stops immmiately after receiving the COMMAND_STOP - /// signal and stops only after the ring buffer is empty after receiving the COMMAND_STOP_GRACEFULLY signal. Stops if - /// the ring buffer hasn't read data for more than timeout_ milliseconds. When stopping, it deallocates the buffers, - /// stops the I2S driver, unlocks the I2S port, and deletes the task. It communicates the state and any errors via - /// event_group_. + /// Allocates space for the buffers, reads audio from the ring buffer and writes audio to the I2S port. Stops + /// immmiately after receiving the COMMAND_STOP signal and stops only after the ring buffer is empty after receiving + /// the COMMAND_STOP_GRACEFULLY signal. Stops if the ring buffer hasn't read data for more than timeout_ milliseconds. + /// When stopping, it deallocates the buffers. It communicates its state and any errors via ``event_group_``. /// @param params I2SAudioSpeaker component static void speaker_task(void *params); - /// @brief Sends a stop command to the speaker task via event_group_. + /// @brief Sends a stop command to the speaker task via ``event_group_``. /// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal. void stop_(bool wait_on_empty); - /// @brief Sets the corresponding ERR_ESP event group bits. - /// @param err esp_err_t error code. - /// @return True if an ERR_ESP bit is set and false if err == ESP_OK - bool send_esp_err_to_event_group_(esp_err_t err); - #ifndef USE_I2S_LEGACY - static bool i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); + /// @brief Callback function used to send playback timestamps the to the speaker task. + /// @param handle (i2s_chan_handle_t) + /// @param event (i2s_event_data_t) + /// @param user_ctx (void*) User context pointer that the callback accesses + /// @return True if a higher priority task was interrupted + static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); #endif - /// @brief Allocates the data buffer and ring buffer - /// @param data_buffer_size Number of bytes to allocate for the data buffer. - /// @param ring_buffer_size Number of bytes to allocate for the ring buffer. - /// @return ESP_ERR_NO_MEM if either buffer fails to allocate - /// ESP_OK if successful - esp_err_t allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size); - /// @brief Starts the ESP32 I2S driver. /// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out - /// pin. If it fails, it will unlock the I2S port and uninstall the driver, if necessary. + /// pin. If it fails, it will unlock the I2S port and uninstalls the driver, if necessary. /// @param audio_stream_info Stream information for the I2S driver. /// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream. /// ESP_ERR_INVALID_STATE if the I2S port is already locked. - /// ESP_ERR_INVALID_ARG if nstalling the driver or setting the data outpin fails due to a parameter error. + /// ESP_ERR_INVALID_ARG if installing the driver or setting the data outpin fails due to a parameter error. /// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error. - /// ESP_FAIL if setting the data out pin fails due to an IO error ESP_OK if successful + /// ESP_FAIL if setting the data out pin fails due to an IO error + /// ESP_OK if successful esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info); - /// @brief Deletes the speaker's task. - /// Deallocates the data_buffer_ and audio_ring_buffer_, if necessary, and deletes the task. Should only be called by - /// the speaker_task itself. - /// @param buffer_size The allocated size of the data_buffer_. - void delete_task_(size_t buffer_size); + /// @brief Stops the I2S driver and unlocks the I2S port + void stop_i2s_driver_(); TaskHandle_t speaker_task_handle_{nullptr}; EventGroupHandle_t event_group_{nullptr}; QueueHandle_t i2s_event_queue_; - uint8_t *data_buffer_; - std::shared_ptr audio_ring_buffer_; + std::weak_ptr audio_ring_buffer_; uint32_t buffer_duration_ms_; optional timeout_; - bool task_created_{false}; bool pause_state_{false}; int16_t q15_volume_factor_{INT16_MAX}; - size_t bytes_written_{0}; + audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info #ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_DAC @@ -148,8 +135,6 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp std::string i2s_comm_fmt_; i2s_chan_handle_t tx_handle_; #endif - - uint32_t accumulated_frames_written_{0}; }; } // namespace i2s_audio From 15ba2326ad188a270f58014eee6525bbe9af16de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 17:15:32 -1000 Subject: [PATCH 099/125] [esp32] Fix threading model for single-core variants (S2, C3, C6, H2) (#9851) --- esphome/components/esp32/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 587d75b64b..e24815741a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -98,6 +98,16 @@ ARDUINO_ALLOWED_VARIANTS = [ VARIANT_ESP32S3, ] +# Single-core ESP32 variants +SINGLE_CORE_VARIANTS = frozenset( + [ + VARIANT_ESP32S2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + ] +) + def get_cpu_frequencies(*frequencies): return [str(x) + "MHZ" for x in frequencies] @@ -714,7 +724,11 @@ async def to_code(config): cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) - cg.add_define(CoreModel.MULTI_ATOMICS) + # Set threading model based on core count + if config[CONF_VARIANT] in SINGLE_CORE_VARIANTS: + cg.add_define(CoreModel.SINGLE) + else: + cg.add_define(CoreModel.MULTI_ATOMICS) cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "strict") From 04d9698681efcd1f25836d4333ed21382b12f916 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 17:16:54 -1000 Subject: [PATCH 100/125] [api] Replace magic numbers with MESSAGE_TYPE constants in protobuf switch cases (#9776) --- esphome/components/api/api_pb2_service.cpp | 110 ++++++++++----------- script/api_protobuf/api_protobuf.py | 9 +- 2 files changed, 60 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 498c396ae3..4ac2f75fb0 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -16,7 +16,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { - case 1: { + case HelloRequest::MESSAGE_TYPE: { HelloRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -25,7 +25,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_hello_request(msg); break; } - case 3: { + case ConnectRequest::MESSAGE_TYPE: { ConnectRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -34,7 +34,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_connect_request(msg); break; } - case 5: { + case DisconnectRequest::MESSAGE_TYPE: { DisconnectRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -43,7 +43,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_disconnect_request(msg); break; } - case 6: { + case DisconnectResponse::MESSAGE_TYPE: { DisconnectResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -52,7 +52,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_disconnect_response(msg); break; } - case 7: { + case PingRequest::MESSAGE_TYPE: { PingRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -61,7 +61,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_ping_request(msg); break; } - case 8: { + case PingResponse::MESSAGE_TYPE: { PingResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -70,7 +70,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_ping_response(msg); break; } - case 9: { + case DeviceInfoRequest::MESSAGE_TYPE: { DeviceInfoRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -79,7 +79,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_device_info_request(msg); break; } - case 11: { + case ListEntitiesRequest::MESSAGE_TYPE: { ListEntitiesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -88,7 +88,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_list_entities_request(msg); break; } - case 20: { + case SubscribeStatesRequest::MESSAGE_TYPE: { SubscribeStatesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -97,7 +97,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_subscribe_states_request(msg); break; } - case 28: { + case SubscribeLogsRequest::MESSAGE_TYPE: { SubscribeLogsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -107,7 +107,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #ifdef USE_COVER - case 30: { + case CoverCommandRequest::MESSAGE_TYPE: { CoverCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -118,7 +118,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_FAN - case 31: { + case FanCommandRequest::MESSAGE_TYPE: { FanCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -129,7 +129,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_LIGHT - case 32: { + case LightCommandRequest::MESSAGE_TYPE: { LightCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -140,7 +140,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_SWITCH - case 33: { + case SwitchCommandRequest::MESSAGE_TYPE: { SwitchCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -150,7 +150,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #endif - case 34: { + case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { SubscribeHomeassistantServicesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -159,7 +159,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_subscribe_homeassistant_services_request(msg); break; } - case 36: { + case GetTimeRequest::MESSAGE_TYPE: { GetTimeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -168,7 +168,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_get_time_request(msg); break; } - case 37: { + case GetTimeResponse::MESSAGE_TYPE: { GetTimeResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -177,7 +177,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_get_time_response(msg); break; } - case 38: { + case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { SubscribeHomeAssistantStatesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -186,7 +186,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_subscribe_home_assistant_states_request(msg); break; } - case 40: { + case HomeAssistantStateResponse::MESSAGE_TYPE: { HomeAssistantStateResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -196,7 +196,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #ifdef USE_API_SERVICES - case 42: { + case ExecuteServiceRequest::MESSAGE_TYPE: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -207,7 +207,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_CAMERA - case 45: { + case CameraImageRequest::MESSAGE_TYPE: { CameraImageRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -218,7 +218,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_CLIMATE - case 48: { + case ClimateCommandRequest::MESSAGE_TYPE: { ClimateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -229,7 +229,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_NUMBER - case 51: { + case NumberCommandRequest::MESSAGE_TYPE: { NumberCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -240,7 +240,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_SELECT - case 54: { + case SelectCommandRequest::MESSAGE_TYPE: { SelectCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -251,7 +251,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_SIREN - case 57: { + case SirenCommandRequest::MESSAGE_TYPE: { SirenCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -262,7 +262,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_LOCK - case 60: { + case LockCommandRequest::MESSAGE_TYPE: { LockCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -273,7 +273,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BUTTON - case 62: { + case ButtonCommandRequest::MESSAGE_TYPE: { ButtonCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -284,7 +284,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_MEDIA_PLAYER - case 65: { + case MediaPlayerCommandRequest::MESSAGE_TYPE: { MediaPlayerCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -295,7 +295,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 66: { + case SubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { SubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -306,7 +306,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 68: { + case BluetoothDeviceRequest::MESSAGE_TYPE: { BluetoothDeviceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -317,7 +317,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 70: { + case BluetoothGATTGetServicesRequest::MESSAGE_TYPE: { BluetoothGATTGetServicesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -328,7 +328,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 73: { + case BluetoothGATTReadRequest::MESSAGE_TYPE: { BluetoothGATTReadRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -339,7 +339,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 75: { + case BluetoothGATTWriteRequest::MESSAGE_TYPE: { BluetoothGATTWriteRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -350,7 +350,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 76: { + case BluetoothGATTReadDescriptorRequest::MESSAGE_TYPE: { BluetoothGATTReadDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -361,7 +361,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 77: { + case BluetoothGATTWriteDescriptorRequest::MESSAGE_TYPE: { BluetoothGATTWriteDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -372,7 +372,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 78: { + case BluetoothGATTNotifyRequest::MESSAGE_TYPE: { BluetoothGATTNotifyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -383,7 +383,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 80: { + case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { SubscribeBluetoothConnectionsFreeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -394,7 +394,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 87: { + case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { UnsubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -405,7 +405,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 89: { + case SubscribeVoiceAssistantRequest::MESSAGE_TYPE: { SubscribeVoiceAssistantRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -416,7 +416,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 91: { + case VoiceAssistantResponse::MESSAGE_TYPE: { VoiceAssistantResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -427,7 +427,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 92: { + case VoiceAssistantEventResponse::MESSAGE_TYPE: { VoiceAssistantEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -438,7 +438,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_ALARM_CONTROL_PANEL - case 96: { + case AlarmControlPanelCommandRequest::MESSAGE_TYPE: { AlarmControlPanelCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -449,7 +449,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_TEXT - case 99: { + case TextCommandRequest::MESSAGE_TYPE: { TextCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -460,7 +460,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_DATETIME_DATE - case 102: { + case DateCommandRequest::MESSAGE_TYPE: { DateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -471,7 +471,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_DATETIME_TIME - case 105: { + case TimeCommandRequest::MESSAGE_TYPE: { TimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -482,7 +482,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 106: { + case VoiceAssistantAudio::MESSAGE_TYPE: { VoiceAssistantAudio msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -493,7 +493,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VALVE - case 111: { + case ValveCommandRequest::MESSAGE_TYPE: { ValveCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -504,7 +504,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_DATETIME_DATETIME - case 114: { + case DateTimeCommandRequest::MESSAGE_TYPE: { DateTimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -515,7 +515,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 115: { + case VoiceAssistantTimerEventResponse::MESSAGE_TYPE: { VoiceAssistantTimerEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -526,7 +526,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_UPDATE - case 118: { + case UpdateCommandRequest::MESSAGE_TYPE: { UpdateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -537,7 +537,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 119: { + case VoiceAssistantAnnounceRequest::MESSAGE_TYPE: { VoiceAssistantAnnounceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -548,7 +548,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 121: { + case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: { VoiceAssistantConfigurationRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -559,7 +559,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 123: { + case VoiceAssistantSetConfiguration::MESSAGE_TYPE: { VoiceAssistantSetConfiguration msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -570,7 +570,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_API_NOISE - case 124: { + case NoiseEncryptionSetKeyRequest::MESSAGE_TYPE: { NoiseEncryptionSetKeyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -581,7 +581,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 127: { + case BluetoothScannerSetModeRequest::MESSAGE_TYPE: { BluetoothScannerSetModeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 766a84c9fd..24ee1aaa47 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1934,8 +1934,8 @@ def build_service_message_type( case += "#endif\n" case += f"this->{func}(msg);\n" case += "break;" - # Store the ifdef with the case for later use - RECEIVE_CASES[id_] = (case, ifdef) + # Store the message name and ifdef with the case for later use + RECEIVE_CASES[id_] = (case, ifdef, mt.name) # Only close ifdef if we opened it if ifdef is not None: @@ -2200,10 +2200,11 @@ static const char *const TAG = "api.service"; hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" - for i, (case, ifdef) in cases: + for i, (case, ifdef, message_name) in cases: if ifdef is not None: out += f"#ifdef {ifdef}\n" - c = f" case {i}: {{\n" + + c = f" case {message_name}::MESSAGE_TYPE: {{\n" c += indent(case, " ") + "\n" c += " }" out += c + "\n" From f863189f96c4b048efbf56fd8e326fecd6340624 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 17:18:01 -1000 Subject: [PATCH 101/125] [api] Simplify generated authentication check code (#9806) --- esphome/components/api/api_pb2_service.cpp | 30 ++++++++-------------- script/api_protobuf/api_protobuf.py | 19 ++++++-------- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4ac2f75fb0..d8ff4fcd24 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -617,10 +617,8 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) { } } void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { - if (this->check_connection_setup_()) { - if (!this->send_device_info_response(msg)) { - this->on_fatal_error(); - } + if (this->check_connection_setup_() && !this->send_device_info_response(msg)) { + this->on_fatal_error(); } } void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { @@ -650,10 +648,8 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc } } void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { - if (this->check_connection_setup_()) { - if (!this->send_get_time_response(msg)) { - this->on_fatal_error(); - } + if (this->check_connection_setup_() && !this->send_get_time_response(msg)) { + this->on_fatal_error(); } } #ifdef USE_API_SERVICES @@ -665,10 +661,8 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest #endif #ifdef USE_API_NOISE void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { - if (this->check_authenticated_()) { - if (!this->send_noise_encryption_set_key_response(msg)) { - this->on_fatal_error(); - } + if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) { + this->on_fatal_error(); } } #endif @@ -858,10 +852,8 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_connections_free_request( const SubscribeBluetoothConnectionsFreeRequest &msg) { - if (this->check_authenticated_()) { - if (!this->send_subscribe_bluetooth_connections_free_response(msg)) { - this->on_fatal_error(); - } + if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) { + this->on_fatal_error(); } } #endif @@ -889,10 +881,8 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { - if (this->check_authenticated_()) { - if (!this->send_voice_assistant_get_configuration_response(msg)) { - this->on_fatal_error(); - } + if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) { + this->on_fatal_error(); } } #endif diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 24ee1aaa47..fb5db70ae8 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2261,19 +2261,16 @@ static const char *const TAG = "api.service"; else: check_func = "this->check_connection_setup_()" - body = f"if ({check_func}) {{\n" - - # Add the actual handler code, indented - handler_body = "" if is_void: - handler_body = f"this->{func}(msg);\n" + # For void methods, just wrap with auth check + body = f"if ({check_func}) {{\n" + body += f" this->{func}(msg);\n" + body += "}\n" else: - handler_body = f"if (!this->send_{func}_response(msg)) {{\n" - handler_body += " this->on_fatal_error();\n" - handler_body += "}\n" - - body += indent(handler_body) + "\n" - body += "}\n" + # For non-void methods, combine auth check and send response check + body = f"if ({check_func} && !this->send_{func}_response(msg)) {{\n" + body += " this->on_fatal_error();\n" + body += "}\n" else: # No auth check needed, just call the handler body = "" From 4a27b34685ff6792e352355d2cf265849e2b7528 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 17:19:58 -1000 Subject: [PATCH 102/125] [api] Reduce code duplication in protobuf dump methods with helper functions (#9809) --- esphome/components/api/api_pb2_dump.cpp | 4042 +++++------------------ script/api_protobuf/api_protobuf.py | 195 +- 2 files changed, 1014 insertions(+), 3223 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 4e44bff11e..a2e69255e1 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -19,6 +19,82 @@ static inline void append_quoted_string(std::string &out, const StringRef &ref) out.append("'"); } +// Common helpers for dump_field functions +static inline void append_field_prefix(std::string &out, const char *field_name, int indent) { + out.append(indent, ' ').append(field_name).append(": "); +} + +static inline void append_with_newline(std::string &out, const char *str) { + out.append(str); + out.append("\n"); +} + +// RAII helper for message dump formatting +class MessageDumpHelper { + public: + MessageDumpHelper(std::string &out, const char *message_name) : out_(out) { + out_.append(message_name); + out_.append(" {\n"); + } + ~MessageDumpHelper() { out_.append(" }"); } + + private: + std::string &out_; +}; + +// Helper functions to reduce code duplication in dump methods +static void dump_field(std::string &out, const char *field_name, int32_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%" PRId32, value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, uint32_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%" PRIu32, value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, float value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%g", value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%llu", value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, bool value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append(YESNO(value)); + out.append("\n"); +} + +static void dump_field(std::string &out, const char *field_name, const std::string &value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\n"); +} + +static void dump_field(std::string &out, const char *field_name, StringRef value, int indent = 2) { + append_field_prefix(out, field_name, indent); + append_quoted_string(out, value); + out.append("\n"); +} + +template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append(proto_enum_to_string(value)); + out.append("\n"); +} + template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: @@ -558,61 +634,20 @@ template<> const char *proto_enum_to_string(enums::UpdateC #endif void HelloRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HelloRequest {\n"); - out.append(" client_info: "); - out.append("'").append(this->client_info).append("'"); - out.append("\n"); - - out.append(" api_version_major: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); - out.append(buffer); - out.append("\n"); - - out.append(" api_version_minor: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "HelloRequest"); + dump_field(out, "client_info", this->client_info); + dump_field(out, "api_version_major", this->api_version_major); + dump_field(out, "api_version_minor", this->api_version_minor); } void HelloResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HelloResponse {\n"); - out.append(" api_version_major: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); - out.append(buffer); - out.append("\n"); - - out.append(" api_version_minor: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); - out.append(buffer); - out.append("\n"); - - out.append(" server_info: "); - append_quoted_string(out, this->server_info_ref_); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - out.append("}"); -} -void ConnectRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ConnectRequest {\n"); - out.append(" password: "); - out.append("'").append(this->password).append("'"); - out.append("\n"); - out.append("}"); -} -void ConnectResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ConnectResponse {\n"); - out.append(" invalid_password: "); - out.append(YESNO(this->invalid_password)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "HelloResponse"); + dump_field(out, "api_version_major", this->api_version_major); + dump_field(out, "api_version_minor", this->api_version_minor); + dump_field(out, "server_info", this->server_info_ref_); + dump_field(out, "name", this->name_ref_); } +void ConnectRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); } +void ConnectResponse::dump_to(std::string &out) const { dump_field(out, "invalid_password", this->invalid_password); } void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } @@ -620,132 +655,57 @@ void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {} void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } #ifdef USE_AREAS void AreaInfo::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("AreaInfo {\n"); - out.append(" area_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "AreaInfo"); + dump_field(out, "area_id", this->area_id); + dump_field(out, "name", this->name_ref_); } #endif #ifdef USE_DEVICES void DeviceInfo::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DeviceInfo {\n"); - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" area_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "DeviceInfo"); + dump_field(out, "device_id", this->device_id); + dump_field(out, "name", this->name_ref_); + dump_field(out, "area_id", this->area_id); } #endif void DeviceInfoResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DeviceInfoResponse {\n"); + MessageDumpHelper helper(out, "DeviceInfoResponse"); #ifdef USE_API_PASSWORD - out.append(" uses_password: "); - out.append(YESNO(this->uses_password)); - out.append("\n"); - + dump_field(out, "uses_password", this->uses_password); #endif - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" mac_address: "); - append_quoted_string(out, this->mac_address_ref_); - out.append("\n"); - - out.append(" esphome_version: "); - append_quoted_string(out, this->esphome_version_ref_); - out.append("\n"); - - out.append(" compilation_time: "); - append_quoted_string(out, this->compilation_time_ref_); - out.append("\n"); - - out.append(" model: "); - append_quoted_string(out, this->model_ref_); - out.append("\n"); - + dump_field(out, "name", this->name_ref_); + dump_field(out, "mac_address", this->mac_address_ref_); + dump_field(out, "esphome_version", this->esphome_version_ref_); + dump_field(out, "compilation_time", this->compilation_time_ref_); + dump_field(out, "model", this->model_ref_); #ifdef USE_DEEP_SLEEP - out.append(" has_deep_sleep: "); - out.append(YESNO(this->has_deep_sleep)); - out.append("\n"); - + dump_field(out, "has_deep_sleep", this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - out.append(" project_name: "); - append_quoted_string(out, this->project_name_ref_); - out.append("\n"); - + dump_field(out, "project_name", this->project_name_ref_); #endif #ifdef ESPHOME_PROJECT_NAME - out.append(" project_version: "); - append_quoted_string(out, this->project_version_ref_); - out.append("\n"); - + dump_field(out, "project_version", this->project_version_ref_); #endif #ifdef USE_WEBSERVER - out.append(" webserver_port: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->webserver_port); - out.append(buffer); - out.append("\n"); - + dump_field(out, "webserver_port", this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - out.append(" bluetooth_proxy_feature_flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->bluetooth_proxy_feature_flags); - out.append(buffer); - out.append("\n"); - + dump_field(out, "bluetooth_proxy_feature_flags", this->bluetooth_proxy_feature_flags); #endif - out.append(" manufacturer: "); - append_quoted_string(out, this->manufacturer_ref_); - out.append("\n"); - - out.append(" friendly_name: "); - append_quoted_string(out, this->friendly_name_ref_); - out.append("\n"); - + dump_field(out, "manufacturer", this->manufacturer_ref_); + dump_field(out, "friendly_name", this->friendly_name_ref_); #ifdef USE_VOICE_ASSISTANT - out.append(" voice_assistant_feature_flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); - out.append(buffer); - out.append("\n"); - + dump_field(out, "voice_assistant_feature_flags", this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - out.append(" suggested_area: "); - append_quoted_string(out, this->suggested_area_ref_); - out.append("\n"); - + dump_field(out, "suggested_area", this->suggested_area_ref_); #endif #ifdef USE_BLUETOOTH_PROXY - out.append(" bluetooth_mac_address: "); - append_quoted_string(out, this->bluetooth_mac_address_ref_); - out.append("\n"); - + dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address_ref_); #endif #ifdef USE_API_NOISE - out.append(" api_encryption_supported: "); - out.append(YESNO(this->api_encryption_supported)); - out.append("\n"); - + dump_field(out, "api_encryption_supported", this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { @@ -753,7 +713,6 @@ void DeviceInfoResponse::dump_to(std::string &out) const { it.dump_to(out); out.append("\n"); } - #endif #ifdef USE_AREAS for (const auto &it : this->areas) { @@ -761,2736 +720,1008 @@ void DeviceInfoResponse::dump_to(std::string &out) const { it.dump_to(out); out.append("\n"); } - #endif #ifdef USE_AREAS out.append(" area: "); this->area.dump_to(out); out.append("\n"); - #endif - out.append("}"); } void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesBinarySensorResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - - out.append(" is_status_binary_sensor: "); - out.append(YESNO(this->is_status_binary_sensor)); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "is_status_binary_sensor", this->is_status_binary_sensor); + dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void BinarySensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BinarySensorStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - + MessageDumpHelper helper(out, "BinarySensorStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_COVER void ListEntitiesCoverResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesCoverResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" supports_position: "); - out.append(YESNO(this->supports_position)); - out.append("\n"); - - out.append(" supports_tilt: "); - out.append(YESNO(this->supports_tilt)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesCoverResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "assumed_state", this->assumed_state); + dump_field(out, "supports_position", this->supports_position); + dump_field(out, "supports_tilt", this->supports_tilt); + dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supports_stop: "); - out.append(YESNO(this->supports_stop)); - out.append("\n"); - + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "supports_stop", this->supports_stop); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void CoverStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CoverStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" position: "); - snprintf(buffer, sizeof(buffer), "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" tilt: "); - snprintf(buffer, sizeof(buffer), "%g", this->tilt); - out.append(buffer); - out.append("\n"); - - out.append(" current_operation: "); - out.append(proto_enum_to_string(this->current_operation)); - out.append("\n"); - + MessageDumpHelper helper(out, "CoverStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "position", this->position); + dump_field(out, "tilt", this->tilt); + dump_field(out, "current_operation", static_cast(this->current_operation)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void CoverCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CoverCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_position: "); - out.append(YESNO(this->has_position)); - out.append("\n"); - - out.append(" position: "); - snprintf(buffer, sizeof(buffer), "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" has_tilt: "); - out.append(YESNO(this->has_tilt)); - out.append("\n"); - - out.append(" tilt: "); - snprintf(buffer, sizeof(buffer), "%g", this->tilt); - out.append(buffer); - out.append("\n"); - - out.append(" stop: "); - out.append(YESNO(this->stop)); - out.append("\n"); - + MessageDumpHelper helper(out, "CoverCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_position", this->has_position); + dump_field(out, "position", this->position); + dump_field(out, "has_tilt", this->has_tilt); + dump_field(out, "tilt", this->tilt); + dump_field(out, "stop", this->stop); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_FAN void ListEntitiesFanResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesFanResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" supports_oscillation: "); - out.append(YESNO(this->supports_oscillation)); - out.append("\n"); - - out.append(" supports_speed: "); - out.append(YESNO(this->supports_speed)); - out.append("\n"); - - out.append(" supports_direction: "); - out.append(YESNO(this->supports_direction)); - out.append("\n"); - - out.append(" supported_speed_count: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->supported_speed_count); - out.append(buffer); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesFanResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "supports_oscillation", this->supports_oscillation); + dump_field(out, "supports_speed", this->supports_speed); + dump_field(out, "supports_direction", this->supports_direction); + dump_field(out, "supported_speed_count", this->supported_speed_count); + dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "entity_category", static_cast(this->entity_category)); for (const auto &it : this->supported_preset_modes) { - out.append(" supported_preset_modes: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "supported_preset_modes", it, 4); } - #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void FanStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("FanStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" oscillating: "); - out.append(YESNO(this->oscillating)); - out.append("\n"); - - out.append(" direction: "); - out.append(proto_enum_to_string(this->direction)); - out.append("\n"); - - out.append(" speed_level: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); - out.append(buffer); - out.append("\n"); - - out.append(" preset_mode: "); - append_quoted_string(out, this->preset_mode_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "FanStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "oscillating", this->oscillating); + dump_field(out, "direction", static_cast(this->direction)); + dump_field(out, "speed_level", this->speed_level); + dump_field(out, "preset_mode", this->preset_mode_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void FanCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("FanCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" has_oscillating: "); - out.append(YESNO(this->has_oscillating)); - out.append("\n"); - - out.append(" oscillating: "); - out.append(YESNO(this->oscillating)); - out.append("\n"); - - out.append(" has_direction: "); - out.append(YESNO(this->has_direction)); - out.append("\n"); - - out.append(" direction: "); - out.append(proto_enum_to_string(this->direction)); - out.append("\n"); - - out.append(" has_speed_level: "); - out.append(YESNO(this->has_speed_level)); - out.append("\n"); - - out.append(" speed_level: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); - out.append(buffer); - out.append("\n"); - - out.append(" has_preset_mode: "); - out.append(YESNO(this->has_preset_mode)); - out.append("\n"); - - out.append(" preset_mode: "); - out.append("'").append(this->preset_mode).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "FanCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_state", this->has_state); + dump_field(out, "state", this->state); + dump_field(out, "has_oscillating", this->has_oscillating); + dump_field(out, "oscillating", this->oscillating); + dump_field(out, "has_direction", this->has_direction); + dump_field(out, "direction", static_cast(this->direction)); + dump_field(out, "has_speed_level", this->has_speed_level); + dump_field(out, "speed_level", this->speed_level); + dump_field(out, "has_preset_mode", this->has_preset_mode); + dump_field(out, "preset_mode", this->preset_mode); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_LIGHT void ListEntitiesLightResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesLightResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesLightResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); for (const auto &it : this->supported_color_modes) { - out.append(" supported_color_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + dump_field(out, "supported_color_modes", static_cast(it), 4); } - - out.append(" min_mireds: "); - snprintf(buffer, sizeof(buffer), "%g", this->min_mireds); - out.append(buffer); - out.append("\n"); - - out.append(" max_mireds: "); - snprintf(buffer, sizeof(buffer), "%g", this->max_mireds); - out.append(buffer); - out.append("\n"); - + dump_field(out, "min_mireds", this->min_mireds); + dump_field(out, "max_mireds", this->max_mireds); for (const auto &it : this->effects) { - out.append(" effects: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "effects", it, 4); } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void LightStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LightStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" brightness: "); - snprintf(buffer, sizeof(buffer), "%g", this->brightness); - out.append(buffer); - out.append("\n"); - - out.append(" color_mode: "); - out.append(proto_enum_to_string(this->color_mode)); - out.append("\n"); - - out.append(" color_brightness: "); - snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); - out.append(buffer); - out.append("\n"); - - out.append(" red: "); - snprintf(buffer, sizeof(buffer), "%g", this->red); - out.append(buffer); - out.append("\n"); - - out.append(" green: "); - snprintf(buffer, sizeof(buffer), "%g", this->green); - out.append(buffer); - out.append("\n"); - - out.append(" blue: "); - snprintf(buffer, sizeof(buffer), "%g", this->blue); - out.append(buffer); - out.append("\n"); - - out.append(" white: "); - snprintf(buffer, sizeof(buffer), "%g", this->white); - out.append(buffer); - out.append("\n"); - - out.append(" color_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" cold_white: "); - snprintf(buffer, sizeof(buffer), "%g", this->cold_white); - out.append(buffer); - out.append("\n"); - - out.append(" warm_white: "); - snprintf(buffer, sizeof(buffer), "%g", this->warm_white); - out.append(buffer); - out.append("\n"); - - out.append(" effect: "); - append_quoted_string(out, this->effect_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "LightStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "brightness", this->brightness); + dump_field(out, "color_mode", static_cast(this->color_mode)); + dump_field(out, "color_brightness", this->color_brightness); + dump_field(out, "red", this->red); + dump_field(out, "green", this->green); + dump_field(out, "blue", this->blue); + dump_field(out, "white", this->white); + dump_field(out, "color_temperature", this->color_temperature); + dump_field(out, "cold_white", this->cold_white); + dump_field(out, "warm_white", this->warm_white); + dump_field(out, "effect", this->effect_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void LightCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LightCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" has_brightness: "); - out.append(YESNO(this->has_brightness)); - out.append("\n"); - - out.append(" brightness: "); - snprintf(buffer, sizeof(buffer), "%g", this->brightness); - out.append(buffer); - out.append("\n"); - - out.append(" has_color_mode: "); - out.append(YESNO(this->has_color_mode)); - out.append("\n"); - - out.append(" color_mode: "); - out.append(proto_enum_to_string(this->color_mode)); - out.append("\n"); - - out.append(" has_color_brightness: "); - out.append(YESNO(this->has_color_brightness)); - out.append("\n"); - - out.append(" color_brightness: "); - snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); - out.append(buffer); - out.append("\n"); - - out.append(" has_rgb: "); - out.append(YESNO(this->has_rgb)); - out.append("\n"); - - out.append(" red: "); - snprintf(buffer, sizeof(buffer), "%g", this->red); - out.append(buffer); - out.append("\n"); - - out.append(" green: "); - snprintf(buffer, sizeof(buffer), "%g", this->green); - out.append(buffer); - out.append("\n"); - - out.append(" blue: "); - snprintf(buffer, sizeof(buffer), "%g", this->blue); - out.append(buffer); - out.append("\n"); - - out.append(" has_white: "); - out.append(YESNO(this->has_white)); - out.append("\n"); - - out.append(" white: "); - snprintf(buffer, sizeof(buffer), "%g", this->white); - out.append(buffer); - out.append("\n"); - - out.append(" has_color_temperature: "); - out.append(YESNO(this->has_color_temperature)); - out.append("\n"); - - out.append(" color_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" has_cold_white: "); - out.append(YESNO(this->has_cold_white)); - out.append("\n"); - - out.append(" cold_white: "); - snprintf(buffer, sizeof(buffer), "%g", this->cold_white); - out.append(buffer); - out.append("\n"); - - out.append(" has_warm_white: "); - out.append(YESNO(this->has_warm_white)); - out.append("\n"); - - out.append(" warm_white: "); - snprintf(buffer, sizeof(buffer), "%g", this->warm_white); - out.append(buffer); - out.append("\n"); - - out.append(" has_transition_length: "); - out.append(YESNO(this->has_transition_length)); - out.append("\n"); - - out.append(" transition_length: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->transition_length); - out.append(buffer); - out.append("\n"); - - out.append(" has_flash_length: "); - out.append(YESNO(this->has_flash_length)); - out.append("\n"); - - out.append(" flash_length: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flash_length); - out.append(buffer); - out.append("\n"); - - out.append(" has_effect: "); - out.append(YESNO(this->has_effect)); - out.append("\n"); - - out.append(" effect: "); - out.append("'").append(this->effect).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "LightCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_state", this->has_state); + dump_field(out, "state", this->state); + dump_field(out, "has_brightness", this->has_brightness); + dump_field(out, "brightness", this->brightness); + dump_field(out, "has_color_mode", this->has_color_mode); + dump_field(out, "color_mode", static_cast(this->color_mode)); + dump_field(out, "has_color_brightness", this->has_color_brightness); + dump_field(out, "color_brightness", this->color_brightness); + dump_field(out, "has_rgb", this->has_rgb); + dump_field(out, "red", this->red); + dump_field(out, "green", this->green); + dump_field(out, "blue", this->blue); + dump_field(out, "has_white", this->has_white); + dump_field(out, "white", this->white); + dump_field(out, "has_color_temperature", this->has_color_temperature); + dump_field(out, "color_temperature", this->color_temperature); + dump_field(out, "has_cold_white", this->has_cold_white); + dump_field(out, "cold_white", this->cold_white); + dump_field(out, "has_warm_white", this->has_warm_white); + dump_field(out, "warm_white", this->warm_white); + dump_field(out, "has_transition_length", this->has_transition_length); + dump_field(out, "transition_length", this->transition_length); + dump_field(out, "has_flash_length", this->has_flash_length); + dump_field(out, "flash_length", this->flash_length); + dump_field(out, "has_effect", this->has_effect); + dump_field(out, "effect", this->effect); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_SENSOR void ListEntitiesSensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSensorResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesSensorResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" unit_of_measurement: "); - append_quoted_string(out, this->unit_of_measurement_ref_); - out.append("\n"); - - out.append(" accuracy_decimals: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->accuracy_decimals); - out.append(buffer); - out.append("\n"); - - out.append(" force_update: "); - out.append(YESNO(this->force_update)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - - out.append(" state_class: "); - out.append(proto_enum_to_string(this->state_class)); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "unit_of_measurement", this->unit_of_measurement_ref_); + dump_field(out, "accuracy_decimals", this->accuracy_decimals); + dump_field(out, "force_update", this->force_update); + dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "state_class", static_cast(this->state_class)); + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void SensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SensorStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - snprintf(buffer, sizeof(buffer), "%g", this->state); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - + MessageDumpHelper helper(out, "SensorStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSwitchResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesSwitchResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - + dump_field(out, "assumed_state", this->assumed_state); + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void SwitchStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SwitchStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - + MessageDumpHelper helper(out, "SwitchStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void SwitchCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SwitchCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - + MessageDumpHelper helper(out, "SwitchCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTextSensorResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesTextSensorResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void TextSensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextSensorStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - append_quoted_string(out, this->state_ref_); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - + MessageDumpHelper helper(out, "TextSensorStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state_ref_); + dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif void SubscribeLogsRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeLogsRequest {\n"); - out.append(" level: "); - out.append(proto_enum_to_string(this->level)); - out.append("\n"); - - out.append(" dump_config: "); - out.append(YESNO(this->dump_config)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SubscribeLogsRequest"); + dump_field(out, "level", static_cast(this->level)); + dump_field(out, "dump_config", this->dump_config); } void SubscribeLogsResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeLogsResponse {\n"); - out.append(" level: "); - out.append(proto_enum_to_string(this->level)); - out.append("\n"); - + MessageDumpHelper helper(out, "SubscribeLogsResponse"); + dump_field(out, "level", static_cast(this->level)); out.append(" message: "); out.append(format_hex_pretty(this->message_ptr_, this->message_len_)); out.append("\n"); - out.append("}"); } #ifdef USE_API_NOISE void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NoiseEncryptionSetKeyRequest {\n"); + MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); out.append(" key: "); out.append(format_hex_pretty(reinterpret_cast(this->key.data()), this->key.size())); out.append("\n"); - out.append("}"); -} -void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NoiseEncryptionSetKeyResponse {\n"); - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - out.append("}"); } +void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); } #endif void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeassistantServicesRequest {}"); } void HomeassistantServiceMap::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeassistantServiceMap {\n"); - out.append(" key: "); - append_quoted_string(out, this->key_ref_); - out.append("\n"); - - out.append(" value: "); - append_quoted_string(out, this->value_ref_); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "HomeassistantServiceMap"); + dump_field(out, "key", this->key_ref_); + dump_field(out, "value", this->value_ref_); } void HomeassistantServiceResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeassistantServiceResponse {\n"); - out.append(" service: "); - append_quoted_string(out, this->service_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "HomeassistantServiceResponse"); + dump_field(out, "service", this->service_ref_); for (const auto &it : this->data) { out.append(" data: "); it.dump_to(out); out.append("\n"); } - for (const auto &it : this->data_template) { out.append(" data_template: "); it.dump_to(out); out.append("\n"); } - for (const auto &it : this->variables) { out.append(" variables: "); it.dump_to(out); out.append("\n"); } - - out.append(" is_event: "); - out.append(YESNO(this->is_event)); - out.append("\n"); - out.append("}"); + dump_field(out, "is_event", this->is_event); } void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); } void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeHomeAssistantStateResponse {\n"); - out.append(" entity_id: "); - append_quoted_string(out, this->entity_id_ref_); - out.append("\n"); - - out.append(" attribute: "); - append_quoted_string(out, this->attribute_ref_); - out.append("\n"); - - out.append(" once: "); - out.append(YESNO(this->once)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); + dump_field(out, "entity_id", this->entity_id_ref_); + dump_field(out, "attribute", this->attribute_ref_); + dump_field(out, "once", this->once); } void HomeAssistantStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeAssistantStateResponse {\n"); - out.append(" entity_id: "); - out.append("'").append(this->entity_id).append("'"); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" attribute: "); - out.append("'").append(this->attribute).append("'"); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "HomeAssistantStateResponse"); + dump_field(out, "entity_id", this->entity_id); + dump_field(out, "state", this->state); + dump_field(out, "attribute", this->attribute); } void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } -void GetTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("GetTimeResponse {\n"); - out.append(" epoch_seconds: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - out.append("}"); -} +void GetTimeResponse::dump_to(std::string &out) const { dump_field(out, "epoch_seconds", this->epoch_seconds); } #ifdef USE_API_SERVICES void ListEntitiesServicesArgument::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesServicesArgument {\n"); - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" type: "); - out.append(proto_enum_to_string(this->type)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); + dump_field(out, "name", this->name_ref_); + dump_field(out, "type", static_cast(this->type)); } void ListEntitiesServicesResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesServicesResponse {\n"); - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesServicesResponse"); + dump_field(out, "name", this->name_ref_); + dump_field(out, "key", this->key); for (const auto &it : this->args) { out.append(" args: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void ExecuteServiceArgument::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ExecuteServiceArgument {\n"); - out.append(" bool_: "); - out.append(YESNO(this->bool_)); - out.append("\n"); - - out.append(" legacy_int: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->legacy_int); - out.append(buffer); - out.append("\n"); - - out.append(" float_: "); - snprintf(buffer, sizeof(buffer), "%g", this->float_); - out.append(buffer); - out.append("\n"); - - out.append(" string_: "); - out.append("'").append(this->string_).append("'"); - out.append("\n"); - - out.append(" int_: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->int_); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ExecuteServiceArgument"); + dump_field(out, "bool_", this->bool_); + dump_field(out, "legacy_int", this->legacy_int); + dump_field(out, "float_", this->float_); + dump_field(out, "string_", this->string_); + dump_field(out, "int_", this->int_); for (const auto it : this->bool_array) { - out.append(" bool_array: "); - out.append(YESNO(it)); - out.append("\n"); + dump_field(out, "bool_array", it, 4); } - for (const auto &it : this->int_array) { - out.append(" int_array: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, it); - out.append(buffer); - out.append("\n"); + dump_field(out, "int_array", it, 4); } - for (const auto &it : this->float_array) { - out.append(" float_array: "); - snprintf(buffer, sizeof(buffer), "%g", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "float_array", it, 4); } - for (const auto &it : this->string_array) { - out.append(" string_array: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "string_array", it, 4); } - out.append("}"); } void ExecuteServiceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ExecuteServiceRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ExecuteServiceRequest"); + dump_field(out, "key", this->key); for (const auto &it : this->args) { out.append(" args: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesCameraResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesCameraResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void CameraImageResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CameraImageResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "CameraImageResponse"); + dump_field(out, "key", this->key); out.append(" data: "); out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); - - out.append(" done: "); - out.append(YESNO(this->done)); - out.append("\n"); - + dump_field(out, "done", this->done); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void CameraImageRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CameraImageRequest {\n"); - out.append(" single: "); - out.append(YESNO(this->single)); - out.append("\n"); - - out.append(" stream: "); - out.append(YESNO(this->stream)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "CameraImageRequest"); + dump_field(out, "single", this->single); + dump_field(out, "stream", this->stream); } #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesClimateResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - - out.append(" supports_current_temperature: "); - out.append(YESNO(this->supports_current_temperature)); - out.append("\n"); - - out.append(" supports_two_point_target_temperature: "); - out.append(YESNO(this->supports_two_point_target_temperature)); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesClimateResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "supports_current_temperature", this->supports_current_temperature); + dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature); for (const auto &it : this->supported_modes) { - out.append(" supported_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + dump_field(out, "supported_modes", static_cast(it), 4); } - - out.append(" visual_min_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_min_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" visual_max_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_max_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" visual_target_temperature_step: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_target_temperature_step); - out.append(buffer); - out.append("\n"); - - out.append(" supports_action: "); - out.append(YESNO(this->supports_action)); - out.append("\n"); - + dump_field(out, "visual_min_temperature", this->visual_min_temperature); + dump_field(out, "visual_max_temperature", this->visual_max_temperature); + dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step); + dump_field(out, "supports_action", this->supports_action); for (const auto &it : this->supported_fan_modes) { - out.append(" supported_fan_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + dump_field(out, "supported_fan_modes", static_cast(it), 4); } - for (const auto &it : this->supported_swing_modes) { - out.append(" supported_swing_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + dump_field(out, "supported_swing_modes", static_cast(it), 4); } - for (const auto &it : this->supported_custom_fan_modes) { - out.append(" supported_custom_fan_modes: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "supported_custom_fan_modes", it, 4); } - for (const auto &it : this->supported_presets) { - out.append(" supported_presets: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + dump_field(out, "supported_presets", static_cast(it), 4); } - for (const auto &it : this->supported_custom_presets) { - out.append(" supported_custom_presets: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "supported_custom_presets", it, 4); } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" visual_current_temperature_step: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_current_temperature_step); - out.append(buffer); - out.append("\n"); - - out.append(" supports_current_humidity: "); - out.append(YESNO(this->supports_current_humidity)); - out.append("\n"); - - out.append(" supports_target_humidity: "); - out.append(YESNO(this->supports_target_humidity)); - out.append("\n"); - - out.append(" visual_min_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_min_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" visual_max_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_max_humidity); - out.append(buffer); - out.append("\n"); - + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "visual_current_temperature_step", this->visual_current_temperature_step); + dump_field(out, "supports_current_humidity", this->supports_current_humidity); + dump_field(out, "supports_target_humidity", this->supports_target_humidity); + dump_field(out, "visual_min_humidity", this->visual_min_humidity); + dump_field(out, "visual_max_humidity", this->visual_max_humidity); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void ClimateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ClimateStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" current_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->current_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature_low: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature_high: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); - out.append(buffer); - out.append("\n"); - - out.append(" action: "); - out.append(proto_enum_to_string(this->action)); - out.append("\n"); - - out.append(" fan_mode: "); - out.append(proto_enum_to_string(this->fan_mode)); - out.append("\n"); - - out.append(" swing_mode: "); - out.append(proto_enum_to_string(this->swing_mode)); - out.append("\n"); - - out.append(" custom_fan_mode: "); - append_quoted_string(out, this->custom_fan_mode_ref_); - out.append("\n"); - - out.append(" preset: "); - out.append(proto_enum_to_string(this->preset)); - out.append("\n"); - - out.append(" custom_preset: "); - append_quoted_string(out, this->custom_preset_ref_); - out.append("\n"); - - out.append(" current_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->current_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" target_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ClimateStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "current_temperature", this->current_temperature); + dump_field(out, "target_temperature", this->target_temperature); + dump_field(out, "target_temperature_low", this->target_temperature_low); + dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, "action", static_cast(this->action)); + dump_field(out, "fan_mode", static_cast(this->fan_mode)); + dump_field(out, "swing_mode", static_cast(this->swing_mode)); + dump_field(out, "custom_fan_mode", this->custom_fan_mode_ref_); + dump_field(out, "preset", static_cast(this->preset)); + dump_field(out, "custom_preset", this->custom_preset_ref_); + dump_field(out, "current_humidity", this->current_humidity); + dump_field(out, "target_humidity", this->target_humidity); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void ClimateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ClimateCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_mode: "); - out.append(YESNO(this->has_mode)); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" has_target_temperature: "); - out.append(YESNO(this->has_target_temperature)); - out.append("\n"); - - out.append(" target_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" has_target_temperature_low: "); - out.append(YESNO(this->has_target_temperature_low)); - out.append("\n"); - - out.append(" target_temperature_low: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); - out.append(buffer); - out.append("\n"); - - out.append(" has_target_temperature_high: "); - out.append(YESNO(this->has_target_temperature_high)); - out.append("\n"); - - out.append(" target_temperature_high: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); - out.append(buffer); - out.append("\n"); - - out.append(" has_fan_mode: "); - out.append(YESNO(this->has_fan_mode)); - out.append("\n"); - - out.append(" fan_mode: "); - out.append(proto_enum_to_string(this->fan_mode)); - out.append("\n"); - - out.append(" has_swing_mode: "); - out.append(YESNO(this->has_swing_mode)); - out.append("\n"); - - out.append(" swing_mode: "); - out.append(proto_enum_to_string(this->swing_mode)); - out.append("\n"); - - out.append(" has_custom_fan_mode: "); - out.append(YESNO(this->has_custom_fan_mode)); - out.append("\n"); - - out.append(" custom_fan_mode: "); - out.append("'").append(this->custom_fan_mode).append("'"); - out.append("\n"); - - out.append(" has_preset: "); - out.append(YESNO(this->has_preset)); - out.append("\n"); - - out.append(" preset: "); - out.append(proto_enum_to_string(this->preset)); - out.append("\n"); - - out.append(" has_custom_preset: "); - out.append(YESNO(this->has_custom_preset)); - out.append("\n"); - - out.append(" custom_preset: "); - out.append("'").append(this->custom_preset).append("'"); - out.append("\n"); - - out.append(" has_target_humidity: "); - out.append(YESNO(this->has_target_humidity)); - out.append("\n"); - - out.append(" target_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ClimateCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_mode", this->has_mode); + dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "has_target_temperature", this->has_target_temperature); + dump_field(out, "target_temperature", this->target_temperature); + dump_field(out, "has_target_temperature_low", this->has_target_temperature_low); + dump_field(out, "target_temperature_low", this->target_temperature_low); + dump_field(out, "has_target_temperature_high", this->has_target_temperature_high); + dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, "has_fan_mode", this->has_fan_mode); + dump_field(out, "fan_mode", static_cast(this->fan_mode)); + dump_field(out, "has_swing_mode", this->has_swing_mode); + dump_field(out, "swing_mode", static_cast(this->swing_mode)); + dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); + dump_field(out, "custom_fan_mode", this->custom_fan_mode); + dump_field(out, "has_preset", this->has_preset); + dump_field(out, "preset", static_cast(this->preset)); + dump_field(out, "has_custom_preset", this->has_custom_preset); + dump_field(out, "custom_preset", this->custom_preset); + dump_field(out, "has_target_humidity", this->has_target_humidity); + dump_field(out, "target_humidity", this->target_humidity); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesNumberResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" min_value: "); - snprintf(buffer, sizeof(buffer), "%g", this->min_value); - out.append(buffer); - out.append("\n"); - - out.append(" max_value: "); - snprintf(buffer, sizeof(buffer), "%g", this->max_value); - out.append(buffer); - out.append("\n"); - - out.append(" step: "); - snprintf(buffer, sizeof(buffer), "%g", this->step); - out.append(buffer); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" unit_of_measurement: "); - append_quoted_string(out, this->unit_of_measurement_ref_); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - + dump_field(out, "min_value", this->min_value); + dump_field(out, "max_value", this->max_value); + dump_field(out, "step", this->step); + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "unit_of_measurement", this->unit_of_measurement_ref_); + dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "device_class", this->device_class_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void NumberStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NumberStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - snprintf(buffer, sizeof(buffer), "%g", this->state); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - + MessageDumpHelper helper(out, "NumberStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void NumberCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NumberCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - snprintf(buffer, sizeof(buffer), "%g", this->state); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "NumberCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_SELECT void ListEntitiesSelectResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSelectResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesSelectResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif for (const auto &it : this->options) { - out.append(" options: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "options", it, 4); } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void SelectStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SelectStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - append_quoted_string(out, this->state_ref_); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - + MessageDumpHelper helper(out, "SelectStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state_ref_); + dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void SelectCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SelectCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "SelectCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_SIREN void ListEntitiesSirenResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSirenResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesSirenResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); for (const auto &it : this->tones) { - out.append(" tones: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "tones", it, 4); } - - out.append(" supports_duration: "); - out.append(YESNO(this->supports_duration)); - out.append("\n"); - - out.append(" supports_volume: "); - out.append(YESNO(this->supports_volume)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "supports_duration", this->supports_duration); + dump_field(out, "supports_volume", this->supports_volume); + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void SirenStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SirenStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - + MessageDumpHelper helper(out, "SirenStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void SirenCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SirenCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" has_tone: "); - out.append(YESNO(this->has_tone)); - out.append("\n"); - - out.append(" tone: "); - out.append("'").append(this->tone).append("'"); - out.append("\n"); - - out.append(" has_duration: "); - out.append(YESNO(this->has_duration)); - out.append("\n"); - - out.append(" duration: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->duration); - out.append(buffer); - out.append("\n"); - - out.append(" has_volume: "); - out.append(YESNO(this->has_volume)); - out.append("\n"); - - out.append(" volume: "); - snprintf(buffer, sizeof(buffer), "%g", this->volume); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "SirenCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_state", this->has_state); + dump_field(out, "state", this->state); + dump_field(out, "has_tone", this->has_tone); + dump_field(out, "tone", this->tone); + dump_field(out, "has_duration", this->has_duration); + dump_field(out, "duration", this->duration); + dump_field(out, "has_volume", this->has_volume); + dump_field(out, "volume", this->volume); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_LOCK void ListEntitiesLockResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesLockResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesLockResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" supports_open: "); - out.append(YESNO(this->supports_open)); - out.append("\n"); - - out.append(" requires_code: "); - out.append(YESNO(this->requires_code)); - out.append("\n"); - - out.append(" code_format: "); - append_quoted_string(out, this->code_format_ref_); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "assumed_state", this->assumed_state); + dump_field(out, "supports_open", this->supports_open); + dump_field(out, "requires_code", this->requires_code); + dump_field(out, "code_format", this->code_format_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void LockStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LockStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - + MessageDumpHelper helper(out, "LockStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", static_cast(this->state)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void LockCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LockCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" has_code: "); - out.append(YESNO(this->has_code)); - out.append("\n"); - - out.append(" code: "); - out.append("'").append(this->code).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "LockCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "command", static_cast(this->command)); + dump_field(out, "has_code", this->has_code); + dump_field(out, "code", this->code); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_BUTTON void ListEntitiesButtonResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesButtonResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesButtonResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void ButtonCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ButtonCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ButtonCommandRequest"); + dump_field(out, "key", this->key); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_MEDIA_PLAYER void MediaPlayerSupportedFormat::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerSupportedFormat {\n"); - out.append(" format: "); - append_quoted_string(out, this->format_ref_); - out.append("\n"); - - out.append(" sample_rate: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_rate); - out.append(buffer); - out.append("\n"); - - out.append(" num_channels: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->num_channels); - out.append(buffer); - out.append("\n"); - - out.append(" purpose: "); - out.append(proto_enum_to_string(this->purpose)); - out.append("\n"); - - out.append(" sample_bytes: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_bytes); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "MediaPlayerSupportedFormat"); + dump_field(out, "format", this->format_ref_); + dump_field(out, "sample_rate", this->sample_rate); + dump_field(out, "num_channels", this->num_channels); + dump_field(out, "purpose", static_cast(this->purpose)); + dump_field(out, "sample_bytes", this->sample_bytes); } void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesMediaPlayerResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesMediaPlayerResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supports_pause: "); - out.append(YESNO(this->supports_pause)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "supports_pause", this->supports_pause); for (const auto &it : this->supported_formats) { out.append(" supported_formats: "); it.dump_to(out); out.append("\n"); } - #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void MediaPlayerStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - - out.append(" volume: "); - snprintf(buffer, sizeof(buffer), "%g", this->volume); - out.append(buffer); - out.append("\n"); - - out.append(" muted: "); - out.append(YESNO(this->muted)); - out.append("\n"); - + MessageDumpHelper helper(out, "MediaPlayerStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", static_cast(this->state)); + dump_field(out, "volume", this->volume); + dump_field(out, "muted", this->muted); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void MediaPlayerCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_command: "); - out.append(YESNO(this->has_command)); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" has_volume: "); - out.append(YESNO(this->has_volume)); - out.append("\n"); - - out.append(" volume: "); - snprintf(buffer, sizeof(buffer), "%g", this->volume); - out.append(buffer); - out.append("\n"); - - out.append(" has_media_url: "); - out.append(YESNO(this->has_media_url)); - out.append("\n"); - - out.append(" media_url: "); - out.append("'").append(this->media_url).append("'"); - out.append("\n"); - - out.append(" has_announcement: "); - out.append(YESNO(this->has_announcement)); - out.append("\n"); - - out.append(" announcement: "); - out.append(YESNO(this->announcement)); - out.append("\n"); - + MessageDumpHelper helper(out, "MediaPlayerCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_command", this->has_command); + dump_field(out, "command", static_cast(this->command)); + dump_field(out, "has_volume", this->has_volume); + dump_field(out, "volume", this->volume); + dump_field(out, "has_media_url", this->has_media_url); + dump_field(out, "media_url", this->media_url); + dump_field(out, "has_announcement", this->has_announcement); + dump_field(out, "announcement", this->announcement); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_BLUETOOTH_PROXY void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); - out.append(" flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SubscribeBluetoothLEAdvertisementsRequest"); + dump_field(out, "flags", this->flags); } void BluetoothLERawAdvertisement::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLERawAdvertisement {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" rssi: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); - out.append(buffer); - out.append("\n"); - - out.append(" address_type: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothLERawAdvertisement"); + dump_field(out, "address", this->address); + dump_field(out, "rssi", this->rssi); + dump_field(out, "address_type", this->address_type); out.append(" data: "); out.append(format_hex_pretty(this->data, this->data_len)); out.append("\n"); - out.append("}"); } void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLERawAdvertisementsResponse {\n"); + MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); for (const auto &it : this->advertisements) { out.append(" advertisements: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void BluetoothDeviceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" request_type: "); - out.append(proto_enum_to_string(this->request_type)); - out.append("\n"); - - out.append(" has_address_type: "); - out.append(YESNO(this->has_address_type)); - out.append("\n"); - - out.append(" address_type: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDeviceRequest"); + dump_field(out, "address", this->address); + dump_field(out, "request_type", static_cast(this->request_type)); + dump_field(out, "has_address_type", this->has_address_type); + dump_field(out, "address_type", this->address_type); } void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceConnectionResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" connected: "); - out.append(YESNO(this->connected)); - out.append("\n"); - - out.append(" mtu: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->mtu); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDeviceConnectionResponse"); + dump_field(out, "address", this->address); + dump_field(out, "connected", this->connected); + dump_field(out, "mtu", this->mtu); + dump_field(out, "error", this->error); } +void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { dump_field(out, "address", this->address); } void BluetoothGATTDescriptor::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTDescriptor {\n"); + MessageDumpHelper helper(out, "BluetoothGATTDescriptor"); for (const auto &it : this->uuid) { - out.append(" uuid: "); - snprintf(buffer, sizeof(buffer), "%llu", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "uuid", it, 4); } - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "handle", this->handle); } void BluetoothGATTCharacteristic::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTCharacteristic {\n"); + MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); for (const auto &it : this->uuid) { - out.append(" uuid: "); - snprintf(buffer, sizeof(buffer), "%llu", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "uuid", it, 4); } - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" properties: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->properties); - out.append(buffer); - out.append("\n"); - + dump_field(out, "handle", this->handle); + dump_field(out, "properties", this->properties); for (const auto &it : this->descriptors) { out.append(" descriptors: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void BluetoothGATTService::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTService {\n"); + MessageDumpHelper helper(out, "BluetoothGATTService"); for (const auto &it : this->uuid) { - out.append(" uuid: "); - snprintf(buffer, sizeof(buffer), "%llu", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "uuid", it, 4); } - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - + dump_field(out, "handle", this->handle); for (const auto &it : this->characteristics) { out.append(" characteristics: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); + dump_field(out, "address", this->address); for (const auto &it : this->services) { out.append(" services: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesDoneResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTGetServicesDoneResponse"); + dump_field(out, "address", this->address); } void BluetoothGATTReadRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTReadRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); } void BluetoothGATTReadResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTReadResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); out.append(" data: "); out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); - out.append("}"); } void BluetoothGATTWriteRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" response: "); - out.append(YESNO(this->response)); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTWriteRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); + dump_field(out, "response", this->response); out.append(" data: "); out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); out.append("\n"); - out.append("}"); } void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadDescriptorRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTReadDescriptorRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); } void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteDescriptorRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTWriteDescriptorRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); out.append(" data: "); out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); out.append("\n"); - out.append("}"); } void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" enable: "); - out.append(YESNO(this->enable)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTNotifyRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); + dump_field(out, "enable", this->enable); } void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyDataResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTNotifyDataResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); out.append(" data: "); out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); - out.append("}"); } void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { out.append("SubscribeBluetoothConnectionsFreeRequest {}"); } void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothConnectionsFreeResponse {\n"); - out.append(" free: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->free); - out.append(buffer); - out.append("\n"); - - out.append(" limit: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->limit); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); + dump_field(out, "free", this->free); + dump_field(out, "limit", this->limit); for (const auto &it : this->allocated) { - out.append(" allocated: "); - snprintf(buffer, sizeof(buffer), "%llu", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "allocated", it, 4); } - out.append("}"); } void BluetoothGATTErrorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTErrorResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTErrorResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); + dump_field(out, "error", this->error); } void BluetoothGATTWriteResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTWriteResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); } void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTNotifyResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); } void BluetoothDevicePairingResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDevicePairingResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" paired: "); - out.append(YESNO(this->paired)); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDevicePairingResponse"); + dump_field(out, "address", this->address); + dump_field(out, "paired", this->paired); + dump_field(out, "error", this->error); } void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceUnpairingResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDeviceUnpairingResponse"); + dump_field(out, "address", this->address); + dump_field(out, "success", this->success); + dump_field(out, "error", this->error); } void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); } void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceClearCacheResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); + dump_field(out, "address", this->address); + dump_field(out, "success", this->success); + dump_field(out, "error", this->error); } void BluetoothScannerStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothScannerStateResponse {\n"); - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); + dump_field(out, "state", static_cast(this->state)); + dump_field(out, "mode", static_cast(this->mode)); } void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothScannerSetModeRequest {\n"); - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); + dump_field(out, "mode", static_cast(this->mode)); } #endif #ifdef USE_VOICE_ASSISTANT void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeVoiceAssistantRequest {\n"); - out.append(" subscribe: "); - out.append(YESNO(this->subscribe)); - out.append("\n"); - - out.append(" flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SubscribeVoiceAssistantRequest"); + dump_field(out, "subscribe", this->subscribe); + dump_field(out, "flags", this->flags); } void VoiceAssistantAudioSettings::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAudioSettings {\n"); - out.append(" noise_suppression_level: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->noise_suppression_level); - out.append(buffer); - out.append("\n"); - - out.append(" auto_gain: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->auto_gain); - out.append(buffer); - out.append("\n"); - - out.append(" volume_multiplier: "); - snprintf(buffer, sizeof(buffer), "%g", this->volume_multiplier); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantAudioSettings"); + dump_field(out, "noise_suppression_level", this->noise_suppression_level); + dump_field(out, "auto_gain", this->auto_gain); + dump_field(out, "volume_multiplier", this->volume_multiplier); } void VoiceAssistantRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantRequest {\n"); - out.append(" start: "); - out.append(YESNO(this->start)); - out.append("\n"); - - out.append(" conversation_id: "); - append_quoted_string(out, this->conversation_id_ref_); - out.append("\n"); - - out.append(" flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "VoiceAssistantRequest"); + dump_field(out, "start", this->start); + dump_field(out, "conversation_id", this->conversation_id_ref_); + dump_field(out, "flags", this->flags); out.append(" audio_settings: "); this->audio_settings.dump_to(out); out.append("\n"); - - out.append(" wake_word_phrase: "); - append_quoted_string(out, this->wake_word_phrase_ref_); - out.append("\n"); - out.append("}"); + dump_field(out, "wake_word_phrase", this->wake_word_phrase_ref_); } void VoiceAssistantResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantResponse {\n"); - out.append(" port: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->port); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - out.append(YESNO(this->error)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantResponse"); + dump_field(out, "port", this->port); + dump_field(out, "error", this->error); } void VoiceAssistantEventData::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantEventData {\n"); - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" value: "); - out.append("'").append(this->value).append("'"); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantEventData"); + dump_field(out, "name", this->name); + dump_field(out, "value", this->value); } void VoiceAssistantEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantEventResponse {\n"); - out.append(" event_type: "); - out.append(proto_enum_to_string(this->event_type)); - out.append("\n"); - + MessageDumpHelper helper(out, "VoiceAssistantEventResponse"); + dump_field(out, "event_type", static_cast(this->event_type)); for (const auto &it : this->data) { out.append(" data: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void VoiceAssistantAudio::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAudio {\n"); + MessageDumpHelper helper(out, "VoiceAssistantAudio"); out.append(" data: "); if (this->data_ptr_ != nullptr) { out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); @@ -3498,938 +1729,341 @@ void VoiceAssistantAudio::dump_to(std::string &out) const { out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); } out.append("\n"); - - out.append(" end: "); - out.append(YESNO(this->end)); - out.append("\n"); - out.append("}"); + dump_field(out, "end", this->end); } void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantTimerEventResponse {\n"); - out.append(" event_type: "); - out.append(proto_enum_to_string(this->event_type)); - out.append("\n"); - - out.append(" timer_id: "); - out.append("'").append(this->timer_id).append("'"); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" total_seconds: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->total_seconds); - out.append(buffer); - out.append("\n"); - - out.append(" seconds_left: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->seconds_left); - out.append(buffer); - out.append("\n"); - - out.append(" is_active: "); - out.append(YESNO(this->is_active)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantTimerEventResponse"); + dump_field(out, "event_type", static_cast(this->event_type)); + dump_field(out, "timer_id", this->timer_id); + dump_field(out, "name", this->name); + dump_field(out, "total_seconds", this->total_seconds); + dump_field(out, "seconds_left", this->seconds_left); + dump_field(out, "is_active", this->is_active); } void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAnnounceRequest {\n"); - out.append(" media_id: "); - out.append("'").append(this->media_id).append("'"); - out.append("\n"); - - out.append(" text: "); - out.append("'").append(this->text).append("'"); - out.append("\n"); - - out.append(" preannounce_media_id: "); - out.append("'").append(this->preannounce_media_id).append("'"); - out.append("\n"); - - out.append(" start_conversation: "); - out.append(YESNO(this->start_conversation)); - out.append("\n"); - out.append("}"); -} -void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAnnounceFinished {\n"); - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantAnnounceRequest"); + dump_field(out, "media_id", this->media_id); + dump_field(out, "text", this->text); + dump_field(out, "preannounce_media_id", this->preannounce_media_id); + dump_field(out, "start_conversation", this->start_conversation); } +void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { dump_field(out, "success", this->success); } void VoiceAssistantWakeWord::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantWakeWord {\n"); - out.append(" id: "); - append_quoted_string(out, this->id_ref_); - out.append("\n"); - - out.append(" wake_word: "); - append_quoted_string(out, this->wake_word_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "VoiceAssistantWakeWord"); + dump_field(out, "id", this->id_ref_); + dump_field(out, "wake_word", this->wake_word_ref_); for (const auto &it : this->trained_languages) { - out.append(" trained_languages: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "trained_languages", it, 4); } - out.append("}"); } void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { out.append("VoiceAssistantConfigurationRequest {}"); } void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantConfigurationResponse {\n"); + MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse"); for (const auto &it : this->available_wake_words) { out.append(" available_wake_words: "); it.dump_to(out); out.append("\n"); } - for (const auto &it : this->active_wake_words) { - out.append(" active_wake_words: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "active_wake_words", it, 4); } - - out.append(" max_active_wake_words: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_active_wake_words); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "max_active_wake_words", this->max_active_wake_words); } void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantSetConfiguration {\n"); + MessageDumpHelper helper(out, "VoiceAssistantSetConfiguration"); for (const auto &it : this->active_wake_words) { - out.append(" active_wake_words: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "active_wake_words", it, 4); } - out.append("}"); } #endif #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesAlarmControlPanelResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesAlarmControlPanelResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supported_features: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->supported_features); - out.append(buffer); - out.append("\n"); - - out.append(" requires_code: "); - out.append(YESNO(this->requires_code)); - out.append("\n"); - - out.append(" requires_code_to_arm: "); - out.append(YESNO(this->requires_code_to_arm)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "supported_features", this->supported_features); + dump_field(out, "requires_code", this->requires_code); + dump_field(out, "requires_code_to_arm", this->requires_code_to_arm); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void AlarmControlPanelStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("AlarmControlPanelStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - + MessageDumpHelper helper(out, "AlarmControlPanelStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", static_cast(this->state)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("AlarmControlPanelCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" code: "); - out.append("'").append(this->code).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "AlarmControlPanelCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "command", static_cast(this->command)); + dump_field(out, "code", this->code); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_TEXT void ListEntitiesTextResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTextResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesTextResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" min_length: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->min_length); - out.append(buffer); - out.append("\n"); - - out.append(" max_length: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_length); - out.append(buffer); - out.append("\n"); - - out.append(" pattern: "); - append_quoted_string(out, this->pattern_ref_); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "min_length", this->min_length); + dump_field(out, "max_length", this->max_length); + dump_field(out, "pattern", this->pattern_ref_); + dump_field(out, "mode", static_cast(this->mode)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void TextStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - append_quoted_string(out, this->state_ref_); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - + MessageDumpHelper helper(out, "TextStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state_ref_); + dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void TextCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "TextCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesDateResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesDateResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void DateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" year: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); - out.append(buffer); - out.append("\n"); - - out.append(" month: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); - out.append(buffer); - out.append("\n"); - - out.append(" day: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "DateStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "missing_state", this->missing_state); + dump_field(out, "year", this->year); + dump_field(out, "month", this->month); + dump_field(out, "day", this->day); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void DateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" year: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); - out.append(buffer); - out.append("\n"); - - out.append(" month: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); - out.append(buffer); - out.append("\n"); - - out.append(" day: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "DateCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "year", this->year); + dump_field(out, "month", this->month); + dump_field(out, "day", this->day); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTimeResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesTimeResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void TimeStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TimeStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" hour: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); - out.append(buffer); - out.append("\n"); - - out.append(" minute: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); - out.append(buffer); - out.append("\n"); - - out.append(" second: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "TimeStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "missing_state", this->missing_state); + dump_field(out, "hour", this->hour); + dump_field(out, "minute", this->minute); + dump_field(out, "second", this->second); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void TimeCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TimeCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" hour: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); - out.append(buffer); - out.append("\n"); - - out.append(" minute: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); - out.append(buffer); - out.append("\n"); - - out.append(" second: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "TimeCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "hour", this->hour); + dump_field(out, "minute", this->minute); + dump_field(out, "second", this->second); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_EVENT void ListEntitiesEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesEventResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesEventResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); for (const auto &it : this->event_types) { - out.append(" event_types: "); - append_quoted_string(out, StringRef(it)); - out.append("\n"); + dump_field(out, "event_types", it, 4); } - #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void EventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("EventResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" event_type: "); - append_quoted_string(out, this->event_type_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "EventResponse"); + dump_field(out, "key", this->key); + dump_field(out, "event_type", this->event_type_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_VALVE void ListEntitiesValveResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesValveResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesValveResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" supports_position: "); - out.append(YESNO(this->supports_position)); - out.append("\n"); - - out.append(" supports_stop: "); - out.append(YESNO(this->supports_stop)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "assumed_state", this->assumed_state); + dump_field(out, "supports_position", this->supports_position); + dump_field(out, "supports_stop", this->supports_stop); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void ValveStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ValveStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" position: "); - snprintf(buffer, sizeof(buffer), "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" current_operation: "); - out.append(proto_enum_to_string(this->current_operation)); - out.append("\n"); - + MessageDumpHelper helper(out, "ValveStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "position", this->position); + dump_field(out, "current_operation", static_cast(this->current_operation)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void ValveCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ValveCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_position: "); - out.append(YESNO(this->has_position)); - out.append("\n"); - - out.append(" position: "); - snprintf(buffer, sizeof(buffer), "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" stop: "); - out.append(YESNO(this->stop)); - out.append("\n"); - + MessageDumpHelper helper(out, "ValveCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_position", this->has_position); + dump_field(out, "position", this->position); + dump_field(out, "stop", this->stop); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesDateTimeResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesDateTimeResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void DateTimeStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateTimeStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" epoch_seconds: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "DateTimeStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "missing_state", this->missing_state); + dump_field(out, "epoch_seconds", this->epoch_seconds); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void DateTimeCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateTimeCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" epoch_seconds: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "DateTimeCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "epoch_seconds", this->epoch_seconds); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif #ifdef USE_UPDATE void ListEntitiesUpdateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesUpdateResponse {\n"); - out.append(" object_id: "); - append_quoted_string(out, this->object_id_ref_); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - append_quoted_string(out, this->name_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesUpdateResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); #ifdef USE_ENTITY_ICON - out.append(" icon: "); - append_quoted_string(out, this->icon_ref_); - out.append("\n"); - + dump_field(out, "icon", this->icon_ref_); #endif - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - append_quoted_string(out, this->device_class_ref_); - out.append("\n"); - + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void UpdateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("UpdateStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" in_progress: "); - out.append(YESNO(this->in_progress)); - out.append("\n"); - - out.append(" has_progress: "); - out.append(YESNO(this->has_progress)); - out.append("\n"); - - out.append(" progress: "); - snprintf(buffer, sizeof(buffer), "%g", this->progress); - out.append(buffer); - out.append("\n"); - - out.append(" current_version: "); - append_quoted_string(out, this->current_version_ref_); - out.append("\n"); - - out.append(" latest_version: "); - append_quoted_string(out, this->latest_version_ref_); - out.append("\n"); - - out.append(" title: "); - append_quoted_string(out, this->title_ref_); - out.append("\n"); - - out.append(" release_summary: "); - append_quoted_string(out, this->release_summary_ref_); - out.append("\n"); - - out.append(" release_url: "); - append_quoted_string(out, this->release_url_ref_); - out.append("\n"); - + MessageDumpHelper helper(out, "UpdateStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "missing_state", this->missing_state); + dump_field(out, "in_progress", this->in_progress); + dump_field(out, "has_progress", this->has_progress); + dump_field(out, "progress", this->progress); + dump_field(out, "current_version", this->current_version_ref_); + dump_field(out, "latest_version", this->latest_version_ref_); + dump_field(out, "title", this->title_ref_); + dump_field(out, "release_summary", this->release_summary_ref_); + dump_field(out, "release_url", this->release_url_ref_); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } void UpdateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("UpdateCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - + MessageDumpHelper helper(out, "UpdateCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "command", static_cast(this->command)); #ifdef USE_DEVICES - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - + dump_field(out, "device_id", this->device_id); #endif - out.append("}"); } #endif diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index fb5db70ae8..635291371e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -224,12 +224,26 @@ class TypeInfo(ABC): encode_func = None + @classmethod + def can_use_dump_field(cls) -> bool: + """Whether this type can use the dump_field helper functions. + + Returns True for simple types that have dump_field overloads. + Complex types like messages and bytes should return False. + """ + return True + + def dump_field_value(self, value: str) -> str: + """Get the value expression to pass to dump_field. + + Most types just pass the value directly, but some (like enums) need a cast. + """ + return value + @property def dump_content(self) -> str: - o = f'out.append(" {self.name}: ");\n' - o += self.dump(f"this->{self.field_name}") + "\n" - o += 'out.append("\\n");\n' - return o + # Default implementation - subclasses can override if they need special handling + return f'dump_field(out, "{self.name}", {self.dump_field_value(f"this->{self.field_name}")});' @abstractmethod def dump(self, name: str) -> str: @@ -593,6 +607,22 @@ class StringType(TypeInfo): f"}}" ) + @property + def dump_content(self) -> str: + # For SOURCE_CLIENT only, use std::string + if not self._needs_encode: + return f'dump_field(out, "{self.name}", this->{self.field_name});' + + # For SOURCE_SERVER, use StringRef with _ref_ suffix + if not self._needs_decode: + return f'dump_field(out, "{self.name}", this->{self.field_name}_ref_);' + + # For SOURCE_BOTH, we need custom logic + o = f'out.append(" {self.name}: ");\n' + o += self.dump(f"this->{self.field_name}") + "\n" + o += 'out.append("\\n");' + return o + def get_size_calculation(self, name: str, force: bool = False) -> str: # For SOURCE_CLIENT only messages, use the string field directly if not self._needs_encode: @@ -615,6 +645,10 @@ class StringType(TypeInfo): @register_type(11) class MessageType(TypeInfo): + @classmethod + def can_use_dump_field(cls) -> bool: + return False + @property def cpp_type(self) -> str: return self._field.type_name[1:] @@ -651,6 +685,13 @@ class MessageType(TypeInfo): o = f"{name}.dump_to(out);" return o + @property + def dump_content(self) -> str: + o = f'out.append(" {self.name}: ");\n' + o += f"this->{self.field_name}.dump_to(out);\n" + o += 'out.append("\\n");' + return o + def get_size_calculation(self, name: str, force: bool = False) -> str: return self._get_simple_size_calculation(name, force, "add_message_object") @@ -664,6 +705,10 @@ class MessageType(TypeInfo): @register_type(12) class BytesType(TypeInfo): + @classmethod + def can_use_dump_field(cls) -> bool: + return False + cpp_type = "std::string" default_value = "" reference_type = "std::string &" @@ -719,6 +764,13 @@ class BytesType(TypeInfo): f" }}" ) + @property + def dump_content(self) -> str: + o = f'out.append(" {self.name}: ");\n' + o += self.dump(f"this->{self.field_name}") + "\n" + o += 'out.append("\\n");' + return o + def get_size_calculation(self, name: str, force: bool = False) -> str: return f"ProtoSize::add_bytes_field(total_size, {self.calculate_field_id_size()}, this->{self.field_name}_len_);" @@ -729,6 +781,10 @@ class BytesType(TypeInfo): class FixedArrayBytesType(TypeInfo): """Special type for fixed-size byte arrays.""" + @classmethod + def can_use_dump_field(cls) -> bool: + return False + def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None: super().__init__(field) self.array_size = size @@ -778,6 +834,13 @@ class FixedArrayBytesType(TypeInfo): o = f"out.append(format_hex_pretty({name}, {name}_len));" return o + @property + def dump_content(self) -> str: + o = f'out.append(" {self.name}: ");\n' + o += f"out.append(format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len));\n" + o += 'out.append("\\n");' + return o + def get_size_calculation(self, name: str, force: bool = False) -> str: # Use the actual length stored in the _len field length_field = f"this->{self.field_name}_len" @@ -850,6 +913,10 @@ class EnumType(TypeInfo): o = f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" return o + def dump_field_value(self, value: str) -> str: + # Enums need explicit cast for the template + return f"static_cast<{self.cpp_type}>({value})" + def get_size_calculation(self, name: str, force: bool = False) -> str: return self._get_simple_size_calculation( name, force, "add_enum_field", f"static_cast({name})" @@ -947,6 +1014,27 @@ class SInt64Type(TypeInfo): return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint +def _generate_array_dump_content( + ti, field_name: str, name: str, is_bool: bool = False +) -> str: + """Generate dump content for array types (repeated or fixed array). + + Shared helper to avoid code duplication between RepeatedTypeInfo and FixedArrayRepeatedType. + """ + o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n" + # Check if underlying type can use dump_field + if type(ti).can_use_dump_field(): + # For types that have dump_field overloads, use them with extra indent + o += f' dump_field(out, "{name}", {ti.dump_field_value("it")}, 4);\n' + else: + # For complex types (messages, bytes), use the old pattern + o += f' out.append(" {name}: ");\n' + o += indent(ti.dump("it")) + "\n" + o += ' out.append("\\n");\n' + o += "}" + return o + + class FixedArrayRepeatedType(TypeInfo): """Special type for fixed-size repeated fields using std::array. @@ -1013,12 +1101,9 @@ class FixedArrayRepeatedType(TypeInfo): @property def dump_content(self) -> str: - o = f"for (const auto &it : this->{self.field_name}) {{\n" - o += f' out.append(" {self.name}: ");\n' - o += indent(self._ti.dump("it")) + "\n" - o += ' out.append("\\n");\n' - o += "}\n" - return o + return _generate_array_dump_content( + self._ti, f"this->{self.field_name}", self.name, is_bool=False + ) def dump(self, name: str) -> str: # This is used when dumping the array itself (not its elements) @@ -1144,12 +1229,9 @@ class RepeatedTypeInfo(TypeInfo): @property def dump_content(self) -> str: - o = f"for (const auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" - o += f' out.append(" {self.name}: ");\n' - o += indent(self._ti.dump("it")) + "\n" - o += ' out.append("\\n");\n' - o += "}\n" - return o + return _generate_array_dump_content( + self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool + ) def dump(self, _: str): pass @@ -1644,10 +1726,8 @@ def build_message_type( dump_impl += f" {dump[0]} " else: dump_impl += "\n" - dump_impl += " __attribute__((unused)) char buffer[64];\n" - dump_impl += f' out.append("{desc.name} {{\\n");\n' + dump_impl += f' MessageDumpHelper helper(out, "{desc.name}");\n' dump_impl += indent("\n".join(dump)) + "\n" - dump_impl += ' out.append("}");\n' else: o2 = f'out.append("{desc.name} {{}}");' if len(dump_impl) + len(o2) + 3 < 120: @@ -2004,6 +2084,83 @@ static inline void append_quoted_string(std::string &out, const StringRef &ref) out.append("'"); } +// Common helpers for dump_field functions +static inline void append_field_prefix(std::string &out, const char *field_name, int indent) { + out.append(indent, ' ').append(field_name).append(": "); +} + +static inline void append_with_newline(std::string &out, const char *str) { + out.append(str); + out.append("\\n"); +} + +// RAII helper for message dump formatting +class MessageDumpHelper { + public: + MessageDumpHelper(std::string &out, const char *message_name) : out_(out) { + out_.append(message_name); + out_.append(" {\\n"); + } + ~MessageDumpHelper() { out_.append(" }"); } + + private: + std::string &out_; +}; + +// Helper functions to reduce code duplication in dump methods +static void dump_field(std::string &out, const char *field_name, int32_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%" PRId32, value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, uint32_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%" PRIu32, value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, float value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%g", value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%llu", value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, bool value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append(YESNO(value)); + out.append("\\n"); +} + +static void dump_field(std::string &out, const char *field_name, const std::string &value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\\n"); +} + +static void dump_field(std::string &out, const char *field_name, StringRef value, int indent = 2) { + append_field_prefix(out, field_name, indent); + append_quoted_string(out, value); + out.append("\\n"); +} + +template +static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append(proto_enum_to_string(value)); + out.append("\\n"); +} + """ content += "namespace enums {\n\n" From 99850255f008867ca1cbb18a804af755ee129989 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 17:21:35 -1000 Subject: [PATCH 103/125] [api] Use emplace_back for TemplatableKeyValuePair construction in HomeAssistant services (#9804) --- esphome/components/api/homeassistant_service.h | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 4980c0224c..d91c1ab287 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -36,6 +36,9 @@ template class TemplatableStringValue : public TemplatableValue class TemplatableKeyValuePair { public: + // Keys are always string literals from YAML dictionary keys (e.g., "code", "event") + // and never templatable values or lambdas. Only the value parameter can be a lambda/template. + // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues. template TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} std::string key; TemplatableStringValue value; @@ -47,14 +50,15 @@ template class HomeAssistantServiceCallAction : public Action void set_service(T service) { this->service_ = service; } - template void add_data(std::string key, T value) { - this->data_.push_back(TemplatableKeyValuePair(key, value)); - } + // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))). + // The value parameter can be a lambda/template, but keys are never templatable. + // Using pass-by-value allows the compiler to optimize for both lvalues and rvalues. + template void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); } template void add_data_template(std::string key, T value) { - this->data_template_.push_back(TemplatableKeyValuePair(key, value)); + this->data_template_.emplace_back(std::move(key), value); } template void add_variable(std::string key, T value) { - this->variables_.push_back(TemplatableKeyValuePair(key, value)); + this->variables_.emplace_back(std::move(key), value); } void play(Ts... x) override { From 544cf9b9c082476aa09cd6539ab13bf624ca0003 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 17:22:42 -1000 Subject: [PATCH 104/125] [core] Fix component state documentation and add state helper method (#9824) --- esphome/core/component.cpp | 28 ++++++++++++---------------- esphome/core/component.h | 11 +++++++---- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a6a59d30d5..8dcc4496b1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -151,26 +151,22 @@ void Component::call() { switch (state) { case COMPONENT_STATE_CONSTRUCTION: // State Construction: Call setup and set state to setup - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_SETUP; + this->set_component_state_(COMPONENT_STATE_SETUP); this->call_setup(); break; case COMPONENT_STATE_SETUP: // State setup: Call first loop and set state to loop - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_LOOP; + this->set_component_state_(COMPONENT_STATE_LOOP); this->call_loop(); break; case COMPONENT_STATE_LOOP: // State loop: Call loop this->call_loop(); break; - case COMPONENT_STATE_FAILED: // NOLINT(bugprone-branch-clone) + case COMPONENT_STATE_FAILED: // State failed: Do nothing - break; - case COMPONENT_STATE_LOOP_DONE: // NOLINT(bugprone-branch-clone) + case COMPONENT_STATE_LOOP_DONE: // State loop done: Do nothing, component has finished its work - break; default: break; } @@ -195,25 +191,26 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { } void Component::mark_failed() { ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source()); - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_FAILED; + this->set_component_state_(COMPONENT_STATE_FAILED); this->status_set_error(); // Also remove from loop since failed components shouldn't loop App.disable_component_loop_(this); } +void Component::set_component_state_(uint8_t state) { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= state; +} void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { ESP_LOGVV(TAG, "%s loop disabled", this->get_component_source()); - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_LOOP_DONE; + this->set_component_state_(COMPONENT_STATE_LOOP_DONE); App.disable_component_loop_(this); } } void Component::enable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { ESP_LOGVV(TAG, "%s loop enabled", this->get_component_source()); - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_LOOP; + this->set_component_state_(COMPONENT_STATE_LOOP); App.enable_component_loop_(this); } } @@ -233,8 +230,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source()); - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + this->set_component_state_(COMPONENT_STATE_CONSTRUCTION); // Clear error status when resetting this->status_clear_error(); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 3734473a02..5f17c1c22a 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -236,6 +236,9 @@ class Component { virtual void call_setup(); virtual void call_dump_config(); + /// Helper to set component state (clears state bits and sets new state) + void set_component_state_(uint8_t state); + /** Set an interval function with a unique name. Empty name means no cancelling possible. * * This will call f every interval ms. Can be cancelled via CancelInterval(). @@ -405,10 +408,10 @@ class Component { const char *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) /// State of this component - each bit has a purpose: - /// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED) - /// Bit 2: STATUS_LED_WARNING - /// Bit 3: STATUS_LED_ERROR - /// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free) + /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) + /// Bit 3: STATUS_LED_WARNING + /// Bit 4: STATUS_LED_ERROR + /// Bits 5-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; From ec2e0c50f1cf115d7941846d1c0bb0f54b9879e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 17:23:45 -1000 Subject: [PATCH 105/125] [bluetooth_proxy] [esp32_ble_tracker] [esp32_ble] Use C++17 nested namespace syntax (#9825) --- esphome/components/bluetooth_proxy/bluetooth_connection.cpp | 6 ++---- esphome/components/bluetooth_proxy/bluetooth_connection.h | 6 ++---- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 6 ++---- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 ++---- esphome/components/esp32_ble/ble.cpp | 6 ++---- esphome/components/esp32_ble/ble.h | 6 ++---- esphome/components/esp32_ble/ble_advertising.cpp | 6 ++---- esphome/components/esp32_ble/ble_advertising.h | 6 ++---- esphome/components/esp32_ble/ble_event.h | 6 ++---- esphome/components/esp32_ble/ble_scan_result.h | 6 ++---- esphome/components/esp32_ble/ble_uuid.cpp | 6 ++---- esphome/components/esp32_ble/ble_uuid.h | 6 ++---- esphome/components/esp32_ble_tracker/automation.h | 6 ++---- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 6 ++---- esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 6 ++---- 15 files changed, 30 insertions(+), 60 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 616dba891a..4b84257e27 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -8,8 +8,7 @@ #include "bluetooth_proxy.h" -namespace esphome { -namespace bluetooth_proxy { +namespace esphome::bluetooth_proxy { static const char *const TAG = "bluetooth_proxy.connection"; @@ -422,7 +421,6 @@ esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisemen return this->proxy_->get_advertisement_parser_type(); } -} // namespace bluetooth_proxy -} // namespace esphome +} // namespace esphome::bluetooth_proxy #endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 2673238fba..3fed9d531f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -4,8 +4,7 @@ #include "esphome/components/esp32_ble_client/ble_client_base.h" -namespace esphome { -namespace bluetooth_proxy { +namespace esphome::bluetooth_proxy { class BluetoothProxy; @@ -43,7 +42,6 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { // 1 byte used, 1 byte padding }; -} // namespace bluetooth_proxy -} // namespace esphome +} // namespace esphome::bluetooth_proxy #endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 8a1a2bff6a..de5508c777 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace bluetooth_proxy { +namespace esphome::bluetooth_proxy { static const char *const TAG = "bluetooth_proxy"; @@ -502,7 +501,6 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace bluetooth_proxy -} // namespace esphome +} // namespace esphome::bluetooth_proxy #endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index b3d9044a2c..d249515fdf 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -18,8 +18,7 @@ #include #include -namespace esphome { -namespace bluetooth_proxy { +namespace esphome::bluetooth_proxy { static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; static const int DONE_SENDING_SERVICES = -2; @@ -158,7 +157,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace bluetooth_proxy -} // namespace esphome +} // namespace esphome::bluetooth_proxy #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8b0cf4da98..35c48a711a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -19,8 +19,7 @@ #include #endif -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; @@ -538,7 +537,6 @@ uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2c5697df82..543b2f26a3 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -21,8 +21,7 @@ #include #include -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { // Maximum number of BLE scan results to buffer // Sized to handle bursts of advertisements while allowing for processing delays @@ -191,7 +190,6 @@ template class BLEDisableAction : public Action { void play(Ts... x) override { global_ble->disable(); } }; -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 8d43b5af33..6a0d677aa7 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -8,8 +8,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble.advertising"; @@ -160,7 +159,6 @@ void BLEAdvertising::register_raw_advertisement_callback(std::functionraw_advertisements_callbacks_.push_back(std::move(callback)); } -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 0b2142115d..2254251486 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -10,8 +10,7 @@ #include #include -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { using raw_adv_data_t = struct { uint8_t *data; @@ -55,7 +54,6 @@ class BLEAdvertising { int8_t current_adv_index_{-1}; // -1 means standard scan response }; -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 9268c710f3..884fc9ba65 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -11,8 +11,7 @@ #include "ble_scan_result.h" -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { // Compile-time verification that ESP-IDF scan complete events only contain a status field // This ensures our reinterpret_cast in ble.cpp is safe @@ -395,7 +394,6 @@ static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScan // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_scan_result.h b/esphome/components/esp32_ble/ble_scan_result.h index 49b0d5523d..980b39b0b2 100644 --- a/esphome/components/esp32_ble/ble_scan_result.h +++ b/esphome/components/esp32_ble/ble_scan_result.h @@ -4,8 +4,7 @@ #include -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { // Structure for BLE scan results - only fields we actually use struct __attribute__((packed)) BLEScanResult { @@ -18,7 +17,6 @@ struct __attribute__((packed)) BLEScanResult { uint8_t search_evt; }; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index aa1edd96b2..fc6981acd3 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -7,8 +7,7 @@ #include #include "esphome/core/log.h" -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; @@ -189,7 +188,6 @@ std::string ESPBTUUID::to_string() const { return ""; } -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 06f84d4da7..150ca359d3 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -8,8 +8,7 @@ #include #include -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { class ESPBTUUID { public: @@ -41,7 +40,6 @@ class ESPBTUUID { esp_bt_uuid_t uuid_; }; -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index ef677922e3..c0e6eee138 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_tracker { +namespace esphome::esp32_ble_tracker { #ifdef USE_ESP32_BLE_DEVICE class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: @@ -108,7 +107,6 @@ template class ESP32BLEStopScanAction : public Action, pu void play(Ts... x) override { this->parent_->stop_scan(); } }; -} // namespace esp32_ble_tracker -} // namespace esphome +} // namespace esphome::esp32_ble_tracker #endif diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 96003073d7..e0029ad15b 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -35,8 +35,7 @@ // bt_trace.h #undef TAG -namespace esphome { -namespace esp32_ble_tracker { +namespace esphome::esp32_ble_tracker { static const char *const TAG = "esp32_ble_tracker"; @@ -882,7 +881,6 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { } #endif // USE_ESP32_BLE_DEVICE -} // namespace esp32_ble_tracker -} // namespace esphome +} // namespace esphome::esp32_ble_tracker #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index e10f4551e8..e1119c0e18 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -22,8 +22,7 @@ #include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble_uuid.h" -namespace esphome { -namespace esp32_ble_tracker { +namespace esphome::esp32_ble_tracker { using namespace esp32_ble; @@ -321,7 +320,6 @@ class ESP32BLETracker : public Component, // NOLINTNEXTLINE extern ESP32BLETracker *global_esp32_ble_tracker; -} // namespace esp32_ble_tracker -} // namespace esphome +} // namespace esphome::esp32_ble_tracker #endif From 705ea4ebaa2924e08f13de680f74790b2843aed0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 23 Jul 2025 23:50:50 -0500 Subject: [PATCH 106/125] [ld2410] Use ``Deduplicator`` for sensors (#9584) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ld2410/__init__.py | 1 + esphome/components/ld2410/ld2410.cpp | 136 ++++++++++---------------- esphome/components/ld2410/ld2410.h | 29 +++--- esphome/components/ld24xx/__init__.py | 1 + esphome/components/ld24xx/ld24xx.h | 65 ++++++++++++ 6 files changed, 137 insertions(+), 96 deletions(-) create mode 100644 esphome/components/ld24xx/__init__.py create mode 100644 esphome/components/ld24xx/ld24xx.h diff --git a/CODEOWNERS b/CODEOWNERS index cd11c7796f..f85993bd87 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,6 +246,7 @@ esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2420/* @descipher esphome/components/ld2450/* @hareeshmu +esphome/components/ld24xx/* @kbx81 esphome/components/ledc/* @OttoWinter esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2 diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index c58b9a4017..4918190179 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -5,6 +5,7 @@ from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_THROTTLE, CONF_TIMEOUT +AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] CODEOWNERS = ["@sebcaps", "@regevbr"] MULTI_CONF = True diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 8f1fb0ee9d..bb6d63a963 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -1,6 +1,5 @@ #include "ld2410.h" -#include #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif @@ -10,10 +9,6 @@ #include "esphome/core/application.h" -#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) -#define highbyte(val) (uint8_t)((val) >> 8) -#define lowbyte(val) (uint8_t)((val) &0xff) - namespace esphome { namespace ld2410 { @@ -165,6 +160,9 @@ static constexpr uint8_t CMD_BLUETOOTH = 0xA4; static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00; static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01; static constexpr uint8_t CMD_DURATION_VALUE = 0x02; +// Bitmasks for target states +static constexpr uint8_t MOVE_BITMASK = 0x01; +static constexpr uint8_t STILL_BITMASK = 0x02; // Header & Footer size static constexpr uint8_t HEADER_FOOTER_SIZE = 4; // Command Header & Footer @@ -202,17 +200,17 @@ void LD2410Component::dump_config() { #endif #ifdef USE_SENSOR ESP_LOGCONFIG(TAG, "Sensors:"); - LOG_SENSOR(" ", "Light", this->light_sensor_); - LOG_SENSOR(" ", "DetectionDistance", this->detection_distance_sensor_); - LOG_SENSOR(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); - LOG_SENSOR(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); - LOG_SENSOR(" ", "StillTargetDistance", this->still_target_distance_sensor_); - LOG_SENSOR(" ", "StillTargetEnergy", this->still_target_energy_sensor_); - for (sensor::Sensor *s : this->gate_move_sensors_) { - LOG_SENSOR(" ", "GateMove", s); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Light", this->light_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "DetectionDistance", this->detection_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetDistance", this->still_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetEnergy", this->still_target_energy_sensor_); + for (auto &s : this->gate_move_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateMove", s); } - for (sensor::Sensor *s : this->gate_still_sensors_) { - LOG_SENSOR(" ", "GateStill", s); + for (auto &s : this->gate_still_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateStill", s); } #endif #ifdef USE_TEXT_SENSOR @@ -304,8 +302,10 @@ void LD2410Component::send_command_(uint8_t command, const uint8_t *command_valu } // frame footer bytes this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); - // FIXME to remove - delay(50); // NOLINT + + if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) { + delay(50); // NOLINT + } } void LD2410Component::handle_periodic_data_() { @@ -348,10 +348,10 @@ void LD2410Component::handle_periodic_data_() { this->target_binary_sensor_->publish_state(target_state != 0x00); } if (this->moving_target_binary_sensor_ != nullptr) { - this->moving_target_binary_sensor_->publish_state(CHECK_BIT(target_state, 0)); + this->moving_target_binary_sensor_->publish_state(target_state & MOVE_BITMASK); } if (this->still_target_binary_sensor_ != nullptr) { - this->still_target_binary_sensor_->publish_state(CHECK_BIT(target_state, 1)); + this->still_target_binary_sensor_->publish_state(target_state & STILL_BITMASK); } #endif /* @@ -362,89 +362,51 @@ void LD2410Component::handle_periodic_data_() { Detect distance: 16~17th bytes */ #ifdef USE_SENSOR - if (this->moving_target_distance_sensor_ != nullptr) { - int new_moving_target_distance = - ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); - if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) - this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); - } - if (this->moving_target_energy_sensor_ != nullptr) { - int new_moving_target_energy = this->buffer_data_[MOVING_ENERGY]; - if (this->moving_target_energy_sensor_->get_state() != new_moving_target_energy) - this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); - } - if (this->still_target_distance_sensor_ != nullptr) { - int new_still_target_distance = - ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); - if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) - this->still_target_distance_sensor_->publish_state(new_still_target_distance); - } - if (this->still_target_energy_sensor_ != nullptr) { - int new_still_target_energy = this->buffer_data_[STILL_ENERGY]; - if (this->still_target_energy_sensor_->get_state() != new_still_target_energy) - this->still_target_energy_sensor_->publish_state(new_still_target_energy); - } - if (this->detection_distance_sensor_ != nullptr) { - int new_detect_distance = - ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH]); - if (this->detection_distance_sensor_->get_state() != new_detect_distance) - this->detection_distance_sensor_->publish_state(new_detect_distance); - } + SAFE_PUBLISH_SENSOR( + this->moving_target_distance_sensor_, + ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) + SAFE_PUBLISH_SENSOR( + this->still_target_distance_sensor_, + ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])); + SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]); + SAFE_PUBLISH_SENSOR( + this->detection_distance_sensor_, + ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH])); + if (engineering_mode) { /* Moving distance range: 18th byte Still distance range: 19th byte Moving energy: 20~28th bytes */ - for (std::vector::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { - sensor::Sensor *s = this->gate_move_sensors_[i]; - if (s != nullptr) { - s->publish_state(this->buffer_data_[MOVING_SENSOR_START + i]); - } + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) } /* Still energy: 29~37th bytes */ - for (std::vector::size_type i = 0; i != this->gate_still_sensors_.size(); i++) { - sensor::Sensor *s = this->gate_still_sensors_[i]; - if (s != nullptr) { - s->publish_state(this->buffer_data_[STILL_SENSOR_START + i]); - } + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) } /* Light sensor: 38th bytes */ - if (this->light_sensor_ != nullptr) { - int new_light_sensor = this->buffer_data_[LIGHT_SENSOR]; - if (this->light_sensor_->get_state() != new_light_sensor) { - this->light_sensor_->publish_state(new_light_sensor); - } - } + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) } else { - for (auto *s : this->gate_move_sensors_) { - if (s != nullptr && !std::isnan(s->get_state())) { - s->publish_state(NAN); - } + for (auto &gate_move_sensor : this->gate_move_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) } - for (auto *s : this->gate_still_sensors_) { - if (s != nullptr && !std::isnan(s->get_state())) { - s->publish_state(NAN); - } - } - if (this->light_sensor_ != nullptr && !std::isnan(this->light_sensor_->get_state())) { - this->light_sensor_->publish_state(NAN); + for (auto &gate_still_sensor : this->gate_still_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor) } + SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_) } #endif #ifdef USE_BINARY_SENSOR - if (engineering_mode) { - if (this->out_pin_presence_status_binary_sensor_ != nullptr) { - this->out_pin_presence_status_binary_sensor_->publish_state(this->buffer_data_[OUT_PIN_SENSOR] == 0x01); - } - } else { - if (this->out_pin_presence_status_binary_sensor_ != nullptr) { - this->out_pin_presence_status_binary_sensor_->publish_state(false); - } + if (this->out_pin_presence_status_binary_sensor_ != nullptr) { + this->out_pin_presence_status_binary_sensor_->publish_state( + engineering_mode ? this->buffer_data_[OUT_PIN_SENSOR] == 0x01 : false); } #endif } @@ -824,8 +786,14 @@ void LD2410Component::set_light_out_control() { } #ifdef USE_SENSOR -void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; } -void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; } +// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. +void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_move_sensors_[gate] = new SensorWithDedup(s); +} + +void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_still_sensors_[gate] = new SensorWithDedup(s); +} #endif } // namespace ld2410 diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 8bd1dbcb5a..e9225ccfe4 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -22,15 +22,20 @@ #ifdef USE_TEXT_SENSOR #include "esphome/components/text_sensor/text_sensor.h" #endif +#include "esphome/components/ld24xx/ld24xx.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace ld2410 { -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +using namespace ld24xx; + +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR @@ -40,12 +45,12 @@ class LD2410Component : public Component, public uart::UARTDevice { SUB_BINARY_SENSOR(target) #endif #ifdef USE_SENSOR - SUB_SENSOR(light) - SUB_SENSOR(detection_distance) - SUB_SENSOR(moving_target_distance) - SUB_SENSOR(moving_target_energy) - SUB_SENSOR(still_target_distance) - SUB_SENSOR(still_target_energy) + SUB_SENSOR_WITH_DEDUP(light, uint8_t) + SUB_SENSOR_WITH_DEDUP(detection_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_energy, uint8_t) + SUB_SENSOR_WITH_DEDUP(still_target_distance, int) + SUB_SENSOR_WITH_DEDUP(still_target_energy, uint8_t) #endif #ifdef USE_TEXT_SENSOR SUB_TEXT_SENSOR(version) @@ -122,12 +127,12 @@ class LD2410Component : public Component, public uart::UARTDevice { uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; bool bluetooth_on_{false}; #ifdef USE_NUMBER - std::vector gate_move_threshold_numbers_ = std::vector(TOTAL_GATES); - std::vector gate_still_threshold_numbers_ = std::vector(TOTAL_GATES); + std::array gate_move_threshold_numbers_{}; + std::array gate_still_threshold_numbers_{}; #endif #ifdef USE_SENSOR - std::vector gate_move_sensors_ = std::vector(TOTAL_GATES); - std::vector gate_still_sensors_ = std::vector(TOTAL_GATES); + std::array *, TOTAL_GATES> gate_move_sensors_{}; + std::array *, TOTAL_GATES> gate_still_sensors_{}; #endif }; diff --git a/esphome/components/ld24xx/__init__.py b/esphome/components/ld24xx/__init__.py new file mode 100644 index 0000000000..516af84856 --- /dev/null +++ b/esphome/components/ld24xx/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@kbx81"] diff --git a/esphome/components/ld24xx/ld24xx.h b/esphome/components/ld24xx/ld24xx.h new file mode 100644 index 0000000000..1cd5e01163 --- /dev/null +++ b/esphome/components/ld24xx/ld24xx.h @@ -0,0 +1,65 @@ +#pragma once + +#include "esphome/core/defines.h" + +#include + +#ifdef USE_SENSOR +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" + +#define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \ + protected: \ + ld24xx::SensorWithDedup *name##_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_sensor(sensor::Sensor *sensor) { \ + this->name##_sensor_ = new ld24xx::SensorWithDedup(sensor); \ + } +#endif + +#define LOG_SENSOR_WITH_DEDUP_SAFE(tag, name, sensor) \ + if ((sensor) != nullptr) { \ + LOG_SENSOR(tag, name, (sensor)->sens); \ + } + +#define SAFE_PUBLISH_SENSOR(sensor, value) \ + if ((sensor) != nullptr) { \ + (sensor)->publish_state_if_not_dup(value); \ + } + +#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) \ + if ((sensor) != nullptr) { \ + (sensor)->publish_state_unknown(); \ + } + +#define highbyte(val) (uint8_t)((val) >> 8) +#define lowbyte(val) (uint8_t)((val) &0xff) + +namespace esphome { +namespace ld24xx { + +#ifdef USE_SENSOR +// Helper class to store a sensor with a deduplicator & publish state only when the value changes +template class SensorWithDedup { + public: + SensorWithDedup(sensor::Sensor *sens) : sens(sens) {} + + void publish_state_if_not_dup(T state) { + if (this->publish_dedup.next(state)) { + this->sens->publish_state(static_cast(state)); + } + } + + void publish_state_unknown() { + if (this->publish_dedup.next_unknown()) { + this->sens->publish_state(NAN); + } + } + + sensor::Sensor *sens; + Deduplicator publish_dedup; +}; +#endif +} // namespace ld24xx +} // namespace esphome From c74f12be989739c6229c27c24a8148913bcf9c62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Jul 2025 21:15:42 -1000 Subject: [PATCH 107/125] [api] Use C++17 nested namespace syntax (#9856) --- esphome/components/api/api_connection.cpp | 6 ++-- esphome/components/api/api_connection.h | 6 ++-- esphome/components/api/api_frame_helper.cpp | 6 ++-- esphome/components/api/api_frame_helper.h | 6 ++-- .../components/api/api_frame_helper_noise.cpp | 6 ++-- .../components/api/api_frame_helper_noise.h | 6 ++-- .../api/api_frame_helper_plaintext.cpp | 6 ++-- .../api/api_frame_helper_plaintext.h | 6 ++-- esphome/components/api/api_noise_context.h | 6 ++-- esphome/components/api/api_pb2.cpp | 6 ++-- esphome/components/api/api_pb2.h | 6 ++-- esphome/components/api/api_pb2_dump.cpp | 6 ++-- esphome/components/api/api_pb2_service.cpp | 6 ++-- esphome/components/api/api_pb2_service.h | 6 ++-- esphome/components/api/api_server.cpp | 6 ++-- esphome/components/api/api_server.h | 6 ++-- esphome/components/api/custom_api_device.h | 6 ++-- .../components/api/homeassistant_service.h | 6 ++-- esphome/components/api/list_entities.cpp | 6 ++-- esphome/components/api/list_entities.h | 6 ++-- esphome/components/api/proto.cpp | 6 ++-- esphome/components/api/proto.h | 6 ++-- esphome/components/api/subscribe_state.cpp | 6 ++-- esphome/components/api/subscribe_state.h | 6 ++-- esphome/components/api/user_services.cpp | 6 ++-- esphome/components/api/user_services.h | 6 ++-- script/api_protobuf/api_protobuf.py | 30 +++++++------------ 27 files changed, 62 insertions(+), 124 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 60dc0a113d..76713c54c8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -31,8 +31,7 @@ #include "esphome/components/voice_assistant/voice_assistant.h" #endif -namespace esphome { -namespace api { +namespace esphome::api { // Read a maximum of 5 messages per loop iteration to prevent starving other components. // This is a balance between API responsiveness and allowing other components to run. @@ -1837,6 +1836,5 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); } -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 5365a48292..6214d5ba82 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -13,8 +13,7 @@ #include #include -namespace esphome { -namespace api { +namespace esphome::api { // Client information structure struct ClientInfo { @@ -723,6 +722,5 @@ class APIConnection : public APIServerConnection { } }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index b1c9478e59..6ca38e80ed 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -9,8 +9,7 @@ #include #include -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api.frame_helper"; @@ -247,6 +246,5 @@ APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { return APIError::OK; } -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 231a3366ce..76dfe1366c 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -12,8 +12,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { // uncomment to log raw packets //#define HELPER_LOG_PACKETS @@ -184,7 +183,6 @@ class APIFrameHelper { APIError handle_socket_read_result_(ssize_t received); }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // USE_API diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index dcb9de9c93..35d1715931 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -10,8 +10,7 @@ #include #include -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api.noise"; static const char *const PROLOGUE_INIT = "NoiseAPIInit"; @@ -579,7 +578,6 @@ void noise_rand_bytes(void *output, size_t len) { } } -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // USE_API_NOISE #endif // USE_API diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index ed5141d625..e82e5daadb 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -5,8 +5,7 @@ #include "noise/protocol.h" #include "api_noise_context.h" -namespace esphome { -namespace api { +namespace esphome::api { class APINoiseFrameHelper : public APIFrameHelper { public: @@ -64,7 +63,6 @@ class APINoiseFrameHelper : public APIFrameHelper { // 4 bytes total, no padding }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // USE_API_NOISE #endif // USE_API diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index d0bc631e1b..fdaacbd94e 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -10,8 +10,7 @@ #include #include -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api.plaintext"; @@ -286,7 +285,6 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); } -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // USE_API_PLAINTEXT #endif // USE_API diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 465ceae827..b50902dd75 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -3,8 +3,7 @@ #ifdef USE_API #ifdef USE_API_PLAINTEXT -namespace esphome { -namespace api { +namespace esphome::api { class APIPlaintextFrameHelper : public APIFrameHelper { public: @@ -49,7 +48,6 @@ class APIPlaintextFrameHelper : public APIFrameHelper { // 8 bytes total, no padding needed }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // USE_API_PLAINTEXT #endif // USE_API diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h index fa4435e570..b5f7016689 100644 --- a/esphome/components/api/api_noise_context.h +++ b/esphome/components/api/api_noise_context.h @@ -3,8 +3,7 @@ #include #include "esphome/core/defines.h" -namespace esphome { -namespace api { +namespace esphome::api { #ifdef USE_API_NOISE using psk_t = std::array; @@ -28,5 +27,4 @@ class APINoiseContext { }; #endif // USE_API_NOISE -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d51f641746..6d2e17dc27 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" #include -namespace esphome { -namespace api { +namespace esphome::api { bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2981,5 +2980,4 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 9bf50fd18b..91a285fc6c 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -7,8 +7,7 @@ #include "proto.h" -namespace esphome { -namespace api { +namespace esphome::api { namespace enums { @@ -2891,5 +2890,4 @@ class UpdateCommandRequest : public CommandProtoMessage { }; #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index a2e69255e1..5db9b79cfa 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -7,8 +7,7 @@ #ifdef HAS_PROTO_MESSAGE_DUMP -namespace esphome { -namespace api { +namespace esphome::api { // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(std::string &out, const StringRef &ref) { @@ -2067,7 +2066,6 @@ void UpdateCommandRequest::dump_to(std::string &out) const { } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index d8ff4fcd24..d7d302a238 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -3,8 +3,7 @@ #include "api_pb2_service.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api.service"; @@ -901,5 +900,4 @@ void APIServerConnection::on_alarm_control_panel_command_request(const AlarmCont } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index f06ebdf9d5..38008197fa 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -6,8 +6,7 @@ #include "api_pb2.h" -namespace esphome { -namespace api { +namespace esphome::api { class APIServerConnectionBase : public ProtoService { public: @@ -444,5 +443,4 @@ class APIServerConnection : public APIServerConnectionBase { #endif }; -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 88966089cc..6d1729e611 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -16,8 +16,7 @@ #include -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api"; @@ -483,6 +482,5 @@ bool APIServer::teardown() { return this->clients_.empty(); } -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index edbd289421..54663a013f 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -18,8 +18,7 @@ #include -namespace esphome { -namespace api { +namespace esphome::api { #ifdef USE_API_NOISE struct SavedNoisePsk { @@ -196,6 +195,5 @@ template class APIConnectedCondition : public Condition { bool check(Ts... x) override { return global_api_server->is_connected(); } }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 6375dbfb9f..73c7804ff3 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -6,8 +6,7 @@ #ifdef USE_API_SERVICES #include "user_services.h" #endif -namespace esphome { -namespace api { +namespace esphome::api { #ifdef USE_API_SERVICES template class CustomAPIDeviceService : public UserServiceBase { @@ -222,6 +221,5 @@ class CustomAPIDevice { } }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d91c1ab287..212b3b22d6 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include -namespace esphome { -namespace api { +namespace esphome::api { template class TemplatableStringValue : public TemplatableValue { private: @@ -99,6 +98,5 @@ template class HomeAssistantServiceCallAction : public Action> variables_; }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 809c658803..da4800a45e 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace api { +namespace esphome::api { // Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR @@ -90,6 +89,5 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index b4cbf6c489..769d7b9b6e 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -4,8 +4,7 @@ #ifdef USE_API #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -namespace esphome { -namespace api { +namespace esphome::api { class APIConnection; @@ -96,6 +95,5 @@ class ListEntitiesIterator : public ComponentIterator { APIConnection *client_; }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index bf64d5f723..cb6c07ec3c 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api.proto"; @@ -89,5 +88,4 @@ std::string ProtoMessage::dump() const { } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 3e59ee1541..44f9716516 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -13,8 +13,7 @@ #define HAS_PROTO_MESSAGE_DUMP #endif -namespace esphome { -namespace api { +namespace esphome::api { /* * StringRef Ownership Model for API Protocol Messages @@ -910,5 +909,4 @@ class ProtoService { } }; -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 12accf4613..3a563f2221 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -3,8 +3,7 @@ #include "api_connection.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { // Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR @@ -69,6 +68,5 @@ INITIAL_STATE_HANDLER(update, update::UpdateEntity) InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 2b7b508056..2c22c322ec 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" #include "esphome/core/controller.h" -namespace esphome { -namespace api { +namespace esphome::api { class APIConnection; @@ -89,6 +88,5 @@ class InitialStateIterator : public ComponentIterator { APIConnection *client_; }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 7e73722a92..27b30eb332 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -1,8 +1,7 @@ #include "user_services.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { template<> bool get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.bool_; } template<> int32_t get_execute_arg_value(const ExecuteServiceArgument &arg) { @@ -40,5 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type>() return enums::SERVICE_ARG_TYPE_STRING_ARRAY; } -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index deec636cae..5f040e8433 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -8,8 +8,7 @@ #include "api_pb2.h" #ifdef USE_API_SERVICES -namespace esphome { -namespace api { +namespace esphome::api { class UserServiceDescriptor { public: @@ -74,6 +73,5 @@ template class UserServiceTrigger : public UserServiceBasetrigger(x...); } // NOLINT }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // USE_API_SERVICES diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 635291371e..424565a893 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2045,8 +2045,7 @@ def main() -> None: #include "proto.h" -namespace esphome { -namespace api { +namespace esphome::api { """ @@ -2057,8 +2056,7 @@ namespace api { #include "esphome/core/helpers.h" #include -namespace esphome { -namespace api { +namespace esphome::api { """ @@ -2072,8 +2070,7 @@ namespace api { #ifdef HAS_PROTO_MESSAGE_DUMP -namespace esphome { -namespace api { +namespace esphome::api { // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(std::string &out, const StringRef &ref) { @@ -2265,19 +2262,16 @@ static void dump_field(std::string &out, const char *field_name, T value, int in content += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api """ cpp += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api """ dump_cpp += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // HAS_PROTO_MESSAGE_DUMP """ @@ -2299,8 +2293,7 @@ static void dump_field(std::string &out, const char *field_name, T value, int in #include "api_pb2.h" -namespace esphome { -namespace api { +namespace esphome::api { """ @@ -2309,8 +2302,7 @@ namespace api { #include "api_pb2_service.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api.service"; @@ -2451,13 +2443,11 @@ static const char *const TAG = "api.service"; hpp += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api """ cpp += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api """ with open(root / "api_pb2_service.h", "w", encoding="utf-8") as f: From 568e7741160c1737de043fa28e4315ad69513009 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 24 Jul 2025 18:31:37 +1000 Subject: [PATCH 108/125] [mipi] Keep models from different drivers separate (#9865) --- esphome/components/mipi/__init__.py | 9 +++++++++ esphome/components/mipi_spi/display.py | 14 ++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index a6cb8f31eb..387df10c32 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -234,6 +234,15 @@ class DriverChip: self.defaults = defaults DriverChip.models[name] = self + @classmethod + def get_models(cls): + """ + Return the current set of models and reset the models dictionary. + """ + models = cls.models + cls.models = {} + return models + def extend(self, name, **kwargs) -> "DriverChip": defaults = self.defaults.copy() if ( diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index d5c9d7aa0f..cb2de6c3d7 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -1,4 +1,6 @@ +import importlib import logging +import pkgutil from esphome import pins import esphome.codegen as cg @@ -52,8 +54,7 @@ from esphome.core import CORE from esphome.cpp_generator import TemplateArguments from esphome.final_validate import full_config -from . import CONF_BUS_MODE, CONF_SPI_16, DOMAIN -from .models import adafruit, amoled, cyd, ili, jc, lanbon, lilygo, waveshare +from . import CONF_BUS_MODE, CONF_SPI_16, DOMAIN, models DEPENDENCIES = ["spi"] @@ -91,10 +92,11 @@ BusTypes = { DriverChip("CUSTOM") -MODELS = DriverChip.models -# This loop is a noop, but suppresses linting of side-effect-only imports -for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit): - pass +# Import all models dynamically from the models package +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) + +MODELS = DriverChip.get_models() DISPLAY_18BIT = "18bit" From 5bff9bc8d97de268bd341772d869b2f012aa71a3 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 24 Jul 2025 04:02:03 -0500 Subject: [PATCH 109/125] [ld2450] Use ``Deduplicator`` for sensors (#9863) --- esphome/components/ld2450/__init__.py | 1 + esphome/components/ld2450/ld2450.cpp | 229 +++++++++----------------- esphome/components/ld2450/ld2450.h | 77 +++------ 3 files changed, 102 insertions(+), 205 deletions(-) diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 442fdaa125..cdbf8a17c4 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -3,6 +3,7 @@ from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_THROTTLE +AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] CODEOWNERS = ["@hareeshmu"] MULTI_CONF = True diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 09761b2937..fc1add8268 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -1,6 +1,5 @@ #include "ld2450.h" -#include -#include + #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif @@ -11,8 +10,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#define highbyte(val) (uint8_t)((val) >> 8) -#define lowbyte(val) (uint8_t)((val) &0xff) +#include +#include namespace esphome { namespace ld2450 { @@ -170,21 +169,16 @@ static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) { } static inline float calculate_angle(float base, float hypotenuse) { - if (base < 0.0 || hypotenuse <= 0.0) { - return 0.0; + if (base < 0.0f || hypotenuse <= 0.0f) { + return 0.0f; } - float angle_radians = std::acos(base / hypotenuse); - float angle_degrees = angle_radians * (180.0 / M_PI); + float angle_radians = acosf(base / hypotenuse); + float angle_degrees = angle_radians * (180.0f / std::numbers::pi_v); return angle_degrees; } -static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { - for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { - if (header_footer[i] != buffer[i]) { - return false; // Mismatch in header/footer - } - } - return true; // Valid header/footer +static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } void LD2450Component::setup() { @@ -217,41 +211,41 @@ void LD2450Component::dump_config() { #endif #ifdef USE_SENSOR ESP_LOGCONFIG(TAG, "Sensors:"); - LOG_SENSOR(" ", "MovingTargetCount", this->moving_target_count_sensor_); - LOG_SENSOR(" ", "StillTargetCount", this->still_target_count_sensor_); - LOG_SENSOR(" ", "TargetCount", this->target_count_sensor_); - for (sensor::Sensor *s : this->move_x_sensors_) { - LOG_SENSOR(" ", "TargetX", s); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetCount", this->moving_target_count_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetCount", this->still_target_count_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetCount", this->target_count_sensor_); + for (auto &s : this->move_x_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetX", s); } - for (sensor::Sensor *s : this->move_y_sensors_) { - LOG_SENSOR(" ", "TargetY", s); + for (auto &s : this->move_y_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetY", s); } - for (sensor::Sensor *s : this->move_angle_sensors_) { - LOG_SENSOR(" ", "TargetAngle", s); + for (auto &s : this->move_angle_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetAngle", s); } - for (sensor::Sensor *s : this->move_distance_sensors_) { - LOG_SENSOR(" ", "TargetDistance", s); + for (auto &s : this->move_distance_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetDistance", s); } - for (sensor::Sensor *s : this->move_resolution_sensors_) { - LOG_SENSOR(" ", "TargetResolution", s); + for (auto &s : this->move_resolution_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetResolution", s); } - for (sensor::Sensor *s : this->move_speed_sensors_) { - LOG_SENSOR(" ", "TargetSpeed", s); + for (auto &s : this->move_speed_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetSpeed", s); } - for (sensor::Sensor *s : this->zone_target_count_sensors_) { - LOG_SENSOR(" ", "ZoneTargetCount", s); + for (auto &s : this->zone_target_count_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneTargetCount", s); } - for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) { - LOG_SENSOR(" ", "ZoneMovingTargetCount", s); + for (auto &s : this->zone_moving_target_count_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneMovingTargetCount", s); } - for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { - LOG_SENSOR(" ", "ZoneStillTargetCount", s); + for (auto &s : this->zone_still_target_count_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneStillTargetCount", s); } #endif #ifdef USE_TEXT_SENSOR ESP_LOGCONFIG(TAG, "Text Sensors:"); LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); - LOG_TEXT_SENSOR(" ", "Mac", this->mac_text_sensor_); + LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_); for (text_sensor::TextSensor *s : this->direction_text_sensors_) { LOG_TEXT_SENSOR(" ", "Direction", s); } @@ -419,19 +413,19 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu if (command_value != nullptr) { len += command_value_len; } - uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; + // 2 length bytes (low, high) + 2 command bytes (low, high) + uint8_t len_cmd[] = {len, 0x00, command, 0x00}; this->write_array(len_cmd, sizeof(len_cmd)); - // command value bytes if (command_value != nullptr) { - for (uint8_t i = 0; i < command_value_len; i++) { - this->write_byte(command_value[i]); - } + this->write_array(command_value, command_value_len); } // frame footer bytes this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); - // FIXME to remove - delay(50); // NOLINT + + if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) { + delay(50); // NOLINT + } } // LD2450 Radar data message: @@ -459,8 +453,8 @@ void LD2450Component::handle_periodic_data_() { int16_t target_count = 0; int16_t still_target_count = 0; int16_t moving_target_count = 0; + int16_t res = 0; int16_t start = 0; - int16_t val = 0; int16_t tx = 0; int16_t ty = 0; int16_t td = 0; @@ -478,85 +472,43 @@ void LD2450Component::handle_periodic_data_() { start = TARGET_X + index * 8; is_moving = false; // tx is used for further calculations, so always needs to be populated - val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - tx = val; - sensor::Sensor *sx = this->move_x_sensors_[index]; - if (sx != nullptr) { - if (this->cached_target_data_[index].x != val) { - sx->publish_state(val); - this->cached_target_data_[index].x = val; - } - } + tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); + SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx); // Y start = TARGET_Y + index * 8; - // ty is used for further calculations, so always needs to be populated - val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - ty = val; - sensor::Sensor *sy = this->move_y_sensors_[index]; - if (sy != nullptr) { - if (this->cached_target_data_[index].y != val) { - sy->publish_state(val); - this->cached_target_data_[index].y = val; - } - } + ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); + SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty); // RESOLUTION start = TARGET_RESOLUTION + index * 8; - sensor::Sensor *sr = this->move_resolution_sensors_[index]; - if (sr != nullptr) { - val = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start]; - if (this->cached_target_data_[index].resolution != val) { - sr->publish_state(val); - this->cached_target_data_[index].resolution = val; - } - } + res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start]; + SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res); #endif // SPEED start = TARGET_SPEED + index * 8; - val = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]); - ts = val; - if (val) { + ts = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]); + if (ts) { is_moving = true; moving_target_count++; } #ifdef USE_SENSOR - sensor::Sensor *ss = this->move_speed_sensors_[index]; - if (ss != nullptr) { - if (this->cached_target_data_[index].speed != val) { - ss->publish_state(val); - this->cached_target_data_[index].speed = val; - } - } + SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts); #endif // DISTANCE // Optimized: use already decoded tx and ty values, replace pow() with multiplication int32_t x_squared = (int32_t) tx * tx; int32_t y_squared = (int32_t) ty * ty; - val = (uint16_t) sqrt(x_squared + y_squared); - td = val; - if (val > 0) { + td = (uint16_t) sqrtf(x_squared + y_squared); + if (td > 0) { target_count++; } #ifdef USE_SENSOR - sensor::Sensor *sd = this->move_distance_sensors_[index]; - if (sd != nullptr) { - if (this->cached_target_data_[index].distance != val) { - sd->publish_state(val); - this->cached_target_data_[index].distance = val; - } - } + SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); // ANGLE angle = ld2450::calculate_angle(static_cast(ty), static_cast(td)); if (tx > 0) { angle = angle * -1; } - sensor::Sensor *sa = this->move_angle_sensors_[index]; - if (sa != nullptr) { - if (std::isnan(this->cached_target_data_[index].angle) || - std::abs(this->cached_target_data_[index].angle - angle) > 0.1f) { - sa->publish_state(angle); - this->cached_target_data_[index].angle = angle; - } - } + SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); #endif #ifdef USE_TEXT_SENSOR // DIRECTION @@ -570,11 +522,9 @@ void LD2450Component::handle_periodic_data_() { direction = DIRECTION_STATIONARY; } text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; - if (tsd != nullptr) { - if (this->cached_target_data_[index].direction != direction) { - tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction)); - this->cached_target_data_[index].direction = direction; - } + const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction); + if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) { + tsd->publish_state(dir_str); } #endif @@ -599,53 +549,19 @@ void LD2450Component::handle_periodic_data_() { zone_all_targets = zone_still_targets + zone_moving_targets; // Publish Still Target Count in Zones - sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index]; - if (szstc != nullptr) { - if (this->cached_zone_data_[index].still_count != zone_still_targets) { - szstc->publish_state(zone_still_targets); - this->cached_zone_data_[index].still_count = zone_still_targets; - } - } + SAFE_PUBLISH_SENSOR(this->zone_still_target_count_sensors_[index], zone_still_targets); // Publish Moving Target Count in Zones - sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index]; - if (szmtc != nullptr) { - if (this->cached_zone_data_[index].moving_count != zone_moving_targets) { - szmtc->publish_state(zone_moving_targets); - this->cached_zone_data_[index].moving_count = zone_moving_targets; - } - } + SAFE_PUBLISH_SENSOR(this->zone_moving_target_count_sensors_[index], zone_moving_targets); // Publish All Target Count in Zones - sensor::Sensor *sztc = this->zone_target_count_sensors_[index]; - if (sztc != nullptr) { - if (this->cached_zone_data_[index].total_count != zone_all_targets) { - sztc->publish_state(zone_all_targets); - this->cached_zone_data_[index].total_count = zone_all_targets; - } - } - + SAFE_PUBLISH_SENSOR(this->zone_target_count_sensors_[index], zone_all_targets); } // End loop thru zones // Target Count - if (this->target_count_sensor_ != nullptr) { - if (this->cached_global_data_.target_count != target_count) { - this->target_count_sensor_->publish_state(target_count); - this->cached_global_data_.target_count = target_count; - } - } + SAFE_PUBLISH_SENSOR(this->target_count_sensor_, target_count); // Still Target Count - if (this->still_target_count_sensor_ != nullptr) { - if (this->cached_global_data_.still_count != still_target_count) { - this->still_target_count_sensor_->publish_state(still_target_count); - this->cached_global_data_.still_count = still_target_count; - } - } + SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count); // Moving Target Count - if (this->moving_target_count_sensor_ != nullptr) { - if (this->cached_global_data_.moving_count != moving_target_count) { - this->moving_target_count_sensor_->publish_state(moving_target_count); - this->cached_global_data_.moving_count = moving_target_count; - } - } + SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count); #endif #ifdef USE_BINARY_SENSOR @@ -942,28 +858,33 @@ void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QU void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); } #ifdef USE_SENSOR -void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { this->move_x_sensors_[target] = s; } -void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { this->move_y_sensors_[target] = s; } +// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. +void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { + this->move_x_sensors_[target] = new SensorWithDedup(s); +} +void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { + this->move_y_sensors_[target] = new SensorWithDedup(s); +} void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) { - this->move_speed_sensors_[target] = s; + this->move_speed_sensors_[target] = new SensorWithDedup(s); } void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) { - this->move_angle_sensors_[target] = s; + this->move_angle_sensors_[target] = new SensorWithDedup(s); } void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) { - this->move_distance_sensors_[target] = s; + this->move_distance_sensors_[target] = new SensorWithDedup(s); } void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) { - this->move_resolution_sensors_[target] = s; + this->move_resolution_sensors_[target] = new SensorWithDedup(s); } void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_target_count_sensors_[zone] = s; + this->zone_target_count_sensors_[zone] = new SensorWithDedup(s); } void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_still_target_count_sensors_[zone] = s; + this->zone_still_target_count_sensors_[zone] = new SensorWithDedup(s); } void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_moving_target_count_sensors_[zone] = s; + this->zone_moving_target_count_sensors_[zone] = new SensorWithDedup(s); } #endif #ifdef USE_TEXT_SENSOR diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index ae72a0d8cb..0fba0f9be3 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,12 +1,7 @@ #pragma once -#include "esphome/components/uart/uart.h" -#include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/helpers.h" -#include "esphome/core/preferences.h" -#include -#include +#include "esphome/core/component.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif @@ -29,18 +24,23 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -#ifndef M_PI -#define M_PI 3.14 -#endif +#include "esphome/components/ld24xx/ld24xx.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +#include namespace esphome { namespace ld2450 { +using namespace ld24xx; + // Constants -static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static const uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. +static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, @@ -81,9 +81,9 @@ class LD2450Component : public Component, public uart::UARTDevice { SUB_BINARY_SENSOR(target) #endif #ifdef USE_SENSOR - SUB_SENSOR(moving_target_count) - SUB_SENSOR(still_target_count) - SUB_SENSOR(target_count) + SUB_SENSOR_WITH_DEDUP(moving_target_count, uint8_t) + SUB_SENSOR_WITH_DEDUP(still_target_count, uint8_t) + SUB_SENSOR_WITH_DEDUP(target_count, uint8_t) #endif #ifdef USE_TEXT_SENSOR SUB_TEXT_SENSOR(mac) @@ -176,48 +176,23 @@ class LD2450Component : public Component, public uart::UARTDevice { Target target_info_[MAX_TARGETS]; Zone zone_config_[MAX_ZONES]; - // Change detection - cache previous values to avoid redundant publishes - // All values are initialized to sentinel values that are outside the valid sensor ranges - // to ensure the first real measurement is always published - struct CachedTargetData { - int16_t x = std::numeric_limits::min(); // -32768, outside range of -4860 to 4860 - int16_t y = std::numeric_limits::min(); // -32768, outside range of 0 to 7560 - int16_t speed = std::numeric_limits::min(); // -32768, outside practical sensor range - uint16_t resolution = std::numeric_limits::max(); // 65535, unlikely resolution value - uint16_t distance = std::numeric_limits::max(); // 65535, outside range of 0 to ~8990 - Direction direction = DIRECTION_UNDEFINED; // Undefined, will differ from any real direction - float angle = NAN; // NAN, safe sentinel for floats - } cached_target_data_[MAX_TARGETS]; - - struct CachedZoneData { - uint8_t still_count = std::numeric_limits::max(); // 255, unlikely zone count - uint8_t moving_count = std::numeric_limits::max(); // 255, unlikely zone count - uint8_t total_count = std::numeric_limits::max(); // 255, unlikely zone count - } cached_zone_data_[MAX_ZONES]; - - struct CachedGlobalData { - uint8_t target_count = std::numeric_limits::max(); // 255, max 3 targets possible - uint8_t still_count = std::numeric_limits::max(); // 255, max 3 targets possible - uint8_t moving_count = std::numeric_limits::max(); // 255, max 3 targets possible - } cached_global_data_; - #ifdef USE_NUMBER ESPPreferenceObject pref_; // only used when numbers are in use ZoneOfNumbers zone_numbers_[MAX_ZONES]; #endif #ifdef USE_SENSOR - std::vector move_x_sensors_ = std::vector(MAX_TARGETS); - std::vector move_y_sensors_ = std::vector(MAX_TARGETS); - std::vector move_speed_sensors_ = std::vector(MAX_TARGETS); - std::vector move_angle_sensors_ = std::vector(MAX_TARGETS); - std::vector move_distance_sensors_ = std::vector(MAX_TARGETS); - std::vector move_resolution_sensors_ = std::vector(MAX_TARGETS); - std::vector zone_target_count_sensors_ = std::vector(MAX_ZONES); - std::vector zone_still_target_count_sensors_ = std::vector(MAX_ZONES); - std::vector zone_moving_target_count_sensors_ = std::vector(MAX_ZONES); + std::array *, MAX_TARGETS> move_x_sensors_{}; + std::array *, MAX_TARGETS> move_y_sensors_{}; + std::array *, MAX_TARGETS> move_speed_sensors_{}; + std::array *, MAX_TARGETS> move_angle_sensors_{}; + std::array *, MAX_TARGETS> move_distance_sensors_{}; + std::array *, MAX_TARGETS> move_resolution_sensors_{}; + std::array *, MAX_ZONES> zone_target_count_sensors_{}; + std::array *, MAX_ZONES> zone_still_target_count_sensors_{}; + std::array *, MAX_ZONES> zone_moving_target_count_sensors_{}; #endif #ifdef USE_TEXT_SENSOR - std::vector direction_text_sensors_ = std::vector(3); + std::array direction_text_sensors_{}; #endif }; From 1344103086a3bcfffd75d27939cf2f168010cf0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Jul 2025 01:04:00 -1000 Subject: [PATCH 110/125] [core] Revert #9851 and rename ESPHOME_CORES to ESPHOME_THREAD (#9862) --- esphome/components/esp32/__init__.py | 18 ++----------- esphome/components/esp8266/__init__.py | 4 +-- esphome/components/host/__init__.py | 4 +-- esphome/components/libretiny/__init__.py | 4 +-- esphome/components/nrf52/__init__.py | 4 +-- esphome/components/rp2040/__init__.py | 4 +-- esphome/const.py | 10 ++++---- esphome/core/defines.h | 4 +-- esphome/core/scheduler.cpp | 32 ++++++++++++------------ esphome/core/scheduler.h | 18 ++++++------- 10 files changed, 44 insertions(+), 58 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index e24815741a..5dd2288076 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -31,7 +31,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP32, - CoreModel, + ThreadModel, __version__, ) from esphome.core import CORE, HexInt, TimePeriod @@ -98,16 +98,6 @@ ARDUINO_ALLOWED_VARIANTS = [ VARIANT_ESP32S3, ] -# Single-core ESP32 variants -SINGLE_CORE_VARIANTS = frozenset( - [ - VARIANT_ESP32S2, - VARIANT_ESP32C3, - VARIANT_ESP32C6, - VARIANT_ESP32H2, - ] -) - def get_cpu_frequencies(*frequencies): return [str(x) + "MHZ" for x in frequencies] @@ -724,11 +714,7 @@ async def to_code(config): cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) - # Set threading model based on core count - if config[CONF_VARIANT] in SINGLE_CORE_VARIANTS: - cg.add_define(CoreModel.SINGLE) - else: - cg.add_define(CoreModel.MULTI_ATOMICS) + cg.add_define(ThreadModel.MULTI_ATOMICS) cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "strict") diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index d08d7121b7..0184c25965 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -15,7 +15,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP8266, - CoreModel, + ThreadModel, ) from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed @@ -188,7 +188,7 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") - cg.add_define(CoreModel.SINGLE) + cg.add_define(ThreadModel.SINGLE) cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 2d77f2f7ab..ba05e497c8 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -7,7 +7,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_HOST, - CoreModel, + ThreadModel, ) from esphome.core import CORE @@ -44,7 +44,7 @@ async def to_code(config): cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") - cg.add_define(CoreModel.MULTI_ATOMICS) + cg.add_define(ThreadModel.MULTI_ATOMICS) cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "strict") diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 7f2a0bc0a5..178660cb40 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -20,7 +20,7 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, - CoreModel, + ThreadModel, __version__, ) from esphome.core import CORE @@ -261,7 +261,7 @@ async def component_to_code(config): cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) - cg.add_define(CoreModel.MULTI_NO_ATOMICS) + cg.add_define(ThreadModel.MULTI_NO_ATOMICS) # force using arduino framework cg.add_platformio_option("framework", "arduino") diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 870c51066c..17807b9e2b 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -23,7 +23,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_NRF52, - CoreModel, + ThreadModel, ) from esphome.core import CORE, EsphomeError, coroutine_with_priority from esphome.storage_json import StorageJSON @@ -110,7 +110,7 @@ async def to_code(config: ConfigType) -> None: cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "NRF52") # nRF52 processors are single-core - cg.add_define(CoreModel.SINGLE) + cg.add_define(ThreadModel.SINGLE) cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) cg.add_platformio_option( "platform", diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 28c3bbd70c..46eabb5325 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -16,7 +16,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_RP2040, - CoreModel, + ThreadModel, ) from esphome.core import CORE, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file @@ -172,7 +172,7 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") - cg.add_define(CoreModel.SINGLE) + cg.add_define(ThreadModel.SINGLE) cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) diff --git a/esphome/const.py b/esphome/const.py index 627b6bac18..7d373ff26c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -35,12 +35,12 @@ class Framework(StrEnum): ZEPHYR = "zephyr" -class CoreModel(StrEnum): - """Core model identifiers for ESPHome scheduler.""" +class ThreadModel(StrEnum): + """Threading model identifiers for ESPHome scheduler.""" - SINGLE = "ESPHOME_CORES_SINGLE" - MULTI_NO_ATOMICS = "ESPHOME_CORES_MULTI_NO_ATOMICS" - MULTI_ATOMICS = "ESPHOME_CORES_MULTI_ATOMICS" + SINGLE = "ESPHOME_THREAD_SINGLE" + MULTI_NO_ATOMICS = "ESPHOME_THREAD_MULTI_NO_ATOMICS" + MULTI_ATOMICS = "ESPHOME_THREAD_MULTI_ATOMICS" class PlatformFramework(Enum): diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9355d56084..348f288863 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -15,8 +15,8 @@ #define ESPHOME_VARIANT "ESP32" #define ESPHOME_DEBUG_SCHEDULER -// Default threading model for static analysis (ESP32 is multi-core with atomics) -#define ESPHOME_CORES_MULTI_ATOMICS +// Default threading model for static analysis (ESP32 is multi-threaded with atomics) +#define ESPHOME_THREAD_MULTI_ATOMICS // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index fc5d43d262..dd80199dc0 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -84,7 +84,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->callback = std::move(func); item->remove = false; -#ifndef ESPHOME_CORES_SINGLE +#ifndef ESPHOME_THREAD_SINGLE // Special handling for defer() (delay = 0, type = TIMEOUT) // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { @@ -94,7 +94,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type this->defer_queue_.push_back(std::move(item)); return; } -#endif /* not ESPHOME_CORES_SINGLE */ +#endif /* not ESPHOME_THREAD_SINGLE */ // Get fresh timestamp for new timer/interval - ensures accurate scheduling const auto now = this->millis_64_(millis()); // Fresh millis() call @@ -238,7 +238,7 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { return item->next_execution_ - now_64; } void HOT Scheduler::call(uint32_t now) { -#ifndef ESPHOME_CORES_SINGLE +#ifndef ESPHOME_THREAD_SINGLE // Process defer queue first to guarantee FIFO execution order for deferred items. // Previously, defer() used the heap which gave undefined order for equal timestamps, // causing race conditions on multi-core systems (ESP32, BK7200). @@ -268,7 +268,7 @@ void HOT Scheduler::call(uint32_t now) { this->execute_item_(item.get(), now); } } -#endif /* not ESPHOME_CORES_SINGLE */ +#endif /* not ESPHOME_THREAD_SINGLE */ // Convert the fresh timestamp from main loop to 64-bit for scheduler operations const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() @@ -280,15 +280,15 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector> old_items; -#ifdef ESPHOME_CORES_MULTI_ATOMICS +#ifdef ESPHOME_THREAD_MULTI_ATOMICS const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, major_dbg, last_dbg); -#else /* not ESPHOME_CORES_MULTI_ATOMICS */ +#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, this->millis_major_, this->last_millis_); -#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ +#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { @@ -473,7 +473,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c size_t total_cancelled = 0; // Check all containers for matching items -#ifndef ESPHOME_CORES_SINGLE +#ifndef ESPHOME_THREAD_SINGLE // Only check defer queue for timeouts (intervals never go there) if (type == SchedulerItem::TIMEOUT) { for (auto &item : this->defer_queue_) { @@ -483,7 +483,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c } } } -#endif /* not ESPHOME_CORES_SINGLE */ +#endif /* not ESPHOME_THREAD_SINGLE */ // Cancel items in the main heap for (auto &item : this->items_) { @@ -509,9 +509,9 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c uint64_t Scheduler::millis_64_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags - // - ESPHOME_CORES_SINGLE - Runs on single-core platforms (ESP8266, RP2040, etc.) - // - ESPHOME_CORES_MULTI_NO_ATOMICS - Runs on multi-core platforms without atomics (LibreTiny) - // - ESPHOME_CORES_MULTI_ATOMICS - Runs on multi-core platforms with atomics (ESP32, HOST, etc.) + // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.) + // - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny) + // - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (ESP32, HOST, etc.) // // Make sure all changes are synchronized if you edit this function. // @@ -520,7 +520,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // helps maintain accuracy. // -#ifdef ESPHOME_CORES_SINGLE +#ifdef ESPHOME_THREAD_SINGLE // This is the single core implementation. // // Single-core platforms have no concurrency, so this is a simple implementation @@ -546,7 +546,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time return now + (static_cast(major) << 32); -#elif defined(ESPHOME_CORES_MULTI_NO_ATOMICS) +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) // This is the multi core no atomics implementation. // // Without atomics, this implementation uses locks more aggressively: @@ -595,7 +595,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time return now + (static_cast(major) << 32); -#elif defined(ESPHOME_CORES_MULTI_ATOMICS) +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) // This is the multi core with atomics implementation. // // Uses atomic operations with acquire/release semantics to ensure coherent @@ -660,7 +660,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { #else #error \ - "No platform threading model defined. One of ESPHOME_CORES_SINGLE, ESPHOME_CORES_MULTI_NO_ATOMICS, or ESPHOME_CORES_MULTI_ATOMICS must be defined." + "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index c14b7debe4..7bf83f7877 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -5,7 +5,7 @@ #include #include #include -#ifdef ESPHOME_CORES_MULTI_ATOMICS +#ifdef ESPHOME_THREAD_MULTI_ATOMICS #include #endif @@ -200,13 +200,13 @@ class Scheduler { Mutex lock_; std::vector> items_; std::vector> to_add_; -#ifndef ESPHOME_CORES_SINGLE +#ifndef ESPHOME_THREAD_SINGLE // Single-core platforms don't need the defer queue and save 40 bytes of RAM std::deque> defer_queue_; // FIFO queue for defer() calls -#endif /* ESPHOME_CORES_SINGLE */ +#endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; -#ifdef ESPHOME_CORES_MULTI_ATOMICS +#ifdef ESPHOME_THREAD_MULTI_ATOMICS /* * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates * @@ -218,10 +218,10 @@ class Scheduler { * it also observes the corresponding increment of `millis_major_`. */ std::atomic last_millis_{0}; -#else /* not ESPHOME_CORES_MULTI_ATOMICS */ +#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ // Platforms without atomic support or single-threaded platforms uint32_t last_millis_{0}; -#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ +#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ /* * Upper 16 bits of the 64-bit millis counter. Incremented only while holding @@ -229,11 +229,11 @@ class Scheduler { * Ordering relative to `last_millis_` is provided by its release store and the * corresponding acquire loads. */ -#ifdef ESPHOME_CORES_MULTI_ATOMICS +#ifdef ESPHOME_THREAD_MULTI_ATOMICS std::atomic millis_major_{0}; -#else /* not ESPHOME_CORES_MULTI_ATOMICS */ +#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; -#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ +#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ }; } // namespace esphome From ba1de5feff8026368b36fd8d27f368675e1dc19f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 24 Jul 2025 23:18:29 +1200 Subject: [PATCH 111/125] [CI] Refactor auto-label workflow: modular architecture, CODEOWNERS automation, and performance improvements (#9860) --- .github/workflows/auto-label-pr.yml | 867 +++++++++++++++------------- 1 file changed, 474 insertions(+), 393 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index b30f6cf28a..17c77a1920 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -23,24 +23,6 @@ jobs: - name: Checkout uses: actions/checkout@v4.2.2 - - name: Get changes - id: changes - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Get PR number - pr_number="${{ github.event.pull_request.number }}" - - # Get list of changed files using gh CLI - files=$(gh pr diff $pr_number --name-only) - echo "files<> $GITHUB_OUTPUT - echo "$files" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - # Get file stats (additions + deletions) using gh CLI - stats=$(gh pr view $pr_number --json files --jq '.files | map(.additions + .deletions) | add') - echo "total_changes=${stats:-0}" >> $GITHUB_OUTPUT - - name: Generate a token id: generate-token uses: actions/create-github-app-token@v2 @@ -55,93 +37,453 @@ jobs: script: | const fs = require('fs'); + // Constants + const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); + const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}'); + const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); + const BOT_COMMENT_MARKER = ''; + const CODEOWNERS_MARKER = ''; + const TOO_BIG_MARKER = ''; + + const MANAGED_LABELS = [ + 'new-component', + 'new-platform', + 'new-target-platform', + 'merging-to-release', + 'merging-to-beta', + 'core', + 'small-pr', + 'dashboard', + 'github-actions', + 'by-code-owner', + 'has-tests', + 'needs-tests', + 'needs-docs', + 'needs-codeowners', + 'too-big', + 'labeller-recheck' + ]; + + const DOCS_PR_PATTERNS = [ + /https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/, + /esphome\/esphome-docs#\d+/ + ]; + + // Global state const { owner, repo } = context.repo; const pr_number = context.issue.number; - // Hidden marker to identify bot comments from this workflow - const BOT_COMMENT_MARKER = ''; - - // Get current labels + // Get current labels and PR data const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({ owner, repo, issue_number: pr_number }); const currentLabels = currentLabelsData.map(label => label.name); - - // Define managed labels that this workflow controls const managedLabels = currentLabels.filter(label => - label.startsWith('component: ') || - [ - 'new-component', - 'new-platform', - 'new-target-platform', - 'merging-to-release', - 'merging-to-beta', - 'core', - 'small-pr', - 'dashboard', - 'github-actions', - 'by-code-owner', - 'has-tests', - 'needs-tests', - 'needs-docs', - 'too-big', - 'labeller-recheck' - ].includes(label) + label.startsWith('component: ') || MANAGED_LABELS.includes(label) ); + const { data: prFiles } = await github.rest.pulls.listFiles({ + owner, + repo, + pull_number: pr_number + }); + + // Calculate data from PR files + const changedFiles = prFiles.map(file => file.filename); + const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + console.log('Current labels:', currentLabels.join(', ')); - console.log('Managed labels:', managedLabels.join(', ')); - - // Get changed files - const changedFiles = `${{ steps.changes.outputs.files }}`.split('\n').filter(f => f.length > 0); - const totalChanges = parseInt('${{ steps.changes.outputs.total_changes }}') || 0; - console.log('Changed files:', changedFiles.length); console.log('Total changes:', totalChanges); - const labels = new Set(); - - // Fetch TARGET_PLATFORMS and PLATFORM_COMPONENTS from API - let targetPlatforms = []; - let platformComponents = []; - - try { - const response = await fetch('https://data.esphome.io/components.json'); - const componentsData = await response.json(); - - // Extract target platforms and platform components directly from API - targetPlatforms = componentsData.target_platforms || []; - platformComponents = componentsData.platform_components || []; - - console.log('Target platforms from API:', targetPlatforms.length, targetPlatforms); - console.log('Platform components from API:', platformComponents.length, platformComponents); - } catch (error) { - console.log('Failed to fetch components data from API:', error.message); + // Fetch API data + async function fetchApiData() { + try { + const response = await fetch('https://data.esphome.io/components.json'); + const componentsData = await response.json(); + return { + targetPlatforms: componentsData.target_platforms || [], + platformComponents: componentsData.platform_components || [] + }; + } catch (error) { + console.log('Failed to fetch components data from API:', error.message); + return { targetPlatforms: [], platformComponents: [] }; + } } - // Get environment variables - const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); - const maxLabels = parseInt('${{ env.MAX_LABELS }}'); - const tooBigThreshold = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); + // Strategy: Merge branch detection + async function detectMergeBranch() { + const labels = new Set(); + const baseRef = context.payload.pull_request.base.ref; - // Strategy: Merge to release or beta branch - const baseRef = context.payload.pull_request.base.ref; - if (baseRef !== 'dev') { if (baseRef === 'release') { labels.add('merging-to-release'); } else if (baseRef === 'beta') { labels.add('merging-to-beta'); } - // When targeting non-dev branches, only use merge warning labels - const finalLabels = Array.from(labels); + return labels; + } + + // Strategy: Component and platform labeling + async function detectComponentPlatforms(apiData) { + const labels = new Set(); + const componentRegex = /^esphome\/components\/([^\/]+)\//; + const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`); + + for (const file of changedFiles) { + const componentMatch = file.match(componentRegex); + if (componentMatch) { + labels.add(`component: ${componentMatch[1]}`); + } + + const platformMatch = file.match(targetPlatformRegex); + if (platformMatch) { + labels.add(`platform: ${platformMatch[1]}`); + } + } + + return labels; + } + + // Strategy: New component detection + async function detectNewComponents() { + const labels = new Set(); + const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); + + for (const file of addedFiles) { + const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/); + if (componentMatch) { + try { + const content = fs.readFileSync(file, 'utf8'); + if (content.includes('IS_TARGET_PLATFORM = True')) { + labels.add('new-target-platform'); + } + } catch (error) { + console.log(`Failed to read content of ${file}:`, error.message); + } + labels.add('new-component'); + } + } + + return labels; + } + + // Strategy: New platform detection + async function detectNewPlatforms(apiData) { + const labels = new Set(); + const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); + + for (const file of addedFiles) { + const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/); + if (platformFileMatch) { + const [, component, platform] = platformFileMatch; + if (apiData.platformComponents.includes(platform)) { + labels.add('new-platform'); + } + } + + const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/); + if (platformDirMatch) { + const [, component, platform] = platformDirMatch; + if (apiData.platformComponents.includes(platform)) { + labels.add('new-platform'); + } + } + } + + return labels; + } + + // Strategy: Core files detection + async function detectCoreChanges() { + const labels = new Set(); + const coreFiles = changedFiles.filter(file => + file.startsWith('esphome/core/') || + (file.startsWith('esphome/') && file.split('/').length === 2) + ); + + if (coreFiles.length > 0) { + labels.add('core'); + } + + return labels; + } + + // Strategy: PR size detection + async function detectPRSize() { + const labels = new Set(); + const testChanges = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + + const nonTestChanges = totalChanges - testChanges; + + if (totalChanges <= SMALL_PR_THRESHOLD) { + labels.add('small-pr'); + } + + if (nonTestChanges > TOO_BIG_THRESHOLD) { + labels.add('too-big'); + } + + return labels; + } + + // Strategy: Dashboard changes + async function detectDashboardChanges() { + const labels = new Set(); + const dashboardFiles = changedFiles.filter(file => + file.startsWith('esphome/dashboard/') || + file.startsWith('esphome/components/dashboard_import/') + ); + + if (dashboardFiles.length > 0) { + labels.add('dashboard'); + } + + return labels; + } + + // Strategy: GitHub Actions changes + async function detectGitHubActionsChanges() { + const labels = new Set(); + const githubActionsFiles = changedFiles.filter(file => + file.startsWith('.github/workflows/') + ); + + if (githubActionsFiles.length > 0) { + labels.add('github-actions'); + } + + return labels; + } + + // Strategy: Code owner detection + async function detectCodeOwner() { + const labels = new Set(); + + try { + const { data: codeownersFile } = await github.rest.repos.getContent({ + owner, + repo, + path: 'CODEOWNERS', + }); + + const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + const prAuthor = context.payload.pull_request.user.login; + + const codeownersLines = codeownersContent.split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + const codeownersRegexes = codeownersLines.map(line => { + const parts = line.split(/\s+/); + const pattern = parts[0]; + const owners = parts.slice(1); + + let regex; + if (pattern.endsWith('*')) { + const dir = pattern.slice(0, -1); + regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); + } else if (pattern.includes('*')) { + const regexPattern = pattern + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/\\*/g, '.*'); + regex = new RegExp(`^${regexPattern}$`); + } else { + regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); + } + + return { regex, owners }; + }); + + for (const file of changedFiles) { + for (const { regex, owners } of codeownersRegexes) { + if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) { + labels.add('by-code-owner'); + return labels; + } + } + } + } catch (error) { + console.log('Failed to read or parse CODEOWNERS file:', error.message); + } + + return labels; + } + + // Strategy: Test detection + async function detectTests() { + const labels = new Set(); + const testFiles = changedFiles.filter(file => file.startsWith('tests/')); + + if (testFiles.length > 0) { + labels.add('has-tests'); + } + + return labels; + } + + // Strategy: Requirements detection + async function detectRequirements(allLabels) { + const labels = new Set(); + + // Check for missing tests + if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) { + labels.add('needs-tests'); + } + + // Check for missing docs + if (allLabels.has('new-component') || allLabels.has('new-platform')) { + const prBody = context.payload.pull_request.body || ''; + const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody)); + + if (!hasDocsLink) { + labels.add('needs-docs'); + } + } + + // Check for missing CODEOWNERS + if (allLabels.has('new-component')) { + const codeownersModified = prFiles.some(file => + file.filename === 'CODEOWNERS' && + (file.status === 'modified' || file.status === 'added') && + (file.additions || 0) > 0 + ); + + if (!codeownersModified) { + labels.add('needs-codeowners'); + } + } + + return labels; + } + + // Generate review messages + function generateReviewMessages(finalLabels) { + const messages = []; + const prAuthor = context.payload.pull_request.user.login; + + // Too big message + if (finalLabels.includes('too-big')) { + const testChanges = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + const nonTestChanges = totalChanges - testChanges; + + const tooManyLabels = finalLabels.length > MAX_LABELS; + const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; + + let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; + + if (tooManyLabels && tooManyChanges) { + message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`; + } else if (tooManyLabels) { + message += `This PR affects ${finalLabels.length} different components/areas.`; + } else { + message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; + } + + message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`; + message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`; + + messages.push(message); + } + + // CODEOWNERS message + if (finalLabels.includes('needs-codeowners')) { + const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` + + `Hey there @${prAuthor},\n` + + `Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` + + `This way we can notify you if a bug report for this integration is reported.\n\n` + + `In \`__init__.py\` of the integration, please add:\n\n` + + `\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` + + `And run \`script/build_codeowners.py\``; + + messages.push(message); + } + + return messages; + } + + // Handle reviews + async function handleReviews(finalLabels) { + const reviewMessages = generateReviewMessages(finalLabels); + const hasReviewableLabels = finalLabels.some(label => + ['too-big', 'needs-codeowners'].includes(label) + ); + + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + const botReviews = reviews.filter(review => + review.user.type === 'Bot' && + review.state === 'CHANGES_REQUESTED' && + review.body && review.body.includes(BOT_COMMENT_MARKER) + ); + + if (hasReviewableLabels) { + const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`; + + if (botReviews.length > 0) { + // Update existing review + await github.rest.pulls.updateReview({ + owner, + repo, + pull_number: pr_number, + review_id: botReviews[0].id, + body: reviewBody + }); + console.log('Updated existing bot review'); + } else { + // Create new review + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pr_number, + body: reviewBody, + event: 'REQUEST_CHANGES' + }); + console.log('Created new bot review'); + } + } else if (botReviews.length > 0) { + // Dismiss existing reviews + for (const review of botReviews) { + try { + await github.rest.pulls.dismissReview({ + owner, + repo, + pull_number: pr_number, + review_id: review.id, + message: 'Review dismissed: All requirements have been met' + }); + console.log(`Dismissed bot review ${review.id}`); + } catch (error) { + console.log(`Failed to dismiss review ${review.id}:`, error.message); + } + } + } + } + + // Main execution + const apiData = await fetchApiData(); + const baseRef = context.payload.pull_request.base.ref; + + // Early exit for non-dev branches + if (baseRef !== 'dev') { + const branchLabels = await detectMergeBranch(); + const finalLabels = Array.from(branchLabels); + console.log('Computed labels (merge branch only):', finalLabels.join(', ')); - // Add new labels + // Apply labels if (finalLabels.length > 0) { - console.log(`Adding labels: ${finalLabels.join(', ')}`); await github.rest.issues.addLabels({ owner, repo, @@ -150,13 +492,9 @@ jobs: }); } - // Remove old managed labels that are no longer needed - const labelsToRemove = managedLabels.filter(label => - !finalLabels.includes(label) - ); - + // Remove old managed labels + const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label)); for (const label of labelsToRemove) { - console.log(`Removing label: ${label}`); try { await github.rest.issues.removeLabel({ owner, @@ -169,324 +507,70 @@ jobs: } } - return; // Exit early, don't process other strategies + return; } - // Strategy: Component and Platform labeling - const componentRegex = /^esphome\/components\/([^\/]+)\//; - const targetPlatformRegex = new RegExp(`^esphome\/components\/(${targetPlatforms.join('|')})/`); + // Run all strategies + const [ + branchLabels, + componentLabels, + newComponentLabels, + newPlatformLabels, + coreLabels, + sizeLabels, + dashboardLabels, + actionsLabels, + codeOwnerLabels, + testLabels + ] = await Promise.all([ + detectMergeBranch(), + detectComponentPlatforms(apiData), + detectNewComponents(), + detectNewPlatforms(apiData), + detectCoreChanges(), + detectPRSize(), + detectDashboardChanges(), + detectGitHubActionsChanges(), + detectCodeOwner(), + detectTests() + ]); - for (const file of changedFiles) { - // Check for component changes - const componentMatch = file.match(componentRegex); - if (componentMatch) { - const component = componentMatch[1]; - labels.add(`component: ${component}`); - } + // Combine all labels + const allLabels = new Set([ + ...branchLabels, + ...componentLabels, + ...newComponentLabels, + ...newPlatformLabels, + ...coreLabels, + ...sizeLabels, + ...dashboardLabels, + ...actionsLabels, + ...codeOwnerLabels, + ...testLabels + ]); - // Check for target platform changes - const platformMatch = file.match(targetPlatformRegex); - if (platformMatch) { - const targetPlatform = platformMatch[1]; - labels.add(`platform: ${targetPlatform}`); - } + // Detect requirements based on all other labels + const requirementLabels = await detectRequirements(allLabels); + for (const label of requirementLabels) { + allLabels.add(label); } - // Get PR files for new component/platform detection - const { data: prFiles } = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number: pr_number - }); + let finalLabels = Array.from(allLabels); - const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); + // Handle too many labels + const isMegaPR = currentLabels.includes('mega-pr'); + const tooManyLabels = finalLabels.length > MAX_LABELS; - // Calculate changes excluding root tests directory for too-big calculation - const testChanges = prFiles - .filter(file => file.filename.startsWith('tests/')) - .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); - - const nonTestChanges = totalChanges - testChanges; - console.log(`Test changes: ${testChanges}, Non-test changes: ${nonTestChanges}`); - - // Strategy: New Component detection - for (const file of addedFiles) { - // Check for new component files: esphome/components/{component}/__init__.py - const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/); - if (componentMatch) { - try { - // Read the content directly from the filesystem since we have it checked out - const content = fs.readFileSync(file, 'utf8'); - - // Strategy: New Target Platform detection - if (content.includes('IS_TARGET_PLATFORM = True')) { - labels.add('new-target-platform'); - } - labels.add('new-component'); - } catch (error) { - console.log(`Failed to read content of ${file}:`, error.message); - // Fallback: assume it's a new component if we can't read the content - labels.add('new-component'); - } - } + if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { + finalLabels = ['too-big']; } - // Strategy: New Platform detection - for (const file of addedFiles) { - // Check for new platform files: esphome/components/{component}/{platform}.py - const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/); - if (platformFileMatch) { - const [, component, platform] = platformFileMatch; - if (platformComponents.includes(platform)) { - labels.add('new-platform'); - } - } - - // Check for new platform files: esphome/components/{component}/{platform}/__init__.py - const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/); - if (platformDirMatch) { - const [, component, platform] = platformDirMatch; - if (platformComponents.includes(platform)) { - labels.add('new-platform'); - } - } - } - - const coreFiles = changedFiles.filter(file => - file.startsWith('esphome/core/') || - (file.startsWith('esphome/') && file.split('/').length === 2) - ); - - if (coreFiles.length > 0) { - labels.add('core'); - } - - // Strategy: Small PR detection - if (totalChanges <= smallPrThreshold) { - labels.add('small-pr'); - } - - // Strategy: Dashboard changes - const dashboardFiles = changedFiles.filter(file => - file.startsWith('esphome/dashboard/') || - file.startsWith('esphome/components/dashboard_import/') - ); - - if (dashboardFiles.length > 0) { - labels.add('dashboard'); - } - - // Strategy: GitHub Actions changes - const githubActionsFiles = changedFiles.filter(file => - file.startsWith('.github/workflows/') - ); - - if (githubActionsFiles.length > 0) { - labels.add('github-actions'); - } - - // Strategy: Code Owner detection - try { - // Fetch CODEOWNERS file from the repository (in case it was changed in this PR) - const { data: codeownersFile } = await github.rest.repos.getContent({ - owner, - repo, - path: 'CODEOWNERS', - }); - - const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); - const prAuthor = context.payload.pull_request.user.login; - - // Parse CODEOWNERS file - const codeownersLines = codeownersContent.split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - let isCodeOwner = false; - - // Precompile CODEOWNERS patterns into regex objects - const codeownersRegexes = codeownersLines.map(line => { - const parts = line.split(/\s+/); - const pattern = parts[0]; - const owners = parts.slice(1); - - let regex; - if (pattern.endsWith('*')) { - // Directory pattern like "esphome/components/api/*" - const dir = pattern.slice(0, -1); - regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); - } else if (pattern.includes('*')) { - // Glob pattern - const regexPattern = pattern - .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - .replace(/\\*/g, '.*'); - regex = new RegExp(`^${regexPattern}$`); - } else { - // Exact match - regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); - } - - return { regex, owners }; - }); - - for (const file of changedFiles) { - for (const { regex, owners } of codeownersRegexes) { - if (regex.test(file)) { - // Check if PR author is in the owners list - if (owners.some(owner => owner === `@${prAuthor}`)) { - isCodeOwner = true; - break; - } - } - } - if (isCodeOwner) break; - } - - if (isCodeOwner) { - labels.add('by-code-owner'); - } - } catch (error) { - console.log('Failed to read or parse CODEOWNERS file:', error.message); - } - - // Strategy: Test detection - const testFiles = changedFiles.filter(file => - file.startsWith('tests/') - ); - - if (testFiles.length > 0) { - labels.add('has-tests'); - } else { - // Only check for needs-tests if this is a new component or new platform - if (labels.has('new-component') || labels.has('new-platform')) { - labels.add('needs-tests'); - } - } - - // Strategy: Documentation check for new components/platforms - if (labels.has('new-component') || labels.has('new-platform')) { - const prBody = context.payload.pull_request.body || ''; - - // Look for documentation PR links - // Patterns to match: - // - https://github.com/esphome/esphome-docs/pull/1234 - // - esphome/esphome-docs#1234 - const docsPrPatterns = [ - /https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/, - /esphome\/esphome-docs#\d+/ - ]; - - const hasDocsLink = docsPrPatterns.some(pattern => pattern.test(prBody)); - - if (!hasDocsLink) { - labels.add('needs-docs'); - } - } - - // Convert Set to Array - let finalLabels = Array.from(labels); - console.log('Computed labels:', finalLabels.join(', ')); - // Check if PR has mega-pr label - const isMegaPR = currentLabels.includes('mega-pr'); + // Handle reviews + await handleReviews(finalLabels); - // Check if PR is too big (either too many labels or too many line changes) - const tooManyLabels = finalLabels.length > maxLabels; - const tooManyChanges = nonTestChanges > tooBigThreshold; - - if ((tooManyLabels || tooManyChanges) && !isMegaPR) { - const originalLength = finalLabels.length; - console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges} (non-test: ${nonTestChanges})`); - - // Get all reviews on this PR to check for existing bot reviews - const { data: reviews } = await github.rest.pulls.listReviews({ - owner, - repo, - pull_number: pr_number - }); - - // Check if there's already an active bot review requesting changes - const existingBotReview = reviews.find(review => - review.user.type === 'Bot' && - review.state === 'CHANGES_REQUESTED' && - review.body && review.body.includes(BOT_COMMENT_MARKER) - ); - - // If too big due to line changes only, keep original labels and add too-big - // If too big due to too many labels, replace with just too-big - if (tooManyChanges && !tooManyLabels) { - finalLabels.push('too-big'); - } else { - finalLabels = ['too-big']; - } - - // Only create a new review if there isn't already an active bot review - if (!existingBotReview) { - // Create appropriate review message - let reviewBody; - if (tooManyLabels && tooManyChanges) { - reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; - } else if (tooManyLabels) { - reviewBody = `${BOT_COMMENT_MARKER}\nThis PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; - } else { - reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${nonTestChanges} line changes (excluding tests). Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; - } - - // Request changes on the PR - await github.rest.pulls.createReview({ - owner, - repo, - pull_number: pr_number, - body: reviewBody, - event: 'REQUEST_CHANGES' - }); - console.log('Created new "too big" review requesting changes'); - } else { - console.log('Skipping review creation - existing bot review already requesting changes'); - } - } else { - // Check if PR was previously too big but is now acceptable - const wasPreviouslyTooBig = currentLabels.includes('too-big'); - - if (wasPreviouslyTooBig || isMegaPR) { - console.log('PR is no longer too big or has mega-pr label - dismissing bot reviews'); - - // Get all reviews on this PR to find reviews to dismiss - const { data: reviews } = await github.rest.pulls.listReviews({ - owner, - repo, - pull_number: pr_number - }); - - // Find bot reviews that requested changes - const botReviews = reviews.filter(review => - review.user.type === 'Bot' && - review.state === 'CHANGES_REQUESTED' && - review.body && review.body.includes(BOT_COMMENT_MARKER) - ); - - // Dismiss bot reviews - for (const review of botReviews) { - try { - await github.rest.pulls.dismissReview({ - owner, - repo, - pull_number: pr_number, - review_id: review.id, - message: isMegaPR ? - 'Review dismissed: mega-pr label was added' : - 'Review dismissed: PR size is now acceptable' - }); - console.log(`Dismissed review ${review.id}`); - } catch (error) { - console.log(`Failed to dismiss review ${review.id}:`, error.message); - } - } - } - } - - // Add new labels + // Apply labels if (finalLabels.length > 0) { console.log(`Adding labels: ${finalLabels.join(', ')}`); await github.rest.issues.addLabels({ @@ -497,11 +581,8 @@ jobs: }); } - // Remove old managed labels that are no longer needed - const labelsToRemove = managedLabels.filter(label => - !finalLabels.includes(label) - ); - + // Remove old managed labels + const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label)); for (const label of labelsToRemove) { console.log(`Removing label: ${label}`); try { From ba72298a638a2240d0186ffbfaac5e21c1caf402 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:21:59 +1000 Subject: [PATCH 112/125] [factory_reset] Allow factory reset by rapid power cycle (#9749) --- esphome/components/factory_reset/__init__.py | 92 +++++++++++++++++++ .../factory_reset/factory_reset.cpp | 76 +++++++++++++++ .../components/factory_reset/factory_reset.h | 43 +++++++++ tests/components/factory_reset/common.yaml | 4 + .../factory_reset/test.bk72xx-ard.yaml | 1 + .../factory_reset/test.esp8266-ard.yaml | 3 + .../factory_reset/test.rp2040-ard.yaml | 4 +- 7 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 esphome/components/factory_reset/factory_reset.cpp create mode 100644 esphome/components/factory_reset/factory_reset.h create mode 100644 tests/components/factory_reset/test.bk72xx-ard.yaml diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index f1bcfd8c55..f3cefe6970 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -1,5 +1,97 @@ +from esphome.automation import Trigger, build_automation, validate_automation import esphome.codegen as cg +from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266 +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_TRIGGER_ID, + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_LN882X, + PLATFORM_RTL87XX, +) +from esphome.core import CORE +from esphome.final_validate import full_config CODEOWNERS = ["@anatoly-savchenkov"] factory_reset_ns = cg.esphome_ns.namespace("factory_reset") +FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component) +FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component) + +CONF_MAX_DELAY = "max_delay" +CONF_RESETS_REQUIRED = "resets_required" +CONF_ON_INCREMENT = "on_increment" + + +def _validate(config): + if CONF_RESETS_REQUIRED in config: + return cv.only_on( + [ + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + )(config) + + if CONF_ON_INCREMENT in config: + raise cv.Invalid( + f"'{CONF_ON_INCREMENT}' requires a value for '{CONF_RESETS_REQUIRED}'" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FactoryResetComponent), + cv.Optional(CONF_MAX_DELAY, default="10s"): cv.All( + cv.positive_time_period_seconds, + cv.Range(min=cv.TimePeriod(milliseconds=1000)), + ), + cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int, + cv.Optional(CONF_ON_INCREMENT): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + _validate, +) + + +def _final_validate(config): + if CORE.is_esp8266 and CONF_RESETS_REQUIRED in config: + fconfig = full_config.get() + if not fconfig.get_config_for_path([KEY_ESP8266, CONF_RESTORE_FROM_FLASH]): + raise cv.Invalid( + "'resets_required' needs 'restore_from_flash' to be enabled in the 'esp8266' configuration" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + if reset_count := config.get(CONF_RESETS_REQUIRED): + var = cg.new_Pvariable( + config[CONF_ID], + reset_count, + config[CONF_MAX_DELAY].total_milliseconds, + ) + await cg.register_component(var, config) + for conf in config.get(CONF_ON_INCREMENT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await build_automation( + trigger, + [ + (cg.uint8, "x"), + (cg.uint8, "target"), + ], + conf, + ) diff --git a/esphome/components/factory_reset/factory_reset.cpp b/esphome/components/factory_reset/factory_reset.cpp new file mode 100644 index 0000000000..c900759d90 --- /dev/null +++ b/esphome/components/factory_reset/factory_reset.cpp @@ -0,0 +1,76 @@ +#include "factory_reset.h" + +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include + +#if !defined(USE_RP2040) && !defined(USE_HOST) + +namespace esphome { +namespace factory_reset { + +static const char *const TAG = "factory_reset"; +static const uint32_t POWER_CYCLES_KEY = 0xFA5C0DE; + +static bool was_power_cycled() { +#ifdef USE_ESP32 + return esp_reset_reason() == ESP_RST_POWERON; +#endif +#ifdef USE_ESP8266 + auto reset_reason = EspClass::getResetReason(); + return strcasecmp(reset_reason.c_str(), "power On") == 0 || strcasecmp(reset_reason.c_str(), "external system") == 0; +#endif +#ifdef USE_LIBRETINY + auto reason = lt_get_reboot_reason(); + return reason == REBOOT_REASON_POWER || reason == REBOOT_REASON_HARDWARE; +#endif +} + +void FactoryResetComponent::dump_config() { + uint8_t count = 0; + this->flash_.load(&count); + ESP_LOGCONFIG(TAG, "Factory Reset by Reset:"); + ESP_LOGCONFIG(TAG, + " Max interval between resets %" PRIu32 " seconds\n" + " Current count: %u\n" + " Factory reset after %u resets", + this->max_interval_ / 1000, count, this->required_count_); +} + +void FactoryResetComponent::save_(uint8_t count) { + this->flash_.save(&count); + global_preferences->sync(); + this->defer([count, this] { this->increment_callback_.call(count, this->required_count_); }); +} + +void FactoryResetComponent::setup() { + this->flash_ = global_preferences->make_preference(POWER_CYCLES_KEY, true); + if (was_power_cycled()) { + uint8_t count = 0; + this->flash_.load(&count); + // this is a power on reset or external system reset + count++; + if (count == this->required_count_) { + ESP_LOGW(TAG, "Reset count reached, factory resetting"); + global_preferences->reset(); + // delay to allow log to be sent + delay(100); // NOLINT + App.safe_reboot(); // should not return + } + this->save_(count); + ESP_LOGD(TAG, "Power on reset detected, incremented count to %u", count); + this->set_timeout(this->max_interval_, [this]() { + ESP_LOGD(TAG, "No reset in the last %" PRIu32 " seconds, resetting count", this->max_interval_ / 1000); + this->save_(0); // reset count + }); + } else { + this->save_(0); // reset count if not a power cycle + } +} + +} // namespace factory_reset +} // namespace esphome + +#endif // !defined(USE_RP2040) && !defined(USE_HOST) diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h new file mode 100644 index 0000000000..80942b29bd --- /dev/null +++ b/esphome/components/factory_reset/factory_reset.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/core/preferences.h" +#if !defined(USE_RP2040) && !defined(USE_HOST) + +#ifdef USE_ESP32 +#include +#endif + +namespace esphome { +namespace factory_reset { +class FactoryResetComponent : public Component { + public: + FactoryResetComponent(uint8_t required_count, uint32_t max_interval) + : required_count_(required_count), max_interval_(max_interval) {} + + void dump_config() override; + void setup() override; + void add_increment_callback(std::function &&callback) { + this->increment_callback_.add(std::move(callback)); + } + + protected: + ~FactoryResetComponent() = default; + void save_(uint8_t count); + ESPPreferenceObject flash_{}; // saves the number of fast power cycles + uint8_t required_count_; // The number of boot attempts before fast boot is enabled + uint32_t max_interval_; // max interval between power cycles + CallbackManager increment_callback_{}; +}; + +class FastBootTrigger : public Trigger { + public: + explicit FastBootTrigger(FactoryResetComponent *parent) { + parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); }); + } +}; +} // namespace factory_reset +} // namespace esphome + +#endif // !defined(USE_RP2040) && !defined(USE_HOST) diff --git a/tests/components/factory_reset/common.yaml b/tests/components/factory_reset/common.yaml index ad3abd603e..7617dc4b81 100644 --- a/tests/components/factory_reset/common.yaml +++ b/tests/components/factory_reset/common.yaml @@ -1,3 +1,7 @@ button: - platform: factory_reset name: Reset to Factory Default Settings + +factory_reset: + resets_required: 5 + max_delay: 10s diff --git a/tests/components/factory_reset/test.bk72xx-ard.yaml b/tests/components/factory_reset/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/factory_reset/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp8266-ard.yaml b/tests/components/factory_reset/test.esp8266-ard.yaml index dade44d145..591a319b81 100644 --- a/tests/components/factory_reset/test.esp8266-ard.yaml +++ b/tests/components/factory_reset/test.esp8266-ard.yaml @@ -1 +1,4 @@ +esp8266: + restore_from_flash: true + <<: !include common.yaml diff --git a/tests/components/factory_reset/test.rp2040-ard.yaml b/tests/components/factory_reset/test.rp2040-ard.yaml index dade44d145..ad3abd603e 100644 --- a/tests/components/factory_reset/test.rp2040-ard.yaml +++ b/tests/components/factory_reset/test.rp2040-ard.yaml @@ -1 +1,3 @@ -<<: !include common.yaml +button: + - platform: factory_reset + name: Reset to Factory Default Settings From 729f20d765d3d789d0fa5c64c3c99b8bcae198ca Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 24 Jul 2025 06:23:42 -0500 Subject: [PATCH 113/125] [gps] Patches to build on IDF, other optimizations (#9728) --- .clang-tidy.hash | 2 +- esphome/components/gps/__init__.py | 8 +- esphome/components/gps/gps.cpp | 87 +++++++++++---------- esphome/components/gps/gps.h | 12 +-- esphome/components/gps/time/gps_time.cpp | 12 +-- esphome/components/gps/time/gps_time.h | 11 +-- platformio.ini | 2 +- tests/components/gps/test.esp32-c3-idf.yaml | 5 ++ tests/components/gps/test.esp32-idf.yaml | 5 ++ 9 files changed, 73 insertions(+), 71 deletions(-) create mode 100644 tests/components/gps/test.esp32-c3-idf.yaml create mode 100644 tests/components/gps/test.esp32-idf.yaml diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 9efe5b2270..256b128cd4 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -7920671c938a5ea6a11ac4594204b5ec8f38d579c962bf1f185e8d5e3ad879be +8016e9cbe199bf1a65a0c90e7a37d768ec774f4fff70de530a9b55708af71e74 diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 7ccd82dec3..a872cf7015 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -84,7 +84,6 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.polling_component_schema("20s")) .extend(uart.UART_DEVICE_SCHEMA), - cv.only_with_arduino, ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True) @@ -123,4 +122,9 @@ async def to_code(config): cg.add(var.set_hdop_sensor(sens)) # https://platformio.org/lib/show/1655/TinyGPSPlus - cg.add_library("mikalhart/TinyGPSPlus", "1.1.0") + # Using fork of TinyGPSPlus patched to build on ESP-IDF + cg.add_library( + "TinyGPSPlus", + None, + "https://github.com/esphome/TinyGPSPlus.git#v1.1.0", + ) diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 9dcb351b39..cbbd36887b 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "gps.h" #include "esphome/core/log.h" @@ -22,73 +20,76 @@ void GPS::dump_config() { } void GPS::update() { - if (this->latitude_sensor_ != nullptr) + if (this->latitude_sensor_ != nullptr) { this->latitude_sensor_->publish_state(this->latitude_); + } - if (this->longitude_sensor_ != nullptr) + if (this->longitude_sensor_ != nullptr) { this->longitude_sensor_->publish_state(this->longitude_); + } - if (this->speed_sensor_ != nullptr) + if (this->speed_sensor_ != nullptr) { this->speed_sensor_->publish_state(this->speed_); + } - if (this->course_sensor_ != nullptr) + if (this->course_sensor_ != nullptr) { this->course_sensor_->publish_state(this->course_); + } - if (this->altitude_sensor_ != nullptr) + if (this->altitude_sensor_ != nullptr) { this->altitude_sensor_->publish_state(this->altitude_); + } - if (this->satellites_sensor_ != nullptr) + if (this->satellites_sensor_ != nullptr) { this->satellites_sensor_->publish_state(this->satellites_); + } - if (this->hdop_sensor_ != nullptr) + if (this->hdop_sensor_ != nullptr) { this->hdop_sensor_->publish_state(this->hdop_); + } } void GPS::loop() { while (this->available() > 0 && !this->has_time_) { - if (this->tiny_gps_.encode(this->read())) { - if (this->tiny_gps_.location.isUpdated()) { - this->latitude_ = this->tiny_gps_.location.lat(); - this->longitude_ = this->tiny_gps_.location.lng(); + if (!this->tiny_gps_.encode(this->read())) { + return; + } + if (this->tiny_gps_.location.isUpdated()) { + this->latitude_ = this->tiny_gps_.location.lat(); + this->longitude_ = this->tiny_gps_.location.lng(); + ESP_LOGV(TAG, "Latitude, Longitude: %.6f°, %.6f°", this->latitude_, this->longitude_); + } - ESP_LOGD(TAG, "Location:"); - ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_); - ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_); - } + if (this->tiny_gps_.speed.isUpdated()) { + this->speed_ = this->tiny_gps_.speed.kmph(); + ESP_LOGV(TAG, "Speed: %.3f km/h", this->speed_); + } - if (this->tiny_gps_.speed.isUpdated()) { - this->speed_ = this->tiny_gps_.speed.kmph(); - ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_); - } + if (this->tiny_gps_.course.isUpdated()) { + this->course_ = this->tiny_gps_.course.deg(); + ESP_LOGV(TAG, "Course: %.2f°", this->course_); + } - if (this->tiny_gps_.course.isUpdated()) { - this->course_ = this->tiny_gps_.course.deg(); - ESP_LOGD(TAG, "Course: %.2f °", this->course_); - } + if (this->tiny_gps_.altitude.isUpdated()) { + this->altitude_ = this->tiny_gps_.altitude.meters(); + ESP_LOGV(TAG, "Altitude: %.2f m", this->altitude_); + } - if (this->tiny_gps_.altitude.isUpdated()) { - this->altitude_ = this->tiny_gps_.altitude.meters(); - ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_); - } + if (this->tiny_gps_.satellites.isUpdated()) { + this->satellites_ = this->tiny_gps_.satellites.value(); + ESP_LOGV(TAG, "Satellites: %d", this->satellites_); + } - if (this->tiny_gps_.satellites.isUpdated()) { - this->satellites_ = this->tiny_gps_.satellites.value(); - ESP_LOGD(TAG, "Satellites: %d", this->satellites_); - } + if (this->tiny_gps_.hdop.isUpdated()) { + this->hdop_ = this->tiny_gps_.hdop.hdop(); + ESP_LOGV(TAG, "HDOP: %.3f", this->hdop_); + } - if (this->tiny_gps_.hdop.isUpdated()) { - this->hdop_ = this->tiny_gps_.hdop.hdop(); - ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_); - } - - for (auto *listener : this->listeners_) { - listener->on_update(this->tiny_gps_); - } + for (auto *listener : this->listeners_) { + listener->on_update(this->tiny_gps_); } } } } // namespace gps } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 7bc23ed1e0..36923c68be 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -1,10 +1,8 @@ #pragma once -#ifdef USE_ARDUINO - -#include "esphome/core/component.h" -#include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" #include #include @@ -53,8 +51,9 @@ class GPS : public PollingComponent, public uart::UARTDevice { float speed_{NAN}; float course_{NAN}; float altitude_{NAN}; - uint16_t satellites_{0}; float hdop_{NAN}; + uint16_t satellites_{0}; + bool has_time_{false}; sensor::Sensor *latitude_sensor_{nullptr}; sensor::Sensor *longitude_sensor_{nullptr}; @@ -64,12 +63,9 @@ class GPS : public PollingComponent, public uart::UARTDevice { sensor::Sensor *satellites_sensor_{nullptr}; sensor::Sensor *hdop_sensor_{nullptr}; - bool has_time_{false}; TinyGPSPlus tiny_gps_; std::vector listeners_{}; }; } // namespace gps } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index 0f1b989f77..cff8c1fb07 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "gps_time.h" #include "esphome/core/log.h" @@ -9,12 +7,10 @@ namespace gps { static const char *const TAG = "gps.time"; void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { - if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid()) - return; - if (!tiny_gps.time.isUpdated() || !tiny_gps.date.isUpdated()) - return; - if (tiny_gps.date.year() < 2019) + if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid() || !tiny_gps.time.isUpdated() || + !tiny_gps.date.isUpdated() || tiny_gps.date.year() < 2025) { return; + } ESPTime val{}; val.year = tiny_gps.date.year(); @@ -34,5 +30,3 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { } // namespace gps } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/gps/time/gps_time.h b/esphome/components/gps/time/gps_time.h index d0d1db83b5..a8414f0015 100644 --- a/esphome/components/gps/time/gps_time.h +++ b/esphome/components/gps/time/gps_time.h @@ -1,10 +1,8 @@ #pragma once -#ifdef USE_ARDUINO - -#include "esphome/core/component.h" -#include "esphome/components/time/real_time_clock.h" #include "esphome/components/gps/gps.h" +#include "esphome/components/time/real_time_clock.h" +#include "esphome/core/component.h" namespace esphome { namespace gps { @@ -13,8 +11,9 @@ class GPSTime : public time::RealTimeClock, public GPSListener { public: void update() override { this->from_tiny_gps_(this->get_tiny_gps()); }; void on_update(TinyGPSPlus &tiny_gps) override { - if (!this->has_time_) + if (!this->has_time_) { this->from_tiny_gps_(tiny_gps); + } } protected: @@ -24,5 +23,3 @@ class GPSTime : public time::RealTimeClock, public GPSListener { } // namespace gps } // namespace esphome - -#endif // USE_ARDUINO diff --git a/platformio.ini b/platformio.ini index 350f220853..1b37c9aba4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -73,7 +73,7 @@ lib_deps = heman/AsyncMqttClient-esphome@1.0.0 ; mqtt ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base - mikalhart/TinyGPSPlus@1.1.0 ; gps + https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr diff --git a/tests/components/gps/test.esp32-c3-idf.yaml b/tests/components/gps/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/gps/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/gps/test.esp32-idf.yaml b/tests/components/gps/test.esp32-idf.yaml new file mode 100644 index 0000000000..811f6b72a6 --- /dev/null +++ b/tests/components/gps/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO12 + rx_pin: GPIO14 + +<<: !include common.yaml From 73f58dfe809df1cee24fbef7802e0e775acaaf4e Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 24 Jul 2025 13:26:21 +0200 Subject: [PATCH 114/125] [sound_level] fix spelling mistake (#9843) --- esphome/components/sound_level/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sound_level/sensor.py b/esphome/components/sound_level/sensor.py index 292efadab8..8ca0feccc0 100644 --- a/esphome/components/sound_level/sensor.py +++ b/esphome/components/sound_level/sensor.py @@ -12,7 +12,7 @@ from esphome.const import ( UNIT_DECIBEL, ) -AUTOLOAD = ["audio"] +AUTO_LOAD = ["audio"] CODEOWNERS = ["@kahrendt"] DEPENDENCIES = ["microphone"] From 27119ef7ade501d91dc853abfc878b41ea5781b7 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Thu, 24 Jul 2025 14:43:44 +0200 Subject: [PATCH 115/125] rc522: fix buffer overflow in UID/buffer formatting helpers (#9375) --- esphome/components/rc522/rc522.cpp | 40 +++++++----------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 018b714190..fa8564f614 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -1,4 +1,5 @@ #include "rc522.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" // Based on: @@ -13,30 +14,6 @@ static const char *const TAG = "rc522"; static const uint8_t RESET_COUNT = 5; -std::string format_buffer(uint8_t *b, uint8_t len) { - char buf[32]; - int offset = 0; - for (uint8_t i = 0; i < len; i++) { - const char *format = "%02X"; - if (i + 1 < len) - format = "%02X-"; - offset += sprintf(buf + offset, format, b[i]); - } - return std::string(buf); -} - -std::string format_uid(std::vector &uid) { - char buf[32]; - int offset = 0; - for (size_t i = 0; i < uid.size(); i++) { - const char *format = "%02X"; - if (i + 1 < uid.size()) - format = "%02X-"; - offset += sprintf(buf + offset, format, uid[i]); - } - return std::string(buf); -} - void RC522::setup() { state_ = STATE_SETUP; // Pull device out of power down / reset state. @@ -215,7 +192,7 @@ void RC522::loop() { ESP_LOGV(TAG, "STATE_READ_SERIAL_DONE -> TIMEOUT (no tag present) %d", status); } else { ESP_LOGW(TAG, "Unexpected response. Read status is %d. Read bytes: %d (%s)", status, back_length_, - format_buffer(buffer_, 9).c_str()); + format_hex_pretty(buffer_, back_length_, '-', false).c_str()); } state_ = STATE_DONE; @@ -239,7 +216,7 @@ void RC522::loop() { std::vector rfid_uid(std::begin(uid_buffer_), std::begin(uid_buffer_) + uid_idx_); uid_idx_ = 0; - // ESP_LOGD(TAG, "Processing '%s'", format_uid(rfid_uid).c_str()); + // ESP_LOGD(TAG, "Processing '%s'", format_hex_pretty(rfid_uid, '-', false).c_str()); pcd_antenna_off_(); state_ = STATE_INIT; // scan again on next update bool report = true; @@ -260,13 +237,13 @@ void RC522::loop() { trigger->process(rfid_uid); if (report) { - ESP_LOGD(TAG, "Found new tag '%s'", format_uid(rfid_uid).c_str()); + ESP_LOGD(TAG, "Found new tag '%s'", format_hex_pretty(rfid_uid, '-', false).c_str()); } break; } case STATE_DONE: { if (!this->current_uid_.empty()) { - ESP_LOGV(TAG, "Tag '%s' removed", format_uid(this->current_uid_).c_str()); + ESP_LOGV(TAG, "Tag '%s' removed", format_hex_pretty(this->current_uid_, '-', false).c_str()); for (auto *trigger : this->triggers_ontagremoved_) trigger->process(this->current_uid_); } @@ -361,7 +338,7 @@ void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to * @return STATUS_OK on success, STATUS_??? otherwise. */ void RC522::pcd_transceive_data_(uint8_t send_len) { - ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_buffer(buffer_, send_len).c_str()); + ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_hex_pretty(buffer_, send_len, '-', false).c_str()); delayMicroseconds(1000); // we need 1 ms delay between antenna on and those communication commands send_len_ = send_len; // Prepare values for BitFramingReg @@ -435,7 +412,8 @@ RC522::StatusCode RC522::await_transceive_() { error_reg_value); // TODO: is this always due to collissions? return STATUS_ERROR; } - ESP_LOGV(TAG, "received %d bytes: %s", back_length_, format_buffer(buffer_ + send_len_, back_length_).c_str()); + ESP_LOGV(TAG, "received %d bytes: %s", back_length_, + format_hex_pretty(buffer_ + send_len_, back_length_, '-', false).c_str()); return STATUS_OK; } @@ -499,7 +477,7 @@ bool RC522BinarySensor::process(std::vector &data) { this->found_ = result; return result; } -void RC522Trigger::process(std::vector &data) { this->trigger(format_uid(data)); } +void RC522Trigger::process(std::vector &data) { this->trigger(format_hex_pretty(data, '-', false)); } } // namespace rc522 } // namespace esphome From 06eba96fdce7a69afc0d5ba2ca16542383768ef5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:12:22 -1000 Subject: [PATCH 116/125] Bump ruff from 0.12.4 to 0.12.5 (#9871) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5b45f27aa..ae3858c0ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.4 + rev: v0.12.5 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index ad5e4a3e3d..a87a4bafac 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.4 # also change in .pre-commit-config.yaml when updating +ruff==0.12.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From cb87f156d0b6da523cd039ade0e54e5ff95c0f36 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 24 Jul 2025 23:10:03 -0500 Subject: [PATCH 117/125] [platformio.ini] Move GPS to common lib_deps (#9883) --- .clang-tidy.hash | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 256b128cd4..47f68ff022 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -8016e9cbe199bf1a65a0c90e7a37d768ec774f4fff70de530a9b55708af71e74 +a0bc970a2edcf618945646316f28238c53accb6963bad3726148614d2509ede8 diff --git a/platformio.ini b/platformio.ini index 1b37c9aba4..0aaa4a6346 100644 --- a/platformio.ini +++ b/platformio.ini @@ -40,6 +40,7 @@ lib_deps = functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier kikuchan98/pngle@1.1.0 ; online_image + https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps ; Using the repository directly, otherwise ESP-IDF can't use the library https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image ; This is using the repository until a new release is published to PlatformIO @@ -73,7 +74,6 @@ lib_deps = heman/AsyncMqttClient-esphome@1.0.0 ; mqtt ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base - https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr From ffebd30033100acc15371a1a61dc42758dd65258 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Jul 2025 20:26:08 -1000 Subject: [PATCH 118/125] [ruff] Enable SIM rules and fix code simplification violations (#9872) --- esphome/__main__.py | 4 +- esphome/components/api/client.py | 6 +- esphome/components/canbus/__init__.py | 5 +- esphome/components/esp32/__init__.py | 18 ++- .../components/esp32_ble_server/__init__.py | 22 +-- esphome/components/esp32_touch/__init__.py | 5 +- esphome/components/esp8266/__init__.py | 2 +- esphome/components/ethernet/__init__.py | 2 +- esphome/components/fastled_spi/light.py | 4 +- esphome/components/graph/__init__.py | 2 +- esphome/components/http_request/__init__.py | 5 +- esphome/components/hydreon_rgxx/sensor.py | 9 +- esphome/components/i2s_audio/__init__.py | 5 +- .../i2s_audio/microphone/__init__.py | 10 +- esphome/components/ili9xxx/display.py | 7 +- esphome/components/image/__init__.py | 10 +- esphome/components/improv_serial/__init__.py | 15 +- esphome/components/ina2xx_base/__init__.py | 7 +- esphome/components/ld2450/text_sensor.py | 9 +- esphome/components/light/effects.py | 2 +- esphome/components/logger/__init__.py | 15 +- esphome/components/lvgl/__init__.py | 5 +- esphome/components/matrix_keypad/__init__.py | 7 +- esphome/components/mixer/speaker/__init__.py | 9 +- esphome/components/mlx90393/sensor.py | 12 +- esphome/components/mpr121/__init__.py | 13 +- .../components/opentherm/number/__init__.py | 4 +- esphome/components/openthread/__init__.py | 5 +- .../components/packet_transport/__init__.py | 7 +- esphome/components/pulse_counter/sensor.py | 15 +- esphome/components/qspi_dbi/display.py | 5 +- esphome/components/qwiic_pir/binary_sensor.py | 5 +- esphome/components/remote_base/__init__.py | 11 +- .../components/resampler/speaker/__init__.py | 9 +- esphome/components/rpi_dpi_rgb/display.py | 4 +- .../speaker/media_player/__init__.py | 13 +- esphome/components/spi/__init__.py | 8 +- esphome/components/sprinkler/__init__.py | 8 +- esphome/components/ssd1306_base/__init__.py | 17 ++- esphome/components/st7701s/display.py | 4 +- esphome/components/substitutions/__init__.py | 17 +-- esphome/components/sun/__init__.py | 5 +- esphome/components/sx1509/__init__.py | 12 +- esphome/components/thermostat/climate.py | 8 +- esphome/components/time/__init__.py | 4 +- esphome/components/tuya/climate/__init__.py | 19 ++- esphome/components/tuya/number/__init__.py | 14 +- esphome/components/wifi/__init__.py | 4 +- esphome/config.py | 14 +- esphome/config_validation.py | 6 +- esphome/cpp_generator.py | 5 +- esphome/dashboard/dashboard.py | 5 +- esphome/espota2.py | 5 +- esphome/helpers.py | 4 +- esphome/log.py | 2 +- esphome/mqtt.py | 5 +- esphome/platformio_api.py | 8 +- esphome/util.py | 2 +- esphome/wizard.py | 5 +- esphome/writer.py | 12 +- esphome/yaml_util.py | 9 +- pyproject.toml | 11 +- script/api_protobuf/api_protobuf.py | 6 +- script/build_language_schema.py | 26 ++-- script/helpers.py | 8 +- tests/integration/conftest.py | 24 ++- tests/integration/test_api_custom_services.py | 144 +++++++++--------- tests/integration/test_device_id_in_state.py | 4 +- .../test_scheduler_defer_cancel.py | 12 +- tests/integration/test_scheduler_null_name.py | 56 +++---- .../test_scheduler_rapid_cancellation.py | 7 +- tests/script/test_determine_jobs.py | 44 +++--- 72 files changed, 400 insertions(+), 432 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 658aef4722..cf0fed2d67 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -119,9 +119,7 @@ def mqtt_logging_enabled(mqtt_config): return False if CONF_TOPIC not in log_topic: return False - if log_topic.get(CONF_LEVEL, None) == "NONE": - return False - return True + return log_topic.get(CONF_LEVEL, None) != "NONE" def get_port_type(port): diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 2d4bc37c89..5239e07435 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -14,6 +14,8 @@ with warnings.catch_warnings(): from aioesphomeapi import APIClient, parse_log_message from aioesphomeapi.log_runner import async_run +import contextlib + from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.core import CORE @@ -66,7 +68,5 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: def run_logs(config: dict[str, Any], address: str) -> None: """Run the logs command.""" - try: + with contextlib.suppress(KeyboardInterrupt): asyncio.run(async_run_logs(config, address)) - except KeyboardInterrupt: - pass diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index cdb57fd481..e1de1eb2f2 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -22,9 +22,8 @@ def validate_id(config): if CONF_CAN_ID in config: can_id = config[CONF_CAN_ID] id_ext = config[CONF_USE_EXTENDED_ID] - if not id_ext: - if can_id > 0x7FF: - raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") + if not id_ext and can_id > 0x7FF: + raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") return config diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 5dd2288076..f0a5517185 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -953,14 +953,16 @@ def _write_idf_component_yml(): # Called by writer.py def copy_files(): - if CORE.using_arduino: - if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) + if ( + CORE.using_arduino + and "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] + ): + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) if CORE.using_esp_idf: _write_sdkconfig() _write_idf_component_yml() diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 773445a1a7..19f466eb7b 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -140,20 +140,22 @@ VALUE_TYPES = { def validate_char_on_write(char_config): - if CONF_ON_WRITE in char_config: - if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]: - raise cv.Invalid( - f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set" - ) + if ( + CONF_ON_WRITE in char_config + and not char_config[CONF_WRITE] + and not char_config[CONF_WRITE_NO_RESPONSE] + ): + raise cv.Invalid( + f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set" + ) return char_config def validate_descriptor(desc_config): - if CONF_ON_WRITE in desc_config: - if not desc_config[CONF_WRITE]: - raise cv.Invalid( - f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set" - ) + if CONF_ON_WRITE in desc_config and not desc_config[CONF_WRITE]: + raise cv.Invalid( + f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set" + ) if CONF_MAX_LENGTH not in desc_config: value = desc_config[CONF_VALUE][CONF_DATA] if cg.is_template(value): diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index 224e301683..b6cb19ebb1 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -294,9 +294,8 @@ async def to_code(config): ) ) - if get_esp32_variant() == VARIANT_ESP32: - if CONF_IIR_FILTER in config: - cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) + if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config: + cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3: if CONF_FILTER_MODE in config: diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 0184c25965..33a4149571 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -245,7 +245,7 @@ async def to_code(config): if ver <= cv.Version(2, 3, 0): # No ld script support ld_script = None - if ver <= cv.Version(2, 4, 2): + elif ver <= cv.Version(2, 4, 2): # Old ld script path ld_script = ld_scripts[0] else: diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 619346b914..7a412a643d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -112,7 +112,7 @@ def _is_framework_spi_polling_mode_supported(): return True if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1): return True - if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): + if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103 return True return False if CORE.using_arduino: diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index 81d8c3b32a..e863d33846 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -55,9 +55,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = await fastled_base.new_fastled_light(config) - rgb_order = cg.RawExpression( - config[CONF_RGB_ORDER] if CONF_RGB_ORDER in config else "RGB" - ) + rgb_order = cg.RawExpression(config.get(CONF_RGB_ORDER, "RGB")) data_rate = None if CONF_DATA_RATE in config: diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index 6e8ba44bec..d72fe40dd2 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -116,7 +116,7 @@ GRAPH_SCHEMA = cv.Schema( def _relocate_fields_to_subfolder(config, subfolder, subschema): - fields = [k.schema for k in subschema.schema.keys()] + fields = [k.schema for k in subschema.schema] fields.remove(CONF_ID) if subfolder in config: # Ensure no ambiguous fields in base of config diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 0d32bc97c2..146458f53b 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -70,9 +70,8 @@ def validate_url(value): def validate_ssl_verification(config): error_message = "" - if CORE.is_esp32: - if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]: - error_message = "ESPHome supports certificate verification only via ESP-IDF" + if CORE.is_esp32 and not CORE.using_esp_idf and config[CONF_VERIFY_SSL]: + error_message = "ESPHome supports certificate verification only via ESP-IDF" if CORE.is_rp2040 and config[CONF_VERIFY_SSL]: error_message = "ESPHome does not support certificate verification on RP2040" diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index 6c9f1e2877..f270b72e24 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -66,11 +66,10 @@ PROTOCOL_NAMES = { def _validate(config): for conf, models in SUPPORTED_OPTIONS.items(): - if conf in config: - if config[CONF_MODEL] not in models: - raise cv.Invalid( - f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}" - ) + if conf in config and config[CONF_MODEL] not in models: + raise cv.Invalid( + f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}" + ) return config diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 575b914605..aa0a688fa0 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -243,10 +243,7 @@ def _final_validate(_): def use_legacy(): - if CORE.using_esp_idf: - if not _use_legacy_driver: - return False - return True + return not (CORE.using_esp_idf and not _use_legacy_driver) FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 7bbb94f6e3..0f02ba6c3a 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -44,9 +44,8 @@ PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] def _validate_esp32_variant(config): variant = esp32.get_esp32_variant() if config[CONF_ADC_TYPE] == "external": - if config[CONF_PDM]: - if variant not in PDM_VARIANTS: - raise cv.Invalid(f"{variant} does not support PDM") + if config[CONF_PDM] and variant not in PDM_VARIANTS: + raise cv.Invalid(f"{variant} does not support PDM") return config if config[CONF_ADC_TYPE] == "internal": if variant not in INTERNAL_ADC_VARIANTS: @@ -122,9 +121,8 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config): - if not use_legacy(): - if config[CONF_ADC_TYPE] == "internal": - raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.") + if not use_legacy() and config[CONF_ADC_TYPE] == "internal": + raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.") FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 14b7f15218..9588bccd55 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -138,9 +138,10 @@ def _validate(config): ]: raise cv.Invalid("Selected model can't run on ESP8266.") - if model == "CUSTOM": - if CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config: - raise cv.Invalid("CUSTOM model requires init_sequence and dimensions") + if model == "CUSTOM" and ( + CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config + ): + raise cv.Invalid("CUSTOM model requires init_sequence and dimensions") return config diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index f6d8673a08..99646c9f7e 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import hashlib import io import logging @@ -174,9 +175,8 @@ class ImageGrayscale(ImageEncoder): b = 1 if self.invert_alpha: b ^= 0xFF - if self.transparency == CONF_ALPHA_CHANNEL: - if a != 0xFF: - b = a + if self.transparency == CONF_ALPHA_CHANNEL and a != 0xFF: + b = a self.data[self.index] = b self.index += 1 @@ -672,10 +672,8 @@ async def write_image(config, all_frames=False): invert_alpha = config[CONF_INVERT_ALPHA] frame_count = 1 if all_frames: - try: + with contextlib.suppress(AttributeError): frame_count = image.n_frames - except AttributeError: - pass if frame_count <= 1: _LOGGER.warning("Image file %s has no animation frames", path) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 544af212e0..568b200a85 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -27,14 +27,13 @@ def validate_logger(config): logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") - if CORE.using_esp_idf: - if ( - logger_conf[CONF_HARDWARE_UART] == USB_CDC - and get_esp32_variant() == VARIANT_ESP32S3 - ): - raise cv.Invalid( - "improv_serial does not support the selected logger hardware_uart" - ) + if CORE.using_esp_idf and ( + logger_conf[CONF_HARDWARE_UART] == USB_CDC + and get_esp32_variant() == VARIANT_ESP32S3 + ): + raise cv.Invalid( + "improv_serial does not support the selected logger hardware_uart" + ) return config diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index 7c3d3c8899..ff70f217ec 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -78,11 +78,8 @@ def validate_model_config(config): model = config[CONF_MODEL] for key in config: - if key in SENSOR_MODEL_OPTIONS: - if model not in SENSOR_MODEL_OPTIONS[key]: - raise cv.Invalid( - f"Device model '{model}' does not support '{key}' sensor" - ) + if key in SENSOR_MODEL_OPTIONS and model not in SENSOR_MODEL_OPTIONS[key]: + raise cv.Invalid(f"Device model '{model}' does not support '{key}' sensor") tempco = config[CONF_TEMPERATURE_COEFFICIENT] if tempco > 0 and model not in ["INA228", "INA229"]: diff --git a/esphome/components/ld2450/text_sensor.py b/esphome/components/ld2450/text_sensor.py index 6c11024b89..89b6939a29 100644 --- a/esphome/components/ld2450/text_sensor.py +++ b/esphome/components/ld2450/text_sensor.py @@ -56,7 +56,8 @@ async def to_code(config): sens = await text_sensor.new_text_sensor(mac_address_config) cg.add(ld2450_component.set_mac_text_sensor(sens)) for n in range(MAX_TARGETS): - if direction_conf := config.get(f"target_{n + 1}"): - if direction_config := direction_conf.get(CONF_DIRECTION): - sens = await text_sensor.new_text_sensor(direction_config) - cg.add(ld2450_component.set_direction_text_sensor(n, sens)) + if (direction_conf := config.get(f"target_{n + 1}")) and ( + direction_config := direction_conf.get(CONF_DIRECTION) + ): + sens = await text_sensor.new_text_sensor(direction_config) + cg.add(ld2450_component.set_direction_text_sensor(n, sens)) diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 67c318eb8e..6f878ff5f1 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -526,7 +526,7 @@ def validate_effects(allowed_effects): errors = [] names = set() for i, x in enumerate(value): - key = next(it for it in x.keys()) + key = next(it for it in x) if key not in allowed_effects: errors.append( cv.Invalid( diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index e79396da04..cb338df466 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -346,14 +346,13 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") - if CORE.using_arduino: - if config[CONF_HARDWARE_UART] == USB_CDC: - cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") - if CORE.is_esp32 and get_esp32_variant() in ( - VARIANT_ESP32C3, - VARIANT_ESP32C6, - ): - cg.add_build_flag("-DARDUINO_USB_MODE=1") + if CORE.using_arduino and config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") + if CORE.is_esp32 and get_esp32_variant() in ( + VARIANT_ESP32C3, + VARIANT_ESP32C6, + ): + cg.add_build_flag("-DARDUINO_USB_MODE=1") if CORE.using_esp_idf: if config[CONF_HARDWARE_UART] == USB_CDC: diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index b1879e6314..0cd65d298f 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -201,9 +201,8 @@ def final_validation(configs): multi_conf_validate(configs) global_config = full_config.get() for config in configs: - if pages := config.get(CONF_PAGES): - if all(p[df.CONF_SKIP] for p in pages): - raise cv.Invalid("At least one page must not be skipped") + if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): + raise cv.Invalid("At least one page must not be skipped") for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] display = global_config.get_config_for_path(path) diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index b2bcde98ec..f7a1d622a1 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -28,9 +28,10 @@ CONF_HAS_PULLDOWNS = "has_pulldowns" def check_keys(obj): - if CONF_KEYS in obj: - if len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len(obj[CONF_COLUMNS]): - raise cv.Invalid("The number of key codes must equal the number of buttons") + if CONF_KEYS in obj and len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len( + obj[CONF_COLUMNS] + ): + raise cv.Invalid("The number of key codes must equal the number of buttons") return obj diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index a451f2b7b4..46729f8eda 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -124,11 +124,10 @@ async def to_code(config): if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM): cg.add(var.set_task_stack_in_psram(task_stack_in_psram)) - if task_stack_in_psram: - if config[CONF_TASK_STACK_IN_PSRAM]: - esp32.add_idf_sdkconfig_option( - "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True - ) + if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]: + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) for speaker_config in config[CONF_SOURCE_SPEAKERS]: source_speaker = cg.new_Pvariable(speaker_config[CONF_ID]) diff --git a/esphome/components/mlx90393/sensor.py b/esphome/components/mlx90393/sensor.py index 372bb05bda..d93c379506 100644 --- a/esphome/components/mlx90393/sensor.py +++ b/esphome/components/mlx90393/sensor.py @@ -63,11 +63,13 @@ def _validate(config): raise cv.Invalid( f"{axis}: {CONF_RESOLUTION} cannot be {res} with {CONF_TEMPERATURE_COMPENSATION} enabled" ) - if config[CONF_HALLCONF] == 0xC: - if (config[CONF_OVERSAMPLING], config[CONF_FILTER]) in [(0, 0), (1, 0), (0, 1)]: - raise cv.Invalid( - f"{CONF_OVERSAMPLING}=={config[CONF_OVERSAMPLING]} and {CONF_FILTER}=={config[CONF_FILTER]} not allowed with {CONF_HALLCONF}=={config[CONF_HALLCONF]:#02x}" - ) + if config[CONF_HALLCONF] == 0xC and ( + config[CONF_OVERSAMPLING], + config[CONF_FILTER], + ) in [(0, 0), (1, 0), (0, 1)]: + raise cv.Invalid( + f"{CONF_OVERSAMPLING}=={config[CONF_OVERSAMPLING]} and {CONF_FILTER}=={config[CONF_FILTER]} not allowed with {CONF_HALLCONF}=={config[CONF_HALLCONF]:#02x}" + ) return config diff --git a/esphome/components/mpr121/__init__.py b/esphome/components/mpr121/__init__.py index b736a7e4f0..0bf9377275 100644 --- a/esphome/components/mpr121/__init__.py +++ b/esphome/components/mpr121/__init__.py @@ -56,12 +56,13 @@ def _final_validate(config): for binary_sensor in binary_sensors: if binary_sensor.get(CONF_MPR121_ID) == config[CONF_ID]: max_touch_channel = max(max_touch_channel, binary_sensor[CONF_CHANNEL]) - if max_touch_channel_in_config := config.get(CONF_MAX_TOUCH_CHANNEL): - if max_touch_channel != max_touch_channel_in_config: - raise cv.Invalid( - "Max touch channel must equal the highest binary sensor channel or be removed for auto calculation", - path=[CONF_MAX_TOUCH_CHANNEL], - ) + if ( + max_touch_channel_in_config := config.get(CONF_MAX_TOUCH_CHANNEL) + ) and max_touch_channel != max_touch_channel_in_config: + raise cv.Invalid( + "Max touch channel must equal the highest binary sensor channel or be removed for auto calculation", + path=[CONF_MAX_TOUCH_CHANNEL], + ) path = fconf.get_path_for_id(config[CONF_ID])[:-1] this_config = fconf.get_config_for_path(path) this_config[CONF_MAX_TOUCH_CHANNEL] = max_touch_channel diff --git a/esphome/components/opentherm/number/__init__.py b/esphome/components/opentherm/number/__init__.py index a65864647a..6dbc45f49b 100644 --- a/esphome/components/opentherm/number/__init__.py +++ b/esphome/components/opentherm/number/__init__.py @@ -25,9 +25,9 @@ async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable: await cg.register_component(var, config) input.generate_setters(var, config) - if (initial_value := config.get(CONF_INITIAL_VALUE, None)) is not None: + if (initial_value := config.get(CONF_INITIAL_VALUE)) is not None: cg.add(var.set_initial_value(initial_value)) - if (restore_value := config.get(CONF_RESTORE_VALUE, None)) is not None: + if (restore_value := config.get(CONF_RESTORE_VALUE)) is not None: cg.add(var.set_restore_value(restore_value)) return var diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 25e3153d1b..2f085ebaae 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -79,9 +79,8 @@ def set_sdkconfig_options(config): "CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower() ) - if force_dataset := config.get(CONF_FORCE_DATASET): - if force_dataset: - cg.add_define("USE_OPENTHREAD_FORCE_DATASET") + if config.get(CONF_FORCE_DATASET): + cg.add_define("USE_OPENTHREAD_FORCE_DATASET") add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 99c1d824ca..bfb2bbc4f8 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -89,9 +89,10 @@ def validate_(config): raise cv.Invalid("No sensors or binary sensors to encrypt") elif config[CONF_ROLLING_CODE_ENABLE]: raise cv.Invalid("Rolling code requires an encryption key") - if config[CONF_PING_PONG_ENABLE]: - if not any(CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or ()): - raise cv.Invalid("Ping-pong requires at least one encrypted provider") + if config[CONF_PING_PONG_ENABLE] and not any( + CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or () + ): + raise cv.Invalid("Ping-pong requires at least one encrypted provider") return config diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index b330758000..dbf67fd2ad 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -49,12 +49,15 @@ def validate_internal_filter(value): [CONF_USE_PCNT], ) - if CORE.is_esp32 and use_pcnt: - if value.get(CONF_INTERNAL_FILTER).total_microseconds > 13: - raise cv.Invalid( - "Maximum internal filter value when using ESP32 hardware PCNT is 13us", - [CONF_INTERNAL_FILTER], - ) + if ( + CORE.is_esp32 + and use_pcnt + and value.get(CONF_INTERNAL_FILTER).total_microseconds > 13 + ): + raise cv.Invalid( + "Maximum internal filter value when using ESP32 hardware PCNT is 13us", + [CONF_INTERNAL_FILTER], + ) return value diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index 5b01bcc6ca..74d837a794 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -73,9 +73,8 @@ def map_sequence(value): def _validate(config): chip = DriverChip.chips[config[CONF_MODEL]] - if not chip.initsequence: - if CONF_INIT_SEQUENCE not in config: - raise cv.Invalid(f"{chip.name} model requires init_sequence") + if not chip.initsequence and CONF_INIT_SEQUENCE not in config: + raise cv.Invalid(f"{chip.name} model requires init_sequence") return config diff --git a/esphome/components/qwiic_pir/binary_sensor.py b/esphome/components/qwiic_pir/binary_sensor.py index 0a549ccb32..cd3eda5ac8 100644 --- a/esphome/components/qwiic_pir/binary_sensor.py +++ b/esphome/components/qwiic_pir/binary_sensor.py @@ -24,9 +24,8 @@ QwiicPIRComponent = qwiic_pir_ns.class_( def validate_no_debounce_unless_native(config): - if CONF_DEBOUNCE in config: - if config[CONF_DEBOUNCE_MODE] != "NATIVE": - raise cv.Invalid("debounce can only be set if debounce_mode is NATIVE") + if CONF_DEBOUNCE in config and config[CONF_DEBOUNCE_MODE] != "NATIVE": + raise cv.Invalid("debounce can only be set if debounce_mode is NATIVE") return config diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index fc824ef704..8163661c65 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1062,12 +1062,11 @@ def validate_raw_alternating(value): last_negative = None for i, val in enumerate(value): this_negative = val < 0 - if i != 0: - if this_negative == last_negative: - raise cv.Invalid( - f"Values must alternate between being positive and negative, please see index {i} and {i + 1}", - [i], - ) + if i != 0 and this_negative == last_negative: + raise cv.Invalid( + f"Values must alternate between being positive and negative, please see index {i} and {i + 1}", + [i], + ) last_negative = this_negative return value diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index 9e9b32476f..def62547b2 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -90,11 +90,10 @@ async def to_code(config): if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM): cg.add(var.set_task_stack_in_psram(task_stack_in_psram)) - if task_stack_in_psram: - if config[CONF_TASK_STACK_IN_PSRAM]: - esp32.add_idf_sdkconfig_option( - "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True - ) + if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]: + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index 556ee5eeb4..513ed8eb58 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -140,7 +140,6 @@ async def to_code(config): cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) - index = 0 dpins = [] if CONF_RED in config[CONF_DATA_PINS]: red_pins = config[CONF_DATA_PINS][CONF_RED] @@ -158,10 +157,9 @@ async def to_code(config): dpins = dpins[8:16] + dpins[0:8] else: dpins = config[CONF_DATA_PINS] - for pin in dpins: + for index, pin in enumerate(dpins): data_pin = await cg.gpio_pin_expression(pin) cg.add(var.add_data_pin(data_pin, index)) - index += 1 if enable_pin := config.get(CONF_ENABLE_PIN): enable = await cg.gpio_pin_expression(enable_pin) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index dc2dae2ef1..16dcc855c3 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -204,13 +204,14 @@ def _validate_pipeline(config): def _validate_repeated_speaker(config): - if (announcement_config := config.get(CONF_ANNOUNCEMENT_PIPELINE)) and ( - media_config := config.get(CONF_MEDIA_PIPELINE) + if ( + (announcement_config := config.get(CONF_ANNOUNCEMENT_PIPELINE)) + and (media_config := config.get(CONF_MEDIA_PIPELINE)) + and announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER] ): - if announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER]: - raise cv.Invalid( - "The announcement and media pipelines cannot use the same speaker. Use the `mixer` speaker component to create two source speakers." - ) + raise cv.Invalid( + "The announcement and media pipelines cannot use the same speaker. Use the `mixer` speaker component to create two source speakers." + ) return config diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 065ccc2668..a436bc6dab 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -115,9 +115,7 @@ def get_target_platform(): def get_target_variant(): - return ( - CORE.data[KEY_ESP32][KEY_VARIANT] if KEY_VARIANT in CORE.data[KEY_ESP32] else "" - ) + return CORE.data[KEY_ESP32].get(KEY_VARIANT, "") # Get a list of available hardware interfaces based on target and variant. @@ -213,9 +211,7 @@ def validate_hw_pins(spi, index=-1): return False if sdo_pin_no not in pin_set[CONF_MOSI_PIN]: return False - if sdi_pin_no not in pin_set[CONF_MISO_PIN]: - return False - return True + return sdi_pin_no in pin_set[CONF_MISO_PIN] return False diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 3c94d97739..2dccb6896a 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -130,11 +130,11 @@ def validate_sprinkler(config): if ( CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY in sprinkler_controller and CONF_VALVE_OPEN_DELAY not in sprinkler_controller + and sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY] ): - if sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY]: - raise cv.Invalid( - f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" - ) + raise cv.Invalid( + f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" + ) if ( CONF_REPEAT in sprinkler_controller diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 6633b24607..9d397e396b 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -42,14 +42,15 @@ SSD1306_MODEL = cv.enum(MODELS, upper=True, space="_") def _validate(value): model = value[CONF_MODEL] - if model not in ("SSD1305_128X32", "SSD1305_128X64"): - # Contrast is default value (1.0) while brightness is not - # Indicates user is using old `brightness` option - if value[CONF_BRIGHTNESS] != 1.0 and value[CONF_CONTRAST] == 1.0: - raise cv.Invalid( - "SSD1306/SH1106 no longer accepts brightness option, " - 'please use "contrast" instead.' - ) + if ( + model not in ("SSD1305_128X32", "SSD1305_128X64") + and value[CONF_BRIGHTNESS] != 1.0 + and value[CONF_CONTRAST] == 1.0 + ): + raise cv.Invalid( + "SSD1306/SH1106 no longer accepts brightness option, " + 'please use "contrast" instead.' + ) return value diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index e2452a4c55..497740b8d2 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -189,7 +189,6 @@ async def to_code(config): cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) - index = 0 dpins = [] if CONF_RED in config[CONF_DATA_PINS]: red_pins = config[CONF_DATA_PINS][CONF_RED] @@ -207,10 +206,9 @@ async def to_code(config): dpins = dpins[8:16] + dpins[0:8] else: dpins = config[CONF_DATA_PINS] - for pin in dpins: + for index, pin in enumerate(dpins): data_pin = await cg.gpio_pin_expression(pin) cg.add(var.add_data_pin(data_pin, index)) - index += 1 if dc_pin := config.get(CONF_DC_PIN): dc = await cg.gpio_pin_expression(dc_pin) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index e494529b9e..a96f56a045 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -49,15 +49,14 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing): try: # Invoke the jinja engine to evaluate the expression. value, err = jinja.expand(value) - if err is not None: - if not ignore_missing and "password" not in path: - _LOGGER.warning( - "Found '%s' (see %s) which looks like an expression," - " but could not resolve all the variables: %s", - value, - "->".join(str(x) for x in path), - err.message, - ) + if err is not None and not ignore_missing and "password" not in path: + _LOGGER.warning( + "Found '%s' (see %s) which looks like an expression," + " but could not resolve all the variables: %s", + value, + "->".join(str(x) for x in path), + err.message, + ) except ( TemplateError, TemplateRuntimeError, diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index 5a2a39c427..c065a82958 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -1,3 +1,4 @@ +import contextlib import re from esphome import automation @@ -41,12 +42,10 @@ ELEVATION_MAP = { def elevation(value): if isinstance(value, str): - try: + with contextlib.suppress(cv.Invalid): value = ELEVATION_MAP[ cv.one_of(*ELEVATION_MAP, lower=True, space="_")(value) ] - except cv.Invalid: - pass value = cv.angle(value) return cv.float_range(min=-180, max=180)(value) diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index f1b08a505a..67dc924903 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -41,11 +41,13 @@ SX1509KeyTrigger = sx1509_ns.class_( def check_keys(config): - if CONF_KEYS in config: - if len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS]: - raise cv.Invalid( - "The number of key codes must equal the number of rows * columns" - ) + if ( + CONF_KEYS in config + and len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS] + ): + raise cv.Invalid( + "The number of key codes must equal the number of rows * columns" + ) return config diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 0314d877a3..5d0d9442e8 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -477,11 +477,11 @@ def validate_thermostat(config): if ( CONF_ON_BOOT_RESTORE_FROM in config and config[CONF_ON_BOOT_RESTORE_FROM] is OnBootRestoreFrom.DEFAULT_PRESET + and CONF_DEFAULT_PRESET not in config ): - if CONF_DEFAULT_PRESET not in config: - raise cv.Invalid( - f"{CONF_DEFAULT_PRESET} must be defined to use {CONF_ON_BOOT_RESTORE_FROM} in DEFAULT_PRESET mode" - ) + raise cv.Invalid( + f"{CONF_DEFAULT_PRESET} must be defined to use {CONF_ON_BOOT_RESTORE_FROM} in DEFAULT_PRESET mode" + ) if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: raise cv.Invalid( diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 58d35c4baf..63d4ba17f2 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -236,7 +236,7 @@ def validate_time_at(value): def validate_cron_keys(value): if CONF_CRON in value: - for key in value.keys(): + for key in value: if key in CRON_KEYS: raise cv.Invalid(f"Cannot use option {key} when cron: is specified.") if CONF_AT in value: @@ -246,7 +246,7 @@ def validate_cron_keys(value): value.update(cron_) return value if CONF_AT in value: - for key in value.keys(): + for key in value: if key in CRON_KEYS: raise cv.Invalid(f"Cannot use option {key} when at: is specified.") at_ = value[CONF_AT] diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 4dbdf07651..cefe5d5a4b 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -46,16 +46,15 @@ TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component) def validate_temperature_multipliers(value): - if CONF_TEMPERATURE_MULTIPLIER in value: - if ( - CONF_CURRENT_TEMPERATURE_MULTIPLIER in value - or CONF_TARGET_TEMPERATURE_MULTIPLIER in value - ): - raise cv.Invalid( - f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as " - f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and " - f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" - ) + if CONF_TEMPERATURE_MULTIPLIER in value and ( + CONF_CURRENT_TEMPERATURE_MULTIPLIER in value + or CONF_TARGET_TEMPERATURE_MULTIPLIER in value + ): + raise cv.Invalid( + f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as " + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and " + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" + ) if ( CONF_CURRENT_TEMPERATURE_MULTIPLIER in value and CONF_TARGET_TEMPERATURE_MULTIPLIER not in value diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py index bd57c8be14..7a61e69c5c 100644 --- a/esphome/components/tuya/number/__init__.py +++ b/esphome/components/tuya/number/__init__.py @@ -34,12 +34,14 @@ def validate_min_max(config): min_value = config[CONF_MIN_VALUE] if max_value <= min_value: raise cv.Invalid("max_value must be greater than min_value") - if hidden_config := config.get(CONF_DATAPOINT_HIDDEN): - if (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None: - if (initial_value > max_value) or (initial_value < min_value): - raise cv.Invalid( - f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" - ) + if ( + (hidden_config := config.get(CONF_DATAPOINT_HIDDEN)) + and (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None + and ((initial_value > max_value) or (initial_value < min_value)) + ): + raise cv.Invalid( + f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" + ) return config diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 61f37556ba..8cb784233f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -442,9 +442,7 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) - elif CORE.is_esp32 and CORE.using_arduino: - cg.add_library("WiFi", None) - elif CORE.is_rp2040: + elif (CORE.is_esp32 and CORE.using_arduino) or CORE.is_rp2040: cg.add_library("WiFi", None) if CORE.is_esp32 and CORE.using_esp_idf: diff --git a/esphome/config.py b/esphome/config.py index c4aa9aea24..670cbe7233 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -198,10 +198,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): self.output_paths.remove((path, domain)) def is_in_error_path(self, path: ConfigPath) -> bool: - for err in self.errors: - if _path_begins_with(err.path, path): - return True - return False + return any(_path_begins_with(err.path, path) for err in self.errors) def set_by_path(self, path, value): conf = self @@ -224,7 +221,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): for index, path_item in enumerate(path): try: if path_item in data: - key_data = [x for x in data.keys() if x == path_item][0] + key_data = [x for x in data if x == path_item][0] if isinstance(key_data, ESPHomeDataBase): doc_range = key_data.esp_range if get_key and index == len(path) - 1: @@ -1081,7 +1078,7 @@ def dump_dict( ret += "{}" multiline = False - for k in conf.keys(): + for k in conf: path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: @@ -1097,10 +1094,7 @@ def dump_dict( msg = f"\n{indent(msg)}" if inf is not None: - if m: - msg = f" {inf}{msg}" - else: - msg = f"{msg} {inf}" + msg = f" {inf}{msg}" if m else f"{msg} {inf}" ret += f"{st + msg}\n" elif isinstance(conf, str): if is_secret(conf): diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 756464b563..1a4976e235 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2,7 +2,7 @@ from __future__ import annotations -from contextlib import contextmanager +from contextlib import contextmanager, suppress from dataclasses import dataclass from datetime import datetime from ipaddress import ( @@ -2113,10 +2113,8 @@ def require_esphome_version(year, month, patch): @contextmanager def suppress_invalid(): - try: + with suppress(vol.Invalid): yield - except vol.Invalid: - pass GIT_SCHEMA = Schema( diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 060dd36f8f..72aadcb139 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1037,10 +1037,7 @@ class MockObjClass(MockObj): def inherits_from(self, other: "MockObjClass") -> bool: if str(self) == str(other): return True - for parent in self._parents: - if str(parent) == str(other): - return True - return False + return any(str(parent) == str(other) for parent in self._parents) def template(self, *args: SafeExpType) -> "MockObjClass": if len(args) != 1 or not isinstance(args[0], TemplateArguments): diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 9de2d39ce2..81c10763e7 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from asyncio import events from concurrent.futures import ThreadPoolExecutor +import contextlib import logging import os import socket @@ -125,10 +126,8 @@ def start_dashboard(args) -> None: asyncio.set_event_loop_policy(DashboardEventLoopPolicy(settings.verbose)) - try: + with contextlib.suppress(KeyboardInterrupt): asyncio.run(async_start(args)) - except KeyboardInterrupt: - pass async def async_start(args) -> None: diff --git a/esphome/espota2.py b/esphome/espota2.py index 4f2a08fb4a..279bafee8e 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -88,10 +88,7 @@ def recv_decode(sock, amount, decode=True): def receive_exactly(sock, amount, msg, expect, decode=True): - if decode: - data = [] - else: - data = b"" + data = [] if decode else b"" try: data += recv_decode(sock, 1, decode=decode) diff --git a/esphome/helpers.py b/esphome/helpers.py index bf0e3b5cf7..d1f3080e34 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -96,9 +96,7 @@ def cpp_string_escape(string, encoding="utf-8"): def _should_escape(byte: int) -> bool: if not 32 <= byte < 127: return True - if byte in (ord("\\"), ord('"')): - return True - return False + return byte in (ord("\\"), ord('"')) if isinstance(string, str): string = string.encode(encoding) diff --git a/esphome/log.py b/esphome/log.py index 0e91eb32c2..8831b1b2b3 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -61,7 +61,7 @@ class ESPHomeLogFormatter(logging.Formatter): }.get(record.levelname, "") message = f"{prefix}{formatted}{AnsiStyle.RESET_ALL.value}" if CORE.dashboard: - try: + try: # noqa: SIM105 message = message.replace("\033", "\\033") except UnicodeEncodeError: pass diff --git a/esphome/mqtt.py b/esphome/mqtt.py index a420b5ba7f..acfa8a0926 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,3 +1,4 @@ +import contextlib from datetime import datetime import hashlib import json @@ -52,10 +53,8 @@ def initialize( client = prepare( config, subscriptions, on_message, on_connect, username, password, client_id ) - try: + with contextlib.suppress(KeyboardInterrupt): client.loop_forever() - except KeyboardInterrupt: - pass return 0 diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index e34ac028f8..7415ec9794 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -131,9 +131,11 @@ def _load_idedata(config): temp_idedata = Path(CORE.relative_internal_path("idedata", f"{CORE.name}.json")) changed = False - if not platformio_ini.is_file() or not temp_idedata.is_file(): - changed = True - elif platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime: + if ( + not platformio_ini.is_file() + or not temp_idedata.is_file() + or platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime + ): changed = True if not changed: diff --git a/esphome/util.py b/esphome/util.py index 79cb630200..3b346371bc 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -59,7 +59,7 @@ def safe_print(message="", end="\n"): from esphome.core import CORE if CORE.dashboard: - try: + try: # noqa: SIM105 message = message.replace("\033", "\\033") except UnicodeEncodeError: pass diff --git a/esphome/wizard.py b/esphome/wizard.py index 6f7cbd1ff4..8602e90222 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -116,10 +116,7 @@ def wizard_file(**kwargs): kwargs["fallback_name"] = ap_name kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) - if kwargs.get("friendly_name"): - base = BASE_CONFIG_FRIENDLY - else: - base = BASE_CONFIG + base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG config = base.format(**kwargs) diff --git a/esphome/writer.py b/esphome/writer.py index d2ec112ec8..2c2e00b513 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -86,21 +86,17 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: if old.src_version != new.src_version: return True - if old.build_path != new.build_path: - return True - - return False + return old.build_path != new.build_path def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool: if ( old.loaded_integrations != new.loaded_integrations or old.loaded_platforms != new.loaded_platforms - ): - if new.core_platform == PLATFORM_ESP32: - from esphome.components.esp32 import FRAMEWORK_ESP_IDF + ) and new.core_platform == PLATFORM_ESP32: + from esphome.components.esp32 import FRAMEWORK_ESP_IDF - return new.framework == FRAMEWORK_ESP_IDF + return new.framework == FRAMEWORK_ESP_IDF return False diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index e52fc9e788..33a56fc158 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -56,9 +56,12 @@ class ESPHomeDataBase: def from_node(self, node): # pylint: disable=attribute-defined-outside-init self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) - if isinstance(node, yaml.ScalarNode): - if node.style is not None and node.style in "|>": - self._content_offset = 1 + if ( + isinstance(node, yaml.ScalarNode) + and node.style is not None + and node.style in "|>" + ): + self._content_offset = 1 def from_database(self, database): # pylint: disable=attribute-defined-outside-init diff --git a/pyproject.toml b/pyproject.toml index 5d48779ad5..971b9fbf74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,11 +111,12 @@ exclude = ['generated'] [tool.ruff.lint] select = [ - "E", # pycodestyle - "F", # pyflakes/autoflake - "I", # isort - "PL", # pylint - "UP", # pyupgrade + "E", # pycodestyle + "F", # pyflakes/autoflake + "I", # isort + "PL", # pylint + "SIM", # flake8-simplify + "UP", # pyupgrade ] ignore = [ diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 424565a893..92c85d2366 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -61,9 +61,7 @@ def indent_list(text: str, padding: str = " ") -> list[str]: """Indent each line of the given text with the specified padding.""" lines = [] for line in text.splitlines(): - if line == "": - p = "" - elif line.startswith("#ifdef") or line.startswith("#endif"): + if line == "" or line.startswith("#ifdef") or line.startswith("#endif"): p = "" else: p = padding @@ -2385,7 +2383,7 @@ static const char *const TAG = "api.service"; needs_conn = get_opt(m, pb.needs_setup_connection, True) needs_auth = get_opt(m, pb.needs_authentication, True) - ifdef = message_ifdef_map.get(inp, ifdefs.get(inp, None)) + ifdef = message_ifdef_map.get(inp, ifdefs.get(inp)) if ifdef is not None: hpp += f"#ifdef {ifdef}\n" diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 960c35ca0f..c114d15315 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -71,11 +71,13 @@ def get_component_names(): skip_components = [] for d in os.listdir(CORE_COMPONENTS_PATH): - if not d.startswith("__") and os.path.isdir( - os.path.join(CORE_COMPONENTS_PATH, d) + if ( + not d.startswith("__") + and os.path.isdir(os.path.join(CORE_COMPONENTS_PATH, d)) + and d not in component_names + and d not in skip_components ): - if d not in component_names and d not in skip_components: - component_names.append(d) + component_names.append(d) return sorted(component_names) @@ -139,11 +141,10 @@ def register_module_schemas(key, module, manifest=None): for name, schema in module_schemas(module): register_known_schema(key, name, schema) - if manifest: + if manifest and manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]: # Multi conf should allow list of components # not sure about 2nd part of the if, might be useless config (e.g. as3935) - if manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]: - output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True + output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True def register_known_schema(module, name, schema): @@ -230,7 +231,7 @@ def add_module_registries(domain, module): reg_type = attr_name.partition("_")[0].lower() found_registries[repr(attr_obj)] = f"{domain}.{reg_type}" - for name in attr_obj.keys(): + for name in attr_obj: if "." not in name: reg_entry_name = name else: @@ -700,7 +701,7 @@ def is_convertible_schema(schema): if repr(schema) in ejs.registry_schemas: return True if isinstance(schema, dict): - for k in schema.keys(): + for k in schema: if isinstance(k, (cv.Required, cv.Optional)): return True return False @@ -818,7 +819,7 @@ def convert(schema, config_var, path): elif schema_type == "automation": extra_schema = None config_var[S_TYPE] = "trigger" - if automation.AUTOMATION_SCHEMA == ejs.extended_schemas[repr(data)][0]: + if ejs.extended_schemas[repr(data)][0] == automation.AUTOMATION_SCHEMA: extra_schema = ejs.extended_schemas[repr(data)][1] if ( extra_schema is not None and len(extra_schema) > 1 @@ -926,9 +927,8 @@ def convert(schema, config_var, path): config = convert_config(schema_type, path + "/type_" + schema_key) types[schema_key] = config["schema"] - elif DUMP_UNKNOWN: - if S_TYPE not in config_var: - config_var["unknown"] = repr_schema + elif DUMP_UNKNOWN and S_TYPE not in config_var: + config_var["unknown"] = repr_schema if DUMP_PATH: config_var["path"] = path diff --git a/script/helpers.py b/script/helpers.py index 9032451b4f..4903521e2d 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -365,9 +365,11 @@ def load_idedata(environment: str) -> dict[str, Any]: platformio_ini = Path(root_path) / "platformio.ini" temp_idedata = Path(temp_folder) / f"idedata-{environment}.json" changed = False - if not platformio_ini.is_file() or not temp_idedata.is_file(): - changed = True - elif platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime: + if ( + not platformio_ini.is_file() + or not temp_idedata.is_file() + or platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime + ): changed = True if "idf" in environment: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 6e2f398f49..46eb6c88e2 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -183,19 +183,17 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s content = content.replace("api:", f"api:\n port: {unused_tcp_port}") # Add debug build flags for integration tests to enable assertions - if "esphome:" in content: - # Check if platformio_options already exists - if "platformio_options:" not in content: - # Add platformio_options with debug flags after esphome: - content = content.replace( - "esphome:", - "esphome:\n" - " # Enable assertions for integration tests\n" - " platformio_options:\n" - " build_flags:\n" - ' - "-DDEBUG" # Enable assert() statements\n' - ' - "-g" # Add debug symbols', - ) + if "esphome:" in content and "platformio_options:" not in content: + # Add platformio_options with debug flags after esphome: + content = content.replace( + "esphome:", + "esphome:\n" + " # Enable assertions for integration tests\n" + " platformio_options:\n" + " build_flags:\n" + ' - "-DDEBUG" # Enable assert() statements\n' + ' - "-g" # Add debug symbols', + ) return content diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 2862dcc900..9ae4cdcb5d 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -59,86 +59,86 @@ async def test_api_custom_services( custom_arrays_future.set_result(True) # Run with log monitoring - async with run_compiled(yaml_config, line_callback=check_output): - async with api_client_connected() as client: - # Verify device info - device_info = await client.device_info() - assert device_info is not None - assert device_info.name == "api-custom-services-test" + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "api-custom-services-test" - # List services - _, services = await client.list_entities_services() + # List services + _, services = await client.list_entities_services() - # Should have 4 services: 1 YAML + 3 CustomAPIDevice - assert len(services) == 4, f"Expected 4 services, found {len(services)}" + # Should have 4 services: 1 YAML + 3 CustomAPIDevice + assert len(services) == 4, f"Expected 4 services, found {len(services)}" - # Find our services - yaml_service: UserService | None = None - custom_service: UserService | None = None - custom_args_service: UserService | None = None - custom_arrays_service: UserService | None = None + # Find our services + yaml_service: UserService | None = None + custom_service: UserService | None = None + custom_args_service: UserService | None = None + custom_arrays_service: UserService | None = None - for service in services: - if service.name == "test_yaml_service": - yaml_service = service - elif service.name == "custom_test_service": - custom_service = service - elif service.name == "custom_service_with_args": - custom_args_service = service - elif service.name == "custom_service_with_arrays": - custom_arrays_service = service + for service in services: + if service.name == "test_yaml_service": + yaml_service = service + elif service.name == "custom_test_service": + custom_service = service + elif service.name == "custom_service_with_args": + custom_args_service = service + elif service.name == "custom_service_with_arrays": + custom_arrays_service = service - assert yaml_service is not None, "test_yaml_service not found" - assert custom_service is not None, "custom_test_service not found" - assert custom_args_service is not None, "custom_service_with_args not found" - assert custom_arrays_service is not None, ( - "custom_service_with_arrays not found" - ) + assert yaml_service is not None, "test_yaml_service not found" + assert custom_service is not None, "custom_test_service not found" + assert custom_args_service is not None, "custom_service_with_args not found" + assert custom_arrays_service is not None, "custom_service_with_arrays not found" - # Test YAML service - client.execute_service(yaml_service, {}) - await asyncio.wait_for(yaml_service_future, timeout=5.0) + # Test YAML service + client.execute_service(yaml_service, {}) + await asyncio.wait_for(yaml_service_future, timeout=5.0) - # Test simple CustomAPIDevice service - client.execute_service(custom_service, {}) - await asyncio.wait_for(custom_service_future, timeout=5.0) + # Test simple CustomAPIDevice service + client.execute_service(custom_service, {}) + await asyncio.wait_for(custom_service_future, timeout=5.0) - # Verify custom_args_service arguments - assert len(custom_args_service.args) == 4 - arg_types = {arg.name: arg.type for arg in custom_args_service.args} - assert arg_types["arg_string"] == UserServiceArgType.STRING - assert arg_types["arg_int"] == UserServiceArgType.INT - assert arg_types["arg_bool"] == UserServiceArgType.BOOL - assert arg_types["arg_float"] == UserServiceArgType.FLOAT + # Verify custom_args_service arguments + assert len(custom_args_service.args) == 4 + arg_types = {arg.name: arg.type for arg in custom_args_service.args} + assert arg_types["arg_string"] == UserServiceArgType.STRING + assert arg_types["arg_int"] == UserServiceArgType.INT + assert arg_types["arg_bool"] == UserServiceArgType.BOOL + assert arg_types["arg_float"] == UserServiceArgType.FLOAT - # Test CustomAPIDevice service with arguments - client.execute_service( - custom_args_service, - { - "arg_string": "test_string", - "arg_int": 456, - "arg_bool": True, - "arg_float": 78.9, - }, - ) - await asyncio.wait_for(custom_args_future, timeout=5.0) + # Test CustomAPIDevice service with arguments + client.execute_service( + custom_args_service, + { + "arg_string": "test_string", + "arg_int": 456, + "arg_bool": True, + "arg_float": 78.9, + }, + ) + await asyncio.wait_for(custom_args_future, timeout=5.0) - # Verify array service arguments - assert len(custom_arrays_service.args) == 4 - array_arg_types = {arg.name: arg.type for arg in custom_arrays_service.args} - assert array_arg_types["bool_array"] == UserServiceArgType.BOOL_ARRAY - assert array_arg_types["int_array"] == UserServiceArgType.INT_ARRAY - assert array_arg_types["float_array"] == UserServiceArgType.FLOAT_ARRAY - assert array_arg_types["string_array"] == UserServiceArgType.STRING_ARRAY + # Verify array service arguments + assert len(custom_arrays_service.args) == 4 + array_arg_types = {arg.name: arg.type for arg in custom_arrays_service.args} + assert array_arg_types["bool_array"] == UserServiceArgType.BOOL_ARRAY + assert array_arg_types["int_array"] == UserServiceArgType.INT_ARRAY + assert array_arg_types["float_array"] == UserServiceArgType.FLOAT_ARRAY + assert array_arg_types["string_array"] == UserServiceArgType.STRING_ARRAY - # Test CustomAPIDevice service with arrays - client.execute_service( - custom_arrays_service, - { - "bool_array": [True, False], - "int_array": [1, 2, 3], - "float_array": [1.1, 2.2], - "string_array": ["hello", "world"], - }, - ) - await asyncio.wait_for(custom_arrays_future, timeout=5.0) + # Test CustomAPIDevice service with arrays + client.execute_service( + custom_arrays_service, + { + "bool_array": [True, False], + "int_array": [1, 2, 3], + "float_array": [1.1, 2.2], + "string_array": ["hello", "world"], + }, + ) + await asyncio.wait_for(custom_arrays_future, timeout=5.0) diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index fb61569e59..51088bcbf7 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -47,9 +47,7 @@ async def test_device_id_in_state( entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] elif entity.name == "Motion Detected": entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "Temperature Monitor Power": - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Temperature Status": + elif entity.name in {"Temperature Monitor Power", "Temperature Status"}: entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] elif entity.name == "Motion Light": entity_device_mapping[entity.key] = device_ids["Motion Sensor"] diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py index 7bce0eda54..34c46bab82 100644 --- a/tests/integration/test_scheduler_defer_cancel.py +++ b/tests/integration/test_scheduler_defer_cancel.py @@ -70,11 +70,13 @@ async def test_scheduler_defer_cancel( test_complete_future.set_result(True) return - if state.key == test_result_entity.key and not test_result_future.done(): - # Event type should be "defer_executed_X" where X is the defer number - if state.event_type.startswith("defer_executed_"): - defer_num = int(state.event_type.split("_")[-1]) - test_result_future.set_result(defer_num) + if ( + state.key == test_result_entity.key + and not test_result_future.done() + and state.event_type.startswith("defer_executed_") + ): + defer_num = int(state.event_type.split("_")[-1]) + test_result_future.set_result(defer_num) client.subscribe_states(on_state) diff --git a/tests/integration/test_scheduler_null_name.py b/tests/integration/test_scheduler_null_name.py index 66e25d4a11..75864ea2d2 100644 --- a/tests/integration/test_scheduler_null_name.py +++ b/tests/integration/test_scheduler_null_name.py @@ -27,33 +27,33 @@ async def test_scheduler_null_name( if not test_complete_future.done() and test_complete_pattern.search(line): test_complete_future.set_result(True) - async with run_compiled(yaml_config, line_callback=check_output): - async with api_client_connected() as client: - # Verify we can connect - device_info = await client.device_info() - assert device_info is not None - assert device_info.name == "scheduler-null-name" + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-null-name" - # List services - _, services = await asyncio.wait_for( - client.list_entities_services(), timeout=5.0 + # List services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + test_null_name_service = next( + (s for s in services if s.name == "test_null_name"), None + ) + assert test_null_name_service is not None, "test_null_name service not found" + + # Execute the test + client.execute_service(test_null_name_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + except TimeoutError: + pytest.fail( + "Test did not complete within timeout - likely crashed due to NULL name" ) - - # Find our test service - test_null_name_service = next( - (s for s in services if s.name == "test_null_name"), None - ) - assert test_null_name_service is not None, ( - "test_null_name service not found" - ) - - # Execute the test - client.execute_service(test_null_name_service, {}) - - # Wait for test completion - try: - await asyncio.wait_for(test_complete_future, timeout=10.0) - except TimeoutError: - pytest.fail( - "Test did not complete within timeout - likely crashed due to NULL name" - ) diff --git a/tests/integration/test_scheduler_rapid_cancellation.py b/tests/integration/test_scheduler_rapid_cancellation.py index 6b6277c752..1b7da32aaa 100644 --- a/tests/integration/test_scheduler_rapid_cancellation.py +++ b/tests/integration/test_scheduler_rapid_cancellation.py @@ -61,9 +61,10 @@ async def test_scheduler_rapid_cancellation( elif "Total executed:" in line: if match := re.search(r"Total executed: (\d+)", line): test_stats["final_executed"] = int(match.group(1)) - elif "Implicit cancellations (replaced):" in line: - if match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line): - test_stats["final_implicit_cancellations"] = int(match.group(1)) + elif "Implicit cancellations (replaced):" in line and ( + match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line) + ): + test_stats["final_implicit_cancellations"] = int(match.group(1)) # Check for crash indicators if any( diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 84be7344c3..7200afc2ee 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -146,9 +146,11 @@ def test_main_list_components_fails( mock_subprocess_run.side_effect = subprocess.CalledProcessError(1, "cmd") # Run main function with mocked argv - should raise - with patch("sys.argv", ["determine-jobs.py"]): - with pytest.raises(subprocess.CalledProcessError): - determine_jobs.main() + with ( + patch("sys.argv", ["determine-jobs.py"]), + pytest.raises(subprocess.CalledProcessError), + ): + determine_jobs.main() def test_main_with_branch_argument( @@ -243,17 +245,21 @@ def test_should_run_integration_tests_with_branch() -> None: def test_should_run_integration_tests_component_dependency() -> None: """Test that integration tests run when components used in fixtures change.""" - with patch.object( - determine_jobs, "changed_files", return_value=["esphome/components/api/api.cpp"] - ): - with patch.object( + with ( + patch.object( + determine_jobs, + "changed_files", + return_value=["esphome/components/api/api.cpp"], + ), + patch.object( determine_jobs, "get_components_from_integration_fixtures" - ) as mock_fixtures: - mock_fixtures.return_value = {"api", "sensor"} - with patch.object(determine_jobs, "get_all_dependencies") as mock_deps: - mock_deps.return_value = {"api", "sensor", "network"} - result = determine_jobs.should_run_integration_tests() - assert result is True + ) as mock_fixtures, + ): + mock_fixtures.return_value = {"api", "sensor"} + with patch.object(determine_jobs, "get_all_dependencies") as mock_deps: + mock_deps.return_value = {"api", "sensor", "network"} + result = determine_jobs.should_run_integration_tests() + assert result is True @pytest.mark.parametrize( @@ -272,12 +278,14 @@ def test_should_run_clang_tidy( expected_result: bool, ) -> None: """Test should_run_clang_tidy function.""" - with patch.object(determine_jobs, "changed_files", return_value=changed_files): + with ( + patch.object(determine_jobs, "changed_files", return_value=changed_files), + patch("subprocess.run") as mock_run, + ): # Test with hash check returning specific code - with patch("subprocess.run") as mock_run: - mock_run.return_value = Mock(returncode=check_returncode) - result = determine_jobs.should_run_clang_tidy() - assert result == expected_result + mock_run.return_value = Mock(returncode=check_returncode) + result = determine_jobs.should_run_clang_tidy() + assert result == expected_result def test_should_run_clang_tidy_hash_check_exception() -> None: From e79589efeeabb7bdc8203aa8c7ee3aa7f9900ded Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 25 Jul 2025 01:31:32 -0500 Subject: [PATCH 119/125] [platformio.ini] Add GPS to nrf52-zephyr lib_deps (#9884) --- .clang-tidy.hash | 2 +- platformio.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 47f68ff022..02aad99327 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -a0bc970a2edcf618945646316f28238c53accb6963bad3726148614d2509ede8 +2dc5fb2038fb2081e8f27fa19c4d5e9d107ceabda951e68f2d6d296edfb54613 diff --git a/platformio.ini b/platformio.ini index 0aaa4a6346..8b24d18d0d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -239,6 +239,7 @@ lib_deps = wjtje/qr-code-generator-library@1.7.0 ; qr_code pavlodn/HaierProtocol@0.9.31 ; haier functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 + https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library lvgl/lvgl@8.4.0 ; lvgl From c5c0237a4bfd556c353a3723eac201c315cb1255 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 25 Jul 2025 20:16:23 +1200 Subject: [PATCH 120/125] Remove redundant platformio environments (#9886) --- .clang-tidy.hash | 2 +- platformio.ini | 53 ++---------------------------------------------- 2 files changed, 3 insertions(+), 52 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 02aad99327..316f43e706 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -2dc5fb2038fb2081e8f27fa19c4d5e9d107ceabda951e68f2d6d296edfb54613 +32b0db73b3ae01ba18c9cbb1dabbd8156bc14dded500471919bd0a3dc33916e0 diff --git a/platformio.ini b/platformio.ini index 8b24d18d0d..bf0754ead3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -180,13 +180,6 @@ build_unflags = ${common.build_unflags} extra_scripts = post:esphome/components/esp32/post_build.py.script -; This are common settings for the ESP32 using the latest ESP-IDF version. -[common:esp32-idf-5_3] -extends = common:esp32-idf -platform = platformio/espressif32@6.8.0 -platform_packages = - platformio/framework-espidf@~3.50300.0 - ; These are common settings for the RP2040 using Arduino. [common:rp2040-arduino] extends = common:arduino @@ -299,17 +292,6 @@ build_flags = build_unflags = ${common.build_unflags} -[env:esp32-idf-5_3] -extends = common:esp32-idf-5_3 -board = esp32dev -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32 -build_unflags = - ${common.build_unflags} - [env:esp32-idf-tidy] extends = common:esp32-idf board = esp32dev @@ -354,17 +336,6 @@ build_flags = build_unflags = ${common.build_unflags} -[env:esp32c3-idf-5_3] -extends = common:esp32-idf-5_3 -board = esp32-c3-devkitm-1 -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32C3 -build_unflags = - ${common.build_unflags} - [env:esp32c3-idf-tidy] extends = common:esp32-idf board = esp32-c3-devkitm-1 @@ -420,17 +391,6 @@ build_flags = build_unflags = ${common.build_unflags} -[env:esp32s2-idf-5_3] -extends = common:esp32-idf-5_3 -board = esp32-s2-kaluga-1 -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32S2 -build_unflags = - ${common.build_unflags} - [env:esp32s2-idf-tidy] extends = common:esp32-idf board = esp32-s2-kaluga-1 @@ -475,17 +435,6 @@ build_flags = build_unflags = ${common.build_unflags} -[env:esp32s3-idf-5_3] -extends = common:esp32-idf-5_3 -board = esp32-s3-devkitc-1 -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s3-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32S3 -build_unflags = - ${common.build_unflags} - [env:esp32s3-idf-tidy] extends = common:esp32-idf board = esp32-s3-devkitc-1 @@ -566,6 +515,8 @@ build_flags = build_unflags = ${common.build_unflags} +;;;;;;;; Host ;;;;;;;; + [env:host] extends = common platform = platformio/native From 773a8b8fb70b530d7a33a37f6a026b83be71934e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:14:28 +1200 Subject: [PATCH 121/125] [CI] Better mega-pr label handling (#9888) --- .github/workflows/auto-label-pr.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 17c77a1920..f855044d31 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -14,6 +14,7 @@ env: SMALL_PR_THRESHOLD: 30 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 + COMPONENT_LABEL_THRESHOLD: 10 jobs: label: @@ -41,6 +42,7 @@ jobs: const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}'); const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); + const COMPONENT_LABEL_THRESHOLD = parseInt('${{ env.COMPONENT_LABEL_THRESHOLD }}'); const BOT_COMMENT_MARKER = ''; const CODEOWNERS_MARKER = ''; const TOO_BIG_MARKER = ''; @@ -84,6 +86,9 @@ jobs: label.startsWith('component: ') || MANAGED_LABELS.includes(label) ); + // Check for mega-PR early - if present, skip most automatic labeling + const isMegaPR = currentLabels.includes('mega-pr'); + const { data: prFiles } = await github.rest.pulls.listFiles({ owner, repo, @@ -97,6 +102,9 @@ jobs: console.log('Current labels:', currentLabels.join(', ')); console.log('Changed files:', changedFiles.length); console.log('Total changes:', totalChanges); + if (isMegaPR) { + console.log('Mega-PR detected - applying limited labeling logic'); + } // Fetch API data async function fetchApiData() { @@ -225,7 +233,8 @@ jobs: labels.add('small-pr'); } - if (nonTestChanges > TOO_BIG_THRESHOLD) { + // Don't add too-big if mega-pr label is already present + if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) { labels.add('too-big'); } @@ -557,8 +566,16 @@ jobs: let finalLabels = Array.from(allLabels); - // Handle too many labels - const isMegaPR = currentLabels.includes('mega-pr'); + // For mega-PRs, exclude component labels if there are too many + if (isMegaPR) { + const componentLabels = finalLabels.filter(label => label.startsWith('component: ')); + if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) { + finalLabels = finalLabels.filter(label => !label.startsWith('component: ')); + console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`); + } + } + + // Handle too many labels (only for non-mega PRs) const tooManyLabels = finalLabels.length > MAX_LABELS; if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { From 457689fa1d9591205528204448d8acf0242c4203 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:40:42 +1200 Subject: [PATCH 122/125] [CI] Fix auto-label workflow - codeowners & listFiles (#9890) --- .github/workflows/auto-label-pr.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index f855044d31..729fae27fe 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -89,11 +89,15 @@ jobs: // Check for mega-PR early - if present, skip most automatic labeling const isMegaPR = currentLabels.includes('mega-pr'); - const { data: prFiles } = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number: pr_number - }); + // Get all PR files with automatic pagination + const prFiles = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: pr_number + } + ); // Calculate data from PR files const changedFiles = prFiles.map(file => file.filename); @@ -298,9 +302,10 @@ jobs: const dir = pattern.slice(0, -1); regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); } else if (pattern.includes('*')) { + // First escape all regex special chars except *, then replace * with .* const regexPattern = pattern - .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - .replace(/\\*/g, '.*'); + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*'); regex = new RegExp(`^${regexPattern}$`); } else { regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); From 9ac10d7276d531bb88976a64beff22467f46a95f Mon Sep 17 00:00:00 2001 From: GilDev Date: Fri, 25 Jul 2025 13:33:29 +0200 Subject: [PATCH 123/125] =?UTF-8?q?[mqtt]=20Don=E2=80=99t=20log=20state=20?= =?UTF-8?q?topic=20subscription=20for=20buttons=20(#9887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esphome/components/mqtt/mqtt_button.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index c619a02344..b3435edf38 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -27,7 +27,7 @@ void MQTTButtonComponent::setup() { } void MQTTButtonComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Button '%s': ", this->button_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, true); + LOG_MQTT_COMPONENT(false, true); } void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { From 88ccde4ba11d5a8eb50b1e3a426c1cbf58877d64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Jul 2025 08:14:15 -1000 Subject: [PATCH 124/125] [scheduler] Fix retry race condition on cancellation (#9788) --- esphome/core/scheduler.cpp | 20 ++++- esphome/core/scheduler.h | 25 +++++- .../fixtures/scheduler_retry_test.yaml | 78 ++++++++++++++----- .../integration/test_scheduler_retry_test.py | 14 ++-- 4 files changed, 104 insertions(+), 33 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index dd80199dc0..41027f1d3f 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -65,7 +65,7 @@ static void validate_static_string(const char *name) { // Common implementation for both timeout and interval void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, - const void *name_ptr, uint32_t delay, std::function func) { + const void *name_ptr, uint32_t delay, std::function func, bool is_retry) { // Get the name as const char* const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); @@ -130,6 +130,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type #endif /* ESPHOME_DEBUG_SCHEDULER */ LockGuard guard{this->lock_}; + + // For retries, check if there's a cancelled timeout first + if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && + (has_cancelled_timeout_in_container_(this->items_, component, name_cstr) || + has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr))) { + // Skip scheduling - the retry was cancelled +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); +#endif + return; + } + // If name is provided, do atomic cancel-and-add // Cancel existing items this->cancel_item_locked_(component, name_cstr, type); @@ -178,12 +190,14 @@ struct RetryArgs { Scheduler *scheduler; }; -static void retry_handler(const std::shared_ptr &args) { +void retry_handler(const std::shared_ptr &args) { RetryResult const retry_result = args->func(--args->retry_countdown); if (retry_result == RetryResult::DONE || args->retry_countdown <= 0) return; // second execution of `func` happens after `initial_wait_time` - args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); }); + args->scheduler->set_timer_common_( + args->component, Scheduler::SchedulerItem::TIMEOUT, false, &args->name, args->current_interval, + [args]() { retry_handler(args); }, true); // backoff_increase_factor applied to third & later executions args->current_interval *= args->backoff_increase_factor; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 7bf83f7877..fa189bacf7 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -15,8 +15,15 @@ namespace esphome { class Component; +struct RetryArgs; + +// Forward declaration of retry_handler - needs to be non-static for friend declaration +void retry_handler(const std::shared_ptr &args); class Scheduler { + // Allow retry_handler to access protected members + friend void ::esphome::retry_handler(const std::shared_ptr &args); + public: // Public API - accepts std::string for backward compatibility void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); @@ -147,7 +154,7 @@ class Scheduler { // Common implementation for both timeout and interval void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, - uint32_t delay, std::function func); + uint32_t delay, std::function func, bool is_retry = false); uint64_t millis_64_(uint32_t now); // Cleanup logically deleted items from the scheduler @@ -170,8 +177,8 @@ class Scheduler { // Helper function to check if item matches criteria for cancellation inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type) { - if (item->component != component || item->type != type || item->remove) { + SchedulerItem::Type type, bool skip_removed = true) const { + if (item->component != component || item->type != type || (skip_removed && item->remove)) { return false; } const char *item_name = item->get_name(); @@ -197,6 +204,18 @@ class Scheduler { return item->remove || (item->component != nullptr && item->component->is_failed()); } + // Template helper to check if any item in a container matches our criteria + template + bool has_cancelled_timeout_in_container_(const Container &container, Component *component, + const char *name_cstr) const { + for (const auto &item : container) { + if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, false)) { + return true; + } + } + return false; + } + Mutex lock_; std::vector> items_; std::vector> to_add_; diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml index 1b6ee8e5c2..c6fcc53f8c 100644 --- a/tests/integration/fixtures/scheduler_retry_test.yaml +++ b/tests/integration/fixtures/scheduler_retry_test.yaml @@ -10,7 +10,7 @@ esphome: host: api: logger: - level: VERBOSE + level: VERY_VERBOSE globals: - id: simple_retry_counter @@ -19,6 +19,9 @@ globals: - id: backoff_retry_counter type: int initial_value: '0' + - id: backoff_last_attempt_time + type: uint32_t + initial_value: '0' - id: immediate_done_counter type: int initial_value: '0' @@ -35,20 +38,55 @@ globals: type: int initial_value: '0' +# Using different component types for each test to ensure isolation sensor: - platform: template - name: Test Sensor - id: test_sensor + name: Simple Retry Test Sensor + id: simple_retry_sensor lambda: return 1.0; update_interval: never + - platform: template + name: Backoff Retry Test Sensor + id: backoff_retry_sensor + lambda: return 2.0; + update_interval: never + + - platform: template + name: Immediate Done Test Sensor + id: immediate_done_sensor + lambda: return 3.0; + update_interval: never + +binary_sensor: + - platform: template + name: Cancel Retry Test Binary Sensor + id: cancel_retry_binary_sensor + lambda: return false; + + - platform: template + name: Empty Name Test Binary Sensor + id: empty_name_binary_sensor + lambda: return true; + +switch: + - platform: template + name: Script Retry Test Switch + id: script_retry_switch + optimistic: true + + - platform: template + name: Multiple Same Name Test Switch + id: multiple_same_name_switch + optimistic: true + script: - id: run_all_tests then: # Test 1: Simple retry - logger.log: "=== Test 1: Simple retry ===" - lambda: |- - auto *component = id(test_sensor); + auto *component = id(simple_retry_sensor); App.scheduler.set_retry(component, "simple_retry", 50, 3, [](uint8_t retry_countdown) { id(simple_retry_counter)++; @@ -65,19 +103,19 @@ script: # Test 2: Backoff retry - logger.log: "=== Test 2: Retry with backoff ===" - lambda: |- - auto *component = id(test_sensor); - static uint32_t backoff_start_time = 0; - static uint32_t last_attempt_time = 0; - - backoff_start_time = millis(); - last_attempt_time = backoff_start_time; + auto *component = id(backoff_retry_sensor); App.scheduler.set_retry(component, "backoff_retry", 50, 4, [](uint8_t retry_countdown) { id(backoff_retry_counter)++; uint32_t now = millis(); - uint32_t interval = now - last_attempt_time; - last_attempt_time = now; + uint32_t interval = 0; + + // Only calculate interval after first attempt + if (id(backoff_retry_counter) > 1) { + interval = now - id(backoff_last_attempt_time); + } + id(backoff_last_attempt_time) = now; ESP_LOGI("test", "Backoff retry attempt %d (countdown=%d, interval=%dms)", id(backoff_retry_counter), retry_countdown, interval); @@ -100,7 +138,7 @@ script: # Test 3: Immediate done - logger.log: "=== Test 3: Immediate done ===" - lambda: |- - auto *component = id(test_sensor); + auto *component = id(immediate_done_sensor); App.scheduler.set_retry(component, "immediate_done", 50, 5, [](uint8_t retry_countdown) { id(immediate_done_counter)++; @@ -111,8 +149,8 @@ script: # Test 4: Cancel retry - logger.log: "=== Test 4: Cancel retry ===" - lambda: |- - auto *component = id(test_sensor); - App.scheduler.set_retry(component, "cancel_test", 25, 10, + auto *component = id(cancel_retry_binary_sensor); + App.scheduler.set_retry(component, "cancel_test", 30, 10, [](uint8_t retry_countdown) { id(cancel_retry_counter)++; ESP_LOGI("test", "Cancel test retry attempt %d", id(cancel_retry_counter)); @@ -121,7 +159,7 @@ script: // Cancel it after 100ms App.scheduler.set_timeout(component, "cancel_timer", 100, []() { - bool cancelled = App.scheduler.cancel_retry(id(test_sensor), "cancel_test"); + bool cancelled = App.scheduler.cancel_retry(id(cancel_retry_binary_sensor), "cancel_test"); ESP_LOGI("test", "Retry cancellation result: %s", cancelled ? "true" : "false"); ESP_LOGI("test", "Cancel retry ran %d times before cancellation", id(cancel_retry_counter)); }); @@ -129,7 +167,7 @@ script: # Test 5: Empty name retry - logger.log: "=== Test 5: Empty name retry ===" - lambda: |- - auto *component = id(test_sensor); + auto *component = id(empty_name_binary_sensor); App.scheduler.set_retry(component, "", 100, 5, [](uint8_t retry_countdown) { id(empty_name_retry_counter)++; @@ -139,7 +177,7 @@ script: // Try to cancel after 150ms App.scheduler.set_timeout(component, "empty_cancel_timer", 150, []() { - bool cancelled = App.scheduler.cancel_retry(id(test_sensor), ""); + bool cancelled = App.scheduler.cancel_retry(id(empty_name_binary_sensor), ""); ESP_LOGI("test", "Empty name retry cancel result: %s", cancelled ? "true" : "false"); ESP_LOGI("test", "Empty name retry ran %d times", id(empty_name_retry_counter)); @@ -169,7 +207,7 @@ script: # Test 7: Multiple same name - logger.log: "=== Test 7: Multiple retries with same name ===" - lambda: |- - auto *component = id(test_sensor); + auto *component = id(multiple_same_name_switch); // Set first retry App.scheduler.set_retry(component, "duplicate_retry", 100, 5, @@ -200,7 +238,7 @@ script: ESP_LOGI("test", "Simple retry counter: %d (expected 2)", id(simple_retry_counter)); ESP_LOGI("test", "Backoff retry counter: %d (expected 4)", id(backoff_retry_counter)); ESP_LOGI("test", "Immediate done counter: %d (expected 1)", id(immediate_done_counter)); - ESP_LOGI("test", "Cancel retry counter: %d (expected ~3-4)", id(cancel_retry_counter)); + ESP_LOGI("test", "Cancel retry counter: %d (expected 2-4)", id(cancel_retry_counter)); ESP_LOGI("test", "Empty name retry counter: %d (expected 1-2)", id(empty_name_retry_counter)); ESP_LOGI("test", "Component retry counter: %d (expected 2)", id(script_retry_counter)); ESP_LOGI("test", "Multiple same name counter: %d (expected 20+)", id(multiple_same_name_counter)); diff --git a/tests/integration/test_scheduler_retry_test.py b/tests/integration/test_scheduler_retry_test.py index 0c4d573c1b..1a469fcff1 100644 --- a/tests/integration/test_scheduler_retry_test.py +++ b/tests/integration/test_scheduler_retry_test.py @@ -148,16 +148,16 @@ async def test_scheduler_retry_test( f"Expected at least 2 intervals, got {len(backoff_intervals)}" ) if len(backoff_intervals) >= 3: - # First interval should be ~50ms - assert 30 <= backoff_intervals[0] <= 70, ( + # First interval should be ~50ms (very wide tolerance for heavy system load) + assert 20 <= backoff_intervals[0] <= 150, ( f"First interval {backoff_intervals[0]}ms not ~50ms" ) # Second interval should be ~100ms (50ms * 2.0) - assert 80 <= backoff_intervals[1] <= 120, ( + assert 50 <= backoff_intervals[1] <= 250, ( f"Second interval {backoff_intervals[1]}ms not ~100ms" ) # Third interval should be ~200ms (100ms * 2.0) - assert 180 <= backoff_intervals[2] <= 220, ( + assert 100 <= backoff_intervals[2] <= 500, ( f"Third interval {backoff_intervals[2]}ms not ~200ms" ) @@ -175,7 +175,7 @@ async def test_scheduler_retry_test( # Wait for cancel retry test try: - await asyncio.wait_for(cancel_retry_done.wait(), timeout=2.0) + await asyncio.wait_for(cancel_retry_done.wait(), timeout=3.0) except TimeoutError: pytest.fail( f"Cancel retry test did not complete. Count: {cancel_retry_count}" @@ -195,8 +195,8 @@ async def test_scheduler_retry_test( ) # Empty name retry should run at least once before being cancelled - assert 1 <= empty_name_retry_count <= 2, ( - f"Expected 1-2 empty name retry attempts, got {empty_name_retry_count}" + assert 1 <= empty_name_retry_count <= 3, ( + f"Expected 1-3 empty name retry attempts, got {empty_name_retry_count}" ) assert empty_cancel_result is True, ( "Empty name retry cancel should have succeeded" From f808c38f10f49b51d15e0e9010adf263a24bf9b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Jul 2025 08:15:54 -1000 Subject: [PATCH 125/125] [ruff] Enable PERF rules and fix all violations (#9874) --- esphome/__main__.py | 6 +- esphome/components/binary_sensor/__init__.py | 23 +++--- .../components/dfrobot_sen0395/__init__.py | 3 +- esphome/components/esp32/__init__.py | 2 +- .../components/esp32_ble_tracker/__init__.py | 4 +- esphome/components/esphome/ota/__init__.py | 3 +- esphome/components/lcd_gpio/display.py | 4 +- esphome/components/light/effects.py | 70 +++++++++---------- .../components/pipsolar/sensor/__init__.py | 2 +- esphome/core/config.py | 2 +- esphome/dashboard/web_server.py | 2 +- pyproject.toml | 13 ++-- script/clang-format | 7 +- script/clang-tidy | 5 +- script/helpers_zephyr.py | 16 +++-- tests/integration/test_duplicate_entities.py | 3 +- .../integration/test_host_mode_fan_preset.py | 4 +- tests/unit_tests/test_substitutions.py | 17 ++--- 18 files changed, 90 insertions(+), 96 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index cf0fed2d67..c9f5037890 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -89,9 +89,9 @@ def choose_prompt(options, purpose: str = None): def choose_upload_log_host( default, check_default, show_ota, show_mqtt, show_api, purpose: str = None ): - options = [] - for port in get_serial_ports(): - options.append((f"{port.path} ({port.description})", port.path)) + options = [ + (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() + ] if default == "SERIAL": return choose_prompt(options, purpose=purpose) if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c97de6d5e5..e3931e3946 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -266,8 +266,10 @@ async def delayed_off_filter_to_code(config, filter_id): async def autorepeat_filter_to_code(config, filter_id): timings = [] if len(config) > 0: - for conf in config: - timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])) + timings.extend( + (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]) + for conf in config + ) else: timings.append( ( @@ -573,16 +575,15 @@ async def setup_binary_sensor_core_(var, config): await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_MULTI_CLICK, []): - timings = [] - for tim in conf[CONF_TIMING]: - timings.append( - cg.StructInitializer( - MultiClickTriggerEvent, - ("state", tim[CONF_STATE]), - ("min_length", tim[CONF_MIN_LENGTH]), - ("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)), - ) + timings = [ + cg.StructInitializer( + MultiClickTriggerEvent, + ("state", tim[CONF_STATE]), + ("min_length", tim[CONF_MIN_LENGTH]), + ("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)), ) + for tim in conf[CONF_TIMING] + ] trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings) if CONF_INVALID_COOLDOWN in conf: cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index 8c9438dccb..d54b147036 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -74,8 +74,7 @@ def range_segment_list(input): if isinstance(input, list): for list_item in input: if isinstance(list_item, list): - for item in list_item: - flat_list.append(item) + flat_list.extend(list_item) else: flat_list.append(list_item) else: diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f0a5517185..2b4c4ff043 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -982,7 +982,7 @@ def copy_files(): __version__, ) - for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): + for file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].values(): if file[KEY_PATH].startswith("http"): import requests diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 046f3f679f..9daa6ee34e 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -310,9 +310,7 @@ async def to_code(config): for conf in config.get(CONF_ON_BLE_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: - addr_list = [] - for it in conf[CONF_MAC_ADDRESS]: - addr_list.append(it.as_hex) + addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]] cg.add(trigger.set_addresses(addr_list)) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 901657ec82..9facdc3bc6 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -73,8 +73,7 @@ def ota_esphome_final_validate(config): else: new_ota_conf.append(ota_conf) - for port_conf in merged_ota_esphome_configs_by_port.values(): - new_ota_conf.append(port_conf) + new_ota_conf.extend(merged_ota_esphome_configs_by_port.values()) full_conf[CONF_OTA] = new_ota_conf fv.full_config.set(full_conf) diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py index caa73194c9..0a77daf336 100644 --- a/esphome/components/lcd_gpio/display.py +++ b/esphome/components/lcd_gpio/display.py @@ -41,9 +41,7 @@ CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await lcd_base.setup_lcd_display(var, config) - pins_ = [] - for conf in config[CONF_DATA_PINS]: - pins_.append(await cg.gpio_pin_expression(conf)) + pins_ = [await cg.gpio_pin_expression(conf) for conf in config[CONF_DATA_PINS]] cg.add(var.set_data_pins(*pins_)) enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable)) diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 6f878ff5f1..f5749a17ab 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -291,31 +291,30 @@ async def random_effect_to_code(config, effect_id): ) async def strobe_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - colors = [] - for color in config.get(CONF_COLORS, []): - colors.append( - cg.StructInitializer( - StrobeLightEffectColor, - ( - "color", - LightColorValues( - color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), - color[CONF_STATE], - color[CONF_BRIGHTNESS], - color[CONF_COLOR_BRIGHTNESS], - color[CONF_RED], - color[CONF_GREEN], - color[CONF_BLUE], - color[CONF_WHITE], - color.get(CONF_COLOR_TEMPERATURE, 0.0), - color[CONF_COLD_WHITE], - color[CONF_WARM_WHITE], - ), + colors = [ + cg.StructInitializer( + StrobeLightEffectColor, + ( + "color", + LightColorValues( + color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), + color[CONF_STATE], + color[CONF_BRIGHTNESS], + color[CONF_COLOR_BRIGHTNESS], + color[CONF_RED], + color[CONF_GREEN], + color[CONF_BLUE], + color[CONF_WHITE], + color.get(CONF_COLOR_TEMPERATURE, 0.0), + color[CONF_COLD_WHITE], + color[CONF_WARM_WHITE], ), - ("duration", color[CONF_DURATION]), - ("transition_length", color[CONF_TRANSITION_LENGTH]), - ) + ), + ("duration", color[CONF_DURATION]), + ("transition_length", color[CONF_TRANSITION_LENGTH]), ) + for color in config.get(CONF_COLORS, []) + ] cg.add(var.set_colors(colors)) return var @@ -404,20 +403,19 @@ async def addressable_color_wipe_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL])) cg.add(var.set_reverse(config[CONF_REVERSE])) - colors = [] - for color in config.get(CONF_COLORS, []): - colors.append( - cg.StructInitializer( - AddressableColorWipeEffectColor, - ("r", int(round(color[CONF_RED] * 255))), - ("g", int(round(color[CONF_GREEN] * 255))), - ("b", int(round(color[CONF_BLUE] * 255))), - ("w", int(round(color[CONF_WHITE] * 255))), - ("random", color[CONF_RANDOM]), - ("num_leds", color[CONF_NUM_LEDS]), - ("gradient", color[CONF_GRADIENT]), - ) + colors = [ + cg.StructInitializer( + AddressableColorWipeEffectColor, + ("r", int(round(color[CONF_RED] * 255))), + ("g", int(round(color[CONF_GREEN] * 255))), + ("b", int(round(color[CONF_BLUE] * 255))), + ("w", int(round(color[CONF_WHITE] * 255))), + ("random", color[CONF_RANDOM]), + ("num_leds", color[CONF_NUM_LEDS]), + ("gradient", color[CONF_GRADIENT]), ) + for color in config.get(CONF_COLORS, []) + ] cg.add(var.set_colors(colors)) return var diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 0d00ba0083..929865b480 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -273,7 +273,7 @@ CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( async def to_code(config): paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) - for type, _ in TYPES.items(): + for type in TYPES: if type in config: conf = config[type] sens = await sensor.new_sensor(conf) diff --git a/esphome/core/config.py b/esphome/core/config.py index f73369f28f..6d93117164 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -317,7 +317,7 @@ def preload_core_config(config, result) -> str: target_platforms = [] - for domain, _ in config.items(): + for domain in config: if domain.startswith("."): continue if _is_target_platform(domain): diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 480285b6c1..286dc9e1d7 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -144,7 +144,7 @@ def websocket_class(cls): if not hasattr(cls, "_message_handlers"): cls._message_handlers = {} - for _, method in cls.__dict__.items(): + for method in cls.__dict__.values(): if hasattr(method, "_message_handler"): cls._message_handlers[method._message_handler] = method diff --git a/pyproject.toml b/pyproject.toml index 971b9fbf74..742cd6e83d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,12 +111,13 @@ exclude = ['generated'] [tool.ruff.lint] select = [ - "E", # pycodestyle - "F", # pyflakes/autoflake - "I", # isort - "PL", # pylint - "SIM", # flake8-simplify - "UP", # pyupgrade + "E", # pycodestyle + "F", # pyflakes/autoflake + "I", # isort + "PERF", # performance + "PL", # pylint + "SIM", # flake8-simplify + "UP", # pyupgrade ] ignore = [ diff --git a/script/clang-format b/script/clang-format index b1e84a56b7..d62a5b59c7 100755 --- a/script/clang-format +++ b/script/clang-format @@ -66,9 +66,10 @@ def main(): ) args = parser.parse_args() - files = [] - for path in git_ls_files(["*.cpp", "*.h", "*.tcc"]): - files.append(os.path.relpath(path, os.getcwd())) + cwd = os.getcwd() + files = [ + os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp", "*.h", "*.tcc"]) + ] if args.files: # Match against files specified on command-line diff --git a/script/clang-tidy b/script/clang-tidy index 187acd02ad..9576b8da8b 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -219,9 +219,8 @@ def main(): ) args = parser.parse_args() - files = [] - for path in git_ls_files(["*.cpp"]): - files.append(os.path.relpath(path, os.getcwd())) + cwd = os.getcwd() + files = [os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp"])] # Print initial file count if it's large if len(files) > 50: diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py index c3ba149005..305ca00c0c 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -41,11 +41,12 @@ CONFIG_NEWLIB_LIBC=y return include_paths def extract_defines(command): - defines = [] define_pattern = re.compile(r"-D\s*([^\s]+)") - for match in define_pattern.findall(command): - if match not in ("_ASMLANGUAGE"): - defines.append(match) + defines = [ + match + for match in define_pattern.findall(command) + if match not in ("_ASMLANGUAGE") + ] return defines def find_cxx_path(commands): @@ -78,13 +79,14 @@ CONFIG_NEWLIB_LIBC=y return include_paths def extract_cxx_flags(command): - flags = [] # Extracts CXXFLAGS from the command string, excluding includes and defines. flag_pattern = re.compile( r"(-O[0-3s]|-g|-std=[^\s]+|-Wall|-Wextra|-Werror|--[^\s]+|-f[^\s]+|-m[^\s]+|-imacros\s*[^\s]+)" ) - for match in flag_pattern.findall(command): - flags.append(match.replace("-imacros ", "-imacros")) + flags = [ + match.replace("-imacros ", "-imacros") + for match in flag_pattern.findall(command) + ] return flags def transform_to_idedata_format(compile_commands): diff --git a/tests/integration/test_duplicate_entities.py b/tests/integration/test_duplicate_entities.py index c738bb3fe0..9b2670c4ae 100644 --- a/tests/integration/test_duplicate_entities.py +++ b/tests/integration/test_duplicate_entities.py @@ -38,8 +38,7 @@ async def test_duplicate_entities_on_different_devices( # Get entity list entities = await client.list_entities_services() all_entities: list[EntityInfo] = [] - for entity_list in entities[0]: - all_entities.append(entity_list) + all_entities.extend(entities[0]) # Group entities by type for easier testing sensors = [e for e in all_entities if e.__class__.__name__ == "SensorInfo"] diff --git a/tests/integration/test_host_mode_fan_preset.py b/tests/integration/test_host_mode_fan_preset.py index d18b9f08ad..d44fdc0a3b 100644 --- a/tests/integration/test_host_mode_fan_preset.py +++ b/tests/integration/test_host_mode_fan_preset.py @@ -23,9 +23,7 @@ async def test_host_mode_fan_preset( entities = await client.list_entities_services() fans: list[FanInfo] = [] for entity_list in entities: - for entity in entity_list: - if isinstance(entity, FanInfo): - fans.append(entity) + fans.extend(entity for entity in entity_list if isinstance(entity, FanInfo)) # Create a map of fan names to entity info fan_map = {fan.name: fan for fan in fans} diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 3208923116..b65fecb26e 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -31,10 +31,8 @@ def dict_diff(a, b, path=""): if isinstance(a, dict) and isinstance(b, dict): a_keys = set(a) b_keys = set(b) - for key in a_keys - b_keys: - diffs.append(f"{path}/{key} only in actual") - for key in b_keys - a_keys: - diffs.append(f"{path}/{key} only in expected") + diffs.extend(f"{path}/{key} only in actual" for key in a_keys - b_keys) + diffs.extend(f"{path}/{key} only in expected" for key in b_keys - a_keys) for key in a_keys & b_keys: diffs.extend(dict_diff(a[key], b[key], f"{path}/{key}")) elif isinstance(a, list) and isinstance(b, list): @@ -42,11 +40,14 @@ def dict_diff(a, b, path=""): for i in range(min_len): diffs.extend(dict_diff(a[i], b[i], f"{path}[{i}]")) if len(a) > len(b): - for i in range(min_len, len(a)): - diffs.append(f"{path}[{i}] only in actual: {a[i]!r}") + diffs.extend( + f"{path}[{i}] only in actual: {a[i]!r}" for i in range(min_len, len(a)) + ) elif len(b) > len(a): - for i in range(min_len, len(b)): - diffs.append(f"{path}[{i}] only in expected: {b[i]!r}") + diffs.extend( + f"{path}[{i}] only in expected: {b[i]!r}" + for i in range(min_len, len(b)) + ) elif a != b: diffs.append(f"\t{path}: actual={a!r} expected={b!r}") return diffs